Merge pull request MisskeyIO#355 from merge-upstream

This commit is contained in:
まっちゃとーにゅ 2024-01-14 02:55:39 +09:00 committed by GitHub
commit 8703ebfa85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
106 changed files with 4931 additions and 687 deletions

View File

@ -32,8 +32,8 @@ jobs:
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'
- name: Install swagger-cli - name: Install Redocly CLI
run: npm i -g swagger-cli run: npm i -g @redocly/cli
- run: corepack enable - run: corepack enable
- run: pnpm i --frozen-lockfile - run: pnpm i --frozen-lockfile
- name: Check pnpm-lock.yaml - name: Check pnpm-lock.yaml
@ -43,4 +43,4 @@ jobs:
- name: Build and generate - name: Build and generate
run: pnpm build && pnpm --filter backend generate-api-json run: pnpm build && pnpm --filter backend generate-api-json
- name: Validation - name: Validation
run: swagger-cli validate ./packages/backend/built/api.json run: npx @redocly/cli lint --extends=minimal ./packages/backend/built/api.json

View File

@ -20,6 +20,8 @@
### Client ### Client
- Feat: 新しいゲームを追加 - Feat: 新しいゲームを追加
- Feat: 絵文字の詳細ダイアログを追加
- Feat: 枠線をつけるMFM`$[border.width=1,style=solid,color=fff,radius=0 ...]`を追加
- Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように - Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように
- Enhance: チャンネルノートのピン留めをノートのメニューからできるように - Enhance: チャンネルノートのピン留めをノートのメニューからできるように
- Enhance: 管理者の場合はAPI tokenの発行画面で管理機能に関する権限を付与できるように - Enhance: 管理者の場合はAPI tokenの発行画面で管理機能に関する権限を付与できるように
@ -35,7 +37,10 @@
- Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました - Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました
- Enhance: ActivityPub Deliver queueでBodyを事前処理するように (#12916) - Enhance: ActivityPub Deliver queueでBodyを事前処理するように (#12916)
- Enhance: クリップをエクスポートできるように - Enhance: クリップをエクスポートできるように
- Enhance: `api.json`のOpenAPI Specificationを3.1.0に更新
- Fix: `drive/files/update`でファイル名のバリデーションが機能していない問題を修正 - Fix: `drive/files/update`でファイル名のバリデーションが機能していない問題を修正
- Fix: `notes/create`で、`text`が空白文字のみで構成されているか`null`であって、かつ`text`だけであるリクエストに対するレスポンスが400になるように変更
- Fix: `notes/create`で、`text`が空白文字のみで構成されていてかつリノート、ファイルまたは投票を含んでいるリクエストに対するレスポンスの`text`が`""`から`null`になるように変更
## 2023.12.2 ## 2023.12.2

2
locales/index.d.ts vendored
View File

@ -1207,6 +1207,8 @@ export interface Locale {
"replay": string; "replay": string;
"replaying": string; "replaying": string;
"ranking": string; "ranking": string;
"lastNDays": string;
"backToTitle": string;
"abuseReportCategory": string; "abuseReportCategory": string;
"selectCategory": string; "selectCategory": string;
"reportComplete": string; "reportComplete": string;

View File

@ -1204,6 +1204,8 @@ showReplay: "リプレイを見る"
replay: "リプレイ" replay: "リプレイ"
replaying: "リプレイ中" replaying: "リプレイ中"
ranking: "ランキング" ranking: "ランキング"
lastNDays: "直近{n}日"
backToTitle: "タイトルへ"
abuseReportCategory: "通報の種類" abuseReportCategory: "通報の種類"
selectCategory: "カテゴリを選択" selectCategory: "カテゴリを選択"
reportComplete: "通報完了" reportComplete: "通報完了"

View File

@ -331,6 +331,9 @@ export class NoteCreateService implements OnApplicationShutdown {
data.text = data.text.slice(0, DB_MAX_NOTE_TEXT_LENGTH); data.text = data.text.slice(0, DB_MAX_NOTE_TEXT_LENGTH);
} }
data.text = data.text.trim(); data.text = data.text.trim();
if (data.text === '') {
data.text = null;
}
} else { } else {
data.text = null; data.text = null;
} }

View File

@ -36,7 +36,7 @@ export const paramDef = {
untilId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' },
folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null }, folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
type: { type: 'string', nullable: true, pattern: /^[a-zA-Z\/\-*]+$/.toString().slice(1, -1) }, type: { type: 'string', nullable: true, pattern: /^[a-zA-Z\/\-*]+$/.toString().slice(1, -1) },
sort: { type: 'string', nullable: true, enum: ['+createdAt', '-createdAt', '+name', '-name', '+size', '-size'] }, sort: { type: 'string', nullable: true, enum: ['+createdAt', '-createdAt', '+name', '-name', '+size', '-size', null] },
}, },
required: [], required: [],
} as const; } as const;

View File

@ -60,6 +60,7 @@ export const paramDef = {
'-firstRetrievedAt', '-firstRetrievedAt',
'+latestRequestReceivedAt', '+latestRequestReceivedAt',
'-latestRequestReceivedAt', '-latestRequestReceivedAt',
null,
], ],
}, },
}, },

View File

@ -109,13 +109,13 @@ export const meta = {
items: { items: {
type: 'string', type: 'string',
enum: [ enum: [
"ble", 'ble',
"cable", 'cable',
"hybrid", 'hybrid',
"internal", 'internal',
"nfc", 'nfc',
"smart-card", 'smart-card',
"usb", 'usb',
], ],
}, },
}, },
@ -129,8 +129,8 @@ export const meta = {
authenticatorAttachment: { authenticatorAttachment: {
type: 'string', type: 'string',
enum: [ enum: [
"cross-platform", 'cross-platform',
"platform", 'platform',
], ],
}, },
requireResidentKey: { requireResidentKey: {
@ -139,9 +139,9 @@ export const meta = {
userVerification: { userVerification: {
type: 'string', type: 'string',
enum: [ enum: [
"discouraged", 'discouraged',
"preferred", 'preferred',
"required", 'required',
], ],
}, },
}, },
@ -150,10 +150,11 @@ export const meta = {
type: 'string', type: 'string',
nullable: true, nullable: true,
enum: [ enum: [
"direct", 'direct',
"enterprise", 'enterprise',
"indirect", 'indirect',
"none", 'none',
null,
], ],
}, },
extensions: { extensions: {

View File

@ -34,11 +34,10 @@ describe('api:notes/create', () => {
.toBe(VALID); .toBe(VALID);
}); });
// TODO test('null post', () => {
//test('null post', () => { expect(v({ text: null }))
// expect(v({ text: null })) .toBe(INVALID);
// .toBe(INVALID); });
//});
test('0 characters post', () => { test('0 characters post', () => {
expect(v({ text: '' })) expect(v({ text: '' }))
@ -49,6 +48,11 @@ describe('api:notes/create', () => {
expect(v({ text: await tooLong })) expect(v({ text: await tooLong }))
.toBe(INVALID); .toBe(INVALID);
}); });
test('whitespace-only post', () => {
expect(v({ text: ' ' }))
.toBe(INVALID);
});
}); });
describe('cw', () => { describe('cw', () => {

View File

@ -173,13 +173,33 @@ export const paramDef = {
}, },
}, },
// (re)note with text, files and poll are optional // (re)note with text, files and poll are optional
anyOf: [ if: {
{ required: ['text'] }, properties: {
{ required: ['renoteId'] }, renoteId: {
{ required: ['fileIds'] }, type: 'null',
{ required: ['mediaIds'] }, },
{ required: ['poll'] }, fileIds: {
], type: 'null',
},
mediaIds: {
type: 'null',
},
poll: {
type: 'null',
},
},
},
then: {
properties: {
text: {
type: 'string',
minLength: 1,
maxLength: MAX_NOTE_TEXT_LENGTH,
pattern: '[^\\s]+',
},
},
required: ['text'],
},
} as const; } as const;
@Injectable() @Injectable()

View File

@ -10,7 +10,7 @@ import { schemas, convertSchemaToOpenApiSchema } from './schemas.js';
export function genOpenapiSpec(config: Config) { export function genOpenapiSpec(config: Config) {
const spec = { const spec = {
openapi: '3.0.0', openapi: '3.1.0',
info: { info: {
version: config.version, version: config.version,
@ -56,7 +56,7 @@ export function genOpenapiSpec(config: Config) {
} }
} }
const resSchema = endpoint.meta.res ? convertSchemaToOpenApiSchema(endpoint.meta.res) : {}; const resSchema = endpoint.meta.res ? convertSchemaToOpenApiSchema(endpoint.meta.res, 'res') : {};
let desc = (endpoint.meta.description ? endpoint.meta.description : 'No description provided.') + '\n\n'; let desc = (endpoint.meta.description ? endpoint.meta.description : 'No description provided.') + '\n\n';
@ -71,7 +71,7 @@ export function genOpenapiSpec(config: Config) {
} }
const requestType = endpoint.meta.requireFile ? 'multipart/form-data' : 'application/json'; const requestType = endpoint.meta.requireFile ? 'multipart/form-data' : 'application/json';
const schema = { ...endpoint.params }; const schema = { ...convertSchemaToOpenApiSchema(endpoint.params, 'param') };
if (endpoint.meta.requireFile) { if (endpoint.meta.requireFile) {
schema.properties = { schema.properties = {
@ -210,7 +210,9 @@ export function genOpenapiSpec(config: Config) {
}; };
spec.paths['/' + endpoint.name] = { spec.paths['/' + endpoint.name] = {
...(endpoint.meta.allowGet ? { get: info } : {}), ...(endpoint.meta.allowGet ? {
get: info,
} : {}),
post: info, post: info,
}; };
} }

View File

@ -6,32 +6,35 @@
import type { Schema } from '@/misc/json-schema.js'; import type { Schema } from '@/misc/json-schema.js';
import { refs } from '@/misc/json-schema.js'; import { refs } from '@/misc/json-schema.js';
export function convertSchemaToOpenApiSchema(schema: Schema) { export function convertSchemaToOpenApiSchema(schema: Schema, type: 'param' | 'res') {
// optional, refはスキーマ定義に含まれないので分離しておく // optional, nullable, refはスキーマ定義に含まれないので分離しておく
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const { optional, ref, ...res }: any = schema; const { optional, nullable, ref, ...res }: any = schema;
if (schema.type === 'object' && schema.properties) { if (schema.type === 'object' && schema.properties) {
const required = Object.entries(schema.properties).filter(([k, v]) => !v.optional).map(([k]) => k); if (type === 'res') {
if (required.length > 0) { const required = Object.entries(schema.properties).filter(([k, v]) => !v.optional).map(([k]) => k);
if (required.length > 0) {
// 空配列は許可されない // 空配列は許可されない
res.required = required; res.required = required;
}
} }
for (const k of Object.keys(schema.properties)) { for (const k of Object.keys(schema.properties)) {
res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k]); res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k], type);
} }
} }
if (schema.type === 'array' && schema.items) { if (schema.type === 'array' && schema.items) {
res.items = convertSchemaToOpenApiSchema(schema.items); res.items = convertSchemaToOpenApiSchema(schema.items, type);
} }
if (schema.anyOf) res.anyOf = schema.anyOf.map(convertSchemaToOpenApiSchema); for (const o of ['anyOf', 'oneOf', 'allOf'] as const) {
if (schema.oneOf) res.oneOf = schema.oneOf.map(convertSchemaToOpenApiSchema); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (schema.allOf) res.allOf = schema.allOf.map(convertSchemaToOpenApiSchema); if (o in schema) res[o] = schema[o]!.map(schema => convertSchemaToOpenApiSchema(schema, type));
}
if (schema.ref) { if (type === 'res' && schema.ref) {
const $ref = `#/components/schemas/${schema.ref}`; const $ref = `#/components/schemas/${schema.ref}`;
if (schema.nullable || schema.optional) { if (schema.nullable || schema.optional) {
res.allOf = [{ $ref }]; res.allOf = [{ $ref }];
@ -40,6 +43,14 @@ export function convertSchemaToOpenApiSchema(schema: Schema) {
} }
} }
if (schema.nullable) {
if (Array.isArray(schema.type) && !schema.type.includes('null')) {
res.type.push('null');
} else if (typeof schema.type === 'string') {
res.type = [res.type, 'null'];
}
}
return res; return res;
} }
@ -72,6 +83,6 @@ export const schemas = {
}, },
...Object.fromEntries( ...Object.fromEntries(
Object.entries(refs).map(([key, schema]) => [key, convertSchemaToOpenApiSchema(schema)]), Object.entries(refs).map(([key, schema]) => [key, convertSchemaToOpenApiSchema(schema, 'res')]),
), ),
}; };

