diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2022-02-09 14:50:38 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2022-02-09 14:50:38 +0900 |
| commit | 5f985ee832eed61e31ceb51eaa1c51810ad6de39 (patch) | |
| tree | 60cd65d92e377ea0a0f0d2bc3b767f8f26196310 /packages/client/src/components | |
| parent | Merge branch 'develop' (diff) | |
| parent | 12.104.0 (diff) | |
| download | misskey-5f985ee832eed61e31ceb51eaa1c51810ad6de39.tar.gz misskey-5f985ee832eed61e31ceb51eaa1c51810ad6de39.tar.bz2 misskey-5f985ee832eed61e31ceb51eaa1c51810ad6de39.zip | |
Merge branch 'develop'
Diffstat (limited to 'packages/client/src/components')
| -rw-r--r-- | packages/client/src/components/chart-tooltip.vue | 2 | ||||
| -rw-r--r-- | packages/client/src/components/chart.vue | 224 | ||||
| -rw-r--r-- | packages/client/src/components/emoji-picker.vue | 4 | ||||
| -rw-r--r-- | packages/client/src/components/instance-stats.vue | 6 | ||||
| -rw-r--r-- | packages/client/src/components/instance-ticker.vue | 18 | ||||
| -rw-r--r-- | packages/client/src/components/post-form.vue | 1 | ||||
| -rw-r--r-- | packages/client/src/components/ui/modal.vue | 3 | ||||
| -rw-r--r-- | packages/client/src/components/ui/tooltip.vue | 141 |
8 files changed, 311 insertions, 88 deletions
diff --git a/packages/client/src/components/chart-tooltip.vue b/packages/client/src/components/chart-tooltip.vue index b080eaf2b4..20e094a5a7 100644 --- a/packages/client/src/components/chart-tooltip.vue +++ b/packages/client/src/components/chart-tooltip.vue @@ -1,5 +1,5 @@ <template> -<MkTooltip ref="tooltip" :showing="showing" :x="x" :y="y" :max-width="340" @closed="emit('closed')"> +<MkTooltip ref="tooltip" :showing="showing" :x="x" :y="y" :max-width="340" :direction="'left'" :inner-margin="16" @closed="emit('closed')"> <div v-if="title" class="qpcyisrl"> <div class="title">{{ title }}</div> <div v-for="x in series" class="series"> diff --git a/packages/client/src/components/chart.vue b/packages/client/src/components/chart.vue index 3e46c51b47..ced0d481c4 100644 --- a/packages/client/src/components/chart.vue +++ b/packages/client/src/components/chart.vue @@ -29,6 +29,7 @@ import { import 'chartjs-adapter-date-fns'; import { enUS } from 'date-fns/locale'; import zoomPlugin from 'chartjs-plugin-zoom'; +import gradient from 'chartjs-plugin-gradient'; import * as os from '@/os'; import { defaultStore } from '@/store'; import MkChartTooltip from '@/components/chart-tooltip.vue'; @@ -49,6 +50,7 @@ Chart.register( SubTitle, Filler, zoomPlugin, + gradient, ); const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b)); @@ -61,9 +63,17 @@ const alpha = (hex, a) => { return `rgba(${r}, ${g}, ${b}, ${a})`; }; -const colors = ['#008FFB', '#00E396', '#FEB019', '#FF4560']; +const colors = { + blue: '#008FFB', + green: '#00E396', + yellow: '#FEB019', + red: '#FF4560', + purple: '#e300db', + orange: '#fe6919', +}; +const colorSets = [colors.blue, colors.green, colors.yellow, colors.red, colors.purple]; const getColor = (i) => { - return colors[i % colors.length]; + return colorSets[i % colorSets.length]; }; export default defineComponent({ @@ -95,6 +105,11 @@ export default defineComponent({ required: false, default: false }, + bar: { + type: Boolean, + required: false, + default: false + }, aspectRatio: { type: Number, required: false, @@ -186,22 +201,36 @@ export default defineComponent({ // フォントカラー Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg'); + const maxes = data.series.map((x, i) => Math.max(...x.data.map(d => d.y))); + chartInstance = new Chart(chartEl.value, { - type: 'line', + type: props.bar ? 'bar' : 'line', data: { labels: new Array(props.limit).fill(0).map((_, i) => getDate(i).toLocaleString()).slice().reverse(), datasets: data.series.map((x, i) => ({ parsing: false, label: x.name, data: x.data.slice().reverse(), + tension: 0.3, pointRadius: 0, - tension: 0, borderWidth: 2, borderColor: x.color ? x.color : getColor(i), borderDash: x.borderDash || [], borderJoinStyle: 'round', backgroundColor: alpha(x.color ? x.color : getColor(i), 0.1), + gradient: { + backgroundColor: { + axis: 'y', + colors: { + 0: alpha(x.color ? x.color : getColor(i), 0), + [maxes[i]]: alpha(x.color ? x.color : getColor(i), 0.1), + }, + }, + }, + barPercentage: 0.9, + categoryPercentage: 0.9, fill: x.type === 'area', + clip: 8, hidden: !!x.hidden, })), }, @@ -210,7 +239,7 @@ export default defineComponent({ layout: { padding: { left: 0, - right: 0, + right: 8, top: 0, bottom: 0, }, @@ -218,6 +247,7 @@ export default defineComponent({ scales: { x: { type: 'time', + stacked: props.stacked, time: { stepSize: 1, unit: props.span === 'day' ? 'month' : 'day', @@ -228,6 +258,8 @@ export default defineComponent({ }, ticks: { display: props.detailed, + maxRotation: 0, + autoSkipPadding: 16, }, adapters: { date: { @@ -245,12 +277,21 @@ export default defineComponent({ }, ticks: { display: props.detailed, + //mirror: true, }, }, }, interaction: { intersect: false, + mode: 'index', }, + elements: { + point: { + hoverRadius: 5, + hoverBorderWidth: 2, + }, + }, + animation: false, plugins: { legend: { display: props.detailed, @@ -294,6 +335,7 @@ export default defineComponent({ }, } }, + gradient, }, }, plugins: [{ @@ -324,20 +366,60 @@ export default defineComponent({ // TODO }; - const fetchFederationInstancesChart = async (total: boolean): Promise<typeof data> => { + const fetchFederationChart = async (): Promise<typeof data> => { const raw = await os.api('charts/federation', { limit: props.limit, span: props.span }); return { series: [{ - name: 'Instances', + name: 'Total', type: 'area', - data: format(total - ? raw.instance.total - : sum(raw.instance.inc, negate(raw.instance.dec)) - ), + data: format(raw.instance.total), + color: '#888888', + }, { + name: 'Inc/Dec', + type: 'area', + data: format(sum(raw.instance.inc, negate(raw.instance.dec))), + color: colors.purple, + }, { + name: 'Received', + type: 'area', + data: format(raw.inboxInstances), + color: colors.blue, + }, { + name: 'Delivered', + type: 'area', + data: format(raw.deliveredInstances), + color: colors.green, + }, { + name: 'Stalled', + type: 'area', + data: format(raw.stalled), + color: colors.red, }], }; }; + const fetchApRequestChart = async (): Promise<typeof data> => { + const raw = await os.api('charts/ap-request', { limit: props.limit, span: props.span }); + return { + series: [{ + name: 'In', + type: 'area', + color: '#008FFB', + data: format(raw.inboxReceived) + }, { + name: 'Out (succ)', + type: 'area', + color: '#00E396', + data: format(raw.deliverSucceeded) + }, { + name: 'Out (fail)', + type: 'area', + color: '#FEB019', + data: format(raw.deliverFailed) + }] + }; + }; + const fetchNotesChart = async (type: string): Promise<typeof data> => { const raw = await os.api('charts/notes', { limit: props.limit, span: props.span }); return { @@ -349,6 +431,7 @@ export default defineComponent({ ? sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec)) : sum(raw[type].inc, negate(raw[type].dec)) ), + color: '#888888', }, { name: 'Renotes', type: 'area', @@ -356,6 +439,7 @@ export default defineComponent({ ? sum(raw.local.diffs.renote, raw.remote.diffs.renote) : raw[type].diffs.renote ), + color: colors.green, }, { name: 'Replies', type: 'area', @@ -363,6 +447,7 @@ export default defineComponent({ ? sum(raw.local.diffs.reply, raw.remote.diffs.reply) : raw[type].diffs.reply ), + color: colors.yellow, }, { name: 'Normal', type: 'area', @@ -370,6 +455,15 @@ export default defineComponent({ ? sum(raw.local.diffs.normal, raw.remote.diffs.normal) : raw[type].diffs.normal ), + color: colors.blue, + }, { + name: 'With file', + type: 'area', + data: format(type == 'combined' + ? sum(raw.local.diffs.withFile, raw.remote.diffs.withFile) + : raw[type].diffs.withFile + ), + color: colors.purple, }], }; }; @@ -425,17 +519,50 @@ export default defineComponent({ const raw = await os.api('charts/active-users', { limit: props.limit, span: props.span }); return { series: [{ - name: 'Combined', - type: 'line', - data: format(sum(raw.local.users, raw.remote.users)), + name: 'Read & Write', + type: 'area', + data: format(raw.readWrite), + color: colors.orange, }, { - name: 'Local', + name: 'Write', type: 'area', - data: format(raw.local.users), + data: format(raw.write), + color: colors.blue, }, { - name: 'Remote', + name: 'Read', + type: 'area', + data: format(raw.read), + color: '#888888', + }, { + name: '< Week', type: 'area', - data: format(raw.remote.users), + data: format(raw.registeredWithinWeek), + color: colors.green, + }, { + name: '< Month', + type: 'area', + data: format(raw.registeredWithinMonth), + color: colors.yellow, + }, { + name: '< Year', + type: 'area', + data: format(raw.registeredWithinYear), + color: colors.red, + }, { + name: '> Week', + type: 'area', + data: format(raw.registeredOutsideWeek), + color: colors.yellow, + }, { + name: '> Month', + type: 'area', + data: format(raw.registeredOutsideMonth), + color: colors.red, + }, { + name: '> Year', + type: 'area', + data: format(raw.registeredOutsideYear), + color: colors.purple, }], }; }; @@ -476,26 +603,6 @@ export default defineComponent({ }; }; - const fetchDriveTotalChart = async (): Promise<typeof data> => { - const raw = await os.api('charts/drive', { limit: props.limit, span: props.span }); - return { - bytes: true, - series: [{ - name: 'Combined', - type: 'line', - data: format(sum(raw.local.totalSize, raw.remote.totalSize)), - }, { - name: 'Local', - type: 'area', - data: format(raw.local.totalSize), - }, { - name: 'Remote', - type: 'area', - data: format(raw.remote.totalSize), - }], - }; - }; - const fetchDriveFilesChart = async (): Promise<typeof data> => { const raw = await os.api('charts/drive', { limit: props.limit, span: props.span }); return { @@ -531,25 +638,6 @@ export default defineComponent({ }; }; - const fetchDriveFilesTotalChart = async (): Promise<typeof data> => { - const raw = await os.api('charts/drive', { limit: props.limit, span: props.span }); - return { - series: [{ - name: 'Combined', - type: 'line', - data: format(sum(raw.local.totalCount, raw.remote.totalCount)), - }, { - name: 'Local', - type: 'area', - data: format(raw.local.totalCount), - }, { - name: 'Remote', - type: 'area', - data: format(raw.remote.totalCount), - }], - }; - }; - const fetchInstanceRequestsChart = async (): Promise<typeof data> => { const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); return { @@ -680,11 +768,26 @@ export default defineComponent({ }; }; + const fetchPerUserDriveChart = async (): Promise<typeof data> => { + const raw = await os.api('charts/user/drive', { userId: props.args.user.id, limit: props.limit, span: props.span }); + return { + series: [{ + name: 'Inc', + type: 'area', + data: format(raw.incSize), + }, { + name: 'Dec', + type: 'area', + data: format(raw.decSize), + }], + }; + }; + const fetchAndRender = async () => { const fetchData = () => { switch (props.src) { - case 'federation-instances': return fetchFederationInstancesChart(false); - case 'federation-instances-total': return fetchFederationInstancesChart(true); + case 'federation': return fetchFederationChart(); + case 'ap-request': return fetchApRequestChart(); case 'users': return fetchUsersChart(false); case 'users-total': return fetchUsersChart(true); case 'active-users': return fetchActiveUsersChart(); @@ -693,9 +796,7 @@ export default defineComponent({ case 'remote-notes': return fetchNotesChart('remote'); case 'notes-total': return fetchNotesTotalChart(); case 'drive': return fetchDriveChart(); - case 'drive-total': return fetchDriveTotalChart(); case 'drive-files': return fetchDriveFilesChart(); - case 'drive-files-total': return fetchDriveFilesTotalChart(); case 'instance-requests': return fetchInstanceRequestsChart(); case 'instance-users': return fetchInstanceUsersChart(false); @@ -710,6 +811,7 @@ export default defineComponent({ case 'instance-drive-files-total': return fetchInstanceDriveFilesChart(true); case 'per-user-notes': return fetchPerUserNotesChart(); + case 'per-user-drive': return fetchPerUserDriveChart(); } }; fetching.value = true; diff --git a/packages/client/src/components/emoji-picker.vue b/packages/client/src/components/emoji-picker.vue index 6999ad6517..3e1208979f 100644 --- a/packages/client/src/components/emoji-picker.vue +++ b/packages/client/src/components/emoji-picker.vue @@ -81,7 +81,7 @@ import { getStaticImageUrl } from '@/scripts/get-static-image-url'; import Ripple from '@/components/ripple.vue'; import * as os from '@/os'; import { isTouchUsing } from '@/scripts/touch'; -import { isMobile } from '@/scripts/is-mobile'; +import { deviceKind } from '@/scripts/device-kind'; import { emojiCategories, instance } from '@/instance'; import XSection from './emoji-picker.section.vue'; import { i18n } from '@/i18n'; @@ -263,7 +263,7 @@ watch(q, () => { }); function focus() { - if (!isMobile && !isTouchUsing) { + if (!['smartphone', 'tablet'].includes(deviceKind) && !isTouchUsing) { search.value?.focus({ preventScroll: true }); diff --git a/packages/client/src/components/instance-stats.vue b/packages/client/src/components/instance-stats.vue index 409c3a49ca..d2aa5a151a 100644 --- a/packages/client/src/components/instance-stats.vue +++ b/packages/client/src/components/instance-stats.vue @@ -3,8 +3,8 @@ <div class="selects" style="display: flex;"> <MkSelect v-model="chartSrc" style="margin: 0; flex: 1;"> <optgroup :label="$ts.federation"> - <option value="federation-instances">{{ $ts._charts.federationInstancesIncDec }}</option> - <option value="federation-instances-total">{{ $ts._charts.federationInstancesTotal }}</option> + <option value="federation">{{ $ts._charts.federation }}</option> + <option value="ap-request">{{ $ts._charts.apRequest }}</option> </optgroup> <optgroup :label="$ts.users"> <option value="users">{{ $ts._charts.usersIncDec }}</option> @@ -19,9 +19,7 @@ </optgroup> <optgroup :label="$ts.drive"> <option value="drive-files">{{ $ts._charts.filesIncDec }}</option> - <option value="drive-files-total">{{ $ts._charts.filesTotal }}</option> <option value="drive">{{ $ts._charts.storageUsageIncDec }}</option> - <option value="drive-total">{{ $ts._charts.storageUsageTotal }}</option> </optgroup> </MkSelect> <MkSelect v-model="chartSpan" style="margin: 0 0 0 10px;"> diff --git a/packages/client/src/components/instance-ticker.vue b/packages/client/src/components/instance-ticker.vue index 77fd8bb344..9b0a18ec90 100644 --- a/packages/client/src/components/instance-ticker.vue +++ b/packages/client/src/components/instance-ticker.vue @@ -7,15 +7,27 @@ <script lang="ts" setup> import { } from 'vue'; +import { instanceName } from '@/config'; const props = defineProps<{ - instance: any; // TODO + instance?: { + faviconUrl?: string + name: string + themeColor?: string + } }>(); -const themeColor = props.instance.themeColor || '#777777'; +// if no instance data is given, this is for the local instance +const instance = props.instance ?? { + faviconUrl: '/favicon.ico', + name: instanceName, + themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement)?.content +}; + +const themeColor = instance.themeColor ?? '#777777'; const bg = { - background: `linear-gradient(90deg, ${themeColor}, ${themeColor + '00'})` + background: `linear-gradient(90deg, ${themeColor}, ${themeColor}00)` }; </script> diff --git a/packages/client/src/components/post-form.vue b/packages/client/src/components/post-form.vue index 535218ecf9..64a6478f45 100644 --- a/packages/client/src/components/post-form.vue +++ b/packages/client/src/components/post-form.vue @@ -342,6 +342,7 @@ function addTag(tag: string) { function focus() { textareaEl.focus(); + textareaEl.setSelectionRange(textareaEl.value.length, textareaEl.value.length); } function chooseFileFrom(ev) { diff --git a/packages/client/src/components/ui/modal.vue b/packages/client/src/components/ui/modal.vue index 3c3bb5c226..b42c0e4d42 100644 --- a/packages/client/src/components/ui/modal.vue +++ b/packages/client/src/components/ui/modal.vue @@ -14,6 +14,7 @@ import { nextTick, onMounted, computed, ref, watch, provide } from 'vue'; import * as os from '@/os'; import { isTouchUsing } from '@/scripts/touch'; import { defaultStore } from '@/store'; +import { deviceKind } from '@/scripts/device-kind'; function getFixedContainer(el: Element | null): Element | null { if (el == null || el.tagName === 'BODY') return null; @@ -62,7 +63,7 @@ const content = ref<HTMLElement>(); const zIndex = os.claimZIndex(props.zPriority); const type = computed(() => { if (props.preferType === 'auto') { - if (!defaultStore.state.disableDrawer && isTouchUsing && window.innerWidth < 500 && window.innerHeight < 1000) { + if (!defaultStore.state.disableDrawer && isTouchUsing && deviceKind === 'smartphone') { return 'drawer'; } else { return props.src != null ? 'popup' : 'dialog'; diff --git a/packages/client/src/components/ui/tooltip.vue b/packages/client/src/components/ui/tooltip.vue index 1892877cc1..3ccd1b7316 100644 --- a/packages/client/src/components/ui/tooltip.vue +++ b/packages/client/src/components/ui/tooltip.vue @@ -17,8 +17,12 @@ const props = withDefaults(defineProps<{ y?: number; text?: string; maxWidth?: number; + direction?: 'top' | 'bottom' | 'right' | 'left'; + innerMargin?: number; }>(), { maxWidth: 250, + direction: 'top', + innerMargin: 0, }); const emit = defineEmits<{ @@ -34,39 +38,144 @@ const setPosition = () => { const contentWidth = el.value.offsetWidth; const contentHeight = el.value.offsetHeight; - let left: number; - let top: number; - let rect: DOMRect; if (props.targetElement) { rect = props.targetElement.getBoundingClientRect(); + } + + const calcPosWhenTop = () => { + let left: number; + let top: number; - left = rect.left + window.pageXOffset + (props.targetElement.offsetWidth / 2); - top = rect.top + window.pageYOffset - contentHeight; + if (props.targetElement) { + left = rect.left + window.pageXOffset + (props.targetElement.offsetWidth / 2); + top = (rect.top + window.pageYOffset - contentHeight) - props.innerMargin; + } else { + left = props.x; + top = (props.y - contentHeight) - props.innerMargin; + } - el.value.style.transformOrigin = 'center bottom'; - } else { - left = props.x; - top = props.y - contentHeight; + left -= (el.value.offsetWidth / 2); + + if (left + contentWidth - window.pageXOffset > window.innerWidth) { + left = window.innerWidth - contentWidth + window.pageXOffset - 1; + } + + return [left, top]; } - left -= (el.value.offsetWidth / 2); + const calcPosWhenBottom = () => { + let left: number; + let top: number; + + if (props.targetElement) { + left = rect.left + window.pageXOffset + (props.targetElement.offsetWidth / 2); + top = (rect.top + window.pageYOffset + props.targetElement.offsetHeight) + props.innerMargin; + } else { + left = props.x; + top = (props.y) + props.innerMargin; + } + + left -= (el.value.offsetWidth / 2); - if (left + contentWidth - window.pageXOffset > window.innerWidth) { - left = window.innerWidth - contentWidth + window.pageXOffset - 1; + if (left + contentWidth - window.pageXOffset > window.innerWidth) { + left = window.innerWidth - contentWidth + window.pageXOffset - 1; + } + + return [left, top]; } - // ツールチップを上に向かって表示するスペースがなければ下に向かって出す - if (top - window.pageYOffset < 0) { + const calcPosWhenLeft = () => { + let left: number; + let top: number; + if (props.targetElement) { - top = rect.top + window.pageYOffset + props.targetElement.offsetHeight; - el.value.style.transformOrigin = 'center top'; + left = (rect.left + window.pageXOffset - contentWidth) - props.innerMargin; + top = rect.top + window.pageYOffset + (props.targetElement.offsetHeight / 2); } else { + left = (props.x - contentWidth) - props.innerMargin; top = props.y; } + + top -= (el.value.offsetHeight / 2); + + if (top + contentHeight - window.pageYOffset > window.innerHeight) { + top = window.innerHeight - contentHeight + window.pageYOffset - 1; + } + + return [left, top]; + } + + const calcPosWhenRight = () => { + let left: number; + let top: number; + + if (props.targetElement) { + left = (rect.left + window.pageXOffset) + props.innerMargin; + top = rect.top + window.pageYOffset + (props.targetElement.offsetHeight / 2); + } else { + left = props.x + props.innerMargin; + top = props.y; + } + + top -= (el.value.offsetHeight / 2); + + if (top + contentHeight - window.pageYOffset > window.innerHeight) { + top = window.innerHeight - contentHeight + window.pageYOffset - 1; + } + + return [left, top]; + } + + const calc = (): { + left: number; + top: number; + transformOrigin: string; + } => { + switch (props.direction) { + case 'top': { + const [left, top] = calcPosWhenTop(); + + // ツールチップを上に向かって表示するスペースがなければ下に向かって出す + if (top - window.pageYOffset < 0) { + const [left, top] = calcPosWhenBottom(); + return { left, top, transformOrigin: 'center top' }; + } + + return { left, top, transformOrigin: 'center bottom' }; + } + + case 'bottom': { + const [left, top] = calcPosWhenBottom(); + // TODO: ツールチップを下に向かって表示するスペースがなければ上に向かって出す + return { left, top, transformOrigin: 'center top' }; + } + + case 'left': { + const [left, top] = calcPosWhenLeft(); + + // ツールチップを左に向かって表示するスペースがなければ右に向かって出す + if (left - window.pageXOffset < 0) { + const [left, top] = calcPosWhenRight(); + return { left, top, transformOrigin: 'left center' }; + } + + return { left, top, transformOrigin: 'right center' }; + } + + case 'right': { + const [left, top] = calcPosWhenRight(); + // TODO: ツールチップを右に向かって表示するスペースがなければ左に向かって出す + return { left, top, transformOrigin: 'left center' }; + } + } + + return null as never; } + const { left, top, transformOrigin } = calc(); + el.value.style.transformOrigin = transformOrigin; el.value.style.left = left + 'px'; el.value.style.top = top + 'px'; }; |