Merge remote-tracking branch 'misskey-dev/develop' into io
|
@ -32,8 +32,8 @@ jobs:
|
|||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
- name: Install swagger-cli
|
||||
run: npm i -g swagger-cli
|
||||
- name: Install Redocly CLI
|
||||
run: npm i -g @redocly/cli
|
||||
- run: corepack enable
|
||||
- run: pnpm i --frozen-lockfile
|
||||
- name: Check pnpm-lock.yaml
|
||||
|
@ -43,4 +43,4 @@ jobs:
|
|||
- name: Build and generate
|
||||
run: pnpm build && pnpm --filter backend generate-api-json
|
||||
- name: Validation
|
||||
run: swagger-cli validate ./packages/backend/built/api.json
|
||||
run: npx @redocly/cli lint --extends=minimal ./packages/backend/built/api.json
|
||||
|
|
|
@ -20,6 +20,8 @@
|
|||
|
||||
### Client
|
||||
- Feat: 新しいゲームを追加
|
||||
- Feat: 絵文字の詳細ダイアログを追加
|
||||
- Feat: 枠線をつけるMFM`$[border.width=1,style=solid,color=fff,radius=0 ...]`を追加
|
||||
- Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように
|
||||
- Enhance: チャンネルノートのピン留めをノートのメニューからできるように
|
||||
- Enhance: 管理者の場合はAPI tokenの発行画面で管理機能に関する権限を付与できるように
|
||||
|
@ -35,7 +37,10 @@
|
|||
- Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました
|
||||
- Enhance: ActivityPub Deliver queueでBodyを事前処理するように (#12916)
|
||||
- Enhance: クリップをエクスポートできるように
|
||||
- Enhance: `api.json`のOpenAPI Specificationを3.1.0に更新
|
||||
- Fix: `drive/files/update`でファイル名のバリデーションが機能していない問題を修正
|
||||
- Fix: `notes/create`で、`text`が空白文字のみで構成されているか`null`であって、かつ`text`だけであるリクエストに対するレスポンスが400になるように変更
|
||||
- Fix: `notes/create`で、`text`が空白文字のみで構成されていてかつリノート、ファイルまたは投票を含んでいるリクエストに対するレスポンスの`text`が`""`から`null`になるように変更
|
||||
|
||||
## 2023.12.2
|
||||
|
||||
|
|
|
@ -1207,6 +1207,8 @@ export interface Locale {
|
|||
"replay": string;
|
||||
"replaying": string;
|
||||
"ranking": string;
|
||||
"lastNDays": string;
|
||||
"backToTitle": string;
|
||||
"abuseReportCategory": string;
|
||||
"selectCategory": string;
|
||||
"reportComplete": string;
|
||||
|
|
|
@ -1204,6 +1204,8 @@ showReplay: "リプレイを見る"
|
|||
replay: "リプレイ"
|
||||
replaying: "リプレイ中"
|
||||
ranking: "ランキング"
|
||||
lastNDays: "直近{n}日"
|
||||
backToTitle: "タイトルへ"
|
||||
abuseReportCategory: "通報の種類"
|
||||
selectCategory: "カテゴリを選択"
|
||||
reportComplete: "通報完了"
|
||||
|
|
|
@ -331,6 +331,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
data.text = data.text.slice(0, DB_MAX_NOTE_TEXT_LENGTH);
|
||||
}
|
||||
data.text = data.text.trim();
|
||||
if (data.text === '') {
|
||||
data.text = null;
|
||||
}
|
||||
} else {
|
||||
data.text = null;
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ export const paramDef = {
|
|||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
|
||||
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: [],
|
||||
} as const;
|
||||
|
|
|
@ -60,6 +60,7 @@ export const paramDef = {
|
|||
'-firstRetrievedAt',
|
||||
'+latestRequestReceivedAt',
|
||||
'-latestRequestReceivedAt',
|
||||
null,
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -109,13 +109,13 @@ export const meta = {
|
|||
items: {
|
||||
type: 'string',
|
||||
enum: [
|
||||
"ble",
|
||||
"cable",
|
||||
"hybrid",
|
||||
"internal",
|
||||
"nfc",
|
||||
"smart-card",
|
||||
"usb",
|
||||
'ble',
|
||||
'cable',
|
||||
'hybrid',
|
||||
'internal',
|
||||
'nfc',
|
||||
'smart-card',
|
||||
'usb',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -129,8 +129,8 @@ export const meta = {
|
|||
authenticatorAttachment: {
|
||||
type: 'string',
|
||||
enum: [
|
||||
"cross-platform",
|
||||
"platform",
|
||||
'cross-platform',
|
||||
'platform',
|
||||
],
|
||||
},
|
||||
requireResidentKey: {
|
||||
|
@ -139,9 +139,9 @@ export const meta = {
|
|||
userVerification: {
|
||||
type: 'string',
|
||||
enum: [
|
||||
"discouraged",
|
||||
"preferred",
|
||||
"required",
|
||||
'discouraged',
|
||||
'preferred',
|
||||
'required',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -150,10 +150,11 @@ export const meta = {
|
|||
type: 'string',
|
||||
nullable: true,
|
||||
enum: [
|
||||
"direct",
|
||||
"enterprise",
|
||||
"indirect",
|
||||
"none",
|
||||
'direct',
|
||||
'enterprise',
|
||||
'indirect',
|
||||
'none',
|
||||
null,
|
||||
],
|
||||
},
|
||||
extensions: {
|
||||
|
|
|
@ -34,11 +34,10 @@ describe('api:notes/create', () => {
|
|||
.toBe(VALID);
|
||||
});
|
||||
|
||||
// TODO
|
||||
//test('null post', () => {
|
||||
// expect(v({ text: null }))
|
||||
// .toBe(INVALID);
|
||||
//});
|
||||
test('null post', () => {
|
||||
expect(v({ text: null }))
|
||||
.toBe(INVALID);
|
||||
});
|
||||
|
||||
test('0 characters post', () => {
|
||||
expect(v({ text: '' }))
|
||||
|
@ -49,6 +48,11 @@ describe('api:notes/create', () => {
|
|||
expect(v({ text: await tooLong }))
|
||||
.toBe(INVALID);
|
||||
});
|
||||
|
||||
test('whitespace-only post', () => {
|
||||
expect(v({ text: ' ' }))
|
||||
.toBe(INVALID);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cw', () => {
|
||||
|
|
|
@ -173,13 +173,33 @@ export const paramDef = {
|
|||
},
|
||||
},
|
||||
// (re)note with text, files and poll are optional
|
||||
anyOf: [
|
||||
{ required: ['text'] },
|
||||
{ required: ['renoteId'] },
|
||||
{ required: ['fileIds'] },
|
||||
{ required: ['mediaIds'] },
|
||||
{ required: ['poll'] },
|
||||
],
|
||||
if: {
|
||||
properties: {
|
||||
renoteId: {
|
||||
type: 'null',
|
||||
},
|
||||
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;
|
||||
|
||||
@Injectable()
|
||||
|
|
|
@ -10,7 +10,7 @@ import { schemas, convertSchemaToOpenApiSchema } from './schemas.js';
|
|||
|
||||
export function genOpenapiSpec(config: Config) {
|
||||
const spec = {
|
||||
openapi: '3.0.0',
|
||||
openapi: '3.1.0',
|
||||
|
||||
info: {
|
||||
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';
|
||||
|
||||
|
@ -71,7 +71,7 @@ export function genOpenapiSpec(config: Config) {
|
|||
}
|
||||
|
||||
const requestType = endpoint.meta.requireFile ? 'multipart/form-data' : 'application/json';
|
||||
const schema = { ...endpoint.params };
|
||||
const schema = { ...convertSchemaToOpenApiSchema(endpoint.params, 'param') };
|
||||
|
||||
if (endpoint.meta.requireFile) {
|
||||
schema.properties = {
|
||||
|
@ -210,7 +210,9 @@ export function genOpenapiSpec(config: Config) {
|
|||
};
|
||||
|
||||
spec.paths['/' + endpoint.name] = {
|
||||
...(endpoint.meta.allowGet ? { get: info } : {}),
|
||||
...(endpoint.meta.allowGet ? {
|
||||
get: info,
|
||||
} : {}),
|
||||
post: info,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -6,32 +6,35 @@
|
|||
import type { Schema } from '@/misc/json-schema.js';
|
||||
import { refs } from '@/misc/json-schema.js';
|
||||
|
||||
export function convertSchemaToOpenApiSchema(schema: Schema) {
|
||||
// optional, refはスキーマ定義に含まれないので分離しておく
|
||||
export function convertSchemaToOpenApiSchema(schema: Schema, type: 'param' | 'res') {
|
||||
// optional, nullable, refはスキーマ定義に含まれないので分離しておく
|
||||
// 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) {
|
||||
const required = Object.entries(schema.properties).filter(([k, v]) => !v.optional).map(([k]) => k);
|
||||
if (required.length > 0) {
|
||||
if (type === 'res') {
|
||||
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)) {
|
||||
res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k]);
|
||||
res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k], type);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
if (schema.oneOf) res.oneOf = schema.oneOf.map(convertSchemaToOpenApiSchema);
|
||||
if (schema.allOf) res.allOf = schema.allOf.map(convertSchemaToOpenApiSchema);
|
||||
for (const o of ['anyOf', 'oneOf', 'allOf'] as const) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
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}`;
|
||||
if (schema.nullable || schema.optional) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -72,6 +83,6 @@ export const schemas = {
|
|||
},
|
||||
|
||||
...Object.fromEntries(
|
||||
Object.entries(refs).map(([key, schema]) => [key, convertSchemaToOpenApiSchema(schema)]),
|
||||
Object.entries(refs).map(([key, schema]) => [key, convertSchemaToOpenApiSchema(schema, 'res')]),
|
||||
),
|
||||
};
|
||||
|
|
|
@ -136,6 +136,19 @@ describe('Note', () => {
|
|||
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 () => {
|
||||
const createRes = await api('/notes/create', {
|
||||
text: 'test',
|
||||
|
|
After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 8.0 KiB |
After Width: | Height: | Size: 54 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 8.5 KiB |
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 607 B |
After Width: | Height: | Size: 522 B |
After Width: | Height: | Size: 91 KiB |
After Width: | Height: | Size: 96 KiB |
After Width: | Height: | Size: 54 KiB |
After Width: | Height: | Size: 66 KiB |
After Width: | Height: | Size: 56 KiB |
After Width: | Height: | Size: 86 KiB |
After Width: | Height: | Size: 92 KiB |
After Width: | Height: | Size: 66 KiB |
After Width: | Height: | Size: 41 KiB |
After Width: | Height: | Size: 59 KiB |
|
@ -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" 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>
|
|
@ -15,6 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<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';
|
||||
|
@ -23,9 +24,16 @@ import { initChart } from '@/scripts/init-chart.js';
|
|||
|
||||
initChart();
|
||||
|
||||
const props = defineProps<{
|
||||
src: string;
|
||||
}>();
|
||||
export type HeatmapSource = 'active-users' | 'notes' | 'ap-requests-inbox-received' | 'ap-requests-deliver-succeeded' | 'ap-requests-deliver-failed';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
src: HeatmapSource;
|
||||
user?: Misskey.entities.User;
|
||||
label?: string;
|
||||
}>(), {
|
||||
user: undefined,
|
||||
label: '',
|
||||
});
|
||||
|
||||
const rootEl = shallowRef<HTMLDivElement>(null);
|
||||
const chartEl = shallowRef<HTMLCanvasElement>(null);
|
||||
|
@ -75,8 +83,13 @@ async function renderChart() {
|
|||
const raw = await misskeyApi('charts/active-users', { limit: chartLimit, span: 'day' });
|
||||
values = raw.readWrite;
|
||||
} else if (props.src === 'notes') {
|
||||
const raw = await misskeyApi('charts/notes', { limit: chartLimit, span: 'day' });
|
||||
values = raw.local.inc;
|
||||
if (props.user) {
|
||||
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') {
|
||||
const raw = await misskeyApi('charts/ap-request', { limit: chartLimit, span: 'day' });
|
||||
values = raw.inboxReceived;
|
||||
|
@ -105,7 +118,7 @@ async function renderChart() {
|
|||
type: 'matrix',
|
||||
data: {
|
||||
datasets: [{
|
||||
label: 'Read & Write',
|
||||
label: props.label,
|
||||
data: format(values),
|
||||
pointRadius: 0,
|
||||
borderWidth: 0,
|
||||
|
@ -128,6 +141,9 @@ async function renderChart() {
|
|||
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: {
|
||||
|
@ -195,7 +211,7 @@ async function renderChart() {
|
|||
},
|
||||
label(context) {
|
||||
const v = context.dataset.data[context.dataIndex];
|
||||
return ['Active: ' + v.v];
|
||||
return [v.v];
|
||||
},
|
||||
},
|
||||
//mode: 'index',
|
||||
|
|
|
@ -51,7 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<option value="ap-requests-deliver-failed">AP Requests: deliverFailed</option>
|
||||
</MkSelect>
|
||||
<div class="_panel" :class="$style.heatmap">
|
||||
<MkHeatmap :src="heatmapSrc"/>
|
||||
<MkHeatmap :src="heatmapSrc" :label="'Read & Write'"/>
|
||||
</div>
|
||||
</MkFoldableSection>
|
||||
|
||||
|
@ -92,7 +92,7 @@ import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
|
|||
import * as os from '@/os.js';
|
||||
import { misskeyApiGet } from '@/scripts/misskey-api.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 MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue';
|
||||
import MkRetentionLineChart from '@/components/MkRetentionLineChart.vue';
|
||||
|
@ -103,7 +103,7 @@ initChart();
|
|||
const chartLimit = 500;
|
||||
const chartSpan = ref<'hour' | 'day'>('hour');
|
||||
const chartSrc = ref('active-users');
|
||||
const heatmapSrc = ref('active-users');
|
||||
const heatmapSrc = ref<HeatmapSource>('active-users');
|
||||
const subDoughnutEl = shallowRef<HTMLCanvasElement>();
|
||||
const pubDoughnutEl = shallowRef<HTMLCanvasElement>();
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ import * as os from '@/os.js';
|
|||
const props = withDefaults(defineProps<{
|
||||
x: number;
|
||||
y: number;
|
||||
value?: number;
|
||||
value?: number | string;
|
||||
}>(), {
|
||||
value: 1,
|
||||
});
|
||||
|
|
|
@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
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' }]"
|
||||
@click="toggleReaction()"
|
||||
@contextmenu.prevent.stop="menu"
|
||||
>
|
||||
<MkReactionIcon :class="defaultStore.state.limitWidthOfReaction ? $style.limitWidth : ''" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/>
|
||||
<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 XDetails from '@/components/MkReactionsViewer.details.vue';
|
||||
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||
import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.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() {
|
||||
if (document.hidden) return;
|
||||
if (!defaultStore.state.animation) return;
|
||||
|
|
|
@ -24,9 +24,11 @@ import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy.js'
|
|||
import { defaultStore } from '@/store.js';
|
||||
import { customEmojisMap } from '@/custom-emojis.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApiGet } from '@/scripts/misskey-api.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import * as sound from '@/scripts/sound.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
name: string;
|
||||
|
@ -93,7 +95,19 @@ function onClick(ev: MouseEvent) {
|
|||
react(`:${props.name}:`);
|
||||
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>
|
||||
|
|
|
@ -61,7 +61,12 @@ export default function(props: MfmProps, context: SetupContext<MfmEvents>) {
|
|||
if (t == null) return null;
|
||||
return t.match(/^[0-9.]+s$/) ? t : null;
|
||||
};
|
||||
|
||||
|
||||
const validColor = (c: string | null | undefined): string | null => {
|
||||
if (c == null) return null;
|
||||
return c.match(/^[0-9a-f]{3,6}$/i) ? c : null;
|
||||
};
|
||||
|
||||
const useAnim = defaultStore.state.advancedMfm && defaultStore.state.animatedMfm;
|
||||
|
||||
/**
|
||||
|
@ -240,17 +245,30 @@ export default function(props: MfmProps, context: SetupContext<MfmEvents>) {
|
|||
break;
|
||||
}
|
||||
case 'fg': {
|
||||
let color = token.props.args.color;
|
||||
if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00';
|
||||
let color = validColor(token.props.args.color);
|
||||
color = color ?? 'f00';
|
||||
style = `color: #${color}; overflow-wrap: anywhere;`;
|
||||
break;
|
||||
}
|
||||
case 'bg': {
|
||||
let color = token.props.args.color;
|
||||
if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00';
|
||||
let color = validColor(token.props.args.color);
|
||||
color = color ?? 'f00';
|
||||
style = `background-color: #${color}; overflow-wrap: anywhere;`;
|
||||
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': {
|
||||
if (token.children.length === 1) {
|
||||
const child = token.children[0];
|
||||
|
|
|
@ -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_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'];
|
||||
|
|
|
@ -4,16 +4,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkSpacer :contentMax="800">
|
||||
<Transition
|
||||
:enterActiveClass="$style.transition_zoom_enterActive"
|
||||
:leaveActiveClass="$style.transition_zoom_leaveActive"
|
||||
:enterFromClass="$style.transition_zoom_enterFrom"
|
||||
:leaveToClass="$style.transition_zoom_leaveTo"
|
||||
:moveClass="$style.transition_zoom_move"
|
||||
mode="out-in"
|
||||
>
|
||||
<div v-if="!gameStarted" :class="$style.root">
|
||||
<Transition
|
||||
:enterActiveClass="$style.transition_zoom_enterActive"
|
||||
:leaveActiveClass="$style.transition_zoom_leaveActive"
|
||||
:enterFromClass="$style.transition_zoom_enterFrom"
|
||||
:leaveToClass="$style.transition_zoom_leaveTo"
|
||||
:moveClass="$style.transition_zoom_move"
|
||||
mode="out-in"
|
||||
>
|
||||
<MkSpacer v-if="!gameStarted" :contentMax="800">
|
||||
<div :class="$style.root">
|
||||
<div class="_gaps">
|
||||
<div :class="$style.frame" style="text-align: center;">
|
||||
<div :class="$style.frameInner">
|
||||
|
@ -26,6 +26,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkSelect v-model="gameMode">
|
||||
<option value="normal">NORMAL</option>
|
||||
<option value="square">SQUARE</option>
|
||||
<option value="yen">YEN</option>
|
||||
<!--<option value="sweets">SWEETS</option>-->
|
||||
</MkSelect>
|
||||
<MkButton primary gradate large rounded inline @click="start">{{ i18n.ts.start }}</MkButton>
|
||||
</div>
|
||||
|
@ -42,12 +44,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div :class="$style.frame">
|
||||
<div :class="$style.frameInner">
|
||||
<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-for="r in ranking" :key="r.id" :class="$style.rankingRecord">
|
||||
<MkAvatar :link="true" style="width: 24px; height: 24px; margin-right: 4px;" :user="r.user"/>
|
||||
<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 v-else>{{ i18n.ts.loading }}</div>
|
||||
|
@ -77,15 +79,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<XGame :gameMode="gameMode" :mute="mute" @end="onGameEnd"/>
|
||||
</div>
|
||||
</Transition>
|
||||
</MkSpacer>
|
||||
</MkSpacer>
|
||||
<XGame v-else :gameMode="gameMode" :mute="mute" @end="onGameEnd"/>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import XGame from './drop-and-fusion.game.vue';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
@ -94,7 +94,7 @@ import MkSelect from '@/components/MkSelect.vue';
|
|||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
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 mute = ref(false);
|
||||
const ranking = ref(null);
|
||||
|
@ -103,6 +103,14 @@ watch(gameMode, async () => {
|
|||
ranking.value = await misskeyApiGet('bubble-game/ranking', { gameMode: gameMode.value });
|
||||
}, { immediate: true });
|
||||
|
||||
function getScoreUnit(gameMode: string) {
|
||||
return gameMode === 'normal' ? 'pt' :
|
||||
gameMode === 'square' ? 'pt' :
|
||||
gameMode === 'yen' ? '円' :
|
||||
gameMode === 'sweets' ? 'kcal' :
|
||||
'' as never;
|
||||
}
|
||||
|
||||
async function start() {
|
||||
gameStarted.value = true;
|
||||
}
|
||||
|
|
|
@ -14,19 +14,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as os from '@/os.js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { misskeyApiGet } from '@/scripts/misskey-api.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
emoji: {
|
||||
name: string;
|
||||
aliases: string[];
|
||||
category: string;
|
||||
url: string;
|
||||
};
|
||||
emoji: Misskey.entities.EmojiSimple;
|
||||
}>();
|
||||
|
||||
function menu(ev) {
|
||||
|
@ -43,12 +39,13 @@ function menu(ev) {
|
|||
}, {
|
||||
text: i18n.ts.info,
|
||||
icon: 'ti ti-info-circle',
|
||||
action: () => {
|
||||
misskeyApiGet('emoji', { name: props.emoji.name }).then(res => {
|
||||
os.alert({
|
||||
type: 'info',
|
||||
text: `Name: ${res.name}\nAliases: ${res.aliases.join(' ')}\nCategory: ${res.category}\nisSensitive: ${res.isSensitive}\nlocalOnly: ${res.localOnly}\nLicense: ${res.license}\nURL: ${res.url}`,
|
||||
});
|
||||
action: async () => {
|
||||
os.popup(MkCustomEmojiDetailedDialog, {
|
||||
emoji: await misskeyApiGet('emoji', {
|
||||
name: props.emoji.name,
|
||||
})
|
||||
}, {
|
||||
anchor: ev.target,
|
||||
});
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
|
|
|
@ -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>
|
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div class="_gaps">
|
||||
<MkFoldableSection class="item">
|
||||
<template #header><i class="ti ti-activity"></i> Heatmap</template>
|
||||
<XHeatmap :user="user" :src="'notes'"/>
|
||||
<MkHeatmap :user="user" :src="'notes'"/>
|
||||
</MkFoldableSection>
|
||||
<MkFoldableSection class="item">
|
||||
<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>
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XHeatmap from './activity.heatmap.vue';
|
||||
import XPv from './activity.pv.vue';
|
||||
import XNotes from './activity.notes.vue';
|
||||
import XFollowing from './activity.following.vue';
|
||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
import MkHeatmap from '@/components/MkHeatmap.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
user: Misskey.entities.User;
|
||||
|
|
|
@ -10,14 +10,12 @@ import seedrandom from 'seedrandom';
|
|||
export type Mono = {
|
||||
id: string;
|
||||
level: number;
|
||||
size: number;
|
||||
shape: 'circle' | 'rectangle';
|
||||
sizeX: number;
|
||||
sizeY: number;
|
||||
shape: 'circle' | 'rectangle' | 'custom';
|
||||
vertices?: Matter.Vector[][];
|
||||
score: number;
|
||||
dropCandidate: boolean;
|
||||
sfxPitch: number;
|
||||
img: string;
|
||||
imgSize: number;
|
||||
spriteScale: number;
|
||||
};
|
||||
|
||||
type Log = {
|
||||
|
@ -32,23 +30,469 @@ type Log = {
|
|||
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<{
|
||||
changeScore: (newScore: number) => void;
|
||||
changeCombo: (newCombo: number) => void;
|
||||
changeStock: (newStock: { id: string; mono: Mono }[]) => void;
|
||||
changeHolding: (newHolding: { id: string; mono: Mono } | null) => 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;
|
||||
gameOver: () => void;
|
||||
sfx(type: string, params: { volume: number; pan: number; pitch: number; }): void;
|
||||
}> {
|
||||
private PHYSICS_QUALITY_FACTOR = 16; // 低いほどパフォーマンスが高いがガタガタして安定しなくなる、逆に高すぎても何故か不安定になる
|
||||
private COMBO_INTERVAL = 60; // frame
|
||||
public readonly GAME_VERSION = 1;
|
||||
public readonly GAME_VERSION = 2;
|
||||
public readonly GAME_WIDTH = 450;
|
||||
public readonly GAME_HEIGHT = 600;
|
||||
public readonly DROP_INTERVAL = 500;
|
||||
public readonly DROP_COOLTIME = 30; // frame
|
||||
public readonly PLAYAREA_MARGIN = 25;
|
||||
private STOCK_MAX = 4;
|
||||
private TICK_DELTA = 1000 / 60; // 60fps
|
||||
|
@ -58,15 +502,16 @@ export class DropAndFusionGame extends EventEmitter<{
|
|||
private tickCallbackQueue: { frame: number; callback: () => void; }[] = [];
|
||||
private overflowCollider: Matter.Body;
|
||||
private isGameOver = false;
|
||||
private monoDefinitions: Mono[] = [];
|
||||
private gameMode: 'normal' | 'yen' | 'square' | 'sweets';
|
||||
private rng: () => number;
|
||||
private logs: Log[] = [];
|
||||
private replaying = false;
|
||||
|
||||
/**
|
||||
* フィールドに出ていて、かつ合体の対象となるアイテム
|
||||
*/
|
||||
private activeBodyIds: Matter.Body['id'][] = [];
|
||||
private fusionReadyBodyIds: Matter.Body['id'][] = [];
|
||||
|
||||
private gameOverReadyBodyIds: Matter.Body['id'][] = [];
|
||||
|
||||
/**
|
||||
* fusion予約アイテムのペア
|
||||
|
@ -74,13 +519,20 @@ export class DropAndFusionGame extends EventEmitter<{
|
|||
*/
|
||||
private fusionReservedPairs: { bodyA: Matter.Body; bodyB: Matter.Body }[] = [];
|
||||
|
||||
private latestDroppedBodyId: Matter.Body['id'] | null = null;
|
||||
|
||||
private latestDroppedAt = 0;
|
||||
private latestDroppedAt = 0; // frame
|
||||
private latestFusionedAt = 0; // frame
|
||||
private stock: { id: string; mono: Mono }[] = [];
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
private _combo = 0;
|
||||
private get combo() {
|
||||
return this._combo;
|
||||
|
@ -99,16 +551,24 @@ export class DropAndFusionGame extends EventEmitter<{
|
|||
this.emit('changeScore', value);
|
||||
}
|
||||
|
||||
private getMonoRenderOptions: null | ((mono: Mono) => Partial<Matter.IBodyRenderOptions>) = null;
|
||||
|
||||
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();
|
||||
|
||||
this.replaying = !!env.replaying;
|
||||
this.monoDefinitions = env.monoDefinitions;
|
||||
this.rng = seedrandom(env.seed);
|
||||
|
||||
//#region BIND
|
||||
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({
|
||||
constraintIterations: 2 * this.PHYSICS_QUALITY_FACTOR,
|
||||
|
@ -147,6 +607,7 @@ export class DropAndFusionGame extends EventEmitter<{
|
|||
//#endregion
|
||||
|
||||
this.overflowCollider = Matter.Bodies.rectangle(this.GAME_WIDTH / 2, 0, this.GAME_WIDTH, 200, {
|
||||
label: '_overflow_',
|
||||
isStatic: true,
|
||||
isSensor: true,
|
||||
render: {
|
||||
|
@ -157,34 +618,37 @@ export class DropAndFusionGame extends EventEmitter<{
|
|||
Matter.Composite.add(this.engine.world, this.overflowCollider);
|
||||
}
|
||||
|
||||
private msToFrame(ms: number) {
|
||||
public msToFrame(ms: number) {
|
||||
return Math.round(ms / this.TICK_DELTA);
|
||||
}
|
||||
|
||||
public frameToMs(frame: number) {
|
||||
return frame * this.TICK_DELTA;
|
||||
}
|
||||
|
||||
private createBody(mono: Mono, x: number, y: number) {
|
||||
const options: Matter.IBodyDefinition = {
|
||||
label: mono.id,
|
||||
//density: 0.0005,
|
||||
density: mono.size / 1000,
|
||||
density: ((mono.sizeX + mono.sizeY) / 2) / 1000,
|
||||
restitution: 0.2,
|
||||
frictionAir: 0.01,
|
||||
friction: 0.7,
|
||||
frictionStatic: 5,
|
||||
slop: 1.0,
|
||||
//mass: 0,
|
||||
render: {
|
||||
sprite: {
|
||||
texture: mono.img,
|
||||
xScale: (mono.size / mono.imgSize) * mono.spriteScale,
|
||||
yScale: (mono.size / mono.imgSize) * mono.spriteScale,
|
||||
},
|
||||
},
|
||||
render: this.getMonoRenderOptions ? this.getMonoRenderOptions(mono) : undefined,
|
||||
};
|
||||
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
|
||||
} 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 {
|
||||
throw new Error('unrecognized shape');
|
||||
}
|
||||
|
@ -198,15 +662,15 @@ export class DropAndFusionGame extends EventEmitter<{
|
|||
}
|
||||
this.latestFusionedAt = this.frame;
|
||||
|
||||
// TODO: 単に位置だけでなくそれぞれの動きベクトルも融合する?
|
||||
const newX = (bodyA.position.x + bodyB.position.x) / 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]);
|
||||
this.activeBodyIds = this.activeBodyIds.filter(x => x !== bodyA.id && x !== bodyB.id);
|
||||
|
||||
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) {
|
||||
const body = this.createBody(nextMono, newX, newY);
|
||||
|
@ -216,43 +680,24 @@ export class DropAndFusionGame extends EventEmitter<{
|
|||
this.tickCallbackQueue.push({
|
||||
frame: this.frame + this.msToFrame(100),
|
||||
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('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>) {
|
||||
const minCollisionEnergyForSound = 2.5;
|
||||
const maxCollisionEnergyForSound = 9;
|
||||
const soundPitchMax = 4;
|
||||
const soundPitchMin = 0.5;
|
||||
|
||||
for (const pairs of event.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) &&
|
||||
!this.fusionReservedPairs.some(x =>
|
||||
x.bodyA.id === bodyA.id ||
|
||||
|
@ -261,7 +706,7 @@ export class DropAndFusionGame extends EventEmitter<{
|
|||
x.bodyB.id === bodyB.id);
|
||||
|
||||
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);
|
||||
} else {
|
||||
this.fusionReservedPairs.push({ bodyA, bodyB });
|
||||
|
@ -275,17 +720,30 @@ export class DropAndFusionGame extends EventEmitter<{
|
|||
}
|
||||
} else {
|
||||
const energy = pairs.collision.depth;
|
||||
if (energy > minCollisionEnergyForSound) {
|
||||
const volume = (Math.min(maxCollisionEnergyForSound, energy - minCollisionEnergyForSound) / maxCollisionEnergyForSound) / 4;
|
||||
const panV =
|
||||
pairs.bodyA.label === '_wall_' ? bodyB.position.x - this.PLAYAREA_MARGIN :
|
||||
pairs.bodyB.label === '_wall_' ? bodyA.position.x - this.PLAYAREA_MARGIN :
|
||||
((bodyA.position.x + bodyB.position.x) / 2) - this.PLAYAREA_MARGIN;
|
||||
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('sfx', 'collision', { volume, pan, pitch });
|
||||
|
||||
if (bodyA.label === '_overflow_' || bodyB.label === '_overflow_') continue;
|
||||
|
||||
if (bodyA.label !== '_wall_' && bodyB.label !== '_wall_') {
|
||||
if (!this.gameOverReadyBodyIds.includes(bodyA.id)) this.gameOverReadyBodyIds.push(bodyA.id);
|
||||
if (!this.gameOverReadyBodyIds.includes(bodyB.id)) this.gameOverReadyBodyIds.push(bodyB.id);
|
||||
}
|
||||
|
||||
this.emit('collision', energy, bodyA, bodyB);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -314,6 +772,7 @@ export class DropAndFusionGame extends EventEmitter<{
|
|||
this.emit('changeStock', this.stock);
|
||||
|
||||
Matter.Events.on(this.engine, 'collisionStart', this.onCollision.bind(this));
|
||||
Matter.Events.on(this.engine, 'collisionActive', this.onCollisionActive.bind(this));
|
||||
}
|
||||
|
||||
public getLogs() {
|
||||
|
@ -349,8 +808,7 @@ export class DropAndFusionGame extends EventEmitter<{
|
|||
|
||||
public drop(_x: number) {
|
||||
if (this.isGameOver) return;
|
||||
// TODO: フレームで計算するようにすればリプレイかどうかのチェックは不要になる
|
||||
if (!this.replaying && (Date.now() - this.latestDroppedAt < this.DROP_INTERVAL)) return;
|
||||
if (this.frame - this.latestDroppedAt < this.DROP_COOLTIME) return;
|
||||
|
||||
const head = this.stock.shift()!;
|
||||
this.stock.push({
|
||||
|
@ -360,17 +818,18 @@ export class DropAndFusionGame extends EventEmitter<{
|
|||
this.emit('changeStock', this.stock);
|
||||
|
||||
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 body = this.createBody(head.mono, x, 50 + head.mono.size / 2);
|
||||
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.sizeY / 2);
|
||||
this.logs.push({
|
||||
frame: this.frame,
|
||||
operation: 'drop',
|
||||
x: inputX,
|
||||
});
|
||||
Matter.Composite.add(this.engine.world, body);
|
||||
this.activeBodyIds.push(body.id);
|
||||
this.latestDroppedBodyId = body.id;
|
||||
this.latestDroppedAt = Date.now();
|
||||
|
||||
this.fusionReadyBodyIds.push(body.id);
|
||||
this.latestDroppedAt = this.frame;
|
||||
|
||||
this.emit('dropped', x);
|
||||
this.emit('monoAdded', head.mono);
|
||||
}
|
||||
|
|
|
@ -7,14 +7,14 @@
|
|||
"generate": "tsx src/generator.ts && eslint ./built/**/* --ext .ts --fix"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@apidevtools/swagger-parser": "10.1.0",
|
||||
"@misskey-dev/eslint-plugin": "^1.0.0",
|
||||
"@readme/openapi-parser": "2.5.0",
|
||||
"@types/node": "20.9.1",
|
||||
"@typescript-eslint/eslint-plugin": "6.11.0",
|
||||
"@typescript-eslint/parser": "6.11.0",
|
||||
"eslint": "8.53.0",
|
||||
"openapi-types": "12.1.3",
|
||||
"openapi-typescript": "6.7.1",
|
||||
"openapi-typescript": "6.7.3",
|
||||
"ts-case-convert": "2.0.2",
|
||||
"tsx": "4.4.0",
|
||||
"typescript": "5.3.3"
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { mkdir, writeFile } from 'fs/promises';
|
||||
import { OpenAPIV3 } from 'openapi-types';
|
||||
import { OpenAPIV3_1 } from 'openapi-types';
|
||||
import { toPascal } from 'ts-case-convert';
|
||||
import SwaggerParser from '@apidevtools/swagger-parser';
|
||||
import OpenAPIParser from '@readme/openapi-parser';
|
||||
import openapiTS from 'openapi-typescript';
|
||||
|
||||
function generateVersionHeaderComment(openApiDocs: OpenAPIV3.Document): string {
|
||||
function generateVersionHeaderComment(openApiDocs: OpenAPIV3_1.Document): string {
|
||||
const contents = {
|
||||
version: openApiDocs.info.version,
|
||||
generatedAt: new Date().toISOString(),
|
||||
|
@ -21,7 +21,7 @@ function generateVersionHeaderComment(openApiDocs: OpenAPIV3.Document): string {
|
|||
}
|
||||
|
||||
async function generateBaseTypes(
|
||||
openApiDocs: OpenAPIV3.Document,
|
||||
openApiDocs: OpenAPIV3_1.Document,
|
||||
openApiJsonPath: string,
|
||||
typeFileName: string,
|
||||
) {
|
||||
|
@ -47,7 +47,7 @@ async function generateBaseTypes(
|
|||
}
|
||||
|
||||
async function generateSchemaEntities(
|
||||
openApiDocs: OpenAPIV3.Document,
|
||||
openApiDocs: OpenAPIV3_1.Document,
|
||||
typeFileName: string,
|
||||
outputPath: string,
|
||||
) {
|
||||
|
@ -71,7 +71,7 @@ async function generateSchemaEntities(
|
|||
}
|
||||
|
||||
async function generateEndpoints(
|
||||
openApiDocs: OpenAPIV3.Document,
|
||||
openApiDocs: OpenAPIV3_1.Document,
|
||||
typeFileName: string,
|
||||
entitiesOutputPath: string,
|
||||
endpointOutputPath: string,
|
||||
|
@ -79,7 +79,7 @@ async function generateEndpoints(
|
|||
const endpoints: Endpoint[] = [];
|
||||
|
||||
// misskey-jsはPOST固定で送っているので、こちらも決め打ちする。別メソッドに対応することがあればこちらも直す必要あり
|
||||
const paths = openApiDocs.paths;
|
||||
const paths = openApiDocs.paths ?? {};
|
||||
const postPathItems = Object.keys(paths)
|
||||
.map(it => paths[it]?.post)
|
||||
.filter(filterUndefined);
|
||||
|
@ -160,7 +160,7 @@ async function generateEndpoints(
|
|||
}
|
||||
|
||||
async function generateApiClientJSDoc(
|
||||
openApiDocs: OpenAPIV3.Document,
|
||||
openApiDocs: OpenAPIV3_1.Document,
|
||||
apiClientFileName: string,
|
||||
endpointsFileName: string,
|
||||
warningsOutputPath: string,
|
||||
|
@ -168,7 +168,7 @@ async function generateApiClientJSDoc(
|
|||
const endpoints: { operationId: string; description: string; }[] = [];
|
||||
|
||||
// misskey-jsはPOST固定で送っているので、こちらも決め打ちする。別メソッドに対応することがあればこちらも直す必要あり
|
||||
const paths = openApiDocs.paths;
|
||||
const paths = openApiDocs.paths ?? {};
|
||||
const postPathItems = Object.keys(paths)
|
||||
.map(it => paths[it]?.post)
|
||||
.filter(filterUndefined);
|
||||
|
@ -221,21 +221,21 @@ async function generateApiClientJSDoc(
|
|||
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) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { content } = value as Record<keyof OpenAPIV3.RequestBodyObject, unknown>;
|
||||
const { content } = value as Record<keyof OpenAPIV3_1.RequestBodyObject, unknown>;
|
||||
return content !== undefined;
|
||||
}
|
||||
|
||||
function isResponseObject(value: unknown): value is OpenAPIV3.ResponseObject {
|
||||
function isResponseObject(value: unknown): value is OpenAPIV3_1.ResponseObject {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { description } = value as Record<keyof OpenAPIV3.ResponseObject, unknown>;
|
||||
const { description } = value as Record<keyof OpenAPIV3_1.ResponseObject, unknown>;
|
||||
return description !== undefined;
|
||||
}
|
||||
|
||||
|
@ -330,7 +330,7 @@ async function main() {
|
|||
await mkdir(generatePath, { recursive: true });
|
||||
|
||||
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';
|
||||
await generateBaseTypes(openApiDocs, openApiJsonPath, typeFileName);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* version: 2023.12.2
|
||||
* generatedAt: 2024-01-11T14:29:04.817Z
|
||||
* generatedAt: 2024-01-13T04:31:38.782Z
|
||||
*/
|
||||
|
||||
import type { SwitchCaseResponseType } from '../api.js';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* version: 2023.12.2
|
||||
* generatedAt: 2024-01-11T14:29:04.814Z
|
||||
* generatedAt: 2024-01-13T04:31:38.778Z
|
||||
*/
|
||||
|
||||
import type {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* version: 2023.12.2
|
||||
* generatedAt: 2024-01-11T14:29:04.811Z
|
||||
* generatedAt: 2024-01-13T04:31:38.775Z
|
||||
*/
|
||||
|
||||
import { operations } from './types.js';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* version: 2023.12.2
|
||||
* generatedAt: 2024-01-11T14:29:04.810Z
|
||||
* generatedAt: 2024-01-13T04:31:38.773Z
|
||||
*/
|
||||
|
||||
import { components } from './types.js';
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
/*
|
||||
* 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[];
|
||||
files?: components['schemas']['DriveFile'][];
|
||||
tags?: string[];
|
||||
poll?: Record<string, unknown> | null;
|
||||
poll?: Record<string, never> | null;
|
||||
/**
|
||||
* Format: id
|
||||
* @example xxxxxxxxxx
|
||||
|
@ -3903,7 +3903,7 @@ export type components = {
|
|||
url?: string;
|
||||
reactionAndUserPairCache?: string[];
|
||||
clippedCount?: number;
|
||||
myReaction?: Record<string, unknown> | null;
|
||||
myReaction?: Record<string, never> | null;
|
||||
};
|
||||
NoteReaction: {
|
||||
/**
|
||||
|
|
117
pnpm-lock.yaml
|
@ -1083,12 +1083,12 @@ importers:
|
|||
|
||||
packages/misskey-js/generator:
|
||||
devDependencies:
|
||||
'@apidevtools/swagger-parser':
|
||||
specifier: 10.1.0
|
||||
version: 10.1.0(openapi-types@12.1.3)
|
||||
'@misskey-dev/eslint-plugin':
|
||||
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)
|
||||
'@readme/openapi-parser':
|
||||
specifier: 2.5.0
|
||||
version: 2.5.0(openapi-types@12.1.3)
|
||||
'@types/node':
|
||||
specifier: 20.9.1
|
||||
version: 20.9.1
|
||||
|
@ -1105,8 +1105,8 @@ importers:
|
|||
specifier: 12.1.3
|
||||
version: 12.1.3
|
||||
openapi-typescript:
|
||||
specifier: 6.7.1
|
||||
version: 6.7.1
|
||||
specifier: 6.7.3
|
||||
version: 6.7.3
|
||||
ts-case-convert:
|
||||
specifier: 2.0.2
|
||||
version: 2.0.2
|
||||
|
@ -1170,14 +1170,6 @@ packages:
|
|||
'@jridgewell/trace-mapping': 0.3.18
|
||||
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:
|
||||
resolution: {integrity: sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
@ -1187,21 +1179,6 @@ packages:
|
|||
resolution: {integrity: sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==}
|
||||
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:
|
||||
resolution: {integrity: sha512-Xk1sIhyNC/esHGGVjL/niHLowM0csl/kFO5uawBy4IrWwy0o1G8LGt3jP6nmWGz+USxeeqbihAmp/oVZju6wug==}
|
||||
hasBin: true
|
||||
|
@ -3331,7 +3308,6 @@ packages:
|
|||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
regenerator-runtime: 0.14.0
|
||||
dev: false
|
||||
|
||||
/@babel/template@7.22.15:
|
||||
resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==}
|
||||
|
@ -4553,6 +4529,11 @@ packages:
|
|||
engines: {node: '>=12.22'}
|
||||
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:
|
||||
resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==}
|
||||
dev: true
|
||||
|
@ -5866,6 +5847,48 @@ packages:
|
|||
'@babel/runtime': 7.23.2
|
||||
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):
|
||||
resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
@ -10454,6 +10477,11 @@ packages:
|
|||
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
|
||||
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:
|
||||
resolution: {integrity: sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==}
|
||||
dev: true
|
||||
|
@ -13075,6 +13103,10 @@ packages:
|
|||
/graceful-fs@4.2.11:
|
||||
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:
|
||||
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
|
||||
dev: true
|
||||
|
@ -14666,6 +14698,14 @@ packages:
|
|||
/json-stringify-safe@5.0.1:
|
||||
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:
|
||||
resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
|
||||
hasBin: true
|
||||
|
@ -14713,6 +14753,11 @@ packages:
|
|||
- web-streams-polyfill
|
||||
dev: false
|
||||
|
||||
/jsonpointer@5.0.1:
|
||||
resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/jsprim@1.4.2:
|
||||
resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==}
|
||||
engines: {node: '>=0.6.0'}
|
||||
|
@ -16060,15 +16105,15 @@ packages:
|
|||
/openapi-types@12.1.3:
|
||||
resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
|
||||
|
||||
/openapi-typescript@6.7.1:
|
||||
resolution: {integrity: sha512-Q3Ltt0KUm2smcPrsaR8qKmSwQ1KM4yGDJVoQdpYa0yvKPeN8huDx5utMT7DvwvJastHHzUxajjivK3WN2+fobg==}
|
||||
/openapi-typescript@6.7.3:
|
||||
resolution: {integrity: sha512-es3mGcDXV6TKPo6n3aohzHm0qxhLyR39MhF6mkD1FwFGjhxnqMqfSIgM0eCpInZvqatve4CxmXcMZw3jnnsaXw==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
ansi-colors: 4.1.3
|
||||
fast-glob: 3.3.2
|
||||
js-yaml: 4.1.0
|
||||
supports-color: 9.4.0
|
||||
undici: 5.28.1
|
||||
undici: 5.28.2
|
||||
yargs-parser: 21.1.1
|
||||
dev: true
|
||||
|
||||
|
@ -19557,6 +19602,14 @@ packages:
|
|||
engines: {node: '>=14.0'}
|
||||
dependencies:
|
||||
'@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:
|
||||
resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
module.exports = {
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ['./tsconfig.json'],
|
||||
},
|
||||
extends: [
|
||||
'../../packages/shared/.eslintrc.js',
|
||||
],
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
node_modules
|
||||
coverage
|
||||
.idea
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
import { Release } from './parser.js';
|
||||
|
||||
export class Result {
|
||||
public readonly success: boolean;
|
||||
public readonly message?: string;
|
||||
|
||||
private constructor(success: boolean, message?: string) {
|
||||
this.success = success;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
static ofSuccess(): Result {
|
||||
return new Result(true);
|
||||
}
|
||||
|
||||
static ofFailed(message?: string): Result {
|
||||
return new Result(false, message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* develop -> masterまたはrelease -> masterを想定したパターン。
|
||||
* base側の先頭とhead側で追加された分のリリースより1つ前のバージョンが等価であるかチェックする。
|
||||
*/
|
||||
export function checkNewRelease(base: Release[], head: Release[]): Result {
|
||||
const releaseCountDiff = head.length - base.length;
|
||||
if (releaseCountDiff <= 0) {
|
||||
return Result.ofFailed('Invalid release count.');
|
||||
}
|
||||
|
||||
const baseLatest = base[0];
|
||||
const headPrevious = head[releaseCountDiff];
|
||||
|
||||
if (baseLatest.releaseName !== headPrevious.releaseName) {
|
||||
return Result.ofFailed('Contains unexpected releases.');
|
||||
}
|
||||
|
||||
return Result.ofSuccess();
|
||||
}
|
||||
|
||||
/**
|
||||
* topic -> developまたはtopic -> masterを想定したパターン。
|
||||
* head側の最新リリース配下に書き加えられているかをチェックする。
|
||||
*/
|
||||
export function checkNewTopic(base: Release[], head: Release[]): Result {
|
||||
if (head.length !== base.length) {
|
||||
return Result.ofFailed('Invalid release count.');
|
||||
}
|
||||
|
||||
const headLatest = head[0];
|
||||
for (let relIdx = 0; relIdx < base.length; relIdx++) {
|
||||
const baseItem = base[relIdx];
|
||||
const headItem = head[relIdx];
|
||||
if (baseItem.releaseName !== headItem.releaseName) {
|
||||
// リリースの順番が変わってると成立しないのでエラーにする
|
||||
return Result.ofFailed(`Release is different. base:${baseItem.releaseName}, head:${headItem.releaseName}`);
|
||||
}
|
||||
|
||||
if (baseItem.categories.length !== headItem.categories.length) {
|
||||
// カテゴリごと書き加えられたパターン
|
||||
if (headLatest.releaseName !== headItem.releaseName) {
|
||||
// 最新リリース以外に追記されていた場合
|
||||
return Result.ofFailed(`There is an error in the update history. expected additions:${headLatest.releaseName}, actual additions:${headItem.releaseName}`);
|
||||
}
|
||||
} else {
|
||||
// カテゴリ数の変動はないのでリスト項目の数をチェック
|
||||
for (let catIdx = 0; catIdx < baseItem.categories.length; catIdx++) {
|
||||
const baseCategory = baseItem.categories[catIdx];
|
||||
const headCategory = headItem.categories[catIdx];
|
||||
|
||||
if (baseCategory.categoryName !== headCategory.categoryName) {
|
||||
// カテゴリの順番が変わっていると成立しないのでエラーにする
|
||||
return Result.ofFailed(`Category is different. base:${baseCategory.categoryName}, head:${headCategory.categoryName}`);
|
||||
}
|
||||
|
||||
if (baseCategory.items.length !== headCategory.items.length) {
|
||||
if (headLatest.releaseName !== headItem.releaseName) {
|
||||
// 最新リリース以外に追記されていた場合
|
||||
return Result.ofFailed(`There is an error in the update history. expected additions:${headLatest.releaseName}, actual additions:${headItem.releaseName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Result.ofSuccess();
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import * as process from 'process';
|
||||
import * as fs from 'fs';
|
||||
import { parseChangeLog } from './parser.js';
|
||||
import { checkNewRelease, checkNewTopic } from './checker.js';
|
||||
|
||||
function abort(message?: string) {
|
||||
if (message) {
|
||||
console.error(message);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (!fs.existsSync('./CHANGELOG-base.md') || !fs.existsSync('./CHANGELOG-head.md')) {
|
||||
console.error('CHANGELOG-base.md or CHANGELOG-head.md is missing.');
|
||||
return;
|
||||
}
|
||||
|
||||
const base = parseChangeLog('./CHANGELOG-base.md');
|
||||
const head = parseChangeLog('./CHANGELOG-head.md');
|
||||
|
||||
const result = (base.length < head.length)
|
||||
? checkNewRelease(base, head)
|
||||
: checkNewTopic(base, head);
|
||||
|
||||
if (!result.success) {
|
||||
abort(result.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
|
@ -0,0 +1,62 @@
|
|||
import * as fs from 'node:fs';
|
||||
import { unified } from 'unified';
|
||||
import remarkParse from 'remark-parse';
|
||||
import { Heading, List, Node } from 'mdast';
|
||||
import { toString } from 'mdast-util-to-string';
|
||||
|
||||
export class Release {
|
||||
public readonly releaseName: string;
|
||||
public readonly categories: ReleaseCategory[];
|
||||
|
||||
constructor(releaseName: string, categories: ReleaseCategory[] = []) {
|
||||
this.releaseName = releaseName;
|
||||
this.categories = [...categories];
|
||||
}
|
||||
}
|
||||
|
||||
export class ReleaseCategory {
|
||||
public readonly categoryName: string;
|
||||
public readonly items: string[];
|
||||
|
||||
constructor(categoryName: string, items: string[] = []) {
|
||||
this.categoryName = categoryName;
|
||||
this.items = [...items];
|
||||
}
|
||||
}
|
||||
|
||||
function isHeading(node: Node): node is Heading {
|
||||
return node.type === 'heading';
|
||||
}
|
||||
|
||||
function isList(node: Node): node is List {
|
||||
return node.type === 'list';
|
||||
}
|
||||
|
||||
export function parseChangeLog(path: string): Release[] {
|
||||
const input = fs.readFileSync(path, { encoding: 'utf8' });
|
||||
const processor = unified().use(remarkParse);
|
||||
|
||||
const releases: Release[] = [];
|
||||
const root = processor.parse(input);
|
||||
|
||||
let release: Release | null = null;
|
||||
let category: ReleaseCategory | null = null;
|
||||
for (const it of root.children) {
|
||||
if (isHeading(it) && it.depth === 2) {
|
||||
// リリース
|
||||
release = new Release(toString(it));
|
||||
releases.push(release);
|
||||
} else if (isHeading(it) && it.depth === 3 && release) {
|
||||
// リリース配下のカテゴリ
|
||||
category = new ReleaseCategory(toString(it));
|
||||
release.categories.push(category);
|
||||
} else if (isList(it) && category) {
|
||||
for (const listItem of it.children) {
|
||||
// カテゴリ配下のリスト項目
|
||||
category.items.push(toString(listItem));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return releases;
|
||||
}
|
|
@ -0,0 +1,414 @@
|
|||
import {expect, suite, test} from "vitest";
|
||||
import {Release, ReleaseCategory} from "../src/parser";
|
||||
import {checkNewRelease, checkNewTopic} from "../src/checker";
|
||||
|
||||
suite('checkNewRelease', () => {
|
||||
test('headに新しいリリースがある1', () => {
|
||||
const base = [new Release('2024.12.0')]
|
||||
const head = [new Release('2024.12.1'), new Release('2024.12.0')]
|
||||
|
||||
const result = checkNewRelease(base, head)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
test('headに新しいリリースがある2', () => {
|
||||
const base = [new Release('2024.12.0')]
|
||||
const head = [new Release('2024.12.2'), new Release('2024.12.1'), new Release('2024.12.0')]
|
||||
|
||||
const result = checkNewRelease(base, head)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
|
||||
test('リリースの数が同じ', () => {
|
||||
const base = [new Release('2024.12.0')]
|
||||
const head = [new Release('2024.12.0')]
|
||||
|
||||
const result = checkNewRelease(base, head)
|
||||
|
||||
console.log(result.message)
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
test('baseにあるリリースがheadにない', () => {
|
||||
const base = [new Release('2024.12.0')]
|
||||
const head = [new Release('2024.12.2'), new Release('2024.12.1')]
|
||||
|
||||
const result = checkNewRelease(base, head)
|
||||
|
||||
console.log(result.message)
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
suite('checkNewTopic', () => {
|
||||
test('追記なし', () => {
|
||||
const base = [
|
||||
new Release('2024.12.1', [
|
||||
new ReleaseCategory('Server', [
|
||||
'feat1',
|
||||
'feat2',
|
||||
]),
|
||||
new ReleaseCategory('Client', [
|
||||
'feat3',
|
||||
'feat4',
|
||||
])
|
||||
]),
|
||||
new Release('2024.12.0', [
|
||||
new ReleaseCategory('Server', [
|
||||
'feat1',
|
||||
'feat2',
|
||||
]),
|
||||
new ReleaseCategory('Client', [
|
||||
'feat3',
|
||||
'feat4',
|
||||
])
|
||||
])
|
||||
]
|
||||
|
||||
const head = [
|
||||
new Release('2024.12.1', [
|
||||
new ReleaseCategory('Server', [
|
||||
'feat1',
|
||||
'feat2',
|
||||
]),
|
||||
new ReleaseCategory('Client', [
|
||||
'feat3',
|
||||
'feat4',
|
||||
])
|
||||
]),
|
||||
new Release('2024.12.0', [
|
||||
new ReleaseCategory('Server', [
|
||||
'feat1',
|
||||
'feat2',
|
||||
]),
|
||||
new ReleaseCategory('Client', [
|
||||
'feat3',
|
||||
'feat4',
|
||||
])
|
||||
])
|
||||
]
|
||||
|
||||
const result = checkNewTopic(base, head)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
test('最新バージョンにカテゴリを追加したときはエラーにならない', () => {
|
||||
const base = [
|
||||
new Release('2024.12.1', [
|
||||
new ReleaseCategory('Server', [
|
||||
'feat1',
|
||||
'feat2',
|
||||
]),
|
||||
]),
|
||||
new Release('2024.12.0', [
|
||||
new ReleaseCategory('Server', [
|
||||
'feat1',
|
||||
'feat2',
|
||||
]),
|
||||
new ReleaseCategory('Client', [
|
||||
'feat3',
|
||||
'feat4',
|
||||
])
|
||||
])
|
||||
]
|
||||
|
||||
const head = [
|
||||
new Release('2024.12.1', [
|
||||
new ReleaseCategory('Server', [
|
||||
'feat1',
|
||||
'feat2',
|
||||
]),
|
||||
new ReleaseCategory('Client', [
|
||||
'feat3',
|
||||
'feat4',
|
||||
])
|
||||
]),
|
||||
new Release('2024.12.0', [
|
||||
new ReleaseCategory('Server', [
|
||||
'feat1',
|
||||
'feat2',
|
||||
]),
|
||||
new ReleaseCategory('Client', [
|
||||
'feat3',
|
||||
'feat4',
|
||||
])
|
||||
])
|
||||
]
|
||||
|
||||
const result = checkNewTopic(base, head)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
test('最新バージョンからカテゴリを削除したときはエラーにならない', () => {
|
||||
const base = [
|
||||
new Release('2024.12.1', [
|
||||
new ReleaseCategory('Server', [
|
||||
'feat1',
|
||||
'feat2',
|
||||
]),
|
||||
new ReleaseCategory('Client', [
|
||||
'feat3',
|
||||
'feat4',
|
||||
])
|
||||
]),
|
||||
new Release('2024.12.0', [
|
||||
new ReleaseCategory('Server', [
|
||||
'feat1',
|
||||
'feat2',
|
||||
]),
|
||||
new ReleaseCategory('Client', [
|
||||
'feat3',
|
||||
'feat4',
|
||||
])
|
||||
])
|
||||
]
|
||||
|
||||
const head = [
|
||||
new Release('2024.12.1', [
|
||||
new ReleaseCategory('Server', [
|
||||
'feat1',
|
||||
'feat2',
|
||||
]),
|
||||
]),
|
||||
new Release('2024.12.0', [
|
||||
new ReleaseCategory('Server', [
|
||||
'feat1',
|
||||
'feat2',
|
||||
]),
|
||||
new ReleaseCategory('Client', [
|
||||
'feat3',
|
||||
'feat4',
|
||||
])
|
||||
])
|
||||
]
|
||||
|
||||
const result = checkNewTopic(base, head)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
test('最新バージョンに追記したときはエラーにならない', () => {
|
||||
const base = [
|
||||
new Release('2024.12.1', [
|
||||
new ReleaseCategory('Server', [
|
||||
'feat1',
|
||||
'feat2',
|
||||
]),
|
||||
]),
|
||||
new Release('2024.12.0', [
|
||||
new ReleaseCategory('Server', [
|
||||
'feat1',
|
||||
'feat2',
|
||||
]),
|
||||
])
|
||||
]
|
||||
|
||||
const head = [
|
||||
new Release('2024.12.1', [
|
||||
new ReleaseCategory('Server', [
|
||||
'feat1',
|
||||
'feat2',
|
||||
'feat3',
|
||||
]),
|
||||
]),
|
||||
new Release('2024.12.0', [
|
||||
new ReleaseCategory('Server', [
|
||||
'feat1',
|
||||
'feat2',
|
||||
]),
|
||||
])
|
||||
]
|
||||
|
||||
const result = checkNewTopic(base, head)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
test('最新バージョンから削除したときはエラーにならない', () => {
|
||||
const base = [
|
||||
new Release('2024.12.1', [
|
||||
new ReleaseCategory('Server', [
|
||||
'feat1',
|
||||
'feat2',
|
||||
]),
|
||||
]),
|
||||
new Release('2024.12.0', [
|
||||
new ReleaseCategory('Server', [
|
||||
'feat1',
|
||||
'feat2',
|
||||
]),
|
||||
])
|
||||
]
|
||||
|
||||
const head = [
|
||||
new Release('2024.12.1', [
|
||||
new ReleaseCategory('Server', [
|
||||
'feat1',
|
||||
]),
|
||||
]),
|
||||
new Release('2024.12.0', [
|
||||
new ReleaseCategory('Server', [
|
||||
'feat1',
|
||||
'feat2',
|
||||
]),
|
||||
])
|
||||
]
|
||||
|
||||
const result = checkNewTopic(base, head)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
test('古いバージョンにカテゴリを追加したときはエラーになる', () => {
|
||||
const base = [
|
||||
new Release('2024.12.1', [
|
||||
new ReleaseCategory('Server', [
|
||||
'feat1',
|
||||
'feat2',
|
||||
]),
|
||||
]),
|
||||
new Release('2024.12.0', [
|
||||
new ReleaseCategory('Server', [
|
||||
'feat1',
|
||||
'feat2',
|
||||
]),
|
||||
])
|
||||
]
|
||||
|
||||
const head = [
|
||||
new Release('2024.12.1', [
|
||||
new ReleaseCategory('Server', [
|
||||
'feat1',
|
||||
'feat2',
|
||||
]),
|
||||
]),
|
||||
new Release('2024.12.0', [
|
||||
new ReleaseCategory('Server', [
|
||||
'feat1',
|
||||
'feat2',
|
||||
]),
|
||||
new ReleaseCategory('Client', [
|
||||
'feat1',
|
||||
'feat2',
|
||||
]),
|
||||
])
|
||||
]
|
||||
|
||||
const result = checkNewTopic(base, head)
|
||||
|
||||
console.log(result.message)
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
test('古いバージョンからカテゴリを削除したときはエラーになる', () => {
|
||||
const base = [
|
||||
new Release('2024.12.1', [
|
||||
new ReleaseCategory('Server', [
|
||||
'feat1',
|
||||
'feat2',
|
||||
]),
|
||||
]),
|
||||
new Release('2024.12.0', [
|
||||
new ReleaseCategory('Server', [
|
||||
'feat1',
|
||||
'feat2',
|
||||
]),
|
||||
])
|
||||
]
|
||||
|
||||
const head = [
|
||||
new Release('2024.12.1', [
|
||||
new ReleaseCategory('Server', [
|
||||
'feat1',
|
||||
'feat2',
|
||||
]),
|
||||
]),
|
||||
new Release('2024.12.0', [
|
||||
])
|
||||
]
|
||||
|
||||
const result = checkNewTopic(base, head)
|
||||
|
||||
console.log(result.message)
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
test('古いバージョンに追記したときはエラーになる', () => {
|
||||
const base = [
|
||||
new Release('2024.12.1', [
|
||||
new ReleaseCategory('Server', [
|
||||
'feat1',
|
||||
'feat2',
|
||||
]),
|
||||
]),
|
||||
new Release('2024.12.0', [
|
||||
new ReleaseCategory('Server', [
|
||||
'feat1',
|
||||
'feat2',
|
||||
]),
|
||||
])
|
||||
]
|
||||
|
||||
const head = [
|
||||
new Release('2024.12.1', [
|
||||
new ReleaseCategory('Server', [
|
||||
'feat1',
|
||||
'feat2',
|
||||
]),
|
||||
]),
|
||||
new Release('2024.12.0', [
|
||||
new ReleaseCategory('Server', [
|
||||
'feat1',
|
||||
'feat2',
|
||||
'feat3',
|
||||
]),
|
||||
])
|
||||
]
|
||||
|
||||
const result = checkNewTopic(base, head)
|
||||
|
||||
console.log(result.message)
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
test('古いバージョンから削除したときはエラーになる', () => {
|
||||
const base = [
|
||||
new Release('2024.12.1', [
|
||||
new ReleaseCategory('Server', [
|
||||
'feat1',
|
||||
'feat2',
|
||||
]),
|
||||
]),
|
||||
new Release('2024.12.0', [
|
||||
new ReleaseCategory('Server', [
|
||||
'feat1',
|
||||
'feat2',
|
||||
]),
|
||||
])
|
||||
]
|
||||
|
||||
const head = [
|
||||
new Release('2024.12.1', [
|
||||
new ReleaseCategory('Server', [
|
||||
'feat1',
|
||||
'feat2',
|
||||
]),
|
||||
]),
|
||||
new Release('2024.12.0', [
|
||||
new ReleaseCategory('Server', [
|
||||
'feat1',
|
||||
]),
|
||||
])
|
||||
]
|
||||
|
||||
const result = checkNewTopic(base, head)
|
||||
|
||||
console.log(result.message)
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"strict": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictNullChecks": true,
|
||||
"experimentalDecorators": true,
|
||||
"noImplicitReturns": true,
|
||||
"esModuleInterop": true,
|
||||
"typeRoots": [
|
||||
"./node_modules/@types"
|
||||
],
|
||||
"lib": [
|
||||
"esnext"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"test/**/*"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import {defineConfig} from 'vite';
|
||||
|
||||
|
||||
const config = defineConfig({});
|
||||
|
||||
export default config;
|