Merge tag '2023.11.0' into merge-upstream
This commit is contained in:
commit
2bc887a6c5
301 changed files with 9351 additions and 3283 deletions
|
|
@ -82,6 +82,7 @@ export const ACHIEVEMENT_TYPES = [
|
|||
'cookieClicked',
|
||||
'brainDiver',
|
||||
'smashTestNotificationButton',
|
||||
'tutorialCompleted',
|
||||
] as const;
|
||||
|
||||
export const ACHIEVEMENT_BADGES = {
|
||||
|
|
@ -460,6 +461,11 @@ export const ACHIEVEMENT_BADGES = {
|
|||
bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))',
|
||||
frame: 'bronze',
|
||||
},
|
||||
'tutorialCompleted': {
|
||||
img: '/fluent-emoji/1f393.png',
|
||||
bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
|
||||
frame: 'bronze',
|
||||
},
|
||||
/* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107>
|
||||
} as const satisfies Record<typeof ACHIEVEMENT_TYPES[number], {
|
||||
img: string;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ 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 createAiScriptEnv(opts) {
|
||||
return {
|
||||
|
|
@ -71,5 +72,9 @@ export function createAiScriptEnv(opts) {
|
|||
'Mk:url': values.FN_NATIVE(() => {
|
||||
return values.STR(window.location.href);
|
||||
}),
|
||||
'Mk:nyaize': values.FN_NATIVE(([text]) => {
|
||||
utils.assertString(text);
|
||||
return values.STR(nyaize(text.value));
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
31
packages/frontend/src/scripts/code-highlighter.ts
Normal file
31
packages/frontend/src/scripts/code-highlighter.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { setWasm, setCDN, Highlighter, getHighlighter as _getHighlighter } from 'shiki';
|
||||
|
||||
setWasm('/assets/shiki/dist/onig.wasm');
|
||||
setCDN('/assets/shiki/');
|
||||
|
||||
let _highlighter: Highlighter | null = null;
|
||||
|
||||
export async function getHighlighter(): Promise<Highlighter> {
|
||||
if (!_highlighter) {
|
||||
return await initHighlighter();
|
||||
}
|
||||
return _highlighter;
|
||||
}
|
||||
|
||||
export async function initHighlighter() {
|
||||
const highlighter = await _getHighlighter({
|
||||
theme: 'dark-plus',
|
||||
langs: ['js'],
|
||||
});
|
||||
|
||||
await highlighter.loadLanguage({
|
||||
path: 'languages/aiscript.tmLanguage.json',
|
||||
id: 'aiscript',
|
||||
scopeName: 'source.aiscript',
|
||||
aliases: ['is', 'ais'],
|
||||
});
|
||||
|
||||
_highlighter = highlighter;
|
||||
|
||||
return highlighter;
|
||||
}
|
||||
|
|
@ -3,12 +3,9 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as mfm from 'mfm-js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { extractUrlFromMfm } from './extract-url-from-mfm.js';
|
||||
|
||||
export function shouldCollapsed(note: Misskey.entities.Note): boolean {
|
||||
const urls = note.text ? extractUrlFromMfm(mfm.parse(note.text)) : null;
|
||||
export function shouldCollapsed(note: Misskey.entities.Note, urls: string[]): boolean {
|
||||
const collapsed = note.cw == null && note.text != null && (
|
||||
(note.text.includes('$[x2')) ||
|
||||
(note.text.includes('$[x3')) ||
|
||||
|
|
@ -17,7 +14,7 @@ export function shouldCollapsed(note: Misskey.entities.Note): boolean {
|
|||
(note.text.split('\n').length > 9) ||
|
||||
(note.text.length > 500) ||
|
||||
(note.files.length >= 5) ||
|
||||
(!!urls && urls.length >= 4)
|
||||
(urls.length >= 4)
|
||||
);
|
||||
|
||||
return collapsed;
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ function copyUrl(file: Misskey.entities.DriveFile) {
|
|||
copyToClipboard(file.url);
|
||||
os.success();
|
||||
}
|
||||
|
||||
/*
|
||||
function addApp() {
|
||||
alert('not implemented yet');
|
||||
|
|
@ -77,6 +78,11 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss
|
|||
const isImage = file.type.startsWith('image/');
|
||||
let menu;
|
||||
menu = [{
|
||||
type: 'link',
|
||||
to: `/my/drive/file/${file.id}`,
|
||||
text: i18n.ts._fileViewer.title,
|
||||
icon: 'ti ti-info-circle',
|
||||
}, null, {
|
||||
text: i18n.ts.rename,
|
||||
icon: 'ti ti-forms',
|
||||
action: () => rename(file),
|
||||
|
|
@ -112,11 +118,6 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss
|
|||
text: i18n.ts.download,
|
||||
icon: 'ti ti-download',
|
||||
download: file.name,
|
||||
}, null, {
|
||||
type: 'link',
|
||||
to: `/my/drive/file/${file.id}`,
|
||||
text: i18n.ts._fileViewer.title,
|
||||
icon: 'ti ti-file',
|
||||
}, null, {
|
||||
text: i18n.ts.delete,
|
||||
icon: 'ti ti-trash',
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { miLocalStorage } from '@/local-storage.js';
|
|||
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';
|
||||
|
||||
export async function getNoteClipMenu(props: {
|
||||
note: Misskey.entities.Note;
|
||||
|
|
@ -418,3 +419,122 @@ export function getNoteMenu(props: {
|
|||
cleanup,
|
||||
};
|
||||
}
|
||||
|
||||
type Visibility = 'public' | 'home' | 'followers' | 'specified';
|
||||
|
||||
// defaultStore.state.visibilityがstringなためstringも受け付けている
|
||||
function smallerVisibility(a: Visibility | string, b: Visibility | string): Visibility {
|
||||
if (a === 'specified' || b === 'specified') return 'specified';
|
||||
if (a === 'followers' || b === 'followers') return 'followers';
|
||||
if (a === 'home' || b === 'home') return 'home';
|
||||
// if (a === 'public' || b === 'public')
|
||||
return 'public';
|
||||
}
|
||||
|
||||
export function getRenoteMenu(props: {
|
||||
note: Misskey.entities.Note;
|
||||
renoteButton: Ref<HTMLElement>;
|
||||
mock?: boolean;
|
||||
}) {
|
||||
const isRenote = (
|
||||
props.note.renote != null &&
|
||||
props.note.text == null &&
|
||||
props.note.fileIds.length === 0 &&
|
||||
props.note.poll == null
|
||||
);
|
||||
|
||||
const appearNote = isRenote ? props.note.renote as Misskey.entities.Note : props.note;
|
||||
|
||||
const channelRenoteItems: MenuItem[] = [];
|
||||
const normalRenoteItems: MenuItem[] = [];
|
||||
|
||||
if (appearNote.channel) {
|
||||
channelRenoteItems.push(...[{
|
||||
text: i18n.ts.inChannelRenote,
|
||||
icon: 'ti ti-repeat',
|
||||
action: () => {
|
||||
const el = props.renoteButton.value as HTMLElement | null | undefined;
|
||||
if (el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = rect.left + (el.offsetWidth / 2);
|
||||
const y = rect.top + (el.offsetHeight / 2);
|
||||
os.popup(MkRippleEffect, { x, y }, {}, 'end');
|
||||
}
|
||||
|
||||
if (!props.mock) {
|
||||
os.api('notes/create', {
|
||||
renoteId: appearNote.id,
|
||||
channelId: appearNote.channelId,
|
||||
}).then(() => {
|
||||
os.toast(i18n.ts.renoted);
|
||||
});
|
||||
}
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.inChannelQuote,
|
||||
icon: 'ti ti-quote',
|
||||
action: () => {
|
||||
if (!props.mock) {
|
||||
os.post({
|
||||
renote: appearNote,
|
||||
channel: appearNote.channel,
|
||||
});
|
||||
}
|
||||
},
|
||||
}]);
|
||||
}
|
||||
|
||||
if (!appearNote.channel || appearNote.channel?.allowRenoteToExternal) {
|
||||
normalRenoteItems.push(...[{
|
||||
text: i18n.ts.renote,
|
||||
icon: 'ti ti-repeat',
|
||||
action: () => {
|
||||
const el = props.renoteButton.value as HTMLElement | null | undefined;
|
||||
if (el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = rect.left + (el.offsetWidth / 2);
|
||||
const y = rect.top + (el.offsetHeight / 2);
|
||||
os.popup(MkRippleEffect, { x, y }, {}, 'end');
|
||||
}
|
||||
|
||||
const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility;
|
||||
const localOnly = defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly;
|
||||
|
||||
let visibility = appearNote.visibility;
|
||||
visibility = smallerVisibility(visibility, configuredVisibility);
|
||||
if (appearNote.channel?.isSensitive) {
|
||||
visibility = smallerVisibility(visibility, 'home');
|
||||
}
|
||||
|
||||
if (!props.mock) {
|
||||
os.api('notes/create', {
|
||||
localOnly,
|
||||
visibility,
|
||||
renoteId: appearNote.id,
|
||||
}).then(() => {
|
||||
os.toast(i18n.ts.renoted);
|
||||
});
|
||||
}
|
||||
},
|
||||
}, (props.mock) ? undefined : {
|
||||
text: i18n.ts.quote,
|
||||
icon: 'ti ti-quote',
|
||||
action: () => {
|
||||
os.post({
|
||||
renote: appearNote,
|
||||
});
|
||||
},
|
||||
}]);
|
||||
}
|
||||
|
||||
// nullを挟むことで区切り線を出せる
|
||||
const renoteItems = [
|
||||
...normalRenoteItems,
|
||||
...(channelRenoteItems.length > 0 && normalRenoteItems.length > 0) ? [null] : [],
|
||||
...channelRenoteItems,
|
||||
];
|
||||
|
||||
return {
|
||||
menu: renoteItems,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,6 +114,12 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
|
|||
return !confirm.canceled;
|
||||
}
|
||||
|
||||
async function userInfoUpdate() {
|
||||
os.apiWithDialog('federation/update-remote-user', {
|
||||
userId: user.id,
|
||||
});
|
||||
}
|
||||
|
||||
async function invalidateFollow() {
|
||||
if (!await getConfirmed(i18n.ts.breakFollowConfirm)) return;
|
||||
|
||||
|
|
@ -330,6 +336,14 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
|
|||
}]);
|
||||
}
|
||||
|
||||
if (user.host !== null) {
|
||||
menu = menu.concat([null, {
|
||||
icon: 'ti ti-refresh',
|
||||
text: i18n.ts.updateRemoteUser,
|
||||
action: userInfoUpdate,
|
||||
}]);
|
||||
}
|
||||
|
||||
if (defaultStore.state.devMode) {
|
||||
menu = menu.concat([null, {
|
||||
icon: 'ti ti-id',
|
||||
|
|
|
|||
129
packages/frontend/src/scripts/install-plugin.ts
Normal file
129
packages/frontend/src/scripts/install-plugin.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import { compareVersions } from 'compare-versions';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
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 { i18n } from '@/i18n.js';
|
||||
|
||||
export type AiScriptPluginMeta = {
|
||||
name: string;
|
||||
version: string;
|
||||
author: string;
|
||||
description?: string;
|
||||
permissions?: string[];
|
||||
config?: Record<string, any>;
|
||||
};
|
||||
|
||||
const parser = new Parser();
|
||||
|
||||
export function savePlugin({ id, meta, src, token }: {
|
||||
id: string;
|
||||
meta: AiScriptPluginMeta;
|
||||
src: string;
|
||||
token: string;
|
||||
}) {
|
||||
ColdDeviceStorage.set('plugins', ColdDeviceStorage.get('plugins').concat({
|
||||
...meta,
|
||||
id,
|
||||
active: true,
|
||||
configData: {},
|
||||
token: token,
|
||||
src: src,
|
||||
} as Plugin));
|
||||
}
|
||||
|
||||
export function isSupportedAiScriptVersion(version: string): boolean {
|
||||
try {
|
||||
return (compareVersions(version, '0.12.0') >= 0);
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function parsePluginMeta(code: string): Promise<AiScriptPluginMeta> {
|
||||
if (!code) {
|
||||
throw new Error('code is required');
|
||||
}
|
||||
|
||||
const lv = utils.getLangVersion(code);
|
||||
if (lv == null) {
|
||||
throw new Error('No language version annotation found');
|
||||
} else if (!isSupportedAiScriptVersion(lv)) {
|
||||
throw new Error(`Aiscript version '${lv}' is not supported`);
|
||||
}
|
||||
|
||||
let ast;
|
||||
try {
|
||||
ast = parser.parse(code);
|
||||
} catch (err) {
|
||||
throw new Error('Aiscript syntax error');
|
||||
}
|
||||
|
||||
const meta = Interpreter.collectMetadata(ast);
|
||||
if (meta == null) {
|
||||
throw new Error('Meta block not found');
|
||||
}
|
||||
|
||||
const metadata = meta.get(null);
|
||||
if (metadata == null) {
|
||||
throw new Error('Metadata not found');
|
||||
}
|
||||
|
||||
const { name, version, author, description, permissions, config } = metadata;
|
||||
if (name == null || version == null || author == null) {
|
||||
throw new Error('Required property not found');
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
version,
|
||||
author,
|
||||
description,
|
||||
permissions,
|
||||
config,
|
||||
};
|
||||
}
|
||||
|
||||
export async function installPlugin(code: string, meta?: AiScriptPluginMeta) {
|
||||
if (!code) return;
|
||||
|
||||
let realMeta: AiScriptPluginMeta;
|
||||
if (!meta) {
|
||||
realMeta = await parsePluginMeta(code);
|
||||
} else {
|
||||
realMeta = meta;
|
||||
}
|
||||
|
||||
const token = realMeta.permissions == null || realMeta.permissions.length === 0 ? null : await new Promise((res, rej) => {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), {
|
||||
title: i18n.ts.tokenRequested,
|
||||
information: i18n.ts.pluginTokenRequestedDescription,
|
||||
initialName: realMeta.name,
|
||||
initialPermissions: realMeta.permissions,
|
||||
}, {
|
||||
done: async result => {
|
||||
const { name, permissions } = result;
|
||||
const { token } = await os.api('miauth/gen-token', {
|
||||
session: null,
|
||||
name: name,
|
||||
permission: permissions,
|
||||
});
|
||||
res(token);
|
||||
},
|
||||
}, 'closed');
|
||||
});
|
||||
|
||||
savePlugin({
|
||||
id: uuid(),
|
||||
meta: realMeta,
|
||||
token,
|
||||
src: code,
|
||||
});
|
||||
}
|
||||
37
packages/frontend/src/scripts/install-theme.ts
Normal file
37
packages/frontend/src/scripts/install-theme.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import JSON5 from 'json5';
|
||||
import { addTheme, getThemes } from '@/theme-store.js';
|
||||
import { Theme, applyTheme, validateTheme } from '@/scripts/theme.js';
|
||||
|
||||
export function parseThemeCode(code: string): Theme {
|
||||
let theme;
|
||||
|
||||
try {
|
||||
theme = JSON5.parse(code);
|
||||
} catch (err) {
|
||||
throw new Error('Failed to parse theme json');
|
||||
}
|
||||
if (!validateTheme(theme)) {
|
||||
throw new Error('This theme is invaild');
|
||||
}
|
||||
if (getThemes().some(t => t.id === theme.id)) {
|
||||
throw new Error('This theme is already installed');
|
||||
}
|
||||
|
||||
return theme;
|
||||
}
|
||||
|
||||
export function previewTheme(code: string): void {
|
||||
const theme = parseThemeCode(code);
|
||||
if (theme) applyTheme(theme, false);
|
||||
}
|
||||
|
||||
export async function installTheme(code: string): Promise<void> {
|
||||
const theme = parseThemeCode(code);
|
||||
if (!theme) return;
|
||||
await addTheme(theme);
|
||||
}
|
||||
|
|
@ -6,12 +6,41 @@
|
|||
import { lang } from '@/config.js';
|
||||
|
||||
export const versatileLang = (lang ?? 'ja-JP').replace('ja-KS', 'ja-JP');
|
||||
export const dateTimeFormat = new Intl.DateTimeFormat(versatileLang, {
|
||||
year: 'numeric',
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
second: 'numeric',
|
||||
});
|
||||
export const numberFormat = new Intl.NumberFormat(versatileLang);
|
||||
|
||||
let _dateTimeFormat: Intl.DateTimeFormat;
|
||||
try {
|
||||
_dateTimeFormat = new Intl.DateTimeFormat(versatileLang, {
|
||||
year: 'numeric',
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
second: 'numeric',
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
if (_DEV_) console.log('[Intl] Fallback to en-US');
|
||||
|
||||
// Fallback to en-US
|
||||
_dateTimeFormat = new Intl.DateTimeFormat('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
second: 'numeric',
|
||||
});
|
||||
}
|
||||
export const dateTimeFormat = _dateTimeFormat;
|
||||
|
||||
let _numberFormat: Intl.NumberFormat;
|
||||
try {
|
||||
_numberFormat = new Intl.NumberFormat(versatileLang);
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
if (_DEV_) console.log('[Intl] Fallback to en-US');
|
||||
|
||||
// Fallback to en-US
|
||||
_numberFormat = new Intl.NumberFormat('en-US');
|
||||
}
|
||||
export const numberFormat = _numberFormat;
|
||||
|
|
|
|||
|
|
@ -3,18 +3,25 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
const enRegex1 = /(?<=n)a/gi;
|
||||
const enRegex2 = /(?<=morn)ing/gi;
|
||||
const enRegex3 = /(?<=every)one/gi;
|
||||
const koRegex1 = /[나-낳]/g;
|
||||
const koRegex2 = /(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm;
|
||||
const koRegex3 = /(야(?=\?))|(야$)|(야(?= ))/gm;
|
||||
|
||||
export function nyaize(text: string): string {
|
||||
return text
|
||||
// ja-JP
|
||||
.replaceAll('な', 'にゃ').replaceAll('ナ', 'ニャ').replaceAll('ナ', 'ニャ')
|
||||
// en-US
|
||||
.replace(/(?<=n)a/gi, x => x === 'A' ? 'YA' : 'ya')
|
||||
.replace(/(?<=morn)ing/gi, x => x === 'ING' ? 'YAN' : 'yan')
|
||||
.replace(/(?<=every)one/gi, x => x === 'ONE' ? 'NYAN' : 'nyan')
|
||||
.replace(enRegex1, x => x === 'A' ? 'YA' : 'ya')
|
||||
.replace(enRegex2, x => x === 'ING' ? 'YAN' : 'yan')
|
||||
.replace(enRegex3, x => x === 'ONE' ? 'NYAN' : 'nyan')
|
||||
// ko-KR
|
||||
.replace(/[나-낳]/g, match => String.fromCharCode(
|
||||
.replace(koRegex1, match => String.fromCharCode(
|
||||
match.charCodeAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0),
|
||||
))
|
||||
.replace(/(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm, '다냥')
|
||||
.replace(/(야(?=\?))|(야$)|(야(?= ))/gm, '냥');
|
||||
.replace(koRegex2, '다냥')
|
||||
.replace(koRegex3, '냥');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ export function onScrollTop(el: HTMLElement, cb: () => unknown, tolerance = 1, o
|
|||
};
|
||||
|
||||
function removeListener() { container.removeEventListener('scroll', onScroll); }
|
||||
|
||||
container.addEventListener('scroll', onScroll, { passive: true });
|
||||
return removeListener;
|
||||
}
|
||||
|
|
@ -71,6 +72,7 @@ export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1
|
|||
function removeListener() {
|
||||
containerOrWindow.removeEventListener('scroll', onScroll);
|
||||
}
|
||||
|
||||
containerOrWindow.addEventListener('scroll', onScroll, { passive: true });
|
||||
return removeListener;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,47 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { markRaw } from 'vue';
|
||||
import { Storage } from '@/pizzax.js';
|
||||
|
||||
export const soundConfigStore = markRaw(new Storage('sound', {
|
||||
sound_masterVolume: {
|
||||
where: 'device',
|
||||
default: 0.3,
|
||||
},
|
||||
sound_note: {
|
||||
where: 'account',
|
||||
default: { type: 'syuilo/n-aec', volume: 1 },
|
||||
},
|
||||
sound_noteMy: {
|
||||
where: 'account',
|
||||
default: { type: 'syuilo/n-cea-4va', volume: 1 },
|
||||
},
|
||||
sound_notification: {
|
||||
where: 'account',
|
||||
default: { type: 'syuilo/n-ea', volume: 1 },
|
||||
},
|
||||
sound_antenna: {
|
||||
where: 'account',
|
||||
default: { type: 'syuilo/triple', volume: 1 },
|
||||
},
|
||||
sound_channel: {
|
||||
where: 'account',
|
||||
default: { type: 'syuilo/square-pico', volume: 1 },
|
||||
},
|
||||
}));
|
||||
|
||||
await soundConfigStore.ready;
|
||||
|
||||
//#region サウンドのColdDeviceStorage => indexedDBのマイグレーション
|
||||
for (const target of Object.keys(soundConfigStore.state) as Array<keyof typeof soundConfigStore.state>) {
|
||||
const value = localStorage.getItem(`miux:${target}`);
|
||||
if (value) {
|
||||
soundConfigStore.set(target, JSON.parse(value) as typeof soundConfigStore.def[typeof target]['default']);
|
||||
localStorage.removeItem(`miux:${target}`);
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
const cache = new Map<string, HTMLAudioElement>();
|
||||
|
||||
|
|
@ -173,13 +133,13 @@ export function getAudio(file: string, useCache = true): HTMLAudioElement {
|
|||
}
|
||||
|
||||
export function setVolume(audio: HTMLAudioElement, volume: number): HTMLAudioElement {
|
||||
const masterVolume = soundConfigStore.state.sound_masterVolume;
|
||||
const masterVolume = defaultStore.state.sound_masterVolume;
|
||||
audio.volume = masterVolume - ((1 - volume) * masterVolume);
|
||||
return audio;
|
||||
}
|
||||
|
||||
export function play(type: 'noteMy' | 'note' | 'antenna' | 'channel' | 'notification') {
|
||||
const sound = soundConfigStore.state[`sound_${type}`];
|
||||
const sound = defaultStore.state[`sound_${type}`];
|
||||
if (_DEV_) console.log('play', type, sound);
|
||||
if (sound.type == null) return;
|
||||
playFile(sound.type, sound.volume);
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export function useTooltip(
|
|||
};
|
||||
|
||||
autoHidingTimer = window.setInterval(() => {
|
||||
if (!document.body.contains(elRef.value)) {
|
||||
if (elRef.value == null || !document.body.contains(elRef.value instanceof Element ? elRef.value : elRef.value.$el)) {
|
||||
if (!isHovering) return;
|
||||
isHovering = false;
|
||||
window.clearTimeout(timeoutId);
|
||||
|
|
|
|||
|
|
@ -71,9 +71,11 @@ export class WorkerMultiDispatch<POST = any, RETURN = any> {
|
|||
public isTerminated() {
|
||||
return this.terminated;
|
||||
}
|
||||
|
||||
public getWorkers() {
|
||||
return this.workers;
|
||||
}
|
||||
|
||||
public getSymbol() {
|
||||
return this.symbol;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue