Merge remote-tracking branch 'misskey-dev/develop' into io
This commit is contained in:
		
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -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; | ||||
| } | ||||
|   | ||||
| @@ -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: 'ti ti-info-circle', | ||||
| 		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); | ||||
|   | ||||
| @@ -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> | ||||
| @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 	<div class="_gaps"> | ||||
| 		<MkFoldableSection class="item"> | ||||
| 			<template #header><i class="ti ti-activity"></i> Heatmap</template> | ||||
| 			<XHeatmap :user="user" :src="'notes'"/> | ||||
| 			<MkHeatmap :user="user" :src="'notes'"/> | ||||
| 		</MkFoldableSection> | ||||
| 		<MkFoldableSection class="item"> | ||||
| 			<template #header><i class="ti ti-pencil"></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; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 まっちゃとーにゅ
					まっちゃとーにゅ