merge: upstream
This commit is contained in:
commit
15d2319011
92 changed files with 1677 additions and 1086 deletions
|
@ -11,7 +11,7 @@ import { alert, confirm, popup, post, toast } from '@/os.js';
|
|||
import { useStream } from '@/stream.js';
|
||||
import * as sound from '@/scripts/sound.js';
|
||||
import { $i, signout, updateAccount } from '@/account.js';
|
||||
import { fetchInstance, instance } from '@/instance.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { ColdDeviceStorage, defaultStore } from '@/store.js';
|
||||
import { makeHotkey } from '@/scripts/hotkey.js';
|
||||
import { reactionPicker } from '@/scripts/reaction-picker.js';
|
||||
|
@ -233,12 +233,10 @@ export async function mainBoot() {
|
|||
}
|
||||
}
|
||||
|
||||
fetchInstance().then(() => {
|
||||
const modifiedVersionMustProminentlyOfferInAgplV3Section13Read = miLocalStorage.getItem('modifiedVersionMustProminentlyOfferInAgplV3Section13Read');
|
||||
if (modifiedVersionMustProminentlyOfferInAgplV3Section13Read !== 'true' && instance.repositoryUrl !== 'https://activitypub.software/TransFem-org/Sharkey/') {
|
||||
popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {}, 'closed');
|
||||
}
|
||||
});
|
||||
const modifiedVersionMustProminentlyOfferInAgplV3Section13Read = miLocalStorage.getItem('modifiedVersionMustProminentlyOfferInAgplV3Section13Read');
|
||||
if (modifiedVersionMustProminentlyOfferInAgplV3Section13Read !== 'true' && instance.repositoryUrl !== 'https://activitypub.software/TransFem-org/Sharkey/') {
|
||||
popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {}, 'closed');
|
||||
}
|
||||
|
||||
if ('Notification' in window) {
|
||||
// 許可を得ていなかったらリクエスト
|
||||
|
|
|
@ -57,18 +57,7 @@ import { i18n } from '@/i18n.js';
|
|||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { customEmojis } from '@/custom-emojis.js';
|
||||
import { MFM_TAGS, MFM_PARAMS } from '@/const.js';
|
||||
|
||||
type EmojiDef = {
|
||||
emoji: string;
|
||||
name: string;
|
||||
url: string;
|
||||
aliasOf?: string;
|
||||
} | {
|
||||
emoji: string;
|
||||
name: string;
|
||||
aliasOf?: string;
|
||||
isCustomEmoji?: true;
|
||||
};
|
||||
import { searchEmoji, EmojiDef } from '@/scripts/search-emoji.js';
|
||||
|
||||
const lib = emojilist.filter(x => x.category !== 'flags');
|
||||
|
||||
|
@ -249,7 +238,7 @@ function exec() {
|
|||
return;
|
||||
}
|
||||
|
||||
emojis.value = emojiAutoComplete(props.q.toLowerCase(), emojiDb.value);
|
||||
emojis.value = searchEmoji(props.q.toLowerCase(), emojiDb.value);
|
||||
} else if (props.type === 'mfmTag') {
|
||||
if (!props.q || props.q === '') {
|
||||
mfmTags.value = MFM_TAGS;
|
||||
|
@ -267,87 +256,6 @@ function exec() {
|
|||
}
|
||||
}
|
||||
|
||||
type EmojiScore = { emoji: EmojiDef, score: number };
|
||||
|
||||
function emojiAutoComplete(query: string | null, emojiDb: EmojiDef[], max = 30): EmojiDef[] {
|
||||
if (!query) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const matched = new Map<string, EmojiScore>();
|
||||
// 完全一致(エイリアス込み)
|
||||
emojiDb.some(x => {
|
||||
if (x.name.toLowerCase() === query && !matched.has(x.aliasOf ?? x.name)) {
|
||||
matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length + 2 });
|
||||
}
|
||||
return matched.size === max;
|
||||
});
|
||||
|
||||
// 前方一致(エイリアスなし)
|
||||
if (matched.size < max) {
|
||||
emojiDb.some(x => {
|
||||
if (x.name.startsWith(query) && !x.aliasOf) {
|
||||
matched.set(x.name, { emoji: x, score: query.length + 1 });
|
||||
}
|
||||
return matched.size === max;
|
||||
});
|
||||
}
|
||||
|
||||
// 前方一致(エイリアス込み)
|
||||
if (matched.size < max) {
|
||||
emojiDb.some(x => {
|
||||
if (x.name.toLowerCase().startsWith(query) && !matched.has(x.aliasOf ?? x.name)) {
|
||||
matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length });
|
||||
}
|
||||
return matched.size === max;
|
||||
});
|
||||
}
|
||||
|
||||
// 部分一致(エイリアス込み)
|
||||
if (matched.size < max) {
|
||||
emojiDb.some(x => {
|
||||
if (x.name.toLowerCase().includes(query) && !matched.has(x.aliasOf ?? x.name)) {
|
||||
matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length - 1 });
|
||||
}
|
||||
return matched.size === max;
|
||||
});
|
||||
}
|
||||
|
||||
// 簡易あいまい検索(3文字以上)
|
||||
if (matched.size < max && query.length > 3) {
|
||||
const queryChars = [...query];
|
||||
const hitEmojis = new Map<string, EmojiScore>();
|
||||
|
||||
for (const x of emojiDb) {
|
||||
// 文字列の位置を進めながら、クエリの文字を順番に探す
|
||||
|
||||
let pos = 0;
|
||||
let hit = 0;
|
||||
for (const c of queryChars) {
|
||||
pos = x.name.toLowerCase().indexOf(c, pos);
|
||||
if (pos <= -1) break;
|
||||
hit++;
|
||||
}
|
||||
|
||||
// 半分以上の文字が含まれていればヒットとする
|
||||
if (hit > Math.ceil(queryChars.length / 2) && hit - 2 > (matched.get(x.aliasOf ?? x.name)?.score ?? 0)) {
|
||||
hitEmojis.set(x.aliasOf ?? x.name, { emoji: x, score: hit - 2 });
|
||||
}
|
||||
}
|
||||
|
||||
// ヒットしたものを全部追加すると雑多になるので、先頭の6件程度だけにしておく(6件=オートコンプリートのポップアップのサイズ分)
|
||||
[...hitEmojis.values()]
|
||||
.sort((x, y) => y.score - x.score)
|
||||
.slice(0, 6)
|
||||
.forEach(it => matched.set(it.emoji.name, it));
|
||||
}
|
||||
|
||||
return [...matched.values()]
|
||||
.sort((x, y) => y.score - x.score)
|
||||
.slice(0, max)
|
||||
.map(it => it.emoji);
|
||||
}
|
||||
|
||||
function onMousedown(event: Event) {
|
||||
if (!contains(rootEl.value, event.target) && (rootEl.value !== event.target)) props.close();
|
||||
}
|
||||
|
|
|
@ -240,7 +240,7 @@ const render = () => {
|
|||
},
|
||||
external: externalTooltipHandler,
|
||||
callbacks: {
|
||||
label: (item) => chartData?.bytes ? bytes(item.parsed.y * 1000, 1) : item.parsed.y.toString(),
|
||||
label: (item) => `${item.dataset.label}: ${chartData?.bytes ? bytes(item.parsed.y * 1000, 1) : item.parsed.y.toString()}`,
|
||||
},
|
||||
},
|
||||
zoom: props.detailed ? {
|
||||
|
|
|
@ -52,7 +52,7 @@ async function fetchLanguage(to: string): Promise<void> {
|
|||
return bundle.id === language || bundle.aliases?.includes(language);
|
||||
});
|
||||
if (bundles.length > 0) {
|
||||
console.log(`Loading language: ${language}`);
|
||||
if (_DEV_) console.log(`Loading language: ${language}`);
|
||||
await highlighter.loadLanguage(bundles[0].import);
|
||||
codeLang.value = language;
|
||||
} else {
|
||||
|
|
|
@ -152,11 +152,11 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent): void {
|
|||
icon: 'ph-crop ph-bold ph-lg',
|
||||
action: () : void => { crop(file); },
|
||||
}] : [], {
|
||||
type: 'divider',
|
||||
}, {
|
||||
text: i18n.ts.attachCancel,
|
||||
icon: 'ph-x-circle ph-bold ph-lg',
|
||||
action: () => { detachMedia(file.id); },
|
||||
}, {
|
||||
type: 'divider',
|
||||
}, {
|
||||
text: i18n.ts.deleteFile,
|
||||
icon: 'ph-trash ph-bold ph-lg',
|
||||
|
|
|
@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton @click="close">{{ i18n.ts.gotIt }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
<button class="_button" :class="$style.close" @click="close"><i class="ti ti-x"></i></button>
|
||||
<button class="_button" :class="$style.close" @click="close"><i class="ph-x ph-bold ph-lg"></i></button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import MkTime from './MkTime.vue';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { dateTimeFormat } from '@/scripts/intl-const.js';
|
||||
const now = new Date('2023-04-01T00:00:00.000Z');
|
||||
const future = new Date(8640000000000000);
|
||||
const future = new Date('3000-04-01T00:00:00.000Z');
|
||||
const oneHourAgo = new Date(now.getTime() - 3600000);
|
||||
const oneDayAgo = new Date(now.getTime() - 86400000);
|
||||
const oneWeekAgo = new Date(now.getTime() - 604800000);
|
||||
|
@ -49,11 +49,12 @@ export const Empty = {
|
|||
export const RelativeFuture = {
|
||||
...Empty,
|
||||
async play({ canvasElement }) {
|
||||
await expect(canvasElement).toHaveTextContent(i18n.ts._ago.future);
|
||||
await expect(canvasElement).toHaveTextContent(i18n.tsx._timeIn.years({ n: 977 }));
|
||||
},
|
||||
args: {
|
||||
...Empty.args,
|
||||
time: future,
|
||||
origin: now,
|
||||
},
|
||||
} satisfies StoryObj<typeof MkTime>;
|
||||
export const AbsoluteFuture = {
|
||||
|
|
|
@ -99,7 +99,6 @@ export class UserPreview {
|
|||
this.el.removeEventListener('mouseover', this.onMouseover);
|
||||
this.el.removeEventListener('mouseleave', this.onMouseleave);
|
||||
this.el.removeEventListener('click', this.onClick);
|
||||
window.clearInterval(this.checkTimer);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,13 +11,24 @@ import { DEFAULT_INFO_IMAGE_URL, DEFAULT_NOT_FOUND_IMAGE_URL, DEFAULT_SERVER_ERR
|
|||
|
||||
// TODO: 他のタブと永続化されたstateを同期
|
||||
|
||||
const cached = miLocalStorage.getItem('instance');
|
||||
//#region loader
|
||||
const providedMetaEl = document.getElementById('misskey_meta');
|
||||
|
||||
let cachedMeta = miLocalStorage.getItem('instance') ? JSON.parse(miLocalStorage.getItem('instance')!) : null;
|
||||
let cachedAt = miLocalStorage.getItem('instanceCachedAt') ? parseInt(miLocalStorage.getItem('instanceCachedAt')!) : 0;
|
||||
const providedMeta = providedMetaEl && providedMetaEl.textContent ? JSON.parse(providedMetaEl.textContent) : null;
|
||||
const providedAt = providedMetaEl && providedMetaEl.dataset.generatedAt ? parseInt(providedMetaEl.dataset.generatedAt) : 0;
|
||||
if (providedAt > cachedAt) {
|
||||
miLocalStorage.setItem('instance', JSON.stringify(providedMeta));
|
||||
miLocalStorage.setItem('instanceCachedAt', providedAt.toString());
|
||||
cachedMeta = providedMeta;
|
||||
cachedAt = providedAt;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
// TODO: instanceをリアクティブにするかは再考の余地あり
|
||||
|
||||
export const instance: Misskey.entities.MetaResponse = reactive(cached ? JSON.parse(cached) : {
|
||||
// TODO: set default values
|
||||
});
|
||||
export const instance: Misskey.entities.MetaResponse = reactive(cachedMeta ?? {});
|
||||
|
||||
export const serverErrorImageUrl = computed(() => instance.serverErrorImageUrl ?? DEFAULT_SERVER_ERROR_IMAGE_URL);
|
||||
|
||||
|
@ -25,7 +36,15 @@ export const infoImageUrl = computed(() => instance.infoImageUrl ?? DEFAULT_INFO
|
|||
|
||||
export const notFoundImageUrl = computed(() => instance.notFoundImageUrl ?? DEFAULT_NOT_FOUND_IMAGE_URL);
|
||||
|
||||
export async function fetchInstance() {
|
||||
export async function fetchInstance(force = false): Promise<void> {
|
||||
if (!force) {
|
||||
const cachedAt = miLocalStorage.getItem('instanceCachedAt') ? parseInt(miLocalStorage.getItem('instanceCachedAt')!) : 0;
|
||||
|
||||
if (Date.now() - cachedAt < 1000 * 60 * 60) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const meta = await misskeyApi('meta', {
|
||||
detail: false,
|
||||
});
|
||||
|
@ -35,4 +54,5 @@ export async function fetchInstance() {
|
|||
}
|
||||
|
||||
miLocalStorage.setItem('instance', JSON.stringify(instance));
|
||||
miLocalStorage.setItem('instanceCachedAt', Date.now().toString());
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ type Keys =
|
|||
'v' |
|
||||
'lastVersion' |
|
||||
'instance' |
|
||||
'instanceCachedAt' |
|
||||
'account' |
|
||||
'accounts' |
|
||||
'latestDonationInfoShownAt' |
|
||||
|
|
|
@ -142,7 +142,7 @@ function save() {
|
|||
turnstileSiteKey: turnstileSiteKey.value,
|
||||
turnstileSecretKey: turnstileSecretKey.value,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -179,7 +179,7 @@ function save() {
|
|||
feedbackUrl: feedbackUrl.value === '' ? null : feedbackUrl.value,
|
||||
manifestJsonOverride: manifestJsonOverride.value === '' ? '{}' : JSON.stringify(JSON5.parse(manifestJsonOverride.value)),
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -124,7 +124,7 @@ function save() {
|
|||
smtpUser: smtpUser.value,
|
||||
smtpPass: smtpPass.value,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -75,7 +75,7 @@ function save() {
|
|||
deeplFreeMode: deeplFreeMode.value,
|
||||
deeplFreeInstance: deeplFreeInstance.value,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@ function save() {
|
|||
silencedHosts: silencedHosts.value.split('\n') || [],
|
||||
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -127,7 +127,7 @@ function save() {
|
|||
preservedUsernames: preservedUsernames.value.split('\n'),
|
||||
bubbleInstances: bubbleTimeline.value.split('\n'),
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -114,6 +114,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="log.type === 'updateRemoteInstanceNote'">
|
||||
<div>{{ i18n.ts.user }}: {{ log.info.userId }}</div>
|
||||
<div :class="$style.diff">
|
||||
<CodeDiff :context="5" :hideHeader="true" :oldString="log.info.before ?? ''" :newString="log.info.after ?? ''" maxHeight="300px"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<details>
|
||||
<summary>raw</summary>
|
||||
|
|
|
@ -143,7 +143,7 @@ function save() {
|
|||
objectStorageSetPublicRead: objectStorageSetPublicRead.value,
|
||||
objectStorageS3ForcePathStyle: objectStorageS3ForcePathStyle.value,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -93,7 +93,7 @@ function save() {
|
|||
enableChartsForRemoteUser: enableChartsForRemoteUser.value,
|
||||
enableChartsForFederatedInstances: enableChartsForFederatedInstances.value,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ function save() {
|
|||
os.apiWithDialog('admin/update-meta', {
|
||||
proxyAccountId: proxyAccountId.value,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -140,7 +140,7 @@ async function init() {
|
|||
enableTruemailApi.value = meta.enableTruemailApi;
|
||||
truemailInstance.value = meta.truemailInstance;
|
||||
truemailAuthKey.value = meta.truemailAuthKey;
|
||||
bannedEmailDomains.value = meta.bannedEmailDomains?.join('\n') || "";
|
||||
bannedEmailDomains.value = meta.bannedEmailDomains?.join('\n') || '';
|
||||
}
|
||||
|
||||
function save() {
|
||||
|
@ -155,7 +155,7 @@ function save() {
|
|||
truemailAuthKey: truemailAuthKey.value,
|
||||
bannedEmailDomains: bannedEmailDomains.value.split('\n'),
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -58,7 +58,7 @@ const save = async () => {
|
|||
await os.apiWithDialog('admin/update-meta', {
|
||||
serverRules: serverRules.value,
|
||||
});
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
};
|
||||
|
||||
const remove = (index: number): void => {
|
||||
|
|
|
@ -251,7 +251,7 @@ async function save(): void {
|
|||
notesPerOneAd: notesPerOneAd.value,
|
||||
});
|
||||
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
}
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
|
|
@ -7,9 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkSpacer :contentMax="800">
|
||||
<div :class="$style.root">
|
||||
<div v-if="!gameLoaded" :class="$style.loadingScreen">
|
||||
<div>
|
||||
Loading...
|
||||
</div>
|
||||
<div>{{ i18n.ts.loading }}<MkEllipsis/></div>
|
||||
</div>
|
||||
<!-- ↓に対してTransitionコンポーネントを使うと何故かkeyを指定していてもキャッシュが効かず様々なコンポーネントが都度再評価されてパフォーマンスが低下する -->
|
||||
<div v-show="gameLoaded" class="_gaps_s">
|
||||
|
@ -32,18 +30,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</Transition>
|
||||
|
||||
<div :class="$style.header">
|
||||
<div :class="[$style.frame, $style.headerTitle]">
|
||||
<div :class="$style.frameInner">
|
||||
<b>BUBBLE GAME</b>
|
||||
<div>- {{ gameMode }} -</div>
|
||||
<div class="_woodenFrame" :class="[$style.headerTitle]">
|
||||
<div class="_woodenFrameInner">
|
||||
<b>{{ i18n.ts.bubbleGame }}</b>
|
||||
<div>- {{ gameMode.toUpperCase() }} -</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="[$style.frame, $style.frameH]">
|
||||
<div :class="$style.frameInner">
|
||||
<MkButton inline small @click="hold">HOLD</MkButton>
|
||||
<div class="_woodenFrame _woodenFrameH">
|
||||
<div class="_woodenFrameInner">
|
||||
<MkButton inline small @click="hold">{{ i18n.ts._bubbleGame.hold }}</MkButton>
|
||||
<img v-if="holdingStock" :src="getTextureImageUrl(holdingStock.mono)" style="width: 32px; margin-left: 8px; vertical-align: bottom;"/>
|
||||
</div>
|
||||
<div :class="[$style.frameInner, $style.stock]" style="text-align: center;">
|
||||
<div class="_woodenFrameInner" :class="$style.stock" style="text-align: center;">
|
||||
<TransitionGroup
|
||||
:enterActiveClass="$style.transition_stock_enterActive"
|
||||
:leaveActiveClass="$style.transition_stock_leaveActive"
|
||||
|
@ -90,58 +88,74 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-if="isGameOver && !replaying" :class="$style.gameOverLabel">
|
||||
<div class="_gaps_s">
|
||||
<img src="/client-assets/drop-and-fusion/gameover.png" style="width: 200px; max-width: 100%; display: block; margin: auto; margin-bottom: -5px;"/>
|
||||
<div>SCORE: <MkNumber :value="score"/>{{ getScoreUnit(gameMode) }}</div>
|
||||
<div>MAX CHAIN: <MkNumber :value="maxCombo"/></div>
|
||||
<div v-if="gameMode === 'yen'">TOTAL EARNINGS: <b><MkNumber :value="yenTotal ?? score"/>円</b></div>
|
||||
<div v-if="gameMode === 'sweets'"><b>おにぎり<MkNumber :value="score / 130"/>個分</b></div>
|
||||
<div>{{ i18n.ts._bubbleGame._score.score }}: <MkNumber :value="score"/>{{ getScoreUnit(gameMode) }}</div>
|
||||
<div>{{ i18n.ts._bubbleGame._score.maxChain }}: <MkNumber :value="maxCombo"/></div>
|
||||
<div v-if="gameMode === 'yen'">
|
||||
{{ i18n.ts._bubbleGame._score.scoreYen }}:
|
||||
<I18n :src="i18n.ts._bubbleGame._score.yen" tag="b">
|
||||
<template #yen><MkNumber :value="yenTotal ?? score"/></template>
|
||||
</I18n>
|
||||
</div>
|
||||
<I18n v-if="gameMode === 'sweets'" :src="i18n.ts._bubbleGame._score.scoreSweets" tag="div">
|
||||
<template #onigiriQtyWithUnit>
|
||||
<I18n :src="i18n.ts._bubbleGame._score.estimatedQty" tag="b">
|
||||
<template #qty><MkNumber :value="score / 130"/></template>
|
||||
</I18n>
|
||||
</template>
|
||||
</I18n>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="replaying" :class="$style.replayIndicator"><span :class="$style.replayIndicatorText"><i class="ph-play ph-bold ph-lg"></i> {{ i18n.ts.replaying }}</span></div>
|
||||
</div>
|
||||
|
||||
<div v-if="replaying" :class="$style.frame">
|
||||
<div :class="$style.frameInner">
|
||||
<div v-if="replaying" class="_woodenFrame">
|
||||
<div class="_woodenFrameInner">
|
||||
<div style="background: #0004;">
|
||||
<div style="height: 10px; background: var(--accent); will-change: width;" :style="{ width: `${(currentFrame / endedAtFrame) * 100}%` }"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.frameInner">
|
||||
<div class="_woodenFrameInner">
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton @click="endReplay"><i class="ph-stop ph-bold ph-lg"></i> END</MkButton>
|
||||
<MkButton @click="endReplay"><i class="ph-stop ph-bold ph-lg"></i> {{ i18n.ts.endReplay }}</MkButton>
|
||||
<MkButton :primary="replayPlaybackRate === 4" @click="replayPlaybackRate = replayPlaybackRate === 4 ? 1 : 4"><i class="ph-skip-forward ph-bold ph-lg"></i> x4</MkButton>
|
||||
<MkButton :primary="replayPlaybackRate === 16" @click="replayPlaybackRate = replayPlaybackRate === 16 ? 1 : 16"><i class="ph-skip-forward ph-bold ph-lg"></i> x16</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isGameOver" :class="$style.frame">
|
||||
<div :class="$style.frameInner">
|
||||
<div v-if="isGameOver" class="_woodenFrame">
|
||||
<div class="_woodenFrameInner">
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton primary rounded @click="backToTitle">{{ i18n.ts.backToTitle }}</MkButton>
|
||||
<MkButton primary rounded @click="replay">{{ i18n.ts.showReplay }}</MkButton>
|
||||
<MkButton primary rounded @click="share">{{ i18n.ts.share }}</MkButton>
|
||||
<MkButton rounded @click="exportLog">Copy replay data</MkButton>
|
||||
<MkButton rounded @click="exportLog">{{ i18n.ts.copyReplayData }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex;">
|
||||
<div :class="$style.frame" style="flex: 1; margin-right: 10px;">
|
||||
<div :class="$style.frameInner">
|
||||
<div>SCORE: <b><MkNumber :value="score"/>{{ getScoreUnit(gameMode) }}</b></div>
|
||||
<div>HIGH SCORE: <b v-if="highScore"><MkNumber :value="highScore"/>{{ getScoreUnit(gameMode) }}</b><b v-else>-</b></div>
|
||||
<div v-if="gameMode === 'yen'">TOTAL EARNINGS: <b v-if="yenTotal"><MkNumber :value="yenTotal"/>円</b><b v-else>-</b></div>
|
||||
<div class="_woodenFrame" style="flex: 1; margin-right: 10px;">
|
||||
<div class="_woodenFrameInner">
|
||||
<div>{{ i18n.ts._bubbleGame._score.score }}: <MkNumber :value="score"/>{{ getScoreUnit(gameMode) }}</div>
|
||||
<div>{{ i18n.ts._bubbleGame._score.highScore }}: <b v-if="highScore"><MkNumber :value="highScore"/>{{ getScoreUnit(gameMode) }}</b><b v-else>-</b></div>
|
||||
<div v-if="gameMode === 'yen'">
|
||||
{{ i18n.ts._bubbleGame._score.scoreYen }}:
|
||||
<I18n :src="i18n.ts._bubbleGame._score.yen" tag="b">
|
||||
<template #yen><MkNumber :value="yenTotal ?? score"/></template>
|
||||
</I18n>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="[$style.frame]" style="margin-left: auto;">
|
||||
<div :class="$style.frameInner" style="text-align: center;">
|
||||
<div class="_woodenFrame" style="margin-left: auto;">
|
||||
<div class="_woodenFrameInner" style="text-align: center;">
|
||||
<div @click="showConfig = !showConfig"><i class="ph-gear ph-bold ph-lg"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showConfig" :class="$style.frame">
|
||||
<div :class="$style.frameInner">
|
||||
<div v-if="showConfig" class="_woodenFrame">
|
||||
<div class="_woodenFrameInner">
|
||||
<div class="_gaps">
|
||||
<MkRange v-model="bgmVolume" :min="0" :max="1" :step="0.01" :textConverter="(v) => `${Math.floor(v * 100)}%`" :continuousUpdate="true" @dragEnded="(v) => updateSettings('bgmVolume', v)">
|
||||
<template #label>BGM {{ i18n.ts.volume }}</template>
|
||||
|
@ -153,8 +167,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="$style.frame">
|
||||
<div :class="$style.frameInner">
|
||||
<div class="_woodenFrame">
|
||||
<div class="_woodenFrameInner">
|
||||
<div>FUSION RECIPE</div>
|
||||
<div>
|
||||
<div v-for="(mono, i) in game.monoDefinitions.sort((a, b) => a.level - b.level)" :key="mono.id" style="display: inline-block;">
|
||||
|
@ -165,10 +179,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="$style.frame">
|
||||
<div :class="$style.frameInner">
|
||||
<MkButton v-if="!isGameOver && !replaying" full danger @click="surrender">Surrender</MkButton>
|
||||
<MkButton v-else full @click="restart">Retry</MkButton>
|
||||
<div class="_woodenFrame">
|
||||
<div class="_woodenFrameInner">
|
||||
<MkButton v-if="!isGameOver && !replaying" full danger @click="surrender">{{ i18n.ts.surrender }}</MkButton>
|
||||
<MkButton v-else full @click="restart">{{ i18n.ts.gameRetry }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1313,38 +1327,6 @@ definePageMetadata(() => ({
|
|||
max-width: 100%;
|
||||
}
|
||||
|
||||
.frame {
|
||||
padding: 7px;
|
||||
background: #8C4F26;
|
||||
box-shadow: 0 6px 16px #0007, 0 0 1px 1px #693410, inset 0 0 2px 1px #ce8a5c;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.frameH {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.frameInner {
|
||||
padding: 8px;
|
||||
margin-top: 8px;
|
||||
background: #F1E8DC;
|
||||
box-shadow: 0 0 2px 1px #ce8a5c, inset 0 0 1px 1px #693410;
|
||||
border-radius: 6px;
|
||||
color: #693410;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.frameDivider {
|
||||
height: 0;
|
||||
border: none;
|
||||
border-top: 1px solid #693410;
|
||||
border-bottom: 1px solid #ce8a5c;
|
||||
}
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
|
|
|
@ -15,13 +15,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkSpacer v-if="!gameStarted" :contentMax="800">
|
||||
<div :class="$style.root">
|
||||
<div class="_gaps">
|
||||
<div :class="$style.frame" style="text-align: center;">
|
||||
<div :class="$style.frameInner">
|
||||
<div class="_woodenFrame" style="text-align: center;">
|
||||
<div class="_woodenFrameInner">
|
||||
<img src="/client-assets/drop-and-fusion/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.frame" style="text-align: center;">
|
||||
<div :class="$style.frameInner">
|
||||
<div class="_woodenFrame" style="text-align: center;">
|
||||
<div class="_woodenFrameInner">
|
||||
<div class="_gaps" style="padding: 16px;">
|
||||
<MkSelect v-model="gameMode">
|
||||
<option value="normal">NORMAL</option>
|
||||
|
@ -33,19 +33,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton primary gradate large rounded inline @click="start">{{ i18n.ts.start }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.frameInner">
|
||||
<div class="_woodenFrameInner">
|
||||
<div class="_gaps" style="padding: 16px;">
|
||||
<div style="font-size: 90%;"><i class="ti ti-music"></i> {{ i18n.ts.soundWillBePlayed }}</div>
|
||||
<div style="font-size: 90%;"><i class="ph-music-notes ph-bold ph-lg"></i> {{ i18n.ts.soundWillBePlayed }}</div>
|
||||
<MkSwitch v-model="mute">
|
||||
<template #label>{{ i18n.ts.mute }}</template>
|
||||
</MkSwitch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.frame">
|
||||
<div :class="$style.frameInner">
|
||||
<div class="_woodenFrame">
|
||||
<div class="_woodenFrameInner">
|
||||
<div class="_gaps_s" style="padding: 16px;">
|
||||
<div><b>{{ i18n.tsx.lastNDays({ n: 7 }) }} {{ i18n.ts.ranking }}</b> ({{ gameMode }})</div>
|
||||
<div><b>{{ i18n.tsx.lastNDays({ n: 7 }) }} {{ i18n.ts.ranking }}</b> ({{ gameMode.toUpperCase() }})</div>
|
||||
<div v-if="ranking" class="_gaps_s">
|
||||
<div v-for="r in ranking" :key="r.id" :class="$style.rankingRecord">
|
||||
<MkAvatar :link="true" style="width: 24px; height: 24px; margin-right: 4px;" :user="r.user"/>
|
||||
|
@ -57,8 +57,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.frame">
|
||||
<div :class="$style.frameInner" style="padding: 16px;">
|
||||
<div class="_woodenFrame">
|
||||
<div class="_woodenFrameInner" style="padding: 16px;">
|
||||
<div style="font-weight: bold;">{{ i18n.ts._bubbleGame.howToPlay }}</div>
|
||||
<ol>
|
||||
<li>{{ i18n.ts._bubbleGame._howToPlay.section1 }}</li>
|
||||
|
@ -67,8 +67,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.frame">
|
||||
<div :class="$style.frameInner">
|
||||
<div class="_woodenFrame">
|
||||
<div class="_woodenFrameInner">
|
||||
<div class="_gaps_s" style="padding: 16px;">
|
||||
<div><b>Credit</b></div>
|
||||
<div>
|
||||
|
@ -123,7 +123,7 @@ function onGameEnd() {
|
|||
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.bubbleGame,
|
||||
icon: 'ti ti-device-gamepad',
|
||||
icon: 'ph-game-controller ph-bold ph-lg',
|
||||
}));
|
||||
</script>
|
||||
|
||||
|
@ -149,38 +149,6 @@ definePageMetadata(() => ({
|
|||
}
|
||||
}
|
||||
|
||||
.frame {
|
||||
padding: 7px;
|
||||
background: #8C4F26;
|
||||
box-shadow: 0 6px 16px #0007, 0 0 1px 1px #693410, inset 0 0 2px 1px #ce8a5c;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.frameH {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.frameInner {
|
||||
padding: 8px;
|
||||
margin-top: 8px;
|
||||
background: #F1E8DC;
|
||||
box-shadow: 0 0 2px 1px #ce8a5c, inset 0 0 1px 1px #693410;
|
||||
border-radius: 6px;
|
||||
color: #693410;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.frameDivider {
|
||||
height: 0;
|
||||
border: none;
|
||||
border-top: 1px solid #693410;
|
||||
border-bottom: 1px solid #ce8a5c;
|
||||
}
|
||||
|
||||
.rankingRecord {
|
||||
display: flex;
|
||||
line-height: 24px;
|
||||
|
|
|
@ -40,6 +40,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkSwitch v-model="isSilenced" :disabled="!meta || !instance" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch>
|
||||
<MkSwitch v-model="isNSFW" :disabled="!instance" @update:modelValue="toggleNSFW">Mark as NSFW</MkSwitch>
|
||||
<MkButton @click="refreshMetadata"><i class="ph-arrows-clockwise ph-bold ph-lg"></i> Refresh metadata</MkButton>
|
||||
<MkTextarea v-model="moderationNote" manualSave>
|
||||
<template #label>{{ i18n.ts.moderationNote }}</template>
|
||||
</MkTextarea>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
|
@ -120,7 +123,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkChart from '@/components/MkChart.vue';
|
||||
import MkObjectView from '@/components/MkObjectView.vue';
|
||||
|
@ -142,6 +145,7 @@ import MkPagination from '@/components/MkPagination.vue';
|
|||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
|
||||
import { dateString } from '@/filters/date.js';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
host: string;
|
||||
|
@ -157,6 +161,7 @@ const isBlocked = ref(false);
|
|||
const isSilenced = ref(false);
|
||||
const isNSFW = ref(false);
|
||||
const faviconUrl = ref<string | null>(null);
|
||||
const moderationNote = ref('');
|
||||
|
||||
const usersPagination = {
|
||||
endpoint: iAmModerator ? 'admin/show-users' : 'users' as const,
|
||||
|
@ -169,6 +174,10 @@ const usersPagination = {
|
|||
offsetMode: true,
|
||||
};
|
||||
|
||||
watch(moderationNote, async () => {
|
||||
await misskeyApi('admin/federation/update-instance', { host: instance.value.host, moderationNote: moderationNote.value });
|
||||
});
|
||||
|
||||
async function fetch(): Promise<void> {
|
||||
if (iAmAdmin) {
|
||||
meta.value = await misskeyApi('admin/meta');
|
||||
|
@ -181,6 +190,7 @@ async function fetch(): Promise<void> {
|
|||
isSilenced.value = instance.value?.isSilenced ?? false;
|
||||
isNSFW.value = instance.value?.isNSFW ?? false;
|
||||
faviconUrl.value = getProxiedImageUrlNullable(instance.value?.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.value?.iconUrl, 'preview');
|
||||
moderationNote.value = instance.value?.moderationNote;
|
||||
}
|
||||
|
||||
async function toggleBlock(): Promise<void> {
|
||||
|
|
|
@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="$style.board">
|
||||
<div class="_woodenFrame">
|
||||
<div :class="$style.boardInner">
|
||||
<div v-if="showBoardLabels" :class="$style.labelsX">
|
||||
<span v-for="i in game.map[0].length" :key="i" :class="$style.labelsXLabel">{{ String.fromCharCode(64 + i) }}</span>
|
||||
|
@ -124,8 +124,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkFolder>
|
||||
<template #label>{{ i18n.ts.options }}</template>
|
||||
<div class="_gaps_s" style="text-align: left;">
|
||||
<MkSwitch v-model="showBoardLabels">Show labels</MkSwitch>
|
||||
<MkSwitch v-model="useAvatarAsStone">useAvatarAsStone</MkSwitch>
|
||||
<MkSwitch v-model="showBoardLabels">{{ i18n.ts._reversi.showBoardLabels }}</MkSwitch>
|
||||
<MkSwitch v-model="useAvatarAsStone">{{ i18n.ts._reversi.useAvatarAsStone }}</MkSwitch>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
|
@ -248,7 +248,7 @@ if (game.value.isStarted && !game.value.isEnded) {
|
|||
crc32: crc32.toString(),
|
||||
}).then((res) => {
|
||||
if (res.desynced) {
|
||||
console.log('resynced');
|
||||
if (_DEV_) console.log('resynced');
|
||||
restoreGame(res.game!);
|
||||
}
|
||||
});
|
||||
|
@ -500,17 +500,6 @@ $gap: 4px;
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.board {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin: 0 auto;
|
||||
|
||||
padding: 7px;
|
||||
background: #8C4F26;
|
||||
box-shadow: 0 6px 16px #0007, 0 0 1px 1px #693410, inset 0 0 2px 1px #ce8a5c;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.boardInner {
|
||||
padding: 32px;
|
||||
|
||||
|
|
|
@ -2,14 +2,18 @@ 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';
|
||||
import { fetchInstance } from '@/instance.js';
|
||||
|
||||
export async function clearCache() {
|
||||
os.waiting();
|
||||
miLocalStorage.removeItem('instance');
|
||||
miLocalStorage.removeItem('instanceCachedAt');
|
||||
miLocalStorage.removeItem('locale');
|
||||
miLocalStorage.removeItem('localeVersion');
|
||||
miLocalStorage.removeItem('theme');
|
||||
miLocalStorage.removeItem('emojis');
|
||||
miLocalStorage.removeItem('lastEmojisFetchedAt');
|
||||
await fetchInstance(true);
|
||||
await fetchCustomEmojis(true);
|
||||
unisonReload();
|
||||
}
|
||||
|
|
101
packages/frontend/src/scripts/search-emoji.ts
Normal file
101
packages/frontend/src/scripts/search-emoji.ts
Normal file
|
@ -0,0 +1,101 @@
|
|||
export type EmojiDef = {
|
||||
emoji: string;
|
||||
name: string;
|
||||
url: string;
|
||||
aliasOf?: string;
|
||||
} | {
|
||||
emoji: string;
|
||||
name: string;
|
||||
aliasOf?: string;
|
||||
isCustomEmoji?: true;
|
||||
};
|
||||
type EmojiScore = { emoji: EmojiDef, score: number };
|
||||
|
||||
export function searchEmoji(query: string | null, emojiDb: EmojiDef[], max = 30): EmojiDef[] {
|
||||
if (!query) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const matched = new Map<string, EmojiScore>();
|
||||
// 完全一致(エイリアスなし)
|
||||
emojiDb.some(x => {
|
||||
if (x.name.toLowerCase() === query && !x.aliasOf) {
|
||||
matched.set(x.name, { emoji: x, score: query.length + 3 });
|
||||
}
|
||||
return matched.size === max;
|
||||
});
|
||||
|
||||
// 完全一致(エイリアス込み)
|
||||
if (matched.size < max) {
|
||||
emojiDb.some(x => {
|
||||
if (x.name.toLowerCase() === query && !matched.has(x.aliasOf ?? x.name)) {
|
||||
matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length + 2 });
|
||||
}
|
||||
return matched.size === max;
|
||||
});
|
||||
}
|
||||
|
||||
// 前方一致(エイリアスなし)
|
||||
if (matched.size < max) {
|
||||
emojiDb.some(x => {
|
||||
if (x.name.toLowerCase().startsWith(query) && !x.aliasOf && !matched.has(x.name)) {
|
||||
matched.set(x.name, { emoji: x, score: query.length + 1 });
|
||||
}
|
||||
return matched.size === max;
|
||||
});
|
||||
}
|
||||
|
||||
// 前方一致(エイリアス込み)
|
||||
if (matched.size < max) {
|
||||
emojiDb.some(x => {
|
||||
if (x.name.toLowerCase().startsWith(query) && !matched.has(x.aliasOf ?? x.name)) {
|
||||
matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length });
|
||||
}
|
||||
return matched.size === max;
|
||||
});
|
||||
}
|
||||
|
||||
// 部分一致(エイリアス込み)
|
||||
if (matched.size < max) {
|
||||
emojiDb.some(x => {
|
||||
if (x.name.toLowerCase().includes(query) && !matched.has(x.aliasOf ?? x.name)) {
|
||||
matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length - 1 });
|
||||
}
|
||||
return matched.size === max;
|
||||
});
|
||||
}
|
||||
|
||||
// 簡易あいまい検索(3文字以上)
|
||||
if (matched.size < max && query.length > 3) {
|
||||
const queryChars = [...query];
|
||||
const hitEmojis = new Map<string, EmojiScore>();
|
||||
|
||||
for (const x of emojiDb) {
|
||||
// 文字列の位置を進めながら、クエリの文字を順番に探す
|
||||
|
||||
let pos = 0;
|
||||
let hit = 0;
|
||||
for (const c of queryChars) {
|
||||
pos = x.name.indexOf(c, pos);
|
||||
if (pos <= -1) break;
|
||||
hit++;
|
||||
}
|
||||
|
||||
// 半分以上の文字が含まれていればヒットとする
|
||||
if (hit > Math.ceil(queryChars.length / 2) && hit - 2 > (matched.get(x.aliasOf ?? x.name)?.score ?? 0)) {
|
||||
hitEmojis.set(x.aliasOf ?? x.name, { emoji: x, score: hit - 2 });
|
||||
}
|
||||
}
|
||||
|
||||
// ヒットしたものを全部追加すると雑多になるので、先頭の6件程度だけにしておく(6件=オートコンプリートのポップアップのサイズ分)
|
||||
[...hitEmojis.values()]
|
||||
.sort((x, y) => y.score - x.score)
|
||||
.slice(0, 6)
|
||||
.forEach(it => matched.set(it.emoji.name, it));
|
||||
}
|
||||
|
||||
return [...matched.values()]
|
||||
.sort((x, y) => y.score - x.score)
|
||||
.slice(0, max)
|
||||
.map(it => it.emoji);
|
||||
}
|
|
@ -126,7 +126,7 @@ export async function loadAudio(url: string, options?: { useCache?: boolean; })
|
|||
*/
|
||||
export function playMisskeySfx(operationType: OperationType) {
|
||||
const sound = defaultStore.state[`sound_${operationType}`];
|
||||
if (sound.type == null || !canPlay) return;
|
||||
if (sound.type == null || !canPlay || !navigator.userActivation.hasBeenActive) return;
|
||||
|
||||
canPlay = false;
|
||||
playMisskeySfxFile(sound).finally(() => {
|
||||
|
|
|
@ -451,6 +451,39 @@ rt {
|
|||
transition-timing-function: cubic-bezier(0,.5,.5,1);
|
||||
}
|
||||
|
||||
._woodenFrame {
|
||||
padding: 7px;
|
||||
background: #8C4F26;
|
||||
box-shadow: 0 6px 16px #0007, 0 0 1px 1px #693410, inset 0 0 2px 1px #ce8a5c;
|
||||
border-radius: 10px;
|
||||
|
||||
--bg: #F1E8DC;
|
||||
--panel: #fff;
|
||||
--fg: #693410;
|
||||
--switchOffBg: rgba(0, 0, 0, 0.1);
|
||||
--switchOffFg: rgb(255, 255, 255);
|
||||
--switchOnBg: var(--accent);
|
||||
--switchOnFg: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
._woodenFrameH {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
._woodenFrameInner {
|
||||
padding: 8px;
|
||||
margin-top: 8px;
|
||||
background: var(--bg);
|
||||
box-shadow: 0 0 2px 1px #ce8a5c, inset 0 0 1px 1px #693410;
|
||||
border-radius: 6px;
|
||||
color: var(--fg);
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
._transition_zoom-enter-active, ._transition_zoom-leave-active {
|
||||
transition: opacity 0.5s, transform 0.5s !important;
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template v-if="column.channelId">
|
||||
<div style="padding: 8px; text-align: center;">
|
||||
<MkButton primary gradate rounded inline @click="post"><i class="ph-pencil-simple ph-bold ph-lg"></i></MkButton>
|
||||
<MkButton primary gradate rounded inline small @click="post"><i class="ph-pencil-simple ph-bold ph-lg"></i></MkButton>
|
||||
</div>
|
||||
<MkTimeline ref="timeline" src="channel" :channel="column.channelId"/>
|
||||
</template>
|
||||
|
|
34
packages/frontend/test/autocomplete.test.ts
Normal file
34
packages/frontend/test/autocomplete.test.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { assert, describe, test } from 'vitest';
|
||||
import { searchEmoji } from '@/scripts/search-emoji.js';
|
||||
|
||||
describe('emoji autocomplete', () => {
|
||||
test('名前の完全一致は名前の前方一致より優先される', async () => {
|
||||
const result = searchEmoji('foooo', [{ emoji: ':foooo:', name: 'foooo' }, { emoji: ':foooobaaar:', name: 'foooobaaar' }]);
|
||||
assert.equal(result[0].emoji, ':foooo:');
|
||||
});
|
||||
|
||||
test('名前の前方一致は名前の部分一致より優先される', async () => {
|
||||
const result = searchEmoji('baaa', [{ emoji: ':baaar:', name: 'baaar' }, { emoji: ':foooobaaar:', name: 'foooobaaar' }]);
|
||||
assert.equal(result[0].emoji, ':baaar:');
|
||||
});
|
||||
|
||||
test('名前の完全一致はタグの完全一致より優先される', async () => {
|
||||
const result = searchEmoji('foooo', [{ emoji: ':foooo:', name: 'foooo' }, { emoji: ':baaar:', name: 'foooo', aliasOf: 'baaar' }]);
|
||||
assert.equal(result[0].emoji, ':foooo:');
|
||||
});
|
||||
|
||||
test('名前の前方一致はタグの前方一致より優先される', async () => {
|
||||
const result = searchEmoji('foo', [{ emoji: ':foooo:', name: 'foooo' }, { emoji: ':baaar:', name: 'foooo', aliasOf: 'baaar' }]);
|
||||
assert.equal(result[0].emoji, ':foooo:');
|
||||
});
|
||||
|
||||
test('名前の部分一致はタグの部分一致より優先される', async () => {
|
||||
const result = searchEmoji('oooo', [{ emoji: ':foooo:', name: 'foooo' }, { emoji: ':baaar:', name: 'foooo', aliasOf: 'baaar' }]);
|
||||
assert.equal(result[0].emoji, ':foooo:');
|
||||
});
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue