merge: upstream

This commit is contained in:
Marie 2024-01-15 12:07:35 +01:00
commit 4c1f6be735
No known key found for this signature in database
GPG key ID: 56569BBE47D2C828
132 changed files with 12167 additions and 792 deletions

File diff suppressed because it is too large Load diff

View file

@ -4,16 +4,16 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkSpacer :contentMax="800">
<Transition
:enterActiveClass="$style.transition_zoom_enterActive"
:leaveActiveClass="$style.transition_zoom_leaveActive"
:enterFromClass="$style.transition_zoom_enterFrom"
:leaveToClass="$style.transition_zoom_leaveTo"
:moveClass="$style.transition_zoom_move"
mode="out-in"
>
<div v-if="!gameStarted" :class="$style.root">
<Transition
:enterActiveClass="$style.transition_zoom_enterActive"
:leaveActiveClass="$style.transition_zoom_leaveActive"
:enterFromClass="$style.transition_zoom_enterFrom"
:leaveToClass="$style.transition_zoom_leaveTo"
:moveClass="$style.transition_zoom_move"
mode="out-in"
>
<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">
@ -26,6 +26,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSelect v-model="gameMode">
<option value="normal">NORMAL</option>
<option value="square">SQUARE</option>
<option value="yen">YEN</option>
<option value="sweets">SWEETS</option>
</MkSelect>
<MkButton primary gradate large rounded inline @click="start">{{ i18n.ts.start }}</MkButton>
</div>
@ -42,12 +44,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.frame">
<div :class="$style.frameInner">
<div class="_gaps_s" style="padding: 16px;">
<div><b>{{ i18n.ts.ranking }}</b> ({{ gameMode }})</div>
<div><b>{{ i18n.t('lastNDays', { n: 7 }) }} {{ i18n.ts.ranking }}</b> ({{ gameMode }})</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"/>
<MkUserName :user="r.user" :nowrap="true"/>
<b style="margin-left: auto;">{{ r.score.toLocaleString() }} pt</b>
<b style="margin-left: auto;">{{ r.score.toLocaleString() }} {{ getScoreUnit(gameMode) }}</b>
</div>
</div>
<div v-else>{{ i18n.ts.loading }}</div>
@ -77,15 +79,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
</div>
<div v-else>
<XGame :gameMode="gameMode" :mute="mute" @end="onGameEnd"/>
</div>
</Transition>
</MkSpacer>
</MkSpacer>
<XGame v-else :gameMode="gameMode" :mute="mute" @end="onGameEnd"/>
</Transition>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { computed, ref, watch } from 'vue';
import XGame from './drop-and-fusion.game.vue';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkButton from '@/components/MkButton.vue';
@ -94,7 +94,7 @@ import MkSelect from '@/components/MkSelect.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import { misskeyApiGet } from '@/scripts/misskey-api.js';
const gameMode = ref<'normal' | 'square'>('normal');
const gameMode = ref<'normal' | 'square' | 'yen' | 'sweets'>('normal');
const gameStarted = ref(false);
const mute = ref(false);
const ranking = ref(null);
@ -103,6 +103,14 @@ watch(gameMode, async () => {
ranking.value = await misskeyApiGet('bubble-game/ranking', { gameMode: gameMode.value });
}, { immediate: true });
function getScoreUnit(gameMode: string) {
return gameMode === 'normal' ? 'pt' :
gameMode === 'square' ? 'pt' :
gameMode === 'yen' ? '円' :
gameMode === 'sweets' ? 'kcal' :
'' as never;
}
async function start() {
gameStarted.value = true;
}

View file