View File

@ -43,7 +43,7 @@ html
} }
body body
a#a(href=`https://${host}` target="_blank") a#a(href=`https://${host}` rel="noopener" target="_blank")
header#banner(style=`background-image: url(${meta.bannerUrl})`) header#banner(style=`background-image: url(${meta.bannerUrl})`)
div#title= meta.name || host div#title= meta.name || host
div#content div#content

View File

@ -136,6 +136,19 @@ describe('Note', () => {
assert.strictEqual(res.body.createdNote.renote.text, bobPost.text); assert.strictEqual(res.body.createdNote.renote.text, bobPost.text);
}); });
test('引用renoteで空白文字のみで構成されたtextにするとレスポンスがtext: nullになる', async () => {
const bobPost = await post(bob, {
text: 'test',
});
const res = await api('/notes/create', {
text: ' ',
renoteId: bobPost.id,
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.createdNote.text, null);
});
test('visibility: followersでrenoteできる', async () => { test('visibility: followersでrenoteできる', async () => {
const createRes = await api('/notes/create', { const createRes = await api('/notes/create', {
text: 'test', text: 'test',

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 607 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@ -0,0 +1,102 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModalWindow ref="dialogEl" @close="cancel()" @closed="$emit('closed')">
<template #header>:{{ emoji.name }}:</template>
<template #default>
<MkSpacer>
<div style="display: flex; flex-direction: column; gap: 1em;">
<div :class="$style.emojiImgWrapper">
<MkCustomEmoji :name="emoji.name" :normal="true" style="height: 100%;"></MkCustomEmoji>
</div>
<MkKeyValue>
<template #key>{{ i18n.ts.name }}</template>
<template #value>{{ emoji.name }}</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.tags }}</template>
<template #value>
<div v-if="emoji.aliases.length === 0">{{ i18n.ts.none }}</div>
<div v-else :class="$style.aliases">
<span v-for="alias in emoji.aliases" :key="alias" :class="$style.alias">
{{ alias }}
</span>
</div>
</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.category }}</template>
<template #value>{{ emoji.category ?? i18n.ts.none }}</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.sensitive }}</template>
<template #value>{{ emoji.isSensitive ? i18n.ts.yes : i18n.ts.no }}</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.localOnly }}</template>
<template #value>{{ emoji.localOnly ? i18n.ts.yes : i18n.ts.no }}</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.license }}</template>
<template #value>{{ emoji.license ?? i18n.ts.none }}</template>
</MkKeyValue>
<MkKeyValue :copy="emoji.url">
<template #key>{{ i18n.ts.emojiUrl }}</template>
<template #value>
<a :href="emoji.url" rel="nofollow noopener" target="_blank">{{ emoji.url }}</a>
</template>
</MkKeyValue>
</div>
</MkSpacer>
</template>
</MkModalWindow>
</template>
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
import { defineProps, shallowRef } from 'vue';
import { i18n } from '@/i18n.js';
import MkModalWindow from '@/components/MkModalWindow.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
const props = defineProps<{
emoji: Misskey.entities.EmojiDetailed,
}>();
const emit = defineEmits<{
(ev: 'ok', cropped: Misskey.entities.DriveFile): void;
(ev: 'cancel'): void;
(ev: 'closed'): void;
}>();
const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
const cancel = () => {
emit('cancel');
dialogEl.value!.close();
};
</script>
<style lang="scss" module>
.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);
margin: auto;
overflow-y: hidden;
}
.aliases {
display: flex;
flex-wrap: wrap;
gap: 3px;
}
.alias {
display: inline-block;
padding: 3px 10px;
background-color: var(--X5);
border: solid 1px var(--divider);
border-radius: var(--radius);
}
</style>

View File

@ -15,6 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, nextTick, watch, shallowRef, ref } from 'vue'; import { onMounted, nextTick, watch, shallowRef, ref } from 'vue';
import { Chart } from 'chart.js'; import { Chart } from 'chart.js';
import * as Misskey from 'misskey-js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
@ -23,9 +24,16 @@ import { initChart } from '@/scripts/init-chart.js';
initChart(); initChart();
const props = defineProps<{ export type HeatmapSource = 'active-users' | 'notes' | 'ap-requests-inbox-received' | 'ap-requests-deliver-succeeded' | 'ap-requests-deliver-failed';
src: string;
}>(); const props = withDefaults(defineProps<{
src: HeatmapSource;
user?: Misskey.entities.User;
label?: string;
}>(), {
user: undefined,
label: '',
});
const rootEl = shallowRef<HTMLDivElement>(null); const rootEl = shallowRef<HTMLDivElement>(null);
const chartEl = shallowRef<HTMLCanvasElement>(null); const chartEl = shallowRef<HTMLCanvasElement>(null);
@ -75,8 +83,13 @@ async function renderChart() {
const raw = await misskeyApi('charts/active-users', { limit: chartLimit, span: 'day' }); const raw = await misskeyApi('charts/active-users', { limit: chartLimit, span: 'day' });
values = raw.readWrite; values = raw.readWrite;
} else if (props.src === 'notes') { } else if (props.src === 'notes') {
const raw = await misskeyApi('charts/notes', { limit: chartLimit, span: 'day' }); if (props.user) {
values = raw.local.inc; const raw = await misskeyApi('charts/user/notes', { userId: props.user.id, limit: chartLimit, span: 'day' });
values = raw.inc;
} else {
const raw = await misskeyApi('charts/notes', { limit: chartLimit, span: 'day' });
values = raw.local.inc;
}
} else if (props.src === 'ap-requests-inbox-received') { } else if (props.src === 'ap-requests-inbox-received') {
const raw = await misskeyApi('charts/ap-request', { limit: chartLimit, span: 'day' }); const raw = await misskeyApi('charts/ap-request', { limit: chartLimit, span: 'day' });
values = raw.inboxReceived; values = raw.inboxReceived;
@ -105,7 +118,7 @@ async function renderChart() {
type: 'matrix', type: 'matrix',
data: { data: {
datasets: [{ datasets: [{
label: 'Read & Write', label: props.label,
data: format(values), data: format(values),
pointRadius: 0, pointRadius: 0,
borderWidth: 0, borderWidth: 0,
@ -128,6 +141,9 @@ async function renderChart() {
const a = c.chart.chartArea ?? {}; const a = c.chart.chartArea ?? {};
return (a.bottom - a.top) / 7 - marginEachCell; return (a.bottom - a.top) / 7 - marginEachCell;
}, },
/* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107>
}] satisfies ChartData[],
*/
}], }],
}, },
options: { options: {
@ -195,7 +211,7 @@ async function renderChart() {
}, },
label(context) { label(context) {
const v = context.dataset.data[context.dataIndex]; const v = context.dataset.data[context.dataIndex];
return ['Active: ' + v.v]; return [v.v];
}, },
}, },
//mode: 'index', //mode: 'index',

View File

@ -51,7 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<option value="ap-requests-deliver-failed">AP Requests: deliverFailed</option> <option value="ap-requests-deliver-failed">AP Requests: deliverFailed</option>
</MkSelect> </MkSelect>
<div class="_panel" :class="$style.heatmap"> <div class="_panel" :class="$style.heatmap">
<MkHeatmap :src="heatmapSrc"/> <MkHeatmap :src="heatmapSrc" :label="'Read & Write'"/>
</div> </div>
</MkFoldableSection> </MkFoldableSection>
@ -92,7 +92,7 @@ import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApiGet } from '@/scripts/misskey-api.js'; import { misskeyApiGet } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import MkHeatmap from '@/components/MkHeatmap.vue'; import MkHeatmap, { type HeatmapSource } from '@/components/MkHeatmap.vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue';
import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue'; import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue';
import MkRetentionLineChart from '@/components/MkRetentionLineChart.vue'; import MkRetentionLineChart from '@/components/MkRetentionLineChart.vue';
@ -103,7 +103,7 @@ initChart();
const chartLimit = 500; const chartLimit = 500;
const chartSpan = ref<'hour' | 'day'>('hour'); const chartSpan = ref<'hour' | 'day'>('hour');
const chartSrc = ref('active-users'); const chartSrc = ref('active-users');
const heatmapSrc = ref('active-users'); const heatmapSrc = ref<HeatmapSource>('active-users');
const subDoughnutEl = shallowRef<HTMLCanvasElement>(); const subDoughnutEl = shallowRef<HTMLCanvasElement>();
const pubDoughnutEl = shallowRef<HTMLCanvasElement>(); const pubDoughnutEl = shallowRef<HTMLCanvasElement>();

View File

@ -16,7 +16,7 @@ import * as os from '@/os.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
x: number; x: number;
y: number; y: number;
value?: number; value?: number | string;
}>(), { }>(), {
value: 1, value: 1,
}); });

View File

@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
class="_button" class="_button"
:class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.small]: defaultStore.state.reactionsDisplaySize === 'small', [$style.large]: defaultStore.state.reactionsDisplaySize === 'large' }]" :class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.small]: defaultStore.state.reactionsDisplaySize === 'small', [$style.large]: defaultStore.state.reactionsDisplaySize === 'large' }]"
@click="toggleReaction()" @click="toggleReaction()"
@contextmenu.prevent.stop="menu"
> >
<MkReactionIcon :class="defaultStore.state.limitWidthOfReaction ? $style.limitWidth : ''" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/> <MkReactionIcon :class="defaultStore.state.limitWidthOfReaction ? $style.limitWidth : ''" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/>
<span :class="$style.count">{{ count }}</span> <span :class="$style.count">{{ count }}</span>
@ -21,6 +22,7 @@ import { computed, inject, onMounted, shallowRef, watch } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import XDetails from '@/components/MkReactionsViewer.details.vue'; import XDetails from '@/components/MkReactionsViewer.details.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js'; import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
import { useTooltip } from '@/scripts/use-tooltip.js'; import { useTooltip } from '@/scripts/use-tooltip.js';
@ -98,6 +100,22 @@ async function toggleReaction() {
} }
} }
async function menu(ev) {
if (!canToggle.value) return;
if (!props.reaction.includes(":")) return;
os.popupMenu([{
text: i18n.ts.info,
icon: 'ti ti-info-circle',
action: async () => {
os.popup(MkCustomEmojiDetailedDialog, {
emoji: await misskeyApiGet('emoji', {
name: props.reaction.replace(/:/g, '').replace(/@\./, ''),
}),
});
},
}], ev.currentTarget ?? ev.target);
}
function anime() { function anime() {
if (document.hidden) return; if (document.hidden) return;
if (!defaultStore.state.animation) return; if (!defaultStore.state.animation) return;

View File

@ -34,8 +34,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ tosPrivacyPolicyLabel }}</template> <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(--success)"></i></template>
<div class="_gaps_s"> <div class="_gaps_s">
<div v-if="availableTos"><a :href="instance.tosUrl" class="_link" target="_blank">{{ i18n.ts.termsOfService }} <i class="ti ti-external-link"></i></a></div> <div v-if="availableTos"><a :href="instance.tosUrl" class="_link" rel="nofollow noopener" target="_blank">{{ i18n.ts.termsOfService }} <i class="ti ti-external-link"></i></a></div>
<div v-if="availablePrivacyPolicy"><a :href="instance.privacyPolicyUrl" class="_link" target="_blank">{{ i18n.ts.privacyPolicy }} <i class="ti ti-external-link"></i></a></div> <div v-if="availablePrivacyPolicy"><a :href="instance.privacyPolicyUrl" class="_link" rel="nofollow noopener" target="_blank">{{ i18n.ts.privacyPolicy }} <i class="ti ti-external-link"></i></a></div>
</div> </div>
<MkSwitch :modelValue="agreeTosAndPrivacyPolicy" style="margin-top: 16px;" @update:modelValue="updateAgreeTosAndPrivacyPolicy">{{ i18n.ts.agree }}</MkSwitch> <MkSwitch :modelValue="agreeTosAndPrivacyPolicy" style="margin-top: 16px;" @update:modelValue="updateAgreeTosAndPrivacyPolicy">{{ i18n.ts.agree }}</MkSwitch>
@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.basicNotesBeforeCreateAccount }}</template> <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(--success)"></i></template>
<a href="https://misskey-hub.net/docs/for-users/onboarding/warning/" class="_link" target="_blank">{{ i18n.ts.basicNotesBeforeCreateAccount }} <i class="ti ti-external-link"></i></a> <a href="https://misskey-hub.net/docs/for-users/onboarding/warning/" class="_link" rel="nofollow noopener" target="_blank">{{ i18n.ts.basicNotesBeforeCreateAccount }} <i class="ti ti-external-link"></i></a>
<MkSwitch :modelValue="agreeNote" style="margin-top: 16px;" data-cy-signup-rules-notes-agree @update:modelValue="updateAgreeNote">{{ i18n.ts.agree }}</MkSwitch> <MkSwitch :modelValue="agreeNote" style="margin-top: 16px;" data-cy-signup-rules-notes-agree @update:modelValue="updateAgreeNote">{{ i18n.ts.agree }}</MkSwitch>
</MkFolder> </MkFolder>

