Merge branch 'develop' into feat-12909
This commit is contained in:
@@ -21,6 +21,7 @@
|
|||||||
### Client
|
### Client
|
||||||
- Feat: 新しいゲームを追加
|
- Feat: 新しいゲームを追加
|
||||||
- Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように
|
- Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように
|
||||||
|
- Fix: ネイティブモードの絵文字がモノクロにならないように
|
||||||
- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
|
- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
|
||||||
- Enhance: チャンネルノートのピン留めをノートのメニューからできるよ
|
- Enhance: チャンネルノートのピン留めをノートのメニューからできるよ
|
||||||
|
|
||||||
|
9
locales/index.d.ts
vendored
9
locales/index.d.ts
vendored
@@ -1659,6 +1659,15 @@ export interface Locale {
|
|||||||
"title": string;
|
"title": string;
|
||||||
"description": string;
|
"description": string;
|
||||||
};
|
};
|
||||||
|
"_bubbleGameExplodingHead": {
|
||||||
|
"title": string;
|
||||||
|
"description": string;
|
||||||
|
};
|
||||||
|
"_bubbleGameDoubleExplodingHead": {
|
||||||
|
"title": string;
|
||||||
|
"description": string;
|
||||||
|
"flavor": string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
"_role": {
|
"_role": {
|
||||||
|
@@ -1570,6 +1570,13 @@ _achievements:
|
|||||||
_tutorialCompleted:
|
_tutorialCompleted:
|
||||||
title: "Misskey初心者講座 修了証"
|
title: "Misskey初心者講座 修了証"
|
||||||
description: "チュートリアルを完了した"
|
description: "チュートリアルを完了した"
|
||||||
|
_bubbleGameExplodingHead:
|
||||||
|
title: "🤯"
|
||||||
|
description: "バブルゲームで最も大きいモノを出した"
|
||||||
|
_bubbleGameDoubleExplodingHead:
|
||||||
|
title: "ダブル🤯"
|
||||||
|
description: "バブルゲームで最も大きいモノを2つ同時に出した"
|
||||||
|
flavor: "これくらいの おべんとばこに 🤯 🤯 ちょっとつめて"
|
||||||
|
|
||||||
_role:
|
_role:
|
||||||
new: "ロールの作成"
|
new: "ロールの作成"
|
||||||
|
@@ -3,7 +3,6 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { setTimeout } from 'node:timers/promises';
|
|
||||||
import { Global, Inject, Module } from '@nestjs/common';
|
import { Global, Inject, Module } from '@nestjs/common';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
@@ -12,6 +11,7 @@ import { DI } from './di-symbols.js';
|
|||||||
import { Config, loadConfig } from './config.js';
|
import { Config, loadConfig } from './config.js';
|
||||||
import { createPostgresDataSource } from './postgres.js';
|
import { createPostgresDataSource } from './postgres.js';
|
||||||
import { RepositoryModule } from './models/RepositoryModule.js';
|
import { RepositoryModule } from './models/RepositoryModule.js';
|
||||||
|
import { allSettled } from './misc/promise-tracker.js';
|
||||||
import type { Provider, OnApplicationShutdown } from '@nestjs/common';
|
import type { Provider, OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
const $config: Provider = {
|
const $config: Provider = {
|
||||||
@@ -33,7 +33,7 @@ const $meilisearch: Provider = {
|
|||||||
useFactory: (config: Config) => {
|
useFactory: (config: Config) => {
|
||||||
if (config.meilisearch) {
|
if (config.meilisearch) {
|
||||||
return new MeiliSearch({
|
return new MeiliSearch({
|
||||||
host: `${config.meilisearch.ssl ? 'https' : 'http' }://${config.meilisearch.host}:${config.meilisearch.port}`,
|
host: `${config.meilisearch.ssl ? 'https' : 'http'}://${config.meilisearch.host}:${config.meilisearch.port}`,
|
||||||
apiKey: config.meilisearch.apiKey,
|
apiKey: config.meilisearch.apiKey,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -91,17 +91,12 @@ export class GlobalModule implements OnApplicationShutdown {
|
|||||||
@Inject(DI.redisForPub) private redisForPub: Redis.Redis,
|
@Inject(DI.redisForPub) private redisForPub: Redis.Redis,
|
||||||
@Inject(DI.redisForSub) private redisForSub: Redis.Redis,
|
@Inject(DI.redisForSub) private redisForSub: Redis.Redis,
|
||||||
@Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis,
|
@Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
public async dispose(): Promise<void> {
|
public async dispose(): Promise<void> {
|
||||||
if (process.env.NODE_ENV === 'test') {
|
// Wait for all potential DB queries
|
||||||
// XXX:
|
await allSettled();
|
||||||
// Shutting down the existing connections causes errors on Jest as
|
// And then disconnect from DB
|
||||||
// Misskey has asynchronous postgres/redis connections that are not
|
|
||||||
// awaited.
|
|
||||||
// Let's wait for some random time for them to finish.
|
|
||||||
await setTimeout(5000);
|
|
||||||
}
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.db.destroy(),
|
this.db.destroy(),
|
||||||
this.redisClient.disconnect(),
|
this.redisClient.disconnect(),
|
||||||
|
@@ -87,6 +87,8 @@ export const ACHIEVEMENT_TYPES = [
|
|||||||
'brainDiver',
|
'brainDiver',
|
||||||
'smashTestNotificationButton',
|
'smashTestNotificationButton',
|
||||||
'tutorialCompleted',
|
'tutorialCompleted',
|
||||||
|
'bubbleGameExplodingHead',
|
||||||
|
'bubbleGameDoubleExplodingHead',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@@ -58,6 +58,7 @@ import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
|||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||||
import { isReply } from '@/misc/is-reply.js';
|
import { isReply } from '@/misc/is-reply.js';
|
||||||
|
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||||
|
|
||||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||||
|
|
||||||
@@ -676,7 +677,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
this.relayService.deliverToRelays(user, noteActivity);
|
this.relayService.deliverToRelays(user, noteActivity);
|
||||||
}
|
}
|
||||||
|
|
||||||
dm.execute();
|
trackPromise(dm.execute());
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
@@ -14,6 +14,7 @@ import { IdService } from '@/core/IdService.js';
|
|||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import type { NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository } from '@/models/_.js';
|
import type { NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository } from '@/models/_.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NoteReadService implements OnApplicationShutdown {
|
export class NoteReadService implements OnApplicationShutdown {
|
||||||
@@ -107,7 +108,7 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||||||
|
|
||||||
// TODO: ↓まとめてクエリしたい
|
// TODO: ↓まとめてクエリしたい
|
||||||
|
|
||||||
this.noteUnreadsRepository.countBy({
|
trackPromise(this.noteUnreadsRepository.countBy({
|
||||||
userId: userId,
|
userId: userId,
|
||||||
isMentioned: true,
|
isMentioned: true,
|
||||||
}).then(mentionsCount => {
|
}).then(mentionsCount => {
|
||||||
@@ -115,9 +116,9 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||||||
// 全て既読になったイベントを発行
|
// 全て既読になったイベントを発行
|
||||||
this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
|
this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
|
|
||||||
this.noteUnreadsRepository.countBy({
|
trackPromise(this.noteUnreadsRepository.countBy({
|
||||||
userId: userId,
|
userId: userId,
|
||||||
isSpecified: true,
|
isSpecified: true,
|
||||||
}).then(specifiedCount => {
|
}).then(specifiedCount => {
|
||||||
@@ -125,7 +126,7 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||||||
// 全て既読になったイベントを発行
|
// 全て既読になったイベントを発行
|
||||||
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
|
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -20,6 +20,7 @@ import { CacheService } from '@/core/CacheService.js';
|
|||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { UserListService } from '@/core/UserListService.js';
|
import { UserListService } from '@/core/UserListService.js';
|
||||||
import type { FilterUnionByProperty } from '@/types.js';
|
import type { FilterUnionByProperty } from '@/types.js';
|
||||||
|
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NotificationService implements OnApplicationShutdown {
|
export class NotificationService implements OnApplicationShutdown {
|
||||||
@@ -74,7 +75,18 @@ export class NotificationService implements OnApplicationShutdown {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async createNotification<T extends MiNotification['type']>(
|
public createNotification<T extends MiNotification['type']>(
|
||||||
|
notifieeId: MiUser['id'],
|
||||||
|
type: T,
|
||||||
|
data: Omit<FilterUnionByProperty<MiNotification, 'type', T>, 'type' | 'id' | 'createdAt' | 'notifierId'>,
|
||||||
|
notifierId?: MiUser['id'] | null,
|
||||||
|
) {
|
||||||
|
trackPromise(
|
||||||
|
this.#createNotificationInternal(notifieeId, type, data, notifierId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async #createNotificationInternal<T extends MiNotification['type']>(
|
||||||
notifieeId: MiUser['id'],
|
notifieeId: MiUser['id'],
|
||||||
type: T,
|
type: T,
|
||||||
data: Omit<FilterUnionByProperty<MiNotification, 'type', T>, 'type' | 'id' | 'createdAt' | 'notifierId'>,
|
data: Omit<FilterUnionByProperty<MiNotification, 'type', T>, 'type' | 'id' | 'createdAt' | 'notifierId'>,
|
||||||
|
@@ -3,12 +3,12 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { setTimeout } from 'node:timers/promises';
|
|
||||||
import { Inject, Module, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Module, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import * as Bull from 'bullmq';
|
import * as Bull from 'bullmq';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { QUEUE, baseQueueOptions } from '@/queue/const.js';
|
import { QUEUE, baseQueueOptions } from '@/queue/const.js';
|
||||||
|
import { allSettled } from '@/misc/promise-tracker.js';
|
||||||
import type { Provider } from '@nestjs/common';
|
import type { Provider } from '@nestjs/common';
|
||||||
import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js';
|
import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js';
|
||||||
|
|
||||||
@@ -106,14 +106,9 @@ export class QueueModule implements OnApplicationShutdown {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async dispose(): Promise<void> {
|
public async dispose(): Promise<void> {
|
||||||
if (process.env.NODE_ENV === 'test') {
|
// Wait for all potential queue jobs
|
||||||
// XXX:
|
await allSettled();
|
||||||
// Shutting down the existing connections causes errors on Jest as
|
// And then close all queues
|
||||||
// Misskey has asynchronous postgres/redis connections that are not
|
|
||||||
// awaited.
|
|
||||||
// Let's wait for some random time for them to finish.
|
|
||||||
await setTimeout(5000);
|
|
||||||
}
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.systemQueue.close(),
|
this.systemQueue.close(),
|
||||||
this.endedPollNotificationQueue.close(),
|
this.endedPollNotificationQueue.close(),
|
||||||
|
@@ -28,6 +28,7 @@ import { UserBlockingService } from '@/core/UserBlockingService.js';
|
|||||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { FeaturedService } from '@/core/FeaturedService.js';
|
import { FeaturedService } from '@/core/FeaturedService.js';
|
||||||
|
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||||
|
|
||||||
const FALLBACK = '❤';
|
const FALLBACK = '❤';
|
||||||
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
|
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
|
||||||
@@ -268,7 +269,7 @@ export class ReactionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dm.execute();
|
trackPromise(dm.execute());
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
}
|
}
|
||||||
@@ -316,7 +317,7 @@ export class ReactionService {
|
|||||||
dm.addDirectRecipe(reactee as MiRemoteUser);
|
dm.addDirectRecipe(reactee as MiRemoteUser);
|
||||||
}
|
}
|
||||||
dm.addFollowersRecipe();
|
dm.addFollowersRecipe();
|
||||||
dm.execute();
|
trackPromise(dm.execute());
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
}
|
}
|
||||||
|
@@ -144,7 +144,7 @@ class DeliverManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// deliver
|
// deliver
|
||||||
this.queueService.deliverMany(this.actor, this.activity, inboxes);
|
await this.queueService.deliverMany(this.actor, this.activity, inboxes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -37,7 +37,7 @@ export class ServerStatsService implements OnApplicationShutdown {
|
|||||||
const log = [] as any[];
|
const log = [] as any[];
|
||||||
|
|
||||||
ev.on('requestServerStatsLog', x => {
|
ev.on('requestServerStatsLog', x => {
|
||||||
ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length ?? 50));
|
ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length));
|
||||||
});
|
});
|
||||||
|
|
||||||
const tick = async () => {
|
const tick = async () => {
|
||||||
|
23
packages/backend/src/misc/promise-tracker.ts
Normal file
23
packages/backend/src/misc/promise-tracker.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
const promiseRefs: Set<WeakRef<Promise<unknown>>> = new Set();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This tracks promises that other modules decided not to wait for,
|
||||||
|
* and makes sure they are all settled before fully closing down the server.
|
||||||
|
*/
|
||||||
|
export function trackPromise(promise: Promise<unknown>) {
|
||||||
|
if (process.env.NODE_ENV !== 'test') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ref = new WeakRef(promise);
|
||||||
|
promiseRefs.add(ref);
|
||||||
|
promise.finally(() => promiseRefs.delete(ref));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function allSettled(): Promise<void> {
|
||||||
|
await Promise.allSettled([...promiseRefs].map(r => r.deref()));
|
||||||
|
}
|
@@ -14,6 +14,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
|||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
@@ -92,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
|
|
||||||
antenna.isActive = true;
|
antenna.isActive = true;
|
||||||
antenna.lastUsedAt = new Date();
|
antenna.lastUsedAt = new Date();
|
||||||
this.antennasRepository.update(antenna.id, antenna);
|
trackPromise(this.antennasRepository.update(antenna.id, antenna));
|
||||||
|
|
||||||
if (needPublishEvent) {
|
if (needPublishEvent) {
|
||||||
this.globalEventService.publishInternalEvent('antennaUpdated', antenna);
|
this.globalEventService.publishInternalEvent('antennaUpdated', antenna);
|
||||||
|
BIN
packages/frontend/assets/drop-and-fusion/bgm_1.mp3
Normal file
BIN
packages/frontend/assets/drop-and-fusion/bgm_1.mp3
Normal file
Binary file not shown.
BIN
packages/frontend/assets/drop-and-fusion/bubble2.mp3
Normal file
BIN
packages/frontend/assets/drop-and-fusion/bubble2.mp3
Normal file
Binary file not shown.
BIN
packages/frontend/assets/drop-and-fusion/gameover.png
Normal file
BIN
packages/frontend/assets/drop-and-fusion/gameover.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
BIN
packages/frontend/assets/drop-and-fusion/logo.png
Normal file
BIN
packages/frontend/assets/drop-and-fusion/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 248 KiB |
BIN
packages/frontend/assets/drop-and-fusion/poi1.mp3
Normal file
BIN
packages/frontend/assets/drop-and-fusion/poi1.mp3
Normal file
Binary file not shown.
BIN
packages/frontend/assets/drop-and-fusion/poi2.mp3
Normal file
BIN
packages/frontend/assets/drop-and-fusion/poi2.mp3
Normal file
Binary file not shown.
@@ -4,7 +4,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"watch": "vite",
|
"watch": "vite",
|
||||||
"dev": "vite --config vite.config.local-dev.ts",
|
"dev": "vite --config vite.config.local-dev.ts --debug hmr",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"storybook-dev": "nodemon --verbose --watch src --ext \"mdx,ts,vue\" --ignore \"*.stories.ts\" --exec \"pnpm build-storybook-pre && pnpm exec storybook dev -p 6006 --ci\"",
|
"storybook-dev": "nodemon --verbose --watch src --ext \"mdx,ts,vue\" --ignore \"*.stories.ts\" --exec \"pnpm build-storybook-pre && pnpm exec storybook dev -p 6006 --ci\"",
|
||||||
"build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js",
|
"build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js",
|
||||||
|
@@ -22,6 +22,7 @@ import { getAccountFromId } from '@/scripts/get-account-from-id.js';
|
|||||||
import { deckStore } from '@/ui/deck/deck-store.js';
|
import { deckStore } from '@/ui/deck/deck-store.js';
|
||||||
import { miLocalStorage } from '@/local-storage.js';
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
import { fetchCustomEmojis } from '@/custom-emojis.js';
|
import { fetchCustomEmojis } from '@/custom-emojis.js';
|
||||||
|
import { setupRouter } from '@/global/router/definition.js';
|
||||||
|
|
||||||
export async function common(createVue: () => App<Element>) {
|
export async function common(createVue: () => App<Element>) {
|
||||||
console.info(`Misskey v${version}`);
|
console.info(`Misskey v${version}`);
|
||||||
@@ -241,6 +242,8 @@ export async function common(createVue: () => App<Element>) {
|
|||||||
|
|
||||||
const app = createVue();
|
const app = createVue();
|
||||||
|
|
||||||
|
setupRouter(app);
|
||||||
|
|
||||||
if (_DEV_) {
|
if (_DEV_) {
|
||||||
app.config.performance = true;
|
app.config.performance = true;
|
||||||
}
|
}
|
||||||
|
@@ -3,23 +3,23 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createApp, markRaw, defineAsyncComponent } from 'vue';
|
import { createApp, defineAsyncComponent, markRaw } from 'vue';
|
||||||
import { common } from './common.js';
|
import { common } from './common.js';
|
||||||
import { ui } from '@/config.js';
|
import { ui } from '@/config.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { confirm, alert, post, popup, toast } from '@/os.js';
|
import { alert, confirm, popup, post, toast } from '@/os.js';
|
||||||
import { useStream } from '@/stream.js';
|
import { useStream } from '@/stream.js';
|
||||||
import * as sound from '@/scripts/sound.js';
|
import * as sound from '@/scripts/sound.js';
|
||||||
import { $i, updateAccount, signout } from '@/account.js';
|
import { $i, signout, updateAccount } from '@/account.js';
|
||||||
import { defaultStore, ColdDeviceStorage } from '@/store.js';
|
import { ColdDeviceStorage, defaultStore } from '@/store.js';
|
||||||
import { makeHotkey } from '@/scripts/hotkey.js';
|
import { makeHotkey } from '@/scripts/hotkey.js';
|
||||||
import { reactionPicker } from '@/scripts/reaction-picker.js';
|
import { reactionPicker } from '@/scripts/reaction-picker.js';
|
||||||
import { miLocalStorage } from '@/local-storage.js';
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js';
|
import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js';
|
||||||
import { mainRouter } from '@/router.js';
|
|
||||||
import { initializeSw } from '@/scripts/initialize-sw.js';
|
import { initializeSw } from '@/scripts/initialize-sw.js';
|
||||||
import { deckStore } from '@/ui/deck/deck-store.js';
|
import { deckStore } from '@/ui/deck/deck-store.js';
|
||||||
import { emojiPicker } from '@/scripts/emoji-picker.js';
|
import { emojiPicker } from '@/scripts/emoji-picker.js';
|
||||||
|
import { mainRouter } from '@/global/router/main.js';
|
||||||
|
|
||||||
export async function mainBoot() {
|
export async function mainBoot() {
|
||||||
const { isClientUpdated } = await common(() => createApp(
|
const { isClientUpdated } = await common(() => createApp(
|
||||||
|
@@ -45,9 +45,9 @@ import bytes from '@/filters/bytes.js';
|
|||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import { useRouter } from '@/router.js';
|
|
||||||
import { getDriveFileMenu } from '@/scripts/get-drive-file-menu.js';
|
import { getDriveFileMenu } from '@/scripts/get-drive-file-menu.js';
|
||||||
import { deviceKind } from '@/scripts/device-kind.js';
|
import { deviceKind } from '@/scripts/device-kind.js';
|
||||||
|
import { useRouter } from '@/global/router/supplier.js';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@@ -23,26 +23,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div ref="contents" :class="$style.root" style="container-type: inline-size;">
|
<div ref="contents" :class="$style.root" style="container-type: inline-size;">
|
||||||
<RouterView :key="reloadCount" :router="router"/>
|
<RouterView :key="reloadCount" :router="windowRouter"/>
|
||||||
</div>
|
</div>
|
||||||
</MkWindow>
|
</MkWindow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ComputedRef, onMounted, onUnmounted, provide, shallowRef, ref, computed } from 'vue';
|
import { computed, ComputedRef, onMounted, onUnmounted, provide, ref, shallowRef } from 'vue';
|
||||||
import RouterView from '@/components/global/RouterView.vue';
|
import RouterView from '@/components/global/RouterView.vue';
|
||||||
import MkWindow from '@/components/MkWindow.vue';
|
import MkWindow from '@/components/MkWindow.vue';
|
||||||
import { popout as _popout } from '@/scripts/popout.js';
|
import { popout as _popout } from '@/scripts/popout.js';
|
||||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||||
import { url } from '@/config.js';
|
import { url } from '@/config.js';
|
||||||
import { mainRouter, routes, page } from '@/router.js';
|
import { useScrollPositionManager } from '@/nirax.js';
|
||||||
import { $i } from '@/account.js';
|
|
||||||
import { Router, useScrollPositionManager } from '@/nirax.js';
|
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
|
import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
|
||||||
import { openingWindowsCount } from '@/os.js';
|
import { openingWindowsCount } from '@/os.js';
|
||||||
import { claimAchievement } from '@/scripts/achievements.js';
|
import { claimAchievement } from '@/scripts/achievements.js';
|
||||||
import { getScrollContainer } from '@/scripts/scroll.js';
|
import { getScrollContainer } from '@/scripts/scroll.js';
|
||||||
|
import { useRouterFactory } from '@/global/router/supplier.js';
|
||||||
|
import { mainRouter } from '@/global/router/main.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
initialPath: string;
|
initialPath: string;
|
||||||
@@ -52,14 +52,15 @@ defineEmits<{
|
|||||||
(ev: 'closed'): void;
|
(ev: 'closed'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const router = new Router(routes, props.initialPath, !!$i, page(() => import('@/pages/not-found.vue')));
|
const routerFactory = useRouterFactory();
|
||||||
|
const windowRouter = routerFactory(props.initialPath);
|
||||||
|
|
||||||
const contents = shallowRef<HTMLElement>();
|
const contents = shallowRef<HTMLElement>();
|
||||||
const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
|
const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
|
||||||
const windowEl = shallowRef<InstanceType<typeof MkWindow>>();
|
const windowEl = shallowRef<InstanceType<typeof MkWindow>>();
|
||||||
const history = ref<{ path: string; key: any; }[]>([{
|
const history = ref<{ path: string; key: any; }[]>([{
|
||||||
path: router.getCurrentPath(),
|
path: windowRouter.getCurrentPath(),
|
||||||
key: router.getCurrentKey(),
|
key: windowRouter.getCurrentKey(),
|
||||||
}]);
|
}]);
|
||||||
const buttonsLeft = computed(() => {
|
const buttonsLeft = computed(() => {
|
||||||
const buttons = [];
|
const buttons = [];
|
||||||
@@ -88,11 +89,11 @@ const buttonsRight = computed(() => {
|
|||||||
});
|
});
|
||||||
const reloadCount = ref(0);
|
const reloadCount = ref(0);
|
||||||
|
|
||||||
router.addListener('push', ctx => {
|
windowRouter.addListener('push', ctx => {
|
||||||
history.value.push({ path: ctx.path, key: ctx.key });
|
history.value.push({ path: ctx.path, key: ctx.key });
|
||||||
});
|
});
|
||||||
|
|
||||||
provide('router', router);
|
provide('router', windowRouter);
|
||||||
provideMetadataReceiver((info) => {
|
provideMetadataReceiver((info) => {
|
||||||
pageMetadata.value = info;
|
pageMetadata.value = info;
|
||||||
});
|
});
|
||||||
@@ -112,20 +113,20 @@ const contextmenu = computed(() => ([{
|
|||||||
icon: 'ti ti-external-link',
|
icon: 'ti ti-external-link',
|
||||||
text: i18n.ts.openInNewTab,
|
text: i18n.ts.openInNewTab,
|
||||||
action: () => {
|
action: () => {
|
||||||
window.open(url + router.getCurrentPath(), '_blank', 'noopener');
|
window.open(url + windowRouter.getCurrentPath(), '_blank', 'noopener');
|
||||||
windowEl.value.close();
|
windowEl.value.close();
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
icon: 'ti ti-link',
|
icon: 'ti ti-link',
|
||||||
text: i18n.ts.copyLink,
|
text: i18n.ts.copyLink,
|
||||||
action: () => {
|
action: () => {
|
||||||
copyToClipboard(url + router.getCurrentPath());
|
copyToClipboard(url + windowRouter.getCurrentPath());
|
||||||
},
|
},
|
||||||
}]));
|
}]));
|
||||||
|
|
||||||
function back() {
|
function back() {
|
||||||
history.value.pop();
|
history.value.pop();
|
||||||
router.replace(history.value.at(-1)!.path, history.value.at(-1)!.key);
|
windowRouter.replace(history.value.at(-1)!.path, history.value.at(-1)!.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
function reload() {
|
function reload() {
|
||||||
@@ -137,16 +138,16 @@ function close() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function expand() {
|
function expand() {
|
||||||
mainRouter.push(router.getCurrentPath(), 'forcePage');
|
mainRouter.push(windowRouter.getCurrentPath(), 'forcePage');
|
||||||
windowEl.value.close();
|
windowEl.value.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
function popout() {
|
function popout() {
|
||||||
_popout(router.getCurrentPath(), windowEl.value.$el);
|
_popout(windowRouter.getCurrentPath(), windowEl.value.$el);
|
||||||
windowEl.value.close();
|
windowEl.value.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
useScrollPositionManager(() => getScrollContainer(contents.value), router);
|
useScrollPositionManager(() => getScrollContainer(contents.value), windowRouter);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
openingWindowsCount.value++;
|
openingWindowsCount.value++;
|
||||||
|
@@ -42,6 +42,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.root {
|
.root {
|
||||||
|
user-select: none;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
width: 128px;
|
width: 128px;
|
||||||
|
@@ -15,7 +15,7 @@ import * as os from '@/os.js';
|
|||||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||||
import { url } from '@/config.js';
|
import { url } from '@/config.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { useRouter } from '@/router.js';
|
import { useRouter } from '@/global/router/supplier.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
to: string;
|
to: string;
|
||||||
|
@@ -5,15 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<img v-if="!useOsNativeEmojis" :class="$style.root" :src="url" :alt="props.emoji" decoding="async" @pointerenter="computeTitle" @click="onClick"/>
|
<img v-if="!useOsNativeEmojis" :class="$style.root" :src="url" :alt="props.emoji" decoding="async" @pointerenter="computeTitle" @click="onClick"/>
|
||||||
<span v-else-if="useOsNativeEmojis" :alt="props.emoji" @pointerenter="computeTitle" @click="onClick">{{ props.emoji }}</span>
|
<span v-else :alt="props.emoji" @pointerenter="computeTitle" @click="onClick">{{ colorizedNativeEmoji }}</span>
|
||||||
<span v-else>{{ emoji }}</span>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject } from 'vue';
|
import { computed, inject } from 'vue';
|
||||||
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base.js';
|
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { getEmojiName } from '@/scripts/emojilist.js';
|
import { colorizeEmoji, getEmojiName } from '@/scripts/emojilist.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||||
import * as sound from '@/scripts/sound.js';
|
import * as sound from '@/scripts/sound.js';
|
||||||
@@ -30,9 +29,8 @@ const react = inject<((name: string) => void) | null>('react', null);
|
|||||||
const char2path = defaultStore.state.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;
|
const char2path = defaultStore.state.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;
|
||||||
|
|
||||||
const useOsNativeEmojis = computed(() => defaultStore.state.emojiStyle === 'native');
|
const useOsNativeEmojis = computed(() => defaultStore.state.emojiStyle === 'native');
|
||||||
const url = computed(() => {
|
const url = computed(() => char2path(props.emoji));
|
||||||
return char2path(props.emoji);
|
const colorizedNativeEmoji = computed(() => colorizeEmoji(props.emoji));
|
||||||
});
|
|
||||||
|
|
||||||
// Searching from an array with 2000 items for every emoji felt like too energy-consuming, so I decided to do it lazily on pointerenter
|
// Searching from an array with 2000 items for every emoji felt like too energy-consuming, so I decided to do it lazily on pointerenter
|
||||||
function computeTitle(event: PointerEvent): void {
|
function computeTitle(event: PointerEvent): void {
|
||||||
|
@@ -16,12 +16,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { inject, onBeforeUnmount, provide, shallowRef, ref } from 'vue';
|
import { inject, onBeforeUnmount, provide, ref, shallowRef } from 'vue';
|
||||||
import { Resolved, Router } from '@/nirax.js';
|
import { IRouter, Resolved } from '@/nirax.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
router?: Router;
|
router?: IRouter;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const router = props.router ?? inject('router');
|
const router = props.router ?? inject('router');
|
||||||
|
571
packages/frontend/src/global/router/definition.ts
Normal file
571
packages/frontend/src/global/router/definition.ts
Normal file
@@ -0,0 +1,571 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { App, AsyncComponentLoader, defineAsyncComponent, provide } from 'vue';
|
||||||
|
import { IRouter, Router } from '@/nirax.js';
|
||||||
|
import { $i, iAmModerator } from '@/account.js';
|
||||||
|
import MkLoading from '@/pages/_loading_.vue';
|
||||||
|
import MkError from '@/pages/_error_.vue';
|
||||||
|
import { setMainRouter } from '@/global/router/main.js';
|
||||||
|
|
||||||
|
const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({
|
||||||
|
loader: loader,
|
||||||
|
loadingComponent: MkLoading,
|
||||||
|
errorComponent: MkError,
|
||||||
|
});
|
||||||
|
const routes = [{
|
||||||
|
path: '/@:initUser/pages/:initPageName/view-source',
|
||||||
|
component: page(() => import('@/pages/page-editor/page-editor.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/@:username/pages/:pageName',
|
||||||
|
component: page(() => import('@/pages/page.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/@:acct/following',
|
||||||
|
component: page(() => import('@/pages/user/following.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/@:acct/followers',
|
||||||
|
component: page(() => import('@/pages/user/followers.vue')),
|
||||||
|
}, {
|
||||||
|
name: 'user',
|
||||||
|
path: '/@:acct/:page?',
|
||||||
|
component: page(() => import('@/pages/user/index.vue')),
|
||||||
|
}, {
|
||||||
|
name: 'note',
|
||||||
|
path: '/notes/:noteId',
|
||||||
|
component: page(() => import('@/pages/note.vue')),
|
||||||
|
}, {
|
||||||
|
name: 'list',
|
||||||
|
path: '/list/:listId',
|
||||||
|
component: page(() => import('@/pages/list.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/clips/:clipId',
|
||||||
|
component: page(() => import('@/pages/clip.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/instance-info/:host',
|
||||||
|
component: page(() => import('@/pages/instance-info.vue')),
|
||||||
|
}, {
|
||||||
|
name: 'settings',
|
||||||
|
path: '/settings',
|
||||||
|
component: page(() => import('@/pages/settings/index.vue')),
|
||||||
|
loginRequired: true,
|
||||||
|
children: [{
|
||||||
|
path: '/profile',
|
||||||
|
name: 'profile',
|
||||||
|
component: page(() => import('@/pages/settings/profile.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/avatar-decoration',
|
||||||
|
name: 'avatarDecoration',
|
||||||
|
component: page(() => import('@/pages/settings/avatar-decoration.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/roles',
|
||||||
|
name: 'roles',
|
||||||
|
component: page(() => import('@/pages/settings/roles.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/privacy',
|
||||||
|
name: 'privacy',
|
||||||
|
component: page(() => import('@/pages/settings/privacy.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/emoji-picker',
|
||||||
|
name: 'emojiPicker',
|
||||||
|
component: page(() => import('@/pages/settings/emoji-picker.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/drive',
|
||||||
|
name: 'drive',
|
||||||
|
component: page(() => import('@/pages/settings/drive.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/drive/cleaner',
|
||||||
|
name: 'drive',
|
||||||
|
component: page(() => import('@/pages/settings/drive-cleaner.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/notifications',
|
||||||
|
name: 'notifications',
|
||||||
|
component: page(() => import('@/pages/settings/notifications.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/email',
|
||||||
|
name: 'email',
|
||||||
|
component: page(() => import('@/pages/settings/email.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/security',
|
||||||
|
name: 'security',
|
||||||
|
component: page(() => import('@/pages/settings/security.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/general',
|
||||||
|
name: 'general',
|
||||||
|
component: page(() => import('@/pages/settings/general.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/theme/install',
|
||||||
|
name: 'theme',
|
||||||
|
component: page(() => import('@/pages/settings/theme.install.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/theme/manage',
|
||||||
|
name: 'theme',
|
||||||
|
component: page(() => import('@/pages/settings/theme.manage.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/theme',
|
||||||
|
name: 'theme',
|
||||||
|
component: page(() => import('@/pages/settings/theme.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/navbar',
|
||||||
|
name: 'navbar',
|
||||||
|
component: page(() => import('@/pages/settings/navbar.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/statusbar',
|
||||||
|
name: 'statusbar',
|
||||||
|
component: page(() => import('@/pages/settings/statusbar.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/sounds',
|
||||||
|
name: 'sounds',
|
||||||
|
component: page(() => import('@/pages/settings/sounds.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/plugin/install',
|
||||||
|
name: 'plugin',
|
||||||
|
component: page(() => import('@/pages/settings/plugin.install.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/plugin',
|
||||||
|
name: 'plugin',
|
||||||
|
component: page(() => import('@/pages/settings/plugin.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/import-export',
|
||||||
|
name: 'import-export',
|
||||||
|
component: page(() => import('@/pages/settings/import-export.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/mute-block',
|
||||||
|
name: 'mute-block',
|
||||||
|
component: page(() => import('@/pages/settings/mute-block.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/api',
|
||||||
|
name: 'api',
|
||||||
|
component: page(() => import('@/pages/settings/api.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/apps',
|
||||||
|
name: 'api',
|
||||||
|
component: page(() => import('@/pages/settings/apps.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/webhook/edit/:webhookId',
|
||||||
|
name: 'webhook',
|
||||||
|
component: page(() => import('@/pages/settings/webhook.edit.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/webhook/new',
|
||||||
|
name: 'webhook',
|
||||||
|
component: page(() => import('@/pages/settings/webhook.new.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/webhook',
|
||||||
|
name: 'webhook',
|
||||||
|
component: page(() => import('@/pages/settings/webhook.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/deck',
|
||||||
|
name: 'deck',
|
||||||
|
component: page(() => import('@/pages/settings/deck.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/preferences-backups',
|
||||||
|
name: 'preferences-backups',
|
||||||
|
component: page(() => import('@/pages/settings/preferences-backups.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/migration',
|
||||||
|
name: 'migration',
|
||||||
|
component: page(() => import('@/pages/settings/migration.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/custom-css',
|
||||||
|
name: 'general',
|
||||||
|
component: page(() => import('@/pages/settings/custom-css.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/accounts',
|
||||||
|
name: 'profile',
|
||||||
|
component: page(() => import('@/pages/settings/accounts.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/other',
|
||||||
|
name: 'other',
|
||||||
|
component: page(() => import('@/pages/settings/other.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/',
|
||||||
|
component: page(() => import('@/pages/_empty_.vue')),
|
||||||
|
}],
|
||||||
|
}, {
|
||||||
|
path: '/reset-password/:token?',
|
||||||
|
component: page(() => import('@/pages/reset-password.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/signup-complete/:code',
|
||||||
|
component: page(() => import('@/pages/signup-complete.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/announcements',
|
||||||
|
component: page(() => import('@/pages/announcements.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/about',
|
||||||
|
component: page(() => import('@/pages/about.vue')),
|
||||||
|
hash: 'initialTab',
|
||||||
|
}, {
|
||||||
|
path: '/about-misskey',
|
||||||
|
component: page(() => import('@/pages/about-misskey.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/invite',
|
||||||
|
name: 'invite',
|
||||||
|
component: page(() => import('@/pages/invite.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/ads',
|
||||||
|
component: page(() => import('@/pages/ads.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/theme-editor',
|
||||||
|
component: page(() => import('@/pages/theme-editor.vue')),
|
||||||
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/roles/:role',
|
||||||
|
component: page(() => import('@/pages/role.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/user-tags/:tag',
|
||||||
|
component: page(() => import('@/pages/user-tag.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/explore',
|
||||||
|
component: page(() => import('@/pages/explore.vue')),
|
||||||
|
hash: 'initialTab',
|
||||||
|
}, {
|
||||||
|
path: '/search',
|
||||||
|
component: page(() => import('@/pages/search.vue')),
|
||||||
|
query: {
|
||||||
|
q: 'query',
|
||||||
|
channel: 'channel',
|
||||||
|
type: 'type',
|
||||||
|
origin: 'origin',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
path: '/authorize-follow',
|
||||||
|
component: page(() => import('@/pages/follow.vue')),
|
||||||
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/share',
|
||||||
|
component: page(() => import('@/pages/share.vue')),
|
||||||
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/api-console',
|
||||||
|
component: page(() => import('@/pages/api-console.vue')),
|
||||||
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/scratchpad',
|
||||||
|
component: page(() => import('@/pages/scratchpad.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/auth/:token',
|
||||||
|
component: page(() => import('@/pages/auth.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/miauth/:session',
|
||||||
|
component: page(() => import('@/pages/miauth.vue')),
|
||||||
|
query: {
|
||||||
|
callback: 'callback',
|
||||||
|
name: 'name',
|
||||||
|
icon: 'icon',
|
||||||
|
permission: 'permission',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
path: '/oauth/authorize',
|
||||||
|
component: page(() => import('@/pages/oauth.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/tags/:tag',
|
||||||
|
component: page(() => import('@/pages/tag.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/pages/new',
|
||||||
|
component: page(() => import('@/pages/page-editor/page-editor.vue')),
|
||||||
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/pages/edit/:initPageId',
|
||||||
|
component: page(() => import('@/pages/page-editor/page-editor.vue')),
|
||||||
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/pages',
|
||||||
|
component: page(() => import('@/pages/pages.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/play/:id/edit',
|
||||||
|
component: page(() => import('@/pages/flash/flash-edit.vue')),
|
||||||
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/play/new',
|
||||||
|
component: page(() => import('@/pages/flash/flash-edit.vue')),
|
||||||
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/play/:id',
|
||||||
|
component: page(() => import('@/pages/flash/flash.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/play',
|
||||||
|
component: page(() => import('@/pages/flash/flash-index.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/gallery/:postId/edit',
|
||||||
|
component: page(() => import('@/pages/gallery/edit.vue')),
|
||||||
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/gallery/new',
|
||||||
|
component: page(() => import('@/pages/gallery/edit.vue')),
|
||||||
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/gallery/:postId',
|
||||||
|
component: page(() => import('@/pages/gallery/post.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/gallery',
|
||||||
|
component: page(() => import('@/pages/gallery/index.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/channels/:channelId/edit',
|
||||||
|
component: page(() => import('@/pages/channel-editor.vue')),
|
||||||
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/channels/new',
|
||||||
|
component: page(() => import('@/pages/channel-editor.vue')),
|
||||||
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/channels/:channelId',
|
||||||
|
component: page(() => import('@/pages/channel.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/channels',
|
||||||
|
component: page(() => import('@/pages/channels.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/custom-emojis-manager',
|
||||||
|
component: page(() => import('@/pages/custom-emojis-manager.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/avatar-decorations',
|
||||||
|
name: 'avatarDecorations',
|
||||||
|
component: page(() => import('@/pages/avatar-decorations.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/registry/keys/:domain/:path(*)?',
|
||||||
|
component: page(() => import('@/pages/registry.keys.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/registry/value/:domain/:path(*)?',
|
||||||
|
component: page(() => import('@/pages/registry.value.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/registry',
|
||||||
|
component: page(() => import('@/pages/registry.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/install-extentions',
|
||||||
|
component: page(() => import('@/pages/install-extentions.vue')),
|
||||||
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/admin/user/:userId',
|
||||||
|
component: iAmModerator ? page(() => import('@/pages/admin-user.vue')) : page(() => import('@/pages/not-found.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/admin/file/:fileId',
|
||||||
|
component: iAmModerator ? page(() => import('@/pages/admin-file.vue')) : page(() => import('@/pages/not-found.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/admin',
|
||||||
|
component: iAmModerator ? page(() => import('@/pages/admin/index.vue')) : page(() => import('@/pages/not-found.vue')),
|
||||||
|
children: [{
|
||||||
|
path: '/overview',
|
||||||
|
name: 'overview',
|
||||||
|
component: page(() => import('@/pages/admin/overview.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/users',
|
||||||
|
name: 'users',
|
||||||
|
component: page(() => import('@/pages/admin/users.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/emojis',
|
||||||
|
name: 'emojis',
|
||||||
|
component: page(() => import('@/pages/custom-emojis-manager.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/avatar-decorations',
|
||||||
|
name: 'avatarDecorations',
|
||||||
|
component: page(() => import('@/pages/avatar-decorations.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/queue',
|
||||||
|
name: 'queue',
|
||||||
|
component: page(() => import('@/pages/admin/queue.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/files',
|
||||||
|
name: 'files',
|
||||||
|
component: page(() => import('@/pages/admin/files.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/federation',
|
||||||
|
name: 'federation',
|
||||||
|
component: page(() => import('@/pages/admin/federation.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/announcements',
|
||||||
|
name: 'announcements',
|
||||||
|
component: page(() => import('@/pages/admin/announcements.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/ads',
|
||||||
|
name: 'ads',
|
||||||
|
component: page(() => import('@/pages/admin/ads.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/roles/:id/edit',
|
||||||
|
name: 'roles',
|
||||||
|
component: page(() => import('@/pages/admin/roles.edit.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/roles/new',
|
||||||
|
name: 'roles',
|
||||||
|
component: page(() => import('@/pages/admin/roles.edit.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/roles/:id',
|
||||||
|
name: 'roles',
|
||||||
|
component: page(() => import('@/pages/admin/roles.role.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/roles',
|
||||||
|
name: 'roles',
|
||||||
|
component: page(() => import('@/pages/admin/roles.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/database',
|
||||||
|
name: 'database',
|
||||||
|
component: page(() => import('@/pages/admin/database.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/abuses',
|
||||||
|
name: 'abuses',
|
||||||
|
component: page(() => import('@/pages/admin/abuses.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/modlog',
|
||||||
|
name: 'modlog',
|
||||||
|
component: page(() => import('@/pages/admin/modlog.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/settings',
|
||||||
|
name: 'settings',
|
||||||
|
component: page(() => import('@/pages/admin/settings.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/branding',
|
||||||
|
name: 'branding',
|
||||||
|
component: page(() => import('@/pages/admin/branding.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/moderation',
|
||||||
|
name: 'moderation',
|
||||||
|
component: page(() => import('@/pages/admin/moderation.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/email-settings',
|
||||||
|
name: 'email-settings',
|
||||||
|
component: page(() => import('@/pages/admin/email-settings.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/object-storage',
|
||||||
|
name: 'object-storage',
|
||||||
|
component: page(() => import('@/pages/admin/object-storage.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/security',
|
||||||
|
name: 'security',
|
||||||
|
component: page(() => import('@/pages/admin/security.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/relays',
|
||||||
|
name: 'relays',
|
||||||
|
component: page(() => import('@/pages/admin/relays.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/instance-block',
|
||||||
|
name: 'instance-block',
|
||||||
|
component: page(() => import('@/pages/admin/instance-block.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/proxy-account',
|
||||||
|
name: 'proxy-account',
|
||||||
|
component: page(() => import('@/pages/admin/proxy-account.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/external-services',
|
||||||
|
name: 'external-services',
|
||||||
|
component: page(() => import('@/pages/admin/external-services.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/other-settings',
|
||||||
|
name: 'other-settings',
|
||||||
|
component: page(() => import('@/pages/admin/other-settings.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/server-rules',
|
||||||
|
name: 'server-rules',
|
||||||
|
component: page(() => import('@/pages/admin/server-rules.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/invites',
|
||||||
|
name: 'invites',
|
||||||
|
component: page(() => import('@/pages/admin/invites.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/',
|
||||||
|
component: page(() => import('@/pages/_empty_.vue')),
|
||||||
|
}],
|
||||||
|
}, {
|
||||||
|
path: '/my/notifications',
|
||||||
|
component: page(() => import('@/pages/notifications.vue')),
|
||||||
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/my/favorites',
|
||||||
|
component: page(() => import('@/pages/favorites.vue')),
|
||||||
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/my/achievements',
|
||||||
|
component: page(() => import('@/pages/achievements.vue')),
|
||||||
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/my/drive/folder/:folder',
|
||||||
|
component: page(() => import('@/pages/drive.vue')),
|
||||||
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/my/drive',
|
||||||
|
component: page(() => import('@/pages/drive.vue')),
|
||||||
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/my/drive/file/:fileId',
|
||||||
|
component: page(() => import('@/pages/drive.file.vue')),
|
||||||
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/my/follow-requests',
|
||||||
|
component: page(() => import('@/pages/follow-requests.vue')),
|
||||||
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/my/lists/:listId',
|
||||||
|
component: page(() => import('@/pages/my-lists/list.vue')),
|
||||||
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/my/lists',
|
||||||
|
component: page(() => import('@/pages/my-lists/index.vue')),
|
||||||
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/my/clips',
|
||||||
|
component: page(() => import('@/pages/my-clips/index.vue')),
|
||||||
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/my/antennas/create',
|
||||||
|
component: page(() => import('@/pages/my-antennas/create.vue')),
|
||||||
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/my/antennas/:antennaId',
|
||||||
|
component: page(() => import('@/pages/my-antennas/edit.vue')),
|
||||||
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/my/antennas',
|
||||||
|
component: page(() => import('@/pages/my-antennas/index.vue')),
|
||||||
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/timeline/list/:listId',
|
||||||
|
component: page(() => import('@/pages/user-list-timeline.vue')),
|
||||||
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/timeline/antenna/:antennaId',
|
||||||
|
component: page(() => import('@/pages/antenna-timeline.vue')),
|
||||||
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/clicker',
|
||||||
|
component: page(() => import('@/pages/clicker.vue')),
|
||||||
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/bubble-game',
|
||||||
|
component: page(() => import('@/pages/drop-and-fusion.vue')),
|
||||||
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/timeline',
|
||||||
|
component: page(() => import('@/pages/timeline.vue')),
|
||||||
|
}, {
|
||||||
|
name: 'index',
|
||||||
|
path: '/',
|
||||||
|
component: $i ? page(() => import('@/pages/timeline.vue')) : page(() => import('@/pages/welcome.vue')),
|
||||||
|
globalCacheKey: 'index',
|
||||||
|
}, {
|
||||||
|
path: '/:(*)',
|
||||||
|
component: page(() => import('@/pages/not-found.vue')),
|
||||||
|
}];
|
||||||
|
|
||||||
|
function createRouterImpl(path: string): IRouter {
|
||||||
|
return new Router(routes, path, !!$i, page(() => import('@/pages/not-found.vue')));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Router}による画面遷移を可能とするために{@link mainRouter}をセットアップする。
|
||||||
|
* また、{@link Router}のインスタンスを作成するためのファクトリも{@link provide}経由で公開する(`routerFactory`というキーで取得可能)
|
||||||
|
*/
|
||||||
|
export function setupRouter(app: App) {
|
||||||
|
app.provide('routerFactory', createRouterImpl);
|
||||||
|
|
||||||
|
const mainRouter = createRouterImpl(location.pathname + location.search + location.hash);
|
||||||
|
|
||||||
|
window.history.replaceState({ key: mainRouter.getCurrentKey() }, '', location.href);
|
||||||
|
|
||||||
|
window.addEventListener('popstate', (event) => {
|
||||||
|
mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key);
|
||||||
|
});
|
||||||
|
|
||||||
|
mainRouter.addListener('push', ctx => {
|
||||||
|
window.history.pushState({ key: ctx.key }, '', ctx.path);
|
||||||
|
});
|
||||||
|
|
||||||
|
setMainRouter(mainRouter);
|
||||||
|
}
|
163
packages/frontend/src/global/router/main.ts
Normal file
163
packages/frontend/src/global/router/main.ts
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ShallowRef } from 'vue';
|
||||||
|
import { EventEmitter } from 'eventemitter3';
|
||||||
|
import { IRouter, Resolved, RouteDef, RouterEvent } from '@/nirax.js';
|
||||||
|
|
||||||
|
function getMainRouter(): IRouter {
|
||||||
|
const router = mainRouterHolder;
|
||||||
|
if (!router) {
|
||||||
|
throw new Error('mainRouter is not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* メインルータを設定する。一度設定すると、それ以降は変更できない。
|
||||||
|
* {@link setupRouter}から呼び出されることのみを想定している。
|
||||||
|
*/
|
||||||
|
export function setMainRouter(router: IRouter) {
|
||||||
|
if (mainRouterHolder) {
|
||||||
|
throw new Error('mainRouter is already exists.');
|
||||||
|
}
|
||||||
|
|
||||||
|
mainRouterHolder = router;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link mainRouter}用のプロキシ実装。
|
||||||
|
* {@link mainRouter}は起動シーケンスの一部にて初期化されるため、僅かにundefinedになる期間がある。
|
||||||
|
* その僅かな期間のためだけに型をundefined込みにしたくないのでこのクラスを緩衝材として使用する。
|
||||||
|
*/
|
||||||
|
class MainRouterProxy implements IRouter {
|
||||||
|
private supplier: () => IRouter;
|
||||||
|
|
||||||
|
constructor(supplier: () => IRouter) {
|
||||||
|
this.supplier = supplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
get current(): Resolved {
|
||||||
|
return this.supplier().current;
|
||||||
|
}
|
||||||
|
|
||||||
|
get currentRef(): ShallowRef<Resolved> {
|
||||||
|
return this.supplier().currentRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
get currentRoute(): ShallowRef<RouteDef> {
|
||||||
|
return this.supplier().currentRoute;
|
||||||
|
}
|
||||||
|
|
||||||
|
get navHook(): ((path: string, flag?: any) => boolean) | null {
|
||||||
|
return this.supplier().navHook;
|
||||||
|
}
|
||||||
|
|
||||||
|
set navHook(value) {
|
||||||
|
this.supplier().navHook = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentKey(): string {
|
||||||
|
return this.supplier().getCurrentKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentPath(): any {
|
||||||
|
return this.supplier().getCurrentPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
push(path: string, flag?: any): void {
|
||||||
|
this.supplier().push(path, flag);
|
||||||
|
}
|
||||||
|
|
||||||
|
replace(path: string, key?: string | null): void {
|
||||||
|
this.supplier().replace(path, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(path: string): Resolved | null {
|
||||||
|
return this.supplier().resolve(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
eventNames(): Array<EventEmitter.EventNames<RouterEvent>> {
|
||||||
|
return this.supplier().eventNames();
|
||||||
|
}
|
||||||
|
|
||||||
|
listeners<T extends EventEmitter.EventNames<RouterEvent>>(
|
||||||
|
event: T,
|
||||||
|
): Array<EventEmitter.EventListener<RouterEvent, T>> {
|
||||||
|
return this.supplier().listeners(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
listenerCount(
|
||||||
|
event: EventEmitter.EventNames<RouterEvent>,
|
||||||
|
): number {
|
||||||
|
return this.supplier().listenerCount(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit<T extends EventEmitter.EventNames<RouterEvent>>(
|
||||||
|
event: T,
|
||||||
|
...args: EventEmitter.EventArgs<RouterEvent, T>
|
||||||
|
): boolean {
|
||||||
|
return this.supplier().emit(event, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
on<T extends EventEmitter.EventNames<RouterEvent>>(
|
||||||
|
event: T,
|
||||||
|
fn: EventEmitter.EventListener<RouterEvent, T>,
|
||||||
|
context?: any,
|
||||||
|
): this {
|
||||||
|
this.supplier().on(event, fn, context);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
addListener<T extends EventEmitter.EventNames<RouterEvent>>(
|
||||||
|
event: T,
|
||||||
|
fn: EventEmitter.EventListener<RouterEvent, T>,
|
||||||
|
context?: any,
|
||||||
|
): this {
|
||||||
|
this.supplier().addListener(event, fn, context);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
once<T extends EventEmitter.EventNames<RouterEvent>>(
|
||||||
|
event: T,
|
||||||
|
fn: EventEmitter.EventListener<RouterEvent, T>,
|
||||||
|
context?: any,
|
||||||
|
): this {
|
||||||
|
this.supplier().once(event, fn, context);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeListener<T extends EventEmitter.EventNames<RouterEvent>>(
|
||||||
|
event: T,
|
||||||
|
fn?: EventEmitter.EventListener<RouterEvent, T>,
|
||||||
|
context?: any,
|
||||||
|
once?: boolean,
|
||||||
|
): this {
|
||||||
|
this.supplier().removeListener(event, fn, context, once);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
off<T extends EventEmitter.EventNames<RouterEvent>>(
|
||||||
|
event: T,
|
||||||
|
fn?: EventEmitter.EventListener<RouterEvent, T>,
|
||||||
|
context?: any,
|
||||||
|
once?: boolean,
|
||||||
|
): this {
|
||||||
|
this.supplier().off(event, fn, context, once);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAllListeners(
|
||||||
|
event?: EventEmitter.EventNames<RouterEvent>,
|
||||||
|
): this {
|
||||||
|
this.supplier().removeAllListeners(event);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mainRouterHolder: IRouter | null = null;
|
||||||
|
|
||||||
|
export const mainRouter: IRouter = new MainRouterProxy(getMainRouter);
|
30
packages/frontend/src/global/router/supplier.ts
Normal file
30
packages/frontend/src/global/router/supplier.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { inject } from 'vue';
|
||||||
|
import { IRouter, Router } from '@/nirax.js';
|
||||||
|
import { mainRouter } from '@/global/router/main.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* メインの{@link Router}を取得する。
|
||||||
|
* あらかじめ{@link setupRouter}を実行しておく必要がある({@link provide}により{@link IRouter}のインスタンスを注入可能であるならばこの限りではない)
|
||||||
|
*/
|
||||||
|
export function useRouter(): IRouter {
|
||||||
|
return inject<Router | null>('router', null) ?? mainRouter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任意の{@link Router}を取得するためのファクトリを取得する。
|
||||||
|
* あらかじめ{@link setupRouter}を実行しておく必要がある。
|
||||||
|
*/
|
||||||
|
export function useRouterFactory(): (path: string) => IRouter {
|
||||||
|
const factory = inject<(path: string) => IRouter>('routerFactory');
|
||||||
|
if (!factory) {
|
||||||
|
console.error('routerFactory is not defined.');
|
||||||
|
throw new Error('routerFactory is not defined.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return factory;
|
||||||
|
}
|
@@ -20,7 +20,7 @@
|
|||||||
worker-src 'self';
|
worker-src 'self';
|
||||||
script-src 'self' 'unsafe-eval' https://*.hcaptcha.com https://challenges.cloudflare.com;
|
script-src 'self' 'unsafe-eval' https://*.hcaptcha.com https://challenges.cloudflare.com;
|
||||||
style-src 'self' 'unsafe-inline';
|
style-src 'self' 'unsafe-inline';
|
||||||
img-src 'self' data: www.google.com xn--931a.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
|
img-src 'self' data: blob: www.google.com xn--931a.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
|
||||||
media-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
|
media-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
|
||||||
connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com;"
|
connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com;"
|
||||||
/>
|
/>
|
||||||
|
@@ -5,11 +5,11 @@
|
|||||||
|
|
||||||
// NIRAX --- A lightweight router
|
// NIRAX --- A lightweight router
|
||||||
|
|
||||||
import { EventEmitter } from 'eventemitter3';
|
|
||||||
import { Component, onMounted, shallowRef, ShallowRef } from 'vue';
|
import { Component, onMounted, shallowRef, ShallowRef } from 'vue';
|
||||||
|
import { EventEmitter } from 'eventemitter3';
|
||||||
import { safeURIDecode } from '@/scripts/safe-uri-decode.js';
|
import { safeURIDecode } from '@/scripts/safe-uri-decode.js';
|
||||||
|
|
||||||
type RouteDef = {
|
export type RouteDef = {
|
||||||
path: string;
|
path: string;
|
||||||
component: Component;
|
component: Component;
|
||||||
query?: Record<string, string>;
|
query?: Record<string, string>;
|
||||||
@@ -27,6 +27,27 @@ type ParsedPath = (string | {
|
|||||||
optional?: boolean;
|
optional?: boolean;
|
||||||
})[];
|
})[];
|
||||||
|
|
||||||
|
export type RouterEvent = {
|
||||||
|
change: (ctx: {
|
||||||
|
beforePath: string;
|
||||||
|
path: string;
|
||||||
|
resolved: Resolved;
|
||||||
|
key: string;
|
||||||
|
}) => void;
|
||||||
|
replace: (ctx: {
|
||||||
|
path: string;
|
||||||
|
key: string;
|
||||||
|
}) => void;
|
||||||
|
push: (ctx: {
|
||||||
|
beforePath: string;
|
||||||
|
path: string;
|
||||||
|
route: RouteDef | null;
|
||||||
|
props: Map<string, string> | null;
|
||||||
|
key: string;
|
||||||
|
}) => void;
|
||||||
|
same: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
export type Resolved = { route: RouteDef; props: Map<string, string | boolean>; child?: Resolved; };
|
export type Resolved = { route: RouteDef; props: Map<string, string | boolean>; child?: Resolved; };
|
||||||
|
|
||||||
function parsePath(path: string): ParsedPath {
|
function parsePath(path: string): ParsedPath {
|
||||||
@@ -54,26 +75,85 @@ function parsePath(path: string): ParsedPath {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Router extends EventEmitter<{
|
export interface IRouter extends EventEmitter<RouterEvent> {
|
||||||
change: (ctx: {
|
current: Resolved;
|
||||||
beforePath: string;
|
currentRef: ShallowRef<Resolved>;
|
||||||
path: string;
|
currentRoute: ShallowRef<RouteDef>;
|
||||||
resolved: Resolved;
|
navHook: ((path: string, flag?: any) => boolean) | null;
|
||||||
key: string;
|
|
||||||
}) => void;
|
resolve(path: string): Resolved | null;
|
||||||
replace: (ctx: {
|
|
||||||
path: string;
|
getCurrentPath(): any;
|
||||||
key: string;
|
|
||||||
}) => void;
|
getCurrentKey(): string;
|
||||||
push: (ctx: {
|
|
||||||
beforePath: string;
|
push(path: string, flag?: any): void;
|
||||||
path: string;
|
|
||||||
route: RouteDef | null;
|
replace(path: string, key?: string | null): void;
|
||||||
props: Map<string, string> | null;
|
|
||||||
key: string;
|
/** @see EventEmitter */
|
||||||
}) => void;
|
eventNames(): Array<EventEmitter.EventNames<RouterEvent>>;
|
||||||
same: () => void;
|
|
||||||
}> {
|
/** @see EventEmitter */
|
||||||
|
listeners<T extends EventEmitter.EventNames<RouterEvent>>(
|
||||||
|
event: T
|
||||||
|
): Array<EventEmitter.EventListener<RouterEvent, T>>;
|
||||||
|
|
||||||
|
/** @see EventEmitter */
|
||||||
|
listenerCount(
|
||||||
|
event: EventEmitter.EventNames<RouterEvent>
|
||||||
|
): number;
|
||||||
|
|
||||||
|
/** @see EventEmitter */
|
||||||
|
emit<T extends EventEmitter.EventNames<RouterEvent>>(
|
||||||
|
event: T,
|
||||||
|
...args: EventEmitter.EventArgs<RouterEvent, T>
|
||||||
|
): boolean;
|
||||||
|
|
||||||
|
/** @see EventEmitter */
|
||||||
|
on<T extends EventEmitter.EventNames<RouterEvent>>(
|
||||||
|
event: T,
|
||||||
|
fn: EventEmitter.EventListener<RouterEvent, T>,
|
||||||
|
context?: any
|
||||||
|
): this;
|
||||||
|
|
||||||
|
/** @see EventEmitter */
|
||||||
|
addListener<T extends EventEmitter.EventNames<RouterEvent>>(
|
||||||
|
event: T,
|
||||||
|
fn: EventEmitter.EventListener<RouterEvent, T>,
|
||||||
|
context?: any
|
||||||
|
): this;
|
||||||
|
|
||||||
|
/** @see EventEmitter */
|
||||||
|
once<T extends EventEmitter.EventNames<RouterEvent>>(
|
||||||
|
event: T,
|
||||||
|
fn: EventEmitter.EventListener<RouterEvent, T>,
|
||||||
|
context?: any
|
||||||
|
): this;
|
||||||
|
|
||||||
|
/** @see EventEmitter */
|
||||||
|
removeListener<T extends EventEmitter.EventNames<RouterEvent>>(
|
||||||
|
event: T,
|
||||||
|
fn?: EventEmitter.EventListener<RouterEvent, T>,
|
||||||
|
context?: any,
|
||||||
|
once?: boolean | undefined
|
||||||
|
): this;
|
||||||
|
|
||||||
|
/** @see EventEmitter */
|
||||||
|
off<T extends EventEmitter.EventNames<RouterEvent>>(
|
||||||
|
event: T,
|
||||||
|
fn?: EventEmitter.EventListener<RouterEvent, T>,
|
||||||
|
context?: any,
|
||||||
|
once?: boolean | undefined
|
||||||
|
): this;
|
||||||
|
|
||||||
|
/** @see EventEmitter */
|
||||||
|
removeAllListeners(
|
||||||
|
event?: EventEmitter.EventNames<RouterEvent>
|
||||||
|
): this;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Router extends EventEmitter<RouterEvent> implements IRouter {
|
||||||
private routes: RouteDef[];
|
private routes: RouteDef[];
|
||||||
public current: Resolved;
|
public current: Resolved;
|
||||||
public currentRef: ShallowRef<Resolved> = shallowRef();
|
public currentRef: ShallowRef<Resolved> = shallowRef();
|
||||||
@@ -277,7 +357,7 @@ export class Router extends EventEmitter<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useScrollPositionManager(getScrollContainer: () => HTMLElement, router: Router) {
|
export function useScrollPositionManager(getScrollContainer: () => HTMLElement, router: IRouter) {
|
||||||
const scrollPosStore = new Map<string, number>();
|
const scrollPosStore = new Map<string, number>();
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
@@ -36,8 +36,8 @@ import { instance } from '@/instance.js';
|
|||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { lookupUser, lookupUserByEmail } from '@/scripts/lookup-user.js';
|
import { lookupUser, lookupUserByEmail } from '@/scripts/lookup-user.js';
|
||||||
import { useRouter } from '@/router.js';
|
|
||||||
import { PageMetadata, definePageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
|
import { PageMetadata, definePageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
|
||||||
|
import { useRouter } from '@/global/router/supplier.js';
|
||||||
|
|
||||||
const isEmpty = (x: string | null) => x == null || x === '';
|
const isEmpty = (x: string | null) => x == null || x === '';
|
||||||
|
|
||||||
|
@@ -31,9 +31,9 @@ import * as os from '@/os.js';
|
|||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { useRouter } from '@/router.js';
|
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { rolesCache } from '@/cache.js';
|
import { rolesCache } from '@/cache.js';
|
||||||
|
import { useRouter } from '@/global/router/supplier.js';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@@ -70,12 +70,12 @@ import * as os from '@/os.js';
|
|||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { useRouter } from '@/router.js';
|
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||||
import MkInfo from '@/components/MkInfo.vue';
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
import MkPagination from '@/components/MkPagination.vue';
|
import MkPagination from '@/components/MkPagination.vue';
|
||||||
import { infoImageUrl } from '@/instance.js';
|
import { infoImageUrl } from '@/instance.js';
|
||||||
|
import { useRouter } from '@/global/router/supplier.js';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@@ -237,9 +237,9 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
|
|||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { instance } from '@/instance.js';
|
import { instance } from '@/instance.js';
|
||||||
import { useRouter } from '@/router.js';
|
|
||||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||||
import { ROLE_POLICIES } from '@/const.js';
|
import { ROLE_POLICIES } from '@/const.js';
|
||||||
|
import { useRouter } from '@/global/router/supplier.js';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const baseRoleQ = ref('');
|
const baseRoleQ = ref('');
|
||||||
|
@@ -30,9 +30,9 @@ import MkTimeline from '@/components/MkTimeline.vue';
|
|||||||
import { scroll } from '@/scripts/scroll.js';
|
import { scroll } from '@/scripts/scroll.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { useRouter } from '@/router.js';
|
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { useRouter } from '@/global/router/supplier.js';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@@ -77,12 +77,12 @@ import MkColorInput from '@/components/MkColorInput.vue';
|
|||||||
import { selectFile } from '@/scripts/select-file.js';
|
import { selectFile } from '@/scripts/select-file.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { useRouter } from '@/router.js';
|
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
import MkSwitch from '@/components/MkSwitch.vue';
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import MkTextarea from '@/components/MkTextarea.vue';
|
import MkTextarea from '@/components/MkTextarea.vue';
|
||||||
|
import { useRouter } from '@/global/router/supplier.js';
|
||||||
|
|
||||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||||
|
|
||||||
|
@@ -75,7 +75,6 @@ import MkTimeline from '@/components/MkTimeline.vue';
|
|||||||
import XChannelFollowButton from '@/components/MkChannelFollowButton.vue';
|
import XChannelFollowButton from '@/components/MkChannelFollowButton.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { useRouter } from '@/router.js';
|
|
||||||
import { $i, iAmModerator } from '@/account.js';
|
import { $i, iAmModerator } from '@/account.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
@@ -92,6 +91,7 @@ import { PageHeaderItem } from '@/types/page-header.js';
|
|||||||
import { isSupportShare } from '@/scripts/navigator.js';
|
import { isSupportShare } from '@/scripts/navigator.js';
|
||||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||||
import { miLocalStorage } from '@/local-storage.js';
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
|
import { useRouter } from '@/global/router/supplier.js';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@@ -58,9 +58,9 @@ import MkInput from '@/components/MkInput.vue';
|
|||||||
import MkRadios from '@/components/MkRadios.vue';
|
import MkRadios from '@/components/MkRadios.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||||
import { useRouter } from '@/router.js';
|
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { useRouter } from '@/global/router/supplier.js';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@@ -80,7 +80,7 @@ import { infoImageUrl } from '@/instance.js';
|
|||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { useRouter } from '@/router.js';
|
import { useRouter } from '@/global/router/supplier.js';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@@ -7,14 +7,24 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<MkStickyContainer>
|
<MkStickyContainer>
|
||||||
<template #header><MkPageHeader/></template>
|
<template #header><MkPageHeader/></template>
|
||||||
<MkSpacer :contentMax="800">
|
<MkSpacer :contentMax="800">
|
||||||
<div v-show="!gameStarted" class="_gaps_s" :class="$style.root">
|
<div v-show="!gameStarted" :class="$style.root">
|
||||||
<div style="text-align: center;">
|
<div style="text-align: center;" class="_gaps">
|
||||||
<div>{{ i18n.ts.bubbleGame }}</div>
|
<div :class="$style.frame">
|
||||||
<MkSelect v-model="gameMode">
|
<div :class="$style.frameInner">
|
||||||
<option value="normal">NORMAL</option>
|
<img src="/client-assets/drop-and-fusion/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/>
|
||||||
<option value="square">SQUARE</option>
|
</div>
|
||||||
</MkSelect>
|
</div>
|
||||||
<MkButton primary gradate large rounded inline @click="start">{{ i18n.ts.start }}</MkButton>
|
<div :class="$style.frame">
|
||||||
|
<div :class="$style.frameInner">
|
||||||
|
<div class="_gaps" style="padding: 16px;">
|
||||||
|
<MkSelect v-model="gameMode">
|
||||||
|
<option value="normal">NORMAL</option>
|
||||||
|
<option value="square">SQUARE</option>
|
||||||
|
</MkSelect>
|
||||||
|
<MkButton primary gradate large rounded inline @click="start">{{ i18n.ts.start }}</MkButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="gameStarted" class="_gaps_s" :class="$style.root">
|
<div v-show="gameStarted" class="_gaps_s" :class="$style.root">
|
||||||
@@ -36,13 +46,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
:moveClass="$style.transition_stock_move"
|
:moveClass="$style.transition_stock_move"
|
||||||
>
|
>
|
||||||
<div v-for="x in stock" :key="x.id" style="display: inline-block;">
|
<div v-for="x in stock" :key="x.id" style="display: inline-block;">
|
||||||
<img :src="x.mono.img" style="width: 32px;"/>
|
<img :src="game.getTextureImageUrl(x.mono)" style="width: 32px;"/>
|
||||||
</div>
|
</div>
|
||||||
</TransitionGroup>
|
</TransitionGroup>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.main">
|
<div :class="$style.main" @contextmenu.stop.prevent>
|
||||||
<div ref="containerEl" :class="[$style.container, { [$style.gameOver]: gameOver }]" @click.stop.prevent="onClick" @touchmove.stop.prevent="onTouchmove" @touchend="onTouchend" @mousemove="onMousemove">
|
<div ref="containerEl" :class="[$style.container, { [$style.gameOver]: gameOver }]" @click.stop.prevent="onClick" @touchmove.stop.prevent="onTouchmove" @touchend="onTouchend" @mousemove="onMousemove">
|
||||||
<img v-if="defaultStore.state.darkMode" src="/client-assets/drop-and-fusion/frame-dark.svg" :class="$style.mainFrameImg"/>
|
<img v-if="defaultStore.state.darkMode" src="/client-assets/drop-and-fusion/frame-dark.svg" :class="$style.mainFrameImg"/>
|
||||||
<img v-else src="/client-assets/drop-and-fusion/frame-light.svg" :class="$style.mainFrameImg"/>
|
<img v-else src="/client-assets/drop-and-fusion/frame-light.svg" :class="$style.mainFrameImg"/>
|
||||||
@@ -56,7 +66,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
>
|
>
|
||||||
<div v-show="combo > 1" :class="$style.combo" :style="{ fontSize: `${100 + ((comboPrev - 2) * 15)}%` }">{{ comboPrev }} Chain!</div>
|
<div v-show="combo > 1" :class="$style.combo" :style="{ fontSize: `${100 + ((comboPrev - 2) * 15)}%` }">{{ comboPrev }} Chain!</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
<img v-if="currentPick" src="/client-assets/drop-and-fusion/dropper.png" :class="$style.dropper" :style="{ left: mouseX + 'px' }"/>
|
<img v-if="currentPick" src="/client-assets/drop-and-fusion/dropper.png" :class="$style.dropper" :style="{ left: dropperX + 'px' }"/>
|
||||||
<Transition
|
<Transition
|
||||||
:enterActiveClass="$style.transition_picked_enterActive"
|
:enterActiveClass="$style.transition_picked_enterActive"
|
||||||
:leaveActiveClass="$style.transition_picked_leaveActive"
|
:leaveActiveClass="$style.transition_picked_leaveActive"
|
||||||
@@ -65,31 +75,51 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
:moveClass="$style.transition_picked_move"
|
:moveClass="$style.transition_picked_move"
|
||||||
mode="out-in"
|
mode="out-in"
|
||||||
>
|
>
|
||||||
<img v-if="currentPick" :key="currentPick.id" :src="currentPick?.mono.img" :class="$style.currentMono" :style="{ top: -(currentPick?.mono.size / 2) + 'px', left: (mouseX - (currentPick?.mono.size / 2)) + 'px', width: `${currentPick?.mono.size}px` }"/>
|
<img v-if="currentPick" :key="currentPick.id" :src="game.getTextureImageUrl(currentPick.mono)" :class="$style.currentMono" :style="{ top: -(currentPick?.mono.size / 2) + 'px', left: (dropperX - (currentPick?.mono.size / 2)) + 'px', width: `${currentPick?.mono.size}px` }"/>
|
||||||
</Transition>
|
</Transition>
|
||||||
<template v-if="dropReady">
|
<template v-if="dropReady && currentPick">
|
||||||
<img src="/client-assets/drop-and-fusion/drop-arrow.svg" :class="$style.currentMonoArrow" :style="{ top: (currentPick?.mono.size / 2) + 10 + 'px', left: (mouseX - 10) + 'px', width: `20px` }"/>
|
<img src="/client-assets/drop-and-fusion/drop-arrow.svg" :class="$style.currentMonoArrow" :style="{ top: (currentPick.mono.size / 2) + 10 + 'px', left: (dropperX - 10) + 'px', width: `20px` }"/>
|
||||||
<div :class="$style.dropGuide" :style="{ left: (mouseX - 2) + 'px' }"/>
|
<div :class="$style.dropGuide" :style="{ left: (dropperX - 2) + 'px' }"/>
|
||||||
</template>
|
</template>
|
||||||
<div v-if="gameOver" :class="$style.gameOverLabel">
|
<div v-if="gameOver" :class="$style.gameOverLabel">
|
||||||
<div>GAME OVER!</div>
|
<div class="_gaps_s">
|
||||||
<div>SCORE: <MkNumber :value="score"/></div>
|
<img src="/client-assets/drop-and-fusion/gameover.png" style="width: 200px; max-width: 100%; display: block; margin: auto; margin-bottom: -5px;"/>
|
||||||
<MkButton primary rounded inline @click="share">Share</MkButton>
|
<div>SCORE: <MkNumber :value="score"/></div>
|
||||||
|
<div>MAX CHAIN: <MkNumber :value="maxCombo"/></div>
|
||||||
|
<div class="_buttonsCenter">
|
||||||
|
<MkButton primary rounded @click="restart">Restart</MkButton>
|
||||||
|
<MkButton primary rounded @click="share">Share</MkButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex;">
|
<div style="display: flex;">
|
||||||
<div :class="$style.frame" style="flex: 1; margin-right: 10px;">
|
<div :class="$style.frame" style="flex: 1; margin-right: 10px;">
|
||||||
<div :class="$style.frameInner">
|
<div :class="$style.frameInner">
|
||||||
<div>SCORE: <b><MkNumber :value="score"/></b></div>
|
<div>SCORE: <b><MkNumber :value="score"/></b> (MAX CHAIN: <b><MkNumber :value="maxCombo"/></b>)</div>
|
||||||
<div>HIGH SCORE: <b v-if="highScore"><MkNumber :value="highScore"/></b><b v-else>-</b></div>
|
<div>HIGH SCORE: <b v-if="highScore"><MkNumber :value="highScore"/></b><b v-else>-</b></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div :class="[$style.frame]" style="margin-left: auto;">
|
<div :class="[$style.frame]" style="margin-left: auto;">
|
||||||
<div :class="$style.frameInner" style="text-align: center;">
|
<div :class="$style.frameInner" style="text-align: center;">
|
||||||
|
<div @click="showConfig = !showConfig"><i class="ti ti-settings"></i></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="showConfig" :class="$style.frame">
|
||||||
|
<div :class="$style.frameInner">
|
||||||
|
<MkRange v-model="bgmVolume" :min="0" :max="1" :step="0.0025" :textConverter="(v) => `${Math.floor(v * 100)}%`" :continuousUpdate="true">
|
||||||
|
<template #label>BGM {{ i18n.ts.volume }}</template>
|
||||||
|
</MkRange>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="showConfig" :class="$style.frame">
|
||||||
|
<div :class="$style.frameInner">
|
||||||
|
<div>Credit</div>
|
||||||
|
<div>BGM: @ys@misskey.design</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div :class="$style.frame">
|
<div :class="$style.frame">
|
||||||
<div :class="$style.frameInner">
|
<div :class="$style.frameInner">
|
||||||
<MkButton @click="restart">Restart</MkButton>
|
<MkButton @click="restart">Restart</MkButton>
|
||||||
@@ -101,17 +131,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import * as Matter from 'matter-js';
|
import { onDeactivated, ref, shallowRef, watch } from 'vue';
|
||||||
import { Ref, onMounted, ref, shallowRef } from 'vue';
|
|
||||||
import { EventEmitter } from 'eventemitter3';
|
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import * as sound from '@/scripts/sound.js';
|
|
||||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import MkNumber from '@/components/MkNumber.vue';
|
import MkNumber from '@/components/MkNumber.vue';
|
||||||
import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue';
|
import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import { claimAchievement } from '@/scripts/achievements.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
@@ -119,23 +147,13 @@ import { useInterval } from '@/scripts/use-interval.js';
|
|||||||
import MkSelect from '@/components/MkSelect.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
import { apiUrl } from '@/config.js';
|
import { apiUrl } from '@/config.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
|
import { DropAndFusionGame, Mono } from '@/scripts/drop-and-fusion-engine.js';
|
||||||
type Mono = {
|
import * as sound from '@/scripts/sound.js';
|
||||||
id: string;
|
import MkRange from '@/components/MkRange.vue';
|
||||||
level: number;
|
|
||||||
size: number;
|
|
||||||
shape: 'circle' | 'rectangle';
|
|
||||||
score: number;
|
|
||||||
dropCandidate: boolean;
|
|
||||||
sfxPitch: number;
|
|
||||||
img: string;
|
|
||||||
imgSize: number;
|
|
||||||
spriteScale: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const containerEl = shallowRef<HTMLElement>();
|
const containerEl = shallowRef<HTMLElement>();
|
||||||
const canvasEl = shallowRef<HTMLCanvasElement>();
|
const canvasEl = shallowRef<HTMLCanvasElement>();
|
||||||
const mouseX = ref(0);
|
const dropperX = ref(0);
|
||||||
|
|
||||||
const NORMAL_BASE_SIZE = 30;
|
const NORMAL_BASE_SIZE = 30;
|
||||||
const NORAML_MONOS: Mono[] = [{
|
const NORAML_MONOS: Mono[] = [{
|
||||||
@@ -365,7 +383,6 @@ const SQUARE_MONOS: Mono[] = [{
|
|||||||
|
|
||||||
const GAME_WIDTH = 450;
|
const GAME_WIDTH = 450;
|
||||||
const GAME_HEIGHT = 600;
|
const GAME_HEIGHT = 600;
|
||||||
const PHYSICS_QUALITY_FACTOR = 16; // 低いほどパフォーマンスが高いがガタガタして安定しなくなる、逆に高すぎても何故か不安定になる
|
|
||||||
|
|
||||||
let viewScaleX = 1;
|
let viewScaleX = 1;
|
||||||
let viewScaleY = 1;
|
let viewScaleY = 1;
|
||||||
@@ -374,338 +391,44 @@ const stock = shallowRef<{ id: string; mono: Mono }[]>([]);
|
|||||||
const score = ref(0);
|
const score = ref(0);
|
||||||
const combo = ref(0);
|
const combo = ref(0);
|
||||||
const comboPrev = ref(0);
|
const comboPrev = ref(0);
|
||||||
|
const maxCombo = ref(0);
|
||||||
const dropReady = ref(true);
|
const dropReady = ref(true);
|
||||||
const gameMode = ref<'normal' | 'square'>('normal');
|
const gameMode = ref<'normal' | 'square'>('normal');
|
||||||
const gameOver = ref(false);
|
const gameOver = ref(false);
|
||||||
const gameStarted = ref(false);
|
const gameStarted = ref(false);
|
||||||
const highScore = ref<number | null>(null);
|
const highScore = ref<number | null>(null);
|
||||||
|
const showConfig = ref(false);
|
||||||
|
const bgmVolume = ref(0.1);
|
||||||
|
|
||||||
class Game extends EventEmitter<{
|
let game: DropAndFusionGame;
|
||||||
changeScore: (score: number) => void;
|
let containerElRect: DOMRect | null = null;
|
||||||
changeCombo: (combo: number) => void;
|
|
||||||
changeStock: (stock: { id: string; mono: Mono }[]) => void;
|
|
||||||
dropped: () => void;
|
|
||||||
fusioned: (x: number, y: number, score: number) => void;
|
|
||||||
gameOver: () => void;
|
|
||||||
}> {
|
|
||||||
private COMBO_INTERVAL = 1000;
|
|
||||||
public readonly DROP_INTERVAL = 500;
|
|
||||||
private PLAYAREA_MARGIN = 25;
|
|
||||||
private STOCK_MAX = 4;
|
|
||||||
private engine: Matter.Engine;
|
|
||||||
private render: Matter.Render;
|
|
||||||
private runner: Matter.Runner;
|
|
||||||
private overflowCollider: Matter.Body;
|
|
||||||
private isGameOver = false;
|
|
||||||
|
|
||||||
private monoDefinitions: Mono[] = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* フィールドに出ていて、かつ合体の対象となるアイテム
|
|
||||||
*/
|
|
||||||
private activeBodyIds: Matter.Body['id'][] = [];
|
|
||||||
|
|
||||||
private latestDroppedBodyId: Matter.Body['id'] | null = null;
|
|
||||||
|
|
||||||
private latestDroppedAt = 0;
|
|
||||||
private latestFusionedAt = 0;
|
|
||||||
private stock: { id: string; mono: Mono }[] = [];
|
|
||||||
|
|
||||||
private _combo = 0;
|
|
||||||
private get combo() {
|
|
||||||
return this._combo;
|
|
||||||
}
|
|
||||||
private set combo(value: number) {
|
|
||||||
this._combo = value;
|
|
||||||
this.emit('changeCombo', value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _score = 0;
|
|
||||||
private get score() {
|
|
||||||
return this._score;
|
|
||||||
}
|
|
||||||
private set score(value: number) {
|
|
||||||
this._score = value;
|
|
||||||
this.emit('changeScore', value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private comboIntervalId: number | null = null;
|
|
||||||
|
|
||||||
constructor(opts: {
|
|
||||||
monoDefinitions: Mono[];
|
|
||||||
}) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.monoDefinitions = opts.monoDefinitions;
|
|
||||||
|
|
||||||
this.engine = Matter.Engine.create({
|
|
||||||
constraintIterations: 2 * PHYSICS_QUALITY_FACTOR,
|
|
||||||
positionIterations: 6 * PHYSICS_QUALITY_FACTOR,
|
|
||||||
velocityIterations: 4 * PHYSICS_QUALITY_FACTOR,
|
|
||||||
gravity: {
|
|
||||||
x: 0,
|
|
||||||
y: 1,
|
|
||||||
},
|
|
||||||
timing: {
|
|
||||||
timeScale: 2,
|
|
||||||
},
|
|
||||||
enableSleeping: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.render = Matter.Render.create({
|
|
||||||
engine: this.engine,
|
|
||||||
canvas: canvasEl.value,
|
|
||||||
options: {
|
|
||||||
width: GAME_WIDTH,
|
|
||||||
height: GAME_HEIGHT,
|
|
||||||
background: 'transparent', // transparent to hide
|
|
||||||
wireframeBackground: 'transparent', // transparent to hide
|
|
||||||
wireframes: false,
|
|
||||||
showSleeping: false,
|
|
||||||
pixelRatio: Math.max(2, window.devicePixelRatio),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
Matter.Render.run(this.render);
|
|
||||||
|
|
||||||
this.runner = Matter.Runner.create();
|
|
||||||
Matter.Runner.run(this.runner, this.engine);
|
|
||||||
|
|
||||||
this.engine.world.bodies = [];
|
|
||||||
|
|
||||||
//#region walls
|
|
||||||
const WALL_OPTIONS: Matter.IChamferableBodyDefinition = {
|
|
||||||
isStatic: true,
|
|
||||||
friction: 0.7,
|
|
||||||
slop: 1.0,
|
|
||||||
render: {
|
|
||||||
strokeStyle: 'transparent',
|
|
||||||
fillStyle: 'transparent',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const thickness = 100;
|
|
||||||
Matter.Composite.add(this.engine.world, [
|
|
||||||
Matter.Bodies.rectangle(GAME_WIDTH / 2, GAME_HEIGHT + (thickness / 2) - this.PLAYAREA_MARGIN, GAME_WIDTH, thickness, WALL_OPTIONS),
|
|
||||||
Matter.Bodies.rectangle(GAME_WIDTH + (thickness / 2) - this.PLAYAREA_MARGIN, GAME_HEIGHT / 2, thickness, GAME_HEIGHT, WALL_OPTIONS),
|
|
||||||
Matter.Bodies.rectangle(-((thickness / 2) - this.PLAYAREA_MARGIN), GAME_HEIGHT / 2, thickness, GAME_HEIGHT, WALL_OPTIONS),
|
|
||||||
]);
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
this.overflowCollider = Matter.Bodies.rectangle(GAME_WIDTH / 2, 0, GAME_WIDTH, 200, {
|
|
||||||
isStatic: true,
|
|
||||||
isSensor: true,
|
|
||||||
render: {
|
|
||||||
strokeStyle: 'transparent',
|
|
||||||
fillStyle: 'transparent',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
Matter.Composite.add(this.engine.world, this.overflowCollider);
|
|
||||||
|
|
||||||
// fit the render viewport to the scene
|
|
||||||
Matter.Render.lookAt(this.render, {
|
|
||||||
min: { x: 0, y: 0 },
|
|
||||||
max: { x: GAME_WIDTH, y: GAME_HEIGHT },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private createBody(mono: Mono, x: number, y: number) {
|
|
||||||
const options: Matter.IBodyDefinition = {
|
|
||||||
label: mono.id,
|
|
||||||
//density: 0.0005,
|
|
||||||
density: mono.size / 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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
if (mono.shape === 'circle') {
|
|
||||||
return Matter.Bodies.circle(x, y, mono.size / 2, options);
|
|
||||||
} else if (mono.shape === 'rectangle') {
|
|
||||||
return Matter.Bodies.rectangle(x, y, mono.size, mono.size, options);
|
|
||||||
} else {
|
|
||||||
throw new Error('unrecognized shape');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fusion(bodyA: Matter.Body, bodyB: Matter.Body) {
|
|
||||||
const now = Date.now();
|
|
||||||
if (this.latestFusionedAt > now - this.COMBO_INTERVAL) {
|
|
||||||
this.combo++;
|
|
||||||
} else {
|
|
||||||
this.combo = 1;
|
|
||||||
}
|
|
||||||
this.latestFusionedAt = now;
|
|
||||||
|
|
||||||
// TODO: 単に位置だけでなくそれぞれの動きベクトルも融合する
|
|
||||||
const newX = (bodyA.position.x + bodyB.position.x) / 2;
|
|
||||||
const newY = (bodyA.position.y + bodyB.position.y) / 2;
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
if (nextMono) {
|
|
||||||
const body = this.createBody(nextMono, newX, newY);
|
|
||||||
Matter.Composite.add(this.engine.world, body);
|
|
||||||
|
|
||||||
// 連鎖してfusionした場合の分かりやすさのため少し間を置いてからfusion対象になるようにする
|
|
||||||
window.setTimeout(() => {
|
|
||||||
this.activeBodyIds.push(body.id);
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
const comboBonus = 1 + ((this.combo - 1) / 5);
|
|
||||||
const additionalScore = Math.round(currentMono.score * comboBonus);
|
|
||||||
this.score += additionalScore;
|
|
||||||
|
|
||||||
const pan = ((newX / GAME_WIDTH) - 0.5) * 2;
|
|
||||||
sound.playRaw('syuilo/bubble2', 1, pan, nextMono.sfxPitch);
|
|
||||||
|
|
||||||
this.emit('fusioned', newX, newY, additionalScore);
|
|
||||||
} else {
|
|
||||||
//const VELOCITY = 30;
|
|
||||||
//for (let i = 0; i < 10; i++) {
|
|
||||||
// const body = createBody(FRUITS.find(x => x.level === (1 + Math.floor(Math.random() * 3)))!, x + ((Math.random() * VELOCITY) - (VELOCITY / 2)), y + ((Math.random() * VELOCITY) - (VELOCITY / 2)));
|
|
||||||
// Matter.Composite.add(world, body);
|
|
||||||
// bodies.push(body);
|
|
||||||
//}
|
|
||||||
//sound.playRaw({
|
|
||||||
// type: 'syuilo/bubble2',
|
|
||||||
// volume: 1,
|
|
||||||
//});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private gameOver() {
|
|
||||||
this.isGameOver = true;
|
|
||||||
Matter.Runner.stop(this.runner);
|
|
||||||
this.emit('gameOver');
|
|
||||||
}
|
|
||||||
|
|
||||||
public start() {
|
|
||||||
for (let i = 0; i < this.STOCK_MAX; i++) {
|
|
||||||
this.stock.push({
|
|
||||||
id: Math.random().toString(),
|
|
||||||
mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(Math.random() * this.monoDefinitions.filter(x => x.dropCandidate).length)],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.emit('changeStock', this.stock);
|
|
||||||
|
|
||||||
// TODO: fusion予約状態のアイテムは光らせるなどの演出をすると楽しそう
|
|
||||||
let fusionReservedPairs: { bodyA: Matter.Body; bodyB: Matter.Body }[] = [];
|
|
||||||
|
|
||||||
const minCollisionEnergyForSound = 2.5;
|
|
||||||
const maxCollisionEnergyForSound = 9;
|
|
||||||
const soundPitchMax = 4;
|
|
||||||
const soundPitchMin = 0.5;
|
|
||||||
|
|
||||||
Matter.Events.on(this.engine, 'collisionStart', (event) => {
|
|
||||||
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) && !fusionReservedPairs.some(x => x.bodyA.id === bodyA.id || x.bodyA.id === bodyB.id || x.bodyB.id === bodyA.id || x.bodyB.id === bodyB.id);
|
|
||||||
if (shouldFusion) {
|
|
||||||
if (this.activeBodyIds.includes(bodyA.id) && this.activeBodyIds.includes(bodyB.id)) {
|
|
||||||
this.fusion(bodyA, bodyB);
|
|
||||||
} else {
|
|
||||||
fusionReservedPairs.push({ bodyA, bodyB });
|
|
||||||
window.setTimeout(() => {
|
|
||||||
fusionReservedPairs = fusionReservedPairs.filter(x => x.bodyA.id !== bodyA.id && x.bodyB.id !== bodyB.id);
|
|
||||||
this.fusion(bodyA, bodyB);
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const energy = pairs.collision.depth;
|
|
||||||
if (energy > minCollisionEnergyForSound) {
|
|
||||||
const vol = (Math.min(maxCollisionEnergyForSound, energy - minCollisionEnergyForSound) / maxCollisionEnergyForSound) / 4;
|
|
||||||
const pan = ((((bodyA.position.x + bodyB.position.x) / 2) / GAME_WIDTH) - 0.5) * 2;
|
|
||||||
const pitch = soundPitchMin + ((soundPitchMax - soundPitchMin) * (1 - (Math.min(10, energy) / 10)));
|
|
||||||
sound.playRaw('syuilo/poi1', vol, pan, pitch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.comboIntervalId = window.setInterval(() => {
|
|
||||||
if (this.latestFusionedAt < Date.now() - this.COMBO_INTERVAL) {
|
|
||||||
this.combo = 0;
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
public drop(_x: number) {
|
|
||||||
if (this.isGameOver) return;
|
|
||||||
if (Date.now() - this.latestDroppedAt < this.DROP_INTERVAL) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const st = this.stock.shift()!;
|
|
||||||
this.stock.push({
|
|
||||||
id: Math.random().toString(),
|
|
||||||
mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(Math.random() * this.monoDefinitions.filter(x => x.dropCandidate).length)],
|
|
||||||
});
|
|
||||||
this.emit('changeStock', this.stock);
|
|
||||||
|
|
||||||
const x = Math.min(GAME_WIDTH - this.PLAYAREA_MARGIN - (st.mono.size / 2), Math.max(this.PLAYAREA_MARGIN + (st.mono.size / 2), _x));
|
|
||||||
const body = this.createBody(st.mono, x, 50 + st.mono.size / 2);
|
|
||||||
Matter.Composite.add(this.engine.world, body);
|
|
||||||
this.activeBodyIds.push(body.id);
|
|
||||||
this.latestDroppedBodyId = body.id;
|
|
||||||
this.latestDroppedAt = Date.now();
|
|
||||||
this.emit('dropped');
|
|
||||||
const pan = ((x / GAME_WIDTH) - 0.5) * 2;
|
|
||||||
sound.playRaw('syuilo/poi2', 1, pan);
|
|
||||||
}
|
|
||||||
|
|
||||||
public dispose() {
|
|
||||||
if (this.comboIntervalId) window.clearInterval(this.comboIntervalId);
|
|
||||||
Matter.Render.stop(this.render);
|
|
||||||
Matter.Runner.stop(this.runner);
|
|
||||||
Matter.World.clear(this.engine.world, false);
|
|
||||||
Matter.Engine.clear(this.engine);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let game: Game;
|
|
||||||
|
|
||||||
function onClick(ev: MouseEvent) {
|
function onClick(ev: MouseEvent) {
|
||||||
const rect = containerEl.value.getBoundingClientRect();
|
if (!containerElRect) return;
|
||||||
|
const x = (ev.clientX - containerElRect.left) / viewScaleX;
|
||||||
const x = (ev.clientX - rect.left) / viewScaleX;
|
|
||||||
|
|
||||||
game.drop(x);
|
game.drop(x);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTouchend(ev: TouchEvent) {
|
function onTouchend(ev: TouchEvent) {
|
||||||
const rect = containerEl.value.getBoundingClientRect();
|
if (!containerElRect) return;
|
||||||
|
const x = (ev.changedTouches[0].clientX - containerElRect.left) / viewScaleX;
|
||||||
const x = (ev.changedTouches[0].clientX - rect.left) / viewScaleX;
|
|
||||||
|
|
||||||
game.drop(x);
|
game.drop(x);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMousemove(ev: MouseEvent) {
|
function onMousemove(ev: MouseEvent) {
|
||||||
mouseX.value = ev.clientX - containerEl.value.getBoundingClientRect().left;
|
if (!containerElRect) return;
|
||||||
|
const x = (ev.clientX - containerElRect.left);
|
||||||
|
moveDropper(containerElRect, x);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTouchmove(ev: TouchEvent) {
|
function onTouchmove(ev: TouchEvent) {
|
||||||
mouseX.value = ev.touches[0].clientX - containerEl.value.getBoundingClientRect().left;
|
if (!containerElRect) return;
|
||||||
|
const x = (ev.touches[0].clientX - containerElRect.left);
|
||||||
|
moveDropper(containerElRect, x);
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveDropper(rect: DOMRect, x: number) {
|
||||||
|
dropperX.value = Math.min(rect.width * ((GAME_WIDTH - game.PLAYAREA_MARGIN) / GAME_WIDTH), Math.max(rect.width * (game.PLAYAREA_MARGIN / GAME_WIDTH), x));
|
||||||
}
|
}
|
||||||
|
|
||||||
function restart() {
|
function restart() {
|
||||||
@@ -720,7 +443,7 @@ function restart() {
|
|||||||
gameStarted.value = false;
|
gameStarted.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function attachGame() {
|
function attachGameEvents() {
|
||||||
game.addListener('changeScore', value => {
|
game.addListener('changeScore', value => {
|
||||||
score.value = value;
|
score.value = value;
|
||||||
});
|
});
|
||||||
@@ -731,6 +454,7 @@ function attachGame() {
|
|||||||
} else {
|
} else {
|
||||||
comboPrev.value = value;
|
comboPrev.value = value;
|
||||||
}
|
}
|
||||||
|
maxCombo.value = Math.max(maxCombo.value, value);
|
||||||
combo.value = value;
|
combo.value = value;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -748,12 +472,26 @@ function attachGame() {
|
|||||||
}, game.DROP_INTERVAL);
|
}, game.DROP_INTERVAL);
|
||||||
});
|
});
|
||||||
|
|
||||||
game.addListener('fusioned', (x, y, score) => {
|
game.addListener('fusioned', (x, y, scoreDelta) => {
|
||||||
|
if (!canvasEl.value) return;
|
||||||
|
|
||||||
const rect = canvasEl.value.getBoundingClientRect();
|
const rect = canvasEl.value.getBoundingClientRect();
|
||||||
const domX = rect.left + (x * viewScaleX);
|
const domX = rect.left + (x * viewScaleX);
|
||||||
const domY = rect.top + (y * viewScaleY);
|
const domY = rect.top + (y * viewScaleY);
|
||||||
os.popup(MkRippleEffect, { x: domX, y: domY }, {}, 'end');
|
os.popup(MkRippleEffect, { x: domX, y: domY }, {}, 'end');
|
||||||
os.popup(MkPlusOneEffect, { x: domX, y: domY, value: score }, {}, 'end');
|
os.popup(MkPlusOneEffect, { x: domX, y: domY, value: scoreDelta }, {}, 'end');
|
||||||
|
});
|
||||||
|
|
||||||
|
game.addListener('monoAdded', (mono) => {
|
||||||
|
// 実績関連
|
||||||
|
if (mono.level === 10) {
|
||||||
|
claimAchievement('bubbleGameExplodingHead');
|
||||||
|
|
||||||
|
const monos = game.getActiveMonos();
|
||||||
|
if (monos.filter(x => x.level === 10).length >= 2) {
|
||||||
|
claimAchievement('bubbleGameDoubleExplodingHead');
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
game.addListener('gameOver', () => {
|
game.addListener('gameOver', () => {
|
||||||
@@ -773,6 +511,8 @@ function attachGame() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let bgmNodes: ReturnType<typeof sound.createSourceNode> = null;
|
||||||
|
|
||||||
async function start() {
|
async function start() {
|
||||||
try {
|
try {
|
||||||
highScore.value = await misskeyApi('i/registry/get', {
|
highScore.value = await misskeyApi('i/registry/get', {
|
||||||
@@ -780,42 +520,85 @@ async function start() {
|
|||||||
key: 'highScore:' + gameMode.value,
|
key: 'highScore:' + gameMode.value,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
highScore.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStarted.value = true;
|
game = new DropAndFusionGame({
|
||||||
game = new Game(gameMode.value === 'normal' ? {
|
width: GAME_WIDTH,
|
||||||
monoDefinitions: NORAML_MONOS,
|
height: GAME_HEIGHT,
|
||||||
} : {
|
canvas: canvasEl.value!,
|
||||||
monoDefinitions: SQUARE_MONOS,
|
...(
|
||||||
|
gameMode.value === 'normal' ? {
|
||||||
|
monoDefinitions: NORAML_MONOS,
|
||||||
|
} : {
|
||||||
|
monoDefinitions: SQUARE_MONOS,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
});
|
||||||
|
attachGameEvents();
|
||||||
|
os.promiseDialog(game.load(), async () => {
|
||||||
|
game.start();
|
||||||
|
gameStarted.value = true;
|
||||||
|
|
||||||
|
if (bgmNodes) {
|
||||||
|
bgmNodes.soundSource.stop();
|
||||||
|
bgmNodes = null;
|
||||||
|
}
|
||||||
|
const bgmBuffer = await sound.loadAudio('/client-assets/drop-and-fusion/bgm_1.mp3');
|
||||||
|
if (!bgmBuffer) return;
|
||||||
|
bgmNodes = sound.createSourceNode(bgmBuffer, bgmVolume.value);
|
||||||
|
if (!bgmNodes) return;
|
||||||
|
bgmNodes.soundSource.loop = true;
|
||||||
|
bgmNodes.soundSource.start();
|
||||||
});
|
});
|
||||||
attachGame();
|
|
||||||
game.start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(bgmVolume, (value) => {
|
||||||
|
if (bgmNodes) {
|
||||||
|
bgmNodes.gainNode.gain.value = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function getGameImageDriveFile() {
|
function getGameImageDriveFile() {
|
||||||
return new Promise<Misskey.entities.DriveFile | null>(res => {
|
return new Promise<Misskey.entities.DriveFile | null>(res => {
|
||||||
canvasEl.value?.toBlob(blob => {
|
const dcanvas = document.createElement('canvas');
|
||||||
if (!blob) return res(null);
|
dcanvas.width = GAME_WIDTH;
|
||||||
if ($i == null) return res(null);
|
dcanvas.height = GAME_HEIGHT;
|
||||||
const formData = new FormData();
|
const ctx = dcanvas.getContext('2d');
|
||||||
formData.append('file', blob);
|
if (!ctx || !canvasEl.value) return res(null);
|
||||||
formData.append('name', `bubble-game-${Date.now()}.png`);
|
const dimage = new Image();
|
||||||
formData.append('isSensitive', 'false');
|
dimage.src = '/client-assets/drop-and-fusion/frame-light.svg';
|
||||||
formData.append('comment', 'null');
|
dimage.addEventListener('load', () => {
|
||||||
formData.append('i', $i.token);
|
ctx.fillStyle = '#fff';
|
||||||
if (defaultStore.state.uploadFolder) {
|
ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
|
||||||
formData.append('folderId', defaultStore.state.uploadFolder);
|
ctx.drawImage(dimage, 0, 0, GAME_WIDTH, GAME_HEIGHT);
|
||||||
}
|
ctx.drawImage(canvasEl.value!, 0, 0, GAME_WIDTH, GAME_HEIGHT);
|
||||||
|
|
||||||
window.fetch(apiUrl + '/drive/files/create', {
|
dcanvas.toBlob(blob => {
|
||||||
method: 'POST',
|
if (!blob) return res(null);
|
||||||
body: formData,
|
if ($i == null) return res(null);
|
||||||
})
|
const formData = new FormData();
|
||||||
.then(response => response.json())
|
formData.append('file', blob);
|
||||||
.then(f => {
|
formData.append('name', `bubble-game-${Date.now()}.png`);
|
||||||
res(f);
|
formData.append('isSensitive', 'false');
|
||||||
});
|
formData.append('comment', 'null');
|
||||||
}, 'image/png');
|
formData.append('i', $i.token);
|
||||||
|
if (defaultStore.state.uploadFolder) {
|
||||||
|
formData.append('folderId', defaultStore.state.uploadFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.fetch(apiUrl + '/drive/files/create', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(f => {
|
||||||
|
res(f);
|
||||||
|
});
|
||||||
|
}, 'image/png');
|
||||||
|
|
||||||
|
dcanvas.remove();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -827,7 +610,7 @@ async function share() {
|
|||||||
os.post({
|
os.post({
|
||||||
initialText: `#BubbleGame
|
initialText: `#BubbleGame
|
||||||
MODE: ${gameMode.value}
|
MODE: ${gameMode.value}
|
||||||
SCORE: ${score.value}`,
|
SCORE: ${score.value} (MAX CHAIN: ${maxCombo.value})})`,
|
||||||
initialFiles: [file],
|
initialFiles: [file],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -838,9 +621,11 @@ useInterval(() => {
|
|||||||
const actualCanvasHeight = canvasEl.value.getBoundingClientRect().height;
|
const actualCanvasHeight = canvasEl.value.getBoundingClientRect().height;
|
||||||
viewScaleX = actualCanvasWidth / GAME_WIDTH;
|
viewScaleX = actualCanvasWidth / GAME_WIDTH;
|
||||||
viewScaleY = actualCanvasHeight / GAME_HEIGHT;
|
viewScaleY = actualCanvasHeight / GAME_HEIGHT;
|
||||||
|
containerElRect = containerEl.value?.getBoundingClientRect() ?? null;
|
||||||
}, 1000, { immediate: false, afterMounted: true });
|
}, 1000, { immediate: false, afterMounted: true });
|
||||||
|
|
||||||
onMounted(async () => {
|
onDeactivated(() => {
|
||||||
|
game.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
definePageMetadata({
|
definePageMetadata({
|
||||||
|
@@ -45,7 +45,7 @@ import MkTextarea from '@/components/MkTextarea.vue';
|
|||||||
import MkCodeEditor from '@/components/MkCodeEditor.vue';
|
import MkCodeEditor from '@/components/MkCodeEditor.vue';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
import { useRouter } from '@/router.js';
|
import { useRouter } from '@/global/router/supplier.js';
|
||||||
|
|
||||||
const PRESET_DEFAULT = `/// @ 0.16.0
|
const PRESET_DEFAULT = `/// @ 0.16.0
|
||||||
|
|
||||||
|
@@ -42,9 +42,9 @@ import { computed, ref } from 'vue';
|
|||||||
import MkFlashPreview from '@/components/MkFlashPreview.vue';
|
import MkFlashPreview from '@/components/MkFlashPreview.vue';
|
||||||
import MkPagination from '@/components/MkPagination.vue';
|
import MkPagination from '@/components/MkPagination.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { useRouter } from '@/router.js';
|
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
|
import { useRouter } from '@/global/router/supplier.js';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@@ -13,9 +13,9 @@ import { } from 'vue';
|
|||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { mainRouter } from '@/router.js';
|
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
import { mainRouter } from '@/global/router/main.js';
|
||||||
|
|
||||||
async function follow(user): Promise<void> {
|
async function follow(user): Promise<void> {
|
||||||
const { canceled } = await os.confirm({
|
const { canceled } = await os.confirm({
|
||||||
|
@@ -48,9 +48,9 @@ import FormSuspense from '@/components/form/suspense.vue';
|
|||||||
import { selectFiles } from '@/scripts/select-file.js';
|
import { selectFiles } from '@/scripts/select-file.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { useRouter } from '@/router.js';
|
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { useRouter } from '@/global/router/supplier.js';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@@ -53,7 +53,7 @@ import MkPagination from '@/components/MkPagination.vue';
|
|||||||
import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue';
|
import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { useRouter } from '@/router.js';
|
import { useRouter } from '@/global/router/supplier.js';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@@ -72,13 +72,13 @@ import MkPagination from '@/components/MkPagination.vue';
|
|||||||
import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue';
|
import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue';
|
||||||
import MkFollowButton from '@/components/MkFollowButton.vue';
|
import MkFollowButton from '@/components/MkFollowButton.vue';
|
||||||
import { url } from '@/config.js';
|
import { url } from '@/config.js';
|
||||||
import { useRouter } from '@/router.js';
|
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import { isSupportShare } from '@/scripts/navigator.js';
|
import { isSupportShare } from '@/scripts/navigator.js';
|
||||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||||
|
import { useRouter } from '@/global/router/supplier.js';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@@ -14,8 +14,8 @@ import { ref } from 'vue';
|
|||||||
import XAntenna from './editor.vue';
|
import XAntenna from './editor.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { useRouter } from '@/router.js';
|
|
||||||
import { antennasCache } from '@/cache.js';
|
import { antennasCache } from '@/cache.js';
|
||||||
|
import { useRouter } from '@/global/router/supplier.js';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@@ -15,9 +15,9 @@ import * as Misskey from 'misskey-js';
|
|||||||
import XAntenna from './editor.vue';
|
import XAntenna from './editor.vue';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { useRouter } from '@/router.js';
|
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { antennasCache } from '@/cache.js';
|
import { antennasCache } from '@/cache.js';
|
||||||
|
import { useRouter } from '@/global/router/supplier.js';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@@ -58,7 +58,6 @@ import * as Misskey from 'misskey-js';
|
|||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { mainRouter } from '@/router.js';
|
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { userPage } from '@/filters/user.js';
|
import { userPage } from '@/filters/user.js';
|
||||||
@@ -70,6 +69,7 @@ import { userListsCache } from '@/cache.js';
|
|||||||
import { signinRequired } from '@/account.js';
|
import { signinRequired } from '@/account.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import MkPagination from '@/components/MkPagination.vue';
|
import MkPagination from '@/components/MkPagination.vue';
|
||||||
|
import { mainRouter } from '@/global/router/main.js';
|
||||||
|
|
||||||
const $i = signinRequired();
|
const $i = signinRequired();
|
||||||
|
|
||||||
|
@@ -73,10 +73,10 @@ import { url } from '@/config.js';
|
|||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { selectFile } from '@/scripts/select-file.js';
|
import { selectFile } from '@/scripts/select-file.js';
|
||||||
import { mainRouter } from '@/router.js';
|
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
|
import { mainRouter } from '@/global/router/main.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
initPageId?: string;
|
initPageId?: string;
|
||||||
|
@@ -40,9 +40,9 @@ import { computed, ref } from 'vue';
|
|||||||
import MkPagePreview from '@/components/MkPagePreview.vue';
|
import MkPagePreview from '@/components/MkPagePreview.vue';
|
||||||
import MkPagination from '@/components/MkPagination.vue';
|
import MkPagination from '@/components/MkPagination.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { useRouter } from '@/router.js';
|
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
|
import { useRouter } from '@/global/router/supplier.js';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@@ -25,8 +25,8 @@ import MkInput from '@/components/MkInput.vue';
|
|||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { mainRouter } from '@/router.js';
|
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
|
import { mainRouter } from '@/global/router/main.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
token?: string;
|
token?: string;
|
||||||
|
@@ -51,8 +51,8 @@ import { i18n } from '@/i18n.js';
|
|||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||||
import { useRouter } from '@/router.js';
|
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
|
import { useRouter } from '@/global/router/supplier.js';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@@ -34,7 +34,7 @@ import { i18n } from '@/i18n.js';
|
|||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { useRouter } from '@/router.js';
|
import { useRouter } from '@/global/router/supplier.js';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@@ -35,9 +35,9 @@ import MkSuperMenu from '@/components/MkSuperMenu.vue';
|
|||||||
import { signout, $i } from '@/account.js';
|
import { signout, $i } from '@/account.js';
|
||||||
import { clearCache } from '@/scripts/clear-cache.js';
|
import { clearCache } from '@/scripts/clear-cache.js';
|
||||||
import { instance } from '@/instance.js';
|
import { instance } from '@/instance.js';
|
||||||
import { useRouter } from '@/router.js';
|
|
||||||
import { PageMetadata, definePageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
|
import { PageMetadata, definePageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
import { useRouter } from '@/global/router/supplier.js';
|
||||||
|
|
||||||
const indexInfo = {
|
const indexInfo = {
|
||||||
title: i18n.ts.settings,
|
title: i18n.ts.settings,
|
||||||
|
@@ -51,7 +51,7 @@ import * as os from '@/os.js';
|
|||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { useRouter } from '@/router.js';
|
import { useRouter } from '@/global/router/supplier.js';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@@ -29,9 +29,9 @@ import * as Misskey from 'misskey-js';
|
|||||||
import MkTimeline from '@/components/MkTimeline.vue';
|
import MkTimeline from '@/components/MkTimeline.vue';
|
||||||
import { scroll } from '@/scripts/scroll.js';
|
import { scroll } from '@/scripts/scroll.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { useRouter } from '@/router.js';
|
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { useRouter } from '@/global/router/supplier.js';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@@ -166,13 +166,13 @@ import { getUserMenu } from '@/scripts/get-user-menu.js';
|
|||||||
import number from '@/filters/number.js';
|
import number from '@/filters/number.js';
|
||||||
import { userPage } from '@/filters/user.js';
|
import { userPage } from '@/filters/user.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { useRouter } from '@/router.js';
|
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { $i, iAmModerator } from '@/account.js';
|
import { $i, iAmModerator } from '@/account.js';
|
||||||
import { dateString } from '@/filters/date.js';
|
import { dateString } from '@/filters/date.js';
|
||||||
import { confetti } from '@/scripts/confetti.js';
|
import { confetti } from '@/scripts/confetti.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
|
import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
|
||||||
|
import { useRouter } from '@/global/router/supplier.js';
|
||||||
|
|
||||||
function calcAge(birthdate: string): number {
|
function calcAge(birthdate: string): number {
|
||||||
const date = new Date(birthdate);
|
const date = new Date(birthdate);
|
||||||
|
@@ -1,561 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { AsyncComponentLoader, defineAsyncComponent, inject } from 'vue';
|
|
||||||
import { Router } from '@/nirax.js';
|
|
||||||
import { $i, iAmModerator } from '@/account.js';
|
|
||||||
import MkLoading from '@/pages/_loading_.vue';
|
|
||||||
import MkError from '@/pages/_error_.vue';
|
|
||||||
|
|
||||||
export const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({
|
|
||||||
loader: loader,
|
|
||||||
loadingComponent: MkLoading,
|
|
||||||
errorComponent: MkError,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const routes = [{
|
|
||||||
path: '/@:initUser/pages/:initPageName/view-source',
|
|
||||||
component: page(() => import('./pages/page-editor/page-editor.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/@:username/pages/:pageName',
|
|
||||||
component: page(() => import('./pages/page.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/@:acct/following',
|
|
||||||
component: page(() => import('./pages/user/following.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/@:acct/followers',
|
|
||||||
component: page(() => import('./pages/user/followers.vue')),
|
|
||||||
}, {
|
|
||||||
name: 'user',
|
|
||||||
path: '/@:acct/:page?',
|
|
||||||
component: page(() => import('./pages/user/index.vue')),
|
|
||||||
}, {
|
|
||||||
name: 'note',
|
|
||||||
path: '/notes/:noteId',
|
|
||||||
component: page(() => import('./pages/note.vue')),
|
|
||||||
}, {
|
|
||||||
name: 'list',
|
|
||||||
path: '/list/:listId',
|
|
||||||
component: page(() => import('./pages/list.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/clips/:clipId',
|
|
||||||
component: page(() => import('./pages/clip.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/instance-info/:host',
|
|
||||||
component: page(() => import('./pages/instance-info.vue')),
|
|
||||||
}, {
|
|
||||||
name: 'settings',
|
|
||||||
path: '/settings',
|
|
||||||
component: page(() => import('./pages/settings/index.vue')),
|
|
||||||
loginRequired: true,
|
|
||||||
children: [{
|
|
||||||
path: '/profile',
|
|
||||||
name: 'profile',
|
|
||||||
component: page(() => import('./pages/settings/profile.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/avatar-decoration',
|
|
||||||
name: 'avatarDecoration',
|
|
||||||
component: page(() => import('./pages/settings/avatar-decoration.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/roles',
|
|
||||||
name: 'roles',
|
|
||||||
component: page(() => import('./pages/settings/roles.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/privacy',
|
|
||||||
name: 'privacy',
|
|
||||||
component: page(() => import('./pages/settings/privacy.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/emoji-picker',
|
|
||||||
name: 'emojiPicker',
|
|
||||||
component: page(() => import('./pages/settings/emoji-picker.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/drive',
|
|
||||||
name: 'drive',
|
|
||||||
component: page(() => import('./pages/settings/drive.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/drive/cleaner',
|
|
||||||
name: 'drive',
|
|
||||||
component: page(() => import('./pages/settings/drive-cleaner.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/notifications',
|
|
||||||
name: 'notifications',
|
|
||||||
component: page(() => import('./pages/settings/notifications.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/email',
|
|
||||||
name: 'email',
|
|
||||||
component: page(() => import('./pages/settings/email.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/security',
|
|
||||||
name: 'security',
|
|
||||||
component: page(() => import('./pages/settings/security.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/general',
|
|
||||||
name: 'general',
|
|
||||||
component: page(() => import('./pages/settings/general.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/theme/install',
|
|
||||||
name: 'theme',
|
|
||||||
component: page(() => import('./pages/settings/theme.install.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/theme/manage',
|
|
||||||
name: 'theme',
|
|
||||||
component: page(() => import('./pages/settings/theme.manage.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/theme',
|
|
||||||
name: 'theme',
|
|
||||||
component: page(() => import('./pages/settings/theme.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/navbar',
|
|
||||||
name: 'navbar',
|
|
||||||
component: page(() => import('./pages/settings/navbar.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/statusbar',
|
|
||||||
name: 'statusbar',
|
|
||||||
component: page(() => import('./pages/settings/statusbar.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/sounds',
|
|
||||||
name: 'sounds',
|
|
||||||
component: page(() => import('./pages/settings/sounds.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/plugin/install',
|
|
||||||
name: 'plugin',
|
|
||||||
component: page(() => import('./pages/settings/plugin.install.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/plugin',
|
|
||||||
name: 'plugin',
|
|
||||||
component: page(() => import('./pages/settings/plugin.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/import-export',
|
|
||||||
name: 'import-export',
|
|
||||||
component: page(() => import('./pages/settings/import-export.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/mute-block',
|
|
||||||
name: 'mute-block',
|
|
||||||
component: page(() => import('./pages/settings/mute-block.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/api',
|
|
||||||
name: 'api',
|
|
||||||
component: page(() => import('./pages/settings/api.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/apps',
|
|
||||||
name: 'api',
|
|
||||||
component: page(() => import('./pages/settings/apps.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/webhook/edit/:webhookId',
|
|
||||||
name: 'webhook',
|
|
||||||
component: page(() => import('./pages/settings/webhook.edit.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/webhook/new',
|
|
||||||
name: 'webhook',
|
|
||||||
component: page(() => import('./pages/settings/webhook.new.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/webhook',
|
|
||||||
name: 'webhook',
|
|
||||||
component: page(() => import('./pages/settings/webhook.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/deck',
|
|
||||||
name: 'deck',
|
|
||||||
component: page(() => import('./pages/settings/deck.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/preferences-backups',
|
|
||||||
name: 'preferences-backups',
|
|
||||||
component: page(() => import('./pages/settings/preferences-backups.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/migration',
|
|
||||||
name: 'migration',
|
|
||||||
component: page(() => import('./pages/settings/migration.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/custom-css',
|
|
||||||
name: 'general',
|
|
||||||
component: page(() => import('./pages/settings/custom-css.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/accounts',
|
|
||||||
name: 'profile',
|
|
||||||
component: page(() => import('./pages/settings/accounts.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/other',
|
|
||||||
name: 'other',
|
|
||||||
component: page(() => import('./pages/settings/other.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/',
|
|
||||||
component: page(() => import('./pages/_empty_.vue')),
|
|
||||||
}],
|
|
||||||
}, {
|
|
||||||
path: '/reset-password/:token?',
|
|
||||||
component: page(() => import('./pages/reset-password.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/signup-complete/:code',
|
|
||||||
component: page(() => import('./pages/signup-complete.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/announcements',
|
|
||||||
component: page(() => import('./pages/announcements.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/about',
|
|
||||||
component: page(() => import('./pages/about.vue')),
|
|
||||||
hash: 'initialTab',
|
|
||||||
}, {
|
|
||||||
path: '/about-misskey',
|
|
||||||
component: page(() => import('./pages/about-misskey.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/invite',
|
|
||||||
name: 'invite',
|
|
||||||
component: page(() => import('./pages/invite.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/ads',
|
|
||||||
component: page(() => import('./pages/ads.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/theme-editor',
|
|
||||||
component: page(() => import('./pages/theme-editor.vue')),
|
|
||||||
loginRequired: true,
|
|
||||||
}, {
|
|
||||||
path: '/roles/:role',
|
|
||||||
component: page(() => import('./pages/role.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/user-tags/:tag',
|
|
||||||
component: page(() => import('./pages/user-tag.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/explore',
|
|
||||||
component: page(() => import('./pages/explore.vue')),
|
|
||||||
hash: 'initialTab',
|
|
||||||
}, {
|
|
||||||
path: '/search',
|
|
||||||
component: page(() => import('./pages/search.vue')),
|
|
||||||
query: {
|
|
||||||
q: 'query',
|
|
||||||
channel: 'channel',
|
|
||||||
type: 'type',
|
|
||||||
origin: 'origin',
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
path: '/authorize-follow',
|
|
||||||
component: page(() => import('./pages/follow.vue')),
|
|
||||||
loginRequired: true,
|
|
||||||
}, {
|
|
||||||
path: '/share',
|
|
||||||
component: page(() => import('./pages/share.vue')),
|
|
||||||
loginRequired: true,
|
|
||||||
}, {
|
|
||||||
path: '/api-console',
|
|
||||||
component: page(() => import('./pages/api-console.vue')),
|
|
||||||
loginRequired: true,
|
|
||||||
}, {
|
|
||||||
path: '/scratchpad',
|
|
||||||
component: page(() => import('./pages/scratchpad.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/auth/:token',
|
|
||||||
component: page(() => import('./pages/auth.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/miauth/:session',
|
|
||||||
component: page(() => import('./pages/miauth.vue')),
|
|
||||||
query: {
|
|
||||||
callback: 'callback',
|
|
||||||
name: 'name',
|
|
||||||
icon: 'icon',
|
|
||||||
permission: 'permission',
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
path: '/oauth/authorize',
|
|
||||||
component: page(() => import('./pages/oauth.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/tags/:tag',
|
|
||||||
component: page(() => import('./pages/tag.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/pages/new',
|
|
||||||
component: page(() => import('./pages/page-editor/page-editor.vue')),
|
|
||||||
loginRequired: true,
|
|
||||||
}, {
|
|
||||||
path: '/pages/edit/:initPageId',
|
|
||||||
component: page(() => import('./pages/page-editor/page-editor.vue')),
|
|
||||||
loginRequired: true,
|
|
||||||
}, {
|
|
||||||
path: '/pages',
|
|
||||||
component: page(() => import('./pages/pages.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/play/:id/edit',
|
|
||||||
component: page(() => import('./pages/flash/flash-edit.vue')),
|
|
||||||
loginRequired: true,
|
|
||||||
}, {
|
|
||||||
path: '/play/new',
|
|
||||||
component: page(() => import('./pages/flash/flash-edit.vue')),
|
|
||||||
loginRequired: true,
|
|
||||||
}, {
|
|
||||||
path: '/play/:id',
|
|
||||||
component: page(() => import('./pages/flash/flash.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/play',
|
|
||||||
component: page(() => import('./pages/flash/flash-index.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/gallery/:postId/edit',
|
|
||||||
component: page(() => import('./pages/gallery/edit.vue')),
|
|
||||||
loginRequired: true,
|
|
||||||
}, {
|
|
||||||
path: '/gallery/new',
|
|
||||||
component: page(() => import('./pages/gallery/edit.vue')),
|
|
||||||
loginRequired: true,
|
|
||||||
}, {
|
|
||||||
path: '/gallery/:postId',
|
|
||||||
component: page(() => import('./pages/gallery/post.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/gallery',
|
|
||||||
component: page(() => import('./pages/gallery/index.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/channels/:channelId/edit',
|
|
||||||
component: page(() => import('./pages/channel-editor.vue')),
|
|
||||||
loginRequired: true,
|
|
||||||
}, {
|
|
||||||
path: '/channels/new',
|
|
||||||
component: page(() => import('./pages/channel-editor.vue')),
|
|
||||||
loginRequired: true,
|
|
||||||
}, {
|
|
||||||
path: '/channels/:channelId',
|
|
||||||
component: page(() => import('./pages/channel.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/channels',
|
|
||||||
component: page(() => import('./pages/channels.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/custom-emojis-manager',
|
|
||||||
component: page(() => import('./pages/custom-emojis-manager.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/avatar-decorations',
|
|
||||||
name: 'avatarDecorations',
|
|
||||||
component: page(() => import('./pages/avatar-decorations.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/registry/keys/:domain/:path(*)?',
|
|
||||||
component: page(() => import('./pages/registry.keys.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/registry/value/:domain/:path(*)?',
|
|
||||||
component: page(() => import('./pages/registry.value.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/registry',
|
|
||||||
component: page(() => import('./pages/registry.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/install-extentions',
|
|
||||||
component: page(() => import('./pages/install-extentions.vue')),
|
|
||||||
loginRequired: true,
|
|
||||||
}, {
|
|
||||||
path: '/admin/user/:userId',
|
|
||||||
component: iAmModerator ? page(() => import('./pages/admin-user.vue')) : page(() => import('./pages/not-found.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/admin/file/:fileId',
|
|
||||||
component: iAmModerator ? page(() => import('./pages/admin-file.vue')) : page(() => import('./pages/not-found.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/admin',
|
|
||||||
component: iAmModerator ? page(() => import('./pages/admin/index.vue')) : page(() => import('./pages/not-found.vue')),
|
|
||||||
children: [{
|
|
||||||
path: '/overview',
|
|
||||||
name: 'overview',
|
|
||||||
component: page(() => import('./pages/admin/overview.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/users',
|
|
||||||
name: 'users',
|
|
||||||
component: page(() => import('./pages/admin/users.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/emojis',
|
|
||||||
name: 'emojis',
|
|
||||||
component: page(() => import('./pages/custom-emojis-manager.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/avatar-decorations',
|
|
||||||
name: 'avatarDecorations',
|
|
||||||
component: page(() => import('./pages/avatar-decorations.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/queue',
|
|
||||||
name: 'queue',
|
|
||||||
component: page(() => import('./pages/admin/queue.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/files',
|
|
||||||
name: 'files',
|
|
||||||
component: page(() => import('./pages/admin/files.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/federation',
|
|
||||||
name: 'federation',
|
|
||||||
component: page(() => import('./pages/admin/federation.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/announcements',
|
|
||||||
name: 'announcements',
|
|
||||||
component: page(() => import('./pages/admin/announcements.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/ads',
|
|
||||||
name: 'ads',
|
|
||||||
component: page(() => import('./pages/admin/ads.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/roles/:id/edit',
|
|
||||||
name: 'roles',
|
|
||||||
component: page(() => import('./pages/admin/roles.edit.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/roles/new',
|
|
||||||
name: 'roles',
|
|
||||||
component: page(() => import('./pages/admin/roles.edit.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/roles/:id',
|
|
||||||
name: 'roles',
|
|
||||||
component: page(() => import('./pages/admin/roles.role.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/roles',
|
|
||||||
name: 'roles',
|
|
||||||
component: page(() => import('./pages/admin/roles.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/database',
|
|
||||||
name: 'database',
|
|
||||||
component: page(() => import('./pages/admin/database.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/abuses',
|
|
||||||
name: 'abuses',
|
|
||||||
component: page(() => import('./pages/admin/abuses.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/modlog',
|
|
||||||
name: 'modlog',
|
|
||||||
component: page(() => import('./pages/admin/modlog.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/settings',
|
|
||||||
name: 'settings',
|
|
||||||
component: page(() => import('./pages/admin/settings.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/branding',
|
|
||||||
name: 'branding',
|
|
||||||
component: page(() => import('./pages/admin/branding.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/moderation',
|
|
||||||
name: 'moderation',
|
|
||||||
component: page(() => import('./pages/admin/moderation.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/email-settings',
|
|
||||||
name: 'email-settings',
|
|
||||||
component: page(() => import('./pages/admin/email-settings.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/object-storage',
|
|
||||||
name: 'object-storage',
|
|
||||||
component: page(() => import('./pages/admin/object-storage.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/security',
|
|
||||||
name: 'security',
|
|
||||||
component: page(() => import('./pages/admin/security.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/relays',
|
|
||||||
name: 'relays',
|
|
||||||
component: page(() => import('./pages/admin/relays.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/instance-block',
|
|
||||||
name: 'instance-block',
|
|
||||||
component: page(() => import('./pages/admin/instance-block.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/proxy-account',
|
|
||||||
name: 'proxy-account',
|
|
||||||
component: page(() => import('./pages/admin/proxy-account.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/external-services',
|
|
||||||
name: 'external-services',
|
|
||||||
component: page(() => import('./pages/admin/external-services.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/other-settings',
|
|
||||||
name: 'other-settings',
|
|
||||||
component: page(() => import('./pages/admin/other-settings.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/server-rules',
|
|
||||||
name: 'server-rules',
|
|
||||||
component: page(() => import('./pages/admin/server-rules.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/invites',
|
|
||||||
name: 'invites',
|
|
||||||
component: page(() => import('./pages/admin/invites.vue')),
|
|
||||||
}, {
|
|
||||||
path: '/',
|
|
||||||
component: page(() => import('./pages/_empty_.vue')),
|
|
||||||
}],
|
|
||||||
}, {
|
|
||||||
path: '/my/notifications',
|
|
||||||
component: page(() => import('./pages/notifications.vue')),
|
|
||||||
loginRequired: true,
|
|
||||||
}, {
|
|
||||||
path: '/my/favorites',
|
|
||||||
component: page(() => import('./pages/favorites.vue')),
|
|
||||||
loginRequired: true,
|
|
||||||
}, {
|
|
||||||
path: '/my/achievements',
|
|
||||||
component: page(() => import('./pages/achievements.vue')),
|
|
||||||
loginRequired: true,
|
|
||||||
}, {
|
|
||||||
path: '/my/drive/folder/:folder',
|
|
||||||
component: page(() => import('./pages/drive.vue')),
|
|
||||||
loginRequired: true,
|
|
||||||
}, {
|
|
||||||
path: '/my/drive',
|
|
||||||
component: page(() => import('./pages/drive.vue')),
|
|
||||||
loginRequired: true,
|
|
||||||
}, {
|
|
||||||
path: '/my/drive/file/:fileId',
|
|
||||||
component: page(() => import('./pages/drive.file.vue')),
|
|
||||||
loginRequired: true,
|
|
||||||
}, {
|
|
||||||
path: '/my/follow-requests',
|
|
||||||
component: page(() => import('./pages/follow-requests.vue')),
|
|
||||||
loginRequired: true,
|
|
||||||
}, {
|
|
||||||
path: '/my/lists/:listId',
|
|
||||||
component: page(() => import('./pages/my-lists/list.vue')),
|
|
||||||
loginRequired: true,
|
|
||||||
}, {
|
|
||||||
path: '/my/lists',
|
|
||||||
component: page(() => import('./pages/my-lists/index.vue')),
|
|
||||||
loginRequired: true,
|
|
||||||
}, {
|
|
||||||
path: '/my/clips',
|
|
||||||
component: page(() => import('./pages/my-clips/index.vue')),
|
|
||||||
loginRequired: true,
|
|
||||||
}, {
|
|
||||||
path: '/my/antennas/create',
|
|
||||||
component: page(() => import('./pages/my-antennas/create.vue')),
|
|
||||||
loginRequired: true,
|
|
||||||
}, {
|
|
||||||
path: '/my/antennas/:antennaId',
|
|
||||||
component: page(() => import('./pages/my-antennas/edit.vue')),
|
|
||||||
loginRequired: true,
|
|
||||||
}, {
|
|
||||||
path: '/my/antennas',
|
|
||||||
component: page(() => import('./pages/my-antennas/index.vue')),
|
|
||||||
loginRequired: true,
|
|
||||||
}, {
|
|
||||||
path: '/timeline/list/:listId',
|
|
||||||
component: page(() => import('./pages/user-list-timeline.vue')),
|
|
||||||
loginRequired: true,
|
|
||||||
}, {
|
|
||||||
path: '/timeline/antenna/:antennaId',
|
|
||||||
component: page(() => import('./pages/antenna-timeline.vue')),
|
|
||||||
loginRequired: true,
|
|
||||||
}, {
|
|
||||||
path: '/clicker',
|
|
||||||
component: page(() => import('./pages/clicker.vue')),
|
|
||||||
loginRequired: true,
|
|
||||||
}, {
|
|
||||||
path: '/bubble-game',
|
|
||||||
component: page(() => import('./pages/drop-and-fusion.vue')),
|
|
||||||
loginRequired: true,
|
|
||||||
}, {
|
|
||||||
path: '/timeline',
|
|
||||||
component: page(() => import('./pages/timeline.vue')),
|
|
||||||
}, {
|
|
||||||
name: 'index',
|
|
||||||
path: '/',
|
|
||||||
component: $i ? page(() => import('./pages/timeline.vue')) : page(() => import('./pages/welcome.vue')),
|
|
||||||
globalCacheKey: 'index',
|
|
||||||
}, {
|
|
||||||
path: '/:(*)',
|
|
||||||
component: page(() => import('./pages/not-found.vue')),
|
|
||||||
}];
|
|
||||||
|
|
||||||
export const mainRouter = new Router(routes, location.pathname + location.search + location.hash, !!$i, page(() => import('@/pages/not-found.vue')));
|
|
||||||
|
|
||||||
window.history.replaceState({ key: mainRouter.getCurrentKey() }, '', location.href);
|
|
||||||
|
|
||||||
mainRouter.addListener('push', ctx => {
|
|
||||||
window.history.pushState({ key: ctx.key }, '', ctx.path);
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener('popstate', (event) => {
|
|
||||||
mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key);
|
|
||||||
});
|
|
||||||
|
|
||||||
export function useRouter(): Router {
|
|
||||||
return inject<Router | null>('router', null) ?? mainRouter;
|
|
||||||
}
|
|
@@ -83,6 +83,8 @@ export const ACHIEVEMENT_TYPES = [
|
|||||||
'brainDiver',
|
'brainDiver',
|
||||||
'smashTestNotificationButton',
|
'smashTestNotificationButton',
|
||||||
'tutorialCompleted',
|
'tutorialCompleted',
|
||||||
|
'bubbleGameExplodingHead',
|
||||||
|
'bubbleGameDoubleExplodingHead',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const ACHIEVEMENT_BADGES = {
|
export const ACHIEVEMENT_BADGES = {
|
||||||
@@ -466,6 +468,16 @@ export const ACHIEVEMENT_BADGES = {
|
|||||||
bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
|
bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
|
||||||
frame: 'bronze',
|
frame: 'bronze',
|
||||||
},
|
},
|
||||||
|
'bubbleGameExplodingHead': {
|
||||||
|
img: '/fluent-emoji/1f92f.png',
|
||||||
|
bg: 'linear-gradient(0deg, rgb(255 77 77), rgb(247 155 214))',
|
||||||
|
frame: 'bronze',
|
||||||
|
},
|
||||||
|
'bubbleGameDoubleExplodingHead': {
|
||||||
|
img: '/fluent-emoji/1f92f.png',
|
||||||
|
bg: 'linear-gradient(0deg, rgb(255 77 77), rgb(247 155 214))',
|
||||||
|
frame: 'silver',
|
||||||
|
},
|
||||||
/* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107>
|
/* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107>
|
||||||
} as const satisfies Record<typeof ACHIEVEMENT_TYPES[number], {
|
} as const satisfies Record<typeof ACHIEVEMENT_TYPES[number], {
|
||||||
img: string;
|
img: string;
|
||||||
|
400
packages/frontend/src/scripts/drop-and-fusion-engine.ts
Normal file
400
packages/frontend/src/scripts/drop-and-fusion-engine.ts
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EventEmitter } from 'eventemitter3';
|
||||||
|
import * as Matter from 'matter-js';
|
||||||
|
import * as sound from '@/scripts/sound.js';
|
||||||
|
|
||||||
|
export type Mono = {
|
||||||
|
id: string;
|
||||||
|
level: number;
|
||||||
|
size: number;
|
||||||
|
shape: 'circle' | 'rectangle';
|
||||||
|
score: number;
|
||||||
|
dropCandidate: boolean;
|
||||||
|
sfxPitch: number;
|
||||||
|
img: string;
|
||||||
|
imgSize: number;
|
||||||
|
spriteScale: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PHYSICS_QUALITY_FACTOR = 16; // 低いほどパフォーマンスが高いがガタガタして安定しなくなる、逆に高すぎても何故か不安定になる
|
||||||
|
|
||||||
|
export class DropAndFusionGame extends EventEmitter<{
|
||||||
|
changeScore: (newScore: number) => void;
|
||||||
|
changeCombo: (newCombo: number) => void;
|
||||||
|
changeStock: (newStock: { id: string; mono: Mono }[]) => void;
|
||||||
|
dropped: () => void;
|
||||||
|
fusioned: (x: number, y: number, scoreDelta: number) => void;
|
||||||
|
monoAdded: (mono: Mono) => void;
|
||||||
|
gameOver: () => void;
|
||||||
|
}> {
|
||||||
|
private COMBO_INTERVAL = 1000;
|
||||||
|
public readonly DROP_INTERVAL = 500;
|
||||||
|
public readonly PLAYAREA_MARGIN = 25;
|
||||||
|
private STOCK_MAX = 4;
|
||||||
|
private loaded = false;
|
||||||
|
private engine: Matter.Engine;
|
||||||
|
private render: Matter.Render;
|
||||||
|
private runner: Matter.Runner;
|
||||||
|
private overflowCollider: Matter.Body;
|
||||||
|
private isGameOver = false;
|
||||||
|
|
||||||
|
private gameWidth: number;
|
||||||
|
private gameHeight: number;
|
||||||
|
private monoDefinitions: Mono[] = [];
|
||||||
|
private monoTextures: Record<string, Blob> = {};
|
||||||
|
private monoTextureUrls: Record<string, string> = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* フィールドに出ていて、かつ合体の対象となるアイテム
|
||||||
|
*/
|
||||||
|
private activeBodyIds: Matter.Body['id'][] = [];
|
||||||
|
|
||||||
|
private latestDroppedBodyId: Matter.Body['id'] | null = null;
|
||||||
|
|
||||||
|
private latestDroppedAt = 0;
|
||||||
|
private latestFusionedAt = 0;
|
||||||
|
private stock: { id: string; mono: Mono }[] = [];
|
||||||
|
|
||||||
|
private _combo = 0;
|
||||||
|
private get combo() {
|
||||||
|
return this._combo;
|
||||||
|
}
|
||||||
|
private set combo(value: number) {
|
||||||
|
this._combo = value;
|
||||||
|
this.emit('changeCombo', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _score = 0;
|
||||||
|
private get score() {
|
||||||
|
return this._score;
|
||||||
|
}
|
||||||
|
private set score(value: number) {
|
||||||
|
this._score = value;
|
||||||
|
this.emit('changeScore', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private comboIntervalId: number | null = null;
|
||||||
|
|
||||||
|
constructor(opts: {
|
||||||
|
canvas: HTMLCanvasElement;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
monoDefinitions: Mono[];
|
||||||
|
}) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.gameWidth = opts.width;
|
||||||
|
this.gameHeight = opts.height;
|
||||||
|
this.monoDefinitions = opts.monoDefinitions;
|
||||||
|
|
||||||
|
this.engine = Matter.Engine.create({
|
||||||
|
constraintIterations: 2 * PHYSICS_QUALITY_FACTOR,
|
||||||
|
positionIterations: 6 * PHYSICS_QUALITY_FACTOR,
|
||||||
|
velocityIterations: 4 * PHYSICS_QUALITY_FACTOR,
|
||||||
|
gravity: {
|
||||||
|
x: 0,
|
||||||
|
y: 1,
|
||||||
|
},
|
||||||
|
timing: {
|
||||||
|
timeScale: 2,
|
||||||
|
},
|
||||||
|
enableSleeping: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.render = Matter.Render.create({
|
||||||
|
engine: this.engine,
|
||||||
|
canvas: opts.canvas,
|
||||||
|
options: {
|
||||||
|
width: this.gameWidth,
|
||||||
|
height: this.gameHeight,
|
||||||
|
background: 'transparent', // transparent to hide
|
||||||
|
wireframeBackground: 'transparent', // transparent to hide
|
||||||
|
wireframes: false,
|
||||||
|
showSleeping: false,
|
||||||
|
pixelRatio: Math.max(2, window.devicePixelRatio),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Matter.Render.run(this.render);
|
||||||
|
|
||||||
|
this.runner = Matter.Runner.create();
|
||||||
|
Matter.Runner.run(this.runner, this.engine);
|
||||||
|
|
||||||
|
this.engine.world.bodies = [];
|
||||||
|
|
||||||
|
//#region walls
|
||||||
|
const WALL_OPTIONS: Matter.IChamferableBodyDefinition = {
|
||||||
|
isStatic: true,
|
||||||
|
friction: 0.7,
|
||||||
|
slop: 1.0,
|
||||||
|
render: {
|
||||||
|
strokeStyle: 'transparent',
|
||||||
|
fillStyle: 'transparent',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const thickness = 100;
|
||||||
|
Matter.Composite.add(this.engine.world, [
|
||||||
|
Matter.Bodies.rectangle(this.gameWidth / 2, this.gameHeight + (thickness / 2) - this.PLAYAREA_MARGIN, this.gameWidth, thickness, WALL_OPTIONS),
|
||||||
|
Matter.Bodies.rectangle(this.gameWidth + (thickness / 2) - this.PLAYAREA_MARGIN, this.gameHeight / 2, thickness, this.gameHeight, WALL_OPTIONS),
|
||||||
|
Matter.Bodies.rectangle(-((thickness / 2) - this.PLAYAREA_MARGIN), this.gameHeight / 2, thickness, this.gameHeight, WALL_OPTIONS),
|
||||||
|
]);
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
this.overflowCollider = Matter.Bodies.rectangle(this.gameWidth / 2, 0, this.gameWidth, 200, {
|
||||||
|
isStatic: true,
|
||||||
|
isSensor: true,
|
||||||
|
render: {
|
||||||
|
strokeStyle: 'transparent',
|
||||||
|
fillStyle: 'transparent',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
Matter.Composite.add(this.engine.world, this.overflowCollider);
|
||||||
|
|
||||||
|
// fit the render viewport to the scene
|
||||||
|
Matter.Render.lookAt(this.render, {
|
||||||
|
min: { x: 0, y: 0 },
|
||||||
|
max: { x: this.gameWidth, y: this.gameHeight },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private createBody(mono: Mono, x: number, y: number) {
|
||||||
|
const options: Matter.IBodyDefinition = {
|
||||||
|
label: mono.id,
|
||||||
|
//density: 0.0005,
|
||||||
|
density: mono.size / 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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (mono.shape === 'circle') {
|
||||||
|
return Matter.Bodies.circle(x, y, mono.size / 2, options);
|
||||||
|
} else if (mono.shape === 'rectangle') {
|
||||||
|
return Matter.Bodies.rectangle(x, y, mono.size, mono.size, options);
|
||||||
|
} else {
|
||||||
|
throw new Error('unrecognized shape');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fusion(bodyA: Matter.Body, bodyB: Matter.Body) {
|
||||||
|
const now = Date.now();
|
||||||
|
if (this.latestFusionedAt > now - this.COMBO_INTERVAL) {
|
||||||
|
this.combo++;
|
||||||
|
} else {
|
||||||
|
this.combo = 1;
|
||||||
|
}
|
||||||
|
this.latestFusionedAt = now;
|
||||||
|
|
||||||
|
// TODO: 単に位置だけでなくそれぞれの動きベクトルも融合する?
|
||||||
|
const newX = (bodyA.position.x + bodyB.position.x) / 2;
|
||||||
|
const newY = (bodyA.position.y + bodyB.position.y) / 2;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (nextMono) {
|
||||||
|
const body = this.createBody(nextMono, newX, newY);
|
||||||
|
Matter.Composite.add(this.engine.world, body);
|
||||||
|
|
||||||
|
// 連鎖してfusionした場合の分かりやすさのため少し間を置いてからfusion対象になるようにする
|
||||||
|
window.setTimeout(() => {
|
||||||
|
this.activeBodyIds.push(body.id);
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
const comboBonus = 1 + ((this.combo - 1) / 5);
|
||||||
|
const additionalScore = Math.round(currentMono.score * comboBonus);
|
||||||
|
this.score += additionalScore;
|
||||||
|
|
||||||
|
// TODO: 効果音再生はコンポーネント側の責務なので移動する
|
||||||
|
const pan = ((newX / this.gameWidth) - 0.5) * 2;
|
||||||
|
sound.playUrl('/client-assets/drop-and-fusion/bubble2.mp3', 1, pan, nextMono.sfxPitch);
|
||||||
|
|
||||||
|
this.emit('monoAdded', nextMono);
|
||||||
|
this.emit('fusioned', newX, newY, additionalScore);
|
||||||
|
} else {
|
||||||
|
//const VELOCITY = 30;
|
||||||
|
//for (let i = 0; i < 10; i++) {
|
||||||
|
// const body = createBody(FRUITS.find(x => x.level === (1 + Math.floor(Math.random() * 3)))!, x + ((Math.random() * VELOCITY) - (VELOCITY / 2)), y + ((Math.random() * VELOCITY) - (VELOCITY / 2)));
|
||||||
|
// Matter.Composite.add(world, body);
|
||||||
|
// bodies.push(body);
|
||||||
|
//}
|
||||||
|
//sound.playUrl({
|
||||||
|
// type: 'syuilo/bubble2',
|
||||||
|
// volume: 1,
|
||||||
|
//});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private gameOver() {
|
||||||
|
this.isGameOver = true;
|
||||||
|
Matter.Runner.stop(this.runner);
|
||||||
|
this.emit('gameOver');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** テクスチャをすべてキャッシュする */
|
||||||
|
private async loadMonoTextures() {
|
||||||
|
async function loadSingleMonoTexture(mono: Mono, game: DropAndFusionGame) {
|
||||||
|
// Matter-js内にキャッシュがある場合はスキップ
|
||||||
|
if (game.render.textures[mono.img]) return;
|
||||||
|
console.log('loading', mono.img);
|
||||||
|
|
||||||
|
let src = mono.img;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
|
if (game.monoTextureUrls[mono.img]) {
|
||||||
|
src = game.monoTextureUrls[mono.img];
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
|
} else if (game.monoTextures[mono.img]) {
|
||||||
|
src = URL.createObjectURL(game.monoTextures[mono.img]);
|
||||||
|
game.monoTextureUrls[mono.img] = src;
|
||||||
|
} else {
|
||||||
|
const res = await fetch(mono.img);
|
||||||
|
const blob = await res.blob();
|
||||||
|
game.monoTextures[mono.img] = blob;
|
||||||
|
src = URL.createObjectURL(blob);
|
||||||
|
game.monoTextureUrls[mono.img] = src;
|
||||||
|
}
|
||||||
|
|
||||||
|
const image = new Image();
|
||||||
|
image.src = src;
|
||||||
|
game.render.textures[mono.img] = image;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(this.monoDefinitions.map(x => loadSingleMonoTexture(x, this)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public start() {
|
||||||
|
if (!this.loaded) throw new Error('game is not loaded yet');
|
||||||
|
|
||||||
|
for (let i = 0; i < this.STOCK_MAX; i++) {
|
||||||
|
this.stock.push({
|
||||||
|
id: Math.random().toString(),
|
||||||
|
mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(Math.random() * this.monoDefinitions.filter(x => x.dropCandidate).length)],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.emit('changeStock', this.stock);
|
||||||
|
|
||||||
|
// TODO: fusion予約状態のアイテムは光らせるなどの演出をすると楽しそう
|
||||||
|
let fusionReservedPairs: { bodyA: Matter.Body; bodyB: Matter.Body }[] = [];
|
||||||
|
|
||||||
|
const minCollisionEnergyForSound = 2.5;
|
||||||
|
const maxCollisionEnergyForSound = 9;
|
||||||
|
const soundPitchMax = 4;
|
||||||
|
const soundPitchMin = 0.5;
|
||||||
|
|
||||||
|
Matter.Events.on(this.engine, 'collisionStart', (event) => {
|
||||||
|
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) && !fusionReservedPairs.some(x => x.bodyA.id === bodyA.id || x.bodyA.id === bodyB.id || x.bodyB.id === bodyA.id || x.bodyB.id === bodyB.id);
|
||||||
|
if (shouldFusion) {
|
||||||
|
if (this.activeBodyIds.includes(bodyA.id) && this.activeBodyIds.includes(bodyB.id)) {
|
||||||
|
this.fusion(bodyA, bodyB);
|
||||||
|
} else {
|
||||||
|
fusionReservedPairs.push({ bodyA, bodyB });
|
||||||
|
window.setTimeout(() => {
|
||||||
|
fusionReservedPairs = fusionReservedPairs.filter(x => x.bodyA.id !== bodyA.id && x.bodyB.id !== bodyB.id);
|
||||||
|
this.fusion(bodyA, bodyB);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const energy = pairs.collision.depth;
|
||||||
|
if (energy > minCollisionEnergyForSound) {
|
||||||
|
// TODO: 効果音再生はコンポーネント側の責務なので移動する
|
||||||
|
const vol = (Math.min(maxCollisionEnergyForSound, energy - minCollisionEnergyForSound) / maxCollisionEnergyForSound) / 4;
|
||||||
|
const pan = ((((bodyA.position.x + bodyB.position.x) / 2) / this.gameWidth) - 0.5) * 2;
|
||||||
|
const pitch = soundPitchMin + ((soundPitchMax - soundPitchMin) * (1 - (Math.min(10, energy) / 10)));
|
||||||
|
sound.playUrl('/client-assets/drop-and-fusion/poi1.mp3', vol, pan, pitch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.comboIntervalId = window.setInterval(() => {
|
||||||
|
if (this.latestFusionedAt < Date.now() - this.COMBO_INTERVAL) {
|
||||||
|
this.combo = 0;
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async load() {
|
||||||
|
await this.loadMonoTextures();
|
||||||
|
this.loaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTextureImageUrl(mono: Mono) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
|
if (this.monoTextureUrls[mono.img]) {
|
||||||
|
return this.monoTextureUrls[mono.img];
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
|
} else if (this.monoTextures[mono.img]) {
|
||||||
|
// Gameクラス内にキャッシュがある場合はそれを使う
|
||||||
|
const out = URL.createObjectURL(this.monoTextures[mono.img]);
|
||||||
|
this.monoTextureUrls[mono.img] = out;
|
||||||
|
return out;
|
||||||
|
} else {
|
||||||
|
return mono.img;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getActiveMonos() {
|
||||||
|
return this.engine.world.bodies.map(x => this.monoDefinitions.find((mono) => mono.id === x.label)!).filter(x => x !== undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
public drop(_x: number) {
|
||||||
|
if (this.isGameOver) return;
|
||||||
|
if (Date.now() - this.latestDroppedAt < this.DROP_INTERVAL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const st = this.stock.shift()!;
|
||||||
|
this.stock.push({
|
||||||
|
id: Math.random().toString(),
|
||||||
|
mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(Math.random() * this.monoDefinitions.filter(x => x.dropCandidate).length)],
|
||||||
|
});
|
||||||
|
this.emit('changeStock', this.stock);
|
||||||
|
|
||||||
|
const x = Math.min(this.gameWidth - this.PLAYAREA_MARGIN - (st.mono.size / 2), Math.max(this.PLAYAREA_MARGIN + (st.mono.size / 2), _x));
|
||||||
|
const body = this.createBody(st.mono, x, 50 + st.mono.size / 2);
|
||||||
|
Matter.Composite.add(this.engine.world, body);
|
||||||
|
this.activeBodyIds.push(body.id);
|
||||||
|
this.latestDroppedBodyId = body.id;
|
||||||
|
this.latestDroppedAt = Date.now();
|
||||||
|
this.emit('dropped');
|
||||||
|
this.emit('monoAdded', st.mono);
|
||||||
|
|
||||||
|
// TODO: 効果音再生はコンポーネント側の責務なので移動する
|
||||||
|
const pan = ((x / this.gameWidth) - 0.5) * 2;
|
||||||
|
sound.playUrl('/client-assets/drop-and-fusion/poi2.mp3', 1, pan);
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose() {
|
||||||
|
if (this.comboIntervalId) window.clearInterval(this.comboIntervalId);
|
||||||
|
Matter.Render.stop(this.render);
|
||||||
|
Matter.Runner.stop(this.runner);
|
||||||
|
Matter.World.clear(this.engine.world, false);
|
||||||
|
Matter.Engine.clear(this.engine);
|
||||||
|
}
|
||||||
|
}
|
@@ -36,7 +36,8 @@ for (let i = 0; i < emojilist.length; i++) {
|
|||||||
export const emojiCharByCategory = _charGroupByCategory;
|
export const emojiCharByCategory = _charGroupByCategory;
|
||||||
|
|
||||||
export function getEmojiName(char: string): string | null {
|
export function getEmojiName(char: string): string | null {
|
||||||
const idx = _indexByChar.get(char);
|
// Colorize it because emojilist.json assumes that
|
||||||
|
const idx = _indexByChar.get(colorizeEmoji(char));
|
||||||
if (idx == null) {
|
if (idx == null) {
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
@@ -44,6 +45,10 @@ export function getEmojiName(char: string): string | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function colorizeEmoji(char: string) {
|
||||||
|
return char.length === 1 ? `${char}\uFE0F` : char;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CustomEmojiFolderTree {
|
export interface CustomEmojiFolderTree {
|
||||||
value: string;
|
value: string;
|
||||||
category: string;
|
category: string;
|
||||||
|
@@ -13,11 +13,11 @@ import * as os from '@/os.js';
|
|||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { defaultStore, userActions } from '@/store.js';
|
import { defaultStore, userActions } from '@/store.js';
|
||||||
import { $i, iAmModerator } from '@/account.js';
|
import { $i, iAmModerator } from '@/account.js';
|
||||||
import { mainRouter } from '@/router.js';
|
import { IRouter } from '@/nirax.js';
|
||||||
import { Router } from '@/nirax.js';
|
|
||||||
import { antennasCache, rolesCache, userListsCache } from '@/cache.js';
|
import { antennasCache, rolesCache, userListsCache } from '@/cache.js';
|
||||||
|
import { mainRouter } from '@/global/router/main.js';
|
||||||
|
|
||||||
export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router = mainRouter) {
|
export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter = mainRouter) {
|
||||||
const meId = $i ? $i.id : null;
|
const meId = $i ? $i.id : null;
|
||||||
|
|
||||||
const cleanups = [] as (() => void)[];
|
const cleanups = [] as (() => void)[];
|
||||||
|
@@ -6,8 +6,8 @@
|
|||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { mainRouter } from '@/router.js';
|
|
||||||
import { Router } from '@/nirax.js';
|
import { Router } from '@/nirax.js';
|
||||||
|
import { mainRouter } from '@/global/router/main.js';
|
||||||
|
|
||||||
export async function lookup(router?: Router) {
|
export async function lookup(router?: Router) {
|
||||||
const _router = router ?? mainRouter;
|
const _router = router ?? mainRouter;
|
||||||
|
@@ -10,12 +10,17 @@ import { $i } from '@/account.js';
|
|||||||
export const pendingApiRequestsCount = ref(0);
|
export const pendingApiRequestsCount = ref(0);
|
||||||
|
|
||||||
// Implements Misskey.api.ApiClient.request
|
// Implements Misskey.api.ApiClient.request
|
||||||
export function misskeyApi<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req']>(
|
export function misskeyApi<
|
||||||
|
ResT = void,
|
||||||
|
E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints,
|
||||||
|
P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req'],
|
||||||
|
_ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType<E, P> : ResT,
|
||||||
|
>(
|
||||||
endpoint: E,
|
endpoint: E,
|
||||||
data: P = {} as any,
|
data: P = {} as any,
|
||||||
token?: string | null | undefined,
|
token?: string | null | undefined,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
): Promise<Misskey.api.SwitchCaseResponseType<E, P>> {
|
): Promise<_ResT> {
|
||||||
if (endpoint.includes('://')) throw new Error('invalid endpoint');
|
if (endpoint.includes('://')) throw new Error('invalid endpoint');
|
||||||
pendingApiRequestsCount.value++;
|
pendingApiRequestsCount.value++;
|
||||||
|
|
||||||
@@ -23,7 +28,7 @@ export function misskeyApi<E extends keyof Misskey.Endpoints, P extends Misskey.
|
|||||||
pendingApiRequestsCount.value--;
|
pendingApiRequestsCount.value--;
|
||||||
};
|
};
|
||||||
|
|
||||||
const promise = new Promise<Misskey.Endpoints[E]['res'] | void>((resolve, reject) => {
|
const promise = new Promise<_ResT>((resolve, reject) => {
|
||||||
// Append a credential
|
// Append a credential
|
||||||
if ($i) (data as any).i = $i.token;
|
if ($i) (data as any).i = $i.token;
|
||||||
if (token !== undefined) (data as any).i = token;
|
if (token !== undefined) (data as any).i = token;
|
||||||
@@ -44,7 +49,7 @@ export function misskeyApi<E extends keyof Misskey.Endpoints, P extends Misskey.
|
|||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
resolve(body);
|
resolve(body);
|
||||||
} else if (res.status === 204) {
|
} else if (res.status === 204) {
|
||||||
resolve();
|
resolve(undefined as _ResT); // void -> undefined
|
||||||
} else {
|
} else {
|
||||||
reject(body.error);
|
reject(body.error);
|
||||||
}
|
}
|
||||||
@@ -57,10 +62,15 @@ export function misskeyApi<E extends keyof Misskey.Endpoints, P extends Misskey.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Implements Misskey.api.ApiClient.request
|
// Implements Misskey.api.ApiClient.request
|
||||||
export function misskeyApiGet<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req']>(
|
export function misskeyApiGet<
|
||||||
|
ResT = void,
|
||||||
|
E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints,
|
||||||
|
P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req'],
|
||||||
|
_ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType<E, P> : ResT,
|
||||||
|
>(
|
||||||
endpoint: E,
|
endpoint: E,
|
||||||
data: P = {} as any,
|
data: P = {} as any,
|
||||||
): Promise<Misskey.api.SwitchCaseResponseType<E, P>> {
|
): Promise<_ResT> {
|
||||||
pendingApiRequestsCount.value++;
|
pendingApiRequestsCount.value++;
|
||||||
|
|
||||||
const onFinally = () => {
|
const onFinally = () => {
|
||||||
@@ -69,7 +79,7 @@ export function misskeyApiGet<E extends keyof Misskey.Endpoints, P extends Missk
|
|||||||
|
|
||||||
const query = new URLSearchParams(data as any);
|
const query = new URLSearchParams(data as any);
|
||||||
|
|
||||||
const promise = new Promise<Misskey.Endpoints[E]['res'] | void>((resolve, reject) => {
|
const promise = new Promise<_ResT>((resolve, reject) => {
|
||||||
// Send request
|
// Send request
|
||||||
window.fetch(`${apiUrl}/${endpoint}?${query}`, {
|
window.fetch(`${apiUrl}/${endpoint}?${query}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -81,7 +91,7 @@ export function misskeyApiGet<E extends keyof Misskey.Endpoints, P extends Missk
|
|||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
resolve(body);
|
resolve(body);
|
||||||
} else if (res.status === 204) {
|
} else if (res.status === 204) {
|
||||||
resolve();
|
resolve(undefined as _ResT); // void -> undefined
|
||||||
} else {
|
} else {
|
||||||
reject(body.error);
|
reject(body.error);
|
||||||
}
|
}
|
||||||
|
@@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
import type { SoundStore } from '@/store.js';
|
import type { SoundStore } from '@/store.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
|
||||||
|
|
||||||
let ctx: AudioContext;
|
let ctx: AudioContext;
|
||||||
const cache = new Map<string, AudioBuffer>();
|
const cache = new Map<string, AudioBuffer>();
|
||||||
@@ -89,69 +88,35 @@ export type OperationType = typeof operationTypes[number];
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 音声を読み込む
|
* 音声を読み込む
|
||||||
* @param soundStore サウンド設定
|
* @param url url
|
||||||
* @param options `useCache`: デフォルトは`true` 一度再生した音声はキャッシュする
|
* @param options `useCache`: デフォルトは`true` 一度再生した音声はキャッシュする
|
||||||
*/
|
*/
|
||||||
export async function loadAudio(soundStore: {
|
export async function loadAudio(url: string, options?: { useCache?: boolean; }) {
|
||||||
type: Exclude<SoundType, '_driveFile_'>;
|
|
||||||
} | {
|
|
||||||
type: '_driveFile_';
|
|
||||||
fileId: string;
|
|
||||||
fileUrl: string;
|
|
||||||
}, options?: { useCache?: boolean; }) {
|
|
||||||
if (_DEV_) console.log('loading audio. opts:', options);
|
if (_DEV_) console.log('loading audio. opts:', options);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
||||||
if (ctx == null) {
|
if (ctx == null) {
|
||||||
ctx = new AudioContext();
|
ctx = new AudioContext();
|
||||||
}
|
}
|
||||||
if (options?.useCache ?? true) {
|
if (options?.useCache ?? true) {
|
||||||
if (soundStore.type === '_driveFile_' && cache.has(soundStore.fileId)) {
|
if (cache.has(url)) {
|
||||||
if (_DEV_) console.log('use cache');
|
if (_DEV_) console.log('use cache');
|
||||||
return cache.get(soundStore.fileId) as AudioBuffer;
|
return cache.get(url) as AudioBuffer;
|
||||||
} else if (cache.has(soundStore.type)) {
|
|
||||||
if (_DEV_) console.log('use cache');
|
|
||||||
return cache.get(soundStore.type) as AudioBuffer;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let response: Response;
|
let response: Response;
|
||||||
|
|
||||||
if (soundStore.type === '_driveFile_') {
|
try {
|
||||||
try {
|
response = await fetch(url);
|
||||||
response = await fetch(soundStore.fileUrl);
|
} catch (err) {
|
||||||
} catch (err) {
|
return;
|
||||||
try {
|
|
||||||
// URLが変わっている可能性があるのでドライブ側からURLを取得するフォールバック
|
|
||||||
const apiRes = await misskeyApi('drive/files/show', {
|
|
||||||
fileId: soundStore.fileId,
|
|
||||||
});
|
|
||||||
response = await fetch(apiRes.url);
|
|
||||||
} catch (fbErr) {
|
|
||||||
// それでも無理なら諦める
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
response = await fetch(`/client-assets/sounds/${soundStore.type}.mp3`);
|
|
||||||
} catch (err) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const arrayBuffer = await response.arrayBuffer();
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
const audioBuffer = await ctx.decodeAudioData(arrayBuffer);
|
const audioBuffer = await ctx.decodeAudioData(arrayBuffer);
|
||||||
|
|
||||||
if (options?.useCache ?? true) {
|
if (options?.useCache ?? true) {
|
||||||
if (soundStore.type === '_driveFile_') {
|
cache.set(url, audioBuffer);
|
||||||
cache.set(soundStore.fileId, audioBuffer);
|
|
||||||
} else {
|
|
||||||
cache.set(soundStore.type, audioBuffer);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return audioBuffer;
|
return audioBuffer;
|
||||||
@@ -180,18 +145,26 @@ export function play(operationType: OperationType) {
|
|||||||
* @param soundStore サウンド設定
|
* @param soundStore サウンド設定
|
||||||
*/
|
*/
|
||||||
export async function playFile(soundStore: SoundStore) {
|
export async function playFile(soundStore: SoundStore) {
|
||||||
const buffer = await loadAudio(soundStore);
|
if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = soundStore.type === '_driveFile_' ? soundStore.fileUrl : `/client-assets/sounds/${soundStore.type}.mp3`;
|
||||||
|
const buffer = await loadAudio(url);
|
||||||
if (!buffer) return;
|
if (!buffer) return;
|
||||||
createSourceNode(buffer, soundStore.volume)?.start();
|
createSourceNode(buffer, soundStore.volume)?.soundSource.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function playRaw(type: Exclude<SoundType, '_driveFile_'>, volume = 1, pan = 0, playbackRate = 1) {
|
export async function playUrl(url: string, volume = 1, pan = 0, playbackRate = 1) {
|
||||||
const buffer = await loadAudio({ type });
|
const buffer = await loadAudio(url);
|
||||||
if (!buffer) return;
|
if (!buffer) return;
|
||||||
createSourceNode(buffer, volume, pan, playbackRate)?.start();
|
createSourceNode(buffer, volume, pan, playbackRate)?.soundSource.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createSourceNode(buffer: AudioBuffer, volume: number, pan = 0, playbackRate = 1) : AudioBufferSourceNode | null {
|
export function createSourceNode(buffer: AudioBuffer, volume: number, pan = 0, playbackRate = 1): {
|
||||||
|
soundSource: AudioBufferSourceNode;
|
||||||
|
panNode: StereoPannerNode;
|
||||||
|
gainNode: GainNode;
|
||||||
|
} | null {
|
||||||
const masterVolume = defaultStore.state.sound_masterVolume;
|
const masterVolume = defaultStore.state.sound_masterVolume;
|
||||||
if (isMute() || masterVolume === 0 || volume === 0) {
|
if (isMute() || masterVolume === 0 || volume === 0) {
|
||||||
return null;
|
return null;
|
||||||
@@ -211,7 +184,7 @@ export function createSourceNode(buffer: AudioBuffer, volume: number, pan = 0, p
|
|||||||
.connect(gainNode)
|
.connect(gainNode)
|
||||||
.connect(ctx.destination);
|
.connect(ctx.destination);
|
||||||
|
|
||||||
return soundSource;
|
return { soundSource, panNode, gainNode };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -7,8 +7,8 @@ import { post } from '@/os.js';
|
|||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { $i, login } from '@/account.js';
|
import { $i, login } from '@/account.js';
|
||||||
import { getAccountFromId } from '@/scripts/get-account-from-id.js';
|
import { getAccountFromId } from '@/scripts/get-account-from-id.js';
|
||||||
import { mainRouter } from '@/router.js';
|
|
||||||
import { deepClone } from '@/scripts/clone.js';
|
import { deepClone } from '@/scripts/clone.js';
|
||||||
|
import { mainRouter } from '@/global/router/main.js';
|
||||||
|
|
||||||
export function swInject() {
|
export function swInject() {
|
||||||
navigator.serviceWorker.addEventListener('message', async ev => {
|
navigator.serviceWorker.addEventListener('message', async ev => {
|
||||||
|
@@ -52,11 +52,11 @@ import XCommon from './_common_/common.vue';
|
|||||||
import { instanceName } from '@/config.js';
|
import { instanceName } from '@/config.js';
|
||||||
import { StickySidebar } from '@/scripts/sticky-sidebar.js';
|
import { StickySidebar } from '@/scripts/sticky-sidebar.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { mainRouter } from '@/router.js';
|
|
||||||
import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
|
import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { miLocalStorage } from '@/local-storage.js';
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
|
import { mainRouter } from '@/global/router/main.js';
|
||||||
const XHeaderMenu = defineAsyncComponent(() => import('./classic.header.vue'));
|
const XHeaderMenu = defineAsyncComponent(() => import('./classic.header.vue'));
|
||||||
const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
|
const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
|
||||||
|
|
||||||
|
@@ -103,7 +103,6 @@ import * as os from '@/os.js';
|
|||||||
import { navbarItemDef } from '@/navbar.js';
|
import { navbarItemDef } from '@/navbar.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { mainRouter } from '@/router.js';
|
|
||||||
import { unisonReload } from '@/scripts/unison-reload.js';
|
import { unisonReload } from '@/scripts/unison-reload.js';
|
||||||
import { deviceKind } from '@/scripts/device-kind.js';
|
import { deviceKind } from '@/scripts/device-kind.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
@@ -117,6 +116,7 @@ import XWidgetsColumn from '@/ui/deck/widgets-column.vue';
|
|||||||
import XMentionsColumn from '@/ui/deck/mentions-column.vue';
|
import XMentionsColumn from '@/ui/deck/mentions-column.vue';
|
||||||
import XDirectColumn from '@/ui/deck/direct-column.vue';
|
import XDirectColumn from '@/ui/deck/direct-column.vue';
|
||||||
import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue';
|
import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue';
|
||||||
|
import { mainRouter } from '@/global/router/main.js';
|
||||||
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
|
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
|
||||||
const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue'));
|
const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue'));
|
||||||
|
|
||||||
|
@@ -24,10 +24,10 @@ import XColumn from './column.vue';
|
|||||||
import { deckStore, Column } from '@/ui/deck/deck-store.js';
|
import { deckStore, Column } from '@/ui/deck/deck-store.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { mainRouter } from '@/router.js';
|
|
||||||
import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
|
import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
|
||||||
import { useScrollPositionManager } from '@/nirax.js';
|
import { useScrollPositionManager } from '@/nirax.js';
|
||||||
import { getScrollContainer } from '@/scripts/scroll.js';
|
import { getScrollContainer } from '@/scripts/scroll.js';
|
||||||
|
import { mainRouter } from '@/global/router/main.js';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
column: Column;
|
column: Column;
|
||||||
|
@@ -16,9 +16,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { provide, ComputedRef, ref } from 'vue';
|
import { provide, ComputedRef, ref } from 'vue';
|
||||||
import XCommon from './_common_/common.vue';
|
import XCommon from './_common_/common.vue';
|
||||||
import { mainRouter } from '@/router.js';
|
|
||||||
import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
|
import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
|
||||||
import { instanceName } from '@/config.js';
|
import { instanceName } from '@/config.js';
|
||||||
|
import { mainRouter } from '@/global/router/main.js';
|
||||||
|
|
||||||
const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
|
const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
|
||||||
|
|
||||||
|
@@ -105,12 +105,12 @@ import { defaultStore } from '@/store.js';
|
|||||||
import { navbarItemDef } from '@/navbar.js';
|
import { navbarItemDef } from '@/navbar.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import { mainRouter } from '@/router.js';
|
|
||||||
import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
|
import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
|
||||||
import { deviceKind } from '@/scripts/device-kind.js';
|
import { deviceKind } from '@/scripts/device-kind.js';
|
||||||
import { miLocalStorage } from '@/local-storage.js';
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
import { CURRENT_STICKY_BOTTOM } from '@/const.js';
|
import { CURRENT_STICKY_BOTTOM } from '@/const.js';
|
||||||
import { useScrollPositionManager } from '@/nirax.js';
|
import { useScrollPositionManager } from '@/nirax.js';
|
||||||
|
import { mainRouter } from '@/global/router/main.js';
|
||||||
|
|
||||||
const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
|
const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
|
||||||
const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/navbar.vue'));
|
const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/navbar.vue'));
|
||||||
|
@@ -79,10 +79,10 @@ import { instance } from '@/instance.js';
|
|||||||
import XSigninDialog from '@/components/MkSigninDialog.vue';
|
import XSigninDialog from '@/components/MkSigninDialog.vue';
|
||||||
import XSignupDialog from '@/components/MkSignupDialog.vue';
|
import XSignupDialog from '@/components/MkSignupDialog.vue';
|
||||||
import { ColdDeviceStorage, defaultStore } from '@/store.js';
|
import { ColdDeviceStorage, defaultStore } from '@/store.js';
|
||||||
import { mainRouter } from '@/router.js';
|
|
||||||
import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
|
import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import MkVisitorDashboard from '@/components/MkVisitorDashboard.vue';
|
import MkVisitorDashboard from '@/components/MkVisitorDashboard.vue';
|
||||||
|
import { mainRouter } from '@/global/router/main.js';
|
||||||
|
|
||||||
const DESKTOP_THRESHOLD = 1100;
|
const DESKTOP_THRESHOLD = 1100;
|
||||||
|
|
||||||
|
@@ -24,10 +24,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { provide, ComputedRef, ref } from 'vue';
|
import { provide, ComputedRef, ref } from 'vue';
|
||||||
import XCommon from './_common_/common.vue';
|
import XCommon from './_common_/common.vue';
|
||||||
import { mainRouter } from '@/router.js';
|
|
||||||
import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
|
import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
|
||||||
import { instanceName, ui } from '@/config.js';
|
import { instanceName, ui } from '@/config.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { mainRouter } from '@/global/router/main.js';
|
||||||
|
|
||||||
const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
|
const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
|
||||||
|
|
||||||
|
@@ -104,10 +104,7 @@ const jammedAudioBuffer = ref<AudioBuffer | null>(null);
|
|||||||
const jammedSoundNodePlaying = ref<boolean>(false);
|
const jammedSoundNodePlaying = ref<boolean>(false);
|
||||||
|
|
||||||
if (defaultStore.state.sound_masterVolume) {
|
if (defaultStore.state.sound_masterVolume) {
|
||||||
sound.loadAudio({
|
sound.loadAudio('/client-assets/sounds/syuilo/queue-jammed.mp3').then(buf => {
|
||||||
type: 'syuilo/queue-jammed',
|
|
||||||
volume: 1,
|
|
||||||
}).then(buf => {
|
|
||||||
if (!buf) throw new Error('[WidgetJobQueue] Failed to initialize AudioBuffer');
|
if (!buf) throw new Error('[WidgetJobQueue] Failed to initialize AudioBuffer');
|
||||||
jammedAudioBuffer.value = buf;
|
jammedAudioBuffer.value = buf;
|
||||||
});
|
});
|
||||||
@@ -126,7 +123,7 @@ const onStats = (stats) => {
|
|||||||
current[domain].delayed = stats[domain].delayed;
|
current[domain].delayed = stats[domain].delayed;
|
||||||
|
|
||||||
if (current[domain].waiting > 0 && widgetProps.sound && jammedAudioBuffer.value && !jammedSoundNodePlaying.value) {
|
if (current[domain].waiting > 0 && widgetProps.sound && jammedAudioBuffer.value && !jammedSoundNodePlaying.value) {
|
||||||
const soundNode = sound.createSourceNode(jammedAudioBuffer.value, 1);
|
const soundNode = sound.createSourceNode(jammedAudioBuffer.value, 1)?.soundSource;
|
||||||
if (soundNode) {
|
if (soundNode) {
|
||||||
jammedSoundNodePlaying.value = true;
|
jammedSoundNodePlaying.value = true;
|
||||||
soundNode.onended = () => jammedSoundNodePlaying.value = false;
|
soundNode.onended = () => jammedSoundNodePlaying.value = false;
|
||||||
|
@@ -80,13 +80,13 @@ import * as Misskey from 'misskey-js';
|
|||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
connection: any,
|
connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>,
|
||||||
meta: Misskey.entities.ServerInfoResponse
|
meta: Misskey.entities.ServerInfoResponse
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const viewBoxX = ref<number>(50);
|
const viewBoxX = ref<number>(50);
|
||||||
const viewBoxY = ref<number>(30);
|
const viewBoxY = ref<number>(30);
|
||||||
const stats = ref<any[]>([]);
|
const stats = ref<Misskey.entities.ServerStats[]>([]);
|
||||||
const cpuGradientId = uuid();
|
const cpuGradientId = uuid();
|
||||||
const cpuMaskId = uuid();
|
const cpuMaskId = uuid();
|
||||||
const memGradientId = uuid();
|
const memGradientId = uuid();
|
||||||
@@ -107,6 +107,7 @@ onMounted(() => {
|
|||||||
props.connection.on('statsLog', onStatsLog);
|
props.connection.on('statsLog', onStatsLog);
|
||||||
props.connection.send('requestLog', {
|
props.connection.send('requestLog', {
|
||||||
id: Math.random().toString().substring(2, 10),
|
id: Math.random().toString().substring(2, 10),
|
||||||
|
length: 50,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -115,7 +116,7 @@ onBeforeUnmount(() => {
|
|||||||
props.connection.off('statsLog', onStatsLog);
|
props.connection.off('statsLog', onStatsLog);
|
||||||
});
|
});
|
||||||
|
|
||||||
function onStats(connStats) {
|
function onStats(connStats: Misskey.entities.ServerStats) {
|
||||||
stats.value.push(connStats);
|
stats.value.push(connStats);
|
||||||
if (stats.value.length > 50) stats.value.shift();
|
if (stats.value.length > 50) stats.value.shift();
|
||||||
|
|
||||||
@@ -136,8 +137,8 @@ function onStats(connStats) {
|
|||||||
memP.value = (connStats.mem.active / props.meta.mem.total * 100).toFixed(0);
|
memP.value = (connStats.mem.active / props.meta.mem.total * 100).toFixed(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onStatsLog(statsLog) {
|
function onStatsLog(statsLog: Misskey.entities.ServerStatsLog) {
|
||||||
for (const revStats of [...statsLog].reverse()) {
|
for (const revStats of statsLog.reverse()) {
|
||||||
onStats(revStats);
|
onStats(revStats);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -20,13 +20,13 @@ import * as Misskey from 'misskey-js';
|
|||||||
import XPie from './pie.vue';
|
import XPie from './pie.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
connection: any,
|
connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>,
|
||||||
meta: Misskey.entities.ServerInfoResponse
|
meta: Misskey.entities.ServerInfoResponse
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const usage = ref<number>(0);
|
const usage = ref<number>(0);
|
||||||
|
|
||||||
function onStats(stats) {
|
function onStats(stats: Misskey.entities.ServerStats) {
|
||||||
usage.value = stats.cpu;
|
usage.value = stats.cpu;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onUnmounted, ref } from 'vue';
|
import { onUnmounted, ref } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { useWidgetPropsManager, Widget, WidgetComponentExpose } from '../widget.js';
|
import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from '../widget.js';
|
||||||
import XCpuMemory from './cpu-mem.vue';
|
import XCpuMemory from './cpu-mem.vue';
|
||||||
import XNet from './net.vue';
|
import XNet from './net.vue';
|
||||||
import XCpu from './cpu.vue';
|
import XCpu from './cpu.vue';
|
||||||
@@ -54,11 +54,8 @@ const widgetPropsDef = {
|
|||||||
|
|
||||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||||
|
|
||||||
// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
|
const props = defineProps<WidgetComponentProps<WidgetProps>>();
|
||||||
//const props = defineProps<WidgetComponentProps<WidgetProps>>();
|
const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
|
||||||
//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
|
|
||||||
const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
|
|
||||||
const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>();
|
|
||||||
|
|
||||||
const { widgetProps, configure, save } = useWidgetPropsManager(name,
|
const { widgetProps, configure, save } = useWidgetPropsManager(name,
|
||||||
widgetPropsDef,
|
widgetPropsDef,
|
||||||
|
@@ -22,7 +22,7 @@ import XPie from './pie.vue';
|
|||||||
import bytes from '@/filters/bytes.js';
|
import bytes from '@/filters/bytes.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
connection: any,
|
connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>,
|
||||||
meta: Misskey.entities.ServerInfoResponse
|
meta: Misskey.entities.ServerInfoResponse
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ const total = ref<number>(0);
|
|||||||
const used = ref<number>(0);
|
const used = ref<number>(0);
|
||||||
const free = ref<number>(0);
|
const free = ref<number>(0);
|
||||||
|
|
||||||
function onStats(stats) {
|
function onStats(stats: Misskey.entities.ServerStats) {
|
||||||
usage.value = stats.mem.active / props.meta.mem.total;
|
usage.value = stats.mem.active / props.meta.mem.total;
|
||||||
total.value = props.meta.mem.total;
|
total.value = props.meta.mem.total;
|
||||||
used.value = stats.mem.active;
|
used.value = stats.mem.active;
|
||||||
|
@@ -54,13 +54,13 @@ import * as Misskey from 'misskey-js';
|
|||||||
import bytes from '@/filters/bytes.js';
|
import bytes from '@/filters/bytes.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
connection: any,
|
connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>,
|
||||||
meta: Misskey.entities.ServerInfoResponse
|
meta: Misskey.entities.ServerInfoResponse
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const viewBoxX = ref<number>(50);
|
const viewBoxX = ref<number>(50);
|
||||||
const viewBoxY = ref<number>(30);
|
const viewBoxY = ref<number>(30);
|
||||||
const stats = ref<any[]>([]);
|
const stats = ref<Misskey.entities.ServerStats[]>([]);
|
||||||
const inPolylinePoints = ref<string>('');
|
const inPolylinePoints = ref<string>('');
|
||||||
const outPolylinePoints = ref<string>('');
|
const outPolylinePoints = ref<string>('');
|
||||||
const inPolygonPoints = ref<string>('');
|
const inPolygonPoints = ref<string>('');
|
||||||
@@ -77,6 +77,7 @@ onMounted(() => {
|
|||||||
props.connection.on('statsLog', onStatsLog);
|
props.connection.on('statsLog', onStatsLog);
|
||||||
props.connection.send('requestLog', {
|
props.connection.send('requestLog', {
|
||||||
id: Math.random().toString().substring(2, 10),
|
id: Math.random().toString().substring(2, 10),
|
||||||
|
length: 50,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -85,7 +86,7 @@ onBeforeUnmount(() => {
|
|||||||
props.connection.off('statsLog', onStatsLog);
|
props.connection.off('statsLog', onStatsLog);
|
||||||
});
|
});
|
||||||
|
|
||||||
function onStats(connStats) {
|
function onStats(connStats: Misskey.entities.ServerStats) {
|
||||||
stats.value.push(connStats);
|
stats.value.push(connStats);
|
||||||
if (stats.value.length > 50) stats.value.shift();
|
if (stats.value.length > 50) stats.value.shift();
|
||||||
|
|
||||||
@@ -109,8 +110,8 @@ function onStats(connStats) {
|
|||||||
outRecent.value = connStats.net.tx;
|
outRecent.value = connStats.net.tx;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onStatsLog(statsLog) {
|
function onStatsLog(statsLog: Misskey.entities.ServerStatsLog) {
|
||||||
for (const revStats of [...statsLog].reverse()) {
|
for (const revStats of statsLog.reverse()) {
|
||||||
onStats(revStats);
|
onStats(revStats);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
41
packages/frontend/test/emoji.test.ts
Normal file
41
packages/frontend/test/emoji.test.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, test, assert, afterEach } from 'vitest';
|
||||||
|
import { render, cleanup, type RenderResult } from '@testing-library/vue';
|
||||||
|
import { defaultStoreState } from './init.js';
|
||||||
|
import { getEmojiName } from '@/scripts/emojilist.js';
|
||||||
|
import { components } from '@/components/index.js';
|
||||||
|
import { directives } from '@/directives/index.js';
|
||||||
|
import MkEmoji from '@/components/global/MkEmoji.vue';
|
||||||
|
|
||||||
|
describe('Emoji', () => {
|
||||||
|
const renderEmoji = (emoji: string): RenderResult => {
|
||||||
|
return render(MkEmoji, {
|
||||||
|
props: { emoji },
|
||||||
|
global: { directives, components },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
defaultStoreState.emojiStyle = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('MkEmoji', () => {
|
||||||
|
test('Should render selector-less heart with color in native mode', async () => {
|
||||||
|
defaultStoreState.emojiStyle = 'native';
|
||||||
|
const mkEmoji = await renderEmoji('\u2764'); // monochrome heart
|
||||||
|
assert.ok(mkEmoji.queryByText('\u2764\uFE0F')); // colored heart
|
||||||
|
assert.ok(!mkEmoji.queryByText('\u2764'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Emoji list', () => {
|
||||||
|
test('Should get the name of the heart', () => {
|
||||||
|
assert.strictEqual(getEmojiName('\u2764'), 'heart');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -17,21 +17,23 @@ updateI18n(locales['en-US']);
|
|||||||
// XXX: misskey-js panics if WebSocket is not defined
|
// XXX: misskey-js panics if WebSocket is not defined
|
||||||
vi.stubGlobal('WebSocket', class WebSocket extends EventTarget { static CLOSING = 2; });
|
vi.stubGlobal('WebSocket', class WebSocket extends EventTarget { static CLOSING = 2; });
|
||||||
|
|
||||||
|
export const defaultStoreState: Record<string, unknown> = {
|
||||||
|
|
||||||
|
// なんかtestがうまいこと動かないのでここに書く
|
||||||
|
dataSaver: {
|
||||||
|
media: false,
|
||||||
|
avatar: false,
|
||||||
|
urlPreview: false,
|
||||||
|
code: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
// XXX: defaultStore somehow becomes undefined in vitest?
|
// XXX: defaultStore somehow becomes undefined in vitest?
|
||||||
vi.mock('@/store.js', () => {
|
vi.mock('@/store.js', () => {
|
||||||
return {
|
return {
|
||||||
defaultStore: {
|
defaultStore: {
|
||||||
state: {
|
state: defaultStoreState,
|
||||||
|
|
||||||
// なんかtestがうまいこと動かないのでここに書く
|
|
||||||
dataSaver: {
|
|
||||||
media: false,
|
|
||||||
avatar: false,
|
|
||||||
urlPreview: false,
|
|
||||||
code: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@@ -2554,7 +2554,7 @@ type QueueStats = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type QueueStatsLog = string[];
|
type QueueStatsLog = QueueStats[];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type RenoteMuteCreateRequest = operations['renote-mute/create']['requestBody']['content']['application/json'];
|
type RenoteMuteCreateRequest = operations['renote-mute/create']['requestBody']['content']['application/json'];
|
||||||
@@ -2628,7 +2628,7 @@ type ServerStats = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type ServerStatsLog = string[];
|
type ServerStatsLog = ServerStats[];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type Signin = components['schemas']['Signin'];
|
type Signin = components['schemas']['Signin'];
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* version: 2023.12.2
|
* version: 2023.12.2
|
||||||
* generatedAt: 2024-01-04T18:10:15.096Z
|
* generatedAt: 2024-01-07T15:22:15.630Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { SwitchCaseResponseType } from '../api.js';
|
import type { SwitchCaseResponseType } from '../api.js';
|
||||||
@@ -2249,6 +2249,18 @@ declare module '../api.js' {
|
|||||||
credential?: string | null,
|
credential?: string | null,
|
||||||
): Promise<SwitchCaseResponseType<E, P>>;
|
): Promise<SwitchCaseResponseType<E, P>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No description provided.
|
||||||
|
*
|
||||||
|
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
|
||||||
|
* **Credential required**: *Yes*
|
||||||
|
*/
|
||||||
|
request<E extends 'i/export-clips', P extends Endpoints[E]['req']>(
|
||||||
|
endpoint: E,
|
||||||
|
params: P,
|
||||||
|
credential?: string | null,
|
||||||
|
): Promise<SwitchCaseResponseType<E, P>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* No description provided.
|
* No description provided.
|
||||||
*
|
*
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* version: 2023.12.2
|
* version: 2023.12.2
|
||||||
* generatedAt: 2024-01-04T18:10:15.094Z
|
* generatedAt: 2024-01-07T15:22:15.626Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@@ -745,6 +745,7 @@ export type Endpoints = {
|
|||||||
'i/export-following': { req: IExportFollowingRequest; res: EmptyResponse };
|
'i/export-following': { req: IExportFollowingRequest; res: EmptyResponse };
|
||||||
'i/export-mute': { req: EmptyRequest; res: EmptyResponse };
|
'i/export-mute': { req: EmptyRequest; res: EmptyResponse };
|
||||||
'i/export-notes': { req: EmptyRequest; res: EmptyResponse };
|
'i/export-notes': { req: EmptyRequest; res: EmptyResponse };
|
||||||
|
'i/export-clips': { req: EmptyRequest; res: EmptyResponse };
|
||||||
'i/export-favorites': { req: EmptyRequest; res: EmptyResponse };
|
'i/export-favorites': { req: EmptyRequest; res: EmptyResponse };
|
||||||
'i/export-user-lists': { req: EmptyRequest; res: EmptyResponse };
|
'i/export-user-lists': { req: EmptyRequest; res: EmptyResponse };
|
||||||
'i/export-antennas': { req: EmptyRequest; res: EmptyResponse };
|
'i/export-antennas': { req: EmptyRequest; res: EmptyResponse };
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* version: 2023.12.2
|
* version: 2023.12.2
|
||||||
* generatedAt: 2024-01-04T18:10:15.093Z
|
* generatedAt: 2024-01-07T15:22:15.624Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { operations } from './types.js';
|
import { operations } from './types.js';
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* version: 2023.12.2
|
* version: 2023.12.2
|
||||||
* generatedAt: 2024-01-04T18:10:15.091Z
|
* generatedAt: 2024-01-07T15:22:15.623Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { components } from './types.js';
|
import { components } from './types.js';
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* version: 2023.12.2
|
* version: 2023.12.2
|
||||||
* generatedAt: 2024-01-04T18:10:15.023Z
|
* generatedAt: 2024-01-07T15:22:15.494Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1966,6 +1966,16 @@ export type paths = {
|
|||||||
*/
|
*/
|
||||||
post: operations['i/export-notes'];
|
post: operations['i/export-notes'];
|
||||||
};
|
};
|
||||||
|
'/i/export-clips': {
|
||||||
|
/**
|
||||||
|
* i/export-clips
|
||||||
|
* @description No description provided.
|
||||||
|
*
|
||||||
|
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
|
||||||
|
* **Credential required**: *Yes*
|
||||||
|
*/
|
||||||
|
post: operations['i/export-clips'];
|
||||||
|
};
|
||||||
'/i/export-favorites': {
|
'/i/export-favorites': {
|
||||||
/**
|
/**
|
||||||
* i/export-favorites
|
* i/export-favorites
|
||||||
@@ -15881,7 +15891,7 @@ export type operations = {
|
|||||||
content: {
|
content: {
|
||||||
'application/json': {
|
'application/json': {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
name: 'notes1' | 'notes10' | 'notes100' | 'notes500' | 'notes1000' | 'notes5000' | 'notes10000' | 'notes20000' | 'notes30000' | 'notes40000' | 'notes50000' | 'notes60000' | 'notes70000' | 'notes80000' | 'notes90000' | 'notes100000' | 'login3' | 'login7' | 'login15' | 'login30' | 'login60' | 'login100' | 'login200' | 'login300' | 'login400' | 'login500' | 'login600' | 'login700' | 'login800' | 'login900' | 'login1000' | 'passedSinceAccountCreated1' | 'passedSinceAccountCreated2' | 'passedSinceAccountCreated3' | 'loggedInOnBirthday' | 'loggedInOnNewYearsDay' | 'noteClipped1' | 'noteFavorited1' | 'myNoteFavorited1' | 'profileFilled' | 'markedAsCat' | 'following1' | 'following10' | 'following50' | 'following100' | 'following300' | 'followers1' | 'followers10' | 'followers50' | 'followers100' | 'followers300' | 'followers500' | 'followers1000' | 'collectAchievements30' | 'viewAchievements3min' | 'iLoveMisskey' | 'foundTreasure' | 'client30min' | 'client60min' | 'noteDeletedWithin1min' | 'postedAtLateNight' | 'postedAt0min0sec' | 'selfQuote' | 'htl20npm' | 'viewInstanceChart' | 'outputHelloWorldOnScratchpad' | 'open3windows' | 'driveFolderCircularReference' | 'reactWithoutRead' | 'clickedClickHere' | 'justPlainLucky' | 'setNameToSyuilo' | 'cookieClicked' | 'brainDiver' | 'smashTestNotificationButton' | 'tutorialCompleted';
|
name: 'notes1' | 'notes10' | 'notes100' | 'notes500' | 'notes1000' | 'notes5000' | 'notes10000' | 'notes20000' | 'notes30000' | 'notes40000' | 'notes50000' | 'notes60000' | 'notes70000' | 'notes80000' | 'notes90000' | 'notes100000' | 'login3' | 'login7' | 'login15' | 'login30' | 'login60' | 'login100' | 'login200' | 'login300' | 'login400' | 'login500' | 'login600' | 'login700' | 'login800' | 'login900' | 'login1000' | 'passedSinceAccountCreated1' | 'passedSinceAccountCreated2' | 'passedSinceAccountCreated3' | 'loggedInOnBirthday' | 'loggedInOnNewYearsDay' | 'noteClipped1' | 'noteFavorited1' | 'myNoteFavorited1' | 'profileFilled' | 'markedAsCat' | 'following1' | 'following10' | 'following50' | 'following100' | 'following300' | 'followers1' | 'followers10' | 'followers50' | 'followers100' | 'followers300' | 'followers500' | 'followers1000' | 'collectAchievements30' | 'viewAchievements3min' | 'iLoveMisskey' | 'foundTreasure' | 'client30min' | 'client60min' | 'noteDeletedWithin1min' | 'postedAtLateNight' | 'postedAt0min0sec' | 'selfQuote' | 'htl20npm' | 'viewInstanceChart' | 'outputHelloWorldOnScratchpad' | 'open3windows' | 'driveFolderCircularReference' | 'reactWithoutRead' | 'clickedClickHere' | 'justPlainLucky' | 'setNameToSyuilo' | 'cookieClicked' | 'brainDiver' | 'smashTestNotificationButton' | 'tutorialCompleted' | 'bubbleGameExplodingHead' | 'bubbleGameDoubleExplodingHead';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -16243,6 +16253,57 @@ export type operations = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* i/export-clips
|
||||||
|
* @description No description provided.
|
||||||
|
*
|
||||||
|
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
|
||||||
|
* **Credential required**: *Yes*
|
||||||
|
*/
|
||||||
|
'i/export-clips': {
|
||||||
|
responses: {
|
||||||
|
/** @description OK (without any results) */
|
||||||
|
204: {
|
||||||
|
content: never;
|
||||||
|
};
|
||||||
|
/** @description Client error */
|
||||||
|
400: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Authentication error */
|
||||||
|
401: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Forbidden error */
|
||||||
|
403: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description I'm Ai */
|
||||||
|
418: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description To many requests */
|
||||||
|
429: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Internal server error */
|
||||||
|
500: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* i/export-favorites
|
* i/export-favorites
|
||||||
* @description No description provided.
|
* @description No description provided.
|
||||||
|
@@ -149,7 +149,7 @@ export type ServerStats = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ServerStatsLog = string[];
|
export type ServerStatsLog = ServerStats[];
|
||||||
|
|
||||||
export type QueueStats = {
|
export type QueueStats = {
|
||||||
deliver: {
|
deliver: {
|
||||||
@@ -166,7 +166,7 @@ export type QueueStats = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type QueueStatsLog = string[];
|
export type QueueStatsLog = QueueStats[];
|
||||||
|
|
||||||
export type EmojiAdded = {
|
export type EmojiAdded = {
|
||||||
emoji: EmojiDetailed
|
emoji: EmojiDetailed
|
||||||
|
Reference in New Issue
Block a user