@ -14,19 +14,15 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { } from 'vue';
import * as os from '@/os.js';
import * as Misskey from 'misskey-js';
import { misskeyApiGet } from '@/scripts/misskey-api.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { i18n } from '@/i18n.js';
import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
const props = defineProps<{
emoji: {
name: string;
aliases: string[];
category: string;
url: string;
};
emoji: Misskey.entities.EmojiSimple;
}>();
function menu(ev) {
@ -43,12 +39,13 @@ function menu(ev) {
}, {
text: i18n.ts.info,
icon: 'ph-info ph-bold ph-lg',
action: () => {
misskeyApiGet('emoji', { name: props.emoji.name }).then(res => {
os.alert({
type: 'info',
text: `Name: ${res.name}\nAliases: ${res.aliases.join(' ')}\nCategory: ${res.category}\nisSensitive: ${res.isSensitive}\nlocalOnly: ${res.localOnly}\nLicense: ${res.license}\nURL: ${res.url}`,
});
action: async () => {
os.popup(MkCustomEmojiDetailedDialog, {
emoji: await misskeyApiGet('emoji', {
name: props.emoji.name,
})
}, {
anchor: ev.target,
});
},
}], ev.currentTarget ?? ev.target);

View file

@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-model="title">
<template #label>{{ i18n.ts._play.title }}</template>
</MkInput>
<MkTextarea v-model="summary">
<MkTextarea v-model="summary" :mfmAutocomplete="true" :mfmPreview="true">
<template #label>{{ i18n.ts._play.summary }}</template>
</MkTextarea>
<MkButton primary @click="selectPreset">{{ i18n.ts.selectFromPresets }}<i class="ph-caret-down ph-bold ph-lg"></i></MkButton>

View file

@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-else :class="$style.ready">
<div class="_panel main">
<div class="title">{{ flash.title }}</div>
<div class="summary">{{ flash.summary }}</div>
<div class="summary"><Mfm :text="flash.summary"/></div>
<MkButton class="start" gradate rounded large @click="start">Play</MkButton>
<div class="info">
<span v-tooltip="i18n.ts.numberOfLikes"><i class="ph-heart ph-bold ph-lg"></i> {{ flash.likedCount }}</span>

View file

@ -0,0 +1,27 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkStickyContainer>
<template #header><MkPageHeader/></template>
<MkSpacer :contentMax="800">
<div class="_panel">
<MkA to="/bubble-game">
<img src="/client-assets/drop-and-fusion/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/>
</MkA>
</div>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
definePageMetadata({
title: 'Misskey Games',
icon: 'ti ti-device-gamepad',
});
</script>

View file

@ -247,9 +247,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps">
<MkFolder>
<template #label>{{ i18n.ts.additionalEmojiDictionary }}</template>
<div v-for="lang in emojiIndexLangs" class="_buttons">
<MkButton @click="downloadEmojiIndex(lang)"><i class="ph-download ph-bold ph-lg"></i> {{ lang }}{{ defaultStore.reactiveState.additionalUnicodeEmojiIndexes.value[lang] ? ` (${ i18n.ts.installed })` : '' }}</MkButton>
<MkButton v-if="defaultStore.reactiveState.additionalUnicodeEmojiIndexes.value[lang]" danger @click="removeEmojiIndex(lang)"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.remove }}</MkButton>
<div class="_buttons">
<template v-for="lang in emojiIndexLangs" :key="lang">
<MkButton v-if="defaultStore.reactiveState.additionalUnicodeEmojiIndexes.value[lang]" danger @click="removeEmojiIndex(lang)"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.remove }} ({{ getEmojiIndexLangName(lang) }})</MkButton>
<MkButton v-else @click="downloadEmojiIndex(lang)"><i class="ph-download ph-bold ph-lg"></i> {{ getEmojiIndexLangName(lang) }}{{ defaultStore.reactiveState.additionalUnicodeEmojiIndexes.value[lang] ? ` (${ i18n.ts.installed })` : '' }}</MkButton>
</template>
</div>
</MkFolder>
<FormLink to="/settings/deck">{{ i18n.ts.deck }}</FormLink>
@ -408,15 +410,29 @@ watch([
await reloadAsk();
});
const emojiIndexLangs = ['en-US'];
const emojiIndexLangs = ['en-US', 'ja-JP', 'ja-JP_hira'] as const;
function downloadEmojiIndex(lang: string) {
function getEmojiIndexLangName(targetLang: typeof emojiIndexLangs[number]) {
if (langs.find(x => x[0] === targetLang)) {
return langs.find(x => x[0] === targetLang)![1];
} else {
//
switch (targetLang) {
case 'ja-JP_hira': return 'ひらがな';
default: return targetLang;
}
}
}
function downloadEmojiIndex(lang: typeof emojiIndexLangs[number]) {
async function main() {
const currentIndexes = defaultStore.state.additionalUnicodeEmojiIndexes;
function download() {
switch (lang) {
case 'en-US': return import('../../unicode-emoji-indexes/en-US.json').then(x => x.default);
case 'ja-JP': return import('../../unicode-emoji-indexes/ja-JP.json').then(x => x.default);
case 'ja-JP_hira': return import('../../unicode-emoji-indexes/ja-JP_hira.json').then(x => x.default);
default: throw new Error('unrecognized lang: ' + lang);
}
}

View file

@ -1,219 +0,0 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div ref="rootEl">
<MkLoading v-if="fetching"/>
<div v-else :class="$style.root" class="_panel">
<canvas ref="chartEl"></canvas>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, nextTick, watch, shallowRef, ref } from 'vue';
import { Chart } from 'chart.js';
import * as Misskey from 'misskey-js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js';
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
import { alpha } from '@/scripts/color.js';
import { initChart } from '@/scripts/init-chart.js';
initChart();
const props = defineProps<{
src: string;
user: Misskey.entities.User;
}>();
const rootEl = shallowRef<HTMLDivElement>(null);
const chartEl = shallowRef<HTMLCanvasElement>(null);
const now = new Date();
let chartInstance: Chart = null;
const fetching = ref(true);
const { handler: externalTooltipHandler } = useChartTooltip({
position: 'middle',
});
async function renderChart() {
if (chartInstance) {
chartInstance.destroy();
}
const wide = rootEl.value.offsetWidth > 700;
const narrow = rootEl.value.offsetWidth < 400;
const weeks = wide ? 50 : narrow ? 10 : 25;
const chartLimit = 7 * weeks;
const getDate = (ago: number) => {
const y = now.getFullYear();
const m = now.getMonth();
const d = now.getDate();
return new Date(y, m, d - ago);
};
const format = (arr) => {
return arr.map((v, i) => {
const dt = getDate(i);
const iso = `${dt.getFullYear()}-${(dt.getMonth() + 1).toString().padStart(2, '0')}-${dt.getDate().toString().padStart(2, '0')}`;
return {
x: iso,
y: dt.getDay(),
d: iso,
v,
};
});
};
let values;
if (props.src === 'notes') {
const raw = await misskeyApi('charts/user/notes', { userId: props.user.id, limit: chartLimit, span: 'day' });
values = raw.inc;
}
fetching.value = false;
await nextTick();
const color = defaultStore.state.darkMode ? '#b4e900' : '#86b300';
// 3
const max = values.slice().sort((a, b) => b - a).slice(0, 3).reduce((a, b) => a + b, 0) / 3;
const min = Math.max(0, Math.min(...values) - 1);
const marginEachCell = 4;
chartInstance = new Chart(chartEl.value, {
type: 'matrix',
data: {
datasets: [{
label: '',
data: format(values),
pointRadius: 0,
borderWidth: 0,
borderJoinStyle: 'round',
borderRadius: 3,
backgroundColor(c) {
const value = c.dataset.data[c.dataIndex].v;
let a = (value - min) / max;
if (value !== 0) { // 0
a = Math.max(a, 0.05);
}
return alpha(color, a);
},
fill: true,
width(c) {
const a = c.chart.chartArea ?? {};
return (a.right - a.left) / weeks - marginEachCell;
},
height(c) {
const a = c.chart.chartArea ?? {};
return (a.bottom - a.top) / 7 - marginEachCell;
},
/* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107>
}] satisfies ChartData[],
*/
}],
},
options: {
aspectRatio: wide ? 6 : narrow ? 1.8 : 3.2,
layout: {
padding: {
left: 8,
right: 0,
top: 0,
bottom: 0,
},
},
scales: {
x: {
type: 'time',
offset: true,
position: 'bottom',
time: {
unit: 'week',
round: 'week',
isoWeekday: 0,
displayFormats: {
day: 'M/d',
month: 'Y/M',
week: 'M/d',
},
},
grid: {
display: false,
},
ticks: {
display: true,
maxRotation: 0,
autoSkipPadding: 8,
},
},
y: {
offset: true,
reverse: true,
position: 'right',
grid: {
display: false,
},
ticks: {
maxRotation: 0,
autoSkip: true,
padding: 1,
font: {
size: 9,
},
callback: (value, index, values) => ['', 'Mon', '', 'Wed', '', 'Fri', ''][value],
},
},
},
plugins: {
legend: {
display: false,
},
tooltip: {
enabled: false,
callbacks: {
title(context) {
const v = context[0].dataset.data[context[0].dataIndex];
return v.d;
},
label(context) {
const v = context.dataset.data[context.dataIndex];
return [v.v];
},
},
//mode: 'index',
animation: {
duration: 0,
},
external: externalTooltipHandler,
},
},
},
});
}
watch(() => props.src, () => {
fetching.value = true;
renderChart();
});
onMounted(async () => {
renderChart();
});
</script>
<style lang="scss" module>
.root {
padding: 20px;
}
</style>

View file

@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps">
<MkFoldableSection class="item">
<template #header><i class="ph-pulse ph-bold ph-lg"></i> Heatmap</template>
<XHeatmap :user="user" :src="'notes'"/>
<MkHeatmap :user="user" :src="'notes'"/>
</MkFoldableSection>
<MkFoldableSection class="item">
<template #header><i class="ph-pencil ph-bold ph-lg"></i> Notes</template>
@ -28,11 +28,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
import XHeatmap from './activity.heatmap.vue';
import XPv from './activity.pv.vue';
import XNotes from './activity.notes.vue';
import XFollowing from './activity.following.vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
import MkHeatmap from '@/components/MkHeatmap.vue';
const props = defineProps<{
user: Misskey.entities.User;