Merge remote-tracking branch 'misskey-dev/develop' into io
This commit is contained in:
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template>
|
||||
<div
|
||||
ref="rootEl"
|
||||
:class="[$style.transitionRoot]"
|
||||
:class="[$style.transitionRoot, { [$style.enableAnimation]: shouldAnimate }]"
|
||||
@touchstart.passive="touchStart"
|
||||
@touchmove.passive="touchMove"
|
||||
@touchend.passive="touchEnd"
|
||||
@@ -44,6 +44,8 @@ const emit = defineEmits<{
|
||||
(ev: 'swiped', newKey: string, direction: 'left' | 'right'): void;
|
||||
}>();
|
||||
|
||||
const shouldAnimate = computed(() => defaultStore.reactiveState.enableHorizontalSwipe.value || defaultStore.reactiveState.animation.value);
|
||||
|
||||
// ▼ しきい値 ▼ //
|
||||
|
||||
// スワイプと判定される最小の距離
|
||||
@@ -188,7 +190,9 @@ watch(tabModel, (newTab, oldTab) => {
|
||||
.transitionChildren {
|
||||
grid-area: 1 / 1 / 2 / 2;
|
||||
transform: translateX(var(--swipe));
|
||||
}
|
||||
|
||||
.enableAnimation .transitionChildren {
|
||||
&.swipeAnimation_enterActive,
|
||||
&.swipeAnimation_leaveActive {
|
||||
transition: transform .3s cubic-bezier(0.65, 0.05, 0.36, 1);
|
||||
|
@@ -12,85 +12,96 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div class="_gaps" :class="{ [$style.disallowInner]: isReady }">
|
||||
<div style="font-size: 1.5em; text-align: center;">{{ i18n.ts._reversi.gameSettings }}</div>
|
||||
|
||||
<div class="_panel">
|
||||
<div style="display: flex; align-items: center; padding: 16px; border-bottom: solid 1px var(--divider);">
|
||||
<div>{{ mapName }}</div>
|
||||
<MkButton style="margin-left: auto;" @click="chooseMap">{{ i18n.ts._reversi.chooseBoard }}</MkButton>
|
||||
</div>
|
||||
<template v-if="game.noIrregularRules">
|
||||
<div>{{ i18n.ts._reversi.disallowIrregularRules }}</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="_panel">
|
||||
<div style="display: flex; align-items: center; padding: 16px; border-bottom: solid 1px var(--divider);">
|
||||
<div>{{ mapName }}</div>
|
||||
<MkButton style="margin-left: auto;" @click="chooseMap">{{ i18n.ts._reversi.chooseBoard }}</MkButton>
|
||||
</div>
|
||||
|
||||
<div style="padding: 16px;">
|
||||
<div v-if="game.map == null"><i class="ti ti-dice"></i></div>
|
||||
<div v-else :class="$style.board" :style="{ 'grid-template-rows': `repeat(${ game.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.map[0].length }, 1fr)` }">
|
||||
<div v-for="(x, i) in game.map.join('')" :class="[$style.boardCell, { [$style.boardCellNone]: x == ' ' }]" @click="onMapCellClick(i, x)">
|
||||
<i v-if="x === 'b' || x === 'w'" style="pointer-events: none; user-select: none;" :class="x === 'b' ? 'ti ti-circle-filled' : 'ti ti-circle'"></i>
|
||||
<div style="padding: 16px;">
|
||||
<div v-if="game.map == null"><i class="ti ti-dice"></i></div>
|
||||
<div v-else :class="$style.board" :style="{ 'grid-template-rows': `repeat(${ game.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.map[0].length }, 1fr)` }">
|
||||
<div v-for="(x, i) in game.map.join('')" :class="[$style.boardCell, { [$style.boardCellNone]: x == ' ' }]" @click="onMapCellClick(i, x)">
|
||||
<i v-if="x === 'b' || x === 'w'" style="pointer-events: none; user-select: none;" :class="x === 'b' ? 'ti ti-circle-filled' : 'ti ti-circle'"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MkFolder :defaultOpen="true">
|
||||
<template #label>{{ i18n.ts._reversi.blackOrWhite }}</template>
|
||||
<MkFolder :defaultOpen="true">
|
||||
<template #label>{{ i18n.ts._reversi.blackOrWhite }}</template>
|
||||
|
||||
<MkRadios v-model="game.bw">
|
||||
<option value="random">{{ i18n.ts.random }}</option>
|
||||
<option :value="'1'">
|
||||
<I18n :src="i18n.ts._reversi.blackIs" tag="span">
|
||||
<template #name>
|
||||
<b><MkUserName :user="game.user1"/></b>
|
||||
</template>
|
||||
</I18n>
|
||||
</option>
|
||||
<option :value="'2'">
|
||||
<I18n :src="i18n.ts._reversi.blackIs" tag="span">
|
||||
<template #name>
|
||||
<b><MkUserName :user="game.user2"/></b>
|
||||
</template>
|
||||
</I18n>
|
||||
</option>
|
||||
</MkRadios>
|
||||
</MkFolder>
|
||||
<MkRadios v-model="game.bw">
|
||||
<option value="random">{{ i18n.ts.random }}</option>
|
||||
<option :value="'1'">
|
||||
<I18n :src="i18n.ts._reversi.blackIs" tag="span">
|
||||
<template #name>
|
||||
<b><MkUserName :user="game.user1"/></b>
|
||||
</template>
|
||||
</I18n>
|
||||
</option>
|
||||
<option :value="'2'">
|
||||
<I18n :src="i18n.ts._reversi.blackIs" tag="span">
|
||||
<template #name>
|
||||
<b><MkUserName :user="game.user2"/></b>
|
||||
</template>
|
||||
</I18n>
|
||||
</option>
|
||||
</MkRadios>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder :defaultOpen="true">
|
||||
<template #label>{{ i18n.ts._reversi.timeLimitForEachTurn }}</template>
|
||||
<template #suffix>{{ game.timeLimitForEachTurn }}{{ i18n.ts._time.second }}</template>
|
||||
<MkFolder :defaultOpen="true">
|
||||
<template #label>{{ i18n.ts._reversi.timeLimitForEachTurn }}</template>
|
||||
<template #suffix>{{ game.timeLimitForEachTurn }}{{ i18n.ts._time.second }}</template>
|
||||
|
||||
<MkRadios v-model="game.timeLimitForEachTurn">
|
||||
<option :value="5">5{{ i18n.ts._time.second }}</option>
|
||||
<option :value="10">10{{ i18n.ts._time.second }}</option>
|
||||
<option :value="30">30{{ i18n.ts._time.second }}</option>
|
||||
<option :value="60">60{{ i18n.ts._time.second }}</option>
|
||||
<option :value="90">90{{ i18n.ts._time.second }}</option>
|
||||
<option :value="120">120{{ i18n.ts._time.second }}</option>
|
||||
<option :value="180">180{{ i18n.ts._time.second }}</option>
|
||||
<option :value="3600">3600{{ i18n.ts._time.second }}</option>
|
||||
</MkRadios>
|
||||
</MkFolder>
|
||||
<MkRadios v-model="game.timeLimitForEachTurn">
|
||||
<option :value="5">5{{ i18n.ts._time.second }}</option>
|
||||
<option :value="10">10{{ i18n.ts._time.second }}</option>
|
||||
<option :value="30">30{{ i18n.ts._time.second }}</option>
|
||||
<option :value="60">60{{ i18n.ts._time.second }}</option>
|
||||
<option :value="90">90{{ i18n.ts._time.second }}</option>
|
||||
<option :value="120">120{{ i18n.ts._time.second }}</option>
|
||||
<option :value="180">180{{ i18n.ts._time.second }}</option>
|
||||
<option :value="3600">3600{{ i18n.ts._time.second }}</option>
|
||||
</MkRadios>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder :defaultOpen="true">
|
||||
<template #label>{{ i18n.ts._reversi.rules }}</template>
|
||||
<MkFolder :defaultOpen="true">
|
||||
<template #label>{{ i18n.ts._reversi.rules }}</template>
|
||||
|
||||
<div class="_gaps_s">
|
||||
<MkSwitch v-model="game.isLlotheo" @update:modelValue="updateSettings('isLlotheo')">{{ i18n.ts._reversi.isLlotheo }}</MkSwitch>
|
||||
<MkSwitch v-model="game.loopedBoard" @update:modelValue="updateSettings('loopedBoard')">{{ i18n.ts._reversi.loopedMap }}</MkSwitch>
|
||||
<MkSwitch v-model="game.canPutEverywhere" @update:modelValue="updateSettings('canPutEverywhere')">{{ i18n.ts._reversi.canPutEverywhere }}</MkSwitch>
|
||||
</div>
|
||||
</MkFolder>
|
||||
<div class="_gaps_s">
|
||||
<MkSwitch v-model="game.isLlotheo" @update:modelValue="updateSettings('isLlotheo')">{{ i18n.ts._reversi.isLlotheo }}</MkSwitch>
|
||||
<MkSwitch v-model="game.loopedBoard" @update:modelValue="updateSettings('loopedBoard')">{{ i18n.ts._reversi.loopedMap }}</MkSwitch>
|
||||
<MkSwitch v-model="game.canPutEverywhere" @update:modelValue="updateSettings('canPutEverywhere')">{{ i18n.ts._reversi.canPutEverywhere }}</MkSwitch>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
<template #footer>
|
||||
<div :class="$style.footer">
|
||||
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="16">
|
||||
<div style="text-align: center; margin-bottom: 10px;">
|
||||
<template v-if="isReady && isOpReady">{{ i18n.ts._reversi.thisGameIsStartedSoon }}<MkEllipsis/></template>
|
||||
<template v-if="isReady && !isOpReady">{{ i18n.ts._reversi.waitingForOther }}<MkEllipsis/></template>
|
||||
<template v-if="!isReady && isOpReady">{{ i18n.ts._reversi.waitingForMe }}</template>
|
||||
<template v-if="!isReady && !isOpReady">{{ i18n.ts._reversi.waitingBoth }}<MkEllipsis/></template>
|
||||
</div>
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton rounded danger @click="cancel">{{ i18n.ts.cancel }}</MkButton>
|
||||
<MkButton v-if="!isReady" rounded primary @click="ready">{{ i18n.ts._reversi.ready }}</MkButton>
|
||||
<MkButton v-if="isReady" rounded @click="unready">{{ i18n.ts._reversi.cancelReady }}</MkButton>
|
||||
<div style="text-align: center;" class="_gaps_s">
|
||||
<div v-if="opponentHasSettingsChanged" style="color: var(--warn);">{{ i18n.ts._reversi.opponentHasSettingsChanged }}</div>
|
||||
<div>
|
||||
<template v-if="isReady && isOpReady">{{ i18n.ts._reversi.thisGameIsStartedSoon }}<MkEllipsis/></template>
|
||||
<template v-if="isReady && !isOpReady">{{ i18n.ts._reversi.waitingForOther }}<MkEllipsis/></template>
|
||||
<template v-if="!isReady && isOpReady">{{ i18n.ts._reversi.waitingForMe }}</template>
|
||||
<template v-if="!isReady && !isOpReady">{{ i18n.ts._reversi.waitingBoth }}<MkEllipsis/></template>
|
||||
</div>
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton rounded danger @click="cancel">{{ i18n.ts.cancel }}</MkButton>
|
||||
<MkButton v-if="!isReady" rounded primary @click="ready">{{ i18n.ts._reversi.ready }}</MkButton>
|
||||
<MkButton v-if="isReady" rounded @click="unready">{{ i18n.ts._reversi.cancelReady }}</MkButton>
|
||||
</div>
|
||||
<div>
|
||||
<MkSwitch v-model="shareWhenStart">{{ i18n.ts._reversi.shareToTlTheGameWhenStart }}</MkSwitch>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
@@ -124,6 +135,8 @@ const props = defineProps<{
|
||||
connection: Misskey.ChannelConnection;
|
||||
}>();
|
||||
|
||||
const shareWhenStart = defineModel<boolean>('shareWhenStart', { default: false });
|
||||
|
||||
const game = ref<Misskey.entities.ReversiGameDetailed>(deepClone(props.game));
|
||||
|
||||
const mapName = computed(() => {
|
||||
@@ -142,6 +155,8 @@ const isOpReady = computed(() => {
|
||||
return false;
|
||||
});
|
||||
|
||||
const opponentHasSettingsChanged = ref(false);
|
||||
|
||||
watch(() => game.value.bw, () => {
|
||||
updateSettings('bw');
|
||||
});
|
||||
@@ -190,6 +205,7 @@ async function cancel() {
|
||||
|
||||
function ready() {
|
||||
props.connection.send('ready', true);
|
||||
opponentHasSettingsChanged.value = false;
|
||||
}
|
||||
|
||||
function unready() {
|
||||
@@ -212,6 +228,10 @@ function onUpdateSettings({ userId, key, value }: { userId: string; key: keyof M
|
||||
if (userId === $i.id) return;
|
||||
if (game.value[key] === value) return;
|
||||
game.value[key] = value;
|
||||
if (isReady.value) {
|
||||
opponentHasSettingsChanged.value = true;
|
||||
unready();
|
||||
}
|
||||
}
|
||||
|
||||
function onMapCellClick(pos: number, pixel: string) {
|
||||
|
@@ -5,12 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<template>
|
||||
<div v-if="game == null || (!game.isEnded && connection == null)"><MkLoading/></div>
|
||||
<GameSetting v-else-if="!game.isStarted" :game="game" :connection="connection!"/>
|
||||
<GameSetting v-else-if="!game.isStarted" v-model:shareWhenStart="shareWhenStart" :game="game" :connection="connection!"/>
|
||||
<GameBoard v-else :game="game" :connection="connection"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, onMounted, shallowRef, onUnmounted } from 'vue';
|
||||
import { computed, watch, onMounted, ref, shallowRef, onUnmounted } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import GameSetting from './game.setting.vue';
|
||||
import GameBoard from './game.board.vue';
|
||||
@@ -21,6 +21,7 @@ import { signinRequired } from '@/account.js';
|
||||
import { useRouter } from '@/global/router/supplier.js';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { useInterval } from '@/scripts/use-interval.js';
|
||||
|
||||
const $i = signinRequired();
|
||||
|
||||
@@ -32,17 +33,32 @@ const props = defineProps<{
|
||||
|
||||
const game = shallowRef<Misskey.entities.ReversiGameDetailed | null>(null);
|
||||
const connection = shallowRef<Misskey.ChannelConnection | null>(null);
|
||||
const shareWhenStart = ref(false);
|
||||
|
||||
watch(() => props.gameId, () => {
|
||||
fetchGame();
|
||||
});
|
||||
|
||||
function start(_game: Misskey.entities.ReversiGameDetailed) {
|
||||
if (game.value?.isStarted) return;
|
||||
|
||||
if (shareWhenStart.value) {
|
||||
misskeyApi('notes/create', {
|
||||
text: i18n.ts._reversi.iStartedAGame + '\n' + location.href,
|
||||
visibility: 'home',
|
||||
});
|
||||
}
|
||||
|
||||
game.value = _game;
|
||||
}
|
||||
|
||||
async function fetchGame() {
|
||||
const _game = await misskeyApi('reversi/show-game', {
|
||||
gameId: props.gameId,
|
||||
});
|
||||
|
||||
game.value = _game;
|
||||
shareWhenStart.value = false;
|
||||
|
||||
if (connection.value) {
|
||||
connection.value.dispose();
|
||||
@@ -52,7 +68,7 @@ async function fetchGame() {
|
||||
gameId: game.value.id,
|
||||
});
|
||||
connection.value.on('started', x => {
|
||||
game.value = x.game;
|
||||
start(x.game);
|
||||
});
|
||||
connection.value.on('canceled', x => {
|
||||
connection.value?.dispose();
|
||||
@@ -68,6 +84,25 @@ async function fetchGame() {
|
||||
}
|
||||
}
|
||||
|
||||
// 通信を取りこぼした場合の救済
|
||||
useInterval(async () => {
|
||||
if (game.value == null) return;
|
||||
if (game.value.isStarted) return;
|
||||
|
||||
const _game = await misskeyApi('reversi/show-game', {
|
||||
gameId: props.gameId,
|
||||
});
|
||||
|
||||
if (_game.isStarted) {
|
||||
start(_game);
|
||||
} else {
|
||||
game.value = _game;
|
||||
}
|
||||
}, 1000 * 10, {
|
||||
immediate: false,
|
||||
afterMounted: true,
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
fetchGame();
|
||||
});
|
||||
@@ -78,10 +113,6 @@ onUnmounted(() => {
|
||||
}
|
||||
});
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
title: 'Reversi',
|
||||
icon: 'ti ti-device-gamepad',
|
||||
|
@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkPagination :pagination="myGamesPagination" :disableAutoLoad="true">
|
||||
<template #default="{ items }">
|
||||
<div :class="$style.gamePreviews">
|
||||
<MkA v-for="g in items" :key="g.id" v-panel :class="[$style.gamePreview, !g.isEnded && $style.gamePreviewActive]" tabindex="-1" :to="`/reversi/g/${g.id}`">
|
||||
<MkA v-for="g in items" :key="g.id" v-panel :class="[$style.gamePreview, !g.isStarted && !g.isEnded && $style.gamePreviewWaiting, g.isStarted && !g.isEnded && $style.gamePreviewActive]" tabindex="-1" :to="`/reversi/g/${g.id}`">
|
||||
<div :class="$style.gamePreviewPlayers">
|
||||
<span v-if="g.winnerId === g.user1Id" style="margin-right: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
|
||||
<span v-if="g.winnerId === g.user2Id" style="margin-right: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span>
|
||||
@@ -45,7 +45,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
|
||||
</div>
|
||||
<div :class="$style.gamePreviewFooter">
|
||||
<span v-if="!g.isEnded" :class="$style.gamePreviewStatusActive">{{ i18n.ts._reversi.playing }}</span>
|
||||
<span v-if="g.isStarted && !g.isEnded" :class="$style.gamePreviewStatusActive">{{ i18n.ts._reversi.playing }}</span>
|
||||
<span v-else-if="!g.isEnded" :class="$style.gamePreviewStatusWaiting"><MkEllipsis/></span>
|
||||
<span v-else>{{ i18n.ts._reversi.ended }}</span>
|
||||
<MkTime style="margin-left: auto; opacity: 0.7;" :time="g.createdAt"/>
|
||||
</div>
|
||||
@@ -60,7 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkPagination :pagination="gamesPagination" :disableAutoLoad="true">
|
||||
<template #default="{ items }">
|
||||
<div :class="$style.gamePreviews">
|
||||
<MkA v-for="g in items" :key="g.id" v-panel :class="[$style.gamePreview, !g.isEnded && $style.gamePreviewActive]" tabindex="-1" :to="`/reversi/g/${g.id}`">
|
||||
<MkA v-for="g in items" :key="g.id" v-panel :class="[$style.gamePreview, !g.isStarted && !g.isEnded && $style.gamePreviewWaiting, g.isStarted && !g.isEnded && $style.gamePreviewActive]" tabindex="-1" :to="`/reversi/g/${g.id}`">
|
||||
<div :class="$style.gamePreviewPlayers">
|
||||
<span v-if="g.winnerId === g.user1Id" style="margin-right: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
|
||||
<span v-if="g.winnerId === g.user2Id" style="margin-right: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span>
|
||||
@@ -71,7 +72,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
|
||||
</div>
|
||||
<div :class="$style.gamePreviewFooter">
|
||||
<span v-if="!g.isEnded" :class="$style.gamePreviewStatusActive">{{ i18n.ts._reversi.playing }}</span>
|
||||
<span v-if="g.isStarted && !g.isEnded" :class="$style.gamePreviewStatusActive">{{ i18n.ts._reversi.playing }}</span>
|
||||
<span v-else-if="!g.isEnded" :class="$style.gamePreviewStatusWaiting"><MkEllipsis/></span>
|
||||
<span v-else>{{ i18n.ts._reversi.ended }}</span>
|
||||
<MkTime style="margin-left: auto; opacity: 0.7;" :time="g.createdAt"/>
|
||||
</div>
|
||||
@@ -137,7 +139,9 @@ if ($i) {
|
||||
const connection = useStream().useChannel('reversi');
|
||||
|
||||
connection.on('matched', x => {
|
||||
startGame(x.game);
|
||||
if (matchingUser.value != null || matchingAny.value) {
|
||||
startGame(x.game);
|
||||
}
|
||||
});
|
||||
|
||||
connection.on('invited', invitation => {
|
||||
@@ -153,6 +157,7 @@ if ($i) {
|
||||
const invitations = ref<Misskey.entities.UserLite[]>([]);
|
||||
const matchingUser = ref<Misskey.entities.UserLite | null>(null);
|
||||
const matchingAny = ref<boolean>(false);
|
||||
const noIrregularRules = ref<boolean>(false);
|
||||
|
||||
function startGame(game: Misskey.entities.ReversiGameDetailed) {
|
||||
matchingUser.value = null;
|
||||
@@ -178,6 +183,7 @@ async function matchHeatbeat() {
|
||||
} else if (matchingAny.value) {
|
||||
const res = await misskeyApi('reversi/match', {
|
||||
userId: null,
|
||||
noIrregularRules: noIrregularRules.value,
|
||||
});
|
||||
|
||||
if (res != null) {
|
||||
@@ -195,10 +201,22 @@ async function matchUser() {
|
||||
matchHeatbeat();
|
||||
}
|
||||
|
||||
async function matchAny() {
|
||||
matchingAny.value = true;
|
||||
|
||||
matchHeatbeat();
|
||||
function matchAny(ev: MouseEvent) {
|
||||
os.popupMenu([{
|
||||
text: i18n.ts._reversi.allowIrregularRules,
|
||||
action: () => {
|
||||
noIrregularRules.value = false;
|
||||
matchingAny.value = true;
|
||||
matchHeatbeat();
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts._reversi.disallowIrregularRules,
|
||||
action: () => {
|
||||
noIrregularRules.value = true;
|
||||
matchingAny.value = true;
|
||||
matchHeatbeat();
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
function cancelMatching() {
|
||||
@@ -220,12 +238,14 @@ async function accept(user) {
|
||||
}
|
||||
}
|
||||
|
||||
useInterval(matchHeatbeat, 1000 * 10, { immediate: false, afterMounted: true });
|
||||
useInterval(matchHeatbeat, 1000 * 5, { immediate: false, afterMounted: true });
|
||||
|
||||
onMounted(() => {
|
||||
misskeyApi('reversi/invitations').then(_invitations => {
|
||||
invitations.value = _invitations;
|
||||
});
|
||||
|
||||
window.addEventListener('beforeunload', cancelMatching);
|
||||
});
|
||||
|
||||
onDeactivated(() => {
|
||||
@@ -273,6 +293,10 @@ definePageMetadata(computed(() => ({
|
||||
box-shadow: inset 0 0 8px 0px var(--accent);
|
||||
}
|
||||
|
||||
.gamePreviewWaiting {
|
||||
box-shadow: inset 0 0 8px 0px var(--warn);
|
||||
}
|
||||
|
||||
.gamePreviewPlayers {
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
@@ -306,6 +330,12 @@ definePageMetadata(computed(() => ({
|
||||
animation: blink 2s infinite;
|
||||
}
|
||||
|
||||
.gamePreviewStatusWaiting {
|
||||
color: var(--warn);
|
||||
font-weight: bold;
|
||||
animation: blink 2s infinite;
|
||||
}
|
||||
|
||||
.waitingScreen {
|
||||
text-align: center;
|
||||
}
|
||||
|
@@ -7,7 +7,6 @@
|
||||
|
||||
import { onUnmounted, Ref, ref, watch } from 'vue';
|
||||
import { BroadcastChannel } from 'broadcast-channel';
|
||||
import defaultsDeep from 'lodash.defaultsdeep';
|
||||
import { $i } from '@/account.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { get, set } from '@/scripts/idb-proxy.js';
|
||||
@@ -81,14 +80,37 @@ export class Storage<T extends StateDef> {
|
||||
this.loaded = this.ready.then(() => this.load());
|
||||
}
|
||||
|
||||
private isPureObject(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||
private isPureObject(value: unknown): value is Record<string | number | symbol, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
private mergeState<T>(value: T, def: T): T {
|
||||
/**
|
||||
* valueにないキーをdefからもらう(再帰的)\
|
||||
* nullはそのまま、undefinedはdefの値
|
||||
**/
|
||||
private mergeObject<X>(value: X, def: X): X {
|
||||
if (this.isPureObject(value) && this.isPureObject(def)) {
|
||||
if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def);
|
||||
return defaultsDeep(value, def) as T;
|
||||
const result = structuredClone(value) as X;
|
||||
for (const [k, v] of Object.entries(def) as [keyof X, X[keyof X]][]) {
|
||||
if (!Object.prototype.hasOwnProperty.call(value, k) || value[k] === undefined) {
|
||||
result[k] = v;
|
||||
} else if (this.isPureObject(v) && this.isPureObject(result[k])) {
|
||||
const child = structuredClone(result[k]) as X[keyof X] & Record<string | number | symbol, unknown>;
|
||||
result[k] = this.mergeObject<typeof v>(child, v);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private mergeState<X>(value: X, def: X): X {
|
||||
if (this.isPureObject(value) && this.isPureObject(def)) {
|
||||
const merged = this.mergeObject(value, def);
|
||||
|
||||
if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def, ' Result: ', merged);
|
||||
|
||||
return merged as X;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
Reference in New Issue
Block a user