merge: upstream

This commit is contained in:
Marie 2023-12-23 02:09:23 +01:00
commit 5db583a3eb
701 changed files with 50809 additions and 13660 deletions

View file

@ -50,6 +50,7 @@ export function createAiScriptEnv(opts) {
return values.ERROR('request_failed', utils.jsToVal(err));
});
}),
/*
'Mk:apiExternal': values.FN_NATIVE(async ([host, ep, param, token]) => {
utils.assertString(host);
utils.assertString(ep);
@ -60,6 +61,7 @@ export function createAiScriptEnv(opts) {
return values.ERROR('request_failed', utils.jsToVal(err));
});
}),
*/
'Mk:save': values.FN_NATIVE(([key, value]) => {
utils.assertString(key);
miLocalStorage.setItem(`aiscript:${opts.storageKey}:${key.value}`, JSON.stringify(utils.valToJs(value)));

View file

@ -121,6 +121,7 @@ export type AsUiPostFormButton = AsUiComponentBase & {
rounded?: boolean;
form?: {
text: string;
cw?: string;
};
};
@ -128,6 +129,7 @@ export type AsUiPostForm = AsUiComponentBase & {
type: 'postForm';
form?: {
text: string;
cw?: string;
};
};
@ -454,8 +456,11 @@ function getPostFormButtonOptions(def: values.Value | undefined, call: (fn: valu
const getForm = () => {
const text = form!.value.get('text');
utils.assertString(text);
const cw = form!.value.get('cw');
if (cw) utils.assertString(cw);
return {
text: text.value,
cw: cw?.value,
};
};
@ -478,8 +483,11 @@ function getPostFormOptions(def: values.Value | undefined, call: (fn: values.VFn
const getForm = () => {
const text = form!.value.get('text');
utils.assertString(text);
const cw = form!.value.get('cw');
if (cw) utils.assertString(cw);
return {
text: text.value,
cw: cw?.value,
};
};

View file

@ -10,7 +10,12 @@ 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']>(endpoint: E, data: P = {} as any, token?: string | null | undefined, signal?: AbortSignal): Promise<Misskey.Endpoints[E]['res']> {
export function api<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req']>(
endpoint: E,
data: P = {} as any,
token?: string | null | undefined,
signal?: AbortSignal,
): Promise<Misskey.api.SwitchCaseResponseType<E, P>> {
if (endpoint.includes('://')) throw new Error('invalid endpoint');
pendingApiRequestsCount.value++;
@ -51,51 +56,11 @@ export function api<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoin
return promise;
}
export function apiExternal<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req']>(hostUrl: string, endpoint: E, data: P = {} as any, token?: string | null | undefined, signal?: AbortSignal): Promise<Misskey.Endpoints[E]['res']> {
if (!/^https?:\/\//.test(hostUrl)) throw new Error('invalid host name');
if (endpoint.includes('://')) throw new Error('invalid endpoint');
pendingApiRequestsCount.value++;
const onFinally = () => {
pendingApiRequestsCount.value--;
};
const promise = new Promise<Misskey.Endpoints[E]['res'] | void>((resolve, reject) => {
// Append a credential
(data as any).i = token;
const fullUrl = (hostUrl.slice(-1) === '/' ? hostUrl.slice(0, -1) : hostUrl)
+ '/api/' + (endpoint.slice(0, 1) === '/' ? endpoint.slice(1) : endpoint);
// Send request
window.fetch(fullUrl, {
method: 'POST',
body: JSON.stringify(data),
credentials: 'omit',
cache: 'no-cache',
headers: {
'Content-Type': 'application/json',
},
signal,
}).then(async (res) => {
const body = res.status === 204 ? null : await res.json();
if (res.status === 200) {
resolve(body);
} else if (res.status === 204) {
resolve();
} else {
reject(body.error);
}
}).catch(reject);
});
promise.then(onFinally, onFinally);
return promise;
}
// Implements Misskey.api.ApiClient.request
export function apiGet <E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req']>(endpoint: E, data: P = {} as any): Promise<Misskey.Endpoints[E]['res']> {
export function apiGet<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req']>(
endpoint: E,
data: P = {} as any,
): Promise<Misskey.api.SwitchCaseResponseType<E, P>> {
pendingApiRequestsCount.value++;
const onFinally = () => {

View file

@ -8,6 +8,8 @@ import getCaretCoordinates from 'textarea-caret';
import { toASCII } from 'punycode/';
import { popup } from '@/os.js';
export type SuggestionType = 'user' | 'hashtag' | 'emoji' | 'mfmTag';
export class Autocomplete {
private suggestion: {
x: Ref<number>;
@ -19,6 +21,7 @@ export class Autocomplete {
private currentType: string;
private textRef: Ref<string>;
private opening: boolean;
private onlyType: SuggestionType[];
private get text(): string {
// Use raw .value to get the latest value
@ -35,7 +38,7 @@ export class Autocomplete {
/**
*
*/
constructor(textarea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>) {
constructor(textarea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>, onlyType?: SuggestionType[]) {
//#region BIND
this.onInput = this.onInput.bind(this);
this.complete = this.complete.bind(this);
@ -46,6 +49,7 @@ export class Autocomplete {
this.textarea = textarea;
this.textRef = textRef;
this.opening = false;
this.onlyType = onlyType ?? ['user', 'hashtag', 'emoji', 'mfmTag'];
this.attach();
}
@ -95,7 +99,7 @@ export class Autocomplete {
let opened = false;
if (isMention) {
if (isMention && this.onlyType.includes('user')) {
const username = text.substring(mentionIndex + 1);
if (username !== '' && username.match(/^[a-zA-Z0-9_.]+$/)) {
this.open('user', username);
@ -106,7 +110,7 @@ export class Autocomplete {
}
}
if (isHashtag && !opened) {
if (isHashtag && !opened && this.onlyType.includes('hashtag')) {
const hashtag = text.substring(hashtagIndex + 1);
if (!hashtag.includes(' ')) {
this.open('hashtag', hashtag);
@ -114,7 +118,7 @@ export class Autocomplete {
}
}
if (isEmoji && !opened) {
if (isEmoji && !opened && this.onlyType.includes('emoji')) {
const emoji = text.substring(emojiIndex + 1);
if (!emoji.includes(' ')) {
this.open('emoji', emoji);
@ -122,7 +126,7 @@ export class Autocomplete {
}
}
if (isMfmTag && !opened) {
if (isMfmTag && !opened && this.onlyType.includes('mfmTag')) {
const mfmTag = text.substring(mfmTagIndex + 1);
if (!mfmTag.includes(' ')) {
this.open('mfmTag', mfmTag.replace('[', ''));

View file

@ -0,0 +1,15 @@
import { unisonReload } from '@/scripts/unison-reload.js';
import * as os from '@/os.js';
import { miLocalStorage } from '@/local-storage.js';
import { fetchCustomEmojis } from '@/custom-emojis.js';
export async function clearCache() {
os.waiting();
miLocalStorage.removeItem('locale');
miLocalStorage.removeItem('localeVersion');
miLocalStorage.removeItem('theme');
miLocalStorage.removeItem('emojis');
miLocalStorage.removeItem('lastEmojisFetchedAt');
await fetchCustomEmojis(true);
unisonReload();
}

View file

@ -0,0 +1,60 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { defineAsyncComponent, Ref, ref } from 'vue';
import { popup } from '@/os.js';
import { defaultStore } from '@/store.js';
/**
*
* {@link ReactionPicker}
* 稿
* 使
*/
class EmojiPicker {
private src: Ref<HTMLElement | null> = ref(null);
private manualShowing = ref(false);
private onChosen?: (emoji: string) => void;
private onClosed?: () => void;
constructor() {
// nop
}
public async init() {
const emojisRef = defaultStore.reactiveState.pinnedEmojis;
await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
src: this.src,
pinnedEmojis: emojisRef,
asReactionPicker: false,
manualShowing: this.manualShowing,
choseAndClose: false,
}, {
done: emoji => {
if (this.onChosen) this.onChosen(emoji);
},
close: () => {
this.manualShowing.value = false;
},
closed: () => {
this.src.value = null;
if (this.onClosed) this.onClosed();
},
});
}
public show(
src: HTMLElement,
onChosen?: EmojiPicker['onChosen'],
onClosed?: EmojiPicker['onClosed'],
) {
this.src.value = src;
this.manualShowing.value = true;
this.onChosen = onChosen;
this.onClosed = onClosed;
}
}
export const emojiPicker = new EmojiPicker();

View file

@ -43,3 +43,9 @@ export function getEmojiName(char: string): string | null {
return emojilist[idx].name;
}
}
export interface CustomEmojiFolderTree {
value: string;
category: string;
children: CustomEmojiFolderTree[];
}

View file

@ -82,7 +82,7 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss
to: `/my/drive/file/${file.id}`,
text: i18n.ts._fileViewer.title,
icon: 'ph-file-text ph-bold ph-lg',
}, null, {
}, { type: 'divider' }, {
text: i18n.ts.rename,
icon: 'ph-textbox ph-bold ph-lg',
action: () => rename(file),
@ -101,7 +101,7 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss
aspectRatio: NaN,
uploadFolder: folder ? folder.id : folder,
}),
}] : [], null, {
}] : [], { type: 'divider' }, {
text: i18n.ts.createNoteFromTheFile,
icon: 'ph-pencil ph-bold ph-lg',
action: () => os.post({
@ -118,7 +118,7 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss
text: i18n.ts.download,
icon: 'ph-download ph-bold ph-lg',
download: file.name,
}, null, {
}, { type: 'divider' }, {
text: i18n.ts.delete,
icon: 'ph-trash ph-bold ph-lg',
danger: true,
@ -126,7 +126,7 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss
}];
if (defaultStore.state.devMode) {
menu = menu.concat([null, {
menu = menu.concat([{ type: 'divider' }, {
icon: 'ph-identification-card ph-bold ph-lg',
text: i18n.ts.copyFileId,
action: () => {

View file

@ -18,6 +18,7 @@ import { getUserMenu } from '@/scripts/get-user-menu.js';
import { clipsCache } from '@/cache.js';
import { MenuItem } from '@/types/menu.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { isSupportShare } from '@/scripts/navigator.js';
export async function getNoteClipMenu(props: {
note: Misskey.entities.Note;
@ -60,7 +61,7 @@ export async function getNoteClipMenu(props: {
},
);
},
})), null, {
})), { type: 'divider' }, {
icon: 'ph-plus ph-bold ph-lg',
text: i18n.ts.createNew,
action: async () => {
@ -93,7 +94,7 @@ export async function getNoteClipMenu(props: {
}];
}
export function getAbuseNoteMenu(note: misskey.entities.Note, text: string): MenuItem {
export function getAbuseNoteMenu(note: Misskey.entities.Note, text: string): MenuItem {
return {
icon: 'ph-warning-circle ph-bold ph-lg',
text,
@ -107,7 +108,7 @@ export function getAbuseNoteMenu(note: misskey.entities.Note, text: string): Men
};
}
export function getCopyNoteLinkMenu(note: misskey.entities.Note, text: string): MenuItem {
export function getCopyNoteLinkMenu(note: Misskey.entities.Note, text: string): MenuItem {
return {
icon: 'ph-link ph-bold ph-lg',
text,
@ -285,7 +286,7 @@ export function getNoteMenu(props: {
text: i18n.ts.unclip,
danger: true,
action: unclip,
}, null] : []
}, { type: 'divider' }] : []
), {
icon: 'ph-info ph-bold ph-lg',
text: i18n.ts.details,
@ -302,20 +303,20 @@ export function getNoteMenu(props: {
icon: 'ph-arrow-square-out ph-bold ph-lg',
text: i18n.ts.showOnRemote,
action: () => {
window.open(appearNote.url ?? appearNote.uri, '_blank');
window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener');
},
} : undefined,
{
...(isSupportShare() ? [{
icon: 'ph-share-network ph-bold ph-lg',
text: i18n.ts.share,
action: share,
},
}] : []),
$i && $i.policies.canUseTranslator && instance.translatorAvailable ? {
icon: 'ph-translate ph-bold ph-lg',
text: i18n.ts.translate,
action: translate,
} : undefined,
null,
{ type: 'divider' },
statePromise.then(state => state.isFavorited ? {
icon: 'ph-star-half ph-bold ph-lg',
text: i18n.ts.unfavorite,
@ -362,7 +363,7 @@ export function getNoteMenu(props: {
},
/*
...($i.isModerator || $i.isAdmin ? [
null,
{ type: 'divider' },
{
icon: 'ph-megaphone ph-bold ph-lg',
text: i18n.ts.promote,
@ -371,13 +372,13 @@ export function getNoteMenu(props: {
: []
),*/
...(appearNote.userId !== $i.id ? [
null,
{ type: 'divider' },
appearNote.userId !== $i.id ? getAbuseNoteMenu(appearNote, i18n.ts.reportAbuse) : undefined,
]
: []
),
...(appearNote.userId === $i.id || $i.isModerator || $i.isAdmin ? [
null,
{ type: 'divider' },
appearNote.userId === $i.id ? {
icon: 'ph-pencil ph-bold ph-lg',
text: i18n.ts.edit,
@ -415,14 +416,14 @@ export function getNoteMenu(props: {
icon: 'ph-arrow-square-out ph-bold ph-lg',
text: i18n.ts.showOnRemote,
action: () => {
window.open(appearNote.url ?? appearNote.uri, '_blank');
window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener');
},
} : undefined]
.filter(x => x !== undefined);
}
if (noteActions.length > 0) {
menu = menu.concat([null, ...noteActions.map(action => ({
menu = menu.concat([{ type: "divider" }, ...noteActions.map(action => ({
icon: 'ph-plug ph-bold ph-lg',
text: action.title,
action: () => {
@ -432,7 +433,7 @@ export function getNoteMenu(props: {
}
if (defaultStore.state.devMode) {
menu = menu.concat([null, {
menu = menu.concat([{ type: "divider" }, {
icon: 'ph-identification-card ph-bold ph-lg',
text: i18n.ts.copyNoteId,
action: () => {
@ -518,7 +519,7 @@ export function getRenoteMenu(props: {
}]);
}
if (!appearNote.channel || appearNote.channel?.allowRenoteToExternal) {
if (!appearNote.channel || appearNote.channel.allowRenoteToExternal) {
normalRenoteItems.push(...[{
text: i18n.ts.renote,
icon: 'ti ti-repeat',
@ -561,10 +562,9 @@ export function getRenoteMenu(props: {
}]);
}
// nullを挟むことで区切り線を出せる
const renoteItems = [
...normalRenoteItems,
...(channelRenoteItems.length > 0 && normalRenoteItems.length > 0) ? [null] : [],
...(channelRenoteItems.length > 0 && normalRenoteItems.length > 0) ? [{ type: 'divider' }] : [],
...channelRenoteItems,
];

View file

@ -119,7 +119,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
userId: user.id,
});
}
async function invalidateFollow() {
if (!await getConfirmed(i18n.ts.breakFollowConfirm)) return;
@ -189,7 +189,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${user.host}`;
os.post({ specified: user, initialText: `${canonical} ` });
},
}, null, {
}, { type: 'divider' }, {
icon: 'ph-pencil ph-bold ph-lg',
text: i18n.ts.editMemo,
action: () => {
@ -313,7 +313,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
}]);
//}
menu = menu.concat([null, {
menu = menu.concat([{ type: 'divider' }, {
icon: user.isMuted ? 'ph-eye ph-bold ph-lg' : 'ph-eye-slash ph-bold ph-lg',
text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute,
action: toggleMute,
@ -335,7 +335,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
}]);
}
menu = menu.concat([null, {
menu = menu.concat([{ type: 'divider' }, {
icon: 'ph-warning-circle ph-bold ph-lg',
text: i18n.ts.reportAbuse,
action: reportAbuse,
@ -343,15 +343,15 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
}
if (user.host !== null) {
menu = menu.concat([null, {
menu = menu.concat([{ type: 'divider' }, {
icon: 'ph-arrows-counter-clockwise ph-bold ph-lg',
text: i18n.ts.updateRemoteUser,
action: userInfoUpdate,
}]);
}
if (defaultStore.state.devMode) {
menu = menu.concat([null, {
menu = menu.concat([{ type: 'divider' }, {
icon: 'ph-identification-card ph-bold ph-lg',
text: i18n.ts.copyUserId,
action: () => {
@ -361,7 +361,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
}
if ($i && meId === user.id) {
menu = menu.concat([null, {
menu = menu.concat([{ type: 'divider' }, {
icon: 'ph-pencil ph-bold ph-lg',
text: i18n.ts.editProfile,
action: () => {
@ -371,7 +371,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
}
if (userActions.length > 0) {
menu = menu.concat([null, ...userActions.map(action => ({
menu = menu.concat([{ type: 'divider' }, ...userActions.map(action => ({
icon: 'ph-plug ph-bold ph-lg',
text: action.title,
action: () => {

View file

@ -6,11 +6,19 @@
import * as Misskey from 'misskey-js';
import { $i } from '@/account.js';
export function isFfVisibleForMe(user: Misskey.entities.UserDetailed): boolean {
export function isFollowingVisibleForMe(user: Misskey.entities.UserDetailed): boolean {
if ($i && $i.id === user.id) return true;
if (user.ffVisibility === 'private') return false;
if (user.ffVisibility === 'followers' && !user.isFollowing) return false;
if (user.followingVisibility === 'private') return false;
if (user.followingVisibility === 'followers' && !user.isFollowing) return false;
return true;
}
export function isFollowersVisibleForMe(user: Misskey.entities.UserDetailed): boolean {
if ($i && $i.id === user.id) return true;
if (user.followersVisibility === 'private') return false;
if (user.followersVisibility === 'followers' && !user.isFollowing) return false;
return true;
}

View file

@ -0,0 +1,9 @@
export default async function hasAudio(media: HTMLMediaElement) {
const cloned = media.cloneNode() as HTMLMediaElement;
cloned.muted = (cloned as typeof cloned & Partial<HTMLVideoElement>).playsInline = true;
cloned.play();
await new Promise((resolve) => cloned.addEventListener('playing', resolve));
const result = !!(cloned as any).audioTracks?.length || (cloned as any).mozHasAudio || !!(cloned as any).webkitAudioDecodedByteCount;
cloned.remove();
return result;
}

View file

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export function isSupportShare(): boolean {
return 'share' in navigator;
}

View file

@ -15,6 +15,7 @@ export type PageMetadata = {
icon?: string | null;
avatar?: Misskey.entities.User | null;
userName?: Misskey.entities.User | null;
needWideArea?: boolean;
};
export function definePageMetadata(metadata: PageMetadata | null | Ref<PageMetadata | null> | ComputedRef<PageMetadata | null>): void {

View file

@ -0,0 +1,25 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export const postMessageEventTypes = [
'misskey:shareForm:shareCompleted',
] as const;
export type PostMessageEventType = typeof postMessageEventTypes[number];
export type MiPostMessageEvent = {
type: PostMessageEventType;
payload?: any;
};
/**
*
*/
export function postMessageToParentWindow(type: PostMessageEventType, payload?: any): void {
window.postMessage({
type,
payload,
}, '*');
}

View file

@ -5,6 +5,7 @@
import { defineAsyncComponent, Ref, ref } from 'vue';
import { popup } from '@/os.js';
import { defaultStore } from '@/store.js';
class ReactionPicker {
private src: Ref<HTMLElement | null> = ref(null);
@ -17,25 +18,27 @@ class ReactionPicker {
}
public async init() {
const reactionsRef = defaultStore.reactiveState.reactions;
await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
src: this.src,
pinnedEmojis: reactionsRef,
asReactionPicker: true,
manualShowing: this.manualShowing,
}, {
done: reaction => {
this.onChosen!(reaction);
if (this.onChosen) this.onChosen(reaction);
},
close: () => {
this.manualShowing.value = false;
},
closed: () => {
this.src.value = null;
this.onClosed!();
if (this.onClosed) this.onClosed();
},
});
}
public show(src: HTMLElement, onChosen: ReactionPicker['onChosen'], onClosed: ReactionPicker['onClosed']) {
public show(src: HTMLElement, onChosen?: ReactionPicker['onChosen'], onClosed?: ReactionPicker['onClosed']) {
this.src.value = src;
this.manualShowing.value = true;
this.onChosen = onChosen;

File diff suppressed because one or more lines are too long

View file

@ -3,13 +3,22 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { SoundStore } from '@/store.js';
import { defaultStore } from '@/store.js';
import * as os from '@/os.js';
const ctx = new AudioContext();
let ctx: AudioContext;
const cache = new Map<string, AudioBuffer>();
let canPlay = true;
export const soundsTypes = [
// 音声なし
null,
// ドライブの音声
'_driveFile_',
// プリインストール
'syuilo/n-aec',
'syuilo/n-aec-4va',
'syuilo/n-aec-4vb',
@ -38,6 +47,8 @@ export const soundsTypes = [
'syuilo/waon',
'syuilo/popo',
'syuilo/triple',
'syuilo/bubble1',
'syuilo/bubble2',
'syuilo/poi1',
'syuilo/poi2',
'syuilo/pirori',
@ -61,46 +72,161 @@ export const soundsTypes = [
'noizenecio/kick_gaba7',
] as const;
export async function getAudio(file: string, useCache = true) {
if (useCache && cache.has(file)) {
return cache.get(file)!;
export const operationTypes = [
'noteMy',
'note',
'antenna',
'channel',
'notification',
'reaction',
] as const;
/** サウンドの種類 */
export type SoundType = typeof soundsTypes[number];
/** スプライトの種類 */
export type OperationType = typeof operationTypes[number];
/**
*
* @param soundStore
* @param options `useCache`: `true`
*/
export async function loadAudio(soundStore: SoundStore, 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 (_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;
}
}
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;
}
}
const response = await fetch(`/client-assets/sounds/${file}.mp3`);
const arrayBuffer = await response.arrayBuffer();
const audioBuffer = await ctx.decodeAudioData(arrayBuffer);
if (useCache) {
cache.set(file, audioBuffer);
if (options?.useCache ?? true) {
if (soundStore.type === '_driveFile_') {
cache.set(soundStore.fileId, audioBuffer);
} else {
cache.set(soundStore.type, audioBuffer);
}
}
return audioBuffer;
}
export function setVolume(audio: HTMLAudioElement, volume: number): HTMLAudioElement {
const masterVolume = defaultStore.state.sound_masterVolume;
audio.volume = masterVolume - ((1 - volume) * masterVolume);
return audio;
/**
*
* @param type
*/
export function play(operationType: OperationType) {
const sound = defaultStore.state[`sound_${operationType}`];
if (_DEV_) console.log('play', operationType, sound);
if (sound.type == null || !canPlay) return;
canPlay = false;
playFile(sound).finally(() => {
// ごく短時間に音が重複しないように
setTimeout(() => {
canPlay = true;
}, 25);
});
}
export function play(type: 'noteMy' | 'note' | 'antenna' | 'channel' | 'notification') {
const sound = defaultStore.state[`sound_${type}`];
if (_DEV_) console.log('play', type, sound);
if (sound.type == null) return;
playFile(sound.type, sound.volume);
/**
*
* @param soundStore
*/
export async function playFile(soundStore: SoundStore) {
const buffer = await loadAudio(soundStore);
if (!buffer) return;
createSourceNode(buffer, soundStore.volume)?.start();
}
export async function playFile(file: string, volume: number) {
export function createSourceNode(buffer: AudioBuffer, volume: number) : AudioBufferSourceNode | null {
const masterVolume = defaultStore.state.sound_masterVolume;
if (masterVolume === 0 || volume === 0) {
return;
if (isMute() || masterVolume === 0 || volume === 0) {
return null;
}
const gainNode = ctx.createGain();
gainNode.gain.value = masterVolume * volume;
const soundSource = ctx.createBufferSource();
soundSource.buffer = await getAudio(file);
soundSource.buffer = buffer;
soundSource.connect(gainNode).connect(ctx.destination);
soundSource.start();
return soundSource;
}
/**
*
* @param file URLIDではない
*/
export async function getSoundDuration(file: string): Promise<number> {
const audioEl = document.createElement('audio');
audioEl.src = file;
return new Promise((resolve) => {
const si = setInterval(() => {
if (audioEl.readyState > 0) {
resolve(audioEl.duration * 1000);
clearInterval(si);
audioEl.remove();
}
}, 100);
});
}
/**
*
*/
export function isMute(): boolean {
if (defaultStore.state.sound_notUseSound) {
// サウンドを出力しない
return true;
}
// noinspection RedundantIfStatementJS
if (defaultStore.state.sound_useSoundOnlyWhenActive && document.visibilityState === 'hidden') {
// ブラウザがアクティブな時のみサウンドを出力する
return true;
}
return false;
}

View file

@ -44,7 +44,7 @@ export const getBuiltinThemes = () => Promise.all(
'd-cherry',
'd-ice',
'd-u0',
].map(name => import(`../themes/${name}.json5`).then(({ default: _default }): Theme => _default)),
].map(name => import(`@/themes/${name}.json5`).then(({ default: _default }): Theme => _default)),
);
export const getBuiltinThemesRef = () => {