View File

@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.divider"></div> <div :class="$style.divider"></div>
<I18n :src="i18n.ts._initialTutorial._timeline.description3" tag="div" style="padding: 0 16px;"> <I18n :src="i18n.ts._initialTutorial._timeline.description3" tag="div" style="padding: 0 16px;">
<template #link> <template #link>
<a href="https://misskey-hub.net/docs/for-users/features/timeline/" target="_blank" class="_link">{{ i18n.ts.help }}</a> <a href="https://misskey-hub.net/docs/for-users/features/timeline/" rel="nofollow noopener" target="_blank" class="_link">{{ i18n.ts.help }}</a>
</template> </template>
</I18n> </I18n>

View File

@ -130,7 +130,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div style="font-size: 120%;">{{ i18n.ts._initialTutorial._done.title }}</div> <div style="font-size: 120%;">{{ i18n.ts._initialTutorial._done.title }}</div>
<I18n :src="i18n.ts._initialTutorial._done.description" tag="div" style="padding: 0 16px;"> <I18n :src="i18n.ts._initialTutorial._done.description" tag="div" style="padding: 0 16px;">
<template #link> <template #link>
<a href="https://misskey-hub.net/docs/for-users/" target="_blank" class="_link">{{ i18n.ts.help }}</a> <a href="https://misskey-hub.net/docs/for-users/" rel="nofollow noopener" target="_blank" class="_link">{{ i18n.ts.help }}</a>
</template> </template>
</I18n> </I18n>
<div>{{ i18n.t('_initialAccountSetting.haveFun', { name: instance.name ?? host }) }}</div> <div>{{ i18n.t('_initialAccountSetting.haveFun', { name: instance.name ?? host }) }}</div>

View File

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<div :class="[$style.root, { [$style.inline]: inline }]"> <div :class="[$style.root, { [$style.inline]: inline }]">
<a v-if="external" :class="$style.main" class="_button" :href="to" target="_blank"> <a v-if="external" :class="$style.main" class="_button" :href="to" rel="nofollow noopener" target="_blank">
<span :class="$style.icon"><slot name="icon"></slot></span> <span :class="$style.icon"><slot name="icon"></slot></span>
<span :class="$style.text"><slot></slot></span> <span :class="$style.text"><slot></slot></span>
<span :class="$style.suffix"> <span :class="$style.suffix">

View File

@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
[$style.form_vertical]: chosen.place === 'vertical', [$style.form_vertical]: chosen.place === 'vertical',
}]" }]"
> >
<a :href="chosen.url" target="_blank" :class="$style.link"> <a :href="chosen.url" rel="noopener" target="_blank" :class="$style.link">
<img :src="chosen.imageUrl" :class="$style.img"> <img :src="chosen.imageUrl" :class="$style.img">
<button class="_button" :class="$style.i" @click.prevent.stop="toggleMenu"><i :class="$style.iIcon" class="ti ti-info-circle"></i></button> <button class="_button" :class="$style.i" @click.prevent.stop="toggleMenu"><i :class="$style.iIcon" class="ti ti-info-circle"></i></button>
</a> </a>

View File

@ -24,9 +24,11 @@ import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy.js'
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { customEmojisMap } from '@/custom-emojis.js'; import { customEmojisMap } from '@/custom-emojis.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApiGet } from '@/scripts/misskey-api.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import * as sound from '@/scripts/sound.js'; import * as sound from '@/scripts/sound.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
const props = defineProps<{ const props = defineProps<{
name: string; name: string;
@ -93,7 +95,19 @@ function onClick(ev: MouseEvent) {
react(`:${props.name}:`); react(`:${props.name}:`);
sound.playMisskeySfx('reaction'); sound.playMisskeySfx('reaction');
}, },
}] : [])], ev.currentTarget ?? ev.target); }] : []), {
text: i18n.ts.info,
icon: 'ti ti-info-circle',
action: async () => {
os.popup(MkCustomEmojiDetailedDialog, {
emoji: await misskeyApiGet('emoji', {
name: customEmojiName.value,
}),
}, {
anchor: ev.target,
});
},
}], ev.currentTarget ?? ev.target);
} }
} }
</script> </script>

View File

@ -59,7 +59,12 @@ export default function(props: MfmProps, context: SetupContext<MfmEvents>) {
const validTime = (t: string | null | undefined) => { const validTime = (t: string | null | undefined) => {
if (t == null) return null; if (t == null) return null;
return t.match(/^[0-9.]+s$/) ? t : null; return RegExp(/^[0-9.]+s$/).exec(t) ? t : null;
};
const validColor = (c: string | null | undefined): string | null => {
if (c == null) return null;
return RegExp(/^[0-9a-f]{3,6}$/i).exec(c) ? c : null;
}; };
const useAnim = defaultStore.state.advancedMfm && defaultStore.state.animatedMfm; const useAnim = defaultStore.state.advancedMfm && defaultStore.state.animatedMfm;
@ -240,17 +245,30 @@ export default function(props: MfmProps, context: SetupContext<MfmEvents>) {
break; break;
} }
case 'fg': { case 'fg': {
let color = token.props.args.color; let color = validColor(token.props.args.color);
if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00'; color = color ?? 'f00';
style = `color: #${color}; overflow-wrap: anywhere;`; style = `color: #${color}; overflow-wrap: anywhere;`;
break; break;
} }
case 'bg': { case 'bg': {
let color = token.props.args.color; let color = validColor(token.props.args.color);
if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00'; color = color ?? 'f00';
style = `background-color: #${color}; overflow-wrap: anywhere;`; style = `background-color: #${color}; overflow-wrap: anywhere;`;
break; break;
} }
case 'border': {
let color = validColor(token.props.args.color);
color = color ? `#${color}` : 'var(--accent)';
let b_style = token.props.args.style;
if (
!['hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset']
.includes(b_style)
) b_style = 'solid';
const width = parseFloat(token.props.args.width ?? '1');
const radius = parseFloat(token.props.args.radius ?? '0');
style = `border: ${width}px ${b_style} ${color}; border-radius: ${radius}px`;
break;
}
case 'ruby': { case 'ruby': {
if (token.children.length === 1) { if (token.children.length === 1) {
const child = token.children[0]; const child = token.children[0];

View File

@ -112,4 +112,4 @@ export const DEFAULT_SERVER_ERROR_IMAGE_URL = 'https://xn--931a.moe/assets/error
export const DEFAULT_NOT_FOUND_IMAGE_URL = 'https://xn--931a.moe/assets/not-found.jpg'; export const DEFAULT_NOT_FOUND_IMAGE_URL = 'https://xn--931a.moe/assets/not-found.jpg';
export const DEFAULT_INFO_IMAGE_URL = 'https://xn--931a.moe/assets/info.jpg'; export const DEFAULT_INFO_IMAGE_URL = 'https://xn--931a.moe/assets/info.jpg';
export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'position', 'fg', 'bg', 'font', 'blur', 'rainbow', 'sparkle', 'rotate', 'ruby', 'unixtime']; export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'position', 'fg', 'bg', 'border', 'font', 'blur', 'rainbow', 'sparkle', 'rotate', 'ruby', 'unixtime'];

View File

@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<button v-if="thereIsTreasure" class="_button treasure" @click="getTreasure"><img src="/fluent-emoji/1f3c6.png" class="treasureImg"></button> <button v-if="thereIsTreasure" class="_button treasure" @click="getTreasure"><img src="/fluent-emoji/1f3c6.png" class="treasureImg"></button>
</div> </div>
<div style="text-align: center;"> <div style="text-align: center;">
{{ i18n.ts._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/about-misskey/" target="_blank" class="_link">{{ i18n.ts.learnMore }}</a> {{ i18n.ts._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/about-misskey/" rel="nofollow noopener" target="_blank" class="_link">{{ i18n.ts.learnMore }}</a>
</div> </div>
<div v-if="$i != null" style="text-align: center;"> <div v-if="$i != null" style="text-align: center;">
<MkButton primary rounded inline @click="iLoveMisskey">I <Mfm text="$[jelly ❤]"/> #Misskey</MkButton> <MkButton primary rounded inline @click="iLoveMisskey">I <Mfm text="$[jelly ❤]"/> #Misskey</MkButton>

View File

@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer v-if="file" :contentMax="600" :marginMin="16" :marginMax="32"> <MkSpacer v-if="file" :contentMax="600" :marginMin="16" :marginMax="32">
<div v-if="tab === 'overview'" class="cxqhhsmd _gaps_m"> <div v-if="tab === 'overview'" class="cxqhhsmd _gaps_m">
<a class="thumbnail" :href="file.url" target="_blank"> <a class="thumbnail" :href="file.url" rel="nofollow noopener" target="_blank">
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/> <MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
</a> </a>
<div> <div>

File diff suppressed because it is too large Load Diff

View File

@ -4,16 +4,16 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<MkSpacer :contentMax="800"> <Transition
<Transition :enterActiveClass="$style.transition_zoom_enterActive"
:enterActiveClass="$style.transition_zoom_enterActive" :leaveActiveClass="$style.transition_zoom_leaveActive"
:leaveActiveClass="$style.transition_zoom_leaveActive" :enterFromClass="$style.transition_zoom_enterFrom"
:enterFromClass="$style.transition_zoom_enterFrom" :leaveToClass="$style.transition_zoom_leaveTo"
:leaveToClass="$style.transition_zoom_leaveTo" :moveClass="$style.transition_zoom_move"
:moveClass="$style.transition_zoom_move" mode="out-in"
mode="out-in" >
> <MkSpacer v-if="!gameStarted" :contentMax="800">
<div v-if="!gameStarted" :class="$style.root"> <div :class="$style.root">
<div class="_gaps"> <div class="_gaps">
<div :class="$style.frame" style="text-align: center;"> <div :class="$style.frame" style="text-align: center;">
<div :class="$style.frameInner"> <div :class="$style.frameInner">
@ -26,6 +26,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSelect v-model="gameMode"> <MkSelect v-model="gameMode">
<option value="normal">NORMAL</option> <option value="normal">NORMAL</option>
<option value="square">SQUARE</option> <option value="square">SQUARE</option>
<option value="yen">YEN</option>
<option value="sweets">SWEETS (β)</option>
</MkSelect> </MkSelect>
<MkButton primary gradate large rounded inline @click="start">{{ i18n.ts.start }}</MkButton> <MkButton primary gradate large rounded inline @click="start">{{ i18n.ts.start }}</MkButton>
</div> </div>
@ -42,12 +44,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.frame"> <div :class="$style.frame">
<div :class="$style.frameInner"> <div :class="$style.frameInner">
<div class="_gaps_s" style="padding: 16px;"> <div class="_gaps_s" style="padding: 16px;">
<div><b>{{ i18n.ts.ranking }}</b> ({{ gameMode }})</div> <div><b>{{ i18n.t('lastNDays', { n: 7 }) }} {{ i18n.ts.ranking }}</b> ({{ gameMode }})</div>
<div v-if="ranking" class="_gaps_s"> <div v-if="ranking" class="_gaps_s">
<div v-for="r in ranking" :key="r.id" :class="$style.rankingRecord"> <div v-for="r in ranking" :key="r.id" :class="$style.rankingRecord">
<MkAvatar :link="true" style="width: 24px; height: 24px; margin-right: 4px;" :user="r.user"/> <MkAvatar :link="true" style="width: 24px; height: 24px; margin-right: 4px;" :user="r.user"/>
<MkUserName :user="r.user" :nowrap="true"/> <MkUserName :user="r.user" :nowrap="true"/>
<b style="margin-left: auto;">{{ r.score.toLocaleString() }} pt</b> <b style="margin-left: auto;">{{ r.score.toLocaleString() }} {{ getScoreUnit(gameMode) }}</b>
</div> </div>
</div> </div>
<div v-else>{{ i18n.ts.loading }}</div> <div v-else>{{ i18n.ts.loading }}</div>
@ -77,15 +79,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</div> </div>
</div> </div>
<div v-else> </MkSpacer>
<XGame :gameMode="gameMode" :mute="mute" @end="onGameEnd"/> <XGame v-else :gameMode="gameMode" :mute="mute" @end="onGameEnd"/>
</div> </Transition>
</Transition>
</MkSpacer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import XGame from './drop-and-fusion.game.vue'; import XGame from './drop-and-fusion.game.vue';
import { definePageMetadata } from '@/scripts/page-metadata.js'; import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
@ -94,7 +94,7 @@ import MkSelect from '@/components/MkSelect.vue';
import MkSwitch from '@/components/MkSwitch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import { misskeyApiGet } from '@/scripts/misskey-api.js'; import { misskeyApiGet } from '@/scripts/misskey-api.js';
const gameMode = ref<'normal' | 'square'>('normal'); const gameMode = ref<'normal' | 'square' | 'yen' | 'sweets'>('normal');
const gameStarted = ref(false); const gameStarted = ref(false);
const mute = ref(false); const mute = ref(false);
const ranking = ref(null); const ranking = ref(null);
@ -103,6 +103,14 @@ watch(gameMode, async () => {
ranking.value = await misskeyApiGet('bubble-game/ranking', { gameMode: gameMode.value }); ranking.value = await misskeyApiGet('bubble-game/ranking', { gameMode: gameMode.value });
}, { immediate: true }); }, { immediate: true });
function getScoreUnit(gameMode: string) {
return gameMode === 'normal' ? 'pt' :
gameMode === 'square' ? 'pt' :
gameMode === 'yen' ? '円' :
gameMode === 'sweets' ? 'kcal' :
'' as never;
}
async function start() { async function start() {
gameStarted.value = true; gameStarted.value = true;
} }

View File

@ -14,19 +14,15 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import * as Misskey from 'misskey-js';
import { misskeyApiGet } from '@/scripts/misskey-api.js'; import { misskeyApiGet } from '@/scripts/misskey-api.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
const props = defineProps<{ const props = defineProps<{
emoji: { emoji: Misskey.entities.EmojiSimple;
name: string;
aliases: string[];
category: string;
url: string;
};
}>(); }>();
function menu(ev) { function menu(ev) {
@ -43,12 +39,13 @@ function menu(ev) {
}, { }, {
text: i18n.ts.info, text: i18n.ts.info,
icon: 'ti ti-info-circle', icon: 'ti ti-info-circle',
action: () => { action: async () => {
misskeyApiGet('emoji', { name: props.emoji.name }).then(res => { os.popup(MkCustomEmojiDetailedDialog, {
os.alert({ emoji: await misskeyApiGet('emoji', {
type: 'info', name: props.emoji.name,
text: `Name: ${res.name}\nAliases: ${res.aliases.join(' ')}\nCategory: ${res.category}\nisSensitive: ${res.isSensitive}\nlocalOnly: ${res.localOnly}\nLicense: ${res.license}\nURL: ${res.url}`, })
}); }, {
anchor: ev.target,
}); });
}, },
}], ev.currentTarget ?? ev.target); }], ev.currentTarget ?? ev.target);

