Merge tag '2024.10.1' into feature/2024.10

This commit is contained in:
dakkar 2024-11-08 15:52:37 +00:00
commit f079edaf3c
454 changed files with 9728 additions and 3363 deletions

View file

@ -43,7 +43,7 @@ async function main() {
const theme = localStorage.getItem('theme');
if (theme) {
for (const [k, v] of Object.entries(JSON.parse(theme))) {
document.documentElement.style.setProperty(`--${k}`, v.toString());
document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
// HTMLの theme-color 適用
if (k === 'htmlThemeColor') {

View file

@ -227,7 +227,7 @@ export async function openAccountMenu(opts: {
function showSigninDialog() {
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
done: res => {
done: (res: Misskey.entities.SigninFlowResponse & { finished: true }) => {
addAccount(res.id, res.i);
success();
},
@ -237,9 +237,9 @@ export async function openAccountMenu(opts: {
function createAccount() {
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
done: res => {
addAccount(res.id, res.i);
switchAccountWithToken(res.i);
done: (res: Misskey.entities.SignupResponse) => {
addAccount(res.id, res.token);
switchAccountWithToken(res.token);
},
closed: () => dispose(),
});

View file

@ -183,24 +183,18 @@ export async function common(createVue: () => App<Element>) {
if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON.parse(instance.defaultLightTheme));
if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON.parse(instance.defaultDarkTheme));
defaultStore.set('themeInitial', false);
} else {
if (defaultStore.state.darkMode) {
applyTheme(darkTheme.value);
} else {
applyTheme(lightTheme.value);
}
}
});
watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none');
document.documentElement.style.setProperty('--MI-modalBgFilter', v ? 'blur(4px)' : 'none');
}, { immediate: true });
watch(defaultStore.reactiveState.useBlurEffect, v => {
if (v) {
document.documentElement.style.removeProperty('--blur');
document.documentElement.style.removeProperty('--MI-blur');
} else {
document.documentElement.style.setProperty('--blur', 'none');
document.documentElement.style.setProperty('--MI-blur', 'none');
}
}, { immediate: true });

View file

@ -1,54 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { action } from '@storybook/addon-actions';
import { StoryObj } from '@storybook/vue3';
import { HttpResponse, http } from 'msw';
import { abuseUserReport } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js';
import MkAbuseReport from './MkAbuseReport.vue';
export const Default = {
render(args) {
return {
components: {
MkAbuseReport,
},
setup() {
return {
args,
};
},
computed: {
props() {
return {
...this.args,
};
},
events() {
return {
resolved: action('resolved'),
};
},
},
template: '<MkAbuseReport v-bind="props" v-on="events" />',
};
},
args: {
report: abuseUserReport(),
},
parameters: {
layout: 'fullscreen',
msw: {
handlers: [
...commonHandlers,
http.post('/api/admin/resolve-abuse-user-report', async ({ request }) => {
action('POST /api/admin/resolve-abuse-user-report')(await request.json());
return HttpResponse.json({});
}),
],
},
},
} satisfies StoryObj<typeof MkAbuseReport>;

View file

@ -4,141 +4,153 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div class="bcekxzvu _margin _panel">
<div class="target">
<MkA v-user-preview="report.targetUserId" class="info" :to="`/admin/user/${report.targetUserId}`" :behavior="'window'">
<MkAvatar class="avatar" :user="report.targetUser" indicator/>
<div class="names">
<MkUserName class="name" :user="report.targetUser"/>
<MkAcct class="acct" :user="report.targetUser" style="display: block;"/>
</div>
</MkA>
<div class="keyvalCtn">
<MkKeyValue>
<template #key>{{ i18n.ts.registeredDate }}</template>
<template #value>{{ dateString(report.targetUser.createdAt) }} (<MkTime :time="report.targetUser.createdAt"/>)</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.reporter }}</template>
<template #value><MkA :to="`/admin/user/${report.reporter.id}`" class="_link" :behavior="'window'">@{{ report.reporter.username }}</MkA></template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.createdAt }}</template>
<template #value><MkTime :time="report.createdAt" mode="absolute"/> (<MkTime :time="report.createdAt" mode="relative"/>)</template>
</MkKeyValue>
</div>
<hr>
<MkFolder>
<template #icon>
<i v-if="report.resolved && report.resolvedAs === 'accept'" class="ti ti-check" style="color: var(--MI_THEME-success)"></i>
<i v-else-if="report.resolved && report.resolvedAs === 'reject'" class="ti ti-x" style="color: var(--MI_THEME-error)"></i>
<i v-else-if="report.resolved" class="ti ti-slash"></i>
<i v-else class="ti ti-exclamation-circle" style="color: var(--MI_THEME-warn)"></i>
</template>
<template #label><MkAcct :user="report.targetUser"/> (by <MkAcct :user="report.reporter"/>)</template>
<template #caption>{{ report.comment }}</template>
<template #suffix><MkTime :time="report.createdAt"/></template>
<template #footer>
<div class="_buttons">
<template v-if="!report.resolved">
<MkButton @click="resolve('accept')"><i class="ti ti-check" style="color: var(--MI_THEME-success)"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts._abuseUserReport.accept }})</MkButton>
<MkButton @click="resolve('reject')"><i class="ti ti-x" style="color: var(--MI_THEME-error)"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts._abuseUserReport.reject }})</MkButton>
<MkButton @click="resolve(null)"><i class="ti ti-slash"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts.other }})</MkButton>
</template>
<template v-if="report.targetUser.host != null">
<MkButton :disabled="report.forwarded" primary @click="forward"><i class="ti ti-corner-up-right"></i> {{ i18n.ts._abuseUserReport.forward }}</MkButton>
<div v-tooltip:dialog="i18n.ts._abuseUserReport.forwardDescription" class="_button _help"><i class="ti ti-help-circle"></i></div>
</template>
<button class="_button" style="margin-left: auto; width: 34px;" @click="showMenu"><i class="ti ti-dots"></i></button>
</div>
<div class="detail">
<div>
</template>
<div :class="$style.root" class="_gaps_s">
<MkFolder :withSpacer="false">
<template #icon><MkAvatar :user="report.targetUser" style="width: 18px; height: 18px;"/></template>
<template #label>{{ i18n.ts.target }}: <MkAcct :user="report.targetUser"/></template>
<template #suffix>#{{ report.targetUserId.toUpperCase() }}</template>
<div style="container-type: inline-size;">
<RouterView :router="targetRouter"/>
</div>
</MkFolder>
<MkFolder :defaultOpen="true">
<template #icon><i class="ti ti-message-2"></i></template>
<template #label>{{ i18n.ts.details }}</template>
<div class="_gaps_s">
<Mfm :text="report.comment" :isBlock="true" :linkNavigationBehavior="'window'"/>
</div>
<hr/>
<div v-if="report.assignee" class="assignee">
{{ i18n.ts.moderator }}:
<MkA :to="`/admin/user/${report.assignee.id}`" class="_link" :behavior="'window'">@{{ report.assignee.username }}</MkA>
</MkFolder>
<MkFolder :withSpacer="false">
<template #icon><MkAvatar :user="report.reporter" style="width: 18px; height: 18px;"/></template>
<template #label>{{ i18n.ts.reporter }}: <MkAcct :user="report.reporter"/></template>
<template #suffix>#{{ report.reporterId.toUpperCase() }}</template>
<div style="container-type: inline-size;">
<RouterView :router="reporterRouter"/>
</div>
<div class="action">
<MkSwitch v-model="forward" c:disabled="report.targetUser.host == null || report.resolved">
{{ i18n.ts.forwardReport }}
<template #caption>{{ i18n.ts.forwardReportIsAnonymous }}</template>
</MkSwitch>
<MkButton v-if="!report.resolved" primary @click="resolve">{{ i18n.ts.abuseMarkAsResolved }}</MkButton>
</MkFolder>
<MkFolder :defaultOpen="false">
<template #icon><i class="ti ti-message-2"></i></template>
<template #label>{{ i18n.ts.moderationNote }}</template>
<template #suffix>{{ moderationNote.length > 0 ? '...' : i18n.ts.none }}</template>
<div class="_gaps_s">
<MkTextarea v-model="moderationNote" manualSave>
<template #caption>{{ i18n.ts.moderationNoteDescription }}</template>
</MkTextarea>
</div>
</MkFolder>
<div v-if="report.assignee">
{{ i18n.ts.moderator }}:
<MkAcct :user="report.assignee"/>
</div>
</div>
</MkFolder>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { provide, ref, watch } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { dateString } from '@/filters/date.js';
import MkFolder from '@/components/MkFolder.vue';
import RouterView from '@/components/global/RouterView.vue';
import { useRouterFactory } from '@/router/supplier';
import MkTextarea from '@/components/MkTextarea.vue';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
const props = defineProps<{
report: any;
report: Misskey.entities.AdminAbuseUserReportsResponse[number];
}>();
const emit = defineEmits<{
(ev: 'resolved', reportId: string): void;
}>();
const forward = ref(props.report.forwarded);
const routerFactory = useRouterFactory();
const targetRouter = routerFactory(`/admin/user/${props.report.targetUserId}`);
targetRouter.init();
const reporterRouter = routerFactory(`/admin/user/${props.report.reporterId}`);
reporterRouter.init();
function resolve() {
os.apiWithDialog('admin/resolve-abuse-user-report', {
forward: forward.value,
const moderationNote = ref(props.report.moderationNote ?? '');
watch(moderationNote, async () => {
os.apiWithDialog('admin/update-abuse-user-report', {
reportId: props.report.id,
moderationNote: moderationNote.value,
}).then(() => {
});
});
function resolve(resolvedAs) {
os.apiWithDialog('admin/resolve-abuse-user-report', {
reportId: props.report.id,
resolvedAs,
}).then(() => {
emit('resolved', props.report.id);
});
}
function forward() {
os.apiWithDialog('admin/forward-abuse-user-report', {
reportId: props.report.id,
}).then(() => {
});
}
function showMenu(ev: MouseEvent) {
os.popupMenu([{
icon: 'ti ti-id',
text: 'Copy ID',
action: () => {
copyToClipboard(props.report.id);
},
}, {
icon: 'ti ti-json',
text: 'Copy JSON',
action: () => {
copyToClipboard(JSON.stringify(props.report, null, '\t'));
},
}], ev.currentTarget ?? ev.target);
}
</script>
<style lang="scss" scoped>
.bcekxzvu {
display: flex;
flex-direction: column;
transition: .1s;
> .target {
box-sizing: border-box;
text-align: left;
padding: 24px 24px 0px 24px;
> .info {
display: flex;
box-sizing: border-box;
align-items: center;
padding: 14px;
border-radius: var(--radius-sm);
--c: rgb(255 196 0 / 15%);
background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
background-size: 16px 16px;
> .avatar {
width: 42px;
height: 42px;
}
> .names {
margin-left: 0.3em;
padding: 0 8px;
flex: 1;
white-space: pre;
overflow: hidden;
> .name {
font-weight: bold;
}
}
}
> .keyvalCtn {
display: inline-flex;
gap: 15px;
margin-top: 15px;
}
}
> .detail {
display: flex;
flex-direction: column;
padding: 0px 24px 24px 24px;
.assignee {
margin-bottom: 15px;
}
.action {
display: flex;
flex-direction: column;
gap: 15px;
}
}
<style lang="scss" module>
.root {
}
</style>

View file

@ -32,9 +32,9 @@ misskeyApi('users/show', { userId: props.movedTo }).then(u => user.value = u);
.root {
padding: 16px;
font-size: 90%;
background: var(--infoWarnBg);
color: var(--error);
border-radius: var(--radius);
background: var(--MI_THEME-infoWarnBg);
color: var(--MI_THEME-error);
border-radius: var(--MI-radius);
}
.link {

View file

@ -193,12 +193,12 @@ tick();
function calcColors() {
const computedStyle = getComputedStyle(document.documentElement);
const dark = tinycolor(computedStyle.getPropertyValue('--bg')).isDark();
const accent = tinycolor(computedStyle.getPropertyValue('--accent')).toHexString();
const dark = tinycolor(computedStyle.getPropertyValue('--MI_THEME-bg')).isDark();
const accent = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
majorGraduationColor.value = dark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)';
//minorGraduationColor = dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
sHandColor.value = dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)';
mHandColor.value = tinycolor(computedStyle.getPropertyValue('--fg')).toHexString();
mHandColor.value = tinycolor(computedStyle.getPropertyValue('--MI_THEME-fg')).toHexString();
hHandColor.value = accent;
nowColor.value = accent;
}

View file

@ -9,9 +9,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.header">
<span :class="$style.icon">
<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i>
<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i>
<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--MI_THEME-success);"></i>
</span>
<span :class="$style.title">{{ announcement.title }}</span>
</div>
@ -83,8 +83,8 @@ onMounted(() => {
min-width: 320px;
max-width: 480px;
box-sizing: border-box;
background: var(--panel);
border-radius: var(--radius);
background: var(--MI_THEME-panel);
border-radius: var(--MI-radius);
}
.header {

View file

@ -170,6 +170,6 @@ function addUser() {
.actions {
margin-top: 16px;
padding: 24px 0;
border-top: solid 0.5px var(--divider);
border-top: solid 0.5px var(--MI_THEME-divider);
}
</style>

View file

@ -106,7 +106,7 @@ const containerStyle = computed(() => {
const border = isBordered ? {
borderWidth: c.borderWidth ?? '1px',
borderColor: c.borderColor ?? 'var(--divider)',
borderColor: c.borderColor ?? 'var(--MI_THEME-divider)',
borderStyle: c.borderStyle ?? 'solid',
} : undefined;
@ -165,7 +165,7 @@ function openPostForm() {
}
.postForm {
background: var(--bg);
background: var(--MI_THEME-bg);
border-radius: var(--radius-sm);
}
</style>

View file

@ -407,16 +407,16 @@ onBeforeUnmount(() => {
text-overflow: ellipsis;
&:hover {
background: var(--X3);
background: var(--MI_THEME-X3);
}
&[data-selected='true'] {
background: var(--accent);
background: var(--MI_THEME-accent);
color: #fff !important;
}
&:active {
background: var(--accentDarken);
background: var(--MI_THEME-accentDarken);
color: #fff !important;
}
}

View file

@ -129,7 +129,7 @@ function onMousedown(evt: MouseEvent): void {
font-size: 95%;
box-shadow: none;
text-decoration: none;
background: var(--buttonBg);
background: var(--MI_THEME-buttonBg);
border-radius: var(--radius-xs);
overflow: clip;
box-sizing: border-box;
@ -140,11 +140,11 @@ function onMousedown(evt: MouseEvent): void {
}
&:not(:disabled):hover {
background: var(--buttonHoverBg);
background: var(--MI_THEME-buttonHoverBg);
}
&:not(:disabled):active {
background: var(--buttonHoverBg);
background: var(--MI_THEME-buttonHoverBg);
}
&.small {
@ -167,15 +167,15 @@ function onMousedown(evt: MouseEvent): void {
&.primary {
font-weight: bold;
color: var(--fgOnAccent) !important;
background: var(--accent);
color: var(--MI_THEME-fgOnAccent) !important;
background: var(--MI_THEME-accent);
&:not(:disabled):hover {
background: hsl(from var(--accent) h s calc(l + 5));
background: hsl(from var(--MI_THEME-accent) h s calc(l + 5));
}
&:not(:disabled):active {
background: hsl(from var(--accent) h s calc(l + 5));
background: hsl(from var(--MI_THEME-accent) h s calc(l + 5));
}
}
@ -216,15 +216,15 @@ function onMousedown(evt: MouseEvent): void {
&.gradate {
font-weight: bold;
color: var(--fgOnAccent) !important;
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
color: var(--MI_THEME-fgOnAccent) !important;
background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
&:not(:disabled):hover {
background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
}
&:not(:disabled):active {
background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
}
}

View file

@ -10,6 +10,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<div id="mcaptcha__widget-container" class="m-captcha-style"></div>
<div ref="captchaEl"></div>
</div>
<div v-if="props.provider == 'testcaptcha'" style="background: #eee; border: solid 1px #888; padding: 8px; color: #000; max-width: 320px; display: flex; gap: 10px; align-items: center; box-shadow: 2px 2px 6px #0004; border-radius: 4px;">
<img src="/client-assets/testcaptcha.png" style="width: 60px; height: 60px; "/>
<div v-if="testcaptchaPassed">
<div style="color: green;">Test captcha passed!</div>
</div>
<div v-else>
<div style="font-size: 13px; margin-bottom: 4px;">Type "ai-chan-kawaii" to pass captcha</div>
<input v-model="testcaptchaInput" data-cy-testcaptcha-input/>
<button type="button" data-cy-testcaptcha-submit @click="testcaptchaSubmit">Submit</button>
</div>
</div>
<div v-else ref="captchaEl"></div>
</div>
</template>
@ -32,7 +43,7 @@ export type Captcha = {
}): void;
};
export type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'turnstile' | 'mcaptcha' | 'fc';
export type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'turnstile' | 'mcaptcha' | 'fc' | 'testcaptcha';
type CaptchaContainer = {
readonly [_ in CaptchaProvider]?: Captcha;
@ -57,6 +68,9 @@ const available = ref(false);
const captchaEl = shallowRef<HTMLDivElement | undefined>();
const testcaptchaInput = ref('');
const testcaptchaPassed = ref(false);
const variable = computed(() => {
switch (props.provider) {
case 'hcaptcha': return 'hcaptcha';
@ -64,6 +78,7 @@ const variable = computed(() => {
case 'turnstile': return 'turnstile';
case 'mcaptcha': return 'mcaptcha';
case 'fc': return 'friendlyChallenge';
case 'testcaptcha': return 'testcaptcha';
}
});
@ -76,6 +91,7 @@ const src = computed(() => {
case 'turnstile': return 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';
case 'fc': return 'https://cdn.jsdelivr.net/npm/friendly-challenge@0.9.18/widget.min.js';
case 'mcaptcha': return null;
case 'testcaptcha': return null;
}
});
@ -83,7 +99,7 @@ const scriptId = computed(() => `script-${props.provider}`);
const captcha = computed<Captcha>(() => window[variable.value] || {} as unknown as Captcha);
if (loaded || props.provider === 'mcaptcha') {
if (loaded || props.provider === 'mcaptcha' || props.provider === 'testcaptcha') {
available.value = true;
} else if (src.value !== null) {
(document.getElementById(scriptId.value) ?? document.head.appendChild(Object.assign(document.createElement('script'), {
@ -96,6 +112,8 @@ if (loaded || props.provider === 'mcaptcha') {
function reset() {
if (captcha.value.reset) captcha.value.reset();
testcaptchaPassed.value = false;
testcaptchaInput.value = '';
}
async function requestRender() {
@ -140,6 +158,12 @@ function onReceivedMessage(message: MessageEvent) {
}
}
function testcaptchaSubmit() {
testcaptchaPassed.value = testcaptchaInput.value === 'ai-chan-kawaii';
callback(testcaptchaPassed.value ? 'testcaptcha-passed' : undefined);
if (!testcaptchaPassed.value) testcaptchaInput.value = '';
}
onMounted(() => {
if (available.value) {
window.addEventListener('message', onReceivedMessage);

View file

@ -68,9 +68,9 @@ async function onClick() {
position: relative;
display: inline-block;
font-weight: bold;
color: var(--accent);
color: var(--MI_THEME-accent);
background: transparent;
border: solid 1px var(--accent);
border: solid 1px var(--MI_THEME-accent);
padding: 0;
height: 31px;
font-size: 16px;
@ -99,17 +99,17 @@ async function onClick() {
}
&.active {
color: var(--fgOnAccent);
background: var(--accent);
color: var(--MI_THEME-fgOnAccent);
background: var(--MI_THEME-accent);
&:hover {
background: var(--accentLighten);
border-color: var(--accentLighten);
background: var(--MI_THEME-accentLighten);
border-color: var(--MI_THEME-accentLighten);
}
&:active {
background: var(--accentDarken);
border-color: var(--accentDarken);
background: var(--MI_THEME-accentDarken);
border-color: var(--MI_THEME-accentDarken);
}
}

View file

@ -100,7 +100,7 @@ const bannerStyle = computed(() => {
height: 100%;
border-radius: inherit;
pointer-events: none;
box-shadow: inset 0 0 0 2px var(--focus);
box-shadow: inset 0 0 0 2px var(--MI_THEME-focus);
}
}
@ -117,7 +117,7 @@ const bannerStyle = computed(() => {
left: 0;
width: 100%;
height: 64px;
background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0));
}
> .name {
@ -148,7 +148,7 @@ const bannerStyle = computed(() => {
bottom: 16px;
left: 16px;
background: rgba(0, 0, 0, 0.7);
color: var(--warn);
color: var(--MI_THEME-warn);
border-radius: var(--radius-sm);
font-weight: bold;
font-size: 1em;
@ -167,7 +167,7 @@ const bannerStyle = computed(() => {
> footer {
padding: 12px 16px;
border-top: solid 0.5px var(--divider);
border-top: solid 0.5px var(--MI_THEME-divider);
> span {
opacity: 0.7;
@ -213,8 +213,8 @@ const bannerStyle = computed(() => {
top: 0;
right: 0;
transform: translate(25%, -25%);
background-color: var(--accent);
border: solid var(--bg) 4px;
background-color: var(--MI_THEME-accent);
border: solid var(--MI_THEME-bg) 4px;
border-radius: var(--radius-full);
width: 1.5rem;
height: 1.5rem;

View file

@ -863,8 +863,8 @@ onMounted(() => {
left: 0;
width: 100%;
height: 100%;
-webkit-backdrop-filter: var(--blur, blur(12px));
backdrop-filter: var(--blur, blur(12px));
-webkit-backdrop-filter: var(--MI-blur, blur(12px));
backdrop-filter: var(--MI-blur, blur(12px));
display: flex;
justify-content: center;
align-items: center;

View file

@ -53,11 +53,11 @@ defineExpose({
> .item {
font-size: 85%;
padding: 4px 12px 4px 8px;
border: solid 1px var(--divider);
border: solid 1px var(--MI_THEME-divider);
border-radius: var(--radius-ellipse);
&:hover {
border-color: var(--inputBorderHover);
border-color: var(--MI_THEME-inputBorderHover);
}
&.disabled {

View file

@ -49,13 +49,13 @@ const remaining = computed(() => {
outline: none;
.root {
box-shadow: inset 0 0 0 2px var(--focus);
box-shadow: inset 0 0 0 2px var(--MI_THEME-focus);
}
}
&:hover {
text-decoration: none;
color: var(--accent);
color: var(--MI_THEME-accent);
}
}
@ -65,7 +65,7 @@ const remaining = computed(() => {
.divider {
height: 1px;
background: var(--divider);
background: var(--MI_THEME-divider);
}
.description {

View file

@ -96,7 +96,7 @@ watch(() => props.lang, (to) => {
margin: .5em 0;
overflow: auto;
border-radius: var(--radius-sm);
border: 1px solid var(--divider);
border: 1px solid var(--MI_THEME-divider);
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
color: var(--shiki-fallback);

View file

@ -71,7 +71,7 @@ function copy() {
.codeBlockFallbackRoot {
display: block;
overflow-wrap: anywhere;
background: var(--bg);
background: var(--MI_THEME-bg);
padding: 1em;
margin: .5em 0;
overflow: auto;
@ -94,8 +94,8 @@ function copy() {
border-radius: var(--radius-sm);
padding: 24px;
margin-top: 4px;
color: var(--fg);
background: var(--bg);
color: var(--MI_THEME-fg);
background: var(--MI_THEME-bg);
}
.codePlaceholderContainer {

View file

@ -140,7 +140,7 @@ watch(v, newValue => {
.caption {
font-size: 0.85em;
padding: 8px 0 0 0;
color: var(--fgTransparentWeak);
color: var(--MI_THEME-fgTransparentWeak);
&:empty {
display: none;
@ -160,17 +160,17 @@ watch(v, newValue => {
margin: 0;
border-radius: var(--radius-sm);
padding: 0;
color: var(--fg);
border: solid 1px var(--panel);
color: var(--MI_THEME-fg);
border: solid 1px var(--MI_THEME-panel);
transition: border-color 0.1s ease-out;
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
&:hover {
border-color: var(--inputBorderHover) !important;
border-color: var(--MI_THEME-inputBorderHover) !important;
}
}
.focused.codeEditorRoot {
border-color: var(--accent) !important;
border-color: var(--MI_THEME-accent) !important;
border-radius: var(--radius-sm);
}
@ -196,7 +196,7 @@ watch(v, newValue => {
resize: none;
text-align: left;
color: transparent;
caret-color: var(--fg);
caret-color: var(--MI_THEME-fg);
background-color: transparent;
border: 0;
border-radius: var(--radius-sm);
@ -213,6 +213,6 @@ watch(v, newValue => {
}
.textarea::selection {
color: var(--bg);
color: var(--MI_THEME-bg);
}
</style>

View file

@ -18,7 +18,7 @@ const props = defineProps<{
display: inline-block;
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
overflow-wrap: anywhere;
background: var(--bg);
background: var(--MI_THEME-bg);
padding: .1em;
border-radius: .3em;
}

View file

@ -60,7 +60,7 @@ const onInput = () => {
.caption {
font-size: 0.85em;
padding: 8px 0 0 0;
color: var(--fgTransparentWeak);
color: var(--MI_THEME-fgTransparentWeak);
&:empty {
display: none;
@ -72,8 +72,8 @@ const onInput = () => {
&.focused {
> .inputCore {
border-color: var(--accent) !important;
//box-shadow: 0 0 0 4px var(--focus);
border-color: var(--MI_THEME-accent) !important;
//box-shadow: 0 0 0 4px var(--MI_THEME-focus);
}
}
@ -98,9 +98,9 @@ const onInput = () => {
font: inherit;
font-weight: normal;
font-size: 1em;
color: var(--fg);
background: var(--panel);
border: solid 1px var(--panel);
color: var(--MI_THEME-fg);
background: var(--MI_THEME-panel);
border: solid 1px var(--MI_THEME-panel);
border-radius: var(--radius-sm);
outline: none;
box-shadow: none;
@ -108,7 +108,7 @@ const onInput = () => {
transition: border-color 0.1s ease-out;
&:hover {
border-color: var(--inputBorderHover) !important;
border-color: var(--MI_THEME-inputBorderHover) !important;
}
}
</style>

View file

@ -165,10 +165,11 @@ onUnmounted(() => {
.header {
position: sticky;
top: var(--stickyTop, 0px);
top: var(--MI-stickyTop, 0px);
left: 0;
color: var(--panelHeaderFg);
border-bottom: solid 0.5px var(--panelHeaderDivider);
color: var(--MI_THEME-panelHeaderFg);
background: var(--MI_THEME-panelHeaderBg);
border-bottom: solid 0.5px var(--MI_THEME-panelHeaderDivider);
z-index: 2;
line-height: 1.4em;
background: color-mix(in srgb, var(--panelHeaderBg) 35%, transparent);
@ -201,7 +202,7 @@ onUnmounted(() => {
}
.content {
--stickyTop: 0px;
--MI-stickyTop: 0px;
&.omitted {
position: relative;
@ -216,11 +217,11 @@ onUnmounted(() => {
left: 0;
width: 100%;
height: 64px;
background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0));
> .fadeLabel {
display: inline-block;
background: var(--panel);
background: var(--MI_THEME-panel);
padding: 6px 10px;
font-size: 0.8em;
border-radius: var(--radius-ellipse);
@ -229,7 +230,7 @@ onUnmounted(() => {
&:hover {
> .fadeLabel {
background: var(--panelHighlight);
background: var(--MI_THEME-panelHighlight);
}
}
}

View file

@ -125,7 +125,7 @@ onMounted(() => {
const computedStyle = getComputedStyle(document.documentElement);
const selection = cropper.getCropperSelection()!;
selection.themeColor = tinycolor(computedStyle.getPropertyValue('--accent')).toHexString();
selection.themeColor = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
selection.aspectRatio = props.aspectRatio;
selection.initialAspectRatio = props.aspectRatio;
selection.outlined = true;
@ -170,8 +170,8 @@ onMounted(() => {
display: flex;
align-items: center;
justify-content: center;
-webkit-backdrop-filter: var(--blur, blur(10px));
backdrop-filter: var(--blur, blur(10px));
-webkit-backdrop-filter: var(--MI-blur, blur(10px));
backdrop-filter: var(--MI-blur, blur(10px));
background: rgba(0, 0, 0, 0.5);
}

View file

@ -85,8 +85,8 @@ function cancel() {
.emojiImgWrapper {
max-width: 100%;
height: 40cqh;
background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--X5) 8px, var(--X5) 14px);
border-radius: var(--radius);
background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--MI_THEME-X5) 8px, var(--MI_THEME-X5) 14px);
border-radius: var(--MI-radius);
margin: auto;
overflow-y: hidden;
}
@ -101,8 +101,8 @@ function cancel() {
display: inline-block;
word-break: break-all;
padding: 3px 10px;
background-color: var(--X5);
border: solid 1px var(--divider);
border-radius: var(--radius);
background-color: var(--MI_THEME-X5);
border: solid 1px var(--MI_THEME-divider);
border-radius: var(--MI-radius);
}
</style>

View file

@ -9,6 +9,7 @@ import MkAd from '@/components/global/MkAd.vue';
import { isDebuggerEnabled, stackTraceInstances } from '@/debug.js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import { instance } from '@/instance.js';
import { defaultStore } from '@/store.js';
import { MisskeyEntity } from '@/types/date-separated-list.js';
@ -99,11 +100,13 @@ export default defineComponent({
return [el, separator];
} else {
if (props.ad && item._shouldInsertAd_) {
return [h(MkAd, {
if (props.ad && instance.ads.length > 0 && item._shouldInsertAd_) {
return [h('div', {
key: item.id + ':ad',
class: $style['ad-wrapper'],
}, [h(MkAd, {
prefer: ['horizontal', 'horizontal-big'],
}), el];
})]), el];
} else {
return el;
}
@ -182,7 +185,7 @@ export default defineComponent({
}
&:not(.date-separated-list-nogap) > *:not(:last-child) {
margin-bottom: var(--margin);
margin-bottom: var(--MI-margin);
}
}
@ -196,7 +199,7 @@ export default defineComponent({
box-shadow: none;
&:not(:last-child) {
border-bottom: solid 0.5px var(--divider);
border-bottom: solid 0.5px var(--MI_THEME-divider);
}
}
}
@ -237,7 +240,7 @@ export default defineComponent({
line-height: 32px;
text-align: center;
font-size: 12px;
color: var(--dateLabelFg);
color: var(--MI_THEME-dateLabelFg);
}
.date-1 {
@ -255,5 +258,11 @@ export default defineComponent({
.date-2-icon {
margin-left: 8px;
}
.ad-wrapper {
padding: 8px;
background-size: auto auto;
background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--MI_THEME-bg) 8px, var(--MI_THEME-bg) 14px);
}
</style>

View file

@ -185,7 +185,7 @@ function onInputKeydown(evt: KeyboardEvent) {
max-width: 480px;
box-sizing: border-box;
text-align: center;
background: var(--panel);
background: var(--MI_THEME-panel);
border-radius: var(--radius-md);
}
@ -207,15 +207,15 @@ function onInputKeydown(evt: KeyboardEvent) {
}
.type_success {
color: var(--success);
color: var(--MI_THEME-success);
}
.type_error {
color: var(--error);
color: var(--MI_THEME-error);
}
.type_warning {
color: var(--warn);
color: var(--MI_THEME-warn);
}
.title {

View file

@ -27,6 +27,6 @@ defineProps<{
<style scoped lang="scss">
.default {
border-top: solid 0.5px var(--divider);
border-top: solid 0.5px var(--MI_THEME-divider);
}
</style>

View file

@ -75,12 +75,12 @@ function neverShow() {
.root {
position: fixed;
z-index: v-bind(zIndex);
bottom: var(--margin);
bottom: var(--MI-margin);
left: 0;
right: 0;
margin: auto;
box-sizing: border-box;
width: calc(100% - (var(--margin) * 2));
width: calc(100% - (var(--MI-margin) * 2));
max-width: 500px;
display: flex;
backdrop-filter: var(--blur, blur(15px));
@ -90,7 +90,7 @@ function neverShow() {
text-align: center;
padding-top: 25px;
width: 100px;
color: var(--accent);
color: var(--MI_THEME-accent);
}
@media (max-width: 500px) {
.icon {

View file

@ -152,14 +152,14 @@ function onDragend() {
}
&.isSelected {
background: var(--accent);
background: var(--MI_THEME-accent);
&:hover {
background: var(--accentLighten);
background: var(--MI_THEME-accentLighten);
}
&:active {
background: var(--accentDarken);
background: var(--MI_THEME-accentDarken);
}
> .label {
@ -248,7 +248,7 @@ function onDragend() {
font-size: 0.8em;
text-align: center;
word-break: break-all;
color: var(--fg);
color: var(--MI_THEME-fg);
overflow: hidden;
}
</style>

View file

@ -36,13 +36,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, defineAsyncComponent, ref } from 'vue';
import * as Misskey from 'misskey-js';
import type { MenuItem } from '@/types/menu.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
import { claimAchievement } from '@/scripts/achievements.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import type { MenuItem } from '@/types/menu.js';
const props = withDefaults(defineProps<{
folder: Misskey.entities.DriveFolder;
@ -313,7 +313,7 @@ function onContextmenu(ev: MouseEvent) {
position: relative;
padding: 8px;
height: 64px;
background: var(--driveFolderBg);
background: var(--MI_THEME-driveFolderBg);
border-radius: var(--radius-xs);
cursor: pointer;
@ -326,7 +326,7 @@ function onContextmenu(ev: MouseEvent) {
right: -4px;
bottom: -4px;
left: -4px;
border: 2px dashed var(--focus);
border: 2px dashed var(--MI_THEME-focus);
border-radius: var(--radius-xs);
}
}
@ -345,13 +345,13 @@ function onContextmenu(ev: MouseEvent) {
width: 18px;
height: 18px;
background: #fff;
border: solid 2px var(--divider);
border: solid 2px var(--MI_THEME-divider);
border-radius: 4px;
box-sizing: border-box;
&.checked {
border-color: var(--accent);
background: var(--accent);
border-color: var(--MI_THEME-accent);
background: var(--MI_THEME-accent);
&::after {
content: "\ea5e";
@ -368,14 +368,13 @@ function onContextmenu(ev: MouseEvent) {
}
&:hover {
background: var(--accentedBg);
background: var(--MI_THEME-accentedBg);
}
}
.name {
margin: 0;
font-size: 0.9em;
color: var(--desktopDriveFolderFg);
}
.icon {
@ -388,6 +387,5 @@ function onContextmenu(ev: MouseEvent) {
margin: 4px 4px;
font-size: 0.8em;
text-align: right;
color: var(--desktopDriveFolderFg);
}
</style>

View file

@ -735,7 +735,7 @@ onBeforeUnmount(() => {
box-sizing: border-box;
overflow: auto;
font-size: 0.9em;
box-shadow: 0 1px 0 var(--divider);
box-shadow: 0 1px 0 var(--MI_THEME-divider);
user-select: none;
}
@ -787,7 +787,7 @@ onBeforeUnmount(() => {
.main {
flex: 1;
overflow: auto;
padding: var(--margin);
padding: var(--MI-margin);
user-select: none;
&.fetching {
@ -834,7 +834,7 @@ onBeforeUnmount(() => {
top: 38px;
width: 100%;
height: calc(100% - 38px);
border: dashed 2px var(--focus);
border: dashed 2px var(--MI_THEME-focus);
pointer-events: none;
}
</style>

View file

@ -69,7 +69,7 @@ const isThumbnailAvailable = computed(() => {
.root {
position: relative;
display: flex;
background: var(--panel);
background: var(--MI_THEME-panel);
border-radius: var(--radius-sm);
overflow: clip;
}
@ -83,7 +83,7 @@ const isThumbnailAvailable = computed(() => {
height: 100%;
pointer-events: none;
border-radius: inherit;
box-shadow: inset 0 0 0 4px var(--warn);
box-shadow: inset 0 0 0 4px var(--MI_THEME-warn);
}
.iconSub {

View file

@ -306,9 +306,9 @@ onUnmounted(() => {
.embedCodeGenPreviewRoot {
position: relative;
background-color: var(--bg);
background-color: var(--MI_THEME-bg);
background-size: auto auto;
background-image: repeating-linear-gradient(135deg, transparent, transparent 6px, var(--panel) 6px, var(--panel) 12px);
background-image: repeating-linear-gradient(135deg, transparent, transparent 6px, var(--MI_THEME-panel) 6px, var(--MI_THEME-panel) 12px);
cursor: not-allowed;
}
@ -381,8 +381,8 @@ onUnmounted(() => {
.embedCodeGenResultHeadingIcon {
margin: 0 auto;
background-color: var(--accentedBg);
color: var(--accent);
background-color: var(--MI_THEME-accentedBg);
color: var(--MI_THEME-accent);
text-align: center;
height: 64px;
width: 64px;

View file

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<!-- このコンポーネントの要素のclassは親から利用されるのでむやみに弄らないこと -->
<!-- フォルダの中にはカスタム絵文字だけUnicode絵文字もこっち -->
<section v-if="!hasChildSection" v-panel style="border-radius: var(--radius-sm); border-bottom: 0.5px solid var(--divider);">
<section v-if="!hasChildSection" v-panel style="border-radius: var(--radius-sm); border-bottom: 0.5px solid var(--MI_THEME-divider);">
<header class="_acrylic" @click="shown = !shown">
<i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ph-smiley-sticker ph-bold ph-lg"></i>:{{ emojis.length }})
</header>
@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</section>
<!-- フォルダの中にはカスタム絵文字やフォルダがある -->
<section v-else v-panel style="border-radius: var(--radius-sm); border-bottom: 0.5px solid var(--divider);">
<section v-else v-panel style="border-radius: var(--radius-sm); border-bottom: 0.5px solid var(--MI_THEME-divider);">
<header class="_acrylic" @click="shown = !shown">
<i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ti ti-folder ti-fw"></i>:{{ customEmojiTree?.length }} <i class="ph-smiley-sticker ph-bold ph-lg ti-fw"></i>:{{ emojis.length }})
</header>

View file

@ -580,7 +580,7 @@ defineExpose({
&:disabled {
cursor: not-allowed;
background: linear-gradient(-45deg, transparent 0% 48%, var(--X6) 48% 52%, transparent 52% 100%);
background: linear-gradient(-45deg, transparent 0% 48%, var(--MI_THEME-X6) 48% 52%, transparent 52% 100%);
opacity: 1;
> .emoji {
@ -615,7 +615,7 @@ defineExpose({
&:disabled {
cursor: not-allowed;
background: linear-gradient(-45deg, transparent 0% 48%, var(--X6) 48% 52%, transparent 52% 100%);
background: linear-gradient(-45deg, transparent 0% 48%, var(--MI_THEME-X6) 48% 52%, transparent 52% 100%);
opacity: 1;
> .emoji {
@ -638,7 +638,7 @@ defineExpose({
outline: none;
border: none;
background: transparent;
color: var(--fg);
color: var(--MI_THEME-fg);
&:not(:focus):not(.filled) {
margin-bottom: env(safe-area-inset-bottom, 0px);
@ -647,7 +647,7 @@ defineExpose({
&:not(.filled) {
order: 1;
z-index: 2;
box-shadow: 0px -1px 0 0px var(--divider);
box-shadow: 0px -1px 0 0px var(--MI_THEME-divider);
}
}
@ -658,11 +658,11 @@ defineExpose({
> .tab {
flex: 1;
height: 38px;
border-top: solid 0.5px var(--divider);
border-top: solid 0.5px var(--MI_THEME-divider);
&.active {
border-top: solid 1px var(--accent);
color: var(--accent);
border-top: solid 1px var(--MI_THEME-accent);
color: var(--MI_THEME-accent);
}
}
}
@ -681,7 +681,7 @@ defineExpose({
> .group {
&:not(.index) {
padding: 4px 0 8px 0;
border-top: solid 0.5px var(--divider);
border-top: solid 0.5px var(--MI_THEME-divider);
}
> header {
@ -708,7 +708,7 @@ defineExpose({
cursor: pointer;
&:hover {
color: var(--accent);
color: var(--MI_THEME-accent);
}
}
@ -730,13 +730,13 @@ defineExpose({
}
&:active {
background: var(--accent);
background: var(--MI_THEME-accent);
box-shadow: inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15);
}
&:disabled {
cursor: not-allowed;
background: linear-gradient(-45deg, transparent 0% 48%, var(--X6) 48% 52%, transparent 52% 100%);
background: linear-gradient(-45deg, transparent 0% 48%, var(--MI_THEME-X6) 48% 52%, transparent 52% 100%);
opacity: 1;
> .emoji {
@ -757,7 +757,7 @@ defineExpose({
}
&.result {
border-bottom: solid 0.5px var(--divider);
border-bottom: solid 0.5px var(--MI_THEME-divider);
&:empty {
display: none;

View file

@ -0,0 +1,83 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { StoryObj } from '@storybook/vue3';
import MkExtensionInstaller from './MkExtensionInstaller.vue';
import lightTheme from '@@/themes/_light.json5';
export const Plugin = {
render(args) {
return {
components: {
MkExtensionInstaller,
},
setup() {
return {
args,
};
},
computed: {
props() {
return {
...this.args,
};
},
},
template: '<MkExtensionInstaller v-bind="props" />',
};
},
args: {
extension: {
type: 'plugin',
raw: '"do nothing"',
meta: {
name: 'do nothing plugin',
version: '1.0',
author: 'syuilo and misskey-project',
description: 'a plugin that does nothing',
permissions: ['read:account'],
config: {
'doNothing': true,
},
},
},
},
parameters: {
layout: 'centered',
},
} satisfies StoryObj<typeof MkExtensionInstaller>;
export const Theme = {
render(args) {
return {
components: {
MkExtensionInstaller,
},
setup() {
return {
args,
};
},
computed: {
props() {
return {
...this.args,
};
},
},
template: '<MkExtensionInstaller v-bind="props" />',
};
},
args: {
extension: {
type: 'theme',
raw: JSON.stringify(lightTheme),
meta: lightTheme,
},
},
parameters: {
layout: 'centered',
},
} satisfies StoryObj<typeof MkExtensionInstaller>;

View file

@ -0,0 +1,146 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div class="_gaps_m" :class="$style.extInstallerRoot">
<div :class="$style.extInstallerIconWrapper">
<i v-if="isPlugin" class="ti ti-plug"></i>
<i v-else-if="isTheme" class="ti ti-palette"></i>
<!-- 拡張用 -->
<i v-else class="ti ti-download"></i>
</div>
<h2 :class="$style.extInstallerTitle">{{ i18n.ts._externalResourceInstaller[`_${extension.type}`].title }}</h2>
<div :class="$style.extInstallerNormDesc">{{ i18n.ts._externalResourceInstaller.checkVendorBeforeInstall }}</div>
<MkInfo v-if="isPlugin" :warn="true">{{ i18n.ts._plugin.installWarn }}</MkInfo>
<FormSection>
<template #label>{{ i18n.ts._externalResourceInstaller[`_${extension.type}`].metaTitle }}</template>
<div class="_gaps_s">
<FormSplit>
<MkKeyValue>
<template #key>{{ i18n.ts.name }}</template>
<template #value>{{ extension.meta.name }}</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.author }}</template>
<template #value>{{ extension.meta.author }}</template>
</MkKeyValue>
</FormSplit>
<MkKeyValue v-if="isPlugin">
<template #key>{{ i18n.ts.description }}</template>
<template #value>{{ extension.meta.description ?? i18n.ts.none }}</template>
</MkKeyValue>
<MkKeyValue v-if="isPlugin">
<template #key>{{ i18n.ts.version }}</template>
<template #value>{{ extension.meta.version }}</template>
</MkKeyValue>
<MkKeyValue v-if="isPlugin">
<template #key>{{ i18n.ts.permission }}</template>
<template #value>
<ul v-if="extension.meta.permissions && extension.meta.permissions.length > 0" :class="$style.extInstallerKVList">
<li v-for="permission in extension.meta.permissions" :key="permission">{{ i18n.ts._permissions[permission] }}</li>
</ul>
<template v-else>{{ i18n.ts.none }}</template>
</template>
</MkKeyValue>
<MkKeyValue v-if="isTheme">
<template #key>{{ i18n.ts._externalResourceInstaller._meta.base }}</template>
<template #value>{{ i18n.ts[extension.meta.base ?? 'none'] }}</template>
</MkKeyValue>
<MkFolder>
<template #icon><i class="ti ti-code"></i></template>
<template #label>{{ i18n.ts._plugin.viewSource }}</template>
<MkCode :code="extension.raw"/>
</MkFolder>
</div>
</FormSection>
<slot name="additionalInfo"/>
<div class="_buttonsCenter">
<MkButton primary @click="emits('confirm')"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton>
</div>
</div>
</template>
<script lang="ts">
export type Extension = {
type: 'plugin';
raw: string;
meta: {
name: string;
version: string;
author: string;
description?: string;
permissions?: string[];
config?: Record<string, any>;
};
} | {
type: 'theme';
raw: string;
meta: {
name: string;
author: string;
base?: 'light' | 'dark';
};
};
</script>
<script lang="ts" setup>
import { computed } from 'vue';
import MkButton from '@/components/MkButton.vue';
import FormSection from '@/components/form/section.vue';
import FormSplit from '@/components/form/split.vue';
import MkCode from '@/components/MkCode.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import { i18n } from '@/i18n.js';
const isPlugin = computed(() => props.extension.type === 'plugin');
const isTheme = computed(() => props.extension.type === 'theme');
const props = defineProps<{
extension: Extension;
}>();
const emits = defineEmits<{
(ev: 'confirm'): void;
}>();
</script>
<style lang="scss" module>
.extInstallerRoot {
border-radius: var(--MI-radius);
background: var(--MI_THEME-panel);
padding: 1.5rem;
}
.extInstallerIconWrapper {
width: 48px;
height: 48px;
font-size: 24px;
line-height: 48px;
text-align: center;
border-radius: 50%;
margin-left: auto;
margin-right: auto;
background-color: var(--MI_THEME-accentedBg);
color: var(--MI_THEME-accent);
}
.extInstallerTitle {
font-size: 1.2rem;
text-align: center;
margin: 0;
}
.extInstallerNormDesc {
text-align: center;
}
.extInstallerKVList {
margin-top: 0;
margin-bottom: 0;
}
</style>

View file

@ -66,7 +66,7 @@ const props = defineProps<{
align-items: center;
&:hover {
color: var(--accent);
color: var(--MI_THEME-accent);
}
> .thumbnail {

View file

@ -36,7 +36,7 @@ const props = defineProps<{
&:hover {
text-decoration: none;
color: var(--accent);
color: var(--MI_THEME-accent);
}
&:focus-visible {
@ -83,7 +83,6 @@ const props = defineProps<{
> p {
display: inline-block;
margin: 0;
color: var(--urlPreviewInfo);
font-size: 0.8em;
line-height: 16px;
vertical-align: top;
@ -92,7 +91,7 @@ const props = defineProps<{
}
&:global(.gray) {
--c: var(--bg);
--c: var(--MI_THEME-bg);
background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
background-size: 16px 16px;
}

View file

@ -83,7 +83,7 @@ function afterLeave(element: Element) {
onMounted(() => {
function getParentBg(el?: HTMLElement | null): string {
if (el == null || el.tagName === 'BODY') return 'var(--bg)';
if (el == null || el.tagName === 'BODY') return 'var(--MI_THEME-bg)';
const background = el.style.background || el.style.backgroundColor;
if (background) {
return background;
@ -118,9 +118,9 @@ onMounted(() => {
position: relative;
z-index: 10;
position: sticky;
top: var(--stickyTop, 0px);
-webkit-backdrop-filter: var(--blur, blur(8px));
backdrop-filter: var(--blur, blur(20px));
top: var(--MI-stickyTop, 0px);
-webkit-backdrop-filter: var(--MI-blur, blur(8px));
backdrop-filter: var(--MI-blur, blur(20px));
}
.title {
@ -134,7 +134,7 @@ onMounted(() => {
flex: 1;
margin: auto;
height: 1px;
background: var(--divider);
background: var(--MI_THEME-divider);
}
.button {

View file

@ -38,9 +38,12 @@ SPDX-License-Identifier: AGPL-3.0-only
>
<KeepAlive>
<div v-show="opened">
<MkSpacer :marginMin="14" :marginMax="22">
<MkSpacer v-if="withSpacer" :marginMin="14" :marginMax="22">
<slot></slot>
</MkSpacer>
<div v-else>
<slot></slot>
</div>
<div v-if="$slots.footer" :class="$style.footer">
<slot name="footer"></slot>
</div>
@ -59,9 +62,11 @@ import { defaultStore } from '@/store.js';
const props = withDefaults(defineProps<{
defaultOpen?: boolean;
maxHeight?: number | null;
withSpacer?: boolean;
}>(), {
defaultOpen: false,
maxHeight: null,
withSpacer: true,
});
const getBgColor = (el: HTMLElement) => {
@ -113,7 +118,7 @@ function toggle() {
onMounted(() => {
const computedStyle = getComputedStyle(document.documentElement);
const parentBg = getBgColor(rootEl.value!.parentElement!);
const myBg = computedStyle.getPropertyValue('--panel');
const myBg = computedStyle.getPropertyValue('--MI_THEME-panel');
bgSame.value = parentBg === myBg;
});
</script>
@ -139,15 +144,15 @@ onMounted(() => {
width: 100%;
box-sizing: border-box;
padding: 9px 12px 9px 12px;
background: var(--folderHeaderBg);
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
background: var(--MI_THEME-folderHeaderBg);
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
backdrop-filter: var(--MI-blur, blur(15px));
border-radius: var(--radius-sm);
transition: border-radius 0.3s;
&:hover {
text-decoration: none;
background: var(--folderHeaderHoverBg);
background: var(--MI_THEME-folderHeaderHoverBg);
}
&:focus-within {
@ -155,8 +160,8 @@ onMounted(() => {
}
&.active {
color: var(--accent);
background: var(--folderHeaderHoverBg);
color: var(--MI_THEME-accent);
background: var(--MI_THEME-folderHeaderHoverBg);
}
&.opened {
@ -170,7 +175,7 @@ onMounted(() => {
}
.headerLower {
color: var(--fgTransparentWeak);
color: var(--MI_THEME-fgTransparentWeak);
font-size: .85em;
padding-left: 4px;
}
@ -204,13 +209,13 @@ onMounted(() => {
}
.headerTextSub {
color: var(--fgTransparentWeak);
color: var(--MI_THEME-fgTransparentWeak);
font-size: .85em;
}
.headerRight {
margin-left: auto;
color: var(--fgTransparentWeak);
color: var(--MI_THEME-fgTransparentWeak);
white-space: nowrap;
}
@ -219,26 +224,26 @@ onMounted(() => {
}
.body {
background: var(--panel);
background: var(--MI_THEME-panel);
border-radius: 0 0 var(--radius-sm) var(--radius-sm);
container-type: inline-size;
&.bgSame {
background: var(--bg);
background: var(--MI_THEME-bg);
}
}
.footer {
position: sticky !important;
z-index: 1;
bottom: var(--stickyBottom, 0px);
bottom: var(--MI-stickyBottom, 0px);
left: 0;
padding: 12px;
background: var(--acrylicBg);
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
background: var(--MI_THEME-acrylicBg);
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
backdrop-filter: var(--MI-blur, blur(15px));
background-size: auto auto;
background-image: repeating-linear-gradient(135deg, transparent, transparent 5px, var(--panel) 5px, var(--panel) 10px);
background-image: repeating-linear-gradient(135deg, transparent, transparent 5px, var(--MI_THEME-panel) 5px, var(--MI_THEME-panel) 10px);
border-radius: 0 0 6px 6px;
}
</style>

View file

@ -165,8 +165,8 @@ onBeforeUnmount(() => {
position: relative;
display: inline-block;
font-weight: bold;
color: var(--fgOnWhite);
border: solid 1px var(--accent);
color: var(--MI_THEME-fgOnWhite);
border: solid 1px var(--MI_THEME-accent);
padding: 0;
height: 31px;
font-size: 16px;
@ -201,17 +201,17 @@ onBeforeUnmount(() => {
}
&.active {
color: var(--fgOnAccent);
background: var(--accent);
color: var(--MI_THEME-fgOnAccent);
background: var(--MI_THEME-accent);
&:hover {
background: var(--accentLighten);
border-color: var(--accentLighten);
background: var(--MI_THEME-accentLighten);
border-color: var(--MI_THEME-accentLighten);
}
&:active {
background: var(--accentDarken);
border-color: var(--accentDarken);
background: var(--MI_THEME-accentDarken);
border-color: var(--MI_THEME-accentDarken);
}
}

View file

@ -66,6 +66,6 @@ function selectButton(ev: MouseEvent) {
<style module>
.fileNotSelected {
font-weight: 700;
color: var(--infoWarnFg);
color: var(--MI_THEME-infoWarnFg);
}
</style>

View file

@ -36,7 +36,7 @@ const props = defineProps<{
}
.text {
color: var(--warn);
color: var(--MI_THEME-warn);
font-size: 90%;
animation: modified-blink 2s infinite;
}

View file

@ -0,0 +1,100 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div
:class="[
$style.root,
tail === 'left' ? $style.left : $style.right,
negativeMargin === true && $style.negativeMargin,
shadow === true && $style.shadow,
]"
>
<div :class="$style.bg">
<svg v-if="tail !== 'none'" :class="$style.tail" version="1.1" viewBox="0 0 14.597 14.58" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-173.71 -87.184)">
<path d="m188.19 87.657c-1.469 2.3218-3.9315 3.8312-6.667 4.0865-2.2309-1.7379-4.9781-2.6816-7.8061-2.6815h-5.1e-4v12.702h12.702v-5.1e-4c2e-5 -1.9998-0.47213-3.9713-1.378-5.754 2.0709-1.6834 3.2732-4.2102 3.273-6.8791-6e-5 -0.49375-0.0413-0.98662-0.1235-1.4735z" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round" stroke-width=".33225" style="paint-order:stroke fill markers"/>
</g>
</svg>
<div :class="$style.content">
<slot></slot>
</div>
</div>
</div>
</template>
<script setup lang="ts">
withDefaults(defineProps<{
tail?: 'left' | 'right' | 'none';
negativeMargin?: boolean;
shadow?: boolean;
}>(), {
tail: 'right',
negativeMargin: false,
shadow: false,
});
</script>
<style module lang="scss">
.root {
--fukidashi-radius: var(--MI-radius);
--fukidashi-bg: var(--MI_THEME-panel);
position: relative;
display: inline-block;
min-height: calc(var(--fukidashi-radius) * 2);
padding-top: calc(var(--fukidashi-radius) * .13);
&.shadow {
filter: drop-shadow(0 4px 32px var(--MI_THEME-shadow));
}
&.left {
padding-left: calc(var(--fukidashi-radius) * .13);
&.negativeMargin {
margin-left: calc(calc(var(--fukidashi-radius) * .13) * -1);
}
}
&.right {
padding-right: calc(var(--fukidashi-radius) * .13);
&.negativeMargin {
margin-right: calc(calc(var(--fukidashi-radius) * .13) * -1);
}
}
}
.bg {
width: 100%;
height: 100%;
background: var(--fukidashi-bg);
border-radius: var(--fukidashi-radius);
}
.content {
position: relative;
padding: 8px 12px;
}
.tail {
position: absolute;
top: 0;
display: block;
width: calc(var(--fukidashi-radius) * 1.13);
height: auto;
fill: var(--fukidashi-bg);
}
.left .tail {
left: 0;
transform: rotateY(180deg);
}
.right .tail {
right: 0;
}
</style>

View file

@ -75,7 +75,7 @@ function leaveHover(): void {
&:hover {
text-decoration: none;
color: var(--accent);
color: var(--MI_THEME-accent);
> .thumbnail {
transform: scale(1.1);

View file

@ -41,7 +41,7 @@ const search = () => {
width: 100%;
height: 40px;
font-size: 16px;
border: solid 1px var(--divider);
border: solid 1px var(--MI_THEME-divider);
border-radius: var(--radius-xs) 0 0 var(--radius-xs);
-webkit-appearance: textfield;
}
@ -50,7 +50,7 @@ const search = () => {
flex-shrink: 0;
margin: 0;
padding: 0 16px;
border: solid 1px var(--divider);
border: solid 1px var(--MI_THEME-divider);
border-left: none;
border-radius: 0 var(--radius-xs) var(--radius-xs) 0;

View file

@ -36,15 +36,15 @@ function close() {
align-items: center;
padding: 12px 14px;
font-size: 90%;
background: color-mix(in srgb, var(--infoBg) 65%, transparent);
color: var(--infoFg);
border-radius: var(--radius);
background: color-mix(in srgb, var(--MI_THEME-infoBg) 65%, transparent);
color: var(--MI_THEME-infoFg);
border-radius: var(--MI-radius);
white-space: pre-wrap;
z-index: 1;
&.warn {
background: color-mix(in srgb, var(--infoWarnBg) 65%, transparent);
color: var(--infoWarnFg);
background: color-mix(in srgb, var(--MI_THEME-infoWarnBg) 65%, transparent);
color: var(--MI_THEME-infoWarnFg);
}
}

View file

@ -199,7 +199,7 @@ defineExpose({
.caption {
font-size: 0.85em;
padding: 8px 0 0 0;
color: var(--fgTransparentWeak);
color: var(--MI_THEME-fgTransparentWeak);
&:empty {
display: none;
@ -216,8 +216,8 @@ defineExpose({
&.focused {
> .inputCore {
border-color: var(--accent) !important;
//box-shadow: 0 0 0 4px var(--focus);
border-color: var(--MI_THEME-accent) !important;
//box-shadow: 0 0 0 4px var(--MI_THEME-focus);
}
}
@ -242,9 +242,9 @@ defineExpose({
font: inherit;
font-weight: normal;
font-size: 1em;
color: var(--fg);
background: var(--panel);
border: solid 1px var(--panel);
color: var(--MI_THEME-fg);
background: var(--MI_THEME-panel);
border: solid 1px var(--MI_THEME-panel);
border-radius: var(--radius-sm);
outline: none;
box-shadow: none;
@ -252,7 +252,7 @@ defineExpose({
transition: border-color 0.1s ease-out;
&:hover {
border-color: var(--inputBorderHover) !important;
border-color: var(--MI_THEME-inputBorderHover) !important;
}
}

View file

@ -46,7 +46,7 @@ function getInstanceIcon(instance): string {
display: flex;
align-items: center;
padding: 16px;
background: var(--panel);
background: var(--MI_THEME-panel);
border-radius: var(--radius-sm);
> :global(.icon) {
@ -62,7 +62,7 @@ function getInstanceIcon(instance): string {
flex: 1;
overflow: hidden;
font-size: 0.9em;
color: var(--fg);
color: var(--MI_THEME-fg);
padding-right: 8px;
> :global(.host) {
@ -109,7 +109,7 @@ function getInstanceIcon(instance): string {
}
&:global(.gray) {
--c: var(--bg);
--c: var(--MI_THEME-bg);
background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
background-size: 16px 16px;
}

View file

@ -121,7 +121,7 @@ function createDoughnut(chartEl, tooltip, data) {
labels: data.map(x => x.name),
datasets: [{
backgroundColor: data.map(x => x.color),
borderColor: getComputedStyle(document.documentElement).getPropertyValue('--panel'),
borderColor: getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-panel'),
borderWidth: 2,
hoverOffset: 0,
data: data.map(x => x.value),
@ -256,8 +256,8 @@ onMounted(() => {
flex: 1;
min-width: 0;
position: relative;
background: var(--panel);
border-radius: var(--radius);
background: var(--MI_THEME-panel);
border-radius: var(--MI-radius);
padding: 24px;
max-height: 300px;

View file

@ -8,8 +8,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ invite.code }}</template>
<template #suffix>
<span v-if="invite.used">{{ i18n.ts.used }}</span>
<span v-else-if="isExpired" style="color: var(--error)">{{ i18n.ts.expired }}</span>
<span v-else style="color: var(--success)">{{ i18n.ts.unused }}</span>
<span v-else-if="isExpired" style="color: var(--MI_THEME-error)">{{ i18n.ts.expired }}</span>
<span v-else style="color: var(--MI_THEME-success)">{{ i18n.ts.unused }}</span>
</template>
<template #footer>
<div class="_buttons">

View file

@ -12,13 +12,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<i class="icon" :class="item.icon"></i>
<div class="text">{{ item.text }}</div>
<span v-if="item.indicate && item.indicateValue" class="_indicateCounter indicatorWithValue">{{ item.indicateValue }}</span>
<span v-else-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span>
<span v-else-if="item.indicate" class="indicator _blink"><i class="_indicatorCircle"></i></span>
</button>
<MkA v-else v-click-anime :to="item.to" class="item" @click.passive="close()">
<i class="icon" :class="item.icon"></i>
<div class="text">{{ item.text }}</div>
<span v-if="item.indicate && item.indicateValue" class="_indicateCounter indicatorWithValue">{{ item.indicateValue }}</span>
<span v-else-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span>
<span v-else-if="item.indicate" class="indicator _blink"><i class="_indicatorCircle"></i></span>
</MkA>
</template>
</div>
@ -105,8 +105,8 @@ function close() {
box-sizing: border-box;
&:hover {
color: var(--accent);
background: var(--accentedBg);
color: var(--MI_THEME-accent);
background: var(--MI_THEME-accentedBg);
text-decoration: none;
}
@ -137,9 +137,8 @@ function close() {
position: absolute;
top: 32px;
left: 32px;
color: var(--indicator);
color: var(--MI_THEME-indicator);
font-size: 8px;
animation: global-blink 1s infinite;
@media (max-width: 500px) {
top: 16px;

View file

@ -394,8 +394,8 @@ onDeactivated(() => {
.audioContainer {
container-type: inline-size;
position: relative;
border: .5px solid var(--divider);
border-radius: var(--radius);
border: .5px solid var(--MI_THEME-divider);
border-radius: var(--MI-radius);
overflow: clip;
&:focus-visible {
@ -415,7 +415,7 @@ onDeactivated(() => {
height: 100%;
pointer-events: none;
border-radius: inherit;
box-shadow: inset 0 0 0 4px var(--warn);
box-shadow: inset 0 0 0 4px var(--MI_THEME-warn);
}
}
@ -457,12 +457,12 @@ onDeactivated(() => {
.controlButton {
padding: 6px;
border-radius: calc(var(--radius) / 2);
border-radius: calc(var(--MI-radius) / 2);
font-size: 1.05rem;
&:hover {
color: var(--accent);
background-color: var(--accentedBg);
color: var(--MI_THEME-accent);
background-color: var(--MI_THEME-accentedBg);
}
&:focus-visible {

View file

@ -68,7 +68,6 @@ async function show() {
}
.download {
background: var(--noteAttachedFile);
}
.sensitive {

View file

@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.indicators">
<div v-if="['image/gif', 'image/apng'].includes(image.type)" :class="$style.indicator">GIF</div>
<div v-if="image.comment" :class="$style.indicator">ALT</div>
<div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
<div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--MI_THEME-warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
<div v-if="!image.comment" :class="$style.indicator" title="Image lacks descriptive text"><i class="ph-pencil-simple ph-bold ph-lg-off"></i></div>
</div>
<button :class="$style.menu" class="_button" @click.stop="showMenu"><i class="ti ti-dots" style="vertical-align: middle;"></i></button>
@ -166,7 +166,7 @@ function showMenu(ev: MouseEvent) {
height: 100%;
pointer-events: none;
border-radius: inherit;
box-shadow: inset 0 0 0 4px var(--warn);
box-shadow: inset 0 0 0 4px var(--MI_THEME-warn);
}
}
@ -188,7 +188,7 @@ function showMenu(ev: MouseEvent) {
position: absolute;
border-radius: var(--radius-sm);
background-color: black;
color: var(--accentLighten);
color: var(--MI_THEME-accentLighten);
font-size: 12px;
opacity: .5;
padding: 5px 8px;
@ -207,19 +207,19 @@ function showMenu(ev: MouseEvent) {
.visible {
position: relative;
//box-shadow: 0 0 0 1px var(--divider) inset;
background: var(--bg);
//box-shadow: 0 0 0 1px var(--MI_THEME-divider) inset;
background: var(--MI_THEME-bg);
background-size: 16px 16px;
}
html[data-color-scheme=dark] .visible {
--c: rgb(255 255 255 / 2%);
background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%);
background-image: linear-gradient(45deg, var(--c) 16.67%, var(--MI_THEME-bg) 16.67%, var(--MI_THEME-bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--MI_THEME-bg) 66.67%, var(--MI_THEME-bg) 100%);
}
html[data-color-scheme=light] .visible {
--c: rgb(0 0 0 / 2%);
background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%);
background-image: linear-gradient(45deg, var(--c) 16.67%, var(--MI_THEME-bg) 16.67%, var(--MI_THEME-bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--MI_THEME-bg) 66.67%, var(--MI_THEME-bg) 100%);
}
.menu {
@ -227,8 +227,8 @@ html[data-color-scheme=light] .visible {
position: absolute;
border-radius: var(--radius-ellipse);
background-color: rgba(0, 0, 0, 0.3);
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
backdrop-filter: var(--MI-blur, blur(15px));
color: #fff;
font-size: 0.8em;
width: 28px;
@ -259,10 +259,10 @@ html[data-color-scheme=light] .visible {
}
.indicator {
/* Hardcode to black because either --bg or --fg makes it hard to read in dark/light mode */
/* Hardcode to black because either --MI_THEME-bg or --MI_THEME-fg makes it hard to read in dark/light mode */
background-color: black;
border-radius: var(--radius-sm);
color: var(--accentLighten);
color: var(--MI_THEME-accentLighten);
display: inline-block;
font-weight: bold;
font-size: 0.8em;

View file

@ -329,14 +329,14 @@ defineExpose({
:global(.pswp) {
--pswp-root-z-index: var(--mk-pswp-root-z-index, 2000700) !important;
--pswp-bg: var(--modalBg) !important;
--pswp-bg: var(--MI_THEME-modalBg) !important;
}
</style>
<style lang="scss">
.pswp__bg {
background: var(--modalBg);
backdrop-filter: var(--modalBgFilter);
background: var(--MI_THEME-modalBg);
backdrop-filter: var(--MI-modalBgFilter);
}
.pswp__alt-text-container {
@ -354,14 +354,14 @@ defineExpose({
}
.pswp__alt-text {
color: var(--fg);
color: var(--MI_THEME-fg);
margin: 0 auto;
text-align: center;
padding: var(--margin);
border-radius: var(--radius);
padding: var(--MI-margin);
border-radius: var(--MI-radius);
max-height: 8em;
overflow-y: auto;
text-shadow: var(--bg) 0 0 10px, var(--bg) 0 0 3px, var(--bg) 0 0 3px;
text-shadow: var(--MI_THEME-bg) 0 0 10px, var(--MI_THEME-bg) 0 0 3px, var(--MI_THEME-bg) 0 0 3px;
white-space: pre-line;
}
</style>

View file

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<!-- Media系専用のinput range -->
<template>
<div :style="sliderBgWhite ? '--sliderBg: rgba(255,255,255,.25);' : '--sliderBg: var(--scrollbarHandle);'">
<div :style="sliderBgWhite ? '--sliderBg: rgba(255,255,255,.25);' : '--sliderBg: var(--MI_THEME-scrollbarHandle);'">
<div :class="$style.controlsSeekbar">
<progress v-if="buffer !== undefined" :class="$style.buffer" :value="isNaN(buffer) ? 0 : buffer" min="0" max="1">{{ Math.round(buffer * 100) }}% buffered</progress>
<input v-model="model" :class="$style.seek" :style="`--value: ${modelValue * 100}%;`" type="range" min="0" max="1" step="any" @change="emit('dragEnded', modelValue)"/>
@ -48,7 +48,7 @@ const modelValue = computed({
background: transparent;
border: 0;
border-radius: 26px;
color: var(--accent);
color: var(--MI_THEME-accent);
display: block;
height: 19px;
margin: 0;

View file

@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i class="ti ti-eye-off" :class="$style.hide" @click="hide = true"></i>
<div :class="$style.indicators">
<div v-if="video.comment" :class="$style.indicator">ALT</div>
<div v-if="video.isSensitive" :class="$style.indicator" style="color: var(--warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
<div v-if="video.isSensitive" :class="$style.indicator" style="color: var(--MI_THEME-warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
</div>
</div>
@ -67,7 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i class="ti ti-eye-off" :class="$style.hide" @click="hide = true"></i>
<div :class="$style.indicators">
<div v-if="video.comment" :class="$style.indicator">ALT</div>
<div v-if="video.isSensitive" :class="$style.indicator" style="color: var(--warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
<div v-if="video.isSensitive" :class="$style.indicator" style="color: var(--MI_THEME-warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
</div>
<div :class="$style.videoControls" @click.self="togglePlayPause">
<div :class="[$style.controlsChild, $style.controlsLeft]">
@ -511,7 +511,7 @@ onDeactivated(() => {
height: 100%;
pointer-events: none;
border-radius: inherit;
box-shadow: inset 0 0 0 4px var(--warn);
box-shadow: inset 0 0 0 4px var(--MI_THEME-warn);
}
}
@ -526,10 +526,10 @@ onDeactivated(() => {
}
.indicator {
/* Hardcode to black because either --bg or --fg makes it hard to read in dark/light mode */
/* Hardcode to black because either --MI_THEME-bg or --MI_THEME-fg makes it hard to read in dark/light mode */
background-color: black;
border-radius: 6px;
color: var(--accentLighten);
color: var(--MI_THEME-accentLighten);
display: inline-block;
font-weight: bold;
font-size: 0.8em;
@ -541,7 +541,7 @@ onDeactivated(() => {
position: absolute;
border-radius: var(--radius-sm);
background-color: black;
color: var(--accentLighten);
color: var(--MI_THEME-accentLighten);
font-size: 12px;
opacity: .5;
padding: 5px 8px;
@ -595,7 +595,7 @@ onDeactivated(() => {
opacity: 0;
transition: opacity .4s ease-in-out;
background: var(--accent);
background: var(--MI_THEME-accent);
color: #fff;
padding: 1rem;
border-radius: 99rem;
@ -661,12 +661,12 @@ onDeactivated(() => {
.controlButton {
padding: 6px;
border-radius: calc(var(--radius) / 2);
border-radius: calc(var(--MI-radius) / 2);
transition: background-color .2s ease-in-out;
font-size: 1.05rem;
&:hover {
background-color: var(--accent);
background-color: var(--MI_THEME-accent);
}
&:focus-visible {

View file

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkA v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe }]" :to="url" :style="{ background: bgCss }" :behavior="navigationBehavior">
<MkA v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe }]" :to="url" :behavior="navigationBehavior">
<img :class="$style.icon" :src="avatarUrl" alt="">
<span>
<span>@{{ username }}</span>
@ -16,7 +16,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { toUnicode } from 'punycode';
import { computed } from 'vue';
import tinycolor from 'tinycolor2';
import { host as localHost } from '@@/js/config.js';
import { $i } from '@/account.js';
import { defaultStore } from '@/store.js';
@ -37,11 +36,7 @@ const isMe = $i && (
`@${props.username}@${toUnicode(props.host)}` === `@${$i.username}@${toUnicode(localHost)}`.toLowerCase()
);
const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue(isMe ? '--mentionMe' : '--mention'));
bg.setAlpha(0.1);
const bgCss = bg.toRgbString();
const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages
const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar
? getStaticImageUrl(`/avatar/@${props.username}@${props.host}`)
: `/avatar/@${props.username}@${props.host}`,
);
@ -52,11 +47,13 @@ const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages
display: inline-block;
padding: 4px 8px 4px 4px;
border-radius: var(--radius-ellipse);
color: var(--mention);
color: var(--MI_THEME-mention);
background: color(from var(--MI_THEME-mention) srgb r g b / 0.1);
white-space: nowrap;
&.isMe {
color: var(--mentionMe);
color: var(--MI_THEME-mentionMe);
background: color(from var(--MI_THEME-mentionMe) srgb r g b / 0.1);
}
}

View file

@ -49,7 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
<div :class="$style.item_content">
<span :class="$style.item_content_text">{{ item.text }}</span>
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
<span v-if="item.indicate" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span>
</div>
</MkA>
<a
@ -68,7 +68,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
<div :class="$style.item_content">
<span :class="$style.item_content_text">{{ item.text }}</span>
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
<span v-if="item.indicate" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span>
</div>
</a>
<button
@ -82,7 +82,7 @@ SPDX-License-Identifier: AGPL-3.0-only
>
<MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/>
<div v-if="item.indicate" :class="$style.item_content">
<span :class="$style.indicator"><i class="_indicatorCircle"></i></span>
<span :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span>
</div>
</button>
<button
@ -161,7 +161,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
<div :class="$style.item_content">
<span :class="$style.item_content_text">{{ item.text }}</span>
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
<span v-if="item.indicate" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span>
</div>
</button>
</template>
@ -437,9 +437,11 @@ onBeforeUnmount(() => {
&.big:not(.asDrawer) {
> .menu {
min-width: 230px;
> .item {
padding: 6px 20px;
font-size: 1em;
font-size: 0.95em;
line-height: 24px;
}
}
@ -505,7 +507,7 @@ onBeforeUnmount(() => {
overflow: hidden;
text-overflow: ellipsis;
text-decoration: none !important;
color: var(--menuFg, var(--fg));
color: var(--menuFg, var(--MI_THEME-fg));
&::before {
content: "";
@ -525,7 +527,7 @@ onBeforeUnmount(() => {
outline: none;
&:not(:hover):not(:active)::before {
outline: var(--focus) solid 2px;
outline: var(--MI_THEME-focus) solid 2px;
outline-offset: -2px;
}
}
@ -534,19 +536,19 @@ onBeforeUnmount(() => {
&:hover,
&:focus-visible:active,
&:focus-visible.active {
color: var(--menuHoverFg, var(--accent));
color: var(--menuHoverFg, var(--MI_THEME-accent));
&::before {
background-color: var(--menuHoverBg, var(--accentedBg));
background-color: var(--menuHoverBg, var(--MI_THEME-accentedBg));
}
}
&:not(:focus-visible):active,
&:not(:focus-visible).active {
color: var(--menuActiveFg, var(--fgOnAccent));
color: var(--menuActiveFg, var(--MI_THEME-fgOnAccent));
&::before {
background-color: var(--menuActiveBg, var(--accent));
background-color: var(--menuActiveBg, var(--MI_THEME-accent));
}
}
}
@ -564,13 +566,13 @@ onBeforeUnmount(() => {
}
&.radio {
--menuActiveFg: var(--accent);
--menuActiveBg: var(--accentedBg);
--menuActiveFg: var(--MI_THEME-accent);
--menuActiveBg: var(--MI_THEME-accentedBg);
}
&.parent {
--menuActiveFg: var(--accent);
--menuActiveBg: var(--accentedBg);
--menuActiveFg: var(--MI_THEME-accent);
--menuActiveBg: var(--MI_THEME-accentedBg);
}
&.label {
@ -635,14 +637,13 @@ onBeforeUnmount(() => {
.indicator {
display: flex;
align-items: center;
color: var(--indicator);
color: var(--MI_THEME-indicator);
font-size: 12px;
animation: global-blink 1s infinite;
}
.divider {
margin: 8px 0;
border-top: solid 0.5px var(--divider);
border-top: solid 0.5px var(--MI_THEME-divider);
}
.radioIcon {
@ -652,11 +653,11 @@ onBeforeUnmount(() => {
height: 1em;
vertical-align: -0.125em;
border-radius: 50%;
border: solid 2px var(--divider);
background-color: var(--panel);
border: solid 2px var(--MI_THEME-divider);
background-color: var(--MI_THEME-panel);
&.radioChecked {
border-color: var(--accent);
border-color: var(--MI_THEME-accent);
&::after {
content: "";
@ -668,7 +669,7 @@ onBeforeUnmount(() => {
width: 50%;
height: 50%;
border-radius: 50%;
background-color: var(--accent);
background-color: var(--MI_THEME-accent);
}
}
}

View file

@ -48,7 +48,7 @@ const polygonPoints = ref('');
const headX = ref<number | null>(null);
const headY = ref<number | null>(null);
const clock = ref<number | null>(null);
const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--accent'));
const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-accent'));
const color = accent.toRgbString();
function draw(): void {

View file

@ -90,12 +90,12 @@ defineExpose({
display: flex;
flex-direction: column;
contain: content;
border-radius: var(--radius);
border-radius: var(--MI-radius);
--root-margin: 24px;
--headerHeight: 46px;
--headerHeightNarrow: 42px;
--MI_THEME-headerHeight: 46px;
--MI_THEME-headerHeightNarrow: 42px;
@media (max-width: 500px) {
--root-margin: 16px;
@ -105,24 +105,24 @@ defineExpose({
.header {
display: flex;
flex-shrink: 0;
background: var(--windowHeader);
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
background: var(--MI_THEME-windowHeader);
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
backdrop-filter: var(--MI-blur, blur(15px));
}
.headerButton {
height: var(--headerHeight);
width: var(--headerHeight);
height: var(--MI_THEME-headerHeight);
width: var(--MI_THEME-headerHeight);
@media (max-width: 500px) {
height: var(--headerHeightNarrow);
width: var(--headerHeightNarrow);
height: var(--MI_THEME-headerHeightNarrow);
width: var(--MI_THEME-headerHeightNarrow);
}
}
.title {
flex: 1;
line-height: var(--headerHeight);
line-height: var(--MI_THEME-headerHeight);
padding-left: 32px;
font-weight: bold;
white-space: nowrap;
@ -131,7 +131,7 @@ defineExpose({
pointer-events: none;
@media (max-width: 500px) {
line-height: var(--headerHeightNarrow);
line-height: var(--MI_THEME-headerHeightNarrow);
padding-left: 16px;
}
}
@ -143,7 +143,7 @@ defineExpose({
.body {
flex: 1;
overflow: auto;
background: var(--panel);
background: var(--MI_THEME-panel);
container-type: size;
}
</style>

View file

@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
v-show="!isDeleted"
ref="rootEl"
v-hotkey="keymap"
:class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover }]"
:class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover, [$style.skipRender]: defaultStore.state.skipNoteRender }]"
:tabindex="isDeleted ? '-1' : '0'"
>
<div v-if="appearNote.reply && inReplyToCollapsed" :class="$style.collapsedInReplyTo">
@ -156,8 +156,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<i class="ph-heart ph-bold ph-lg"></i>
</button>
<button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()" @click.stop>
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--love);"></i>
<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--MI_THEME-love);"></i>
<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></i>
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
<i v-else class="ph-smiley ph-bold ph-lg"></i>
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
@ -201,6 +201,9 @@ import { computed, inject, onMounted, ref, shallowRef, Ref, watch, provide } fro
import * as mfm from '@transfem-org/sfm-js';
import * as Misskey from 'misskey-js';
import { isLink } from '@@/js/is-link.js';
import { shouldCollapsed } from '@@/js/collapsed.js';
import { host } from '@@/js/config.js';
import type { MenuItem } from '@/types/menu.js';
import MkNoteSub from '@/components/MkNoteSub.vue';
import MkNoteHeader from '@/components/MkNoteHeader.vue';
import MkNoteSimple from '@/components/MkNoteSimple.vue';
@ -233,13 +236,10 @@ import { deepClone } from '@/scripts/clone.js';
import { useTooltip } from '@/scripts/use-tooltip.js';
import { claimAchievement } from '@/scripts/achievements.js';
import { getNoteSummary } from '@/scripts/get-note-summary.js';
import type { MenuItem } from '@/types/menu.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
import { useRouter } from '@/router/supplier.js';
import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
import { shouldCollapsed } from '@@/js/collapsed.js';
import { host } from '@@/js/config.js';
import { isEnabledUrlPreview } from '@/instance.js';
import { type Keymap } from '@/scripts/hotkey.js';
import { focusPrev, focusNext } from '@/scripts/focus.js';
@ -877,14 +877,6 @@ function emitUpdReaction(emoji: string, delta: number) {
overflow: clip;
contain: content;
//
//
// contain-intrinsic-size
//
// ()
//content-visibility: auto;
//contain-intrinsic-size: 0 128px;
&:focus-visible {
outline: none;
@ -901,8 +893,8 @@ function emitUpdReaction(emoji: string, delta: number) {
margin: auto;
width: calc(100% - 8px);
height: calc(100% - 8px);
border: dashed 2px var(--focus);
border-radius: var(--radius);
border: dashed 2px var(--MI_THEME-focus);
border-radius: var(--MI-radius);
box-sizing: border-box;
}
}
@ -929,9 +921,9 @@ function emitUpdReaction(emoji: string, delta: number) {
right: 12px;
padding: 0 4px;
margin-bottom: 0 !important;
background: var(--popup);
background: var(--MI_THEME-popup);
border-radius: var(--radius-sm);
box-shadow: 0px 4px 32px var(--shadow);
box-shadow: 0px 4px 32px var(--MI_THEME-shadow);
}
.footerButton {
@ -950,6 +942,11 @@ function emitUpdReaction(emoji: string, delta: number) {
}
}
.skipRender {
content-visibility: auto;
contain-intrinsic-size: 0 150px;
}
.tip {
display: flex;
align-items: center;
@ -976,7 +973,7 @@ function emitUpdReaction(emoji: string, delta: number) {
padding: 16px 32px 8px 32px;
line-height: 28px;
white-space: pre;
color: var(--renote);
color: var(--MI_THEME-renote);
& + .article {
padding-top: 8px;
@ -1081,7 +1078,7 @@ function emitUpdReaction(emoji: string, delta: number) {
width: var(--avatar);
height: var(--avatar);
position: sticky !important;
top: calc(22px + var(--stickyTop, 0px));
top: calc(22px + var(--MI-stickyTop, 0px));
left: 0;
}
@ -1101,12 +1098,12 @@ function emitUpdReaction(emoji: string, delta: number) {
width: 100%;
margin-top: 14px;
position: sticky;
bottom: calc(var(--stickyBottom, 0px) - 100px);
bottom: calc(var(--MI-stickyBottom, 0px) - 100px);
}
.showLessLabel {
display: inline-block;
background: var(--popup);
background: var(--MI_THEME-popup);
padding: 6px 10px;
font-size: 0.8em;
border-radius: var(--radius-ellipse);
@ -1127,16 +1124,16 @@ function emitUpdReaction(emoji: string, delta: number) {
z-index: 2;
width: 100%;
height: 64px;
//background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
//background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0));
&:hover > .collapsedLabel {
background: var(--panelHighlight);
background: var(--MI_THEME-panelHighlight);
}
}
.collapsedLabel {
display: inline-block;
background: var(--panel);
background: var(--MI_THEME-panel);
padding: 6px 10px;
font-size: 0.8em;
border-radius: var(--radius-ellipse);
@ -1149,13 +1146,13 @@ function emitUpdReaction(emoji: string, delta: number) {
}
.replyIcon {
color: var(--accent);
color: var(--MI_THEME-accent);
margin-right: 0.5em;
}
.translation {
border: solid 0.5px var(--divider);
border-radius: var(--radius);
border: solid 0.5px var(--MI_THEME-divider);
border-radius: var(--MI-radius);
padding: 12px;
margin-top: 8px;
}
@ -1178,7 +1175,7 @@ function emitUpdReaction(emoji: string, delta: number) {
.quoteNote {
padding: 16px;
border: dashed 1px var(--renote);
border: dashed 1px var(--MI_THEME-renote);
border-radius: var(--radius-sm);
overflow: clip;
}
@ -1202,7 +1199,7 @@ function emitUpdReaction(emoji: string, delta: number) {
}
&:hover {
color: var(--fgHighlighted);
color: var(--MI_THEME-fgHighlighted);
}
}
@ -1277,7 +1274,7 @@ function emitUpdReaction(emoji: string, delta: number) {
margin: 0 10px 0 0;
width: 46px;
height: 46px;
top: calc(14px + var(--stickyTop, 0px));
top: calc(14px + var(--MI-stickyTop, 0px));
}
}

View file

@ -157,8 +157,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<i class="ph-heart ph-bold ph-lg"></i>
</button>
<button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()">
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--love);"></i>
<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--MI_THEME-love);"></i>
<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></i>
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
<i v-else class="ph-smiley ph-bold ph-lg"></i>
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p>
@ -843,8 +843,8 @@ function animatedMFM() {
margin: auto;
width: calc(100% - 8px);
height: calc(100% - 8px);
border: dashed 2px var(--focus);
border-radius: var(--radius);
border: dashed 2px var(--MI_THEME-focus);
border-radius: var(--MI-radius);
box-sizing: border-box;
}
}
@ -874,7 +874,7 @@ function animatedMFM() {
padding: 16px 32px 8px 32px;
line-height: 28px;
white-space: pre;
color: var(--renote);
color: var(--MI_THEME-renote);
}
.renoteAvatar {
@ -956,7 +956,7 @@ function animatedMFM() {
padding: 4px 6px;
font-size: 80%;
line-height: 1;
border: solid 0.5px var(--divider);
border: solid 0.5px var(--MI_THEME-divider);
border-radius: var(--radius-xs);
}
@ -989,19 +989,19 @@ function animatedMFM() {
}
.noteReplyTarget {
color: var(--accent);
color: var(--MI_THEME-accent);
margin-right: 0.5em;
}
.rn {
margin-left: 4px;
font-style: oblique;
color: var(--renote);
color: var(--MI_THEME-renote);
}
.translation {
border: solid 0.5px var(--divider);
border-radius: var(--radius);
border: solid 0.5px var(--MI_THEME-divider);
border-radius: var(--MI-radius);
padding: 12px;
margin-top: 8px;
}
@ -1016,7 +1016,7 @@ function animatedMFM() {
.quoteNote {
padding: 16px;
border: dashed 1px var(--renote);
border: dashed 1px var(--MI_THEME-renote);
border-radius: var(--radius-sm);
overflow: clip;
}
@ -1042,7 +1042,7 @@ function animatedMFM() {
}
&:hover {
color: var(--fgHighlighted);
color: var(--MI_THEME-fgHighlighted);
}
}
@ -1052,17 +1052,17 @@ function animatedMFM() {
opacity: 0.7;
&.reacted {
color: var(--accent);
color: var(--MI_THEME-accent);
}
}
.reply:not(:first-child) {
border-top: solid 0.5px var(--divider);
border-top: solid 0.5px var(--MI_THEME-divider);
}
.tabs {
border-top: solid 0.5px var(--divider);
border-bottom: solid 0.5px var(--divider);
border-top: solid 0.5px var(--MI_THEME-divider);
border-bottom: solid 0.5px var(--MI_THEME-divider);
display: flex;
}
@ -1074,7 +1074,7 @@ function animatedMFM() {
}
.tabActive {
border-bottom: solid 2px var(--accent);
border-bottom: solid 2px var(--MI_THEME-accent);
}
.tab_renotes {
@ -1094,12 +1094,12 @@ function animatedMFM() {
.reactionTab {
padding: 4px 6px;
border: solid 1px var(--divider);
border: solid 1px var(--MI_THEME-divider);
border-radius: var(--radius-sm);
}
.reactionTabActive {
border-color: var(--accent);
border-color: var(--MI_THEME-accent);
}
@container (max-width: 500px) {

View file

@ -5,18 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<header :class="$style.root">
<component :is="defaultStore.state.enableCondensedLine ? 'MkCondensedLine' : 'div'" :minScale="0.5" style="min-width: 0;">
<div style="display: flex; white-space: nowrap; align-items: baseline;">
<div v-if="mock" :class="$style.name">
<MkUserName :user="note.user"/>
</div>
<MkA v-else v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)">
<MkUserName :user="note.user"/>
</MkA>
<div v-if="note.user.isBot" :class="$style.isBot">bot</div>
<div :class="$style.username"><MkAcct :user="note.user"/></div>
</div>
</component>
<div v-if="mock" :class="$style.name">
<MkUserName :user="note.user"/>
</div>
<MkA v-else v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)">
<MkUserName :user="note.user"/>
</MkA>
<div v-if="note.user.isBot" :class="$style.isBot">bot</div>
<div :class="$style.username"><MkAcct :user="note.user"/></div>
<div v-if="note.user.badgeRoles" :class="$style.badgeRoles">
<img v-for="(role, i) in note.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl!"/>
</div>
@ -95,7 +91,7 @@ const mock = inject<boolean>('mock', false);
margin: 0 .5em 0 0;
padding: 1px 6px;
font-size: 80%;
border: solid 0.5px var(--divider);
border: solid 0.5px var(--MI_THEME-divider);
border-radius: var(--radius-xs);
}

View file

@ -58,7 +58,7 @@ watch(() => props.expandAllCws, (expandAllCws) => {
height: 34px;
border-radius: var(--radius-sm);
position: sticky !important;
top: calc(16px + var(--stickyTop, 0px));
top: calc(16px + var(--MI-stickyTop, 0px));
left: 0;
}

View file

@ -510,7 +510,7 @@ if (props.detail) {
}
.reply, .more {
border-left: solid 0.5px var(--divider);
border-left: solid 0.5px var(--MI_THEME-divider);
margin-top: 10px;
}
@ -531,7 +531,7 @@ if (props.detail) {
.muted {
text-align: center;
padding: 8px !important;
border: 1px solid var(--divider);
border: 1px solid var(--MI_THEME-divider);
margin: 8px 8px 0 8px;
border-radius: var(--radius-sm);
}

View file

@ -64,17 +64,17 @@ defineExpose({
border-radius: var(--radius);
> .notes {
background: color-mix(in srgb, var(--panel) 65%, transparent);
background: color-mix(in srgb, var(--MI_THEME-panel) 65%, transparent);
}
}
&:not(.noGap) {
> .notes {
background: var(--bg);
background: var(--MI_THEME-bg);
.note {
background: color-mix(in srgb, var(--panel) 65%, transparent);
border-radius: var(--radius);
background: color-mix(in srgb, var(--MI_THEME-panel) 65%, transparent);
border-radius: var(--MI-radius);
}
}
}

View file

@ -7,13 +7,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.root">
<div :class="$style.head">
<MkAvatar v-if="['pollEnded', 'note', 'edited'].includes(notification.type) && 'note' in notification" :class="$style.icon" :user="notification.note.user" link preview/>
<MkAvatar v-else-if="['roleAssigned', 'achievementEarned'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
<div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div>
<MkAvatar v-else-if="['roleAssigned', 'achievementEarned', 'exportCompleted', 'login'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
<div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroupHeart]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
<img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/>
<MkAvatar v-else-if="'user' in notification" :class="$style.icon" :user="notification.user" link preview/>
<MkAvatar v-else-if="notification.type === 'exportCompleted'" :class="$style.icon" :user="$i" link preview/>
<img v-else-if="'icon' in notification && notification.icon != null" :class="[$style.icon, $style.icon_app]" :src="notification.icon" alt=""/>
<div
:class="[$style.subIcon, {
@ -27,6 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
[$style.t_pollEnded]: notification.type === 'pollEnded',
[$style.t_achievementEarned]: notification.type === 'achievementEarned',
[$style.t_exportCompleted]: notification.type === 'exportCompleted',
[$style.t_login]: notification.type === 'login',
[$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null,
[$style.t_pollEnded]: notification.type === 'edited',
}]"
@ -41,6 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
<i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i>
<i v-else-if="notification.type === 'exportCompleted'" class="ti ti-archive"></i>
<i v-else-if="notification.type === 'login'" class="ti ti-login-2"></i>
<template v-else-if="notification.type === 'roleAssigned'">
<img v-if="notification.role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/>
<i v-else class="ti ti-badges"></i>
@ -62,6 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="notification.type === 'note'">{{ i18n.ts._notification.newNote }}: <MkUserName :user="notification.note.user"/></span>
<span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span>
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
<span v-else-if="notification.type === 'login'">{{ i18n.ts._notification.login }}</span>
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
<span v-else-if="notification.type === 'exportCompleted'">{{ i18n.tsx._notification.exportOfXCompleted({ x: exportEntityName[notification.exportedEntity] }) }}</span>
<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
@ -228,13 +230,16 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
overflow-wrap: break-word;
display: flex;
contain: content;
content-visibility: auto;
contain-intrinsic-size: 0 100px;
--eventFollow: #36aed2;
--eventRenote: #36d298;
--eventReply: #007aff;
--eventReactionHeart: var(--love);
--eventReactionHeart: var(--MI_THEME-love);
--eventReaction: #e99a0b;
--eventAchievement: #cb9a11;
--eventLogin: #007aff;
--eventOther: #88a6b7;
}
@ -291,8 +296,8 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
height: 20px;
box-sizing: border-box;
border-radius: var(--radius-full);
background: var(--panel);
box-shadow: 0 0 0 3px var(--panel);
background: var(--MI_THEME-panel);
box-shadow: 0 0 0 3px var(--MI_THEME-panel);
font-size: 11px;
text-align: center;
color: #fff;
@ -356,6 +361,12 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
pointer-events: none;
}
.t_login {
padding: 3px;
background: var(--eventLogin);
pointer-events: none;
}
.tail {
flex: 1;
min-width: 0;
@ -438,8 +449,8 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
height: 20px;
box-sizing: border-box;
border-radius: var(--radius-full);
background: var(--panel);
box-shadow: 0 0 0 3px var(--panel);
background: var(--MI_THEME-panel);
box-shadow: 0 0 0 3px var(--MI_THEME-panel);
font-size: 11px;
text-align: center;
color: #fff;

View file

@ -111,6 +111,6 @@ defineExpose({
<style lang="scss" module>
.list {
background: var(--panel);
background: var(--MI_THEME-panel);
}
</style>

View file

@ -24,11 +24,11 @@ const isZero = computed(() => props.value === 0);
<style lang="scss" module>
.isPlus {
color: var(--success);
color: var(--MI_THEME-success);
}
.isMinus {
color: var(--error);
color: var(--MI_THEME-error);
}
.isZero {

View file

@ -78,7 +78,7 @@ function collapsable(v): boolean {
> .boolean {
display: inline;
color: var(--codeBoolean);
color: var(--MI_THEME-codeBoolean);
&.true {
font-weight: bold;
@ -91,12 +91,12 @@ function collapsable(v): boolean {
> .string {
display: inline;
color: var(--codeString);
color: var(--MI_THEME-codeString);
}
> .number {
display: inline;
color: var(--codeNumber);
color: var(--MI_THEME-codeNumber);
}
> .array.empty {
@ -127,7 +127,7 @@ function collapsable(v): boolean {
> .toggle {
width: 16px;
color: var(--accent);
color: var(--MI_THEME-accent);
visibility: hidden;
&.visible {

View file

@ -47,7 +47,7 @@ onUnmounted(() => {
<style lang="scss" module>
.content {
--stickyTop: 0px;
--MI-stickyTop: 0px;
&.omitted {
position: relative;
@ -62,11 +62,11 @@ onUnmounted(() => {
left: 0;
width: 100%;
height: 64px;
//background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
//background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0));
> .fadeLabel {
display: inline-block;
background: var(--panel);
background: var(--MI_THEME-panel);
padding: 6px 10px;
font-size: 0.8em;
border-radius: var(--radius-ellipse);
@ -75,7 +75,7 @@ onUnmounted(() => {
&:hover {
> .fadeLabel {
background: var(--panelHighlight);
background: var(--MI_THEME-panelHighlight);
}
}
}

View file

@ -42,7 +42,7 @@ const props = defineProps<{
.eyeCatchingImageRoot {
width: 100%;
height: 200px;
border-radius: var(--radius) var(--radius) 0 0;
border-radius: var(--MI-radius) var(--MI-radius) 0 0;
overflow: hidden;
}
</style>
@ -54,7 +54,7 @@ const props = defineProps<{
&:hover {
text-decoration: none;
color: var(--accent);
color: var(--MI_THEME-accent);
}
&:focus-within {
@ -67,22 +67,22 @@ const props = defineProps<{
left: 0;
width: 100%;
height: 100%;
border-radius: var(--radius);
border-radius: var(--MI-radius);
pointer-events: none;
box-shadow: inset 0 0 0 2px var(--focus);
box-shadow: inset 0 0 0 2px var(--MI_THEME-focus);
}
}
> .thumbnail {
& + article {
border-radius: 0 0 var(--radius) var(--radius);
border-radius: 0 0 var(--MI-radius) var(--MI-radius);
}
}
> article {
background-color: var(--panel);
background-color: var(--MI_THEME-panel);
padding: 16px;
border-radius: var(--radius);
border-radius: var(--MI-radius);
> header {
margin-bottom: 8px;
@ -115,7 +115,6 @@ const props = defineProps<{
> p {
display: inline-block;
margin: 0;
color: var(--urlPreviewInfo);
font-size: 0.8em;
line-height: 16px;
vertical-align: top;

View file

@ -181,8 +181,8 @@ defineExpose({
overscroll-behavior: contain;
min-height: 100%;
background: var(--bg);
background: var(--MI_THEME-bg);
--margin: var(--marginHalf);
--MI-margin: var(--MI-marginHalf);
}
</style>

View file

@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<li v-for="(choice, i) in props.poll.choices" :key="i" :class="$style.choice" @click="vote(i)">
<div :class="$style.bg" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
<span :class="$style.fg">
<template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--accent);"></i></template>
<template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--MI_THEME-accent);"></i></template>
<Mfm :text="choice.text" :plain="true"/>
<span v-if="showResult" style="margin-left: 4px; opacity: 0.7;">({{ i18n.tsx._poll.votesCount({ n: choice.votes }) }})</span>
</span>
@ -139,8 +139,8 @@ const refreshVotes = async () => {
position: relative;
margin: 4px 0;
padding: 4px;
//border: solid 0.5px var(--divider);
background: var(--accentedBg);
//border: solid 0.5px var(--MI_THEME-divider);
background: var(--MI_THEME-accentedBg);
border-radius: var(--radius-xs);
overflow: clip;
cursor: pointer;
@ -151,8 +151,8 @@ const refreshVotes = async () => {
top: 0;
left: 0;
height: 100%;
background: var(--accent);
background: linear-gradient(90deg,var(--buttonGradateA),var(--buttonGradateB));
background: var(--MI_THEME-accent);
background: linear-gradient(90deg,var(--MI_THEME-buttonGradateA),var(--MI_THEME-buttonGradateB));
transition: width 1s ease;
}
@ -160,12 +160,12 @@ const refreshVotes = async () => {
position: relative;
display: inline-block;
padding: 3px 5px;
background: var(--panel);
background: var(--MI_THEME-panel);
border-radius: var(--radius-xs);
}
.info {
color: var(--fg);
color: var(--MI_THEME-fg);
}
.done {

View file

@ -1175,7 +1175,7 @@ defineExpose({
outline: none;
.submitInner {
outline: 2px solid var(--fgOnAccent);
outline: 2px solid var(--MI_THEME-fgOnAccent);
outline-offset: -4px;
}
}
@ -1190,13 +1190,13 @@ defineExpose({
&:not(:disabled):hover {
> .inner {
background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
}
}
&:not(:disabled):active {
> .inner {
background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
}
}
}
@ -1218,8 +1218,8 @@ defineExpose({
border-radius: var(--radius-sm);
min-width: 90px;
box-sizing: border-box;
color: var(--fgOnAccent);
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
color: var(--MI_THEME-fgOnAccent);
background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
}
.headerRightItem {
@ -1228,7 +1228,7 @@ defineExpose({
border-radius: var(--radius-sm);
&:hover {
background: var(--X5);
background: var(--MI_THEME-X5);
}
&:disabled {
@ -1272,7 +1272,7 @@ defineExpose({
.withQuote {
margin: 0 0 8px 0;
color: var(--accent);
color: var(--MI_THEME-accent);
}
.toSpecified {
@ -1292,7 +1292,7 @@ defineExpose({
margin-right: 14px;
padding: 8px 0 8px 8px;
border-radius: var(--radius-sm);
background: var(--X4);
background: var(--MI_THEME-X4);
}
.hasNotSpecifiedMentions {
@ -1311,7 +1311,7 @@ defineExpose({
border: none;
border-radius: 0;
background: transparent;
color: var(--fg);
color: var(--MI_THEME-fg);
font-family: inherit;
&:focus {
@ -1326,7 +1326,7 @@ defineExpose({
.cwFrame {
z-index: 1;
padding-bottom: 8px;
border-bottom: solid 0.5px var(--divider);
border-bottom: solid 0.5px var(--MI_THEME-divider);
width: 100%;
position: relative;
@ -1336,7 +1336,7 @@ defineExpose({
z-index: 1;
padding-top: 8px;
padding-bottom: 8px;
border-top: solid 0.5px var(--divider);
border-top: solid 0.5px var(--MI_THEME-divider);
}
.textOuter {
@ -1362,7 +1362,7 @@ defineExpose({
right: 2px;
padding: 4px 6px;
font-size: .9em;
color: var(--warn);
color: var(--MI_THEME-warn);
border-radius: var(--radius-sm);
min-width: 1.6em;
text-align: center;
@ -1406,16 +1406,16 @@ defineExpose({
border-radius: var(--radius-sm);
&:hover {
background: var(--X5);
background: var(--MI_THEME-X5);
}
&.footerButtonActive {
color: var(--accent);
color: var(--MI_THEME-accent);
}
}
.previewButtonActive {
color: var(--accent);
color: var(--MI_THEME-accent);
}
@container (max-width: 500px) {

View file

@ -216,7 +216,7 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent | Keyboar
width: 100%;
height: 100%;
z-index: 1;
color: var(--fg);
color: var(--MI_THEME-fg);
}
.sensitive {

View file

@ -53,9 +53,9 @@ function toggle(): void {
cursor: pointer;
padding: 7px 10px;
min-width: 60px;
background-color: var(--panel);
background-color: var(--MI_THEME-panel);
background-clip: padding-box !important;
border: solid 1px var(--panel);
border: solid 1px var(--MI_THEME-panel);
border-radius: var(--radius-sm);
font-size: 90%;
transition: all 0.2s;
@ -67,25 +67,25 @@ function toggle(): void {
}
&:hover {
border-color: var(--inputBorderHover) !important;
border-color: var(--MI_THEME-inputBorderHover) !important;
}
&:focus-within {
outline: none;
box-shadow: 0 0 0 2px var(--focus);
box-shadow: 0 0 0 2px var(--MI_THEME-focus);
}
&.checked {
background-color: var(--accentedBg) !important;
border-color: var(--accentedBg) !important;
color: var(--accent);
background-color: var(--MI_THEME-accentedBg) !important;
border-color: var(--MI_THEME-accentedBg) !important;
color: var(--MI_THEME-accent);
cursor: default !important;
> .button {
border-color: var(--accent);
border-color: var(--MI_THEME-accent);
&::after {
background-color: var(--accent);
background-color: var(--MI_THEME-accent);
transform: scale(1);
opacity: 1;
}
@ -106,7 +106,7 @@ function toggle(): void {
width: 14px;
height: 14px;
background: none;
border: solid 2px var(--inputBorder);
border: solid 2px var(--MI_THEME-inputBorder);
border-radius: var(--radius-full);
transition: inherit;

View file

@ -77,7 +77,7 @@ export default defineComponent({
> .caption {
font-size: 0.85em;
padding: 8px 0 0 0;
color: var(--fgTransparentWeak);
color: var(--MI_THEME-fgTransparentWeak);
&:empty {
display: none;

View file

@ -212,7 +212,7 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
> .caption {
font-size: 0.85em;
padding: 8px 0 0 0;
color: var(--fgTransparentWeak);
color: var(--MI_THEME-fgTransparentWeak);
&:empty {
display: none;
@ -224,8 +224,8 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
> .body {
padding: 7px 12px;
background: var(--panel);
border: solid 1px var(--panel);
background: var(--MI_THEME-panel);
border: solid 1px var(--MI_THEME-panel);
border-radius: var(--radius-sm);
> .container {
@ -250,7 +250,7 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
top: 0;
left: 0;
height: 100%;
background: var(--accent);
background: var(--MI_THEME-accent);
opacity: 0.5;
}
}
@ -272,7 +272,7 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
width: $tickWidth;
height: 3px;
margin-left: - math.div($tickWidth, 2);
background: var(--divider);
background: var(--MI_THEME-divider);
border-radius: var(--radius-ellipse);
}
}
@ -282,11 +282,11 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
width: $thumbWidth;
height: $thumbHeight;
cursor: grab;
background: var(--accent);
background: var(--MI_THEME-accent);
border-radius: var(--radius-ellipse);
&:hover {
background: var(--accentLighten);
background: var(--MI_THEME-accentLighten);
}
}
}

View file

@ -60,7 +60,7 @@ onMounted(() => {
right: 0;
bottom: 0;
margin: auto;
color: var(--accent);
color: var(--MI_THEME-accent);
font-size: 18px;
font-weight: bold;
transform: translateY(-30px);

View file

@ -57,7 +57,7 @@ function getReactionName(reaction: string): string {
max-width: 100px;
padding-right: 10px;
text-align: center;
border-right: solid 0.5px var(--divider);
border-right: solid 0.5px var(--MI_THEME-divider);
}
.reactionIcon {

View file

@ -180,7 +180,7 @@ if (!mock) {
justify-content: center;
&.canToggle {
background: var(--buttonBg);
background: var(--MI_THEME-buttonBg);
&:hover {
background: rgba(0, 0, 0, 0.1);
@ -214,12 +214,12 @@ if (!mock) {
}
&.reacted, &.reacted:hover {
background: var(--accentedBg);
color: var(--accent);
box-shadow: 0 0 0 1px var(--accent) inset;
background: var(--MI_THEME-accentedBg);
color: var(--MI_THEME-accent);
box-shadow: 0 0 0 1px var(--MI_THEME-accent) inset;
> .count {
color: var(--accent);
color: var(--MI_THEME-accent);
}
> .icon {

View file

@ -19,15 +19,15 @@ defineProps<{
.root {
font-size: 0.8em;
padding: 16px;
background: color-mix(in srgb, var(--infoWarnBg) 65%, transparent);
color: var(--infoWarnFg);
border-radius: var(--radius);
background: color-mix(in srgb, var(--MI_THEME-infoWarnBg) 65%, transparent);
color: var(--MI_THEME-infoWarnFg);
border-radius: var(--MI-radius);
overflow: clip;
z-index: 1;
}
.link {
margin-left: 4px;
color: var(--accent);
color: var(--MI_THEME-accent);
}
</style>

View file

@ -44,7 +44,7 @@ onMounted(async () => {
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--accent'));
const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-accent'));
const color = accent.toHex();
if (chartEl.value == null) return;

View file

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }">
<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
<circle fill="none" cx="64" cy="64" style="stroke: var(--accent);">
<circle fill="none" cx="64" cy="64" style="stroke: var(--MI_THEME-accent);">
<animate
attributeName="r"
begin="0s" dur="0.5s"
@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
/>
</circle>
<g fill="none" fill-rule="evenodd">
<circle v-for="(particle, i) in particles" :key="i" :fill="particle.color" style="stroke: var(--accent);">
<circle v-for="(particle, i) in particles" :key="i" :fill="particle.color" style="stroke: var(--MI_THEME-accent);">
<animate
attributeName="r"
begin="0s" dur="0.8s"

View file

@ -6,8 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkA :to="forModeration ? `/admin/roles/${role.id}` : `/roles/${role.id}`" :class="$style.root" tabindex="-1" :style="{ '--color': role.color }">
<template v-if="forModeration">
<i v-if="role.isPublic" class="ti ti-world" :class="$style.icon" style="color: var(--success)"></i>
<i v-else class="ti ti-lock" :class="$style.icon" style="color: var(--warn)"></i>
<i v-if="role.isPublic" class="ti ti-world" :class="$style.icon" style="color: var(--MI_THEME-success)"></i>
<i v-else class="ti ti-lock" :class="$style.icon" style="color: var(--MI_THEME-warn)"></i>
</template>
<div v-adaptive-bg class="_panel" :class="$style.body">
@ -17,8 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<img :class="$style.bodyBadge" :src="role.iconUrl"/>
</template>
<template v-else>
<i v-if="role.isAdministrator" class="ti ti-crown" style="color: var(--accent);"></i>
<i v-else-if="role.isModerator" class="ti ti-shield" style="color: var(--accent);"></i>
<i v-if="role.isAdministrator" class="ti ti-crown" style="color: var(--MI_THEME-accent);"></i>
<i v-else-if="role.isModerator" class="ti ti-shield" style="color: var(--MI_THEME-accent);"></i>
<i v-else class="ti ti-user" style="opacity: 0.7;"></i>
</template>
</span>

View file

@ -202,7 +202,7 @@ function show() {
.caption {
font-size: 0.85em;
padding: 8px 0 0 0;
color: var(--fgTransparentWeak);
color: var(--MI_THEME-fgTransparentWeak);
&:empty {
display: none;
@ -220,8 +220,8 @@ function show() {
&.focused {
> .inputCore {
border-color: var(--accent) !important;
//box-shadow: 0 0 0 4px var(--focus);
border-color: var(--MI_THEME-accent) !important;
//box-shadow: 0 0 0 4px var(--MI_THEME-focus);
}
}
@ -240,7 +240,7 @@ function show() {
&:hover {
> .inputCore {
border-color: var(--inputBorderHover) !important;
border-color: var(--MI_THEME-inputBorderHover) !important;
}
}
}
@ -256,9 +256,9 @@ function show() {
font: inherit;
font-weight: normal;
font-size: 1em;
color: var(--fg);
background: var(--panel);
border: solid 1px var(--panel);
color: var(--MI_THEME-fg);
background: var(--MI_THEME-panel);
border: solid 1px var(--MI_THEME-panel);
border-radius: var(--radius-sm);
outline: none;
box-shadow: none;

View file

@ -0,0 +1,206 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.wrapper" data-cy-signin-page-input>
<div :class="$style.root">
<div :class="$style.avatar">
<i class="ti ti-user"></i>
</div>
<!-- ログイン画面メッセージ -->
<MkInfo v-if="message">
{{ message }}
</MkInfo>
<!-- 外部サーバーへの転送 -->
<div v-if="openOnRemote" class="_gaps_m">
<div class="_gaps_s">
<MkButton type="button" rounded primary style="margin: 0 auto;" @click="openRemote(openOnRemote)">
{{ i18n.ts.continueOnRemote }} <i class="ti ti-external-link"></i>
</MkButton>
<button type="button" class="_button" :class="$style.instanceManualSelectButton" @click="specifyHostAndOpenRemote(openOnRemote)">
{{ i18n.ts.specifyServerHost }}
</button>
</div>
<div :class="$style.orHr">
<p :class="$style.orMsg">{{ i18n.ts.or }}</p>
</div>
</div>
<!-- username入力 -->
<form class="_gaps_s" @submit.prevent="emit('usernameSubmitted', username)">
<MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username webauthn" autofocus required data-cy-signin-username>
<template #prefix>@</template>
<template #suffix>@{{ host }}</template>
</MkInput>
<MkButton type="submit" large primary rounded style="margin: 0 auto;" data-cy-signin-page-input-continue>{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
</form>
<!-- パスワードレスログイン -->
<div :class="$style.orHr">
<p :class="$style.orMsg">{{ i18n.ts.or }}</p>
</div>
<div>
<MkButton type="submit" style="margin: auto auto;" large rounded primary gradate @click="emit('passkeyClick', $event)">
<i class="ti ti-device-usb" style="font-size: medium;"></i>{{ i18n.ts.signinWithPasskey }}
</MkButton>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { toUnicode } from 'punycode/';
import { query, extractDomain } from '@@/js/url.js';
import { host as configHost } from '@@/js/config.js';
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkInfo from '@/components/MkInfo.vue';
const props = withDefaults(defineProps<{
message?: string,
openOnRemote?: OpenOnRemoteOptions,
}>(), {
message: '',
openOnRemote: undefined,
});
const emit = defineEmits<{
(ev: 'usernameSubmitted', v: string): void;
(ev: 'passkeyClick', v: MouseEvent): void;
}>();
const host = toUnicode(configHost);
const username = ref('');
//#region Open on remote
function openRemote(options: OpenOnRemoteOptions, targetHost?: string): void {
switch (options.type) {
case 'web':
case 'lookup': {
let _path: string;
if (options.type === 'lookup') {
// TODO: v2024.7.0URL
// _path = `/lookup?uri=${encodeURIComponent(_path)}`;
_path = `/authorize-follow?acct=${encodeURIComponent(options.url)}`;
} else {
_path = options.path;
}
if (targetHost) {
window.open(`https://${targetHost}${_path}`, '_blank', 'noopener');
} else {
window.open(`https://misskey-hub.net/mi-web/?path=${encodeURIComponent(_path)}`, '_blank', 'noopener');
}
break;
}
case 'share': {
const params = query(options.params);
if (targetHost) {
window.open(`https://${targetHost}/share?${params}`, '_blank', 'noopener');
} else {
window.open(`https://misskey-hub.net/share/?${params}`, '_blank', 'noopener');
}
break;
}
}
}
async function specifyHostAndOpenRemote(options: OpenOnRemoteOptions): Promise<void> {
const { canceled, result: hostTemp } = await os.inputText({
title: i18n.ts.inputHostName,
placeholder: 'misskey.example.com',
});
if (canceled) return;
let targetHost: string | null = hostTemp;
//
targetHost = extractDomain(targetHost ?? '');
if (targetHost == null) {
os.alert({
type: 'error',
title: i18n.ts.invalidValue,
text: i18n.ts.tryAgain,
});
return;
}
openRemote(options, targetHost);
}
//#endregion
</script>
<style lang="scss" module>
.root {
display: flex;
flex-direction: column;
gap: 20px;
}
.wrapper {
display: flex;
align-items: center;
width: 100%;
min-height: 336px;
> .root {
width: 100%;
}
}
.avatar {
margin: 0 auto;
background-color: color-mix(in srgb, var(--MI_THEME-fg), transparent 85%);
color: color-mix(in srgb, var(--MI_THEME-fg), transparent 25%);
text-align: center;
height: 64px;
width: 64px;
font-size: 24px;
line-height: 64px;
border-radius: 50%;
}
.instanceManualSelectButton {
display: block;
text-align: center;
opacity: .7;
font-size: .8em;
&:hover {
text-decoration: underline;
}
}
.orHr {
position: relative;
margin: .4em auto;
width: 100%;
height: 1px;
background: var(--MI_THEME-divider);
}
.orMsg {
position: absolute;
top: -.6em;
display: inline-block;
padding: 0 1em;
background: var(--MI_THEME-panel);
font-size: 0.8em;
color: var(--MI_THEME-fgOnPanel);
margin: 0;
left: 50%;
transform: translateX(-50%);
}
</style>

View file

@ -0,0 +1,92 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.wrapper">
<div class="_gaps" :class="$style.root">
<div class="_gaps_s">
<div :class="$style.passkeyIcon">
<i class="ti ti-fingerprint"></i>
</div>
<div :class="$style.passkeyDescription">{{ i18n.ts.useSecurityKey }}</div>
</div>
<MkButton large primary rounded :disabled="queryingKey" style="margin: 0 auto;" @click="queryKey">{{ i18n.ts.retry }}</MkButton>
<MkButton v-if="isPerformingPasswordlessLogin !== true" transparent rounded :disabled="queryingKey" style="margin: 0 auto;" @click="emit('useTotp')">{{ i18n.ts.useTotp }}</MkButton>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { get as webAuthnRequest } from '@github/webauthn-json/browser-ponyfill';
import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue';
import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill';
const props = defineProps<{
credentialRequest: CredentialRequestOptions;
isPerformingPasswordlessLogin?: boolean;
}>();
const emit = defineEmits<{
(ev: 'done', credential: AuthenticationPublicKeyCredential): void;
(ev: 'useTotp'): void;
}>();
const queryingKey = ref(true);
async function queryKey() {
queryingKey.value = true;
await webAuthnRequest(props.credentialRequest)
.catch(() => {
return Promise.reject(null);
})
.then((credential) => {
emit('done', credential);
})
.finally(() => {
queryingKey.value = false;
});
}
onMounted(() => {
queryKey();
});
</script>
<style lang="scss" module>
.wrapper {
display: flex;
align-items: center;
width: 100%;
min-height: 336px;
> .root {
width: 100%;
}
}
.passkeyIcon {
margin: 0 auto;
background-color: var(--MI_THEME-accentedBg);
color: var(--MI_THEME-accent);
text-align: center;
height: 64px;
width: 64px;
font-size: 24px;
line-height: 64px;
border-radius: 50%;
}
.passkeyDescription {
text-align: center;
font-size: 1.1em;
}
</style>

View file

@ -0,0 +1,188 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.wrapper" data-cy-signin-page-password>
<div class="_gaps" :class="$style.root">
<div :class="$style.avatar" :style="{ backgroundImage: user ? `url('${user.avatarUrl}')` : undefined }"></div>
<div :class="$style.welcomeBackMessage">
<I18n :src="i18n.ts.welcomeBackWithName" tag="span">
<template #name><Mfm :text="user.name ?? user.username" :plain="true"/></template>
</I18n>
</div>
<!-- password入力 -->
<form class="_gaps_s" @submit.prevent="onSubmit">
<!-- ブラウザ オートコンプリート用 -->
<input type="hidden" name="username" autocomplete="username" :value="user.username">
<MkInput v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" :withPasswordToggle="true" required autofocus data-cy-signin-password>
<template #prefix><i class="ti ti-lock"></i></template>
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
</MkInput>
<div v-if="needCaptcha">
<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
<MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
<MkCaptcha v-if="instance.enableTestcaptcha" ref="testcaptcha" v-model="testcaptchaResponse" :class="$style.captcha" provider="testcaptcha"/>
</div>
<MkButton type="submit" :disabled="needCaptcha && captchaFailed" large primary rounded style="margin: 0 auto;" data-cy-signin-page-password-continue>{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
</form>
</div>
</div>
</template>
<script lang="ts">
export type PwResponse = {
password: string;
captcha: {
hCaptchaResponse: string | null;
mCaptchaResponse: string | null;
reCaptchaResponse: string | null;
turnstileResponse: string | null;
testcaptchaResponse: string | null;
};
};
</script>
<script setup lang="ts">
import { ref, computed, useTemplateRef, defineAsyncComponent } from 'vue';
import * as Misskey from 'misskey-js';
import { instance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkCaptcha from '@/components/MkCaptcha.vue';
const props = defineProps<{
user: Misskey.entities.UserDetailed;
needCaptcha: boolean;
}>();
const emit = defineEmits<{
(ev: 'passwordSubmitted', v: PwResponse): void;
}>();
const password = ref('');
const hCaptcha = useTemplateRef('hcaptcha');
const mCaptcha = useTemplateRef('mcaptcha');
const reCaptcha = useTemplateRef('recaptcha');
const turnstile = useTemplateRef('turnstile');
const testcaptcha = useTemplateRef('testcaptcha');
const hCaptchaResponse = ref<string | null>(null);
const mCaptchaResponse = ref<string | null>(null);
const reCaptchaResponse = ref<string | null>(null);
const turnstileResponse = ref<string | null>(null);
const testcaptchaResponse = ref<string | null>(null);
const captchaFailed = computed((): boolean => {
return (
(instance.enableHcaptcha && !hCaptchaResponse.value) ||
(instance.enableMcaptcha && !mCaptchaResponse.value) ||
(instance.enableRecaptcha && !reCaptchaResponse.value) ||
(instance.enableTurnstile && !turnstileResponse.value) ||
(instance.enableTestcaptcha && !testcaptchaResponse.value)
);
});
function resetPassword(): void {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkForgotPassword.vue')), {}, {
closed: () => dispose(),
});
}
function onSubmit() {
emit('passwordSubmitted', {
password: password.value,
captcha: {
hCaptchaResponse: hCaptchaResponse.value,
mCaptchaResponse: mCaptchaResponse.value,
reCaptchaResponse: reCaptchaResponse.value,
turnstileResponse: turnstileResponse.value,
testcaptchaResponse: testcaptchaResponse.value,
},
});
}
function resetCaptcha() {
hCaptcha.value?.reset();
mCaptcha.value?.reset();
reCaptcha.value?.reset();
turnstile.value?.reset();
testcaptcha.value?.reset();
}
defineExpose({
resetCaptcha,
});
</script>
<style lang="scss" module>
.wrapper {
display: flex;
align-items: center;
width: 100%;
min-height: 336px;
> .root {
width: 100%;
}
}
.avatar {
margin: 0 auto 0 auto;
width: 64px;
height: 64px;
background: #ddd;
background-position: center;
background-size: cover;
border-radius: 100%;
}
.welcomeBackMessage {
text-align: center;
font-size: 1.1em;
}
.instanceManualSelectButton {
display: block;
text-align: center;
opacity: .7;
font-size: .8em;
&:hover {
text-decoration: underline;
}
}
.orHr {
position: relative;
margin: .4em auto;
width: 100%;
height: 1px;
background: var(--MI_THEME-divider);
}
.orMsg {
position: absolute;
top: -.6em;
display: inline-block;
padding: 0 1em;
background: var(--MI_THEME-panel);
font-size: 0.8em;
color: var(--MI_THEME-fgOnPanel);
margin: 0;
left: 50%;
transform: translateX(-50%);
}
</style>

View file

@ -0,0 +1,74 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.wrapper">
<div class="_gaps" :class="$style.root">
<div class="_gaps_s">
<div :class="$style.totpIcon">
<i class="ti ti-key"></i>
</div>
<div :class="$style.totpDescription">{{ i18n.ts['2fa'] }}</div>
</div>
<!-- totp入力 -->
<form class="_gaps_s" @submit.prevent="emit('totpSubmitted', token)">
<MkInput v-model="token" type="text" :pattern="isBackupCode ? '^[A-Z0-9]{32}$' :'^[0-9]{6}$'" autocomplete="one-time-code" required autofocus :spellcheck="false" :inputmode="isBackupCode ? undefined : 'numeric'">
<template #label>{{ i18n.ts.token }} ({{ i18n.ts['2fa'] }})</template>
<template #prefix><i v-if="isBackupCode" class="ti ti-key"></i><i v-else class="ti ti-123"></i></template>
<template #caption><button class="_textButton" type="button" @click="isBackupCode = !isBackupCode">{{ isBackupCode ? i18n.ts.useTotp : i18n.ts.useBackupCode }}</button></template>
</MkInput>
<MkButton type="submit" large primary rounded style="margin: 0 auto;">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
</form>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
const emit = defineEmits<{
(ev: 'totpSubmitted', token: string): void;
}>();
const token = ref('');
const isBackupCode = ref(false);
</script>
<style lang="scss" module>
.wrapper {
display: flex;
align-items: center;
width: 100%;
min-height: 336px;
> .root {
width: 100%;
}
}
.totpIcon {
margin: 0 auto;
background-color: var(--MI_THEME-accentedBg);
color: var(--MI_THEME-accent);
text-align: center;
height: 64px;
width: 64px;
font-size: 24px;
line-height: 64px;
border-radius: 50%;
}
.totpDescription {
text-align: center;
font-size: 1.1em;
}
</style>

View file

@ -4,245 +4,290 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<form :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
<div class="_gaps_m">
<div v-show="withAvatar" :class="$style.avatar" :style="{ backgroundImage: user ? `url('${user.avatarUrl}')` : undefined, marginBottom: message ? '1.5em' : undefined }"></div>
<MkInfo v-if="message">
{{ message }}
</MkInfo>
<div v-if="openOnRemote" class="_gaps_m">
<div class="_gaps_s">
<MkButton type="button" rounded primary style="margin: 0 auto;" @click="openRemote(openOnRemote)">
{{ i18n.ts.continueOnRemote }} <i class="ti ti-external-link"></i>
</MkButton>
<button type="button" class="_button" :class="$style.instanceManualSelectButton" @click="specifyHostAndOpenRemote(openOnRemote)">
{{ i18n.ts.specifyServerHost }}
</button>
</div>
<div :class="$style.orHr">
<p :class="$style.orMsg">{{ i18n.ts.or }}</p>
</div>
</div>
<div v-if="!totpLogin" class="normal-signin _gaps_m">
<MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username webauthn" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange">
<template #prefix>@</template>
<template #suffix>@{{ host }}</template>
</MkInput>
<MkInput v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" :withPasswordToggle="true" required data-cy-signin-password>
<template #prefix><i class="ti ti-lock"></i></template>
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
</MkInput>
<MkButton type="submit" large primary rounded :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
</div>
<div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }">
<div v-if="user && user.securityKeys" class="twofa-group tap-group">
<p>{{ i18n.ts.useSecurityKey }}</p>
<MkButton v-if="!queryingKey" @click="query2FaKey">
{{ i18n.ts.retry }}
</MkButton>
</div>
<div v-if="user && user.securityKeys" :class="$style.orHr">
<p :class="$style.orMsg">{{ i18n.ts.or }}</p>
</div>
<div class="twofa-group totp-group _gaps">
<MkInput v-model="token" type="text" :pattern="isBackupCode ? '^[A-Z0-9]{32}$' :'^[0-9]{6}$'" autocomplete="one-time-code" required :spellcheck="false" :inputmode="isBackupCode ? undefined : 'numeric'">
<template #label>{{ i18n.ts.token }} ({{ i18n.ts['2fa'] }})</template>
<template #prefix><i v-if="isBackupCode" class="ti ti-key"></i><i v-else class="ti ti-123"></i></template>
<template #caption><button class="_textButton" type="button" @click="isBackupCode = !isBackupCode">{{ isBackupCode ? i18n.ts.useTotp : i18n.ts.useBackupCode }}</button></template>
</MkInput>
<MkButton type="submit" :disabled="signing" large primary rounded style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
</div>
</div>
<div v-if="!totpLogin && usePasswordLessLogin" :class="$style.orHr">
<p :class="$style.orMsg">{{ i18n.ts.or }}</p>
</div>
<div v-if="!totpLogin && usePasswordLessLogin" class="twofa-group tap-group">
<MkButton v-if="!queryingKey" type="submit" :disabled="signing" style="margin: auto auto;" rounded large primary @click="onPasskeyLogin">
<i class="ti ti-device-usb" style="font-size: medium;"></i>
{{ signing ? i18n.ts.loggingIn : i18n.ts.signinWithPasskey }}
</MkButton>
<p v-if="queryingKey">{{ i18n.ts.useSecurityKey }}</p>
</div>
<div :class="$style.signinRoot">
<Transition
mode="out-in"
:enterActiveClass="$style.transition_enterActive"
:leaveActiveClass="$style.transition_leaveActive"
:enterFromClass="$style.transition_enterFrom"
:leaveToClass="$style.transition_leaveTo"
:inert="waiting"
>
<!-- 1. 外部サーバーへの転送username入力パスキー -->
<XInput
v-if="page === 'input'"
key="input"
:message="message"
:openOnRemote="openOnRemote"
@usernameSubmitted="onUsernameSubmitted"
@passkeyClick="onPasskeyLogin"
/>
<!-- 2. パスワード入力 -->
<XPassword
v-else-if="page === 'password'"
key="password"
ref="passwordPageEl"
:user="userInfo!"
:needCaptcha="needCaptcha"
@passwordSubmitted="onPasswordSubmitted"
/>
<!-- 3. ワンタイムパスワード -->
<XTotp
v-else-if="page === 'totp'"
key="totp"
@totpSubmitted="onTotpSubmitted"
/>
<!-- 4. パスキー -->
<XPasskey
v-else-if="page === 'passkey'"
key="passkey"
:credentialRequest="credentialRequest!"
:isPerformingPasswordlessLogin="doingPasskeyFromInputPage"
@done="onPasskeyDone"
@useTotp="onUseTotp"
/>
</Transition>
<div v-if="waiting" :class="$style.waitingRoot">
<MkLoading/>
</div>
</form>
</div>
</template>
<script lang="ts" setup>
import { defineAsyncComponent, ref } from 'vue';
import { toUnicode } from 'punycode/';
<script setup lang="ts">
import { nextTick, onBeforeUnmount, ref, shallowRef, useTemplateRef } from 'vue';
import * as Misskey from 'misskey-js';
import { supported as webAuthnSupported, get as webAuthnRequest, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
import { SigninWithPasskeyResponse } from 'misskey-js/entities.js';
import { query, extractDomain } from '@@/js/url.js';
import { host as configHost } from '@@/js/config.js';
import MkDivider from './MkDivider.vue';
import { supported as webAuthnSupported, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill';
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkInfo from '@/components/MkInfo.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
import { login } from '@/account.js';
import { i18n } from '@/i18n.js';
import { showSystemAccountDialog } from '@/scripts/show-system-account-dialog.js';
import * as os from '@/os.js';
const signing = ref(false);
const user = ref<Misskey.entities.UserDetailed | null>(null);
const usePasswordLessLogin = ref<Misskey.entities.UserDetailed['usePasswordLessLogin']>(true);
const username = ref('');
const password = ref('');
const token = ref('');
const host = ref(toUnicode(configHost));
const totpLogin = ref(false);
const isBackupCode = ref(false);
const queryingKey = ref(false);
let credentialRequest: CredentialRequestOptions | null = null;
const passkey_context = ref('');
import XInput from '@/components/MkSignin.input.vue';
import XPassword, { type PwResponse } from '@/components/MkSignin.password.vue';
import XTotp from '@/components/MkSignin.totp.vue';
import XPasskey from '@/components/MkSignin.passkey.vue';
const emit = defineEmits<{
(ev: 'login', v: any): void;
(ev: 'login', v: Misskey.entities.SigninFlowResponse & { finished: true }): void;
}>();
const props = withDefaults(defineProps<{
withAvatar?: boolean;
autoSet?: boolean;
message?: string,
openOnRemote?: OpenOnRemoteOptions,
}>(), {
withAvatar: true,
autoSet: false,
message: '',
openOnRemote: undefined,
});
function onUsernameChange(): void {
const usernameRequested = username.value;
misskeyApi('users/show', {
username: usernameRequested,
}).then(userResponse => {
if (userResponse.username === username.value) {
user.value = userResponse;
usePasswordLessLogin.value = userResponse.usePasswordLessLogin;
}
}, () => {
if (usernameRequested === username.value) {
user.value = null;
usePasswordLessLogin.value = true;
}
const page = ref<'input' | 'password' | 'totp' | 'passkey'>('input');
const waiting = ref(false);
const passwordPageEl = useTemplateRef('passwordPageEl');
const needCaptcha = ref(false);
const userInfo = ref<null | Misskey.entities.UserDetailed>(null);
const password = ref('');
//#region Passkey Passwordless
const credentialRequest = shallowRef<CredentialRequestOptions | null>(null);
const passkeyContext = ref('');
const doingPasskeyFromInputPage = ref(false);
function onPasskeyLogin(): void {
if (webAuthnSupported()) {
doingPasskeyFromInputPage.value = true;
waiting.value = true;
misskeyApi('signin-with-passkey', {})
.then((res) => {
passkeyContext.value = res.context ?? '';
credentialRequest.value = parseRequestOptionsFromJSON({
publicKey: res.option,
});
page.value = 'passkey';
waiting.value = false;
})
.catch(onSigninApiError);
}
}
function onPasskeyDone(credential: AuthenticationPublicKeyCredential): void {
waiting.value = true;
if (doingPasskeyFromInputPage.value) {
misskeyApi('signin-with-passkey', {
credential: credential.toJSON(),
context: passkeyContext.value,
}).then((res) => {
if (res.signinResponse == null) {
onSigninApiError();
return;
}
emit('login', res.signinResponse);
}).catch(onSigninApiError);
} else if (userInfo.value != null) {
tryLogin({
username: userInfo.value.username,
password: password.value,
credential: credential.toJSON(),
});
}
}
function onUseTotp(): void {
page.value = 'totp';
}
//#endregion
async function onUsernameSubmitted(username: string) {
waiting.value = true;
userInfo.value = await misskeyApi('users/show', {
username,
}).catch(() => null);
await tryLogin({
username,
});
}
function onLogin(res: any): Promise<void> | void {
if (props.autoSet) {
return login(res.i);
}
}
async function onPasswordSubmitted(pw: PwResponse) {
waiting.value = true;
password.value = pw.password;
async function query2FaKey(): Promise<void> {
if (credentialRequest == null) return;
queryingKey.value = true;
await webAuthnRequest(credentialRequest)
.catch(() => {
queryingKey.value = false;
return Promise.reject(null);
}).then(credential => {
credentialRequest = null;
queryingKey.value = false;
signing.value = true;
return misskeyApi('signin', {
username: username.value,
password: password.value,
credential: credential.toJSON(),
});
}).then(res => {
emit('login', res);
return onLogin(res);
}).catch(err => {
if (err === null) return;
os.alert({
type: 'error',
text: i18n.ts.signinFailed,
});
signing.value = false;
if (userInfo.value == null) {
await os.alert({
type: 'error',
title: i18n.ts.noSuchUser,
text: i18n.ts.signinFailed,
});
}
function onPasskeyLogin(): void {
signing.value = true;
if (webAuthnSupported()) {
misskeyApi('signin-with-passkey', {})
.then((res: SigninWithPasskeyResponse) => {
totpLogin.value = false;
signing.value = false;
queryingKey.value = true;
passkey_context.value = res.context ?? '';
credentialRequest = parseRequestOptionsFromJSON({
publicKey: res.option,
});
})
.then(() => queryPasskey())
.catch(loginFailed);
}
}
async function queryPasskey(): Promise<void> {
if (credentialRequest == null) return;
queryingKey.value = true;
console.log('Waiting passkey auth...');
await webAuthnRequest(credentialRequest)
.catch((err) => {
console.warn('Passkey Auth fail!: ', err);
queryingKey.value = false;
return Promise.reject(null);
}).then(credential => {
credentialRequest = null;
queryingKey.value = false;
signing.value = true;
return misskeyApi('signin-with-passkey', {
credential: credential.toJSON(),
context: passkey_context.value,
});
}).then((res: SigninWithPasskeyResponse) => {
emit('login', res.signinResponse);
return onLogin(res.signinResponse);
});
}
function onSubmit(): void {
signing.value = true;
if (!totpLogin.value && user.value && user.value.twoFactorEnabled) {
if (webAuthnSupported() && user.value.securityKeys) {
misskeyApi('signin', {
username: username.value,
password: password.value,
}).then(res => {
totpLogin.value = true;
signing.value = false;
credentialRequest = parseRequestOptionsFromJSON({
publicKey: res,
});
})
.then(() => query2FaKey())
.catch(loginFailed);
} else {
totpLogin.value = true;
signing.value = false;
}
waiting.value = false;
return;
} else {
misskeyApi('signin', {
username: username.value,
password: password.value,
token: user.value?.twoFactorEnabled ? token.value : undefined,
}).then(res => {
emit('login', res);
onLogin(res);
}).catch(loginFailed);
await tryLogin({
username: userInfo.value.username,
password: pw.password,
'hcaptcha-response': pw.captcha.hCaptchaResponse,
'm-captcha-response': pw.captcha.mCaptchaResponse,
'g-recaptcha-response': pw.captcha.reCaptchaResponse,
'frc-captcha-solution': pw.captcha.fcResponse,
'turnstile-response': pw.captcha.turnstileResponse,
'testcaptcha-response': pw.captcha.testcaptchaResponse,
});
}
}
function loginFailed(err: any): void {
switch (err.id) {
async function onTotpSubmitted(token: string) {
waiting.value = true;
if (userInfo.value == null) {
await os.alert({
type: 'error',
title: i18n.ts.noSuchUser,
text: i18n.ts.signinFailed,
});
waiting.value = false;
return;
} else {
await tryLogin({
username: userInfo.value.username,
password: password.value,
token,
});
}
}
async function tryLogin(req: Partial<Misskey.entities.SigninFlowRequest>): Promise<Misskey.entities.SigninFlowResponse> {
const _req = {
username: req.username ?? userInfo.value?.username,
...req,
};
function assertIsSigninFlowRequest(x: Partial<Misskey.entities.SigninFlowRequest>): x is Misskey.entities.SigninFlowRequest {
return x.username != null;
}
if (!assertIsSigninFlowRequest(_req)) {
throw new Error('Invalid request');
}
return await misskeyApi('signin-flow', _req).then(async (res) => {
if (res.finished) {
emit('login', res);
await onLoginSucceeded(res);
} else {
switch (res.next) {
case 'captcha': {
needCaptcha.value = true;
page.value = 'password';
break;
}
case 'password': {
needCaptcha.value = false;
page.value = 'password';
break;
}
case 'totp': {
page.value = 'totp';
break;
}
case 'passkey': {
if (webAuthnSupported()) {
credentialRequest.value = parseRequestOptionsFromJSON({
publicKey: res.authRequest,
});
page.value = 'passkey';
} else {
page.value = 'totp';
}
break;
}
}
if (doingPasskeyFromInputPage.value === true) {
doingPasskeyFromInputPage.value = false;
page.value = 'input';
password.value = '';
}
passwordPageEl.value?.resetCaptcha();
nextTick(() => {
waiting.value = false;
});
}
return res;
}).catch((err) => {
onSigninApiError(err);
return Promise.reject(err);
});
}
async function onLoginSucceeded(res: Misskey.entities.SigninFlowResponse & { finished: true }) {
if (props.autoSet) {
await login(res.i);
}
}
function onSigninApiError(err?: any): void {
const id = err?.id ?? null;
switch (id) {
case '6cc579cc-885d-43d8-95c2-b8c7fc963280': {
os.alert({
type: 'error',
@ -275,6 +320,14 @@ function loginFailed(err: any): void {
});
break;
}
case 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f': {
os.alert({
type: 'error',
title: i18n.ts.loginFailed,
text: i18n.ts.incorrectTotp,
});
break;
}
case '36b96a7d-b547-412d-aeed-2d611cdc8cdc': {
os.alert({
type: 'error',
@ -283,6 +336,14 @@ function loginFailed(err: any): void {
});
break;
}
case '93b86c4b-72f9-40eb-9815-798928603d1e': {
os.alert({
type: 'error',
title: i18n.ts.loginFailed,
text: i18n.ts.passkeyVerificationFailed,
});
break;
}
case 'b18c89a7-5b5e-4cec-bb5b-0419f332d430': {
os.alert({
type: 'error',
@ -309,113 +370,55 @@ function loginFailed(err: any): void {
}
}
totpLogin.value = false;
signing.value = false;
}
function resetPassword(): void {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkForgotPassword.vue')), {}, {
closed: () => dispose(),
if (doingPasskeyFromInputPage.value === true) {
doingPasskeyFromInputPage.value = false;
page.value = 'input';
password.value = '';
}
passwordPageEl.value?.resetCaptcha();
nextTick(() => {
waiting.value = false;
});
}
function openRemote(options: OpenOnRemoteOptions, targetHost?: string): void {
switch (options.type) {
case 'web':
case 'lookup': {
let _path: string;
if (options.type === 'lookup') {
// TODO: v2024.7.0URL
// _path = `/lookup?uri=${encodeURIComponent(_path)}`;
_path = `/authorize-follow?acct=${encodeURIComponent(options.url)}`;
} else {
_path = options.path;
}
if (targetHost) {
window.open(`https://${targetHost}${_path}`, '_blank', 'noopener');
} else {
window.open(`https://misskey-hub.net/mi-web/?path=${encodeURIComponent(_path)}`, '_blank', 'noopener');
}
break;
}
case 'share': {
const params = query(options.params);
if (targetHost) {
window.open(`https://${targetHost}/share?${params}`, '_blank', 'noopener');
} else {
window.open(`https://misskey-hub.net/share/?${params}`, '_blank', 'noopener');
}
break;
}
}
}
async function specifyHostAndOpenRemote(options: OpenOnRemoteOptions): Promise<void> {
const { canceled, result: hostTemp } = await os.inputText({
title: i18n.ts.inputHostName,
placeholder: 'misskey.example.com',
});
if (canceled) return;
let targetHost: string | null = hostTemp;
//
targetHost = extractDomain(targetHost);
if (targetHost == null) {
os.alert({
type: 'error',
title: i18n.ts.invalidValue,
text: i18n.ts.tryAgain,
});
return;
}
openRemote(options, targetHost);
}
onBeforeUnmount(() => {
password.value = '';
needCaptcha.value = false;
userInfo.value = null;
});
</script>
<style lang="scss" module>
.avatar {
margin: 0 auto 0 auto;
width: 64px;
height: 64px;
background: #ddd;
background-position: center;
background-size: cover;
border-radius: var(--radius-full);
.transition_enterActive,
.transition_leaveActive {
transition: opacity 0.3s cubic-bezier(0,0,.35,1), transform 0.3s cubic-bezier(0,0,.35,1);
}
.transition_enterFrom {
opacity: 0;
transform: translateX(50px);
}
.transition_leaveTo {
opacity: 0;
transform: translateX(-50px);
}
.instanceManualSelectButton {
display: block;
text-align: center;
opacity: .7;
font-size: .8em;
.signinRoot {
overflow-x: hidden;
overflow-x: clip;
&:hover {
text-decoration: underline;
}
}
.orHr {
position: relative;
margin: .4em auto;
width: 100%;
height: 1px;
background: var(--divider);
}
.orMsg {
.waitingRoot {
position: absolute;
top: -.6em;
display: inline-block;
padding: 0 1em;
background: var(--panel);
font-size: 0.8em;
color: var(--fgOnPanel);
margin: 0;
left: 50%;
transform: translateX(-50%);
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: color-mix(in srgb, var(--MI_THEME-panel), transparent 50%);
display: flex;
justify-content: center;
align-items: center;
z-index: 1;
}
</style>

View file

@ -4,26 +4,30 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModalWindow
ref="dialog"
:width="400"
:height="450"
@close="onClose"
<MkModal
ref="modal"
:preferType="'dialog'"
@click="onClose"
@closed="emit('closed')"
>
<template #header>{{ i18n.ts.login }}</template>
<MkSpacer :marginMin="20" :marginMax="28">
<MkSignin :autoSet="autoSet" :message="message" :openOnRemote="openOnRemote" @login="onLogin"/>
</MkSpacer>
</MkModalWindow>
<div :class="$style.root">
<div :class="$style.header">
<div :class="$style.headerText"><i class="ti ti-login-2"></i> {{ i18n.ts.login }}</div>
<button :class="$style.closeButton" class="_button" @click="onClose"><i class="ti ti-x"></i></button>
</div>
<div :class="$style.content">
<MkSignin :autoSet="autoSet" :message="message" :openOnRemote="openOnRemote" @login="onLogin"/>
</div>
</div>
</MkModal>
</template>
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
import { shallowRef } from 'vue';
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
import MkSignin from '@/components/MkSignin.vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
import MkModal from '@/components/MkModal.vue';
import { i18n } from '@/i18n.js';
withDefaults(defineProps<{
@ -37,20 +41,67 @@ withDefaults(defineProps<{
});
const emit = defineEmits<{
(ev: 'done', v: any): void;
(ev: 'done', v: Misskey.entities.SigninFlowResponse & { finished: true }): void;
(ev: 'closed'): void;
(ev: 'cancelled'): void;
}>();
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
const modal = shallowRef<InstanceType<typeof MkModal>>();
function onClose() {
emit('cancelled');
if (dialog.value) dialog.value.close();
if (modal.value) modal.value.close();
}
function onLogin(res) {
function onLogin(res: Misskey.entities.SigninFlowResponse & { finished: true }) {
emit('done', res);
if (dialog.value) dialog.value.close();
if (modal.value) modal.value.close();
}
</script>
<style lang="scss" module>
.root {
overflow: auto;
margin: auto;
position: relative;
width: 100%;
max-width: 400px;
height: 100%;
max-height: 450px;
box-sizing: border-box;
background: var(--MI_THEME-panel);
border-radius: var(--MI-radius);
}
.header {
position: sticky;
top: 0;
left: 0;
width: 100%;
height: 50px;
box-sizing: border-box;
display: flex;
align-items: center;
font-weight: bold;
backdrop-filter: var(--MI-blur, blur(15px));
background: var(--MI_THEME-acrylicBg);
z-index: 1;
}
.headerText {
padding: 0 20px;
box-sizing: border-box;
}
.closeButton {
margin-left: auto;
padding: 16px;
font-size: 16px;
line-height: 16px;
}
.content {
padding: 32px;
box-sizing: border-box;
}
</style>

View file

@ -21,12 +21,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #caption>
<div><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.cannotBeChangedLater }}</div>
<span v-if="usernameState === 'wait'" style="color:#999"><MkLoading :em="true"/> {{ i18n.ts.checking }}</span>
<span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span>
<span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span>
<span v-else-if="usernameState === 'error'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span>
<span v-else-if="usernameState === 'invalid-format'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.usernameInvalidFormat }}</span>
<span v-else-if="usernameState === 'min-range'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooShort }}</span>
<span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooLong }}</span>
<span v-else-if="usernameState === 'ok'" style="color: var(--MI_THEME-success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span>
<span v-else-if="usernameState === 'unavailable'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span>
<span v-else-if="usernameState === 'error'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span>
<span v-else-if="usernameState === 'invalid-format'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.usernameInvalidFormat }}</span>
<span v-else-if="usernameState === 'min-range'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooShort }}</span>
<span v-else-if="usernameState === 'max-range'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooLong }}</span>
</template>
</MkInput>
<MkInput v-if="instance.emailRequiredForSignup" v-model="email" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail">
@ -34,32 +34,32 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #prefix><i class="ti ti-mail"></i></template>
<template #caption>
<span v-if="emailState === 'wait'" style="color:#999"><MkLoading :em="true"/> {{ i18n.ts.checking }}</span>
<span v-else-if="emailState === 'ok'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span>
<span v-else-if="emailState === 'unavailable:used'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.used }}</span>
<span v-else-if="emailState === 'unavailable:format'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.format }}</span>
<span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.disposable }}</span>
<span v-else-if="emailState === 'unavailable:banned'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.banned }}</span>
<span v-else-if="emailState === 'unavailable:mx'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.mx }}</span>
<span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.smtp }}</span>
<span v-else-if="emailState === 'unavailable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span>
<span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span>
<span v-else-if="emailState === 'ok'" style="color: var(--MI_THEME-success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span>
<span v-else-if="emailState === 'unavailable:used'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.used }}</span>
<span v-else-if="emailState === 'unavailable:format'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.format }}</span>
<span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.disposable }}</span>
<span v-else-if="emailState === 'unavailable:banned'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.banned }}</span>
<span v-else-if="emailState === 'unavailable:mx'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.mx }}</span>
<span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.smtp }}</span>
<span v-else-if="emailState === 'unavailable'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span>
<span v-else-if="emailState === 'error'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span>
</template>
</MkInput>
<MkInput v-model="password" type="password" autocomplete="new-password" required data-cy-signup-password @update:modelValue="onChangePassword">
<template #label>{{ i18n.ts.password }}</template>
<template #prefix><i class="ti ti-lock"></i></template>
<template #caption>
<span v-if="passwordStrength == 'low'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.weakPassword }}</span>
<span v-if="passwordStrength == 'medium'" style="color: var(--warn)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.normalPassword }}</span>
<span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.strongPassword }}</span>
<span v-if="passwordStrength == 'low'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.weakPassword }}</span>
<span v-if="passwordStrength == 'medium'" style="color: var(--MI_THEME-warn)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.normalPassword }}</span>
<span v-if="passwordStrength == 'high'" style="color: var(--MI_THEME-success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.strongPassword }}</span>
</template>
</MkInput>
<MkInput v-model="retypedPassword" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype">
<template #label>{{ i18n.ts.password }} ({{ i18n.ts.retype }})</template>
<template #prefix><i class="ti ti-lock"></i></template>
<template #caption>
<span v-if="passwordRetypeState == 'match'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.passwordMatched }}</span>
<span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.passwordNotMatched }}</span>
<span v-if="passwordRetypeState == 'match'" style="color: var(--MI_THEME-success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.passwordMatched }}</span>
<span v-if="passwordRetypeState == 'not-match'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.passwordNotMatched }}</span>
</template>
</MkInput>
<MkInput v-if="instance.approvalRequiredForSignup" v-model="reason" type="text" :spellcheck="false" required data-cy-signup-reason>
@ -71,6 +71,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
<MkCaptcha v-if="instance.enableFC" ref="fc" v-model="fcResponse" :class="$style.captcha" provider="fc" :sitekey="instance.fcSiteKey"/>
<MkCaptcha v-if="instance.enableTestcaptcha" ref="testcaptcha" v-model="testcaptchaResponse" :class="$style.captcha" provider="testcaptcha"/>
<MkButton type="submit" :disabled="shouldDisableSubmitting" large gradate rounded data-cy-signup-submit style="margin: 0 auto;">
<template v-if="submitting">
<MkLoading :em="true" :colored="false"/>
@ -86,10 +87,10 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref, computed } from 'vue';
import { toUnicode } from 'punycode/';
import * as Misskey from 'misskey-js';
import * as config from '@@/js/config.js';
import MkButton from './MkButton.vue';
import MkInput from './MkInput.vue';
import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue';
import * as config from '@@/js/config.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { login } from '@/account.js';
@ -103,7 +104,7 @@ const props = withDefaults(defineProps<{
});
const emit = defineEmits<{
(ev: 'signup', user: Misskey.entities.SigninResponse): void;
(ev: 'signup', user: Misskey.entities.SignupResponse): void;
(ev: 'signupEmailPending'): void;
(ev: 'approvalPending'): void;
}>();
@ -111,9 +112,11 @@ const emit = defineEmits<{
const host = toUnicode(config.host);
const hcaptcha = ref<Captcha | undefined>();
const mcaptcha = ref<Captcha | undefined>();
const recaptcha = ref<Captcha | undefined>();
const turnstile = ref<Captcha | undefined>();
const fc = ref<Captcha | undefined>();
const testcaptcha = ref<Captcha | undefined>();
const username = ref<string>('');
const password = ref<string>('');
@ -131,6 +134,7 @@ const mCaptchaResponse = ref<string | null>(null);
const reCaptchaResponse = ref<string | null>(null);
const turnstileResponse = ref<string | null>(null);
const fcResponse = ref<string | null>(null);
const testcaptchaResponse = ref<string | null>(null);
const usernameAbortController = ref<null | AbortController>(null);
const emailAbortController = ref<null | AbortController>(null);
@ -141,6 +145,7 @@ const shouldDisableSubmitting = computed((): boolean => {
instance.enableRecaptcha && !reCaptchaResponse.value ||
instance.enableTurnstile && !turnstileResponse.value ||
instance.enableFC && !fcResponse.value ||
instance.enableTestcaptcha && !testcaptchaResponse.value ||
instance.emailRequiredForSignup && emailState.value !== 'ok' ||
usernameState.value !== 'ok' ||
passwordRetypeState.value !== 'match';
@ -259,20 +264,33 @@ async function onSubmit(): Promise<void> {
if (submitting.value) return;
submitting.value = true;
try {
await misskeyApi('signup', {
username: username.value,
password: password.value,
emailAddress: email.value,
invitationCode: invitationCode.value,
reason: reason.value,
'hcaptcha-response': hCaptchaResponse.value,
'm-captcha-response': mCaptchaResponse.value,
'g-recaptcha-response': reCaptchaResponse.value,
'turnstile-response': turnstileResponse.value,
'frc-captcha-solution': fcResponse.value,
});
if (instance.emailRequiredForSignup) {
const signupPayload: Misskey.entities.SignupRequest = {
username: username.value,
password: password.value,
emailAddress: email.value,
invitationCode: invitationCode.value,
reason: reason.value,
'hcaptcha-response': hCaptchaResponse.value,
'm-captcha-response': mCaptchaResponse.value,
'g-recaptcha-response': reCaptchaResponse.value,
'turnstile-response': turnstileResponse.value,
'frc-captcha-solution': fcResponse.value,
'testcaptcha-response': testcaptchaResponse.value,
};
const res = await fetch(`${config.apiUrl}/signup`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(signupPayload),
}).catch(() => {
onSignupApiError();
return null;
});
if (res) {
if (res.status === 204 || instance.emailRequiredForSignup) {
os.alert({
type: 'success',
title: i18n.ts._signup.almostThere,
@ -287,28 +305,33 @@ async function onSubmit(): Promise<void> {
});
emit('approvalPending');
} else {
const res = await misskeyApi('signin', {
username: username.value,
password: password.value,
});
emit('signup', res);
const resJson = (await res.json()) as Misskey.entities.SignupResponse;
if (_DEV_) console.log(resJson);
emit('signup', resJson);
if (props.autoSet) {
return login(res.i);
await login(resJson.token);
}
}
} catch {
submitting.value = false;
hcaptcha.value?.reset?.();
recaptcha.value?.reset?.();
turnstile.value?.reset?.();
fc.value?.reset?.();
os.alert({
type: 'error',
text: i18n.ts.somethingHappened,
});
}
submitting.value = false;
}
function onSignupApiError() {
submitting.value = false;
hcaptcha.value?.reset?.();
mcaptcha.value?.reset?.();
recaptcha.value?.reset?.();
turnstile.value?.reset?.();
fc.value?.reset?.();
testcaptcha.value?.reset?.();
os.alert({
type: 'error',
text: i18n.ts.somethingHappened,
});
}
</script>
@ -317,8 +340,8 @@ async function onSubmit(): Promise<void> {
padding: 16px;
text-align: center;
font-size: 26px;
background-color: var(--accentedBg);
color: var(--accent);
background-color: var(--MI_THEME-accentedBg);
color: var(--MI_THEME-accent);
}
.captcha {

View file

@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder v-if="availableServerRules" :defaultOpen="true">
<template #label>{{ i18n.ts.serverRules }}</template>
<template #suffix><i v-if="agreeServerRules" class="ti ti-check" style="color: var(--success)"></i></template>
<template #suffix><i v-if="agreeServerRules" class="ti ti-check" style="color: var(--MI_THEME-success)"></i></template>
<ol class="_gaps_s" :class="$style.rules">
<li v-for="item in instance.serverRules" :class="$style.rule"><div :class="$style.ruleText" v-html="sanitizeHtml(item)"></div></li>
@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder v-if="availableTos || availablePrivacyPolicy" :defaultOpen="true">
<template #label>{{ tosPrivacyPolicyLabel }}</template>
<template #suffix><i v-if="agreeTosAndPrivacyPolicy" class="ti ti-check" style="color: var(--success)"></i></template>
<template #suffix><i v-if="agreeTosAndPrivacyPolicy" class="ti ti-check" style="color: var(--MI_THEME-success)"></i></template>
<div class="_gaps_s">
<div v-if="availableTos"><a :href="instance.tosUrl ?? undefined" class="_link" target="_blank">{{ i18n.ts.termsOfService }} <i class="ti ti-external-link"></i></a></div>
<div v-if="availablePrivacyPolicy"><a :href="instance.privacyPolicyUrl ?? undefined" class="_link" target="_blank">{{ i18n.ts.privacyPolicy }} <i class="ti ti-external-link"></i></a></div>
@ -43,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder :defaultOpen="true">
<template #label>{{ i18n.ts.basicNotesBeforeCreateAccount }}</template>
<template #suffix><i v-if="agreeNote" class="ti ti-check" style="color: var(--success)"></i></template>
<template #suffix><i v-if="agreeNote" class="ti ti-check" style="color: var(--MI_THEME-success)"></i></template>
<a href="https://activitypub.software/TransFem-org/Sharkey/-/blob/stable/IMPORTANT_NOTES.md" class="_link" target="_blank">{{ i18n.ts.basicNotesBeforeCreateAccount }} <i class="ti ti-external-link"></i></a>
@ -151,8 +151,8 @@ async function updateAgreeNote(v: boolean) {
padding: 16px;
text-align: center;
font-size: 26px;
background-color: var(--accentedBg);
color: var(--accent);
background-color: var(--MI_THEME-accentedBg);
color: var(--MI_THEME-accent);
}
.rules {
@ -171,14 +171,14 @@ async function updateAgreeNote(v: boolean) {
flex-shrink: 0;
display: flex;
position: sticky;
top: calc(var(--stickyTop, 0px) + 8px);
top: calc(var(--MI-stickyTop, 0px) + 8px);
counter-increment: item;
content: counter(item);
width: 32px;
height: 32px;
line-height: 32px;
background-color: var(--accentedBg);
color: var(--accent);
background-color: var(--MI_THEME-accentedBg);
color: var(--MI_THEME-accent);
font-size: 13px;
font-weight: bold;
align-items: center;

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