merge: upstream
This commit is contained in:
commit
7552cea69a
413 changed files with 5517 additions and 2309 deletions
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { $i } from '@/account.js';
|
||||
|
||||
export const ACHIEVEMENT_TYPES = [
|
||||
|
|
@ -83,6 +83,8 @@ export const ACHIEVEMENT_TYPES = [
|
|||
'brainDiver',
|
||||
'smashTestNotificationButton',
|
||||
'tutorialCompleted',
|
||||
'bubbleGameExplodingHead',
|
||||
'bubbleGameDoubleExplodingHead',
|
||||
] as const;
|
||||
|
||||
export const ACHIEVEMENT_BADGES = {
|
||||
|
|
@ -466,6 +468,16 @@ export const ACHIEVEMENT_BADGES = {
|
|||
bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
|
||||
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>
|
||||
} as const satisfies Record<typeof ACHIEVEMENT_TYPES[number], {
|
||||
img: string;
|
||||
|
|
@ -489,7 +501,7 @@ export async function claimAchievement(type: typeof ACHIEVEMENT_TYPES[number]) {
|
|||
window.setTimeout(() => {
|
||||
claimingQueue.delete(type);
|
||||
}, 500);
|
||||
os.api('i/claim-achievement', { name: type });
|
||||
misskeyApi('i/claim-achievement', { name: type });
|
||||
}
|
||||
|
||||
if (_DEV_) {
|
||||
|
|
|
|||
|
|
@ -5,12 +5,23 @@
|
|||
|
||||
import { utils, values } from '@syuilo/aiscript';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { customEmojis } from '@/custom-emojis.js';
|
||||
import { url, lang } from '@/config.js';
|
||||
import { nyaize } from '@/scripts/nyaize.js';
|
||||
|
||||
export function aiScriptReadline(q: string): Promise<string> {
|
||||
return new Promise(ok => {
|
||||
os.inputText({
|
||||
title: q,
|
||||
}).then(({ result: a }) => {
|
||||
ok(a ?? '');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function createAiScriptEnv(opts) {
|
||||
return {
|
||||
USER_ID: $i ? values.STR($i.id) : values.NULL,
|
||||
|
|
@ -44,7 +55,7 @@ export function createAiScriptEnv(opts) {
|
|||
if (typeof token.value !== 'string') throw new Error('invalid token');
|
||||
}
|
||||
const actualToken: string|null = token?.value ?? opts.token ?? null;
|
||||
return os.api(ep.value, utils.valToJs(param), actualToken).then(res => {
|
||||
return misskeyApi(ep.value, utils.valToJs(param), actualToken).then(res => {
|
||||
return utils.jsToVal(res);
|
||||
}, err => {
|
||||
return values.ERROR('request_failed', utils.jsToVal(err));
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ function getTextOptions(def: values.Value | undefined): Omit<AsUiText, 'id' | 't
|
|||
};
|
||||
}
|
||||
|
||||
function getMfmOptions(def: values.Value | undefined): Omit<AsUiMfm, 'id' | 'type'> {
|
||||
function getMfmOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiMfm, 'id' | 'type'> {
|
||||
utils.assertObject(def);
|
||||
|
||||
const text = def.value.get('text');
|
||||
|
|
@ -241,7 +241,7 @@ function getMfmOptions(def: values.Value | undefined): Omit<AsUiMfm, 'id' | 'typ
|
|||
color: color?.value,
|
||||
font: font?.value,
|
||||
onClickEv: (evId: string) => {
|
||||
if (onClickEv) call(onClickEv, values.STR(evId));
|
||||
if (onClickEv) call(onClickEv, [values.STR(evId)]);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
import { ref, computed } from 'vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
|
||||
type SaveData = {
|
||||
gameVersion: number;
|
||||
|
|
@ -23,7 +23,7 @@ let prev = '';
|
|||
|
||||
export async function load() {
|
||||
try {
|
||||
saveData.value = await os.api('i/registry/get', {
|
||||
saveData.value = await misskeyApi('i/registry/get', {
|
||||
scope: ['clickerGame'],
|
||||
key: 'saveData',
|
||||
});
|
||||
|
|
@ -63,7 +63,7 @@ export async function save() {
|
|||
const current = JSON.stringify(saveData.value);
|
||||
if (current === prev) return;
|
||||
|
||||
await os.api('i/registry/set', {
|
||||
await misskeyApi('i/registry/set', {
|
||||
scope: ['clickerGame'],
|
||||
key: 'saveData',
|
||||
value: saveData.value,
|
||||
|
|
|
|||
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 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) {
|
||||
return null;
|
||||
} 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 {
|
||||
value: string;
|
||||
category: string;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
type EnumItem = string | {label: string; value: string;};
|
||||
type EnumItem = string | {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type FormItem = {
|
||||
label?: string;
|
||||
type: 'string';
|
||||
|
|
@ -36,16 +40,23 @@ export type FormItem = {
|
|||
label: string;
|
||||
value: unknown;
|
||||
}[];
|
||||
} | {
|
||||
label?: string;
|
||||
type: 'range';
|
||||
default: number | null;
|
||||
step: number;
|
||||
min: number;
|
||||
max: number;
|
||||
} | {
|
||||
label?: string;
|
||||
type: 'object';
|
||||
default: Record<string, unknown> | null;
|
||||
hidden: true;
|
||||
hidden: boolean;
|
||||
} | {
|
||||
label?: string;
|
||||
type: 'array';
|
||||
default: unknown[] | null;
|
||||
hidden: true;
|
||||
hidden: boolean;
|
||||
};
|
||||
|
||||
export type Form = Record<string, FormItem>;
|
||||
|
|
@ -55,6 +66,7 @@ type GetItemType<Item extends FormItem> =
|
|||
Item['type'] extends 'number' ? number :
|
||||
Item['type'] extends 'boolean' ? boolean :
|
||||
Item['type'] extends 'radio' ? unknown :
|
||||
Item['type'] extends 'range' ? number :
|
||||
Item['type'] extends 'enum' ? string :
|
||||
Item['type'] extends 'array' ? unknown[] :
|
||||
Item['type'] extends 'object' ? Record<string, unknown>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export async function genSearchQuery(v: any, q: string) {
|
|||
host = at;
|
||||
}
|
||||
} else {
|
||||
const user = await v.os.api('users/show', Misskey.acct.parse(at)).catch(x => null);
|
||||
const user = await v.api('users/show', Misskey.acct.parse(at)).catch(x => null);
|
||||
if (user) {
|
||||
userId = user.id;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { defineAsyncComponent } from 'vue';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
|
|
@ -18,7 +19,7 @@ function rename(file: Misskey.entities.DriveFile) {
|
|||
default: file.name,
|
||||
}).then(({ canceled, result: name }) => {
|
||||
if (canceled) return;
|
||||
os.api('drive/files/update', {
|
||||
misskeyApi('drive/files/update', {
|
||||
fileId: file.id,
|
||||
name: name,
|
||||
});
|
||||
|
|
@ -31,7 +32,7 @@ function describe(file: Misskey.entities.DriveFile) {
|
|||
file: file,
|
||||
}, {
|
||||
done: caption => {
|
||||
os.api('drive/files/update', {
|
||||
misskeyApi('drive/files/update', {
|
||||
fileId: file.id,
|
||||
comment: caption.length === 0 ? null : caption,
|
||||
});
|
||||
|
|
@ -40,7 +41,7 @@ function describe(file: Misskey.entities.DriveFile) {
|
|||
}
|
||||
|
||||
function toggleSensitive(file: Misskey.entities.DriveFile) {
|
||||
os.api('drive/files/update', {
|
||||
misskeyApi('drive/files/update', {
|
||||
fileId: file.id,
|
||||
isSensitive: !file.isSensitive,
|
||||
}).catch(err => {
|
||||
|
|
@ -69,7 +70,7 @@ async function deleteFile(file: Misskey.entities.DriveFile) {
|
|||
});
|
||||
|
||||
if (canceled) return;
|
||||
os.api('drive/files/delete', {
|
||||
misskeyApi('drive/files/delete', {
|
||||
fileId: file.id,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { $i } from '@/account.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { url } from '@/config.js';
|
||||
import { defaultStore, noteActions } from '@/store.js';
|
||||
|
|
@ -40,7 +41,7 @@ export async function getNoteClipMenu(props: {
|
|||
action: () => {
|
||||
claimAchievement('noteClipped1');
|
||||
os.promiseDialog(
|
||||
os.api('clips/add-note', { clipId: clip.id, noteId: appearNote.id }),
|
||||
misskeyApi('clips/add-note', { clipId: clip.id, noteId: appearNote.id }),
|
||||
null,
|
||||
async (err) => {
|
||||
if (err.id === '734806c4-542c-463a-9311-15c512803965') {
|
||||
|
|
@ -156,7 +157,7 @@ export function getNoteMenu(props: {
|
|||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
|
||||
os.api('notes/delete', {
|
||||
misskeyApi('notes/delete', {
|
||||
noteId: appearNote.id,
|
||||
});
|
||||
|
||||
|
|
@ -173,7 +174,7 @@ export function getNoteMenu(props: {
|
|||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
|
||||
os.api('notes/delete', {
|
||||
misskeyApi('notes/delete', {
|
||||
noteId: appearNote.id,
|
||||
});
|
||||
|
||||
|
|
@ -265,7 +266,7 @@ export function getNoteMenu(props: {
|
|||
async function translate(): Promise<void> {
|
||||
if (props.translation.value != null) return;
|
||||
props.translating.value = true;
|
||||
const res = await os.api('notes/translate', {
|
||||
const res = await misskeyApi('notes/translate', {
|
||||
noteId: appearNote.id,
|
||||
targetLang: miLocalStorage.getItem('lang') ?? navigator.language,
|
||||
});
|
||||
|
|
@ -275,7 +276,7 @@ export function getNoteMenu(props: {
|
|||
|
||||
let menu: MenuItem[];
|
||||
if ($i) {
|
||||
const statePromise = os.api('notes/state', {
|
||||
const statePromise = misskeyApi('notes/state', {
|
||||
noteId: appearNote.id,
|
||||
});
|
||||
|
||||
|
|
@ -355,7 +356,7 @@ export function getNoteMenu(props: {
|
|||
icon: 'ph-user ph-bold ph-lg',
|
||||
text: i18n.ts.user,
|
||||
children: async () => {
|
||||
const user = appearNote.userId === $i?.id ? $i : await os.api('users/show', { userId: appearNote.userId });
|
||||
const user = appearNote.userId === $i?.id ? $i : await misskeyApi('users/show', { userId: appearNote.userId });
|
||||
const { menu, cleanup } = getUserMenu(user);
|
||||
cleanups.push(cleanup);
|
||||
return menu;
|
||||
|
|
@ -377,6 +378,42 @@ export function getNoteMenu(props: {
|
|||
]
|
||||
: []
|
||||
),
|
||||
...(appearNote.channel && (appearNote.channel.userId === $i.id || $i.isModerator || $i.isAdmin) ? [
|
||||
{ type: 'divider' },
|
||||
{
|
||||
type: 'parent' as const,
|
||||
icon: 'ti ti-device-tv',
|
||||
text: i18n.ts.channel,
|
||||
children: async () => {
|
||||
const channelChildMenu = [] as MenuItem[];
|
||||
|
||||
const channel = await misskeyApi('channels/show', { channelId: appearNote.channel!.id });
|
||||
|
||||
if (channel.pinnedNoteIds.includes(appearNote.id)) {
|
||||
channelChildMenu.push({
|
||||
icon: 'ti ti-pinned-off',
|
||||
text: i18n.ts.unpin,
|
||||
action: () => os.apiWithDialog('channels/update', {
|
||||
channelId: appearNote.channel!.id,
|
||||
pinnedNoteIds: channel.pinnedNoteIds.filter(id => id !== appearNote.id),
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
channelChildMenu.push({
|
||||
icon: 'ti ti-pin',
|
||||
text: i18n.ts.pin,
|
||||
action: () => os.apiWithDialog('channels/update', {
|
||||
channelId: appearNote.channel!.id,
|
||||
pinnedNoteIds: [...channel.pinnedNoteIds, appearNote.id],
|
||||
}),
|
||||
});
|
||||
}
|
||||
return channelChildMenu;
|
||||
},
|
||||
},
|
||||
]
|
||||
: []
|
||||
),
|
||||
...(appearNote.userId === $i.id || $i.isModerator || $i.isAdmin ? [
|
||||
{ type: 'divider' },
|
||||
appearNote.userId === $i.id ? {
|
||||
|
|
@ -497,7 +534,7 @@ export function getRenoteMenu(props: {
|
|||
}
|
||||
|
||||
if (!props.mock) {
|
||||
os.api('notes/create', {
|
||||
misskeyApi('notes/create', {
|
||||
renoteId: appearNote.id,
|
||||
channelId: appearNote.channelId,
|
||||
}).then(() => {
|
||||
|
|
@ -542,7 +579,7 @@ export function getRenoteMenu(props: {
|
|||
}
|
||||
|
||||
if (!props.mock) {
|
||||
os.api('notes/create', {
|
||||
misskeyApi('notes/create', {
|
||||
localOnly,
|
||||
visibility,
|
||||
renoteId: appearNote.id,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { Ref, defineAsyncComponent } from 'vue';
|
|||
import * as Misskey from 'misskey-js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from './misskey-api.js';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
import { dateTimeFormat } from './intl-const.js';
|
||||
|
||||
|
|
@ -30,7 +31,7 @@ export async function getNoteVersionsMenu(props: {
|
|||
}
|
||||
|
||||
const menu: MenuItem[] = [];
|
||||
const statePromise = os.api('notes/versions', {
|
||||
const statePromise = misskeyApi('notes/versions', {
|
||||
noteId: appearNote.id,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -10,13 +10,14 @@ import { i18n } from '@/i18n.js';
|
|||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { host, url } from '@/config.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { defaultStore, userActions } from '@/store.js';
|
||||
import { $i, iAmModerator } from '@/account.js';
|
||||
import { mainRouter } from '@/router.js';
|
||||
import { Router } from '@/nirax.js';
|
||||
import { IRouter } from '@/nirax.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 cleanups = [] as (() => void)[];
|
||||
|
|
@ -131,7 +132,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
|
|||
}
|
||||
|
||||
async function editMemo(): Promise<void> {
|
||||
const userDetailed = await os.api('users/show', {
|
||||
const userDetailed = await misskeyApi('users/show', {
|
||||
userId: user.id,
|
||||
});
|
||||
const { canceled, result } = await os.form(i18n.ts.editMemo, {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { Interpreter, Parser, utils } from '@syuilo/aiscript';
|
|||
import type { Plugin } from '@/store.js';
|
||||
import { ColdDeviceStorage } from '@/store.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
export type AiScriptPluginMeta = {
|
||||
|
|
@ -110,7 +111,7 @@ export async function installPlugin(code: string, meta?: AiScriptPluginMeta) {
|
|||
}, {
|
||||
done: async result => {
|
||||
const { name, permissions } = result;
|
||||
const { token } = await os.api('miauth/gen-token', {
|
||||
const { token } = await misskeyApi('miauth/gen-token', {
|
||||
session: null,
|
||||
name: name,
|
||||
permission: permissions,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import * as Misskey from 'misskey-js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
|
||||
export async function lookupUser() {
|
||||
const { canceled, result } = await os.inputText({
|
||||
|
|
@ -17,8 +18,8 @@ export async function lookupUser() {
|
|||
os.pageWindow(`/admin/user/${user.id}`);
|
||||
};
|
||||
|
||||
const usernamePromise = os.api('users/show', Misskey.acct.parse(result));
|
||||
const idPromise = os.api('users/show', { userId: result });
|
||||
const usernamePromise = misskeyApi('users/show', Misskey.acct.parse(result));
|
||||
const idPromise = misskeyApi('users/show', { userId: result });
|
||||
let _notFound = false;
|
||||
const notFound = () => {
|
||||
if (_notFound) {
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@
|
|||
*/
|
||||
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { mainRouter } from '@/router.js';
|
||||
import { Router } from '@/nirax.js';
|
||||
import { mainRouter } from '@/global/router/main.js';
|
||||
|
||||
export async function lookup(router?: Router) {
|
||||
const _router = router ?? mainRouter;
|
||||
|
|
@ -28,7 +29,7 @@ export async function lookup(router?: Router) {
|
|||
}
|
||||
|
||||
if (query.startsWith('https://')) {
|
||||
const promise = os.api('ap/show', {
|
||||
const promise = misskeyApi('ap/show', {
|
||||
uri: query,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -10,12 +10,17 @@ import { $i } from '@/account.js';
|
|||
export const pendingApiRequestsCount = ref(0);
|
||||
|
||||
// Implements Misskey.api.ApiClient.request
|
||||
export function api<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,
|
||||
data: P = {} as any,
|
||||
token?: string | null | undefined,
|
||||
signal?: AbortSignal,
|
||||
): Promise<Misskey.api.SwitchCaseResponseType<E, P>> {
|
||||
): Promise<_ResT> {
|
||||
if (endpoint.includes('://')) throw new Error('invalid endpoint');
|
||||
pendingApiRequestsCount.value++;
|
||||
|
||||
|
|
@ -23,7 +28,7 @@ export function api<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoin
|
|||
pendingApiRequestsCount.value--;
|
||||
};
|
||||
|
||||
const promise = new Promise<Misskey.Endpoints[E]['res'] | void>((resolve, reject) => {
|
||||
const promise = new Promise<_ResT>((resolve, reject) => {
|
||||
// Append a credential
|
||||
if ($i) (data as any).i = $i.token;
|
||||
if (token !== undefined) (data as any).i = token;
|
||||
|
|
@ -44,7 +49,7 @@ export function api<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoin
|
|||
if (res.status === 200) {
|
||||
resolve(body);
|
||||
} else if (res.status === 204) {
|
||||
resolve();
|
||||
resolve(undefined as _ResT); // void -> undefined
|
||||
} else {
|
||||
reject(body.error);
|
||||
}
|
||||
|
|
@ -57,10 +62,15 @@ export function api<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoin
|
|||
}
|
||||
|
||||
// Implements Misskey.api.ApiClient.request
|
||||
export function apiGet<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,
|
||||
data: P = {} as any,
|
||||
): Promise<Misskey.api.SwitchCaseResponseType<E, P>> {
|
||||
): Promise<_ResT> {
|
||||
pendingApiRequestsCount.value++;
|
||||
|
||||
const onFinally = () => {
|
||||
|
|
@ -69,7 +79,7 @@ export function apiGet<E extends keyof Misskey.Endpoints, P extends Misskey.Endp
|
|||
|
||||
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
|
||||
window.fetch(`${apiUrl}/${endpoint}?${query}`, {
|
||||
method: 'GET',
|
||||
|
|
@ -81,7 +91,7 @@ export function apiGet<E extends keyof Misskey.Endpoints, P extends Misskey.Endp
|
|||
if (res.status === 200) {
|
||||
resolve(body);
|
||||
} else if (res.status === 204) {
|
||||
resolve();
|
||||
resolve(undefined as _ResT); // void -> undefined
|
||||
} else {
|
||||
reject(body.error);
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
import { ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { useStream } from '@/stream.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
|
@ -65,7 +66,7 @@ export function chooseFileFromUrl(): Promise<Misskey.entities.DriveFile> {
|
|||
}
|
||||
});
|
||||
|
||||
os.api('drive/files/upload-from-url', {
|
||||
misskeyApi('drive/files/upload-from-url', {
|
||||
url: url,
|
||||
folderId: defaultStore.state.uploadFolder,
|
||||
marker,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
|
||||
import type { SoundStore } from '@/store.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import * as os from '@/os.js';
|
||||
|
||||
let ctx: AudioContext;
|
||||
const cache = new Map<string, AudioBuffer>();
|
||||
|
|
@ -89,63 +88,35 @@ export type OperationType = typeof operationTypes[number];
|
|||
|
||||
/**
|
||||
* 音声を読み込む
|
||||
* @param soundStore サウンド設定
|
||||
* @param url url
|
||||
* @param options `useCache`: デフォルトは`true` 一度再生した音声はキャッシュする
|
||||
*/
|
||||
export async function loadAudio(soundStore: SoundStore, options?: { useCache?: boolean; }) {
|
||||
export async function loadAudio(url: string, options?: { useCache?: boolean; }) {
|
||||
if (_DEV_) console.log('loading audio. opts:', options);
|
||||
// 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) {
|
||||
ctx = new AudioContext();
|
||||
}
|
||||
if (options?.useCache ?? true) {
|
||||
if (soundStore.type === '_driveFile_' && cache.has(soundStore.fileId)) {
|
||||
if (cache.has(url)) {
|
||||
if (_DEV_) console.log('use cache');
|
||||
return cache.get(soundStore.fileId) as AudioBuffer;
|
||||
} else if (cache.has(soundStore.type)) {
|
||||
if (_DEV_) console.log('use cache');
|
||||
return cache.get(soundStore.type) as AudioBuffer;
|
||||
return cache.get(url) as AudioBuffer;
|
||||
}
|
||||
}
|
||||
|
||||
let response: Response;
|
||||
|
||||
if (soundStore.type === '_driveFile_') {
|
||||
try {
|
||||
response = await fetch(soundStore.fileUrl);
|
||||
} catch (err) {
|
||||
try {
|
||||
// URLが変わっている可能性があるのでドライブ側からURLを取得するフォールバック
|
||||
const apiRes = await os.api('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;
|
||||
}
|
||||
try {
|
||||
response = await fetch(url);
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const audioBuffer = await ctx.decodeAudioData(arrayBuffer);
|
||||
|
||||
if (options?.useCache ?? true) {
|
||||
if (soundStore.type === '_driveFile_') {
|
||||
cache.set(soundStore.fileId, audioBuffer);
|
||||
} else {
|
||||
cache.set(soundStore.type, audioBuffer);
|
||||
}
|
||||
cache.set(url, audioBuffer);
|
||||
}
|
||||
|
||||
return audioBuffer;
|
||||
|
|
@ -174,25 +145,46 @@ export function play(operationType: OperationType) {
|
|||
* @param 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;
|
||||
createSourceNode(buffer, soundStore.volume)?.start();
|
||||
createSourceNode(buffer, soundStore.volume)?.soundSource.start();
|
||||
}
|
||||
|
||||
export function createSourceNode(buffer: AudioBuffer, volume: number) : AudioBufferSourceNode | null {
|
||||
export async function playUrl(url: string, volume = 1, pan = 0, playbackRate = 1) {
|
||||
const buffer = await loadAudio(url);
|
||||
if (!buffer) return;
|
||||
createSourceNode(buffer, volume, pan, playbackRate)?.soundSource.start();
|
||||
}
|
||||
|
||||
export function createSourceNode(buffer: AudioBuffer, volume: number, pan = 0, playbackRate = 1): {
|
||||
soundSource: AudioBufferSourceNode;
|
||||
panNode: StereoPannerNode;
|
||||
gainNode: GainNode;
|
||||
} | null {
|
||||
const masterVolume = defaultStore.state.sound_masterVolume;
|
||||
if (isMute() || masterVolume === 0 || volume === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const panNode = ctx.createStereoPanner();
|
||||
panNode.pan.value = pan;
|
||||
|
||||
const gainNode = ctx.createGain();
|
||||
gainNode.gain.value = masterVolume * volume;
|
||||
|
||||
const soundSource = ctx.createBufferSource();
|
||||
soundSource.buffer = buffer;
|
||||
soundSource.connect(gainNode).connect(ctx.destination);
|
||||
soundSource.playbackRate.value = playbackRate;
|
||||
soundSource
|
||||
.connect(panNode)
|
||||
.connect(gainNode)
|
||||
.connect(ctx.destination);
|
||||
|
||||
return soundSource;
|
||||
return { soundSource, panNode, gainNode };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { reactive, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { readAndCompressImage } from 'browser-image-resizer';
|
||||
import { readAndCompressImage } from '@misskey-dev/browser-image-resizer';
|
||||
import { getCompressionConfig } from './upload/compress-config.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { apiUrl } from '@/config.js';
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import isAnimated from 'is-file-animated';
|
||||
import { isWebpSupported } from './isWebpSupported.js';
|
||||
import type { BrowserImageResizerConfig } from 'browser-image-resizer';
|
||||
import type { BrowserImageResizerConfigWithConvertedOutput } from '@misskey-dev/browser-image-resizer';
|
||||
|
||||
const compressTypeMap = {
|
||||
'image/jpeg': { quality: 0.90, mimeType: 'image/webp' },
|
||||
|
|
@ -21,7 +21,7 @@ const compressTypeMapFallback = {
|
|||
'image/svg+xml': { quality: 1, mimeType: 'image/png' },
|
||||
} as const;
|
||||
|
||||
export async function getCompressionConfig(file: File): Promise<BrowserImageResizerConfig | undefined> {
|
||||
export async function getCompressionConfig(file: File): Promise<BrowserImageResizerConfigWithConvertedOutput | undefined> {
|
||||
const imgConfig = (isWebpSupported() ? compressTypeMap : compressTypeMapFallback)[file.type];
|
||||
if (!imgConfig || await isAnimated(file)) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import * as Misskey from 'misskey-js';
|
|||
import { useStream } from '@/stream.js';
|
||||
import { $i } from '@/account.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from './misskey-api.js';
|
||||
|
||||
export function useNoteCapture(props: {
|
||||
rootEl: Ref<HTMLElement>;
|
||||
|
|
@ -32,7 +33,7 @@ export function useNoteCapture(props: {
|
|||
|
||||
// notes/show may throw if the current user can't see the note
|
||||
try {
|
||||
const replyNote = await os.api('notes/show', {
|
||||
const replyNote = await misskeyApi('notes/show', {
|
||||
noteId: body.id,
|
||||
});
|
||||
|
||||
|
|
@ -100,7 +101,7 @@ export function useNoteCapture(props: {
|
|||
|
||||
case 'updated': {
|
||||
try {
|
||||
const editedNote = await os.api('notes/show', {
|
||||
const editedNote = await misskeyApi('notes/show', {
|
||||
noteId: id,
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue