Merge branch 'develop' into deps-update-node

This commit is contained in:
syuilo 2026-01-14 16:13:10 +09:00 committed by GitHub
commit 8e0c38324d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
65 changed files with 773 additions and 530 deletions

View File

@ -25,11 +25,13 @@ async function measureMemory() {
const startTime = Date.now();
// Start the Misskey backend server using fork to enable IPC
const serverProcess = fork(join(__dirname, '../built/boot/entry.js'), [], {
const serverProcess = fork(join(__dirname, '../built/boot/entry.js'), ['expose-gc'], {
cwd: join(__dirname, '..'),
env: {
...process.env,
NODE_ENV: 'test',
NODE_ENV: 'production',
MK_DISABLE_CLUSTERING: '1',
MK_FORCE_GC: '1',
},
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
});

View File

@ -86,6 +86,10 @@ if (!envOption.disableClustering) {
ev.mount();
}
if (envOption.forceGc && global.gc != null) {
global.gc();
}
readyRef.value = true;
// ユニットテスト時にMisskeyが子プロセスで起動された時のため

View File

@ -14,6 +14,10 @@ import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
import { UserEntityService } from './UserEntityService.js';
function assertBw(bw: string): bw is Packed<'ReversiGameDetailed'>['bw'] {
return ['random', '1', '2'].includes(bw);
}
@Injectable()
export class ReversiGameEntityService {
constructor(
@ -58,7 +62,7 @@ export class ReversiGameEntityService {
surrenderedUserId: game.surrenderedUserId,
timeoutUserId: game.timeoutUserId,
black: game.black,
bw: game.bw,
bw: assertBw(game.bw) ? game.bw : 'random',
isLlotheo: game.isLlotheo,
canPutEverywhere: game.canPutEverywhere,
loopedBoard: game.loopedBoard,
@ -116,7 +120,7 @@ export class ReversiGameEntityService {
surrenderedUserId: game.surrenderedUserId,
timeoutUserId: game.timeoutUserId,
black: game.black,
bw: game.bw,
bw: assertBw(game.bw) ? game.bw : 'random',
isLlotheo: game.isLlotheo,
canPutEverywhere: game.canPutEverywhere,
loopedBoard: game.loopedBoard,

View File

@ -11,6 +11,7 @@ const envOption = {
verbose: false,
withLogTime: false,
quiet: false,
forceGc: false,
};
for (const key of Object.keys(envOption) as (keyof typeof envOption)[]) {

View File

@ -64,6 +64,7 @@ import {
packedMetaDetailedOnlySchema,
packedMetaDetailedSchema,
packedMetaLiteSchema,
packedMetaClientOptionsSchema,
} from '@/models/json-schema/meta.js';
import { packedUserWebhookSchema } from '@/models/json-schema/user-webhook.js';
import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js';
@ -135,6 +136,7 @@ export const refs = {
MetaLite: packedMetaLiteSchema,
MetaDetailedOnly: packedMetaDetailedOnlySchema,
MetaDetailed: packedMetaDetailedSchema,
MetaClientOptions: packedMetaClientOptionsSchema,
UserWebhook: packedUserWebhookSchema,
SystemWebhook: packedSystemWebhookSchema,
AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema,

View File

@ -725,7 +725,11 @@ export class MiMeta {
@Column('jsonb', {
default: { },
})
public clientOptions: Record<string, any>;
public clientOptions: {
entrancePageStyle: 'classic' | 'simple';
showTimelineForVisitor: boolean;
showActivitiesForVisitor: boolean;
};
}
export type SoftwareSuspension = {

View File

@ -72,8 +72,7 @@ export const packedMetaLiteSchema = {
optional: false, nullable: true,
},
clientOptions: {
type: 'object',
optional: false, nullable: false,
ref: 'MetaClientOptions',
},
disableRegistration: {
type: 'boolean',
@ -397,3 +396,23 @@ export const packedMetaDetailedSchema = {
},
],
} as const;
export const packedMetaClientOptionsSchema = {
type: 'object',
optional: false, nullable: false,
properties: {
entrancePageStyle: {
type: 'string',
enum: ['classic', 'simple'],
optional: false, nullable: false,
},
showTimelineForVisitor: {
type: 'boolean',
optional: false, nullable: false,
},
showActivitiesForVisitor: {
type: 'boolean',
optional: false, nullable: false,
},
},
} as const;

View File

@ -81,6 +81,7 @@ export const packedReversiGameLiteSchema = {
bw: {
type: 'string',
optional: false, nullable: false,
enum: ['random', '1', '2'],
},
noIrregularRules: {
type: 'boolean',
@ -199,6 +200,7 @@ export const packedReversiGameDetailedSchema = {
bw: {
type: 'string',
optional: false, nullable: false,
enum: ['random', '1', '2'],
},
noIrregularRules: {
type: 'boolean',

View File

@ -428,8 +428,7 @@ export const meta = {
optional: false, nullable: true,
},
clientOptions: {
type: 'object',
optional: false, nullable: false,
ref: 'MetaClientOptions',
},
description: {
type: 'string',

View File

@ -3,7 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Injectable, Inject } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { MiMeta } from '@/models/Meta.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
@ -67,7 +68,14 @@ export const paramDef = {
description: { type: 'string', nullable: true },
defaultLightTheme: { type: 'string', nullable: true },
defaultDarkTheme: { type: 'string', nullable: true },
clientOptions: { type: 'object', nullable: false },
clientOptions: {
type: 'object', nullable: false,
properties: {
entrancePageStyle: { type: 'string', nullable: false, enum: ['classic', 'simple'] },
showTimelineForVisitor: { type: 'boolean', nullable: false },
showActivitiesForVisitor: { type: 'boolean', nullable: false },
},
},
cacheRemoteFiles: { type: 'boolean' },
cacheRemoteSensitiveFiles: { type: 'boolean' },
emailRequiredForSignup: { type: 'boolean' },
@ -217,6 +225,9 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.meta)
private serverSettings: MiMeta,
private metaService: MetaService,
private moderationLogService: ModerationLogService,
) {
@ -329,7 +340,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
if (ps.clientOptions !== undefined) {
set.clientOptions = ps.clientOptions;
set.clientOptions = {
...serverSettings.clientOptions,
...ps.clientOptions,
};
}
if (ps.cacheRemoteFiles !== undefined) {

View File

@ -23,13 +23,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div :class="$style.body">
<div :class="$style.header">
<span :class="$style.title">{{ (i18n.ts._achievements._types as any)['_' + achievement.name].title }}</span>
<span :class="$style.title">{{ i18n.ts._achievements._types[`_${achievement.name}`].title }}</span>
<span :class="$style.time">
<time v-tooltip="new Date(achievement.unlockedAt).toLocaleString()">{{ new Date(achievement.unlockedAt).getFullYear() }}/{{ new Date(achievement.unlockedAt).getMonth() + 1 }}/{{ new Date(achievement.unlockedAt).getDate() }}</time>
</span>
</div>
<div :class="$style.description">{{ withDescription ? (i18n.ts._achievements._types as any)['_' + achievement.name].description : '???' }}</div>
<div v-if="(i18n.ts._achievements._types as any)['_' + achievement.name].flavor && withDescription" :class="$style.flavor">{{ (i18n.ts._achievements._types as any)['_' + achievement.name].flavor }}</div>
<div :class="$style.description">{{ withDescription ? i18n.ts._achievements._types[`_${achievement.name}`].description : '???' }}</div>
<div v-if="'flavor' in i18n.ts._achievements._types[`_${achievement.name}`] && withDescription" :class="$style.flavor">{{ (i18n.ts._achievements._types[`_${achievement.name}`] as { flavor: string; }).flavor }}</div>
</div>
</div>
<template v-if="withLocked">

View File

@ -45,6 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts">
import { markRaw, ref, useTemplateRef, computed, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
import * as Misskey from 'misskey-js';
import sanitizeHtml from 'sanitize-html';
import { emojilist, getEmojiName } from '@@/js/emojilist.js';
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@@/js/emoji-base.js';
@ -63,7 +64,7 @@ import { prefer } from '@/preferences.js';
export type CompleteInfo = {
user: {
payload: any;
payload: Misskey.entities.User;
query: string | null;
},
hashtag: {
@ -185,9 +186,9 @@ const suggests = ref<Element>();
const rootEl = useTemplateRef('rootEl');
const fetching = ref(true);
const users = ref<any[]>([]);
const hashtags = ref<any[]>([]);
const emojis = ref<(EmojiDef)[]>([]);
const users = ref<Misskey.entities.User[]>([]);
const hashtags = ref<string[]>([]);
const emojis = ref<EmojiDef[]>([]);
const items = ref<Element[] | HTMLCollection>([]);
const mfmTags = ref<string[]>([]);
const mfmParams = ref<string[]>([]);
@ -204,8 +205,8 @@ function complete<T extends keyof CompleteInfo>(type: T, value: CompleteInfo[T][
emit('closed');
if (type === 'emoji' || type === 'emojiComplete') {
let recents = store.s.recentlyUsedEmojis;
recents = recents.filter((emoji: any) => emoji !== value);
recents.unshift(value);
recents = recents.filter((emoji) => emoji !== value);
recents.unshift(value as string);
store.set('recentlyUsedEmojis', recents.splice(0, 32));
}
}
@ -254,7 +255,7 @@ function exec() {
limit: 10,
detail: false,
}).then(searchedUsers => {
users.value = searchedUsers as any[];
users.value = searchedUsers;
fetching.value = false;
//
sessionStorage.setItem(cacheKey, JSON.stringify(searchedUsers));
@ -276,7 +277,7 @@ function exec() {
query: props.q,
limit: 30,
}).then(searchedHashtags => {
hashtags.value = searchedHashtags as any[];
hashtags.value = searchedHashtags;
fetching.value = false;
//
sessionStorage.setItem(cacheKey, JSON.stringify(searchedHashtags));

View File

@ -52,7 +52,8 @@ import MkModal from '@/components/MkModal.vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/MkSelect.vue';
import type { MkSelectItem, OptionValue } from '@/components/MkSelect.vue';
import type { MkSelectItem } from '@/components/MkSelect.vue';
import type { OptionValue } from '@/types/option-value.js';
import { useMkSelect } from '@/composables/use-mkselect.js';
import { i18n } from '@/i18n.js';

View File

@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<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 as any)[permission] ?? permission }}</li>
<li v-for="permission in extension.meta.permissions" :key="permission">{{ i18n.ts._permissions[permission] ?? permission }}</li>
</ul>
<template v-else>{{ i18n.ts.none }}</template>
</template>
@ -91,7 +91,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkFolder>
</div>
<slot name="additionalInfo"/>
<slot name="additionalInfo"></slot>
<div class="_buttonsCenter">
<MkButton danger rounded large @click="emits('cancel')"><i class="ti ti-x"></i> {{ i18n.ts.cancel }}</MkButton>
@ -101,6 +101,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts">
import * as Misskey from 'misskey-js';
export type Extension = {
type: 'plugin';
raw: string;
@ -109,7 +111,7 @@ export type Extension = {
version: string;
author: string;
description?: string;
permissions?: string[];
permissions?: (typeof Misskey.permissions)[number][];
config?: Record<string, unknown>;
};
} | {
@ -125,7 +127,6 @@ export type Extension = {
<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';

View File

@ -26,9 +26,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSelect v-else-if="v.type === 'enum'" v-model="values[k]" :items="getMkSelectDef(v)">
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
</MkSelect>
<MkRadios v-else-if="v.type === 'radio'" v-model="values[k]">
<MkRadios v-else-if="v.type === 'radio'" v-model="values[k]" :options="getRadioOptionsDef(v)">
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
<option v-for="option in v.options" :key="getRadioKey(option)" :value="option.value">{{ option.label }}</option>
</MkRadios>
<MkRange v-else-if="v.type === 'range'" v-model="values[k]" :min="v.min" :max="v.max" :step="v.step" :textConverter="v.textConverter">
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
@ -60,6 +59,7 @@ import MkButton from '@/components/MkButton.vue';
import MkRadios from '@/components/MkRadios.vue';
import { i18n } from '@/i18n.js';
import type { MkSelectItem } from '@/components/MkSelect.vue';
import type { MkRadiosOption } from '@/components/MkRadios.vue';
import type { Form, EnumFormItem, RadioFormItem } from '@/utility/form.js';
const props = defineProps<{
@ -113,7 +113,13 @@ function getMkSelectDef(def: EnumFormItem): MkSelectItem[] {
});
}
function getRadioKey(e: RadioFormItem['options'][number]) {
return typeof e.value === 'string' ? e.value : JSON.stringify(e.value);
function getRadioOptionsDef(def: RadioFormItem): MkRadiosOption[] {
return def.options.map<MkRadiosOption>((v) => {
if (typeof v === 'string') {
return { value: v, label: v };
} else {
return { value: v.value, label: v.label };
}
});
}
</script>

View File

@ -28,13 +28,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ v.label ?? k }}</template>
<template v-if="v.caption != null" #caption>{{ v.caption }}</template>
</MkRange>
<MkRadios v-else-if="v.type === 'number:enum'" v-model="params[k]">
<MkRadios v-else-if="v.type === 'number:enum'" v-model="params[k]" :options="v.enum">
<template #label>{{ v.label ?? k }}</template>
<template v-if="v.caption != null" #caption>{{ v.caption }}</template>
<option v-for="item in v.enum" :value="item.value">
<i v-if="item.icon" :class="item.icon"></i>
<template v-else>{{ item.label }}</template>
</option>
</MkRadios>
<div v-else-if="v.type === 'seed'">
<MkRange v-model="params[k]" continuousUpdate type="number" :min="0" :max="10000" :step="1">

View File

@ -323,9 +323,20 @@ async function showRadioOptions(item: MenuRadio, ev: MouseEvent | PointerEvent |
type: 'radioOption',
text: key,
action: () => {
item.ref = value;
if ('value' in item.ref) {
item.ref.value = value;
} else {
// @ts-expect-error
item.ref = value;
}
},
active: computed(() => item.ref === value),
active: computed(() => {
if ('value' in item.ref) {
return item.ref.value === value;
} else {
return item.ref === value;
}
}),
};
});

View File

@ -121,7 +121,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ notification.invitation.room.name }}
</div>
<MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements">
{{ (i18n.ts._achievements._types as any)['_' + notification.achievement].title }}
{{ i18n.ts._achievements._types[`_${notification.achievement}`].title }}
</MkA>
<MkA v-else-if="notification.type === 'exportCompleted'" :class="$style.text" :to="`/my/drive/file/${notification.fileId}`">
{{ i18n.ts.showFile }}

View File

@ -12,11 +12,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="flag" :class="$style.preview__content1__switch_button">
<span>Switch is now {{ flag ? 'on' : 'off' }}</span>
</MkSwitch>
<div :class="$style.preview__content1__input">
<MkRadio v-model="radio" value="misskey">Misskey</MkRadio>
<MkRadio v-model="radio" value="mastodon">Mastodon</MkRadio>
<MkRadio v-model="radio" value="pleroma">Pleroma</MkRadio>
</div>
<div :class="$style.preview__content1__button">
<MkButton inline>This is</MkButton>
<MkButton inline primary>the button</MkButton>
@ -40,15 +35,12 @@ import * as config from '@@/js/config.js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import MkRadio from '@/components/MkRadio.vue';
import * as os from '@/os.js';
import { $i } from '@/i.js';
import { chooseDriveFile } from '@/utility/drive.js';
const text = ref('');
const flag = ref(true);
const radio = ref('misskey');
const mfm = ref(`Hello world! This is an @example mention. BTW you are @${$i ? $i.username : 'guest'}.\nAlso, here is ${config.url} and [example link](${config.url}). for more details, see https://example.com.\nAs you know #misskey is open-source software.`);
const openDialog = async () => {

View File

@ -144,7 +144,7 @@ async function unsubscribe() {
}
function encode(buffer: ArrayBuffer | null) {
return btoa(String.fromCharCode.apply(null, buffer ? new Uint8Array(buffer) as any : []));
return btoa(String.fromCharCode(...(buffer != null ? new Uint8Array(buffer) : [])));
}
/**

View File

@ -1,136 +0,0 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div
v-adaptive-border
:class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]"
:aria-checked="checked"
:aria-disabled="disabled"
role="checkbox"
@click="toggle"
>
<input
type="radio"
:disabled="disabled"
:class="$style.input"
>
<span :class="$style.button">
<span></span>
</span>
<span :class="$style.label"><slot></slot></span>
</div>
</template>
<script lang="ts" setup generic="T extends unknown">
import { computed } from 'vue';
const props = defineProps<{
modelValue: T;
value: T;
disabled?: boolean;
}>();
const emit = defineEmits<{
(ev: 'update:modelValue', value: T): void;
}>();
const checked = computed(() => props.modelValue === props.value);
function toggle(): void {
if (props.disabled) return;
emit('update:modelValue', props.value);
}
</script>
<style lang="scss" module>
.root {
position: relative;
display: inline-flex;
align-items: center;
text-align: left;
cursor: pointer;
padding: 7px 10px;
min-width: 60px;
background-color: var(--MI_THEME-panel);
background-clip: padding-box !important;
border: solid 1px var(--MI_THEME-panel);
border-radius: 6px;
font-size: 90%;
transition: all 0.2s;
user-select: none;
&.disabled {
opacity: 0.6;
cursor: not-allowed !important;
}
&:hover {
border-color: var(--MI_THEME-inputBorderHover) !important;
}
&:focus-within {
outline: none;
box-shadow: 0 0 0 2px var(--MI_THEME-focus);
}
&.checked {
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(--MI_THEME-accent);
&::after {
background-color: var(--MI_THEME-accent);
transform: scale(1);
opacity: 1;
}
}
}
}
.input {
position: absolute;
width: 0;
height: 0;
opacity: 0;
margin: 0;
}
.button {
position: relative;
display: inline-block;
width: 14px;
height: 14px;
background: none;
border: solid 2px var(--MI_THEME-inputBorder);
border-radius: 100%;
transition: inherit;
&::after {
content: '';
display: block;
position: absolute;
top: 3px;
right: 3px;
bottom: 3px;
left: 3px;
border-radius: 100%;
opacity: 0;
transform: scale(0);
transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
}
}
.label {
margin-left: 8px;
display: block;
line-height: 20px;
cursor: pointer;
}
</style>

View File

@ -3,99 +3,225 @@ SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="{ [$style.vertical]: vertical }">
<div :class="$style.label">
<slot name="label"></slot>
</div>
<div :class="$style.body">
<div
v-for="option in options"
:key="getKey(option.value)"
v-adaptive-border
:class="[$style.optionRoot, { [$style.disabled]: option.disabled, [$style.checked]: model === option.value }]"
:aria-checked="model === option.value"
:aria-disabled="option.disabled"
role="checkbox"
@click="toggle(option)"
>
<input
type="radio"
:disabled="option.disabled"
:class="$style.optionInput"
>
<span :class="$style.optionButton">
<span></span>
</span>
<div :class="$style.optionContent">
<i v-if="option.icon" :class="[$style.optionIcon, option.icon]" :style="option.iconStyle"></i>
<div>
<slot v-if="option.slotId != null" :name="`option-${option.slotId as SlotNames}`"></slot>
<template v-else>
<div :style="option.labelStyle">{{ option.label ?? option.value }}</div>
<div v-if="option.caption" :class="$style.optionCaption">{{ option.caption }}</div>
</template>
</div>
</div>
</div>
</div>
<div :class="$style.caption">
<slot name="caption"></slot>
</div>
</div>
</template>
<script lang="ts">
import { Comment, defineComponent, h, ref, watch } from 'vue';
import MkRadio from './MkRadio.vue';
import type { VNode } from 'vue';
import type { StyleValue } from 'vue';
import type { OptionValue } from '@/types/option-value.js';
export default defineComponent({
props: {
modelValue: {
required: false,
},
vertical: {
type: Boolean,
default: false,
},
},
setup(props, context) {
const value = ref(props.modelValue);
watch(value, () => {
context.emit('update:modelValue', value.value);
});
watch(() => props.modelValue, v => {
value.value = v;
});
if (!context.slots.default) return null;
let options = context.slots.default();
const label = context.slots.label && context.slots.label();
const caption = context.slots.caption && context.slots.caption();
// Fragment
if (options.length === 1 && options[0].props == null) options = options[0].children as VNode[];
// vnodev-if=false(trueoptiontype)
options = options.filter(vnode => vnode.type !== Comment);
return () => h('div', {
class: [
'novjtcto',
...(props.vertical ? ['vertical'] : []),
],
}, [
...(label ? [h('div', {
class: 'label',
}, label)] : []),
h('div', {
class: 'body',
}, options.map(option => h(MkRadio, {
key: option.key as string,
value: option.props?.value,
disabled: option.props?.disabled,
modelValue: value.value,
'onUpdate:modelValue': _v => value.value = _v,
}, () => option.children)),
),
...(caption ? [h('div', {
class: 'caption',
}, caption)] : []),
]);
},
});
export type MkRadiosOption<T = OptionValue, S = string> = {
value: T;
slotId?: S;
label?: string;
labelStyle?: StyleValue;
icon?: string;
iconStyle?: StyleValue;
caption?: string;
disabled?: boolean;
};
</script>
<style lang="scss">
.novjtcto {
> .label {
font-size: 0.85em;
padding: 0 0 8px 0;
user-select: none;
<script setup lang="ts" generic="const T extends MkRadiosOption">
defineProps<{
options: T[];
vertical?: boolean;
}>();
&:empty {
display: none;
type SlotNames = NonNullable<T extends MkRadiosOption<any, infer U> ? U : never>;
defineSlots<{
label?: () => void;
caption?: () => void;
} & {
[K in `option-${SlotNames}`]: () => void;
}>();
const model = defineModel<T['value']>({ required: true });
function getKey(value: OptionValue): PropertyKey {
if (value === null) return '___null___';
return value;
}
function toggle(o: MkRadiosOption): void {
if (o.disabled) return;
model.value = o.value;
}
</script>
<style lang="scss" module>
.label {
font-size: 0.85em;
padding: 0 0 8px 0;
user-select: none;
&:empty {
display: none;
}
}
.body {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.caption {
font-size: 0.85em;
padding: 8px 0 0 0;
color: color(from var(--MI_THEME-fg) srgb r g b / 0.75);
&:empty {
display: none;
}
}
.vertical > .body {
flex-direction: column;
}
.optionRoot {
position: relative;
display: inline-flex;
align-items: center;
text-align: left;
cursor: pointer;
padding: 8px 10px;
min-width: 60px;
background-color: var(--MI_THEME-panel);
background-clip: padding-box !important;
border: solid 1px var(--MI_THEME-panel);
border-radius: 6px;
font-size: 90%;
transition: all 0.2s;
user-select: none;
&.disabled {
opacity: 0.6;
cursor: not-allowed !important;
}
&:hover {
border-color: var(--MI_THEME-inputBorderHover) !important;
}
&:focus-within {
outline: none;
box-shadow: 0 0 0 2px var(--MI_THEME-focus);
}
&.checked {
background-color: var(--MI_THEME-accentedBg) !important;
border-color: var(--MI_THEME-accentedBg) !important;
color: var(--MI_THEME-accent);
cursor: default !important;
.optionButton {
border-color: var(--MI_THEME-accent);
&::after {
background-color: var(--MI_THEME-accent);
transform: scale(1);
opacity: 1;
}
}
}
> .body {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
> .caption {
font-size: 0.85em;
padding: 8px 0 0 0;
color: color(from var(--MI_THEME-fg) srgb r g b / 0.75);
&:empty {
display: none;
}
}
&.vertical {
> .body {
flex-direction: column;
.optionCaption {
color: color(from var(--MI_THEME-accent) srgb r g b / 0.75);
}
}
}
.optionInput {
position: absolute;
width: 0;
height: 0;
opacity: 0;
margin: 0;
}
.optionButton {
position: relative;
display: inline-block;
width: 14px;
height: 14px;
background: none;
border: solid 2px var(--MI_THEME-inputBorder);
border-radius: 100%;
transition: inherit;
&::after {
content: '';
display: block;
position: absolute;
top: 3px;
right: 3px;
bottom: 3px;
left: 3px;
border-radius: 100%;
opacity: 0;
transform: scale(0);
transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
}
}
.optionContent {
display: flex;
align-items: center;
gap: 6px;
margin-left: 8px;
}
.optionCaption {
font-size: 0.85em;
padding: 2px 0 0 0;
color: color(from var(--MI_THEME-fg) srgb r g b / 0.75);
transition: all 0.2s;
}
.optionIcon {
flex-shrink: 0;
}
</style>

View File

@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, useTemplateRef } from 'vue';
import { Chart } from 'chart.js';
import type { ScatterDataPoint } from 'chart.js';
import tinycolor from 'tinycolor2';
import { store } from '@/store.js';
import { useChartTooltip } from '@/composables/use-chart-tooltip.js';
@ -18,6 +19,12 @@ import { alpha } from '@/utility/color.js';
import { initChart } from '@/utility/init-chart.js';
import { misskeyApi } from '@/utility/misskey-api.js';
interface RetentionPoint extends ScatterDataPoint {
x: number;
y: number;
d: string;
}
initChart();
const chartEl = useTemplateRef('chartEl');
@ -62,14 +69,14 @@ onMounted(async () => {
fill: false,
tension: 0.4,
data: [{
x: '0',
x: 0,
y: 100,
d: getYYYYMMDD(new Date(record.createdAt)),
}, ...Object.entries(record.data).sort((a, b) => getDate(a[0]) > getDate(b[0]) ? 1 : -1).map(([k, v], i) => ({
x: (i + 1).toString(),
x: i + 1,
y: (v / record.users) * 100,
d: getYYYYMMDD(new Date(record.createdAt)),
}))] as any,
}))],
})),
},
options: {
@ -111,11 +118,11 @@ onMounted(async () => {
enabled: false,
callbacks: {
title(context) {
const v = context[0].dataset.data[context[0].dataIndex] as unknown as { x: string, y: number, d: string };
const v = context[0].dataset.data[context[0].dataIndex] as RetentionPoint;
return `${v.x} days later`;
},
label(context) {
const v = context.dataset.data[context.dataIndex] as unknown as { x: string, y: number, d: string };
const v = context.dataset.data[context.dataIndex] as RetentionPoint;
const p = Math.round(v.y) + '%';
return `${v.d} ${p}`;
},

View File

@ -40,7 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts">
export type OptionValue = string | number | null;
import type { OptionValue } from '@/types/option-value.js';
export type ItemOption<T extends OptionValue = OptionValue> = {
type?: 'option';

View File

@ -14,19 +14,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #icon><i class="ti ti-settings-question"></i></template>
<div class="_gaps_s">
<MkRadios v-model="q_use" :vertical="true">
<option value="single">
<div><i class="ti ti-user"></i> <b>{{ i18n.ts._serverSetupWizard._use.single }}</b></div>
<div>{{ i18n.ts._serverSetupWizard._use.single_description }}</div>
</option>
<option value="group">
<div><i class="ti ti-lock"></i> <b>{{ i18n.ts._serverSetupWizard._use.group }}</b></div>
<div>{{ i18n.ts._serverSetupWizard._use.group_description }}</div>
</option>
<option value="open">
<div><i class="ti ti-world"></i> <b>{{ i18n.ts._serverSetupWizard._use.open }}</b></div>
<div>{{ i18n.ts._serverSetupWizard._use.open_description }}</div>
</option>
<MkRadios
v-model="q_use"
:options="[
{ value: 'single', label: i18n.ts._serverSetupWizard._use.single, icon: 'ti ti-user', caption: i18n.ts._serverSetupWizard._use.single_description },
{ value: 'group', label: i18n.ts._serverSetupWizard._use.group, icon: 'ti ti-lock', caption: i18n.ts._serverSetupWizard._use.group_description },
{ value: 'open', label: i18n.ts._serverSetupWizard._use.open, icon: 'ti ti-world', caption: i18n.ts._serverSetupWizard._use.open_description },
]"
vertical
>
</MkRadios>
<MkInfo v-if="q_use === 'single'">{{ i18n.ts._serverSetupWizard._use.single_youCanCreateMultipleAccounts }}</MkInfo>
@ -40,10 +36,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #icon><i class="ti ti-users"></i></template>
<div class="_gaps_s">
<MkRadios v-model="q_scale" :vertical="true">
<option value="small"><i class="ti ti-user"></i> {{ i18n.ts._serverSetupWizard._scale.small }}</option>
<option value="medium"><i class="ti ti-users"></i> {{ i18n.ts._serverSetupWizard._scale.medium }}</option>
<option value="large"><i class="ti ti-users-group"></i> {{ i18n.ts._serverSetupWizard._scale.large }}</option>
<MkRadios
v-model="q_scale"
:options="[
{ value: 'small', label: i18n.ts._serverSetupWizard._scale.small, icon: 'ti ti-user' },
{ value: 'medium', label: i18n.ts._serverSetupWizard._scale.medium, icon: 'ti ti-users' },
{ value: 'large', label: i18n.ts._serverSetupWizard._scale.large, icon: 'ti ti-users-group' },
]"
vertical
>
</MkRadios>
<MkInfo v-if="q_scale === 'large'"><b>{{ i18n.ts.advice }}:</b> {{ i18n.ts._serverSetupWizard.largeScaleServerAdvice }}</MkInfo>
@ -57,9 +58,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_s">
<div>{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description1 }}<br>{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description2 }}<br><MkLink target="_blank" url="https://wikipedia.org/wiki/Fediverse">{{ i18n.ts.learnMore }}</MkLink></div>
<MkRadios v-model="q_federation" :vertical="true">
<option value="yes">{{ i18n.ts.yes }}</option>
<option value="no">{{ i18n.ts.no }}</option>
<MkRadios
v-model="q_federation"
:options="[
{ value: 'yes', label: i18n.ts.yes },
{ value: 'no', label: i18n.ts.no },
]"
vertical
>
</MkRadios>
<MkInfo v-if="q_federation === 'yes'">{{ i18n.ts._serverSetupWizard.youCanConfigureMoreFederationSettingsLater }}</MkInfo>
@ -212,9 +218,9 @@ const props = withDefaults(defineProps<{
});
const q_name = ref('');
const q_use = ref('single');
const q_scale = ref('small');
const q_federation = ref('yes');
const q_use = ref<'single' | 'group' | 'open'>('single');
const q_scale = ref<'small' | 'medium' | 'large'>('small');
const q_federation = ref<'yes' | 'no'>('no');
const q_remoteContentsCleaning = ref(true);
const q_adminName = ref('');
const q_adminEmail = ref('');
@ -239,7 +245,7 @@ const serverSettings = computed<Misskey.entities.AdminUpdateMetaRequest>(() => {
enableReactionsBuffering,
clientOptions: {
entrancePageStyle: q_use.value === 'open' ? 'classic' : 'simple',
} as any,
},
};
});

View File

@ -22,18 +22,26 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkTextarea v-model="text">
<template #label>{{ i18n.ts.text }}</template>
</MkTextarea>
<MkRadios v-model="icon">
<MkRadios
v-model="icon"
:options="[
{ value: 'info', icon: 'ti ti-info-circle' },
{ value: 'warning', icon: 'ti ti-alert-triangle', iconStyle: 'color: var(--MI_THEME-warn);' },
{ value: 'error', icon: 'ti ti-circle-x', iconStyle: 'color: var(--MI_THEME-error);' },
{ value: 'success', icon: 'ti ti-check', iconStyle: 'color: var(--MI_THEME-success);' },
]"
>
<template #label>{{ i18n.ts.icon }}</template>
<option value="info"><i class="ti ti-info-circle"></i></option>
<option value="warning"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i></option>
<option value="error"><i class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i></option>
<option value="success"><i class="ti ti-check" style="color: var(--MI_THEME-success);"></i></option>
</MkRadios>
<MkRadios v-model="display">
<MkRadios
v-model="display"
:options="[
{ value: 'normal', label: i18n.ts.normal },
{ value: 'banner', label: i18n.ts.banner },
{ value: 'dialog', label: i18n.ts.dialog },
]"
>
<template #label>{{ i18n.ts.display }}</template>
<option value="normal">{{ i18n.ts.normal }}</option>
<option value="banner">{{ i18n.ts.banner }}</option>
<option value="dialog">{{ i18n.ts.dialog }}</option>
</MkRadios>
<MkSwitch v-model="needConfirmationToRead">
{{ i18n.ts._announcement.needConfirmationToRead }}

View File

@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@ok="save()"
@closed="emit('closed')"
>
<template #header><i class="ti ti-icons"></i> {{ (i18n.ts._widgets as any)[widgetName] ?? widgetName }}</template>
<template #header><i class="ti ti-icons"></i> {{ i18n.ts._widgets[widgetName] ?? widgetName }}</template>
<MkPreviewWithControls>
<template #preview>
@ -50,13 +50,14 @@ SPDX-License-Identifier: AGPL-3.0-only
import { useTemplateRef, ref, computed, onBeforeUnmount, onMounted } from 'vue';
import MkPreviewWithControls from './MkPreviewWithControls.vue';
import type { Form } from '@/utility/form.js';
import type { WidgetName } from '@/widgets/index.js';
import { deepClone } from '@/utility/clone.js';
import { i18n } from '@/i18n.js';
import MkModalWindow from '@/components/MkModalWindow.vue';
import MkForm from '@/components/MkForm.vue';
const props = defineProps<{
widgetName: string;
widgetName: WidgetName;
form: Form;
currentSettings: Record<string, any>;
}>();

View File

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<span :class="$style.container">
<span ref="content" :class="$style.content" :style="{ maxWidth: `${100 / minScale}%` }">
<slot/>
<slot></slot>
</span>
</span>
</template>

View File

@ -5,7 +5,8 @@
import { ref } from 'vue';
import type { Ref, MaybeRefOrGetter } from 'vue';
import type { MkSelectItem, OptionValue, GetMkSelectValueTypesFromDef } from '@/components/MkSelect.vue';
import type { MkSelectItem, GetMkSelectValueTypesFromDef } from '@/components/MkSelect.vue';
import type { OptionValue } from '@/types/option-value.js';
type UnwrapReadonlyItems<T> = T extends readonly (infer U)[] ? U[] : T;

View File

@ -51,9 +51,3 @@ export async function fetchInstance(force = false): Promise<Misskey.entities.Met
return instance;
}
export type ClientOptions = {
entrancePageStyle: 'classic' | 'simple';
showTimelineForVisitor: boolean;
showActivitiesForVisitor: boolean;
};

View File

@ -14,7 +14,8 @@ import type { Form, GetFormResultType } from '@/utility/form.js';
import type { MenuItem } from '@/types/menu.js';
import type { PostFormProps } from '@/types/post-form.js';
import type { UploaderFeatures } from '@/composables/use-uploader.js';
import type { MkSelectItem, OptionValue } from '@/components/MkSelect.vue';
import type { MkSelectItem } from '@/components/MkSelect.vue';
import type { OptionValue } from '@/types/option-value.js';
import type { MkDialogReturnType } from '@/components/MkDialog.vue';
import type { OverloadToUnion } from '@/types/overload-to-union.js';
import type MkRoleSelectDialog_TypeReferenceOnly from '@/components/MkRoleSelectDialog.vue';

View File

@ -22,22 +22,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.imageUrl }}</template>
</MkInput>
<MkRadios v-model="ad.place">
<MkRadios
v-model="ad.place"
:options="[
{ value: 'square' },
{ value: 'horizontal' },
{ value: 'horizontal-big' },
]"
>
<template #label>Form</template>
<option value="square">square</option>
<option value="horizontal">horizontal</option>
<option value="horizontal-big">horizontal-big</option>
</MkRadios>
<!--
<div style="margin: 32px 0;">
{{ i18n.ts.priority }}
<MkRadio v-model="ad.priority" value="high">{{ i18n.ts.high }}</MkRadio>
<MkRadio v-model="ad.priority" value="middle">{{ i18n.ts.middle }}</MkRadio>
<MkRadio v-model="ad.priority" value="low">{{ i18n.ts.low }}</MkRadio>
</div>
-->
<FormSplit>
<MkInput v-model="ad.ratio" type="number">
<template #label>{{ i18n.ts.ratio }}</template>
@ -109,7 +104,11 @@ import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { useMkSelect } from '@/composables/use-mkselect.js';
const ads = ref<Misskey.entities.Ad[]>([]);
type Ad = Misskey.entities.Ad & {
place: 'square' | 'horizontal' | 'horizontal-big';
};
const ads = ref<Ad[]>([]);
// ISOTZUTCTZ
const localTime = new Date();
@ -136,7 +135,7 @@ misskeyApi('admin/ad/list', { publishing: publishing }).then(adsResponse => {
exdate.setMilliseconds(exdate.getMilliseconds() - localTimeDiff);
stdate.setMilliseconds(stdate.getMilliseconds() - localTimeDiff);
return {
...r,
...(r as Ad),
expiresAt: exdate.toISOString().slice(0, 16),
startsAt: stdate.toISOString().slice(0, 16),
};
@ -239,7 +238,7 @@ function more() {
exdate.setMilliseconds(exdate.getMilliseconds() - localTimeDiff);
stdate.setMilliseconds(stdate.getMilliseconds() - localTimeDiff);
return {
...r,
...(r as Ad),
expiresAt: exdate.toISOString().slice(0, 16),
startsAt: stdate.toISOString().slice(0, 16),
};
@ -256,7 +255,7 @@ function refresh() {
exdate.setMilliseconds(exdate.getMilliseconds() - localTimeDiff);
stdate.setMilliseconds(stdate.getMilliseconds() - localTimeDiff);
return {
...r,
...(r as Ad),
expiresAt: exdate.toISOString().slice(0, 16),
startsAt: stdate.toISOString().slice(0, 16),
};

View File

@ -45,18 +45,26 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-model="announcement.imageUrl" type="url">
<template #label>{{ i18n.ts.imageUrl }}</template>
</MkInput>
<MkRadios v-model="announcement.icon">
<MkRadios
v-model="announcement.icon"
:options="[
{ value: 'info', icon: 'ti ti-info-circle' },
{ value: 'warning', icon: 'ti ti-alert-triangle', iconStyle: 'color: var(--MI_THEME-warn);' },
{ value: 'error', icon: 'ti ti-circle-x', iconStyle: 'color: var(--MI_THEME-error);' },
{ value: 'success', icon: 'ti ti-check', iconStyle: 'color: var(--MI_THEME-success);' },
]"
>
<template #label>{{ i18n.ts.icon }}</template>
<option value="info"><i class="ti ti-info-circle"></i></option>
<option value="warning"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i></option>
<option value="error"><i class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i></option>
<option value="success"><i class="ti ti-check" style="color: var(--MI_THEME-success);"></i></option>
</MkRadios>
<MkRadios v-model="announcement.display">
<MkRadios
v-model="announcement.display"
:options="[
{ value: 'normal', label: i18n.ts.normal },
{ value: 'banner', label: i18n.ts.banner },
{ value: 'dialog', label: i18n.ts.dialog },
]"
>
<template #label>{{ i18n.ts.display }}</template>
<option value="normal">{{ i18n.ts.normal }}</option>
<option value="banner">{{ i18n.ts.banner }}</option>
<option value="dialog">{{ i18n.ts.dialog }}</option>
</MkRadios>
<MkInfo v-if="announcement.display === 'dialog'" warn>{{ i18n.ts._announcement.dialogAnnouncementUxWarn }}</MkInfo>
<MkSwitch v-model="announcement.forExistingUsers" :helpText="i18n.ts._announcement.forExistingUsersDescription">

View File

@ -19,13 +19,17 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<div class="_gaps_m">
<MkRadios v-model="botProtectionForm.state.provider">
<option value="none">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option>
<option value="hcaptcha">hCaptcha</option>
<option value="mcaptcha">mCaptcha</option>
<option value="recaptcha">reCAPTCHA</option>
<option value="turnstile">Turnstile</option>
<option value="testcaptcha">testCaptcha</option>
<MkRadios
v-model="botProtectionForm.state.provider"
:options="[
{ value: 'none', label: `${i18n.ts.none} (${i18n.ts.notRecommended})` },
{ value: 'hcaptcha', label: 'hCaptcha' },
{ value: 'mcaptcha', label: 'mCaptcha' },
{ value: 'recaptcha', label: 'reCAPTCHA' },
{ value: 'turnstile', label: 'Turnstile' },
{ value: 'testcaptcha', label: 'testCaptcha' },
]"
>
</MkRadios>
<template v-if="botProtectionForm.state.provider === 'hcaptcha'">

View File

@ -9,10 +9,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/admin/branding" :label="i18n.ts.branding" :keywords="['branding']" icon="ti ti-paint">
<div class="_gaps_m">
<SearchMarker :keywords="['entrance', 'welcome', 'landing', 'front', 'home', 'page', 'style']">
<MkRadios v-model="entrancePageStyle">
<MkRadios
v-model="entrancePageStyle"
:options="[
{ value: 'classic' },
{ value: 'simple' },
]"
>
<template #label><SearchLabel>{{ i18n.ts._serverSettings.entrancePageStyle }}</SearchLabel></template>
<option value="classic">Classic</option>
<option value="simple">Simple</option>
</MkRadios>
</SearchMarker>
@ -151,8 +155,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, computed } from 'vue';
import JSON5 from 'json5';
import * as Misskey from 'misskey-js';
import { host } from '@@/js/config.js';
import type { ClientOptions } from '@/instance.js';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import * as os from '@/os.js';
@ -168,11 +172,11 @@ import MkSwitch from '@/components/MkSwitch.vue';
const meta = await misskeyApi('admin/meta');
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const entrancePageStyle = ref<ClientOptions['entrancePageStyle']>(meta.clientOptions.entrancePageStyle ?? 'classic');
const entrancePageStyle = ref<Misskey.entities.MetaClientOptions['entrancePageStyle']>(meta.clientOptions.entrancePageStyle ?? 'classic');
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const showTimelineForVisitor = ref<ClientOptions['showTimelineForVisitor']>(meta.clientOptions.showTimelineForVisitor ?? true);
const showTimelineForVisitor = ref<Misskey.entities.MetaClientOptions['showTimelineForVisitor']>(meta.clientOptions.showTimelineForVisitor ?? true);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const showActivitiesForVisitor = ref<ClientOptions['showActivitiesForVisitor']>(meta.clientOptions.showActivitiesForVisitor ?? true);
const showActivitiesForVisitor = ref<Misskey.entities.MetaClientOptions['showActivitiesForVisitor']>(meta.clientOptions.showActivitiesForVisitor ?? true);
const iconUrl = ref(meta.iconUrl);
const app192IconUrl = ref(meta.app192IconUrl);
@ -191,11 +195,11 @@ const manifestJsonOverride = ref(meta.manifestJsonOverride === '' ? '{}' : JSON.
function save() {
os.apiWithDialog('admin/update-meta', {
clientOptions: ({
clientOptions: {
entrancePageStyle: entrancePageStyle.value,
showTimelineForVisitor: showTimelineForVisitor.value,
showActivitiesForVisitor: showActivitiesForVisitor.value,
} as ClientOptions) as any,
},
iconUrl: iconUrl.value,
app192IconUrl: app192IconUrl.value,
app512IconUrl: app512IconUrl.value,

View File

@ -25,11 +25,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m">
<div><SearchText>{{ i18n.ts._sensitiveMediaDetection.description }}</SearchText></div>
<MkRadios v-model="sensitiveMediaDetectionForm.state.sensitiveMediaDetection">
<option value="none">{{ i18n.ts.none }}</option>
<option value="all">{{ i18n.ts.all }}</option>
<option value="local">{{ i18n.ts.localOnly }}</option>
<option value="remote">{{ i18n.ts.remoteOnly }}</option>
<MkRadios
v-model="sensitiveMediaDetectionForm.state.sensitiveMediaDetection"
:options="[
{ value: 'none', label: i18n.ts.none },
{ value: 'all', label: i18n.ts.all },
{ value: 'local', label: i18n.ts.localOnly },
{ value: 'remote', label: i18n.ts.remoteOnly },
]"
>
</MkRadios>
<SearchMarker :keywords="['sensitivity']">

View File

@ -258,11 +258,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps">
<SearchMarker>
<MkRadios v-model="federationForm.state.federation">
<MkRadios
v-model="federationForm.state.federation"
:options="[
{ value: 'all', label: i18n.ts.all },
{ value: 'specified', label: i18n.ts.specifyHost },
{ value: 'none', label: i18n.ts.none },
]"
>
<template #label><SearchLabel>{{ i18n.ts.behavior }}</SearchLabel><span v-if="federationForm.modifiedStates.federation" class="_modified">{{ i18n.ts.modified }}</span></template>
<option value="all">{{ i18n.ts.all }}</option>
<option value="specified">{{ i18n.ts.specifyHost }}</option>
<option value="none">{{ i18n.ts.none }}</option>
</MkRadios>
</SearchMarker>

View File

@ -5,10 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<section>
<div v-if="app.permission.length > 0">
<div v-if="permissions.length > 0">
<p>{{ i18n.tsx._auth.permission({ name }) }}</p>
<ul>
<li v-for="p in app.permission" :key="p">{{ (i18n.ts._permissions as any)[p] ?? p }}</li>
<li v-for="p in permissions" :key="p">{{ i18n.ts._permissions[p] ?? p }}</li>
</ul>
</div>
<div>{{ i18n.tsx._auth.shareAccess({ name: `${name} (${app.id})` }) }}</div>
@ -37,6 +37,10 @@ const emit = defineEmits<{
const app = computed(() => props.session.app);
const permissions = computed(() => {
return props.session.app.permission.filter((p): p is typeof Misskey.permissions[number] => typeof p === 'string');
});
const name = computed(() => {
const el = window.document.createElement('div');
el.textContent = app.value.name;

View File

@ -11,9 +11,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter="search">
<template #prefix><i class="ti ti-search"></i></template>
</MkInput>
<MkRadios v-model="searchType" @update:modelValue="search()">
<option value="nameAndDescription">{{ i18n.ts._channel.nameAndDescription }}</option>
<option value="nameOnly">{{ i18n.ts._channel.nameOnly }}</option>
<MkRadios
v-model="searchType"
:options="[
{ value: 'nameAndDescription', label: i18n.ts._channel.nameAndDescription },
{ value: 'nameOnly', label: i18n.ts._channel.nameOnly },
]"
@update:modelValue="search()"
>
</MkRadios>
<MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton>
</div>
@ -72,15 +77,17 @@ import { Paginator } from '@/utility/paginator.js';
const router = useRouter();
type SearchType = 'nameAndDescription' | 'nameOnly';
const props = defineProps<{
query: string;
type?: string;
type?: SearchType;
}>();
const key = ref('');
const tab = ref('featured');
const searchQuery = ref('');
const searchType = ref('nameAndDescription');
const searchType = ref<SearchType>('nameAndDescription');
const channelPaginator = shallowRef();
onMounted(() => {

View File

@ -212,7 +212,7 @@ async function run() {
const version = utils.getLangVersion(flash.value.script);
const isLegacy = getIsLegacy(version);
const { Interpreter, Parser, values } = isLegacy ? (await import('@syuilo/aiscript-0-19-0') as any) : await import('@syuilo/aiscript');
const { Interpreter, Parser, values } = (isLegacy ? (await import('@syuilo/aiscript-0-19-0')) : await import('@syuilo/aiscript')) as typeof import('@syuilo/aiscript');
const parser = new Parser();

View File

@ -35,22 +35,28 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder :defaultOpen="true">
<template #label>{{ i18n.ts._reversi.blackOrWhite }}</template>
<MkRadios v-model="game.bw">
<option value="random">{{ i18n.ts.random }}</option>
<option :value="'1'">
<MkRadios
v-model="game.bw"
:options="[
{ value: 'random', label: i18n.ts.random },
{ value: '1', slotId: 'user1' },
{ value: '2', slotId: 'user2' },
]"
>
<template #option-user1>
<I18n :src="i18n.ts._reversi.blackIs" tag="span">
<template #name>
<b><MkUserName :user="game.user1"/></b>
</template>
</I18n>
</option>
<option :value="'2'">
</template>
<template #option-user2>
<I18n :src="i18n.ts._reversi.blackIs" tag="span">
<template #name>
<b><MkUserName :user="game.user2"/></b>
</template>
</I18n>
</option>
</template>
</MkRadios>
</MkFolder>
@ -58,15 +64,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts._reversi.timeLimitForEachTurn }}</template>
<template #suffix>{{ game.timeLimitForEachTurn }}{{ i18n.ts._time.second }}</template>
<MkRadios v-model="game.timeLimitForEachTurn">
<option :value="5">5{{ i18n.ts._time.second }}</option>
<option :value="10">10{{ i18n.ts._time.second }}</option>
<option :value="30">30{{ i18n.ts._time.second }}</option>
<option :value="60">60{{ i18n.ts._time.second }}</option>
<option :value="90">90{{ i18n.ts._time.second }}</option>
<option :value="120">120{{ i18n.ts._time.second }}</option>
<option :value="180">180{{ i18n.ts._time.second }}</option>
<option :value="3600">3600{{ i18n.ts._time.second }}</option>
<MkRadios
v-model="game.timeLimitForEachTurn"
:options="gameTurnOptionsDef"
>
</MkRadios>
</MkFolder>
@ -110,7 +111,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, watch, ref, onMounted, shallowRef, onUnmounted } from 'vue';
import { computed, watch, ref, onUnmounted } from 'vue';
import * as Misskey from 'misskey-js';
import * as Reversi from 'misskey-reversi';
import type { MenuItem } from '@/types/menu.js';
@ -122,6 +123,7 @@ import MkRadios from '@/components/MkRadios.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkFolder from '@/components/MkFolder.vue';
import * as os from '@/os.js';
import type { MkRadiosOption } from '@/components/MkRadios.vue';
import { useRouter } from '@/router.js';
const $i = ensureSignin();
@ -139,6 +141,17 @@ const shareWhenStart = defineModel<boolean>('shareWhenStart', { default: false }
const game = ref<Misskey.entities.ReversiGameDetailed>(deepClone(props.game));
const gameTurnOptionsDef = [
{ value: 5, label: '5' + i18n.ts._time.second },
{ value: 10, label: '10' + i18n.ts._time.second },
{ value: 30, label: '30' + i18n.ts._time.second },
{ value: 60, label: '60' + i18n.ts._time.second },
{ value: 90, label: '90' + i18n.ts._time.second },
{ value: 120, label: '120' + i18n.ts._time.second },
{ value: 180, label: '180' + i18n.ts._time.second },
{ value: 3600, label: '3600' + i18n.ts._time.second },
] as MkRadiosOption<number>[];
const mapName = computed(() => {
if (game.value.map == null) return 'Random';
const found = Object.values(Reversi.maps).find(x => x.data.join('') === game.value.map.join(''));

View File

@ -19,11 +19,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header>{{ i18n.ts.options }}</template>
<div class="_gaps_m">
<MkRadios v-model="searchScope">
<option v-if="instance.federation !== 'none' && noteSearchableScope === 'global'" value="all">{{ i18n.ts._search.searchScopeAll }}</option>
<option value="local">{{ instance.federation === 'none' ? i18n.ts._search.searchScopeAll : i18n.ts._search.searchScopeLocal }}</option>
<option v-if="instance.federation !== 'none' && noteSearchableScope === 'global'" value="server">{{ i18n.ts._search.searchScopeServer }}</option>
<option value="user">{{ i18n.ts._search.searchScopeUser }}</option>
<MkRadios
v-model="searchScope"
:options="searchScopeDef"
>
</MkRadios>
<div v-if="instance.federation !== 'none' && searchScope === 'server'" :class="$style.subOptionRoot">
@ -127,6 +126,7 @@ import MkNotesTimeline from '@/components/MkNotesTimeline.vue';
import MkRadios from '@/components/MkRadios.vue';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
import { Paginator } from '@/utility/paginator.js';
import type { MkRadiosOption } from '@/components/MkRadios.vue';
const props = withDefaults(defineProps<{
query?: string;
@ -183,6 +183,24 @@ const searchScope = ref<'all' | 'local' | 'server' | 'user'>((() => {
return 'all';
})());
const searchScopeDef = computed<MkRadiosOption[]>(() => {
const options: MkRadiosOption[] = [];
if (instance.federation !== 'none' && noteSearchableScope === 'global') {
options.push({ value: 'all', label: i18n.ts._search.searchScopeAll });
}
options.push({ value: 'local', label: instance.federation === 'none' ? i18n.ts._search.searchScopeAll : i18n.ts._search.searchScopeLocal });
if (instance.federation !== 'none' && noteSearchableScope === 'global') {
options.push({ value: 'server', label: i18n.ts._search.searchScopeServer });
}
options.push({ value: 'user', label: i18n.ts._search.searchScopeUser });
return options;
});
type SearchParams = {
readonly query: string;
readonly host?: string;

View File

@ -9,10 +9,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter.prevent="search">
<template #prefix><i class="ti ti-search"></i></template>
</MkInput>
<MkRadios v-if="instance.federation !== 'none'" v-model="searchOrigin" @update:modelValue="search()">
<option value="combined">{{ i18n.ts.all }}</option>
<option value="local">{{ i18n.ts.local }}</option>
<option value="remote">{{ i18n.ts.remote }}</option>
<MkRadios
v-if="instance.federation !== 'none'"
v-model="searchOrigin"
:options="[
{ value: 'combined', label: i18n.ts.all },
{ value: 'local', label: i18n.ts.local },
{ value: 'remote', label: i18n.ts.remote },
]"
@update:modelValue="search()"
>
</MkRadios>
<MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton>
</div>

View File

@ -40,31 +40,43 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['column', 'align']">
<MkPreferenceContainer k="deck.columnAlign">
<MkRadios v-model="columnAlign">
<MkRadios
v-model="columnAlign"
:options="[
{ value: 'left', label: i18n.ts.left },
{ value: 'center', label: i18n.ts.center },
]"
>
<template #label><SearchLabel>{{ i18n.ts._deck.columnAlign }}</SearchLabel></template>
<option value="left">{{ i18n.ts.left }}</option>
<option value="center">{{ i18n.ts.center }}</option>
</MkRadios>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['menu', 'position']">
<MkPreferenceContainer k="deck.menuPosition">
<MkRadios v-model="menuPosition">
<MkRadios
v-model="menuPosition"
:options="[
{ value: 'right', label: i18n.ts.right },
{ value: 'bottom', label: i18n.ts.bottom },
]"
>
<template #label><SearchLabel>{{ i18n.ts._deck.deckMenuPosition }}</SearchLabel></template>
<option value="right">{{ i18n.ts.right }}</option>
<option value="bottom">{{ i18n.ts.bottom }}</option>
</MkRadios>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['navbar', 'position']">
<MkPreferenceContainer k="deck.navbarPosition">
<MkRadios v-model="navbarPosition">
<MkRadios
v-model="navbarPosition"
:options="[
{ value: 'left', label: i18n.ts.left },
{ value: 'top', label: i18n.ts.top },
{ value: 'bottom', label: i18n.ts.bottom },
]"
>
<template #label><SearchLabel>{{ i18n.ts._deck.navbarPosition }}</SearchLabel></template>
<option value="left">{{ i18n.ts.left }}</option>
<option value="top">{{ i18n.ts.top }}</option>
<option value="bottom">{{ i18n.ts.bottom }}</option>
</MkRadios>
</MkPreferenceContainer>
</SearchMarker>

View File

@ -63,38 +63,33 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m">
<SearchMarker :keywords="['emoji', 'picker', 'scale', 'size']">
<MkPreferenceContainer k="emojiPickerScale">
<MkRadios v-model="emojiPickerScale">
<MkRadios
v-model="emojiPickerScale"
:options="emojiPickerScaleDef"
>
<template #label><SearchLabel>{{ i18n.ts.size }}</SearchLabel></template>
<option :value="1">{{ i18n.ts.small }}</option>
<option :value="2">{{ i18n.ts.medium }}</option>
<option :value="3">{{ i18n.ts.large }}</option>
<option :value="4">{{ i18n.ts.large }}+</option>
<option :value="5">{{ i18n.ts.large }}++</option>
</MkRadios>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['emoji', 'picker', 'width', 'column', 'size']">
<MkPreferenceContainer k="emojiPickerWidth">
<MkRadios v-model="emojiPickerWidth">
<MkRadios
v-model="emojiPickerWidth"
:options="emojiPickerWidthDef"
>
<template #label><SearchLabel>{{ i18n.ts.numberOfColumn }}</SearchLabel></template>
<option :value="1">5</option>
<option :value="2">6</option>
<option :value="3">7</option>
<option :value="4">8</option>
<option :value="5">9</option>
</MkRadios>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['emoji', 'picker', 'height', 'size']">
<MkPreferenceContainer k="emojiPickerHeight">
<MkRadios v-model="emojiPickerHeight">
<MkRadios
v-model="emojiPickerHeight"
:options="emojiPickerHeightDef"
>
<template #label><SearchLabel>{{ i18n.ts.height }}</SearchLabel></template>
<option :value="1">{{ i18n.ts.small }}</option>
<option :value="2">{{ i18n.ts.medium }}</option>
<option :value="3">{{ i18n.ts.large }}</option>
<option :value="4">{{ i18n.ts.large }}+</option>
</MkRadios>
</MkPreferenceContainer>
</SearchMarker>
@ -126,6 +121,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, ref, watch } from 'vue';
import XPalette from './emoji-palette.palette.vue';
import type { MkSelectItem } from '@/components/MkSelect.vue';
import type { MkRadiosOption } from '@/components/MkRadios.vue';
import { genId } from '@/utility/id.js';
import MkFeatureBanner from '@/components/MkFeatureBanner.vue';
import MkRadios from '@/components/MkRadios.vue';
@ -158,8 +154,31 @@ const emojiPaletteForMainDef = computed<MkSelectItem[]>(() => [
})),
]);
const emojiPickerScale = prefer.model('emojiPickerScale');
const emojiPickerScaleDef = [
{ label: i18n.ts.small, value: 1 },
{ label: i18n.ts.medium, value: 2 },
{ label: i18n.ts.large, value: 3 },
{ label: i18n.ts.large + '+', value: 4 },
{ label: i18n.ts.large + '++', value: 5 },
] as MkRadiosOption<number>[];
const emojiPickerWidth = prefer.model('emojiPickerWidth');
const emojiPickerWidthDef = [
{ label: '5', value: 1 },
{ label: '6', value: 2 },
{ label: '7', value: 3 },
{ label: '8', value: 4 },
{ label: '9', value: 5 },
] as MkRadiosOption<number>[];
const emojiPickerHeight = prefer.model('emojiPickerHeight');
const emojiPickerHeightDef = [
{ label: i18n.ts.small, value: 1 },
{ label: i18n.ts.medium, value: 2 },
{ label: i18n.ts.large, value: 3 },
{ label: i18n.ts.large + '+', value: 4 },
] as MkRadiosOption<number>[];
const emojiPickerStyle = prefer.model('emojiPickerStyle');
const palettesSyncEnabled = ref(prefer.isSyncEnabled('emojiPalettes'));

View File

@ -32,10 +32,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton primary class="save" @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
</div>
<MkRadios v-model="menuDisplay">
<MkRadios
v-model="menuDisplay"
:options="[
{ value: 'sideFull', label: i18n.ts._menuDisplay.sideFull },
{ value: 'sideIcon', label: i18n.ts._menuDisplay.sideIcon },
]"
>
<template #label>{{ i18n.ts.display }}</template>
<option value="sideFull">{{ i18n.ts._menuDisplay.sideFull }}</option>
<option value="sideIcon">{{ i18n.ts._menuDisplay.sideIcon }}</option>
</MkRadios>
<SearchMarker :keywords="['navbar', 'sidebar', 'toggle', 'button', 'sub']">

View File

@ -58,7 +58,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #key>{{ i18n.ts.permission }}</template>
<template #value>
<ul style="margin-top: 0; margin-bottom: 0;">
<li v-for="permission in plugin.permissions" :key="permission">{{ (i18n.ts._permissions as any)[permission] ?? permission }}</li>
<li v-for="permission in plugin.permissions" :key="permission">{{ i18n.ts._permissions[permission] ?? permission }}</li>
<li v-if="!plugin.permissions || plugin.permissions.length === 0">{{ i18n.ts.none }}</li>
</ul>
</template>

View File

@ -31,12 +31,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</SearchMarker>
<SearchMarker :keywords="['device', 'type', 'kind', 'smartphone', 'tablet', 'desktop']">
<MkRadios v-model="overridedDeviceKind">
<MkRadios
v-model="overridedDeviceKind"
:options="[
{ value: null, label: i18n.ts.auto },
{ value: 'smartphone', label: i18n.ts.smartphone, icon: 'ti ti-device-mobile' },
{ value: 'tablet', label: i18n.ts.tablet, icon: 'ti ti-device-tablet' },
{ value: 'desktop', label: i18n.ts.desktop, icon: 'ti ti-device-desktop' },
]"
>
<template #label><SearchLabel>{{ i18n.ts.overridedDeviceKind }}</SearchLabel></template>
<option :value="null">{{ i18n.ts.auto }}</option>
<option value="smartphone"><i class="ti ti-device-mobile"/> {{ i18n.ts.smartphone }}</option>
<option value="tablet"><i class="ti ti-device-tablet"/> {{ i18n.ts.tablet }}</option>
<option value="desktop"><i class="ti ti-device-desktop"/> {{ i18n.ts.desktop }}</option>
</MkRadios>
</SearchMarker>
@ -121,11 +125,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['emoji', 'style', 'native', 'system', 'fluent', 'twemoji']">
<MkPreferenceContainer k="emojiStyle">
<div>
<MkRadios v-model="emojiStyle">
<MkRadios
v-model="emojiStyle"
:options="[
{ value: 'native', label: i18n.ts.native },
{ value: 'fluentEmoji', label: 'Fluent Emoji' },
{ value: 'twemoji', label: 'Twemoji' },
]"
>
<template #label><SearchLabel>{{ i18n.ts.emojiStyle }}</SearchLabel></template>
<option value="native">{{ i18n.ts.native }}</option>
<option value="fluentEmoji">Fluent Emoji</option>
<option value="twemoji">Twemoji</option>
</MkRadios>
<div style="margin: 8px 0 0 0; font-size: 1.5em;"><Mfm :key="emojiStyle" text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></div>
</div>
@ -240,11 +248,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['reaction', 'size', 'scale', 'display']">
<MkPreferenceContainer k="reactionsDisplaySize">
<MkRadios v-model="reactionsDisplaySize">
<MkRadios
v-model="reactionsDisplaySize"
:options="[
{ value: 'small', label: i18n.ts.small },
{ value: 'medium', label: i18n.ts.medium },
{ value: 'large', label: i18n.ts.large },
]"
>
<template #label><SearchLabel>{{ i18n.ts.reactionsDisplaySize }}</SearchLabel></template>
<option value="small">{{ i18n.ts.small }}</option>
<option value="medium">{{ i18n.ts.medium }}</option>
<option value="large">{{ i18n.ts.large }}</option>
</MkRadios>
</MkPreferenceContainer>
</SearchMarker>
@ -259,12 +271,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'list', 'size', 'height']">
<MkPreferenceContainer k="mediaListWithOneImageAppearance">
<MkRadios v-model="mediaListWithOneImageAppearance">
<MkRadios
v-model="mediaListWithOneImageAppearance"
:options="[
{ value: 'expand', label: i18n.ts.default },
{ value: '16_9', label: i18n.tsx.limitTo({ x: '16:9' }) },
{ value: '1_1', label: i18n.tsx.limitTo({ x: '1:1' }) },
{ value: '2_3', label: i18n.tsx.limitTo({ x: '2:3' }) },
]"
>
<template #label><SearchLabel>{{ i18n.ts.mediaListWithOneImageAppearance }}</SearchLabel></template>
<option value="expand">{{ i18n.ts.default }}</option>
<option value="16_9">{{ i18n.tsx.limitTo({ x: '16:9' }) }}</option>
<option value="1_1">{{ i18n.tsx.limitTo({ x: '1:1' }) }}</option>
<option value="2_3">{{ i18n.tsx.limitTo({ x: '2:3' }) }}</option>
</MkRadios>
</MkPreferenceContainer>
</SearchMarker>
@ -394,22 +410,30 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['position']">
<MkPreferenceContainer k="notificationPosition">
<MkRadios v-model="notificationPosition">
<MkRadios
v-model="notificationPosition"
:options="[
{ value: 'leftTop', label: i18n.ts.leftTop, icon: 'ti ti-align-box-left-top' },
{ value: 'rightTop', label: i18n.ts.rightTop, icon: 'ti ti-align-box-right-top' },
{ value: 'leftBottom', label: i18n.ts.leftBottom, icon: 'ti ti-align-box-left-bottom' },
{ value: 'rightBottom', label: i18n.ts.rightBottom, icon: 'ti ti-align-box-right-bottom' },
]"
>
<template #label><SearchLabel>{{ i18n.ts.position }}</SearchLabel></template>
<option value="leftTop"><i class="ti ti-align-box-left-top"></i> {{ i18n.ts.leftTop }}</option>
<option value="rightTop"><i class="ti ti-align-box-right-top"></i> {{ i18n.ts.rightTop }}</option>
<option value="leftBottom"><i class="ti ti-align-box-left-bottom"></i> {{ i18n.ts.leftBottom }}</option>
<option value="rightBottom"><i class="ti ti-align-box-right-bottom"></i> {{ i18n.ts.rightBottom }}</option>
</MkRadios>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['stack', 'axis', 'direction']">
<MkPreferenceContainer k="notificationStackAxis">
<MkRadios v-model="notificationStackAxis">
<MkRadios
v-model="notificationStackAxis"
:options="[
{ value: 'vertical', label: i18n.ts.vertical, icon: 'ti ti-carousel-vertical' },
{ value: 'horizontal', label: i18n.ts.horizontal, icon: 'ti ti-carousel-horizontal' },
]"
>
<template #label><SearchLabel>{{ i18n.ts.stackAxis }}</SearchLabel></template>
<option value="vertical"><i class="ti ti-carousel-vertical"></i> {{ i18n.ts.vertical }}</option>
<option value="horizontal"><i class="ti ti-carousel-horizontal"></i> {{ i18n.ts.horizontal }}</option>
</MkRadios>
</MkPreferenceContainer>
</SearchMarker>
@ -578,12 +602,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</SearchMarker>
<SearchMarker :keywords="['font', 'size']">
<MkRadios v-model="fontSize">
<MkRadios
v-model="fontSize"
:options="[
{ value: null, label: 'Aa', labelStyle: 'font-size: 14px;' },
{ value: '1', label: 'Aa', labelStyle: 'font-size: 15px;' },
{ value: '2', label: 'Aa', labelStyle: 'font-size: 16px;' },
{ value: '3', label: 'Aa', labelStyle: 'font-size: 17px;' },
]"
>
<template #label><SearchLabel>{{ i18n.ts.fontSize }}</SearchLabel></template>
<option :value="null"><span style="font-size: 14px;">Aa</span></option>
<option value="1"><span style="font-size: 15px;">Aa</span></option>
<option value="2"><span style="font-size: 16px;">Aa</span></option>
<option value="3"><span style="font-size: 17px;">Aa</span></option>
</MkRadios>
</SearchMarker>
@ -792,10 +820,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker>
<MkPreferenceContainer k="hemisphere">
<MkRadios v-model="hemisphere">
<MkRadios
v-model="hemisphere"
:options="[
{ value: 'N', label: i18n.ts._hemisphere.N },
{ value: 'S', label: i18n.ts._hemisphere.S },
]"
>
<template #label><SearchLabel>{{ i18n.ts.hemisphere }}</SearchLabel></template>
<option value="N">{{ i18n.ts._hemisphere.N }}</option>
<option value="S">{{ i18n.ts._hemisphere.S }}</option>
<template #caption>{{ i18n.ts._hemisphere.caption }}</template>
</MkRadios>
</MkPreferenceContainer>
@ -925,7 +957,7 @@ const contextMenu = prefer.model('contextMenu');
const menuStyle = prefer.model('menuStyle');
const makeEveryTextElementsSelectable = prefer.model('makeEveryTextElementsSelectable');
const fontSize = ref(miLocalStorage.getItem('fontSize'));
const fontSize = ref(miLocalStorage.getItem('fontSize') as '1' | '2' | '3' | null);
const useSystemFont = ref(miLocalStorage.getItem('useSystemFont') != null);
watch(lang, () => {
@ -1051,7 +1083,7 @@ function removePinnedList() {
function enableAllDataSaver() {
const g = { ...prefer.s.dataSaver };
Object.keys(g).forEach((key) => { (g as any)[key] = true; });
(Object.keys(g) as (keyof typeof g)[]).forEach((key) => { g[key] = true; });
dataSaver.value = g;
}
@ -1059,7 +1091,7 @@ function enableAllDataSaver() {
function disableAllDataSaver() {
const g = { ...prefer.s.dataSaver };
Object.keys(g).forEach((key) => { (g as any)[key] = false; });
(Object.keys(g) as (keyof typeof g)[]).forEach((key) => { g[key] = false; });
dataSaver.value = g;
}

View File

@ -17,13 +17,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>Black</template>
</MkSwitch>
<MkRadios v-model="statusbar.size">
<MkRadios
v-model="statusbar.size"
:options="[
{ value: 'verySmall', label: i18n.ts.small + '+' },
{ value: 'small', label: i18n.ts.small },
{ value: 'medium', label: i18n.ts.medium },
{ value: 'large', label: i18n.ts.large },
{ value: 'veryLarge', label: i18n.ts.large + '+' },
]"
>
<template #label>{{ i18n.ts.size }}</template>
<option value="verySmall">{{ i18n.ts.small }}+</option>
<option value="small">{{ i18n.ts.small }}</option>
<option value="medium">{{ i18n.ts.medium }}</option>
<option value="large">{{ i18n.ts.large }}</option>
<option value="veryLarge">{{ i18n.ts.large }}+</option>
</MkRadios>
<template v-if="statusbar.type === 'rss'">

View File

@ -26,7 +26,7 @@ export type Plugin = {
version: string;
author?: string;
description?: string;
permissions?: string[];
permissions?: (typeof Misskey.permissions)[number][];
};
export type AiScriptPluginMeta = {
@ -34,7 +34,7 @@ export type AiScriptPluginMeta = {
version: string;
author: string;
description?: string;
permissions?: string[];
permissions?: (typeof Misskey.permissions)[number][];
config?: Record<string, any>;
};
@ -97,7 +97,7 @@ export async function parsePluginMeta(code: string): Promise<AiScriptPluginMeta>
version: version as string,
author: author as string,
description: description as string | undefined,
permissions: permissions as string[] | undefined,
permissions: permissions as (typeof Misskey.permissions)[number][] | undefined,
config: config as Record<string, any> | undefined,
};
}

View File

@ -12,6 +12,7 @@ import { deckStore } from '@/ui/deck/deck-store.js';
import { unisonReload } from '@/utility/unison-reload.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import type { SoundStore } from '@/preferences/def.js';
// TODO: そのうち消す
export function migrateOldSettings() {
@ -126,10 +127,10 @@ export function migrateOldSettings() {
prefer.commit('sound.masterVolume', store.s.sound_masterVolume);
prefer.commit('sound.notUseSound', store.s.sound_notUseSound);
prefer.commit('sound.useSoundOnlyWhenActive', store.s.sound_useSoundOnlyWhenActive);
prefer.commit('sound.on.note', store.s.sound_note as any);
prefer.commit('sound.on.noteMy', store.s.sound_noteMy as any);
prefer.commit('sound.on.notification', store.s.sound_notification as any);
prefer.commit('sound.on.reaction', store.s.sound_reaction as any);
prefer.commit('sound.on.note', store.s.sound_note as SoundStore);
prefer.commit('sound.on.noteMy', store.s.sound_noteMy as SoundStore);
prefer.commit('sound.on.notification', store.s.sound_notification as SoundStore);
prefer.commit('sound.on.reaction', store.s.sound_reaction as SoundStore);
prefer.commit('defaultNoteVisibility', store.s.defaultNoteVisibility);
prefer.commit('defaultNoteLocalOnly', store.s.defaultNoteLocalOnly);

View File

@ -237,7 +237,7 @@ export const PREF_DEF = definePreferences({
default: false,
},
emojiStyle: {
default: 'twemoji', // twemoji / fluentEmoji / native
default: 'twemoji' as 'native' | 'fluentEmoji' | 'twemoji',
},
menuStyle: {
default: 'auto' as 'auto' | 'popup' | 'drawer',
@ -503,7 +503,7 @@ export const PREF_DEF = definePreferences({
default: true,
},
'deck.columnAlign': {
default: 'center' as 'left' | 'right' | 'center',
default: 'center' as 'left' | 'center',
},
'deck.columnGap': {
default: 6,

View File

@ -3,16 +3,12 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { markRaw, ref } from 'vue';
import { markRaw } from 'vue';
import * as Misskey from 'misskey-js';
import lightTheme from '@@/themes/l-light.json5';
import darkTheme from '@@/themes/d-green-lime.json5';
import { prefersReducedMotion } from '@@/js/config.js';
import { hemisphere } from '@@/js/intl-const.js';
import type { DeviceKind } from '@/utility/device-kind.js';
import type { Plugin } from '@/plugin.js';
import type { TIPS } from '@/tips.js';
import { miLocalStorage } from '@/local-storage.js';
import { Pizzax } from '@/lib/pizzax.js';
import { DEFAULT_DEVICE_KIND } from '@/utility/device-kind.js';
@ -83,7 +79,7 @@ export const store = markRaw(new Pizzax('base', {
},
menuDisplay: {
where: 'device',
default: 'sideFull' as 'sideFull' | 'sideIcon' | 'top',
default: 'sideFull' as 'sideFull' | 'sideIcon'/* | 'top' */,
},
postFormWithHashtags: {
where: 'device',
@ -257,7 +253,7 @@ export const store = markRaw(new Pizzax('base', {
},
emojiStyle: {
where: 'device',
default: 'twemoji', // twemoji / fluentEmoji / native
default: 'twemoji' as 'twemoji' | 'fluentEmoji' | 'native',
},
menuStyle: {
where: 'device',

View File

@ -6,10 +6,11 @@
import * as Misskey from 'misskey-js';
import type { Component, ComputedRef, Ref, MaybeRef } from 'vue';
import type { ComponentProps as CP } from 'vue-component-type-helpers';
import type { OptionValue } from '@/types/option-value.js';
type ComponentProps<T extends Component> = { [K in keyof CP<T>]: MaybeRef<CP<T>[K]> };
type MenuRadioOptionsDef = Record<string, any>;
type MenuRadioOptionsDef = Record<string, OptionValue>;
type Text = string | ComputedRef<string>;

View File

@ -0,0 +1,6 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export type OptionValue = string | number | null;

View File

@ -12,6 +12,15 @@ import { popup } from '@/os.js';
export type SuggestionType = 'user' | 'hashtag' | 'emoji' | 'mfmTag' | 'mfmParam';
type CompleteProps<T extends keyof CompleteInfo> = {
type: T;
value: CompleteInfo[T]['payload'];
};
function isCompleteType<T extends keyof CompleteInfo>(expectedType: T, props: CompleteProps<keyof CompleteInfo>): props is CompleteProps<T> {
return props.type === expectedType;
}
export class Autocomplete {
private suggestion: {
x: Ref<number>;
@ -253,19 +262,19 @@ export class Autocomplete {
/**
*
*/
private complete<T extends keyof CompleteInfo>({ type, value }: { type: T; value: CompleteInfo[T]['payload'] }) {
private complete<T extends keyof CompleteInfo>(props: CompleteProps<T>) {
this.close();
const caret = Number(this.textarea.selectionStart);
if (type === 'user') {
if (isCompleteType('user', props)) {
const source = this.text;
const before = source.substring(0, caret);
const trimmedBefore = before.substring(0, before.lastIndexOf('@'));
const after = source.substring(caret);
const acct = value.host === null ? value.username : `${value.username}@${toASCII(value.host)}`;
const acct = props.value.host === null ? props.value.username : `${props.value.username}@${toASCII(props.value.host)}`;
// 挿入
this.text = `${trimmedBefore}@${acct} ${after}`;
@ -276,7 +285,7 @@ export class Autocomplete {
const pos = trimmedBefore.length + (acct.length + 2);
this.textarea.setSelectionRange(pos, pos);
});
} else if (type === 'hashtag') {
} else if (isCompleteType('hashtag', props)) {
const source = this.text;
const before = source.substring(0, caret);
@ -284,15 +293,15 @@ export class Autocomplete {
const after = source.substring(caret);
// 挿入
this.text = `${trimmedBefore}#${value} ${after}`;
this.text = `${trimmedBefore}#${props.value} ${after}`;
// キャレットを戻す
nextTick(() => {
this.textarea.focus();
const pos = trimmedBefore.length + (value.length + 2);
const pos = trimmedBefore.length + (props.value.length + 2);
this.textarea.setSelectionRange(pos, pos);
});
} else if (type === 'emoji') {
} else if (isCompleteType('emoji', props)) {
const source = this.text;
const before = source.substring(0, caret);
@ -300,15 +309,15 @@ export class Autocomplete {
const after = source.substring(caret);
// 挿入
this.text = trimmedBefore + value + after;
this.text = trimmedBefore + props.value + after;
// キャレットを戻す
nextTick(() => {
this.textarea.focus();
const pos = trimmedBefore.length + value.length;
const pos = trimmedBefore.length + props.value.length;
this.textarea.setSelectionRange(pos, pos);
});
} else if (type === 'emojiComplete') {
} else if (isCompleteType('emojiComplete', props)) {
const source = this.text;
const before = source.substring(0, caret);
@ -316,15 +325,15 @@ export class Autocomplete {
const after = source.substring(caret);
// 挿入
this.text = trimmedBefore + value + after;
this.text = trimmedBefore + props.value + after;
// キャレットを戻す
nextTick(() => {
this.textarea.focus();
const pos = trimmedBefore.length + value.length;
const pos = trimmedBefore.length + props.value.length;
this.textarea.setSelectionRange(pos, pos);
});
} else if (type === 'mfmTag') {
} else if (isCompleteType('mfmTag', props)) {
const source = this.text;
const before = source.substring(0, caret);
@ -332,15 +341,15 @@ export class Autocomplete {
const after = source.substring(caret);
// 挿入
this.text = `${trimmedBefore}$[${value} ]${after}`;
this.text = `${trimmedBefore}$[${props.value} ]${after}`;
// キャレットを戻す
nextTick(() => {
this.textarea.focus();
const pos = trimmedBefore.length + (value.length + 3);
const pos = trimmedBefore.length + (props.value.length + 3);
this.textarea.setSelectionRange(pos, pos);
});
} else if (type === 'mfmParam') {
} else if (isCompleteType('mfmParam', props)) {
const source = this.text;
const before = source.substring(0, caret);
@ -348,12 +357,12 @@ export class Autocomplete {
const after = source.substring(caret);
// 挿入
this.text = `${trimmedBefore}.${value}${after}`;
this.text = `${trimmedBefore}.${props.value}${after}`;
// キャレットを戻す
nextTick(() => {
this.textarea.focus();
const pos = trimmedBefore.length + (value.length + 1);
const pos = trimmedBefore.length + (props.value.length + 1);
this.textarea.setSelectionRange(pos, pos);
});
}

View File

@ -4,7 +4,7 @@
*/
import * as Misskey from 'misskey-js';
import type { OptionValue } from '@/components/MkSelect.vue';
import type { OptionValue } from '@/types/option-value.js';
export type EnumItem = string | {
label: string;
@ -45,18 +45,18 @@ export interface BooleanFormItem extends FormItemBase {
export interface EnumFormItem extends FormItemBase {
type: 'enum';
default?: string | null;
default?: OptionValue | null;
required?: boolean;
enum: EnumItem[];
}
export interface RadioFormItem extends FormItemBase {
type: 'radio';
default?: unknown | null;
default?: OptionValue | null;
required?: boolean;
options: {
label: string;
value: unknown;
value: OptionValue;
}[];
}

View File

@ -16,7 +16,7 @@ import { i18n } from '@/i18n.js';
import type { WidgetComponentProps, WidgetComponentEmits, WidgetComponentExpose } from './widget.js';
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
const name = 'ai';
const name = 'aichan';
const widgetPropsDef = {
transparent: {

View File

@ -93,12 +93,12 @@ const { widgetProps, configure, save } = useWidgetPropsManager(name,
const menuOpened = ref(false);
const headerTitle = computed<string>(() => {
if (widgetProps.src === 'list' && widgetProps.list != null) {
return widgetProps.list.name;
} else if (widgetProps.src === 'antenna' && widgetProps.antenna != null) {
return widgetProps.antenna.name;
if (widgetProps.src === 'list') {
return widgetProps.list != null ? widgetProps.list.name : '?';
} else if (widgetProps.src === 'antenna') {
return widgetProps.antenna != null ? widgetProps.antenna.name : '?';
} else {
return (i18n.ts._timelines as any)[widgetProps.src] ?? '?';
return i18n.ts._timelines[widgetProps.src] ?? '?';
}
});

View File

@ -36,7 +36,7 @@ import { misskeyApiGet } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js';
import { prefer } from '@/preferences.js';
const name = 'hashtags';
const name = 'trends';
const widgetPropsDef = {
showHeader: {

View File

@ -75,3 +75,5 @@ export const widgets = [
...federationWidgets,
] as const;
export type WidgetName = typeof widgets[number];

View File

@ -10,6 +10,7 @@ import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
import { getDefaultFormValues } from '@/utility/form.js';
import * as os from '@/os.js';
import { deepClone } from '@/utility/clone.js';
import type { WidgetName } from './index.js';
export type Widget<P extends Record<string, unknown>> = {
id: string;
@ -31,7 +32,7 @@ export type WidgetComponentExpose = {
};
export const useWidgetPropsManager = <F extends FormWithDefault>(
name: string,
name: WidgetName,
propsDef: F,
props: Readonly<WidgetComponentProps<GetFormResultType<F>>>,
emit: WidgetComponentEmits<GetFormResultType<F>>,

View File

@ -2230,6 +2230,7 @@ declare namespace entities {
MetaLite,
MetaDetailedOnly,
MetaDetailed,
MetaClientOptions,
UserWebhook,
SystemWebhook,
AbuseReportNotificationRecipient,
@ -2820,6 +2821,9 @@ type MeDetailed = components['schemas']['MeDetailed'];
// @public (undocumented)
type MeDetailedOnly = components['schemas']['MeDetailedOnly'];
// @public (undocumented)
type MetaClientOptions = components['schemas']['MetaClientOptions'];
// @public (undocumented)
type MetaDetailed = components['schemas']['MetaDetailed'];

View File

@ -58,6 +58,7 @@ export type ReversiGameDetailed = components['schemas']['ReversiGameDetailed'];
export type MetaLite = components['schemas']['MetaLite'];
export type MetaDetailedOnly = components['schemas']['MetaDetailedOnly'];
export type MetaDetailed = components['schemas']['MetaDetailed'];
export type MetaClientOptions = components['schemas']['MetaClientOptions'];
export type UserWebhook = components['schemas']['UserWebhook'];
export type SystemWebhook = components['schemas']['SystemWebhook'];
export type AbuseReportNotificationRecipient = components['schemas']['AbuseReportNotificationRecipient'];

View File

@ -5373,7 +5373,8 @@ export type components = {
/** Format: id */
timeoutUserId: string | null;
black: number | null;
bw: string;
/** @enum {string} */
bw: 'random' | '1' | '2';
noIrregularRules: boolean;
isLlotheo: boolean;
canPutEverywhere: boolean;
@ -5409,7 +5410,8 @@ export type components = {
/** Format: id */
timeoutUserId: string | null;
black: number | null;
bw: string;
/** @enum {string} */
bw: 'random' | '1' | '2';
noIrregularRules: boolean;
isLlotheo: boolean;
canPutEverywhere: boolean;
@ -5439,7 +5441,7 @@ export type components = {
feedbackUrl: string | null;
defaultDarkTheme: string | null;
defaultLightTheme: string | null;
clientOptions: Record<string, never>;
clientOptions: components['schemas']['MetaClientOptions'];
disableRegistration: boolean;
emailRequiredForSignup: boolean;
enableHcaptcha: boolean;
@ -5538,6 +5540,12 @@ export type components = {
cacheRemoteSensitiveFiles: boolean;
};
MetaDetailed: components['schemas']['MetaLite'] & components['schemas']['MetaDetailedOnly'];
MetaClientOptions: {
/** @enum {string} */
entrancePageStyle: 'classic' | 'simple';
showTimelineForVisitor: boolean;
showActivitiesForVisitor: boolean;
};
UserWebhook: {
/** Format: id */
id: string;
@ -9466,7 +9474,7 @@ export interface operations {
deeplIsPro: boolean;
defaultDarkTheme: string | null;
defaultLightTheme: string | null;
clientOptions: Record<string, never>;
clientOptions: components['schemas']['MetaClientOptions'];
description: string | null;
disableRegistration: boolean;
impressumUrl: string | null;
@ -12722,7 +12730,12 @@ export interface operations {
description?: string | null;
defaultLightTheme?: string | null;
defaultDarkTheme?: string | null;
clientOptions?: Record<string, never>;
clientOptions?: {
/** @enum {string} */
entrancePageStyle?: 'classic' | 'simple';
showTimelineForVisitor?: boolean;
showActivitiesForVisitor?: boolean;
};
cacheRemoteFiles?: boolean;
cacheRemoteSensitiveFiles?: boolean;
emailRequiredForSignup?: boolean;