View File

@ -27,10 +27,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps"> <div class="_gaps">
<I18n :src="i18n.ts._2fa.step1" tag="div"> <I18n :src="i18n.ts._2fa.step1" tag="div">
<template #a> <template #a>
<a href="https://authy.com/" rel="noopener" target="_blank" class="_link">Authy</a> <a href="https://authy.com/" rel="nofollow noopener" target="_blank" class="_link">Authy</a>
</template> </template>
<template #b> <template #b>
<a href="https://support.google.com/accounts/answer/1066447" rel="noopener" target="_blank" class="_link">Google Authenticator</a> <a href="https://support.google.com/accounts/answer/1066447" rel="nofollow noopener" target="_blank" class="_link">Google Authenticator</a>
</template> </template>
</I18n> </I18n>
<div>{{ i18n.ts._2fa.step2 }}<br>{{ i18n.ts._2fa.step2Click }}</div> <div>{{ i18n.ts._2fa.step2 }}<br>{{ i18n.ts._2fa.step2Click }}</div>

View File

@ -1,219 +0,0 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div ref="rootEl">
<MkLoading v-if="fetching"/>
<div v-else :class="$style.root" class="_panel">
<canvas ref="chartEl"></canvas>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, nextTick, watch, shallowRef, ref } from 'vue';
import { Chart } from 'chart.js';
import * as Misskey from 'misskey-js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js';
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
import { alpha } from '@/scripts/color.js';
import { initChart } from '@/scripts/init-chart.js';
initChart();
const props = defineProps<{
src: string;
user: Misskey.entities.User;
}>();
const rootEl = shallowRef<HTMLDivElement>(null);
const chartEl = shallowRef<HTMLCanvasElement>(null);
const now = new Date();
let chartInstance: Chart = null;
const fetching = ref(true);
const { handler: externalTooltipHandler } = useChartTooltip({
position: 'middle',
});
async function renderChart() {
if (chartInstance) {
chartInstance.destroy();
}
const wide = rootEl.value.offsetWidth > 700;
const narrow = rootEl.value.offsetWidth < 400;
const weeks = wide ? 50 : narrow ? 10 : 25;
const chartLimit = 7 * weeks;
const getDate = (ago: number) => {
const y = now.getFullYear();
const m = now.getMonth();
const d = now.getDate();
return new Date(y, m, d - ago);
};
const format = (arr) => {
return arr.map((v, i) => {
const dt = getDate(i);
const iso = `${dt.getFullYear()}-${(dt.getMonth() + 1).toString().padStart(2, '0')}-${dt.getDate().toString().padStart(2, '0')}`;
return {
x: iso,
y: dt.getDay(),
d: iso,
v,
};
});
};
let values;
if (props.src === 'notes') {
const raw = await misskeyApi('charts/user/notes', { userId: props.user.id, limit: chartLimit, span: 'day' });
values = raw.inc;
}
fetching.value = false;
await nextTick();
const color = defaultStore.state.darkMode ? '#b4e900' : '#86b300';
// 3
const max = values.slice().sort((a, b) => b - a).slice(0, 3).reduce((a, b) => a + b, 0) / 3;
const min = Math.max(0, Math.min(...values) - 1);
const marginEachCell = 4;
chartInstance = new Chart(chartEl.value, {
type: 'matrix',
data: {
datasets: [{
label: '',
data: format(values),
pointRadius: 0,
borderWidth: 0,
borderJoinStyle: 'round',
borderRadius: 3,
backgroundColor(c) {
const value = c.dataset.data[c.dataIndex].v;
let a = (value - min) / max;
if (value !== 0) { // 0
a = Math.max(a, 0.05);
}
return alpha(color, a);
},
fill: true,
width(c) {
const a = c.chart.chartArea ?? {};
return (a.right - a.left) / weeks - marginEachCell;
},
height(c) {
const a = c.chart.chartArea ?? {};
return (a.bottom - a.top) / 7 - marginEachCell;
},
/* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107>
}] satisfies ChartData[],
*/
}],
},
options: {
aspectRatio: wide ? 6 : narrow ? 1.8 : 3.2,
layout: {
padding: {
left: 8,
right: 0,
top: 0,
bottom: 0,
},
},
scales: {
x: {
type: 'time',
offset: true,
position: 'bottom',
time: {
unit: 'week',
round: 'week',
isoWeekday: 0,
displayFormats: {
day: 'M/d',
month: 'Y/M',
week: 'M/d',
},
},
grid: {
display: false,
},
ticks: {
display: true,
maxRotation: 0,
autoSkipPadding: 8,
},
},
y: {
offset: true,
reverse: true,
position: 'right',
grid: {
display: false,
},
ticks: {
maxRotation: 0,
autoSkip: true,
padding: 1,
font: {
size: 9,
},
callback: (value, index, values) => ['', 'Mon', '', 'Wed', '', 'Fri', ''][value],
},
},
},
plugins: {
legend: {
display: false,
},
tooltip: {
enabled: false,
callbacks: {
title(context) {
const v = context[0].dataset.data[context[0].dataIndex];
return v.d;
},
label(context) {
const v = context.dataset.data[context.dataIndex];
return [v.v];
},
},
//mode: 'index',
animation: {
duration: 0,
},
external: externalTooltipHandler,
},
},
},
});
}
watch(() => props.src, () => {
fetching.value = true;
renderChart();
});
onMounted(async () => {
renderChart();
});
</script>
<style lang="scss" module>
.root {
padding: 20px;
}
</style>

View File

@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps"> <div class="_gaps">
<MkFoldableSection class="item"> <MkFoldableSection class="item">
<template #header><i class="ti ti-activity"></i> Heatmap</template> <template #header><i class="ti ti-activity"></i> Heatmap</template>
<XHeatmap :user="user" :src="'notes'"/> <MkHeatmap :user="user" :src="'notes'"/>
</MkFoldableSection> </MkFoldableSection>
<MkFoldableSection class="item"> <MkFoldableSection class="item">
<template #header><i class="ti ti-pencil"></i> Notes</template> <template #header><i class="ti ti-pencil"></i> Notes</template>
@ -28,11 +28,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import XHeatmap from './activity.heatmap.vue';
import XPv from './activity.pv.vue'; import XPv from './activity.pv.vue';
import XNotes from './activity.notes.vue'; import XNotes from './activity.notes.vue';
import XFollowing from './activity.following.vue'; import XFollowing from './activity.following.vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue';
import MkHeatmap from '@/components/MkHeatmap.vue';
const props = defineProps<{ const props = defineProps<{
user: Misskey.entities.User; user: Misskey.entities.User;

View File

@ -10,14 +10,12 @@ import seedrandom from 'seedrandom';
export type Mono = { export type Mono = {
id: string; id: string;
level: number; level: number;
size: number; sizeX: number;
shape: 'circle' | 'rectangle'; sizeY: number;
shape: 'circle' | 'rectangle' | 'custom';
vertices?: Matter.Vector[][];
score: number; score: number;
dropCandidate: boolean; dropCandidate: boolean;
sfxPitch: number;
img: string;
imgSize: number;
spriteScale: number;
}; };
type Log = { type Log = {
@ -32,23 +30,469 @@ type Log = {
operation: 'surrender'; operation: 'surrender';
}; };
const NORMAL_BASE_SIZE = 30;
const NORAML_MONOS: Mono[] = [{
id: '9377076d-c980-4d83-bdaf-175bc58275b7',
level: 10,
sizeX: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
sizeY: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
shape: 'circle',
score: 512,
dropCandidate: false,
}, {
id: 'be9f38d2-b267-4b1a-b420-904e22e80568',
level: 9,
sizeX: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
sizeY: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
shape: 'circle',
score: 256,
dropCandidate: false,
}, {
id: 'beb30459-b064-4888-926b-f572e4e72e0c',
level: 8,
sizeX: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
sizeY: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
shape: 'circle',
score: 128,
dropCandidate: false,
}, {
id: 'feab6426-d9d8-49ae-849c-048cdbb6cdf0',
level: 7,
sizeX: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
sizeY: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
shape: 'circle',
score: 64,
dropCandidate: false,
}, {
id: 'd6d8fed6-6d18-4726-81a1-6cf2c974df8a',
level: 6,
sizeX: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
sizeY: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
shape: 'circle',
score: 32,
dropCandidate: false,
}, {
id: '249c728e-230f-4332-bbbf-281c271c75b2',
level: 5,
sizeX: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25,
sizeY: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25,
shape: 'circle',
score: 16,
dropCandidate: true,
}, {
id: '23d67613-d484-4a93-b71e-3e81b19d6186',
level: 4,
sizeX: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25,
sizeY: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25,
shape: 'circle',
score: 8,
dropCandidate: true,
}, {
id: '3cbd0add-ad7d-4685-bad0-29f6dddc0b99',
level: 3,
sizeX: NORMAL_BASE_SIZE * 1.25 * 1.25,
sizeY: NORMAL_BASE_SIZE * 1.25 * 1.25,
shape: 'circle',
score: 4,
dropCandidate: true,
}, {
id: '8f86d4f4-ee02-41bf-ad38-1ce0ae457fb5',
level: 2,
sizeX: NORMAL_BASE_SIZE * 1.25,
sizeY: NORMAL_BASE_SIZE * 1.25,
shape: 'circle',
score: 2,
dropCandidate: true,
}, {
id: '64ec4add-ce39-42b4-96cb-33908f3f118d',
level: 1,
sizeX: NORMAL_BASE_SIZE,
sizeY: NORMAL_BASE_SIZE,
shape: 'circle',
score: 1,
dropCandidate: true,
}];
const YEN_BASE_SIZE = 30;
const YEN_SATSU_BASE_SIZE = 70;
const YEN_MONOS: Mono[] = [{
id: '880f9bd9-802f-4135-a7e1-fd0e0331f726',
level: 10,
sizeX: (YEN_SATSU_BASE_SIZE * 2) * 1.25 * 1.25 * 1.25,
sizeY: YEN_SATSU_BASE_SIZE * 1.25 * 1.25 * 1.25,
shape: 'rectangle',
score: 10000,
dropCandidate: false,
}, {
id: 'e807beb6-374a-4314-9cc2-aa5f17d96b6b',
level: 9,
sizeX: (YEN_SATSU_BASE_SIZE * 2) * 1.25 * 1.25,
sizeY: YEN_SATSU_BASE_SIZE * 1.25 * 1.25,
shape: 'rectangle',
score: 5000,
dropCandidate: false,
}, {
id: '033445b7-8f90-4fc9-beca-71a9e87cb530',
level: 8,
sizeX: (YEN_SATSU_BASE_SIZE * 2) * 1.25,
sizeY: YEN_SATSU_BASE_SIZE * 1.25,
shape: 'rectangle',
score: 2000,
dropCandidate: false,
}, {
id: '410a09ec-5f7f-46f6-b26f-cbca4ccbd091',
level: 7,
sizeX: YEN_SATSU_BASE_SIZE * 2,
sizeY: YEN_SATSU_BASE_SIZE,
shape: 'rectangle',
score: 1000,
dropCandidate: false,
}, {
id: '2aae82bc-3fa4-49ad-a6b5-94d888e809f5',
level: 6,
sizeX: YEN_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
sizeY: YEN_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
shape: 'circle',
score: 500,
dropCandidate: false,
}, {
id: 'a619bd67-d08f-4cc0-8c7e-c8072a4950cd',
level: 5,
sizeX: YEN_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25,
sizeY: YEN_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25,
shape: 'circle',
score: 100,
dropCandidate: true,
}, {
id: 'c1c5d8e4-17d6-4455-befd-12154d731faa',
level: 4,
sizeX: YEN_BASE_SIZE * 1.25 * 1.25 * 1.25,
sizeY: YEN_BASE_SIZE * 1.25 * 1.25 * 1.25,
shape: 'circle',
score: 50,
dropCandidate: true,
}, {
id: '7082648c-e428-44c4-887a-25c07a8ebdd5',
level: 3,
sizeX: YEN_BASE_SIZE * 1.25 * 1.25,
sizeY: YEN_BASE_SIZE * 1.25 * 1.25,
shape: 'circle',
score: 10,
dropCandidate: true,
}, {
id: '0d8d40d5-e6e0-4d26-8a95-b8d842363379',
level: 2,
sizeX: YEN_BASE_SIZE * 1.25,
sizeY: YEN_BASE_SIZE * 1.25,
shape: 'circle',
score: 5,
dropCandidate: true,
}, {
id: '9dec1b38-d99d-40de-8288-37367b983d0d',
level: 1,
sizeX: YEN_BASE_SIZE,
sizeY: YEN_BASE_SIZE,
shape: 'circle',
score: 1,
dropCandidate: true,
}];
const SQUARE_BASE_SIZE = 28;
const SQUARE_MONOS: Mono[] = [{
id: 'f75fd0ba-d3d4-40a4-9712-b470e45b0525',
level: 10,
sizeX: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
sizeY: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
shape: 'rectangle',
score: 512,
dropCandidate: false,
}, {
id: '7b70f4af-1c01-45fd-af72-61b1f01e03d1',
level: 9,
sizeX: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
sizeY: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
shape: 'rectangle',
score: 256,
dropCandidate: false,
}, {
id: '41607ef3-b6d6-4829-95b6-3737bf8bb956',
level: 8,
sizeX: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
sizeY: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
shape: 'rectangle',
score: 128,
dropCandidate: false,
}, {
id: '8a8310d2-0374-460f-bb50-ca9cd3ee3416',
level: 7,
sizeX: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
sizeY: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
shape: 'rectangle',
score: 64,
dropCandidate: false,
}, {
id: '1092e069-fe1a-450b-be97-b5d477ec398c',
level: 6,
sizeX: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
sizeY: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
shape: 'rectangle',
score: 32,
dropCandidate: false,
}, {
id: '2294734d-7bb8-4781-bb7b-ef3820abf3d0',
level: 5,
sizeX: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25,
sizeY: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25,
shape: 'rectangle',
score: 16,
dropCandidate: true,
}, {
id: 'ea8a61af-e350-45f7-ba6a-366fcd65692a',
level: 4,
sizeX: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25,
sizeY: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25,
shape: 'rectangle',
score: 8,
dropCandidate: true,
}, {
id: 'd0c74815-fc1c-4fbe-9953-c92e4b20f919',
level: 3,
sizeX: SQUARE_BASE_SIZE * 1.25 * 1.25,
sizeY: SQUARE_BASE_SIZE * 1.25 * 1.25,
shape: 'rectangle',
score: 4,
dropCandidate: true,
}, {
id: 'd8fbd70e-611d-402d-87da-1a7fd8cd2c8d',
level: 2,
sizeX: SQUARE_BASE_SIZE * 1.25,
sizeY: SQUARE_BASE_SIZE * 1.25,
shape: 'rectangle',
score: 2,
dropCandidate: true,
}, {
id: '35e476ee-44bd-4711-ad42-87be245d3efd',
level: 1,
sizeX: SQUARE_BASE_SIZE,
sizeY: SQUARE_BASE_SIZE,
shape: 'rectangle',
score: 1,
dropCandidate: true,
}];
const SWEETS_BASE_SIZE = 30;
// TODO: custom shape vertices
const SWEETS_MONOS: Mono[] = [{
id: '77f724c0-88be-4aeb-8e1a-a00ed18e3844',
level: 10,
sizeX: SWEETS_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
sizeY: SWEETS_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
shape: 'circle',
score: 512,
dropCandidate: false,
}, {
id: 'f3468ef4-2e1e-4906-8795-f147f39f7e1f',
level: 9,
sizeX: SWEETS_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
sizeY: SWEETS_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
shape: 'circle',
score: 256,
dropCandidate: false,
}, {
id: 'bcb41129-6f2d-44ee-89d3-86eb2df564ba',
level: 8,
sizeX: SWEETS_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
sizeY: SWEETS_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
shape: 'circle',
score: 128,
dropCandidate: false,
}, {
id: 'f058e1ad-1981-409b-b3a7-302de0a43744',
level: 7,
sizeX: SWEETS_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
sizeY: SWEETS_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
shape: 'circle',
score: 64,
dropCandidate: false,
}, {
id: 'd22cfe38-5a3b-4b9c-a1a6-907930a3d732',
level: 6,
sizeX: SWEETS_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
sizeY: SWEETS_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
shape: 'circle',
score: 32,
dropCandidate: false,
}, {
id: '79867083-a073-427e-ae82-07a70d9f3b4f',
level: 5,
sizeX: SWEETS_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25,
sizeY: SWEETS_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25,
shape: 'custom',
vertices: [
[
{
'x': 8,
'y': 15,
},
{
'x': 24,
'y': 15,
},
{
'x': 26,
'y': 26,
},
{
'x': 30,
'y': 26,
},
{
'x': 24.7,
'y': 30,
},
{
'x': 7.34,
'y': 30,
},
{
'x': 2,
'y': 26,
},
{
'x': 6,
'y': 26,
},
],
],
score: 16,
dropCandidate: true,
}, {
id: '2e152a12-a567-4100-b4d4-d15d81ba47b1',
level: 4,
sizeX: SWEETS_BASE_SIZE * 1.25 * 1.25 * 1.25,
sizeY: SWEETS_BASE_SIZE * 1.25 * 1.25 * 1.25,
shape: 'circle',
score: 8,
dropCandidate: true,
}, {
id: '12250376-2258-4716-8eec-b3a7239461fc',
level: 3,
sizeX: SWEETS_BASE_SIZE * 1.25 * 1.25,
sizeY: SWEETS_BASE_SIZE * 1.25 * 1.25,
shape: 'circle',
score: 4,
dropCandidate: true,
}, {
id: '4d4f2668-4be7-44a3-aa3a-856df6e25aa6',
level: 2,
sizeX: SWEETS_BASE_SIZE * 1.25,
sizeY: SWEETS_BASE_SIZE * 1.25,
shape: 'custom',
vertices: [
[
{
'x': 12,
'y': 1.9180000000000001,
},
{
'x': 4,
'y': 4,
},
{
'x': 2.016,
'y': 12,
},
{
'x': 6,
'y': 13.375,
},
{
'x': 6,
'y': 18,
},
{
'x': 8,
'y': 22,
},
{
'x': 12,
'y': 25.372,
},
{
'x': 16.008,
'y': 26,
},
{
'x': 19,
'y': 25.372,
},
{
'x': 20,
'y': 30,
},
{
'x': 28,
'y': 27,
},
{
'x': 30,
'y': 20,
},
{
'x': 25.473,
'y': 19,
},
{
'x': 26,
'y': 15,
},
{
'x': 24,
'y': 10,
},
{
'x': 20,
'y': 7,
},
{
'x': 16.008,
'y': 6,
},
{
'x': 13,
'y': 6,
},
],
],
score: 2,
dropCandidate: true,
}, {
id: 'c9984b40-4045-44c3-b260-d47b7b4625b2',
level: 1,
sizeX: SWEETS_BASE_SIZE,
sizeY: SWEETS_BASE_SIZE,
shape: 'circle',
score: 1,
dropCandidate: true,
}];
export class DropAndFusionGame extends EventEmitter<{ export class DropAndFusionGame extends EventEmitter<{
changeScore: (newScore: number) => void; changeScore: (newScore: number) => void;
changeCombo: (newCombo: number) => void; changeCombo: (newCombo: number) => void;
changeStock: (newStock: { id: string; mono: Mono }[]) => void; changeStock: (newStock: { id: string; mono: Mono }[]) => void;
changeHolding: (newHolding: { id: string; mono: Mono } | null) => void; changeHolding: (newHolding: { id: string; mono: Mono } | null) => void;
dropped: (x: number) => void; dropped: (x: number) => void;
fusioned: (x: number, y: number, scoreDelta: number) => void; fusioned: (x: number, y: number, nextMono: Mono | null, scoreDelta: number) => void;
collision: (energy: number, bodyA: Matter.Body, bodyB: Matter.Body) => void;
monoAdded: (mono: Mono) => void; monoAdded: (mono: Mono) => void;
gameOver: () => void; gameOver: () => void;
sfx(type: string, params: { volume: number; pan: number; pitch: number; }): void;
}> { }> {
private PHYSICS_QUALITY_FACTOR = 16; // 低いほどパフォーマンスが高いがガタガタして安定しなくなる、逆に高すぎても何故か不安定になる private PHYSICS_QUALITY_FACTOR = 16; // 低いほどパフォーマンスが高いがガタガタして安定しなくなる、逆に高すぎても何故か不安定になる
private COMBO_INTERVAL = 60; // frame private COMBO_INTERVAL = 60; // frame
public readonly GAME_VERSION = 1; public readonly GAME_VERSION = 2;
public readonly GAME_WIDTH = 450; public readonly GAME_WIDTH = 450;
public readonly GAME_HEIGHT = 600; public readonly GAME_HEIGHT = 600;
public readonly DROP_INTERVAL = 500; public readonly DROP_COOLTIME = 30; // frame
public readonly PLAYAREA_MARGIN = 25; public readonly PLAYAREA_MARGIN = 25;
private STOCK_MAX = 4; private STOCK_MAX = 4;
private TICK_DELTA = 1000 / 60; // 60fps private TICK_DELTA = 1000 / 60; // 60fps
@ -58,15 +502,16 @@ export class DropAndFusionGame extends EventEmitter<{
private tickCallbackQueue: { frame: number; callback: () => void; }[] = []; private tickCallbackQueue: { frame: number; callback: () => void; }[] = [];
private overflowCollider: Matter.Body; private overflowCollider: Matter.Body;
private isGameOver = false; private isGameOver = false;
private monoDefinitions: Mono[] = []; private gameMode: 'normal' | 'yen' | 'square' | 'sweets';
private rng: () => number; private rng: () => number;
private logs: Log[] = []; private logs: Log[] = [];
private replaying = false;
/** /**
* *
*/ */
private activeBodyIds: Matter.Body['id'][] = []; private fusionReadyBodyIds: Matter.Body['id'][] = [];
private gameOverReadyBodyIds: Matter.Body['id'][] = [];
/** /**
* fusion予約アイテムのペア * fusion予約アイテムのペア
@ -74,13 +519,21 @@ export class DropAndFusionGame extends EventEmitter<{
*/ */
private fusionReservedPairs: { bodyA: Matter.Body; bodyB: Matter.Body }[] = []; private fusionReservedPairs: { bodyA: Matter.Body; bodyB: Matter.Body }[] = [];
private latestDroppedBodyId: Matter.Body['id'] | null = null; private latestDroppedAt = 0; // frame
private latestDroppedAt = 0;
private latestFusionedAt = 0; // frame private latestFusionedAt = 0; // frame
private stock: { id: string; mono: Mono }[] = []; private stock: { id: string; mono: Mono }[] = [];
private holding: { id: string; mono: Mono } | null = null; private holding: { id: string; mono: Mono } | null = null;
private get monoDefinitions() {
switch (this.gameMode) {
case 'normal': return NORAML_MONOS;
case 'yen': return YEN_MONOS;
case 'square': return SQUARE_MONOS;
case 'sweets': return SWEETS_MONOS;
default: throw new Error('unknown game mode');
}
}
private _combo = 0; private _combo = 0;
private get combo() { private get combo() {
return this._combo; return this._combo;
@ -99,16 +552,24 @@ export class DropAndFusionGame extends EventEmitter<{
this.emit('changeScore', value); this.emit('changeScore', value);
} }
private getMonoRenderOptions: null | ((mono: Mono) => Partial<Matter.IBodyRenderOptions>) = null;
public replayPlaybackRate = 1; public replayPlaybackRate = 1;
constructor(env: { monoDefinitions: Mono[]; seed: string; replaying?: boolean }) { constructor(env: {
seed: string;
gameMode: DropAndFusionGame['gameMode'];
getMonoRenderOptions?: (mono: Mono) => Partial<Matter.IBodyRenderOptions>;
}) {
super(); super();
this.replaying = !!env.replaying; //#region BIND
this.monoDefinitions = env.monoDefinitions;
this.rng = seedrandom(env.seed);
this.tick = this.tick.bind(this); this.tick = this.tick.bind(this);
//#endregion
this.gameMode = env.gameMode;
this.getMonoRenderOptions = env.getMonoRenderOptions ?? null;
this.rng = seedrandom(env.seed);
this.engine = Matter.Engine.create({ this.engine = Matter.Engine.create({
constraintIterations: 2 * this.PHYSICS_QUALITY_FACTOR, constraintIterations: 2 * this.PHYSICS_QUALITY_FACTOR,
@ -147,6 +608,7 @@ export class DropAndFusionGame extends EventEmitter<{
//#endregion //#endregion
this.overflowCollider = Matter.Bodies.rectangle(this.GAME_WIDTH / 2, 0, this.GAME_WIDTH, 200, { this.overflowCollider = Matter.Bodies.rectangle(this.GAME_WIDTH / 2, 0, this.GAME_WIDTH, 200, {
label: '_overflow_',
isStatic: true, isStatic: true,
isSensor: true, isSensor: true,
render: { render: {
@ -157,34 +619,37 @@ export class DropAndFusionGame extends EventEmitter<{
Matter.Composite.add(this.engine.world, this.overflowCollider); Matter.Composite.add(this.engine.world, this.overflowCollider);
} }
private msToFrame(ms: number) { public msToFrame(ms: number) {
return Math.round(ms / this.TICK_DELTA); return Math.round(ms / this.TICK_DELTA);
} }
public frameToMs(frame: number) {
return frame * this.TICK_DELTA;
}
private createBody(mono: Mono, x: number, y: number) { private createBody(mono: Mono, x: number, y: number) {
const options: Matter.IBodyDefinition = { const options: Matter.IBodyDefinition = {
label: mono.id, label: mono.id,
//density: 0.0005, //density: 0.0005,
density: mono.size / 1000, density: ((mono.sizeX + mono.sizeY) / 2) / 1000,
restitution: 0.2, restitution: 0.2,
frictionAir: 0.01, frictionAir: 0.01,
friction: 0.7, friction: 0.7,
frictionStatic: 5, frictionStatic: 5,
slop: 1.0, slop: 1.0,
//mass: 0, //mass: 0,
render: { render: this.getMonoRenderOptions ? this.getMonoRenderOptions(mono) : undefined,
sprite: {
texture: mono.img,
xScale: (mono.size / mono.imgSize) * mono.spriteScale,
yScale: (mono.size / mono.imgSize) * mono.spriteScale,
},
},
}; };
if (mono.shape === 'circle') { if (mono.shape === 'circle') {
return Matter.Bodies.circle(x, y, mono.size / 2, options); return Matter.Bodies.circle(x, y, mono.sizeX / 2, options);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
} else if (mono.shape === 'rectangle') { } else if (mono.shape === 'rectangle') {
return Matter.Bodies.rectangle(x, y, mono.size, mono.size, options); return Matter.Bodies.rectangle(x, y, mono.sizeX, mono.sizeY, options);
} else if (mono.shape === 'custom') {
return Matter.Bodies.fromVertices(x, y, mono.vertices!.map(i => i.map(j => ({
x: (j.x / 32) * mono.sizeX,
y: (j.y / 32) * mono.sizeY,
}))), options);
} else { } else {
throw new Error('unrecognized shape'); throw new Error('unrecognized shape');
} }
@ -198,15 +663,15 @@ export class DropAndFusionGame extends EventEmitter<{
} }
this.latestFusionedAt = this.frame; this.latestFusionedAt = this.frame;
// TODO: 単に位置だけでなくそれぞれの動きベクトルも融合する?
const newX = (bodyA.position.x + bodyB.position.x) / 2; const newX = (bodyA.position.x + bodyB.position.x) / 2;
const newY = (bodyA.position.y + bodyB.position.y) / 2; const newY = (bodyA.position.y + bodyB.position.y) / 2;
this.fusionReadyBodyIds = this.fusionReadyBodyIds.filter(x => x !== bodyA.id && x !== bodyB.id);
this.gameOverReadyBodyIds = this.gameOverReadyBodyIds.filter(x => x !== bodyA.id && x !== bodyB.id);
Matter.Composite.remove(this.engine.world, [bodyA, bodyB]); Matter.Composite.remove(this.engine.world, [bodyA, bodyB]);
this.activeBodyIds = this.activeBodyIds.filter(x => x !== bodyA.id && x !== bodyB.id);
const currentMono = this.monoDefinitions.find(y => y.id === bodyA.label)!; const currentMono = this.monoDefinitions.find(y => y.id === bodyA.label)!;
const nextMono = this.monoDefinitions.find(x => x.level === currentMono.level + 1); const nextMono = this.monoDefinitions.find(x => x.level === currentMono.level + 1) ?? null;
if (nextMono) { if (nextMono) {
const body = this.createBody(nextMono, newX, newY); const body = this.createBody(nextMono, newX, newY);
@ -216,43 +681,24 @@ export class DropAndFusionGame extends EventEmitter<{
this.tickCallbackQueue.push({ this.tickCallbackQueue.push({
frame: this.frame + this.msToFrame(100), frame: this.frame + this.msToFrame(100),
callback: () => { callback: () => {
this.activeBodyIds.push(body.id); this.fusionReadyBodyIds.push(body.id);
}, },
}); });
const comboBonus = 1 + ((this.combo - 1) / 5);
const additionalScore = Math.round(currentMono.score * comboBonus);
this.score += additionalScore;
this.emit('monoAdded', nextMono); this.emit('monoAdded', nextMono);
this.emit('fusioned', newX, newY, additionalScore);
const panV = newX - this.PLAYAREA_MARGIN;
const panW = this.GAME_WIDTH - this.PLAYAREA_MARGIN - this.PLAYAREA_MARGIN;
const pan = ((panV / panW) - 0.5) * 2;
this.emit('sfx', 'fusion', { volume: 1, pan, pitch: nextMono.sfxPitch });
} else {
// nop
} }
const comboBonus = this.gameMode === 'yen' ? 1 : 1 + ((this.combo - 1) / 5);
const additionalScore = Math.round(currentMono.score * comboBonus);
this.score += additionalScore;
this.emit('fusioned', newX, newY, nextMono, additionalScore);
} }
private onCollision(event: Matter.IEventCollision<Matter.Engine>) { private onCollision(event: Matter.IEventCollision<Matter.Engine>) {
const minCollisionEnergyForSound = 2.5;
const maxCollisionEnergyForSound = 9;
const soundPitchMax = 4;
const soundPitchMin = 0.5;
for (const pairs of event.pairs) { for (const pairs of event.pairs) {
const { bodyA, bodyB } = pairs; const { bodyA, bodyB } = pairs;
if (bodyA.id === this.overflowCollider.id || bodyB.id === this.overflowCollider.id) {
if (bodyA.id === this.latestDroppedBodyId || bodyB.id === this.latestDroppedBodyId) {
continue;
}
this.gameOver();
break;
}
const shouldFusion = (bodyA.label === bodyB.label) && const shouldFusion = (bodyA.label === bodyB.label) &&
!this.fusionReservedPairs.some(x => !this.fusionReservedPairs.some(x =>
x.bodyA.id === bodyA.id || x.bodyA.id === bodyA.id ||
@ -261,7 +707,7 @@ export class DropAndFusionGame extends EventEmitter<{
x.bodyB.id === bodyB.id); x.bodyB.id === bodyB.id);
if (shouldFusion) { if (shouldFusion) {
if (this.activeBodyIds.includes(bodyA.id) && this.activeBodyIds.includes(bodyB.id)) { if (this.fusionReadyBodyIds.includes(bodyA.id) && this.fusionReadyBodyIds.includes(bodyB.id)) {
this.fusion(bodyA, bodyB); this.fusion(bodyA, bodyB);
} else { } else {
this.fusionReservedPairs.push({ bodyA, bodyB }); this.fusionReservedPairs.push({ bodyA, bodyB });
@ -275,16 +721,28 @@ export class DropAndFusionGame extends EventEmitter<{
} }
} else { } else {
const energy = pairs.collision.depth; const energy = pairs.collision.depth;
if (energy > minCollisionEnergyForSound) {
const volume = (Math.min(maxCollisionEnergyForSound, energy - minCollisionEnergyForSound) / maxCollisionEnergyForSound) / 4; if (bodyA.label === '_overflow_' || bodyB.label === '_overflow_') continue;
const panV =
pairs.bodyA.label === '_wall_' ? bodyB.position.x - this.PLAYAREA_MARGIN : if (bodyA.label !== '_wall_' && bodyB.label !== '_wall_') {
pairs.bodyB.label === '_wall_' ? bodyA.position.x - this.PLAYAREA_MARGIN : if (!this.gameOverReadyBodyIds.includes(bodyA.id)) this.gameOverReadyBodyIds.push(bodyA.id);
((bodyA.position.x + bodyB.position.x) / 2) - this.PLAYAREA_MARGIN; if (!this.gameOverReadyBodyIds.includes(bodyB.id)) this.gameOverReadyBodyIds.push(bodyB.id);
const panW = this.GAME_WIDTH - this.PLAYAREA_MARGIN - this.PLAYAREA_MARGIN; }
const pan = ((panV / panW) - 0.5) * 2;
const pitch = soundPitchMin + ((soundPitchMax - soundPitchMin) * (1 - (Math.min(10, energy) / 10))); this.emit('collision', energy, bodyA, bodyB);
this.emit('sfx', 'collision', { volume, pan, pitch }); }
}
}
private onCollisionActive(event: Matter.IEventCollision<Matter.Engine>) {
for (const pairs of event.pairs) {
const { bodyA, bodyB } = pairs;
// ハコからあふれたかどうかの判定
if (bodyA.id === this.overflowCollider.id || bodyB.id === this.overflowCollider.id) {
if (this.gameOverReadyBodyIds.includes(bodyA.id) || this.gameOverReadyBodyIds.includes(bodyB.id)) {
this.gameOver();
break;
} }
} }
} }
@ -314,6 +772,7 @@ export class DropAndFusionGame extends EventEmitter<{
this.emit('changeStock', this.stock); this.emit('changeStock', this.stock);
Matter.Events.on(this.engine, 'collisionStart', this.onCollision.bind(this)); Matter.Events.on(this.engine, 'collisionStart', this.onCollision.bind(this));
Matter.Events.on(this.engine, 'collisionActive', this.onCollisionActive.bind(this));
} }
public getLogs() { public getLogs() {
@ -349,8 +808,7 @@ export class DropAndFusionGame extends EventEmitter<{
public drop(_x: number) { public drop(_x: number) {
if (this.isGameOver) return; if (this.isGameOver) return;
// TODO: フレームで計算するようにすればリプレイかどうかのチェックは不要になる if (this.frame - this.latestDroppedAt < this.DROP_COOLTIME) return;
if (!this.replaying && (Date.now() - this.latestDroppedAt < this.DROP_INTERVAL)) return;
const head = this.stock.shift()!; const head = this.stock.shift()!;
this.stock.push({ this.stock.push({
@ -360,17 +818,18 @@ export class DropAndFusionGame extends EventEmitter<{
this.emit('changeStock', this.stock); this.emit('changeStock', this.stock);
const inputX = Math.round(_x); const inputX = Math.round(_x);
const x = Math.min(this.GAME_WIDTH - this.PLAYAREA_MARGIN - (head.mono.size / 2), Math.max(this.PLAYAREA_MARGIN + (head.mono.size / 2), inputX)); const x = Math.min(this.GAME_WIDTH - this.PLAYAREA_MARGIN - (head.mono.sizeX / 2), Math.max(this.PLAYAREA_MARGIN + (head.mono.sizeX / 2), inputX));
const body = this.createBody(head.mono, x, 50 + head.mono.size / 2); const body = this.createBody(head.mono, x, 50 + head.mono.sizeY / 2);
this.logs.push({ this.logs.push({
frame: this.frame, frame: this.frame,
operation: 'drop', operation: 'drop',
x: inputX, x: inputX,
}); });
Matter.Composite.add(this.engine.world, body); Matter.Composite.add(this.engine.world, body);
this.activeBodyIds.push(body.id);
this.latestDroppedBodyId = body.id; this.fusionReadyBodyIds.push(body.id);
this.latestDroppedAt = Date.now(); this.latestDroppedAt = this.frame;
this.emit('dropped', x); this.emit('dropped', x);
this.emit('monoAdded', head.mono); this.emit('monoAdded', head.mono);
} }

View File

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<div class="mk-app"> <div class="mk-app">
<a v-if="root" href="https://github.com/misskey-dev/misskey" target="_blank" class="github-corner" aria-label="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:var(--panel); color:var(--fg); position: fixed; z-index: 10; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a> <a v-if="root" href="https://github.com/misskey-dev/misskey" rel="nofollow noopener" target="_blank" class="github-corner" aria-label="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:var(--panel); color:var(--fg); position: fixed; z-index: 10; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a>
<div v-if="!narrow && !root" class="side"> <div v-if="!narrow && !root" class="side">
<div class="banner" :style="{ backgroundImage: instance.backgroundImageUrl ? `url(${ instance.backgroundImageUrl })` : 'none' }"></div> <div class="banner" :style="{ backgroundImage: instance.backgroundImageUrl ? `url(${ instance.backgroundImageUrl })` : 'none' }"></div>

View File

@ -7,14 +7,14 @@
"generate": "tsx src/generator.ts && eslint ./built/**/* --ext .ts --fix" "generate": "tsx src/generator.ts && eslint ./built/**/* --ext .ts --fix"
}, },
"devDependencies": { "devDependencies": {
"@apidevtools/swagger-parser": "10.1.0",
"@misskey-dev/eslint-plugin": "^1.0.0", "@misskey-dev/eslint-plugin": "^1.0.0",
"@readme/openapi-parser": "2.5.0",
"@types/node": "20.9.1", "@types/node": "20.9.1",
"@typescript-eslint/eslint-plugin": "6.11.0", "@typescript-eslint/eslint-plugin": "6.11.0",
"@typescript-eslint/parser": "6.11.0", "@typescript-eslint/parser": "6.11.0",
"eslint": "8.53.0", "eslint": "8.53.0",
"openapi-types": "12.1.3", "openapi-types": "12.1.3",
"openapi-typescript": "6.7.1", "openapi-typescript": "6.7.3",
"ts-case-convert": "2.0.2", "ts-case-convert": "2.0.2",
"tsx": "4.4.0", "tsx": "4.4.0",
"typescript": "5.3.3" "typescript": "5.3.3"

View File

@ -1,10 +1,10 @@
import { mkdir, writeFile } from 'fs/promises'; import { mkdir, writeFile } from 'fs/promises';
import { OpenAPIV3 } from 'openapi-types'; import { OpenAPIV3_1 } from 'openapi-types';
import { toPascal } from 'ts-case-convert'; import { toPascal } from 'ts-case-convert';
import SwaggerParser from '@apidevtools/swagger-parser'; import OpenAPIParser from '@readme/openapi-parser';
import openapiTS from 'openapi-typescript'; import openapiTS from 'openapi-typescript';
function generateVersionHeaderComment(openApiDocs: OpenAPIV3.Document): string { function generateVersionHeaderComment(openApiDocs: OpenAPIV3_1.Document): string {
const contents = { const contents = {
version: openApiDocs.info.version, version: openApiDocs.info.version,
generatedAt: new Date().toISOString(), generatedAt: new Date().toISOString(),
@ -21,7 +21,7 @@ function generateVersionHeaderComment(openApiDocs: OpenAPIV3.Document): string {
} }
async function generateBaseTypes( async function generateBaseTypes(
openApiDocs: OpenAPIV3.Document, openApiDocs: OpenAPIV3_1.Document,
openApiJsonPath: string, openApiJsonPath: string,
typeFileName: string, typeFileName: string,
) { ) {
@ -47,7 +47,7 @@ async function generateBaseTypes(
} }
async function generateSchemaEntities( async function generateSchemaEntities(
openApiDocs: OpenAPIV3.Document, openApiDocs: OpenAPIV3_1.Document,
typeFileName: string, typeFileName: string,
outputPath: string, outputPath: string,
) { ) {
@ -71,7 +71,7 @@ async function generateSchemaEntities(
} }
async function generateEndpoints( async function generateEndpoints(
openApiDocs: OpenAPIV3.Document, openApiDocs: OpenAPIV3_1.Document,
typeFileName: string, typeFileName: string,
entitiesOutputPath: string, entitiesOutputPath: string,
endpointOutputPath: string, endpointOutputPath: string,
@ -79,7 +79,7 @@ async function generateEndpoints(
const endpoints: Endpoint[] = []; const endpoints: Endpoint[] = [];
// misskey-jsはPOST固定で送っているので、こちらも決め打ちする。別メソッドに対応することがあればこちらも直す必要あり // misskey-jsはPOST固定で送っているので、こちらも決め打ちする。別メソッドに対応することがあればこちらも直す必要あり
const paths = openApiDocs.paths; const paths = openApiDocs.paths ?? {};
const postPathItems = Object.keys(paths) const postPathItems = Object.keys(paths)
.map(it => paths[it]?.post) .map(it => paths[it]?.post)
.filter(filterUndefined); .filter(filterUndefined);
@ -160,7 +160,7 @@ async function generateEndpoints(
} }
async function generateApiClientJSDoc( async function generateApiClientJSDoc(
openApiDocs: OpenAPIV3.Document, openApiDocs: OpenAPIV3_1.Document,
apiClientFileName: string, apiClientFileName: string,
endpointsFileName: string, endpointsFileName: string,
warningsOutputPath: string, warningsOutputPath: string,
@ -168,7 +168,7 @@ async function generateApiClientJSDoc(
const endpoints: { operationId: string; description: string; }[] = []; const endpoints: { operationId: string; description: string; }[] = [];
// misskey-jsはPOST固定で送っているので、こちらも決め打ちする。別メソッドに対応することがあればこちらも直す必要あり // misskey-jsはPOST固定で送っているので、こちらも決め打ちする。別メソッドに対応することがあればこちらも直す必要あり
const paths = openApiDocs.paths; const paths = openApiDocs.paths ?? {};
const postPathItems = Object.keys(paths) const postPathItems = Object.keys(paths)
.map(it => paths[it]?.post) .map(it => paths[it]?.post)
.filter(filterUndefined); .filter(filterUndefined);
@ -221,21 +221,21 @@ async function generateApiClientJSDoc(
await writeFile(warningsOutputPath, endpointOutputLine.join('\n')); await writeFile(warningsOutputPath, endpointOutputLine.join('\n'));
} }
function isRequestBodyObject(value: unknown): value is OpenAPIV3.RequestBodyObject { function isRequestBodyObject(value: unknown): value is OpenAPIV3_1.RequestBodyObject {
if (!value) { if (!value) {
return false; return false;
} }
const { content } = value as Record<keyof OpenAPIV3.RequestBodyObject, unknown>; const { content } = value as Record<keyof OpenAPIV3_1.RequestBodyObject, unknown>;
return content !== undefined; return content !== undefined;
} }
function isResponseObject(value: unknown): value is OpenAPIV3.ResponseObject { function isResponseObject(value: unknown): value is OpenAPIV3_1.ResponseObject {
if (!value) { if (!value) {
return false; return false;
} }
const { description } = value as Record<keyof OpenAPIV3.ResponseObject, unknown>; const { description } = value as Record<keyof OpenAPIV3_1.ResponseObject, unknown>;
return description !== undefined; return description !== undefined;
} }
@ -330,7 +330,7 @@ async function main() {
await mkdir(generatePath, { recursive: true }); await mkdir(generatePath, { recursive: true });
const openApiJsonPath = './api.json'; const openApiJsonPath = './api.json';
const openApiDocs = await SwaggerParser.validate(openApiJsonPath) as OpenAPIV3.Document; const openApiDocs = await OpenAPIParser.parse(openApiJsonPath) as OpenAPIV3_1.Document;
const typeFileName = './built/autogen/types.ts'; const typeFileName = './built/autogen/types.ts';
await generateBaseTypes(openApiDocs, openApiJsonPath, typeFileName); await generateBaseTypes(openApiDocs, openApiJsonPath, typeFileName);

View File

@ -1,6 +1,6 @@
/* /*
* version: 2023.12.2 * version: 2023.12.2
* generatedAt: 2024-01-11T14:29:04.817Z * generatedAt: 2024-01-13T04:31:38.782Z
*/ */
import type { SwitchCaseResponseType } from '../api.js'; import type { SwitchCaseResponseType } from '../api.js';

View File

@ -1,6 +1,6 @@
/* /*
* version: 2023.12.2 * version: 2023.12.2
* generatedAt: 2024-01-11T14:29:04.814Z * generatedAt: 2024-01-13T04:31:38.778Z
*/ */
import type { import type {

View File

@ -1,6 +1,6 @@
/* /*
* version: 2023.12.2 * version: 2023.12.2
* generatedAt: 2024-01-11T14:29:04.811Z * generatedAt: 2024-01-13T04:31:38.775Z
*/ */
import { operations } from './types.js'; import { operations } from './types.js';

View File

@ -1,6 +1,6 @@
/* /*
* version: 2023.12.2 * version: 2023.12.2
* generatedAt: 2024-01-11T14:29:04.810Z * generatedAt: 2024-01-13T04:31:38.773Z
*/ */
import { components } from './types.js'; import { components } from './types.js';

View File

@ -3,7 +3,7 @@
/* /*
* version: 2023.12.2 * version: 2023.12.2
* generatedAt: 2024-01-11T14:29:04.681Z * generatedAt: 2024-01-13T04:31:38.633Z
*/ */
/** /**
@ -3880,7 +3880,7 @@ export type components = {
fileIds?: string[]; fileIds?: string[];
files?: components['schemas']['DriveFile'][]; files?: components['schemas']['DriveFile'][];
tags?: string[]; tags?: string[];
poll?: Record<string, unknown> | null; poll?: Record<string, never> | null;
/** /**
* Format: id * Format: id
* @example xxxxxxxxxx * @example xxxxxxxxxx
@ -3903,7 +3903,7 @@ export type components = {
url?: string; url?: string;
reactionAndUserPairCache?: string[]; reactionAndUserPairCache?: string[];
clippedCount?: number; clippedCount?: number;
myReaction?: Record<string, unknown> | null; myReaction?: Record<string, never> | null;
}; };
NoteReaction: { NoteReaction: {
/** /**

View File

@ -1083,12 +1083,12 @@ importers:
packages/misskey-js/generator: packages/misskey-js/generator:
devDependencies: devDependencies:
'@apidevtools/swagger-parser':
specifier: 10.1.0
version: 10.1.0(openapi-types@12.1.3)
'@misskey-dev/eslint-plugin': '@misskey-dev/eslint-plugin':
specifier: ^1.0.0 specifier: ^1.0.0
version: 1.0.0(@typescript-eslint/eslint-plugin@6.11.0)(@typescript-eslint/parser@6.11.0)(eslint-plugin-import@2.29.1)(eslint@8.53.0) version: 1.0.0(@typescript-eslint/eslint-plugin@6.11.0)(@typescript-eslint/parser@6.11.0)(eslint-plugin-import@2.29.1)(eslint@8.53.0)
'@readme/openapi-parser':
specifier: 2.5.0
version: 2.5.0(openapi-types@12.1.3)
'@types/node': '@types/node':
specifier: 20.9.1 specifier: 20.9.1
version: 20.9.1 version: 20.9.1
@ -1105,8 +1105,8 @@ importers:
specifier: 12.1.3 specifier: 12.1.3
version: 12.1.3 version: 12.1.3
openapi-typescript: openapi-typescript:
specifier: 6.7.1 specifier: 6.7.3
version: 6.7.1 version: 6.7.3
ts-case-convert: ts-case-convert:
specifier: 2.0.2 specifier: 2.0.2
version: 2.0.2 version: 2.0.2
@ -1170,14 +1170,6 @@ packages:
'@jridgewell/trace-mapping': 0.3.18 '@jridgewell/trace-mapping': 0.3.18
dev: true dev: true
/@apidevtools/json-schema-ref-parser@9.0.6:
resolution: {integrity: sha512-M3YgsLjI0lZxvrpeGVk9Ap032W6TPQkH6pRAZz81Ac3WUNF79VQooAFnp8umjvVzUmD93NkogxEwbSce7qMsUg==}
dependencies:
'@jsdevtools/ono': 7.1.3
call-me-maybe: 1.0.2
js-yaml: 3.14.1
dev: true
/@apidevtools/openapi-schemas@2.1.0: /@apidevtools/openapi-schemas@2.1.0:
resolution: {integrity: sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==} resolution: {integrity: sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -1187,21 +1179,6 @@ packages:
resolution: {integrity: sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==} resolution: {integrity: sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==}
dev: true dev: true
/@apidevtools/swagger-parser@10.1.0(openapi-types@12.1.3):
resolution: {integrity: sha512-9Kt7EuS/7WbMAUv2gSziqjvxwDbFSg3Xeyfuj5laUODX8o/k/CpsAKiQ8W7/R88eXFTMbJYg6+7uAmOWNKmwnw==}
peerDependencies:
openapi-types: '>=7'
dependencies:
'@apidevtools/json-schema-ref-parser': 9.0.6
'@apidevtools/openapi-schemas': 2.1.0
'@apidevtools/swagger-methods': 3.0.2
'@jsdevtools/ono': 7.1.3
ajv: 8.12.0
ajv-draft-04: 1.0.0(ajv@8.12.0)
call-me-maybe: 1.0.2
openapi-types: 12.1.3
dev: true
/@aw-web-design/x-default-browser@1.4.126: /@aw-web-design/x-default-browser@1.4.126:
resolution: {integrity: sha512-Xk1sIhyNC/esHGGVjL/niHLowM0csl/kFO5uawBy4IrWwy0o1G8LGt3jP6nmWGz+USxeeqbihAmp/oVZju6wug==} resolution: {integrity: sha512-Xk1sIhyNC/esHGGVjL/niHLowM0csl/kFO5uawBy4IrWwy0o1G8LGt3jP6nmWGz+USxeeqbihAmp/oVZju6wug==}
hasBin: true hasBin: true
@ -3331,7 +3308,6 @@ packages:
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
dependencies: dependencies:
regenerator-runtime: 0.14.0 regenerator-runtime: 0.14.0
dev: false
/@babel/template@7.22.15: /@babel/template@7.22.15:
resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==} resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==}
@ -4553,6 +4529,11 @@ packages:
engines: {node: '>=12.22'} engines: {node: '>=12.22'}
dev: true dev: true
/@humanwhocodes/momoa@2.0.4:
resolution: {integrity: sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA==}
engines: {node: '>=10.10.0'}
dev: true
/@humanwhocodes/object-schema@2.0.1: /@humanwhocodes/object-schema@2.0.1:
resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==} resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==}
dev: true dev: true
@ -5866,6 +5847,48 @@ packages:
'@babel/runtime': 7.23.2 '@babel/runtime': 7.23.2
dev: true dev: true
/@readme/better-ajv-errors@1.6.0(ajv@8.12.0):
resolution: {integrity: sha512-9gO9rld84Jgu13kcbKRU+WHseNhaVt76wYMeRDGsUGYxwJtI3RmEJ9LY9dZCYQGI8eUZLuxb5qDja0nqklpFjQ==}
engines: {node: '>=14'}
peerDependencies:
ajv: 4.11.8 - 8
dependencies:
'@babel/code-frame': 7.23.5
'@babel/runtime': 7.23.4
'@humanwhocodes/momoa': 2.0.4
ajv: 8.12.0
chalk: 4.1.2
json-to-ast: 2.1.0
jsonpointer: 5.0.1
leven: 3.1.0
dev: true
/@readme/json-schema-ref-parser@1.2.0:
resolution: {integrity: sha512-Bt3QVovFSua4QmHa65EHUmh2xS0XJ3rgTEUPH998f4OW4VVJke3BuS16f+kM0ZLOGdvIrzrPRqwihuv5BAjtrA==}
dependencies:
'@jsdevtools/ono': 7.1.3
'@types/json-schema': 7.0.12
call-me-maybe: 1.0.2
js-yaml: 4.1.0
dev: true
/@readme/openapi-parser@2.5.0(openapi-types@12.1.3):
resolution: {integrity: sha512-IbymbOqRuUzoIgxfAAR7XJt2FWl6n2yqN09fF5adacGm7W03siA3bj1Emql0X9D2T+RpBYz3x9zDsMhuoMP62A==}
engines: {node: '>=14'}
peerDependencies:
openapi-types: '>=7'
dependencies:
'@apidevtools/openapi-schemas': 2.1.0
'@apidevtools/swagger-methods': 3.0.2
'@jsdevtools/ono': 7.1.3
'@readme/better-ajv-errors': 1.6.0(ajv@8.12.0)
'@readme/json-schema-ref-parser': 1.2.0
ajv: 8.12.0
ajv-draft-04: 1.0.0(ajv@8.12.0)
call-me-maybe: 1.0.2
openapi-types: 12.1.3
dev: true
/@rollup/plugin-json@6.1.0(rollup@4.9.1): /@rollup/plugin-json@6.1.0(rollup@4.9.1):
resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==} resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
@ -10454,6 +10477,11 @@ packages:
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
dev: true dev: true
/code-error-fragment@0.0.230:
resolution: {integrity: sha512-cadkfKp6932H8UkhzE/gcUqhRMNf8jHzkAN7+5Myabswaghu4xABTgPHDCjW+dBAJxj/SpkTYokpzDqY4pCzQw==}
engines: {node: '>= 4'}
dev: true
/collect-v8-coverage@1.0.1: /collect-v8-coverage@1.0.1:
resolution: {integrity: sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==} resolution: {integrity: sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==}
dev: true dev: true
@ -13075,6 +13103,10 @@ packages:
/graceful-fs@4.2.11: /graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
/grapheme-splitter@1.0.4:
resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==}
dev: true
/graphemer@1.4.0: /graphemer@1.4.0:
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
dev: true dev: true
@ -14666,6 +14698,14 @@ packages:
/json-stringify-safe@5.0.1: /json-stringify-safe@5.0.1:
resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==}
/json-to-ast@2.1.0:
resolution: {integrity: sha512-W9Lq347r8tA1DfMvAGn9QNcgYm4Wm7Yc+k8e6vezpMnRT+NHbtlxgNBXRVjXe9YM6eTn6+p/MKOlV/aABJcSnQ==}
engines: {node: '>= 4'}
dependencies:
code-error-fragment: 0.0.230
grapheme-splitter: 1.0.4
dev: true
/json5@1.0.2: /json5@1.0.2:
resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
hasBin: true hasBin: true
@ -14713,6 +14753,11 @@ packages:
- web-streams-polyfill - web-streams-polyfill
dev: false dev: false
/jsonpointer@5.0.1:
resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==}
engines: {node: '>=0.10.0'}
dev: true
/jsprim@1.4.2: /jsprim@1.4.2:
resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==} resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==}
engines: {node: '>=0.6.0'} engines: {node: '>=0.6.0'}
@ -16060,15 +16105,15 @@ packages:
/openapi-types@12.1.3: /openapi-types@12.1.3:
resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
/openapi-typescript@6.7.1: /openapi-typescript@6.7.3:
resolution: {integrity: sha512-Q3Ltt0KUm2smcPrsaR8qKmSwQ1KM4yGDJVoQdpYa0yvKPeN8huDx5utMT7DvwvJastHHzUxajjivK3WN2+fobg==} resolution: {integrity: sha512-es3mGcDXV6TKPo6n3aohzHm0qxhLyR39MhF6mkD1FwFGjhxnqMqfSIgM0eCpInZvqatve4CxmXcMZw3jnnsaXw==}
hasBin: true hasBin: true
dependencies: dependencies:
ansi-colors: 4.1.3 ansi-colors: 4.1.3
fast-glob: 3.3.2 fast-glob: 3.3.2
js-yaml: 4.1.0 js-yaml: 4.1.0
supports-color: 9.4.0 supports-color: 9.4.0
undici: 5.28.1 undici: 5.28.2
yargs-parser: 21.1.1 yargs-parser: 21.1.1
dev: true dev: true
@ -19557,6 +19602,14 @@ packages:
engines: {node: '>=14.0'} engines: {node: '>=14.0'}
dependencies: dependencies:
'@fastify/busboy': 2.1.0 '@fastify/busboy': 2.1.0
dev: false
/undici@5.28.2:
resolution: {integrity: sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w==}
engines: {node: '>=14.0'}
dependencies:
'@fastify/busboy': 2.1.0
dev: true
/unicode-canonical-property-names-ecmascript@2.0.0: /unicode-canonical-property-names-ecmascript@2.0.0:
resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==}

View File

@ -0,0 +1,9 @@
module.exports = {
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
extends: [
'../../packages/shared/.eslintrc.js',
],
};

3
scripts/changelog-checker/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules
coverage
.idea

2769
scripts/changelog-checker/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
{
"name": "changelog-checker",
"version": "1.0.0",
"description": "",
"type": "module",
"scripts": {
"run": "vite-node src/index.ts",
"test": "vitest run",
"test:coverage": "vitest run --coverage"
},
"devDependencies": {
"@types/mdast": "4.0.3",
"@types/node": "20.10.7",
"@vitest/coverage-v8": "1.1.3",
"mdast-util-to-string": "4.0.0",
"remark": "15.0.1",
"remark-parse": "11.0.0",
"typescript": "5.3.3",
"unified": "11.0.4",
"vite": "5.0.11",
"vite-node": "1.1.3",
"vitest": "1.1.3"
}
}

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