diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2022-06-11 19:31:03 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-06-11 19:31:03 +0900 |
| commit | 182a1bf653ecfbcf76e4530b3077c6252b0d4827 (patch) | |
| tree | 45d1472747d4cac017e96616f844292f5785ccdd /packages/client/src/components | |
| parent | 12.110.1 (diff) | |
| parent | 12.111.0 (diff) | |
| download | sharkey-182a1bf653ecfbcf76e4530b3077c6252b0d4827.tar.gz sharkey-182a1bf653ecfbcf76e4530b3077c6252b0d4827.tar.bz2 sharkey-182a1bf653ecfbcf76e4530b3077c6252b0d4827.zip | |
Merge pull request #8783 from misskey-dev/develop
Release: 12.111.0
Diffstat (limited to 'packages/client/src/components')
81 files changed, 1813 insertions, 1539 deletions
diff --git a/packages/client/src/components/abuse-report-window.vue b/packages/client/src/components/abuse-report-window.vue index f2cb369802..5114349620 100644 --- a/packages/client/src/components/abuse-report-window.vue +++ b/packages/client/src/components/abuse-report-window.vue @@ -37,7 +37,7 @@ const props = defineProps<{ }>(); const emit = defineEmits<{ - (e: 'closed'): void; + (ev: 'closed'): void; }>(); const window = ref<InstanceType<typeof XWindow>>(); diff --git a/packages/client/src/components/abuse-report.vue b/packages/client/src/components/abuse-report.vue index b67cef209b..a947406f88 100644 --- a/packages/client/src/components/abuse-report.vue +++ b/packages/client/src/components/abuse-report.vue @@ -2,7 +2,7 @@ <div class="bcekxzvu _card _gap"> <div class="_content target"> <MkAvatar class="avatar" :user="report.targetUser" :show-indicator="true"/> - <MkA class="info" :to="userPage(report.targetUser)" v-user-preview="report.targetUserId"> + <MkA v-user-preview="report.targetUserId" class="info" :to="userPage(report.targetUser)"> <MkUserName class="name" :user="report.targetUser"/> <MkAcct class="acct" :user="report.targetUser" style="display: block;"/> </MkA> @@ -43,20 +43,20 @@ export default defineComponent({ MkSwitch, }, - emits: ['resolved'], - props: { report: { type: Object, required: true, } - } + }, + + emits: ['resolved'], data() { return { forward: this.report.forwarded, }; - } + }, methods: { acct, diff --git a/packages/client/src/components/analog-clock.vue b/packages/client/src/components/analog-clock.vue index 59b8e97304..18dd1e3f41 100644 --- a/packages/client/src/components/analog-clock.vue +++ b/packages/client/src/components/analog-clock.vue @@ -42,7 +42,7 @@ <script lang="ts" setup> import { ref, computed, onMounted, onBeforeUnmount } from 'vue'; -import * as tinycolor from 'tinycolor2'; +import tinycolor from 'tinycolor2'; withDefaults(defineProps<{ thickness: number; diff --git a/packages/client/src/components/autocomplete.vue b/packages/client/src/components/autocomplete.vue index adeac4e050..1e4a4506f7 100644 --- a/packages/client/src/components/autocomplete.vue +++ b/packages/client/src/components/autocomplete.vue @@ -131,8 +131,8 @@ const props = defineProps<{ }>(); const emit = defineEmits<{ - (e: 'done', v: { type: string; value: any }): void; - (e: 'closed'): void; + (event: 'done', value: { type: string; value: any }): void; + (event: 'closed'): void; }>(); const suggests = ref<Element>(); @@ -152,7 +152,7 @@ function complete(type: string, value: any) { emit('closed'); if (type === 'emoji') { let recents = defaultStore.state.recentlyUsedEmojis; - recents = recents.filter((e: any) => e !== value); + recents = recents.filter((emoji: any) => emoji !== value); recents.unshift(value); defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32)); } @@ -232,7 +232,7 @@ function exec() { } else if (props.type === 'emoji') { if (!props.q || props.q === '') { // 最近使った絵文字をサジェスト - emojis.value = defaultStore.state.recentlyUsedEmojis.map(emoji => emojiDb.find(e => e.emoji === emoji)).filter(x => x) as EmojiDef[]; + emojis.value = defaultStore.state.recentlyUsedEmojis.map(emoji => emojiDb.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[]; return; } @@ -269,17 +269,17 @@ function exec() { } } -function onMousedown(e: Event) { - if (!contains(rootEl.value, e.target) && (rootEl.value !== e.target)) props.close(); +function onMousedown(event: Event) { + if (!contains(rootEl.value, event.target) && (rootEl.value !== event.target)) props.close(); } -function onKeydown(e: KeyboardEvent) { +function onKeydown(event: KeyboardEvent) { const cancel = () => { - e.preventDefault(); - e.stopPropagation(); + event.preventDefault(); + event.stopPropagation(); }; - switch (e.key) { + switch (event.key) { case 'Enter': if (select.value !== -1) { cancel(); @@ -310,7 +310,7 @@ function onKeydown(e: KeyboardEvent) { break; default: - e.stopPropagation(); + event.stopPropagation(); props.textarea.focus(); } } diff --git a/packages/client/src/components/captcha.vue b/packages/client/src/components/captcha.vue index ccd8880df8..183658471b 100644 --- a/packages/client/src/components/captcha.vue +++ b/packages/client/src/components/captcha.vue @@ -27,8 +27,7 @@ type CaptchaContainer = { }; declare global { - interface Window extends CaptchaContainer { - } + interface Window extends CaptchaContainer { } } const props = defineProps<{ diff --git a/packages/client/src/components/channel-follow-button.vue b/packages/client/src/components/channel-follow-button.vue index 7bbf5ae663..dff02beec0 100644 --- a/packages/client/src/components/channel-follow-button.vue +++ b/packages/client/src/components/channel-follow-button.vue @@ -48,8 +48,8 @@ async function onClick() { }); isFollowing.value = true; } - } catch (e) { - console.error(e); + } catch (err) { + console.error(err); } finally { wait.value = false; } diff --git a/packages/client/src/components/chart.vue b/packages/client/src/components/chart.vue index cc1aa9c20a..4e9c4e587a 100644 --- a/packages/client/src/components/chart.vue +++ b/packages/client/src/components/chart.vue @@ -7,8 +7,13 @@ </div> </template> -<script lang="ts"> -import { defineComponent, onMounted, ref, watch, PropType, onUnmounted, shallowRef } from 'vue'; +<script lang="ts" setup> +/* eslint-disable id-denylist -- + Chart.js has a `data` attribute in most chart definitions, which triggers the + id-denylist violation when setting it. This is causing about 60+ lint issues. + As this is part of Chart.js's API it makes sense to disable the check here. +*/ +import { defineProps, onMounted, ref, watch, PropType, onUnmounted } from 'vue'; import { Chart, ArcElement, @@ -29,11 +34,53 @@ import { import 'chartjs-adapter-date-fns'; import { enUS } from 'date-fns/locale'; import zoomPlugin from 'chartjs-plugin-zoom'; -import gradient from 'chartjs-plugin-gradient'; +// https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114242002 +// We can't use gradient because Vite throws a error. +//import gradient from 'chartjs-plugin-gradient'; import * as os from '@/os'; import { defaultStore } from '@/store'; import MkChartTooltip from '@/components/chart-tooltip.vue'; +const props = defineProps({ + src: { + type: String, + required: true, + }, + args: { + type: Object, + required: false, + }, + limit: { + type: Number, + required: false, + default: 90 + }, + span: { + type: String as PropType<'hour' | 'day'>, + required: true, + }, + detailed: { + type: Boolean, + required: false, + default: false + }, + stacked: { + type: Boolean, + required: false, + default: false + }, + bar: { + type: Boolean, + required: false, + default: false + }, + aspectRatio: { + type: Number, + required: false, + default: null + }, +}); + Chart.register( ArcElement, LineElement, @@ -50,7 +97,7 @@ Chart.register( SubTitle, Filler, zoomPlugin, - gradient, + //gradient, ); const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b)); @@ -78,826 +125,777 @@ const getColor = (i) => { return colorSets[i % colorSets.length]; }; -export default defineComponent({ - props: { - src: { - type: String, - required: true, - }, - args: { - type: Object, - required: false, - }, - limit: { - type: Number, - required: false, - default: 90 - }, - span: { - type: String as PropType<'hour' | 'day'>, - required: true, - }, - detailed: { - type: Boolean, - required: false, - default: false - }, - stacked: { - type: Boolean, - required: false, - default: false - }, - bar: { - type: Boolean, - required: false, - default: false - }, - aspectRatio: { - type: Number, - required: false, - default: null - }, - }, +const now = new Date(); +let chartInstance: Chart = null; +let chartData: { + series: { + name: string; + type: 'line' | 'area'; + color?: string; + dashed?: boolean; + hidden?: boolean; + data: { + x: number; + y: number; + }[]; + }[]; +} = null; - setup(props) { - const now = new Date(); - let chartInstance: Chart = null; - let data: { - series: { - name: string; - type: 'line' | 'area'; - color?: string; - dashed?: boolean; - hidden?: boolean; - data: { - x: number; - y: number; - }[]; - }[]; - } = null; +const chartEl = ref<HTMLCanvasElement>(null); +const fetching = ref(true); - const chartEl = ref<HTMLCanvasElement>(null); - const fetching = ref(true); +const getDate = (ago: number) => { + const y = now.getFullYear(); + const m = now.getMonth(); + const d = now.getDate(); + const h = now.getHours(); - const getDate = (ago: number) => { - const y = now.getFullYear(); - const m = now.getMonth(); - const d = now.getDate(); - const h = now.getHours(); - - return props.span === 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago); - }; + return props.span === 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago); +}; - const format = (arr) => { - return arr.map((v, i) => ({ - x: getDate(i).getTime(), - y: v - })); - }; +const format = (arr) => { + return arr.map((v, i) => ({ + x: getDate(i).getTime(), + y: v + })); +}; - const tooltipShowing = ref(false); - const tooltipX = ref(0); - const tooltipY = ref(0); - const tooltipTitle = ref(null); - const tooltipSeries = ref(null); - let disposeTooltipComponent; +const tooltipShowing = ref(false); +const tooltipX = ref(0); +const tooltipY = ref(0); +const tooltipTitle = ref(null); +const tooltipSeries = ref(null); +let disposeTooltipComponent; - os.popup(MkChartTooltip, { - showing: tooltipShowing, - x: tooltipX, - y: tooltipY, - title: tooltipTitle, - series: tooltipSeries, - }, {}).then(({ dispose }) => { - disposeTooltipComponent = dispose; - }); +os.popup(MkChartTooltip, { + showing: tooltipShowing, + x: tooltipX, + y: tooltipY, + title: tooltipTitle, + series: tooltipSeries, +}, {}).then(({ dispose }) => { + disposeTooltipComponent = dispose; +}); - function externalTooltipHandler(context) { - if (context.tooltip.opacity === 0) { - tooltipShowing.value = false; - return; - } +function externalTooltipHandler(context) { + if (context.tooltip.opacity === 0) { + tooltipShowing.value = false; + return; + } - tooltipTitle.value = context.tooltip.title[0]; - tooltipSeries.value = context.tooltip.body.map((b, i) => ({ - backgroundColor: context.tooltip.labelColors[i].backgroundColor, - borderColor: context.tooltip.labelColors[i].borderColor, - text: b.lines[0], - })); + tooltipTitle.value = context.tooltip.title[0]; + tooltipSeries.value = context.tooltip.body.map((b, i) => ({ + backgroundColor: context.tooltip.labelColors[i].backgroundColor, + borderColor: context.tooltip.labelColors[i].borderColor, + text: b.lines[0], + })); - const rect = context.chart.canvas.getBoundingClientRect(); + const rect = context.chart.canvas.getBoundingClientRect(); - tooltipShowing.value = true; - tooltipX.value = rect.left + window.pageXOffset + context.tooltip.caretX; - tooltipY.value = rect.top + window.pageYOffset + context.tooltip.caretY; - } + tooltipShowing.value = true; + tooltipX.value = rect.left + window.pageXOffset + context.tooltip.caretX; + tooltipY.value = rect.top + window.pageYOffset + context.tooltip.caretY; +} - const render = () => { - if (chartInstance) { - chartInstance.destroy(); - } +const render = () => { + if (chartInstance) { + chartInstance.destroy(); + } - const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; - const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; + const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; + const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; - // フォントカラー - Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg'); + // フォントカラー + Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg'); - const maxes = data.series.map((x, i) => Math.max(...x.data.map(d => d.y))); + const maxes = chartData.series.map((x, i) => Math.max(...x.data.map(d => d.y))); - chartInstance = new Chart(chartEl.value, { - 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, - borderWidth: props.bar ? 0 : 2, - borderColor: x.color ? x.color : getColor(i), - borderDash: x.dashed ? [5, 5] : [], - borderJoinStyle: 'round', - borderRadius: props.bar ? 3 : undefined, - backgroundColor: props.bar ? (x.color ? x.color : getColor(i)) : alpha(x.color ? x.color : getColor(i), 0.1), - gradient: props.bar ? undefined : { - backgroundColor: { - axis: 'y', - colors: { - 0: alpha(x.color ? x.color : getColor(i), 0), - [maxes[i]]: alpha(x.color ? x.color : getColor(i), 0.2), - }, - }, + chartInstance = new Chart(chartEl.value, { + type: props.bar ? 'bar' : 'line', + data: { + labels: new Array(props.limit).fill(0).map((_, i) => getDate(i).toLocaleString()).slice().reverse(), + datasets: chartData.series.map((x, i) => ({ + parsing: false, + label: x.name, + data: x.data.slice().reverse(), + tension: 0.3, + pointRadius: 0, + borderWidth: props.bar ? 0 : 2, + borderColor: x.color ? x.color : getColor(i), + borderDash: x.dashed ? [5, 5] : [], + borderJoinStyle: 'round', + borderRadius: props.bar ? 3 : undefined, + backgroundColor: props.bar ? (x.color ? x.color : getColor(i)) : alpha(x.color ? x.color : getColor(i), 0.1), + /*gradient: props.bar ? undefined : { + backgroundColor: { + axis: 'y', + colors: { + 0: alpha(x.color ? x.color : getColor(i), 0), + [maxes[i]]: alpha(x.color ? x.color : getColor(i), 0.2), }, - barPercentage: 0.9, - categoryPercentage: 0.9, - fill: x.type === 'area', - clip: 8, - hidden: !!x.hidden, - })), + }, + },*/ + barPercentage: 0.9, + categoryPercentage: 0.9, + fill: x.type === 'area', + clip: 8, + hidden: !!x.hidden, + })), + }, + options: { + aspectRatio: props.aspectRatio || 2.5, + layout: { + padding: { + left: 0, + right: 8, + top: 0, + bottom: 0, }, - options: { - aspectRatio: props.aspectRatio || 2.5, - layout: { - padding: { - left: 0, - right: 8, - top: 0, - bottom: 0, - }, + }, + scales: { + x: { + type: 'time', + stacked: props.stacked, + offset: false, + time: { + stepSize: 1, + unit: props.span === 'day' ? 'month' : 'day', }, - scales: { - x: { - type: 'time', - stacked: props.stacked, - offset: false, - time: { - stepSize: 1, - unit: props.span === 'day' ? 'month' : 'day', - }, - grid: { - color: gridColor, - borderColor: 'rgb(0, 0, 0, 0)', - }, - ticks: { - display: props.detailed, - maxRotation: 0, - autoSkipPadding: 16, - }, - adapters: { - date: { - locale: enUS, - }, - }, - min: getDate(props.limit).getTime(), - }, - y: { - position: 'left', - stacked: props.stacked, - suggestedMax: 50, - grid: { - color: gridColor, - borderColor: 'rgb(0, 0, 0, 0)', - }, - ticks: { - display: props.detailed, - //mirror: true, - }, - }, + grid: { + color: gridColor, + borderColor: 'rgb(0, 0, 0, 0)', }, - interaction: { - intersect: false, - mode: 'index', + ticks: { + display: props.detailed, + maxRotation: 0, + autoSkipPadding: 16, }, - elements: { - point: { - hoverRadius: 5, - hoverBorderWidth: 2, + adapters: { + date: { + locale: enUS, }, }, - animation: false, - plugins: { - legend: { - display: props.detailed, - position: 'bottom', - labels: { - boxWidth: 16, - }, + min: getDate(props.limit).getTime(), + }, + y: { + position: 'left', + stacked: props.stacked, + suggestedMax: 50, + grid: { + color: gridColor, + borderColor: 'rgb(0, 0, 0, 0)', + }, + ticks: { + display: props.detailed, + //mirror: true, + }, + }, + }, + interaction: { + intersect: false, + mode: 'index', + }, + elements: { + point: { + hoverRadius: 5, + hoverBorderWidth: 2, + }, + }, + animation: false, + plugins: { + legend: { + display: props.detailed, + position: 'bottom', + labels: { + boxWidth: 16, + }, + }, + tooltip: { + enabled: false, + mode: 'index', + animation: { + duration: 0, + }, + external: externalTooltipHandler, + }, + zoom: props.detailed ? { + pan: { + enabled: true, + }, + zoom: { + wheel: { + enabled: true, }, - tooltip: { + pinch: { + enabled: true, + }, + drag: { enabled: false, - mode: 'index', - animation: { - duration: 0, - }, - external: externalTooltipHandler, }, - zoom: props.detailed ? { - pan: { - enabled: true, - }, - zoom: { - wheel: { - enabled: true, - }, - pinch: { - enabled: true, - }, - drag: { - enabled: false, - }, - mode: 'x', - }, - limits: { - x: { - min: 'original', - max: 'original', - }, - y: { - min: 'original', - max: 'original', - }, - } - } : undefined, - gradient, + mode: 'x', }, - }, - plugins: [{ - id: 'vLine', - beforeDraw(chart, args, options) { - if (chart.tooltip._active && chart.tooltip._active.length) { - const activePoint = chart.tooltip._active[0]; - const ctx = chart.ctx; - const x = activePoint.element.x; - const topY = chart.scales.y.top; - const bottomY = chart.scales.y.bottom; - - ctx.save(); - ctx.beginPath(); - ctx.moveTo(x, bottomY); - ctx.lineTo(x, topY); - ctx.lineWidth = 1; - ctx.strokeStyle = vLineColor; - ctx.stroke(); - ctx.restore(); - } + limits: { + x: { + min: 'original', + max: 'original', + }, + y: { + min: 'original', + max: 'original', + }, } - }] - }); - }; + } : undefined, + //gradient, + }, + }, + plugins: [{ + id: 'vLine', + beforeDraw(chart, args, options) { + if (chart.tooltip._active && chart.tooltip._active.length) { + const activePoint = chart.tooltip._active[0]; + const ctx = chart.ctx; + const x = activePoint.element.x; + const topY = chart.scales.y.top; + const bottomY = chart.scales.y.bottom; - const exportData = () => { - // TODO - }; + ctx.save(); + ctx.beginPath(); + ctx.moveTo(x, bottomY); + ctx.lineTo(x, topY); + ctx.lineWidth = 1; + ctx.strokeStyle = vLineColor; + ctx.stroke(); + ctx.restore(); + } + } + }] + }); +}; - const fetchFederationChart = async (): Promise<typeof data> => { - const raw = await os.api('charts/federation', { limit: props.limit, span: props.span }); - return { - series: [{ - 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, - }, { - name: 'Pub Active', - type: 'line', - data: format(raw.pubActive), - color: colors.purple, - }, { - name: 'Sub Active', - type: 'line', - data: format(raw.subActive), - color: colors.orange, - }, { - name: 'Pub & Sub', - type: 'line', - data: format(raw.pubsub), - dashed: true, - color: colors.cyan, - }, { - name: 'Pub', - type: 'line', - data: format(raw.pub), - dashed: true, - color: colors.purple, - }, { - name: 'Sub', - type: 'line', - data: format(raw.sub), - dashed: true, - color: colors.orange, - }], - }; - }; +const exportData = () => { + // TODO +}; - 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 fetchFederationChart = async (): Promise<typeof chartData> => { + const raw = await os.api('charts/federation', { limit: props.limit, span: props.span }); + return { + series: [{ + 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, + }, { + name: 'Pub Active', + type: 'line', + data: format(raw.pubActive), + color: colors.purple, + }, { + name: 'Sub Active', + type: 'line', + data: format(raw.subActive), + color: colors.orange, + }, { + name: 'Pub & Sub', + type: 'line', + data: format(raw.pubsub), + dashed: true, + color: colors.cyan, + }, { + name: 'Pub', + type: 'line', + data: format(raw.pub), + dashed: true, + color: colors.purple, + }, { + name: 'Sub', + type: 'line', + data: format(raw.sub), + dashed: true, + color: colors.orange, + }], + }; +}; - const fetchNotesChart = async (type: string): Promise<typeof data> => { - const raw = await os.api('charts/notes', { limit: props.limit, span: props.span }); - return { - series: [{ - name: 'All', - type: 'line', - data: format(type == 'combined' - ? 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', - data: format(type == 'combined' - ? sum(raw.local.diffs.renote, raw.remote.diffs.renote) - : raw[type].diffs.renote - ), - color: colors.green, - }, { - name: 'Replies', - type: 'area', - data: format(type == 'combined' - ? sum(raw.local.diffs.reply, raw.remote.diffs.reply) - : raw[type].diffs.reply - ), - color: colors.yellow, - }, { - name: 'Normal', - type: 'area', - data: format(type == 'combined' - ? 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, - }], - }; - }; +const fetchApRequestChart = async (): Promise<typeof chartData> => { + 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 fetchNotesTotalChart = async (): Promise<typeof data> => { - const raw = await os.api('charts/notes', { limit: props.limit, span: props.span }); - return { - series: [{ - name: 'Combined', - type: 'line', - data: format(sum(raw.local.total, raw.remote.total)), - }, { - name: 'Local', - type: 'area', - data: format(raw.local.total), - }, { - name: 'Remote', - type: 'area', - data: format(raw.remote.total), - }], - }; - }; +const fetchNotesChart = async (type: string): Promise<typeof chartData> => { + const raw = await os.api('charts/notes', { limit: props.limit, span: props.span }); + return { + series: [{ + name: 'All', + type: 'line', + data: format(type === 'combined' + ? 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', + data: format(type === 'combined' + ? sum(raw.local.diffs.renote, raw.remote.diffs.renote) + : raw[type].diffs.renote + ), + color: colors.green, + }, { + name: 'Replies', + type: 'area', + data: format(type === 'combined' + ? sum(raw.local.diffs.reply, raw.remote.diffs.reply) + : raw[type].diffs.reply + ), + color: colors.yellow, + }, { + name: 'Normal', + type: 'area', + data: format(type === 'combined' + ? 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, + }], + }; +}; - const fetchUsersChart = async (total: boolean): Promise<typeof data> => { - const raw = await os.api('charts/users', { limit: props.limit, span: props.span }); - return { - series: [{ - name: 'Combined', - type: 'line', - data: format(total - ? sum(raw.local.total, raw.remote.total) - : sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec)) - ), - }, { - name: 'Local', - type: 'area', - data: format(total - ? raw.local.total - : sum(raw.local.inc, negate(raw.local.dec)) - ), - }, { - name: 'Remote', - type: 'area', - data: format(total - ? raw.remote.total - : sum(raw.remote.inc, negate(raw.remote.dec)) - ), - }], - }; - }; +const fetchNotesTotalChart = async (): Promise<typeof chartData> => { + const raw = await os.api('charts/notes', { limit: props.limit, span: props.span }); + return { + series: [{ + name: 'Combined', + type: 'line', + data: format(sum(raw.local.total, raw.remote.total)), + }, { + name: 'Local', + type: 'area', + data: format(raw.local.total), + }, { + name: 'Remote', + type: 'area', + data: format(raw.remote.total), + }], + }; +}; - const fetchActiveUsersChart = async (): Promise<typeof data> => { - const raw = await os.api('charts/active-users', { limit: props.limit, span: props.span }); - return { - series: [{ - name: 'Read & Write', - type: 'area', - data: format(raw.readWrite), - color: colors.orange, - }, { - name: 'Write', - type: 'area', - data: format(raw.write), - color: colors.lime, - }, { - name: 'Read', - type: 'area', - data: format(raw.read), - color: colors.blue, - }, { - name: '< Week', - type: 'area', - 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, - }], - }; - }; +const fetchUsersChart = async (total: boolean): Promise<typeof chartData> => { + const raw = await os.api('charts/users', { limit: props.limit, span: props.span }); + return { + series: [{ + name: 'Combined', + type: 'line', + data: format(total + ? sum(raw.local.total, raw.remote.total) + : sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec)) + ), + }, { + name: 'Local', + type: 'area', + data: format(total + ? raw.local.total + : sum(raw.local.inc, negate(raw.local.dec)) + ), + }, { + name: 'Remote', + type: 'area', + data: format(total + ? raw.remote.total + : sum(raw.remote.inc, negate(raw.remote.dec)) + ), + }], + }; +}; - const fetchDriveChart = async (): Promise<typeof data> => { - const raw = await os.api('charts/drive', { limit: props.limit, span: props.span }); - return { - bytes: true, - series: [{ - name: 'All', - type: 'line', - dashed: true, - data: format( - sum( - raw.local.incSize, - negate(raw.local.decSize), - raw.remote.incSize, - negate(raw.remote.decSize) - ) - ), - }, { - name: 'Local +', - type: 'area', - data: format(raw.local.incSize), - }, { - name: 'Local -', - type: 'area', - data: format(negate(raw.local.decSize)), - }, { - name: 'Remote +', - type: 'area', - data: format(raw.remote.incSize), - }, { - name: 'Remote -', - type: 'area', - data: format(negate(raw.remote.decSize)), - }], - }; - }; +const fetchActiveUsersChart = async (): Promise<typeof chartData> => { + const raw = await os.api('charts/active-users', { limit: props.limit, span: props.span }); + return { + series: [{ + name: 'Read & Write', + type: 'area', + data: format(raw.readWrite), + color: colors.orange, + }, { + name: 'Write', + type: 'area', + data: format(raw.write), + color: colors.lime, + }, { + name: 'Read', + type: 'area', + data: format(raw.read), + color: colors.blue, + }, { + name: '< Week', + type: 'area', + 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, + }], + }; +}; - const fetchDriveFilesChart = async (): Promise<typeof data> => { - const raw = await os.api('charts/drive', { limit: props.limit, span: props.span }); - return { - series: [{ - name: 'All', - type: 'line', - dashed: true, - data: format( - sum( - raw.local.incCount, - negate(raw.local.decCount), - raw.remote.incCount, - negate(raw.remote.decCount) - ) - ), - }, { - name: 'Local +', - type: 'area', - data: format(raw.local.incCount), - }, { - name: 'Local -', - type: 'area', - data: format(negate(raw.local.decCount)), - }, { - name: 'Remote +', - type: 'area', - data: format(raw.remote.incCount), - }, { - name: 'Remote -', - type: 'area', - data: format(negate(raw.remote.decCount)), - }], - }; - }; +const fetchDriveChart = async (): Promise<typeof chartData> => { + const raw = await os.api('charts/drive', { limit: props.limit, span: props.span }); + return { + bytes: true, + series: [{ + name: 'All', + type: 'line', + dashed: true, + data: format( + sum( + raw.local.incSize, + negate(raw.local.decSize), + raw.remote.incSize, + negate(raw.remote.decSize) + ) + ), + }, { + name: 'Local +', + type: 'area', + data: format(raw.local.incSize), + }, { + name: 'Local -', + type: 'area', + data: format(negate(raw.local.decSize)), + }, { + name: 'Remote +', + type: 'area', + data: format(raw.remote.incSize), + }, { + name: 'Remote -', + type: 'area', + data: format(negate(raw.remote.decSize)), + }], + }; +}; - const fetchInstanceRequestsChart = async (): Promise<typeof data> => { - const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); - return { - series: [{ - name: 'In', - type: 'area', - color: '#008FFB', - data: format(raw.requests.received) - }, { - name: 'Out (succ)', - type: 'area', - color: '#00E396', - data: format(raw.requests.succeeded) - }, { - name: 'Out (fail)', - type: 'area', - color: '#FEB019', - data: format(raw.requests.failed) - }] - }; - }; +const fetchDriveFilesChart = async (): Promise<typeof chartData> => { + const raw = await os.api('charts/drive', { limit: props.limit, span: props.span }); + return { + series: [{ + name: 'All', + type: 'line', + dashed: true, + data: format( + sum( + raw.local.incCount, + negate(raw.local.decCount), + raw.remote.incCount, + negate(raw.remote.decCount) + ) + ), + }, { + name: 'Local +', + type: 'area', + data: format(raw.local.incCount), + }, { + name: 'Local -', + type: 'area', + data: format(negate(raw.local.decCount)), + }, { + name: 'Remote +', + type: 'area', + data: format(raw.remote.incCount), + }, { + name: 'Remote -', + type: 'area', + data: format(negate(raw.remote.decCount)), + }], + }; +}; - const fetchInstanceUsersChart = async (total: boolean): Promise<typeof data> => { - const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); - return { - series: [{ - name: 'Users', - type: 'area', - color: '#008FFB', - data: format(total - ? raw.users.total - : sum(raw.users.inc, negate(raw.users.dec)) - ) - }] - }; - }; +const fetchInstanceRequestsChart = async (): Promise<typeof chartData> => { + const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); + return { + series: [{ + name: 'In', + type: 'area', + color: '#008FFB', + data: format(raw.requests.received) + }, { + name: 'Out (succ)', + type: 'area', + color: '#00E396', + data: format(raw.requests.succeeded) + }, { + name: 'Out (fail)', + type: 'area', + color: '#FEB019', + data: format(raw.requests.failed) + }] + }; +}; - const fetchInstanceNotesChart = async (total: boolean): Promise<typeof data> => { - const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); - return { - series: [{ - name: 'Notes', - type: 'area', - color: '#008FFB', - data: format(total - ? raw.notes.total - : sum(raw.notes.inc, negate(raw.notes.dec)) - ) - }] - }; - }; +const fetchInstanceUsersChart = async (total: boolean): Promise<typeof chartData> => { + const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); + return { + series: [{ + name: 'Users', + type: 'area', + color: '#008FFB', + data: format(total + ? raw.users.total + : sum(raw.users.inc, negate(raw.users.dec)) + ) + }] + }; +}; - const fetchInstanceFfChart = async (total: boolean): Promise<typeof data> => { - const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); - return { - series: [{ - name: 'Following', - type: 'area', - color: '#008FFB', - data: format(total - ? raw.following.total - : sum(raw.following.inc, negate(raw.following.dec)) - ) - }, { - name: 'Followers', - type: 'area', - color: '#00E396', - data: format(total - ? raw.followers.total - : sum(raw.followers.inc, negate(raw.followers.dec)) - ) - }] - }; - }; +const fetchInstanceNotesChart = async (total: boolean): Promise<typeof chartData> => { + const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); + return { + series: [{ + name: 'Notes', + type: 'area', + color: '#008FFB', + data: format(total + ? raw.notes.total + : sum(raw.notes.inc, negate(raw.notes.dec)) + ) + }] + }; +}; - const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof data> => { - const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); - return { - bytes: true, - series: [{ - name: 'Drive usage', - type: 'area', - color: '#008FFB', - data: format(total - ? raw.drive.totalUsage - : sum(raw.drive.incUsage, negate(raw.drive.decUsage)) - ) - }] - }; - }; +const fetchInstanceFfChart = async (total: boolean): Promise<typeof chartData> => { + const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); + return { + series: [{ + name: 'Following', + type: 'area', + color: '#008FFB', + data: format(total + ? raw.following.total + : sum(raw.following.inc, negate(raw.following.dec)) + ) + }, { + name: 'Followers', + type: 'area', + color: '#00E396', + data: format(total + ? raw.followers.total + : sum(raw.followers.inc, negate(raw.followers.dec)) + ) + }] + }; +}; - const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof data> => { - const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); - return { - series: [{ - name: 'Drive files', - type: 'area', - color: '#008FFB', - data: format(total - ? raw.drive.totalFiles - : sum(raw.drive.incFiles, negate(raw.drive.decFiles)) - ) - }] - }; - }; +const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof chartData> => { + const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); + return { + bytes: true, + series: [{ + name: 'Drive usage', + type: 'area', + color: '#008FFB', + data: format(total + ? raw.drive.totalUsage + : sum(raw.drive.incUsage, negate(raw.drive.decUsage)) + ) + }] + }; +}; - const fetchPerUserNotesChart = async (): Promise<typeof data> => { - const raw = await os.api('charts/user/notes', { userId: props.args.user.id, limit: props.limit, span: props.span }); - return { - series: [...(props.args.withoutAll ? [] : [{ - name: 'All', - type: 'line', - data: format(sum(raw.inc, negate(raw.dec))), - color: '#888888', - }]), { - name: 'With file', - type: 'area', - data: format(raw.diffs.withFile), - color: colors.purple, - }, { - name: 'Renotes', - type: 'area', - data: format(raw.diffs.renote), - color: colors.green, - }, { - name: 'Replies', - type: 'area', - data: format(raw.diffs.reply), - color: colors.yellow, - }, { - name: 'Normal', - type: 'area', - data: format(raw.diffs.normal), - color: colors.blue, - }], - }; - }; +const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof chartData> => { + const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); + return { + series: [{ + name: 'Drive files', + type: 'area', + color: '#008FFB', + data: format(total + ? raw.drive.totalFiles + : sum(raw.drive.incFiles, negate(raw.drive.decFiles)) + ) + }] + }; +}; - const fetchPerUserFollowingChart = async (): Promise<typeof data> => { - const raw = await os.api('charts/user/following', { userId: props.args.user.id, limit: props.limit, span: props.span }); - return { - series: [{ - name: 'Local', - type: 'area', - data: format(raw.local.followings.total), - }, { - name: 'Remote', - type: 'area', - data: format(raw.remote.followings.total), - }], - }; - }; +const fetchPerUserNotesChart = async (): Promise<typeof chartData> => { + const raw = await os.api('charts/user/notes', { userId: props.args.user.id, limit: props.limit, span: props.span }); + return { + series: [...(props.args.withoutAll ? [] : [{ + name: 'All', + type: 'line', + data: format(sum(raw.inc, negate(raw.dec))), + color: '#888888', + }]), { + name: 'With file', + type: 'area', + data: format(raw.diffs.withFile), + color: colors.purple, + }, { + name: 'Renotes', + type: 'area', + data: format(raw.diffs.renote), + color: colors.green, + }, { + name: 'Replies', + type: 'area', + data: format(raw.diffs.reply), + color: colors.yellow, + }, { + name: 'Normal', + type: 'area', + data: format(raw.diffs.normal), + color: colors.blue, + }], + }; +}; - const fetchPerUserFollowersChart = async (): Promise<typeof data> => { - const raw = await os.api('charts/user/following', { userId: props.args.user.id, limit: props.limit, span: props.span }); - return { - series: [{ - name: 'Local', - type: 'area', - data: format(raw.local.followers.total), - }, { - name: 'Remote', - type: 'area', - data: format(raw.remote.followers.total), - }], - }; - }; +const fetchPerUserFollowingChart = async (): Promise<typeof chartData> => { + const raw = await os.api('charts/user/following', { userId: props.args.user.id, limit: props.limit, span: props.span }); + return { + series: [{ + name: 'Local', + type: 'area', + data: format(raw.local.followings.total), + }, { + name: 'Remote', + type: 'area', + data: format(raw.remote.followings.total), + }], + }; +}; - 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 fetchPerUserFollowersChart = async (): Promise<typeof chartData> => { + const raw = await os.api('charts/user/following', { userId: props.args.user.id, limit: props.limit, span: props.span }); + return { + series: [{ + name: 'Local', + type: 'area', + data: format(raw.local.followers.total), + }, { + name: 'Remote', + type: 'area', + data: format(raw.remote.followers.total), + }], + }; +}; - const fetchAndRender = async () => { - const fetchData = () => { - switch (props.src) { - 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(); - case 'notes': return fetchNotesChart('combined'); - case 'local-notes': return fetchNotesChart('local'); - case 'remote-notes': return fetchNotesChart('remote'); - case 'notes-total': return fetchNotesTotalChart(); - case 'drive': return fetchDriveChart(); - case 'drive-files': return fetchDriveFilesChart(); - - case 'instance-requests': return fetchInstanceRequestsChart(); - case 'instance-users': return fetchInstanceUsersChart(false); - case 'instance-users-total': return fetchInstanceUsersChart(true); - case 'instance-notes': return fetchInstanceNotesChart(false); - case 'instance-notes-total': return fetchInstanceNotesChart(true); - case 'instance-ff': return fetchInstanceFfChart(false); - case 'instance-ff-total': return fetchInstanceFfChart(true); - case 'instance-drive-usage': return fetchInstanceDriveUsageChart(false); - case 'instance-drive-usage-total': return fetchInstanceDriveUsageChart(true); - case 'instance-drive-files': return fetchInstanceDriveFilesChart(false); - case 'instance-drive-files-total': return fetchInstanceDriveFilesChart(true); +const fetchPerUserDriveChart = async (): Promise<typeof chartData> => { + 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), + }], + }; +}; - case 'per-user-notes': return fetchPerUserNotesChart(); - case 'per-user-following': return fetchPerUserFollowingChart(); - case 'per-user-followers': return fetchPerUserFollowersChart(); - case 'per-user-drive': return fetchPerUserDriveChart(); - } - }; - fetching.value = true; - data = await fetchData(); - fetching.value = false; - render(); - }; +const fetchAndRender = async () => { + const fetchData = () => { + switch (props.src) { + 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(); + case 'notes': return fetchNotesChart('combined'); + case 'local-notes': return fetchNotesChart('local'); + case 'remote-notes': return fetchNotesChart('remote'); + case 'notes-total': return fetchNotesTotalChart(); + case 'drive': return fetchDriveChart(); + case 'drive-files': return fetchDriveFilesChart(); + case 'instance-requests': return fetchInstanceRequestsChart(); + case 'instance-users': return fetchInstanceUsersChart(false); + case 'instance-users-total': return fetchInstanceUsersChart(true); + case 'instance-notes': return fetchInstanceNotesChart(false); + case 'instance-notes-total': return fetchInstanceNotesChart(true); + case 'instance-ff': return fetchInstanceFfChart(false); + case 'instance-ff-total': return fetchInstanceFfChart(true); + case 'instance-drive-usage': return fetchInstanceDriveUsageChart(false); + case 'instance-drive-usage-total': return fetchInstanceDriveUsageChart(true); + case 'instance-drive-files': return fetchInstanceDriveFilesChart(false); + case 'instance-drive-files-total': return fetchInstanceDriveFilesChart(true); - watch(() => [props.src, props.span], fetchAndRender); + case 'per-user-notes': return fetchPerUserNotesChart(); + case 'per-user-following': return fetchPerUserFollowingChart(); + case 'per-user-followers': return fetchPerUserFollowersChart(); + case 'per-user-drive': return fetchPerUserDriveChart(); + } + }; + fetching.value = true; + chartData = await fetchData(); + fetching.value = false; + render(); +}; - onMounted(() => { - fetchAndRender(); - }); +watch(() => [props.src, props.span], fetchAndRender); - onUnmounted(() => { - if (disposeTooltipComponent) disposeTooltipComponent(); - }); +onMounted(() => { + fetchAndRender(); +}); - return { - chartEl, - fetching, - }; - }, +onUnmounted(() => { + if (disposeTooltipComponent) disposeTooltipComponent(); }); +/* eslint-enable id-denylist */ </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/cropper-dialog.vue b/packages/client/src/components/cropper-dialog.vue new file mode 100644 index 0000000000..a8bde6ea05 --- /dev/null +++ b/packages/client/src/components/cropper-dialog.vue @@ -0,0 +1,175 @@ +<template> +<XModalWindow + ref="dialogEl" + :width="800" + :height="500" + :scroll="false" + :with-ok-button="true" + @close="cancel()" + @ok="ok()" + @closed="$emit('closed')" +> + <template #header>{{ $ts.cropImage }}</template> + <template #default="{ width, height }"> + <div class="mk-cropper-dialog" :style="`--vw: ${width}px; --vh: ${height}px;`"> + <Transition name="fade"> + <div v-if="loading" class="loading"> + <MkLoading/> + </div> + </Transition> + <div class="container"> + <img ref="imgEl" :src="imgUrl" style="display: none;" @load="onImageLoad"> + </div> + </div> + </template> +</XModalWindow> +</template> + +<script lang="ts" setup> +import { nextTick, onMounted } from 'vue'; +import * as misskey from 'misskey-js'; +import Cropper from 'cropperjs'; +import tinycolor from 'tinycolor2'; +import XModalWindow from '@/components/ui/modal-window.vue'; +import * as os from '@/os'; +import { $i } from '@/account'; +import { defaultStore } from '@/store'; +import { apiUrl, url } from '@/config'; +import { query } from '@/scripts/url'; + +const emit = defineEmits<{ + (ev: 'ok', cropped: misskey.entities.DriveFile): void; + (ev: 'cancel'): void; + (ev: 'closed'): void; +}>(); + +const props = defineProps<{ + file: misskey.entities.DriveFile; + aspectRatio: number; +}>(); + +const imgUrl = `${url}/proxy/image.webp?${query({ + url: props.file.url, +})}`; +let dialogEl = $ref<InstanceType<typeof XModalWindow>>(); +let imgEl = $ref<HTMLImageElement>(); +let cropper: Cropper | null = null; +let loading = $ref(true); + +const ok = async () => { + const promise = new Promise<misskey.entities.DriveFile>(async (res) => { + const croppedCanvas = await cropper?.getCropperSelection()?.$toCanvas(); + croppedCanvas.toBlob(blob => { + const formData = new FormData(); + formData.append('file', blob); + formData.append('i', $i.token); + if (defaultStore.state.uploadFolder) { + formData.append('folderId', defaultStore.state.uploadFolder); + } + + fetch(apiUrl + '/drive/files/create', { + method: 'POST', + body: formData, + }) + .then(response => response.json()) + .then(f => { + res(f); + }); + }); + }); + + os.promiseDialog(promise); + + const f = await promise; + + emit('ok', f); + dialogEl.close(); +}; + +const cancel = () => { + emit('cancel'); + dialogEl.close(); +}; + +const onImageLoad = () => { + loading = false; + + if (cropper) { + cropper.getCropperImage()!.$center('contain'); + cropper.getCropperSelection()!.$center(); + } +}; + +onMounted(() => { + cropper = new Cropper(imgEl, { + }); + + const computedStyle = getComputedStyle(document.documentElement); + + const selection = cropper.getCropperSelection()!; + selection.themeColor = tinycolor(computedStyle.getPropertyValue('--accent')).toHexString(); + selection.aspectRatio = props.aspectRatio; + selection.initialAspectRatio = props.aspectRatio; + selection.outlined = true; + + window.setTimeout(() => { + cropper.getCropperImage()!.$center('contain'); + selection.$center(); + }, 100); + + // モーダルオープンアニメーションが終わったあとで再度調整 + window.setTimeout(() => { + cropper.getCropperImage()!.$center('contain'); + selection.$center(); + }, 500); +}); +</script> + +<style lang="scss" scoped> +.fade-enter-active, +.fade-leave-active { + transition: opacity 0.5s ease 0.5s; +} +.fade-enter-from, +.fade-leave-to { + opacity: 0; +} + +.mk-cropper-dialog { + display: flex; + flex-direction: column; + width: var(--vw); + height: var(--vh); + position: relative; + + > .loading { + position: absolute; + z-index: 10; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + -webkit-backdrop-filter: var(--blur, blur(10px)); + backdrop-filter: var(--blur, blur(10px)); + background: rgba(0, 0, 0, 0.5); + } + + > .container { + flex: 1; + width: 100%; + height: 100%; + + > ::v-deep(cropper-canvas) { + width: 100%; + height: 100%; + + > cropper-selection > cropper-handle[action="move"] { + background: transparent; + } + } + } +} +</style> diff --git a/packages/client/src/components/cw-button.vue b/packages/client/src/components/cw-button.vue index e7c9aabe4e..dd906f9bf3 100644 --- a/packages/client/src/components/cw-button.vue +++ b/packages/client/src/components/cw-button.vue @@ -18,7 +18,7 @@ const props = defineProps<{ }>(); const emit = defineEmits<{ - (e: 'update:modelValue', v: boolean): void; + (ev: 'update:modelValue', v: boolean): void; }>(); const label = computed(() => { diff --git a/packages/client/src/components/dialog.vue b/packages/client/src/components/dialog.vue index 3e106a4f0c..b090f3cb4e 100644 --- a/packages/client/src/components/dialog.vue +++ b/packages/client/src/components/dialog.vue @@ -90,8 +90,8 @@ const props = withDefaults(defineProps<{ }); const emit = defineEmits<{ - (e: 'done', v: { canceled: boolean; result: any }): void; - (e: 'closed'): void; + (ev: 'done', v: { canceled: boolean; result: any }): void; + (ev: 'closed'): void; }>(); const modal = ref<InstanceType<typeof MkModal>>(); @@ -122,14 +122,14 @@ function onBgClick() { if (props.cancelableByBgClick) cancel(); } */ -function onKeydown(e: KeyboardEvent) { - if (e.key === 'Escape') cancel(); +function onKeydown(evt: KeyboardEvent) { + if (evt.key === 'Escape') cancel(); } -function onInputKeydown(e: KeyboardEvent) { - if (e.key === 'Enter') { - e.preventDefault(); - e.stopPropagation(); +function onInputKeydown(evt: KeyboardEvent) { + if (evt.key === 'Enter') { + evt.preventDefault(); + evt.stopPropagation(); ok(); } } diff --git a/packages/client/src/components/drive-file-thumbnail.vue b/packages/client/src/components/drive-file-thumbnail.vue index 81b80e7e8e..dd24440e82 100644 --- a/packages/client/src/components/drive-file-thumbnail.vue +++ b/packages/client/src/components/drive-file-thumbnail.vue @@ -42,7 +42,7 @@ const is = computed(() => { "application/x-tar", "application/gzip", "application/x-7z-compressed" - ].some(e => e === props.file.type)) return 'archive'; + ].some(archiveType => archiveType === props.file.type)) return 'archive'; return 'unknown'; }); diff --git a/packages/client/src/components/drive-select-dialog.vue b/packages/client/src/components/drive-select-dialog.vue index f6c59457d1..03974559d2 100644 --- a/packages/client/src/components/drive-select-dialog.vue +++ b/packages/client/src/components/drive-select-dialog.vue @@ -33,8 +33,8 @@ withDefaults(defineProps<{ }); const emit = defineEmits<{ - (e: 'done', r?: Misskey.entities.DriveFile[]): void; - (e: 'closed'): void; + (ev: 'done', r?: Misskey.entities.DriveFile[]): void; + (ev: 'closed'): void; }>(); const dialog = ref<InstanceType<typeof XModalWindow>>(); diff --git a/packages/client/src/components/drive-window.vue b/packages/client/src/components/drive-window.vue index d08c5fb674..5bbfca83c9 100644 --- a/packages/client/src/components/drive-window.vue +++ b/packages/client/src/components/drive-window.vue @@ -24,6 +24,6 @@ defineProps<{ }>(); const emit = defineEmits<{ - (e: 'closed'): void; + (ev: 'closed'): void; }>(); </script> diff --git a/packages/client/src/components/drive.file.vue b/packages/client/src/components/drive.file.vue index 262eae0de1..aaf7ca3ca3 100644 --- a/packages/client/src/components/drive.file.vue +++ b/packages/client/src/components/drive.file.vue @@ -31,7 +31,7 @@ </template> <script lang="ts" setup> -import { computed, ref } from 'vue'; +import { computed, defineAsyncComponent, ref } from 'vue'; import * as Misskey from 'misskey-js'; import copyToClipboard from '@/scripts/copy-to-clipboard'; import MkDriveFileThumbnail from './drive-file-thumbnail.vue'; @@ -50,9 +50,9 @@ const props = withDefaults(defineProps<{ }); const emit = defineEmits<{ - (e: 'chosen', r: Misskey.entities.DriveFile): void; - (e: 'dragstart'): void; - (e: 'dragend'): void; + (ev: 'chosen', r: Misskey.entities.DriveFile): void; + (ev: 'dragstart'): void; + (ev: 'dragend'): void; }>(); const isDragging = ref(false); @@ -99,14 +99,14 @@ function onClick(ev: MouseEvent) { } } -function onContextmenu(e: MouseEvent) { - os.contextMenu(getMenu(), e); +function onContextmenu(ev: MouseEvent) { + os.contextMenu(getMenu(), ev); } -function onDragstart(e: DragEvent) { - if (e.dataTransfer) { - e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FILE_, JSON.stringify(props.file)); +function onDragstart(ev: DragEvent) { + if (ev.dataTransfer) { + ev.dataTransfer.effectAllowed = 'move'; + ev.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FILE_, JSON.stringify(props.file)); } isDragging.value = true; @@ -133,11 +133,11 @@ function rename() { } function describe() { - os.popup(import('@/components/media-caption.vue'), { + os.popup(defineAsyncComponent(() => import('@/components/media-caption.vue')), { title: i18n.ts.describeFile, input: { placeholder: i18n.ts.inputNewDescription, - default: props.file.comment !== null ? props.file.comment : '', + default: props.file.comment != null ? props.file.comment : '', }, image: props.file }, { @@ -146,7 +146,7 @@ function describe() { let comment = result.result; os.api('drive/files/update', { fileId: props.file.id, - comment: comment.length == 0 ? null : comment + comment: comment.length === 0 ? null : comment }); } }, 'closed'); diff --git a/packages/client/src/components/drive.folder.vue b/packages/client/src/components/drive.folder.vue index 57621bf097..3ccb5d6219 100644 --- a/packages/client/src/components/drive.folder.vue +++ b/packages/client/src/components/drive.folder.vue @@ -27,7 +27,7 @@ </template> <script lang="ts" setup> -import { computed, ref } from 'vue'; +import { computed, defineAsyncComponent, ref } from 'vue'; import * as Misskey from 'misskey-js'; import * as os from '@/os'; import { i18n } from '@/i18n'; @@ -71,7 +71,7 @@ function onMouseover() { } function onMouseout() { - hover.value = false + hover.value = false; } function onDragover(ev: DragEvent) { @@ -84,12 +84,12 @@ function onDragover(ev: DragEvent) { return; } - const isFile = ev.dataTransfer.items[0].kind == 'file'; - const isDriveFile = ev.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_; - const isDriveFolder = ev.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_; + const isFile = ev.dataTransfer.items[0].kind === 'file'; + const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_; + const isDriveFolder = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FOLDER_; if (isFile || isDriveFile || isDriveFolder) { - ev.dataTransfer.dropEffect = ev.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; + ev.dataTransfer.dropEffect = ev.dataTransfer.effectAllowed === 'all' ? 'copy' : 'move'; } else { ev.dataTransfer.dropEffect = 'none'; } @@ -118,7 +118,7 @@ function onDrop(ev: DragEvent) { //#region ドライブのファイル const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); - if (driveFile != null && driveFile != '') { + if (driveFile != null && driveFile !== '') { const file = JSON.parse(driveFile); emit('removeFile', file.id); os.api('drive/files/update', { @@ -130,11 +130,11 @@ function onDrop(ev: DragEvent) { //#region ドライブのフォルダ const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_); - if (driveFolder != null && driveFolder != '') { + if (driveFolder != null && driveFolder !== '') { const folder = JSON.parse(driveFolder); // 移動先が自分自身ならreject - if (folder.id == props.folder.id) return; + if (folder.id === props.folder.id) return; emit('removeFolder', folder.id); os.api('drive/folders/update', { @@ -204,7 +204,7 @@ function deleteFolder() { defaultStore.set('uploadFolder', null); } }).catch(err => { - switch(err.id) { + switch (err.id) { case 'b0fc8a17-963c-405d-bfbc-859a487295e1': os.alert({ type: 'error', @@ -230,7 +230,7 @@ function onContextmenu(ev: MouseEvent) { text: i18n.ts.openInWindow, icon: 'fas fa-window-restore', action: () => { - os.popup(import('./drive-window.vue'), { + os.popup(defineAsyncComponent(() => import('./drive-window.vue')), { initialFolder: props.folder }, { }, 'closed'); diff --git a/packages/client/src/components/drive.nav-folder.vue b/packages/client/src/components/drive.nav-folder.vue index 67223267c1..5482703317 100644 --- a/packages/client/src/components/drive.nav-folder.vue +++ b/packages/client/src/components/drive.nav-folder.vue @@ -24,10 +24,10 @@ const props = defineProps<{ }>(); const emit = defineEmits<{ - (e: 'move', v?: Misskey.entities.DriveFolder): void; - (e: 'upload', file: File, folder?: Misskey.entities.DriveFolder | null): void; - (e: 'removeFile', v: Misskey.entities.DriveFile['id']): void; - (e: 'removeFolder', v: Misskey.entities.DriveFolder['id']): void; + (ev: 'move', v?: Misskey.entities.DriveFolder): void; + (ev: 'upload', file: File, folder?: Misskey.entities.DriveFolder | null): void; + (ev: 'removeFile', v: Misskey.entities.DriveFile['id']): void; + (ev: 'removeFolder', v: Misskey.entities.DriveFolder['id']): void; }>(); const hover = ref(false); @@ -45,22 +45,22 @@ function onMouseout() { hover.value = false; } -function onDragover(e: DragEvent) { - if (!e.dataTransfer) return; +function onDragover(ev: DragEvent) { + if (!ev.dataTransfer) return; // このフォルダがルートかつカレントディレクトリならドロップ禁止 if (props.folder == null && props.parentFolder == null) { - e.dataTransfer.dropEffect = 'none'; + ev.dataTransfer.dropEffect = 'none'; } - const isFile = e.dataTransfer.items[0].kind == 'file'; - const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_; - const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_; + const isFile = ev.dataTransfer.items[0].kind === 'file'; + const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_; + const isDriveFolder = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FOLDER_; if (isFile || isDriveFile || isDriveFolder) { - e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; + ev.dataTransfer.dropEffect = ev.dataTransfer.effectAllowed === 'all' ? 'copy' : 'move'; } else { - e.dataTransfer.dropEffect = 'none'; + ev.dataTransfer.dropEffect = 'none'; } return false; @@ -74,22 +74,22 @@ function onDragleave() { if (props.folder || props.parentFolder) draghover.value = false; } -function onDrop(e: DragEvent) { +function onDrop(ev: DragEvent) { draghover.value = false; - if (!e.dataTransfer) return; + if (!ev.dataTransfer) return; // ファイルだったら - if (e.dataTransfer.files.length > 0) { - for (const file of Array.from(e.dataTransfer.files)) { + if (ev.dataTransfer.files.length > 0) { + for (const file of Array.from(ev.dataTransfer.files)) { emit('upload', file, props.folder); } return; } //#region ドライブのファイル - const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); - if (driveFile != null && driveFile != '') { + const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); + if (driveFile != null && driveFile !== '') { const file = JSON.parse(driveFile); emit('removeFile', file.id); os.api('drive/files/update', { @@ -100,11 +100,11 @@ function onDrop(e: DragEvent) { //#endregion //#region ドライブのフォルダ - const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_); - if (driveFolder != null && driveFolder != '') { + const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_); + if (driveFolder != null && driveFolder !== '') { const folder = JSON.parse(driveFolder); // 移動先が自分自身ならreject - if (props.folder && folder.id == props.folder.id) return; + if (props.folder && folder.id === props.folder.id) return; emit('removeFolder', folder.id); os.api('drive/folders/update', { folderId: folder.id, diff --git a/packages/client/src/components/drive.vue b/packages/client/src/components/drive.vue index e044c67523..6c2c8acad0 100644 --- a/packages/client/src/components/drive.vue +++ b/packages/client/src/components/drive.vue @@ -97,6 +97,7 @@ import * as os from '@/os'; import { stream } from '@/stream'; import { defaultStore } from '@/store'; import { i18n } from '@/i18n'; +import { uploadFile, uploads } from '@/scripts/upload'; const props = withDefaults(defineProps<{ initialFolder?: Misskey.entities.DriveFolder; @@ -109,11 +110,11 @@ const props = withDefaults(defineProps<{ }); const emit = defineEmits<{ - (e: 'selected', v: Misskey.entities.DriveFile | Misskey.entities.DriveFolder): void; - (e: 'change-selection', v: Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]): void; - (e: 'move-root'): void; - (e: 'cd', v: Misskey.entities.DriveFolder | null): void; - (e: 'open-folder', v: Misskey.entities.DriveFolder): void; + (ev: 'selected', v: Misskey.entities.DriveFile | Misskey.entities.DriveFolder): void; + (ev: 'change-selection', v: Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]): void; + (ev: 'move-root'): void; + (ev: 'cd', v: Misskey.entities.DriveFolder | null): void; + (ev: 'open-folder', v: Misskey.entities.DriveFolder): void; }>(); const loadMoreFiles = ref<InstanceType<typeof MkButton>>(); @@ -127,8 +128,9 @@ const moreFolders = ref(false); const hierarchyFolders = ref<Misskey.entities.DriveFolder[]>([]); const selectedFiles = ref<Misskey.entities.DriveFile[]>([]); const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]); -const uploadings = os.uploads; +const uploadings = uploads; const connection = stream.useChannel('drive'); +const keepOriginal = ref<boolean>(defaultStore.state.keepOriginalUploading); // 外部渡しが多いので$refは使わないほうがよい // ドロップされようとしているか const draghover = ref(false); @@ -141,7 +143,7 @@ const fetching = ref(true); const ilFilesObserver = new IntersectionObserver( (entries) => entries.some((entry) => entry.isIntersecting) && !fetching.value && moreFiles.value && fetchMoreFiles() -) +); watch(folder, () => emit('cd', folder.value)); @@ -151,7 +153,7 @@ function onStreamDriveFileCreated(file: Misskey.entities.DriveFile) { function onStreamDriveFileUpdated(file: Misskey.entities.DriveFile) { const current = folder.value ? folder.value.id : null; - if (current != file.folderId) { + if (current !== file.folderId) { removeFile(file); } else { addFile(file, true); @@ -168,7 +170,7 @@ function onStreamDriveFolderCreated(createdFolder: Misskey.entities.DriveFolder) function onStreamDriveFolderUpdated(updatedFolder: Misskey.entities.DriveFolder) { const current = folder.value ? folder.value.id : null; - if (current != updatedFolder.parentId) { + if (current !== updatedFolder.parentId) { removeFolder(updatedFolder); } else { addFolder(updatedFolder, true); @@ -179,23 +181,23 @@ function onStreamDriveFolderDeleted(folderId: string) { removeFolder(folderId); } -function onDragover(e: DragEvent): any { - if (!e.dataTransfer) return; +function onDragover(ev: DragEvent): any { + if (!ev.dataTransfer) return; // ドラッグ元が自分自身の所有するアイテムだったら if (isDragSource.value) { // 自分自身にはドロップさせない - e.dataTransfer.dropEffect = 'none'; + ev.dataTransfer.dropEffect = 'none'; return; } - const isFile = e.dataTransfer.items[0].kind == 'file'; - const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_; - const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_; + const isFile = ev.dataTransfer.items[0].kind === 'file'; + const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_; + const isDriveFolder = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FOLDER_; if (isFile || isDriveFile || isDriveFolder) { - e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; + ev.dataTransfer.dropEffect = ev.dataTransfer.effectAllowed === 'all' ? 'copy' : 'move'; } else { - e.dataTransfer.dropEffect = 'none'; + ev.dataTransfer.dropEffect = 'none'; } return false; @@ -209,24 +211,24 @@ function onDragleave() { draghover.value = false; } -function onDrop(e: DragEvent): any { +function onDrop(ev: DragEvent): any { draghover.value = false; - if (!e.dataTransfer) return; + if (!ev.dataTransfer) return; // ドロップされてきたものがファイルだったら - if (e.dataTransfer.files.length > 0) { - for (const file of Array.from(e.dataTransfer.files)) { + if (ev.dataTransfer.files.length > 0) { + for (const file of Array.from(ev.dataTransfer.files)) { upload(file, folder.value); } return; } //#region ドライブのファイル - const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); - if (driveFile != null && driveFile != '') { + const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); + if (driveFile != null && driveFile !== '') { const file = JSON.parse(driveFile); - if (files.value.some(f => f.id == file.id)) return; + if (files.value.some(f => f.id === file.id)) return; removeFile(file.id); os.api('drive/files/update', { fileId: file.id, @@ -236,13 +238,13 @@ function onDrop(e: DragEvent): any { //#endregion //#region ドライブのフォルダ - const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_); - if (driveFolder != null && driveFolder != '') { + const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_); + if (driveFolder != null && driveFolder !== '') { const droppedFolder = JSON.parse(driveFolder); // 移動先が自分自身ならreject - if (folder.value && droppedFolder.id == folder.value.id) return false; - if (folders.value.some(f => f.id == droppedFolder.id)) return false; + if (folder.value && droppedFolder.id === folder.value.id) return false; + if (folders.value.some(f => f.id === droppedFolder.id)) return false; removeFolder(droppedFolder.id); os.api('drive/folders/update', { folderId: droppedFolder.id, @@ -330,7 +332,7 @@ function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) { // 削除時に親フォルダに移動 move(folderToDelete.parentId); }).catch(err => { - switch(err.id) { + switch (err.id) { case 'b0fc8a17-963c-405d-bfbc-859a487295e1': os.alert({ type: 'error', @@ -355,16 +357,16 @@ function onChangeFileInput() { } function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null) { - os.upload(file, (folderToUpload && typeof folderToUpload == 'object') ? folderToUpload.id : null).then(res => { + uploadFile(file, (folderToUpload && typeof folderToUpload === 'object') ? folderToUpload.id : null, undefined, keepOriginal.value).then(res => { addFile(res, true); }); } function chooseFile(file: Misskey.entities.DriveFile) { - const isAlreadySelected = selectedFiles.value.some(f => f.id == file.id); + const isAlreadySelected = selectedFiles.value.some(f => f.id === file.id); if (props.multiple) { if (isAlreadySelected) { - selectedFiles.value = selectedFiles.value.filter(f => f.id != file.id); + selectedFiles.value = selectedFiles.value.filter(f => f.id !== file.id); } else { selectedFiles.value.push(file); } @@ -380,10 +382,10 @@ function chooseFile(file: Misskey.entities.DriveFile) { } function chooseFolder(folderToChoose: Misskey.entities.DriveFolder) { - const isAlreadySelected = selectedFolders.value.some(f => f.id == folderToChoose.id); + const isAlreadySelected = selectedFolders.value.some(f => f.id === folderToChoose.id); if (props.multiple) { if (isAlreadySelected) { - selectedFolders.value = selectedFolders.value.filter(f => f.id != folderToChoose.id); + selectedFolders.value = selectedFolders.value.filter(f => f.id !== folderToChoose.id); } else { selectedFolders.value.push(folderToChoose); } @@ -402,7 +404,7 @@ function move(target?: Misskey.entities.DriveFolder) { if (!target) { goRoot(); return; - } else if (typeof target == 'object') { + } else if (typeof target === 'object') { target = target.id; } @@ -428,9 +430,9 @@ function move(target?: Misskey.entities.DriveFolder) { function addFolder(folderToAdd: Misskey.entities.DriveFolder, unshift = false) { const current = folder.value ? folder.value.id : null; - if (current != folderToAdd.parentId) return; + if (current !== folderToAdd.parentId) return; - if (folders.value.some(f => f.id == folderToAdd.id)) { + if (folders.value.some(f => f.id === folderToAdd.id)) { const exist = folders.value.map(f => f.id).indexOf(folderToAdd.id); folders.value[exist] = folderToAdd; return; @@ -445,9 +447,9 @@ function addFolder(folderToAdd: Misskey.entities.DriveFolder, unshift = false) { function addFile(fileToAdd: Misskey.entities.DriveFile, unshift = false) { const current = folder.value ? folder.value.id : null; - if (current != fileToAdd.folderId) return; + if (current !== fileToAdd.folderId) return; - if (files.value.some(f => f.id == fileToAdd.id)) { + if (files.value.some(f => f.id === fileToAdd.id)) { const exist = files.value.map(f => f.id).indexOf(fileToAdd.id); files.value[exist] = fileToAdd; return; @@ -462,12 +464,12 @@ function addFile(fileToAdd: Misskey.entities.DriveFile, unshift = false) { function removeFolder(folderToRemove: Misskey.entities.DriveFolder | string) { const folderIdToRemove = typeof folderToRemove === 'object' ? folderToRemove.id : folderToRemove; - folders.value = folders.value.filter(f => f.id != folderIdToRemove); + folders.value = folders.value.filter(f => f.id !== folderIdToRemove); } function removeFile(file: Misskey.entities.DriveFile | string) { const fileId = typeof file === 'object' ? file.id : file; - files.value = files.value.filter(f => f.id != fileId); + files.value = files.value.filter(f => f.id !== fileId); } function appendFile(file: Misskey.entities.DriveFile) { @@ -510,7 +512,7 @@ async function fetch() { folderId: folder.value ? folder.value.id : null, limit: foldersMax + 1 }).then(fetchedFolders => { - if (fetchedFolders.length == foldersMax + 1) { + if (fetchedFolders.length === foldersMax + 1) { moreFolders.value = true; fetchedFolders.pop(); } @@ -522,7 +524,7 @@ async function fetch() { type: props.type, limit: filesMax + 1 }).then(fetchedFiles => { - if (fetchedFiles.length == filesMax + 1) { + if (fetchedFiles.length === filesMax + 1) { moreFiles.value = true; fetchedFiles.pop(); } @@ -549,7 +551,7 @@ function fetchMoreFiles() { untilId: files.value[files.value.length - 1].id, limit: max + 1 }).then(files => { - if (files.length == max + 1) { + if (files.length === max + 1) { moreFiles.value = true; files.pop(); } else { @@ -562,6 +564,10 @@ function fetchMoreFiles() { function getMenu() { return [{ + type: 'switch', + text: i18n.ts.keepOriginalUploading, + ref: keepOriginal, + }, null, { text: i18n.ts.addFile, type: 'label' }, { @@ -601,7 +607,7 @@ function onContextmenu(ev: MouseEvent) { onMounted(() => { if (defaultStore.state.enableInfiniteScroll && loadMoreFiles.value) { nextTick(() => { - ilFilesObserver.observe(loadMoreFiles.value?.$el) + ilFilesObserver.observe(loadMoreFiles.value?.$el); }); } @@ -622,7 +628,7 @@ onMounted(() => { onActivated(() => { if (defaultStore.state.enableInfiniteScroll) { nextTick(() => { - ilFilesObserver.observe(loadMoreFiles.value?.$el) + ilFilesObserver.observe(loadMoreFiles.value?.$el); }); } }); diff --git a/packages/client/src/components/emoji-picker-window.vue b/packages/client/src/components/emoji-picker-window.vue index 4d27fb48ba..610690d701 100644 --- a/packages/client/src/components/emoji-picker-window.vue +++ b/packages/client/src/components/emoji-picker-window.vue @@ -25,8 +25,8 @@ withDefaults(defineProps<{ }); const emit = defineEmits<{ - (e: 'chosen', v: any): void; - (e: 'closed'): void; + (ev: 'chosen', v: any): void; + (ev: 'closed'): void; }>(); function chosen(emoji: any) { diff --git a/packages/client/src/components/emoji-picker.section.vue b/packages/client/src/components/emoji-picker.section.vue index 1026e894d1..52f7047487 100644 --- a/packages/client/src/components/emoji-picker.section.vue +++ b/packages/client/src/components/emoji-picker.section.vue @@ -24,7 +24,7 @@ const props = defineProps<{ }>(); const emit = defineEmits<{ - (e: 'chosen', v: string, ev: MouseEvent): void; + (ev: 'chosen', v: string, event: MouseEvent): void; }>(); const shown = ref(!!props.initialShown); diff --git a/packages/client/src/components/emoji-picker.vue b/packages/client/src/components/emoji-picker.vue index 8601ea121c..64732e7033 100644 --- a/packages/client/src/components/emoji-picker.vue +++ b/packages/client/src/components/emoji-picker.vue @@ -1,6 +1,6 @@ <template> <div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"> - <input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" @paste.stop="paste" @keyup.enter="done()"> + <input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" @paste.stop="paste" @keyup.enter="done()"> <div ref="emojis" class="emojis"> <section class="result"> <div v-if="searchResultCustom.length > 0"> @@ -61,7 +61,7 @@ </div> <div> <header class="_acrylic">{{ i18n.ts.emoji }}</header> - <XSection v-for="category in categories" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)" @chosen="chosen">{{ category }}</XSection> + <XSection v-for="category in categories" :key="category" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)" @chosen="chosen">{{ category }}</XSection> </div> </div> <div class="tabs"> @@ -97,7 +97,7 @@ const props = withDefaults(defineProps<{ }); const emit = defineEmits<{ - (e: 'chosen', v: string): void; + (ev: 'chosen', v: string): void; }>(); const search = ref<HTMLInputElement>(); @@ -138,7 +138,7 @@ watch(q, () => { const emojis = customEmojis; const matches = new Set<Misskey.entities.CustomEmoji>(); - const exactMatch = emojis.find(e => e.name === newQ); + const exactMatch = emojis.find(emoji => emoji.name === newQ); if (exactMatch) matches.add(exactMatch); if (newQ.includes(' ')) { // AND検索 @@ -201,7 +201,7 @@ watch(q, () => { const emojis = emojilist; const matches = new Set<UnicodeEmojiDef>(); - const exactMatch = emojis.find(e => e.name === newQ); + const exactMatch = emojis.find(emoji => emoji.name === newQ); if (exactMatch) matches.add(exactMatch); if (newQ.includes(' ')) { // AND検索 @@ -295,7 +295,7 @@ function chosen(emoji: any, ev?: MouseEvent) { // 最近使った絵文字更新 if (!pinned.value.includes(key)) { let recents = defaultStore.state.recentlyUsedEmojis; - recents = recents.filter((e: any) => e !== key); + recents = recents.filter((emoji: any) => emoji !== key); recents.unshift(key); defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32)); } @@ -313,12 +313,12 @@ function done(query?: any): boolean | void { if (query == null || typeof query !== 'string') return; const q2 = query.replace(/:/g, ''); - const exactMatchCustom = customEmojis.find(e => e.name === q2); + const exactMatchCustom = customEmojis.find(emoji => emoji.name === q2); if (exactMatchCustom) { chosen(exactMatchCustom); return true; } - const exactMatchUnicode = emojilist.find(e => e.char === q2 || e.name === q2); + const exactMatchUnicode = emojilist.find(emoji => emoji.char === q2 || emoji.name === q2); if (exactMatchUnicode) { chosen(exactMatchUnicode); return true; diff --git a/packages/client/src/components/follow-button.vue b/packages/client/src/components/follow-button.vue index 93c9e891c1..efee795e43 100644 --- a/packages/client/src/components/follow-button.vue +++ b/packages/client/src/components/follow-button.vue @@ -28,7 +28,7 @@ </template> <script lang="ts" setup> -import { onBeforeUnmount, onMounted, ref } from 'vue'; +import { onBeforeUnmount, onMounted } from 'vue'; import * as Misskey from 'misskey-js'; import * as os from '@/os'; import { stream } from '@/stream'; @@ -43,32 +43,30 @@ const props = withDefaults(defineProps<{ large: false, }); -const isFollowing = ref(props.user.isFollowing); -const hasPendingFollowRequestFromYou = ref(props.user.hasPendingFollowRequestFromYou); -const wait = ref(false); +let isFollowing = $ref(props.user.isFollowing); +let hasPendingFollowRequestFromYou = $ref(props.user.hasPendingFollowRequestFromYou); +let wait = $ref(false); const connection = stream.useChannel('main'); if (props.user.isFollowing == null) { os.api('users/show', { userId: props.user.id - }).then(u => { - isFollowing.value = u.isFollowing; - hasPendingFollowRequestFromYou.value = u.hasPendingFollowRequestFromYou; - }); + }) + .then(onFollowChange); } function onFollowChange(user: Misskey.entities.UserDetailed) { - if (user.id == props.user.id) { - isFollowing.value = user.isFollowing; - hasPendingFollowRequestFromYou.value = user.hasPendingFollowRequestFromYou; + if (user.id === props.user.id) { + isFollowing = user.isFollowing; + hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou; } } async function onClick() { - wait.value = true; + wait = true; try { - if (isFollowing.value) { + if (isFollowing) { const { canceled } = await os.confirm({ type: 'warning', text: i18n.t('unfollowConfirm', { name: props.user.name || props.user.username }), @@ -80,26 +78,22 @@ async function onClick() { userId: props.user.id }); } else { - if (hasPendingFollowRequestFromYou.value) { + if (hasPendingFollowRequestFromYou) { await os.api('following/requests/cancel', { userId: props.user.id }); - } else if (props.user.isLocked) { - await os.api('following/create', { - userId: props.user.id - }); - hasPendingFollowRequestFromYou.value = true; + hasPendingFollowRequestFromYou = false; } else { await os.api('following/create', { userId: props.user.id }); - hasPendingFollowRequestFromYou.value = true; + hasPendingFollowRequestFromYou = true; } } - } catch (e) { - console.error(e); + } catch (err) { + console.error(err); } finally { - wait.value = false; + wait = false; } } diff --git a/packages/client/src/components/forgot-password.vue b/packages/client/src/components/forgot-password.vue index 46cbf6bd70..19c1f23c85 100644 --- a/packages/client/src/components/forgot-password.vue +++ b/packages/client/src/components/forgot-password.vue @@ -41,8 +41,8 @@ import { instance } from '@/instance'; import { i18n } from '@/i18n'; const emit = defineEmits<{ - (e: 'done'): void; - (e: 'closed'): void; + (ev: 'done'): void; + (ev: 'closed'): void; }>(); let dialog: InstanceType<typeof XModalWindow> = $ref(); diff --git a/packages/client/src/components/form-dialog.vue b/packages/client/src/components/form-dialog.vue index efd0da443d..11459f5937 100644 --- a/packages/client/src/components/form-dialog.vue +++ b/packages/client/src/components/form-dialog.vue @@ -44,7 +44,7 @@ <template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template> <template v-if="form[item].description" #caption>{{ form[item].description }}</template> </FormRange> - <MkButton v-else-if="form[item].type === 'button'" @click="form[item].action($event, values)" class="_formBlock"> + <MkButton v-else-if="form[item].type === 'button'" class="_formBlock" @click="form[item].action($event, values)"> <span v-text="form[item].content || item"></span> </MkButton> </template> diff --git a/packages/client/src/components/form/folder.vue b/packages/client/src/components/form/folder.vue index 571afe50c0..1b960657d7 100644 --- a/packages/client/src/components/form/folder.vue +++ b/packages/client/src/components/form/folder.vue @@ -24,7 +24,7 @@ const props = withDefaults(defineProps<{ defaultOpen: boolean; }>(), { defaultOpen: false, -}) +}); let opened = $ref(props.defaultOpen); let openedAtLeastOnce = $ref(props.defaultOpen); diff --git a/packages/client/src/components/form/radios.vue b/packages/client/src/components/form/radios.vue index ff5d51f9c7..a52acae9e1 100644 --- a/packages/client/src/components/form/radios.vue +++ b/packages/client/src/components/form/radios.vue @@ -14,7 +14,7 @@ export default defineComponent({ data() { return { value: this.modelValue, - } + }; }, watch: { value() { diff --git a/packages/client/src/components/form/range.vue b/packages/client/src/components/form/range.vue index a82348d317..07f2c23124 100644 --- a/packages/client/src/components/form/range.vue +++ b/packages/client/src/components/form/range.vue @@ -16,7 +16,7 @@ </template> <script lang="ts"> -import { computed, defineComponent, onMounted, onUnmounted, ref, watch } from 'vue'; +import { computed, defineAsyncComponent, defineComponent, onMounted, onUnmounted, ref, watch } from 'vue'; import * as os from '@/os'; export default defineComponent({ @@ -112,7 +112,7 @@ export default defineComponent({ ev.preventDefault(); const tooltipShowing = ref(true); - os.popup(import('@/components/ui/tooltip.vue'), { + os.popup(defineAsyncComponent(() => import('@/components/ui/tooltip.vue')), { showing: tooltipShowing, text: computed(() => { return props.textConverter(finalValue.value); diff --git a/packages/client/src/components/form/switch.vue b/packages/client/src/components/form/switch.vue index b5a30d635c..fadb770aee 100644 --- a/packages/client/src/components/form/switch.vue +++ b/packages/client/src/components/form/switch.vue @@ -31,7 +31,7 @@ const props = defineProps<{ }>(); const emit = defineEmits<{ - (e: 'update:modelValue', v: boolean): void; + (ev: 'update:modelValue', v: boolean): void; }>(); let button = $ref<HTMLElement>(); diff --git a/packages/client/src/components/global/a.vue b/packages/client/src/components/global/a.vue index 52fef50f9b..5287d59b3e 100644 --- a/packages/client/src/components/global/a.vue +++ b/packages/client/src/components/global/a.vue @@ -5,14 +5,13 @@ </template> <script lang="ts" setup> -import { inject } from 'vue'; import * as os from '@/os'; import copyToClipboard from '@/scripts/copy-to-clipboard'; import { router } from '@/router'; import { url } from '@/config'; import { popout as popout_ } from '@/scripts/popout'; import { i18n } from '@/i18n'; -import { defaultStore } from '@/store'; +import { MisskeyNavigator } from '@/scripts/navigate'; const props = withDefaults(defineProps<{ to: string; @@ -23,9 +22,7 @@ const props = withDefaults(defineProps<{ behavior: null, }); -type Navigate = (path: string, record?: boolean) => void; -const navHook = inject<null | Navigate>('navHook', null); -const sideViewHook = inject<null | Navigate>('sideViewHook', null); +const mkNav = new MisskeyNavigator(); const active = $computed(() => { if (props.activeClass == null) return false; @@ -48,11 +45,11 @@ function onContextmenu(ev) { action: () => { os.pageWindow(props.to); } - }, sideViewHook ? { + }, mkNav.sideViewHook ? { icon: 'fas fa-columns', text: i18n.ts.openInSideView, action: () => { - sideViewHook(props.to); + if (mkNav.sideViewHook) mkNav.sideViewHook(props.to); } } : undefined, { icon: 'fas fa-expand-alt', @@ -101,18 +98,6 @@ function nav() { } } - if (navHook) { - navHook(props.to); - } else { - if (defaultStore.state.defaultSideView && sideViewHook && props.to !== '/') { - return sideViewHook(props.to); - } - - if (router.currentRoute.value.path === props.to) { - window.scroll({ top: 0, behavior: 'smooth' }); - } else { - router.push(props.to); - } - } + mkNav.push(props.to); } </script> diff --git a/packages/client/src/components/global/avatar.vue b/packages/client/src/components/global/avatar.vue index 27cfb6e4d4..4868896c99 100644 --- a/packages/client/src/components/global/avatar.vue +++ b/packages/client/src/components/global/avatar.vue @@ -32,7 +32,7 @@ const props = withDefaults(defineProps<{ }); const emit = defineEmits<{ - (e: 'click', ev: MouseEvent): void; + (ev: 'click', v: MouseEvent): void; }>(); const url = $computed(() => defaultStore.state.disableShowingAnimatedImages diff --git a/packages/client/src/components/global/emoji.vue b/packages/client/src/components/global/emoji.vue index 92edb1caf9..0075e0867d 100644 --- a/packages/client/src/components/global/emoji.vue +++ b/packages/client/src/components/global/emoji.vue @@ -46,7 +46,7 @@ export default defineComponent({ const url = computed(() => { if (char.value) { let codes = Array.from(char.value).map(x => x.codePointAt(0).toString(16)); - if (!codes.includes('200d')) codes = codes.filter(x => x != 'fe0f'); + if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f'); codes = codes.filter(x => x && x.length); return `${twemojiSvgBase}/${codes.join('-')}.svg`; } else { diff --git a/packages/client/src/components/global/header.vue b/packages/client/src/components/global/header.vue index e558614c12..63db19a520 100644 --- a/packages/client/src/components/global/header.vue +++ b/packages/client/src/components/global/header.vue @@ -38,7 +38,7 @@ <script lang="ts"> import { computed, defineComponent, onMounted, onUnmounted, PropType, ref, inject } from 'vue'; -import * as tinycolor from 'tinycolor2'; +import tinycolor from 'tinycolor2'; import { popupMenu } from '@/os'; import { url } from '@/config'; import { scrollToTop } from '@/scripts/scroll'; diff --git a/packages/client/src/components/global/loading.vue b/packages/client/src/components/global/loading.vue index 43ea1395ed..5a7e362fcf 100644 --- a/packages/client/src/components/global/loading.vue +++ b/packages/client/src/components/global/loading.vue @@ -1,11 +1,24 @@ <template> -<div class="yxspomdl" :class="{ inline, colored, mini }"> - <div class="ring"></div> +<div :class="[$style.root, { [$style.inline]: inline, [$style.colored]: colored, [$style.mini]: mini }]"> + <div :class="$style.container"> + <svg :class="[$style.spinner, $style.bg]" viewBox="0 0 168 168" xmlns="http://www.w3.org/2000/svg"> + <g transform="matrix(1.125,0,0,1.125,12,12)"> + <circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:21.33px;"/> + </g> + </svg> + <svg :class="[$style.spinner, $style.fg]" viewBox="0 0 168 168" xmlns="http://www.w3.org/2000/svg"> + <g transform="matrix(1.125,0,0,1.125,12,12)"> + <path d="M128,64C128,28.654 99.346,0 64,0C99.346,0 128,28.654 128,64Z" style="fill:none;stroke:currentColor;stroke-width:21.33px;"/> + </g> + </svg> + </div> </div> </template> <script lang="ts" setup> -import { } from 'vue'; +import { useCssModule } from 'vue'; + +useCssModule(); const props = withDefaults(defineProps<{ inline?: boolean; @@ -18,8 +31,8 @@ const props = withDefaults(defineProps<{ }); </script> -<style lang="scss" scoped> -@keyframes ring { +<style lang="scss" module> +@keyframes spinner { 0% { transform: rotate(0deg); } @@ -28,12 +41,12 @@ const props = withDefaults(defineProps<{ } } -.yxspomdl { +.root { padding: 32px; text-align: center; cursor: wait; - --size: 48px; + --size: 40px; &.colored { color: var(--accent); @@ -49,34 +62,33 @@ const props = withDefaults(defineProps<{ padding: 16px; --size: 32px; } +} - > .ring { - position: relative; - display: inline-block; - vertical-align: middle; +.container { + position: relative; + width: var(--size); + height: var(--size); + margin: 0 auto; +} - &:before, - &:after { - content: " "; - display: block; - box-sizing: border-box; - width: var(--size); - height: var(--size); - border-radius: 50%; - border: solid 4px; - } +.spinner { + position: absolute; + top: 0; + left: 0; + width: var(--size); + height: var(--size); + fill-rule: evenodd; + clip-rule: evenodd; + stroke-linecap: round; + stroke-linejoin: round; + stroke-miterlimit: 1.5; +} - &:before { - border-color: currentColor; - opacity: 0.3; - } +.bg { + opacity: 0.275; +} - &:after { - position: absolute; - top: 0; - border-color: currentColor transparent transparent transparent; - animation: ring 0.5s linear infinite; - } - } +.fg { + animation: spinner 0.5s linear infinite; } </style> diff --git a/packages/client/src/components/global/misskey-flavored-markdown.vue b/packages/client/src/components/global/misskey-flavored-markdown.vue index 243d8614ba..70d0108e9f 100644 --- a/packages/client/src/components/global/misskey-flavored-markdown.vue +++ b/packages/client/src/components/global/misskey-flavored-markdown.vue @@ -31,6 +31,32 @@ const props = withDefaults(defineProps<{ } } +.mfm-x2 { + --mfm-zoom-size: 200%; +} + +.mfm-x3 { + --mfm-zoom-size: 400%; +} + +.mfm-x4 { + --mfm-zoom-size: 600%; +} + +.mfm-x2, .mfm-x3, .mfm-x4 { + font-size: var(--mfm-zoom-size); + + .mfm-x2, .mfm-x3, .mfm-x4 { + /* only half effective */ + font-size: calc(var(--mfm-zoom-size) / 2 + 50%); + + .mfm-x2, .mfm-x3, .mfm-x4 { + /* disabled */ + font-size: 100%; + } + } +} + @keyframes mfm-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } diff --git a/packages/client/src/components/global/time.vue b/packages/client/src/components/global/time.vue index 5748d9de61..a7f142f961 100644 --- a/packages/client/src/components/global/time.vue +++ b/packages/client/src/components/global/time.vue @@ -17,7 +17,7 @@ const props = withDefaults(defineProps<{ mode: 'relative', }); -const _time = typeof props.time == 'string' ? new Date(props.time) : props.time; +const _time = typeof props.time === 'string' ? new Date(props.time) : props.time; const absolute = _time.toLocaleString(); let now = $ref(new Date()); @@ -32,8 +32,7 @@ const relative = $computed(() => { ago >= 60 ? i18n.t('_ago.minutesAgo', { n: (~~(ago / 60)).toString() }) : ago >= 10 ? i18n.t('_ago.secondsAgo', { n: (~~(ago % 60)).toString() }) : ago >= -1 ? i18n.ts._ago.justNow : - ago < -1 ? i18n.ts._ago.future : - i18n.ts._ago.unknown); + i18n.ts._ago.future); }); function tick() { diff --git a/packages/client/src/components/global/url.vue b/packages/client/src/components/global/url.vue index 55f6c5d5f9..34ba9024cc 100644 --- a/packages/client/src/components/global/url.vue +++ b/packages/client/src/components/global/url.vue @@ -18,7 +18,7 @@ </template> <script lang="ts"> -import { defineComponent, ref } from 'vue'; +import { defineAsyncComponent, defineComponent, ref } from 'vue'; import { toUnicode as decodePunycode } from 'punycode/'; import { url as local } from '@/config'; import * as os from '@/os'; @@ -50,7 +50,7 @@ export default defineComponent({ const el = ref(); useTooltip(el, (showing) => { - os.popup(import('@/components/url-preview-popup.vue'), { + os.popup(defineAsyncComponent(() => import('@/components/url-preview-popup.vue')), { showing, url: props.url, source: el.value, diff --git a/packages/client/src/components/image-viewer.vue b/packages/client/src/components/image-viewer.vue index c39076df16..7bc88399ef 100644 --- a/packages/client/src/components/image-viewer.vue +++ b/packages/client/src/components/image-viewer.vue @@ -25,7 +25,7 @@ const props = withDefaults(defineProps<{ }); const emit = defineEmits<{ - (e: 'closed'): void; + (ev: 'closed'): void; }>(); const modal = $ref<InstanceType<typeof MkModal>>(); diff --git a/packages/client/src/components/instance-ticker.vue b/packages/client/src/components/instance-ticker.vue index 9b0a18ec90..c32409ecf4 100644 --- a/packages/client/src/components/instance-ticker.vue +++ b/packages/client/src/components/instance-ticker.vue @@ -39,6 +39,19 @@ const bg = { border-radius: 4px 0 0 4px; overflow: hidden; color: #fff; + text-shadow: /* .866 ≈ sin(60deg) */ + 1px 0 1px #000, + .866px .5px 1px #000, + .5px .866px 1px #000, + 0 1px 1px #000, + -.5px .866px 1px #000, + -.866px .5px 1px #000, + -1px 0 1px #000, + -.866px -.5px 1px #000, + -.5px -.866px 1px #000, + 0 -1px 1px #000, + .5px -.866px 1px #000, + .866px -.5px 1px #000; > .icon { height: 100%; diff --git a/packages/client/src/components/link.vue b/packages/client/src/components/link.vue index 317c931cec..846a9a3a76 100644 --- a/packages/client/src/components/link.vue +++ b/packages/client/src/components/link.vue @@ -8,7 +8,7 @@ </template> <script lang="ts" setup> -import { } from 'vue'; +import { defineAsyncComponent } from 'vue'; import { url as local } from '@/config'; import { useTooltip } from '@/scripts/use-tooltip'; import * as os from '@/os'; @@ -26,7 +26,7 @@ const target = self ? null : '_blank'; const el = $ref(); useTooltip($$(el), (showing) => { - os.popup(import('@/components/url-preview-popup.vue'), { + os.popup(defineAsyncComponent(() => import('@/components/url-preview-popup.vue')), { showing, url: props.url, source: el, diff --git a/packages/client/src/components/media-caption.vue b/packages/client/src/components/media-caption.vue index ef546f3f70..feed3854f9 100644 --- a/packages/client/src/components/media-caption.vue +++ b/packages/client/src/components/media-caption.vue @@ -77,7 +77,7 @@ export default defineComponent({ computed: { remainingLength(): number { - if (typeof this.inputValue != "string") return 512; + if (typeof this.inputValue !== "string") return 512; return 512 - length(this.inputValue); } }, @@ -116,17 +116,17 @@ export default defineComponent({ } }, - onKeydown(e) { - if (e.which === 27) { // ESC + onKeydown(evt) { + if (evt.which === 27) { // ESC this.cancel(); } }, - onInputKeydown(e) { - if (e.which === 13) { // Enter - if (e.ctrlKey) { - e.preventDefault(); - e.stopPropagation(); + onInputKeydown(evt) { + if (evt.which === 13) { // Enter + if (evt.ctrlKey) { + evt.preventDefault(); + evt.stopPropagation(); this.ok(); } } diff --git a/packages/client/src/components/media-video.vue b/packages/client/src/components/media-video.vue index 680eb27e64..5c38691e69 100644 --- a/packages/client/src/components/media-video.vue +++ b/packages/client/src/components/media-video.vue @@ -8,7 +8,8 @@ <div v-else class="kkjnbbplepmiyuadieoenjgutgcmtsvu"> <video :poster="video.thumbnailUrl" - :title="video.name" + :title="video.comment" + :alt="video.comment" preload="none" controls @contextmenu.stop diff --git a/packages/client/src/components/mention.vue b/packages/client/src/components/mention.vue index 479acfbc8f..70c2f49afa 100644 --- a/packages/client/src/components/mention.vue +++ b/packages/client/src/components/mention.vue @@ -1,22 +1,22 @@ <template> -<MkA v-if="url.startsWith('/')" v-user-preview="canonical" class="ldlomzub" :class="{ isMe }" :to="url" :style="{ background: bg }"> - <img class="icon" :src="`/avatar/@${username}@${host}`" alt=""> +<MkA v-if="url.startsWith('/')" v-user-preview="canonical" :class="[$style.root, { isMe }]" :to="url" :style="{ background: bg }"> + <img :class="$style.icon" :src="`/avatar/@${username}@${host}`" alt=""> <span class="main"> <span class="username">@{{ username }}</span> - <span v-if="(host != localHost) || $store.state.showFullAcct" class="host">@{{ toUnicode(host) }}</span> + <span v-if="(host != localHost) || $store.state.showFullAcct" :class="$style.mainHost">@{{ toUnicode(host) }}</span> </span> </MkA> -<a v-else class="ldlomzub" :href="url" target="_blank" rel="noopener" :style="{ background: bg }"> +<a v-else :class="$style.root" :href="url" target="_blank" rel="noopener" :style="{ background: bg }"> <span class="main"> <span class="username">@{{ username }}</span> - <span class="host">@{{ toUnicode(host) }}</span> + <span :class="$style.mainHost">@{{ toUnicode(host) }}</span> </span> </a> </template> <script lang="ts"> -import { defineComponent } from 'vue'; -import * as tinycolor from 'tinycolor2'; +import { defineComponent, useCssModule } from 'vue'; +import tinycolor from 'tinycolor2'; import { toUnicode } from 'punycode'; import { host as localHost } from '@/config'; import { $i } from '@/account'; @@ -45,6 +45,8 @@ export default defineComponent({ const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue(isMe ? '--mentionMe' : '--mention')); bg.setAlpha(0.1); + useCssModule(); + return { localHost, isMe, @@ -57,8 +59,8 @@ export default defineComponent({ }); </script> -<style lang="scss" scoped> -.ldlomzub { +<style lang="scss" module> +.root { display: inline-block; padding: 4px 8px 4px 4px; border-radius: 999px; @@ -67,18 +69,18 @@ export default defineComponent({ &.isMe { color: var(--mentionMe); } +} - > .icon { - width: 1.5em; - margin: 0 0.2em 0 0; - vertical-align: bottom; - border-radius: 100%; - } +.icon { + width: 1.5em; + height: 1.5em; + object-fit: cover; + margin: 0 0.2em 0 0; + vertical-align: bottom; + border-radius: 100%; +} - > .main { - > .host { - opacity: 0.5; - } - } +.mainHost { + opacity: 0.5; } </style> diff --git a/packages/client/src/components/mfm.ts b/packages/client/src/components/mfm.ts index 37076652fd..4556a82d55 100644 --- a/packages/client/src/components/mfm.ts +++ b/packages/client/src/components/mfm.ts @@ -91,7 +91,8 @@ export default defineComponent({ let style; switch (token.props.name) { case 'tada': { - style = `font-size: 150%;` + (this.$store.state.animatedMfm ? 'animation: tada 1s linear infinite both;' : ''); + const speed = validTime(token.props.args.speed) || '1s'; + style = 'font-size: 150%;' + (this.$store.state.animatedMfm ? `animation: tada ${speed} linear infinite both;` : ''); break; } case 'jelly': { @@ -123,11 +124,13 @@ export default defineComponent({ break; } case 'jump': { - style = this.$store.state.animatedMfm ? 'animation: mfm-jump 0.75s linear infinite;' : ''; + const speed = validTime(token.props.args.speed) || '0.75s'; + style = this.$store.state.animatedMfm ? `animation: mfm-jump ${speed} linear infinite;` : ''; break; } case 'bounce': { - style = this.$store.state.animatedMfm ? 'animation: mfm-bounce 0.75s linear infinite; transform-origin: center bottom;' : ''; + const speed = validTime(token.props.args.speed) || '0.75s'; + style = this.$store.state.animatedMfm ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom;` : ''; break; } case 'flip': { @@ -139,16 +142,19 @@ export default defineComponent({ break; } case 'x2': { - style = `font-size: 200%;`; - break; + return h('span', { + class: 'mfm-x2', + }, genEl(token.children)); } case 'x3': { - style = `font-size: 400%;`; - break; + return h('span', { + class: 'mfm-x3', + }, genEl(token.children)); } case 'x4': { - style = `font-size: 600%;`; - break; + return h('span', { + class: 'mfm-x4', + }, genEl(token.children)); } case 'font': { const family = @@ -168,7 +174,8 @@ export default defineComponent({ }, genEl(token.children)); } case 'rainbow': { - style = this.$store.state.animatedMfm ? 'animation: mfm-rainbow 1s linear infinite;' : ''; + const speed = validTime(token.props.args.speed) || '1s'; + style = this.$store.state.animatedMfm ? `animation: mfm-rainbow ${speed} linear infinite;` : ''; break; } case 'sparkle': { diff --git a/packages/client/src/components/modal-page-window.vue b/packages/client/src/components/modal-page-window.vue index 2e17d5d030..21bdb657b7 100644 --- a/packages/client/src/components/modal-page-window.vue +++ b/packages/client/src/components/modal-page-window.vue @@ -39,8 +39,8 @@ export default defineComponent({ inject: { sideViewHook: { - default: null - } + default: null, + }, }, provide() { @@ -94,31 +94,31 @@ export default defineComponent({ }, { icon: 'fas fa-expand-alt', text: this.$ts.showInPage, - action: this.expand + action: this.expand, }, this.sideViewHook ? { icon: 'fas fa-columns', text: this.$ts.openInSideView, action: () => { this.sideViewHook(this.path); this.$refs.window.close(); - } + }, } : undefined, { icon: 'fas fa-external-link-alt', text: this.$ts.popout, - action: this.popout + action: this.popout, }, null, { icon: 'fas fa-external-link-alt', text: this.$ts.openInNewTab, action: () => { window.open(this.url, '_blank'); this.$refs.window.close(); - } + }, }, { icon: 'fas fa-link', text: this.$ts.copyLink, action: () => { copyToClipboard(this.url); - } + }, }]; }, }, @@ -155,7 +155,7 @@ export default defineComponent({ onContextmenu(ev: MouseEvent) { os.contextMenu(this.contextmenu, ev); - } + }, }, }); </script> diff --git a/packages/client/src/components/note-detailed.vue b/packages/client/src/components/note-detailed.vue index d30284ca5f..6234b710d2 100644 --- a/packages/client/src/components/note-detailed.vue +++ b/packages/client/src/components/note-detailed.vue @@ -2,9 +2,9 @@ <div v-if="!muted" v-show="!isDeleted" + ref="el" v-hotkey="keymap" v-size="{ max: [500, 450, 350, 300] }" - ref="el" class="lxwezrsl _block" :tabindex="!isDeleted ? '-1' : null" :class="{ renote: isRenote }" @@ -197,7 +197,7 @@ const keymap = { 'q': () => renoteButton.value.renote(true), 'esc': blur, 'm|o': () => menu(true), - 's': () => showContent.value != showContent.value, + 's': () => showContent.value !== showContent.value, }; useNoteCapture({ @@ -222,7 +222,7 @@ function react(viaKeyboard = false): void { reactionPicker.show(reactButton.value, reaction => { os.api('notes/reactions/create', { noteId: appearNote.id, - reaction: reaction + reaction: reaction, }); }, () => { focus(); @@ -233,7 +233,7 @@ function undoReact(note): void { const oldReaction = note.myReaction; if (!oldReaction) return; os.api('notes/reactions/delete', { - noteId: note.id + noteId: note.id, }); } @@ -257,7 +257,7 @@ function onContextmenu(ev: MouseEvent): void { function menu(viaKeyboard = false): void { os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton }), menuButton.value, { - viaKeyboard + viaKeyboard, }).then(focus); } @@ -269,12 +269,12 @@ function showRenoteMenu(viaKeyboard = false): void { danger: true, action: () => { os.api('notes/delete', { - noteId: note.id + noteId: note.id, }); isDeleted.value = true; - } + }, }], renoteTime.value, { - viaKeyboard: viaKeyboard + viaKeyboard: viaKeyboard, }); } @@ -288,14 +288,14 @@ function blur() { os.api('notes/children', { noteId: appearNote.id, - limit: 30 + limit: 30, }).then(res => { replies.value = res; }); if (appearNote.replyId) { os.api('notes/conversation', { - noteId: appearNote.replyId + noteId: appearNote.replyId, }).then(res => { conversation.value = res.reverse(); }); diff --git a/packages/client/src/components/note-simple.vue b/packages/client/src/components/note-simple.vue index c6907787b5..b813b9a2b9 100644 --- a/packages/client/src/components/note-simple.vue +++ b/packages/client/src/components/note-simple.vue @@ -5,7 +5,7 @@ <XNoteHeader class="header" :note="note" :mini="true"/> <div class="body"> <p v-if="note.cw != null" class="cw"> - <span v-if="note.cw != ''" class="text">{{ note.cw }}</span> + <Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$i" :custom-emojis="note.emojis"/> <XCwButton v-model="showContent" :note="note"/> </p> <div v-show="note.cw == null || showContent" class="content"> diff --git a/packages/client/src/components/note.vue b/packages/client/src/components/note.vue index 3cd7a819d4..e5744d1ce9 100644 --- a/packages/client/src/components/note.vue +++ b/packages/client/src/components/note.vue @@ -185,7 +185,7 @@ const keymap = { 'down|j|tab': focusAfter, 'esc': blur, 'm|o': () => menu(true), - 's': () => showContent.value != showContent.value, + 's': () => showContent.value !== showContent.value, }; useNoteCapture({ @@ -210,7 +210,7 @@ function react(viaKeyboard = false): void { reactionPicker.show(reactButton.value, reaction => { os.api('notes/reactions/create', { noteId: appearNote.id, - reaction: reaction + reaction: reaction, }); }, () => { focus(); @@ -221,7 +221,7 @@ function undoReact(note): void { const oldReaction = note.myReaction; if (!oldReaction) return; os.api('notes/reactions/delete', { - noteId: note.id + noteId: note.id, }); } @@ -245,7 +245,7 @@ function onContextmenu(ev: MouseEvent): void { function menu(viaKeyboard = false): void { os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton }), menuButton.value, { - viaKeyboard + viaKeyboard, }).then(focus); } @@ -257,12 +257,12 @@ function showRenoteMenu(viaKeyboard = false): void { danger: true, action: () => { os.api('notes/delete', { - noteId: note.id + noteId: note.id, }); isDeleted.value = true; - } + }, }], renoteTime.value, { - viaKeyboard: viaKeyboard + viaKeyboard: viaKeyboard, }); } @@ -284,7 +284,7 @@ function focusAfter() { function readPromo() { os.api('promo/read', { - noteId: appearNote.id + noteId: appearNote.id, }); isDeleted.value = true; } diff --git a/packages/client/src/components/notification-setting-window.vue b/packages/client/src/components/notification-setting-window.vue index ec1efec261..64d828394b 100644 --- a/packages/client/src/components/notification-setting-window.vue +++ b/packages/client/src/components/notification-setting-window.vue @@ -1,5 +1,6 @@ <template> -<XModalWindow ref="dialog" +<XModalWindow + ref="dialog" :width="400" :height="450" :with-ok-button="true" @@ -28,18 +29,18 @@ <script lang="ts"> import { defineComponent, PropType } from 'vue'; -import XModalWindow from '@/components/ui/modal-window.vue'; +import { notificationTypes } from 'misskey-js'; import MkSwitch from './form/switch.vue'; import MkInfo from './ui/info.vue'; import MkButton from './ui/button.vue'; -import { notificationTypes } from 'misskey-js'; +import XModalWindow from '@/components/ui/modal-window.vue'; export default defineComponent({ components: { XModalWindow, MkSwitch, MkInfo, - MkButton + MkButton, }, props: { @@ -53,7 +54,7 @@ export default defineComponent({ type: Boolean, required: false, default: true, - } + }, }, emits: ['done', 'closed'], @@ -93,7 +94,7 @@ export default defineComponent({ for (const type in this.typesMap) { this.typesMap[type as typeof notificationTypes[number]] = true; } - } - } + }, + }, }); </script> diff --git a/packages/client/src/components/notification.vue b/packages/client/src/components/notification.vue index 1a360f9905..cbfd809f37 100644 --- a/packages/client/src/components/notification.vue +++ b/packages/client/src/components/notification.vue @@ -16,7 +16,8 @@ <i v-else-if="notification.type === 'pollVote'" class="fas fa-poll-h"></i> <i v-else-if="notification.type === 'pollEnded'" class="fas fa-poll-h"></i> <!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 --> - <XReactionIcon v-else-if="notification.type === 'reaction'" + <XReactionIcon + v-else-if="notification.type === 'reaction'" ref="reactionRef" :reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction" :custom-emojis="notification.note.emojis" @@ -72,12 +73,12 @@ </template> <script lang="ts"> -import { defineComponent, ref, onMounted, onUnmounted } from 'vue'; +import { defineComponent, ref, onMounted, onUnmounted, watch } from 'vue'; import * as misskey from 'misskey-js'; -import { getNoteSummary } from '@/scripts/get-note-summary'; import XReactionIcon from './reaction-icon.vue'; import MkFollowButton from './follow-button.vue'; import XReactionTooltip from './reaction-tooltip.vue'; +import { getNoteSummary } from '@/scripts/get-note-summary'; import { notePage } from '@/filters/note'; import { userPage } from '@/filters/user'; import { i18n } from '@/i18n'; @@ -87,7 +88,7 @@ import { useTooltip } from '@/scripts/use-tooltip'; export default defineComponent({ components: { - XReactionIcon, MkFollowButton + XReactionIcon, MkFollowButton, }, props: { @@ -116,7 +117,7 @@ export default defineComponent({ const readObserver = new IntersectionObserver((entries, observer) => { if (!entries.some(entry => entry.isIntersecting)) return; stream.send('readNotification', { - id: props.notification.id + id: props.notification.id, }); observer.disconnect(); }); @@ -126,6 +127,10 @@ export default defineComponent({ const connection = stream.useChannel('main'); connection.on('readAllNotifications', () => readObserver.disconnect()); + watch(props.notification.isRead, () => { + readObserver.disconnect(); + }); + onUnmounted(() => { readObserver.disconnect(); connection.dispose(); diff --git a/packages/client/src/components/notifications.vue b/packages/client/src/components/notifications.vue index d522503a14..8eb569c369 100644 --- a/packages/client/src/components/notifications.vue +++ b/packages/client/src/components/notifications.vue @@ -19,8 +19,7 @@ <script lang="ts" setup> import { defineComponent, markRaw, onUnmounted, onMounted, computed, ref } from 'vue'; import { notificationTypes } from 'misskey-js'; -import MkPagination from '@/components/ui/pagination.vue'; -import { Paging } from '@/components/ui/pagination.vue'; +import MkPagination, { Paging } from '@/components/ui/pagination.vue'; import XNotification from '@/components/notification.vue'; import XList from '@/components/date-separated-list.vue'; import XNote from '@/components/note.vue'; @@ -49,14 +48,14 @@ const onNotification = (notification) => { const isMuted = props.includeTypes ? !props.includeTypes.includes(notification.type) : $i.mutingNotificationTypes.includes(notification.type); if (isMuted || document.visibilityState === 'visible') { stream.send('readNotification', { - id: notification.id + id: notification.id, }); } if (!isMuted) { pagingComponent.value.prepend({ ...notification, - isRead: document.visibilityState === 'visible' + isRead: document.visibilityState === 'visible', }); } }; @@ -64,6 +63,31 @@ const onNotification = (notification) => { onMounted(() => { const connection = stream.useChannel('main'); connection.on('notification', onNotification); + connection.on('readAllNotifications', () => { + if (pagingComponent.value) { + for (const item of pagingComponent.value.queue) { + item.isRead = true; + } + for (const item of pagingComponent.value.items) { + item.isRead = true; + } + } + }); + connection.on('readNotifications', notificationIds => { + if (pagingComponent.value) { + for (let i = 0; i < pagingComponent.value.queue.length; i++) { + if (notificationIds.includes(pagingComponent.value.queue[i].id)) { + pagingComponent.value.queue[i].isRead = true; + } + } + for (let i = 0; i < (pagingComponent.value.items || []).length; i++) { + if (notificationIds.includes(pagingComponent.value.items[i].id)) { + pagingComponent.value.items[i].isRead = true; + } + } + } + }); + onUnmounted(() => { connection.dispose(); }); diff --git a/packages/client/src/components/number-diff.vue b/packages/client/src/components/number-diff.vue index 9889c97ec3..e7d4a5472a 100644 --- a/packages/client/src/components/number-diff.vue +++ b/packages/client/src/components/number-diff.vue @@ -12,7 +12,7 @@ export default defineComponent({ props: { value: { type: Number, - required: true + required: true, }, }, @@ -26,7 +26,7 @@ export default defineComponent({ isZero, number, }; - } + }, }); </script> diff --git a/packages/client/src/components/page/page.image.vue b/packages/client/src/components/page/page.image.vue index 04ce74bd7c..6e38a9f424 100644 --- a/packages/client/src/components/page/page.image.vue +++ b/packages/client/src/components/page/page.image.vue @@ -1,34 +1,22 @@ <template> <div class="lzyxtsnt"> - <img v-if="image" :src="image.url"/> + <ImgWithBlurhash v-if="image" :hash="image.blurhash" :src="image.url" :alt="image.comment" :title="image.comment" :cover="false"/> </div> </template> -<script lang="ts"> +<script lang="ts" setup> import { defineComponent, PropType } from 'vue'; +import ImgWithBlurhash from '@/components/img-with-blurhash.vue'; import * as os from '@/os'; import { ImageBlock } from '@/scripts/hpml/block'; import { Hpml } from '@/scripts/hpml/evaluator'; -export default defineComponent({ - props: { - block: { - type: Object as PropType<ImageBlock>, - required: true - }, - hpml: { - type: Object as PropType<Hpml>, - required: true - } - }, - setup(props, ctx) { - const image = props.hpml.page.attachedFiles.find(x => x.id === props.block.fileId); +const props = defineProps<{ + block: PropType<ImageBlock>, + hpml: PropType<Hpml>, +}>(); - return { - image - }; - } -}); +const image = props.hpml.page.attachedFiles.find(x => x.id === props.block.fileId); </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/page/page.post.vue b/packages/client/src/components/page/page.post.vue index 847da37c51..3401f945bd 100644 --- a/packages/client/src/components/page/page.post.vue +++ b/packages/client/src/components/page/page.post.vue @@ -52,21 +52,21 @@ export default defineComponent({ const promise = new Promise((ok) => { const canvas = this.hpml.canvases[this.block.canvasId]; canvas.toBlob(blob => { - const data = new FormData(); - data.append('file', blob); - data.append('i', this.$i.token); + const formData = new FormData(); + formData.append('file', blob); + formData.append('i', this.$i.token); if (this.$store.state.uploadFolder) { - data.append('folderId', this.$store.state.uploadFolder); + formData.append('folderId', this.$store.state.uploadFolder); } fetch(apiUrl + '/drive/files/create', { method: 'POST', - body: data + body: formData, }) .then(response => response.json()) .then(f => { ok(f); - }) + }); }); }); os.promiseDialog(promise); diff --git a/packages/client/src/components/page/page.vue b/packages/client/src/components/page/page.vue index e54147bbd0..a067762372 100644 --- a/packages/client/src/components/page/page.vue +++ b/packages/client/src/components/page/page.vue @@ -38,8 +38,8 @@ export default defineComponent({ let ast; try { ast = parse(props.page.script); - } catch (e) { - console.error(e); + } catch (err) { + console.error(err); /*os.alert({ type: 'error', text: 'Syntax error :(' @@ -48,11 +48,11 @@ export default defineComponent({ } hpml.aiscript.exec(ast).then(() => { hpml.eval(); - }).catch(e => { - console.error(e); + }).catch(err => { + console.error(err); /*os.alert({ type: 'error', - text: e + text: err });*/ }); } else { diff --git a/packages/client/src/components/poll-editor.vue b/packages/client/src/components/poll-editor.vue index 6f3f23a2d3..9aa5510c7f 100644 --- a/packages/client/src/components/poll-editor.vue +++ b/packages/client/src/components/poll-editor.vue @@ -104,7 +104,7 @@ function add() { } function remove(i) { - choices.value = choices.value.filter((_, _i) => _i != i); + choices.value = choices.value.filter((_, _i) => _i !== i); } function get() { diff --git a/packages/client/src/components/post-form-attaches.vue b/packages/client/src/components/post-form-attaches.vue index 9dd69a0ee5..6b9827407b 100644 --- a/packages/client/src/components/post-form-attaches.vue +++ b/packages/client/src/components/post-form-attaches.vue @@ -16,7 +16,7 @@ <script lang="ts"> import { defineComponent, defineAsyncComponent } from 'vue'; -import MkDriveFileThumbnail from './drive-file-thumbnail.vue' +import MkDriveFileThumbnail from './drive-file-thumbnail.vue'; import * as os from '@/os'; export default defineComponent({ @@ -88,7 +88,7 @@ export default defineComponent({ }, async describe(file) { - os.popup(import("@/components/media-caption.vue"), { + os.popup(defineAsyncComponent(() => import("@/components/media-caption.vue")), { title: this.$ts.describeFile, input: { placeholder: this.$ts.inputNewDescription, @@ -98,7 +98,7 @@ export default defineComponent({ }, { done: result => { if (!result || result.canceled) return; - let comment = result.result.length == 0 ? null : result.result; + let comment = result.result.length === 0 ? null : result.result; os.api('drive/files/update', { fileId: file.id, comment: comment, @@ -114,19 +114,19 @@ export default defineComponent({ this.menu = os.popupMenu([{ text: this.$ts.renameFile, icon: 'fas fa-i-cursor', - action: () => { this.rename(file) } + action: () => { this.rename(file); } }, { text: file.isSensitive ? this.$ts.unmarkAsSensitive : this.$ts.markAsSensitive, icon: file.isSensitive ? 'fas fa-eye-slash' : 'fas fa-eye', - action: () => { this.toggleSensitive(file) } + action: () => { this.toggleSensitive(file); } }, { text: this.$ts.describeFile, icon: 'fas fa-i-cursor', - action: () => { this.describe(file) } + action: () => { this.describe(file); } }, { text: this.$ts.attachCancel, icon: 'fas fa-times-circle', - action: () => { this.detachMedia(file.id) } + action: () => { this.detachMedia(file.id); } }], ev.currentTarget ?? ev.target).then(() => this.menu = null); } } diff --git a/packages/client/src/components/post-form.vue b/packages/client/src/components/post-form.vue index 656689ddcb..0197313e0e 100644 --- a/packages/client/src/components/post-form.vue +++ b/packages/client/src/components/post-form.vue @@ -62,7 +62,7 @@ </template> <script lang="ts" setup> -import { inject, watch, nextTick, onMounted } from 'vue'; +import { inject, watch, nextTick, onMounted, defineAsyncComponent } from 'vue'; import * as mfm from 'mfm-js'; import * as misskey from 'misskey-js'; import insertTextAtCursor from 'insert-text-at-cursor'; @@ -87,6 +87,7 @@ import MkInfo from '@/components/ui/info.vue'; import { i18n } from '@/i18n'; import { instance } from '@/instance'; import { $i, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account'; +import { uploadFile } from '@/scripts/upload'; const modal = inject('modal'); @@ -106,7 +107,7 @@ const props = withDefaults(defineProps<{ fixed?: boolean; autofocus?: boolean; }>(), { - initialVisibleUsers: [], + initialVisibleUsers: () => [], autofocus: true, }); @@ -227,7 +228,7 @@ if (props.mention) { text += ' '; } -if (props.reply && (props.reply.user.username != $i.username || (props.reply.user.host != null && props.reply.user.host != host))) { +if (props.reply && (props.reply.user.username !== $i.username || (props.reply.user.host != null && props.reply.user.host !== host))) { text = `@${props.reply.user.username}${props.reply.user.host != null ? '@' + toASCII(props.reply.user.host) : ''} `; } @@ -238,16 +239,15 @@ if (props.reply && props.reply.text != null) { for (const x of extractMentions(ast)) { const mention = x.host ? `@${x.username}@${toASCII(x.host)}` : - (otherHost == null || otherHost == host) ? + (otherHost == null || otherHost === host) ? `@${x.username}` : `@${x.username}@${toASCII(otherHost)}`; // 自分は除外 - if ($i.username == x.username && x.host == null) continue; - if ($i.username == x.username && x.host == host) continue; + if ($i.username === x.username && (x.host == null || x.host === host)) continue; // 重複は除外 - if (text.indexOf(`${mention} `) != -1) continue; + if (text.includes(`${mention} `)) continue; text += `${mention} `; } @@ -302,7 +302,7 @@ function checkMissingMention() { const ast = mfm.parse(text); for (const x of extractMentions(ast)) { - if (!visibleUsers.some(u => (u.username === x.username) && (u.host == x.host))) { + if (!visibleUsers.some(u => (u.username === x.username) && (u.host === x.host))) { hasNotSpecifiedMentions = true; return; } @@ -315,7 +315,7 @@ function addMissingMention() { const ast = mfm.parse(text); for (const x of extractMentions(ast)) { - if (!visibleUsers.some(u => (u.username === x.username) && (u.host == x.host))) { + if (!visibleUsers.some(u => (u.username === x.username) && (u.host === x.host))) { os.api('users/show', { username: x.username, host: x.host }).then(user => { visibleUsers.push(user); }); @@ -356,7 +356,7 @@ function chooseFileFrom(ev) { } function detachFile(id) { - files = files.filter(x => x.id != id); + files = files.filter(x => x.id !== id); } function updateFiles(_files) { @@ -372,7 +372,7 @@ function updateFileName(file, name) { } function upload(file: File, name?: string) { - os.upload(file, defaultStore.state.uploadFolder, name).then(res => { + uploadFile(file, defaultStore.state.uploadFolder, name).then(res => { files.push(res); }); } @@ -383,7 +383,7 @@ function setVisibility() { return; } - os.popup(import('./visibility-picker.vue'), { + os.popup(defineAsyncComponent(() => import('./visibility-picker.vue')), { currentVisibility: visibility, currentLocalOnly: localOnly, src: visibilityButton, @@ -426,24 +426,24 @@ function clear() { quoteId = null; } -function onKeydown(e: KeyboardEvent) { - if ((e.which === 10 || e.which === 13) && (e.ctrlKey || e.metaKey) && canPost) post(); - if (e.which === 27) emit('esc'); +function onKeydown(ev: KeyboardEvent) { + if ((ev.which === 10 || ev.which === 13) && (ev.ctrlKey || ev.metaKey) && canPost) post(); + if (ev.which === 27) emit('esc'); typing(); } -function onCompositionUpdate(e: CompositionEvent) { - imeText = e.data; +function onCompositionUpdate(ev: CompositionEvent) { + imeText = ev.data; typing(); } -function onCompositionEnd(e: CompositionEvent) { +function onCompositionEnd(ev: CompositionEvent) { imeText = ''; } -async function onPaste(e: ClipboardEvent) { - for (const { item, i } of Array.from(e.clipboardData.items).map((item, i) => ({item, i}))) { - if (item.kind == 'file') { +async function onPaste(ev: ClipboardEvent) { + for (const { item, i } of Array.from(ev.clipboardData.items).map((item, i) => ({ item, i }))) { + if (item.kind === 'file') { const file = item.getAsFile(); const lio = file.name.lastIndexOf('.'); const ext = lio >= 0 ? file.name.slice(lio) : ''; @@ -452,10 +452,10 @@ async function onPaste(e: ClipboardEvent) { } } - const paste = e.clipboardData.getData('text'); + const paste = ev.clipboardData.getData('text'); if (!props.renote && !quoteId && paste.startsWith(url + '/notes/')) { - e.preventDefault(); + ev.preventDefault(); os.confirm({ type: 'info', @@ -471,49 +471,49 @@ async function onPaste(e: ClipboardEvent) { } } -function onDragover(e) { - if (!e.dataTransfer.items[0]) return; - const isFile = e.dataTransfer.items[0].kind == 'file'; - const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_; +function onDragover(ev) { + if (!ev.dataTransfer.items[0]) return; + const isFile = ev.dataTransfer.items[0].kind === 'file'; + const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_; if (isFile || isDriveFile) { - e.preventDefault(); + ev.preventDefault(); draghover = true; - e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; + ev.dataTransfer.dropEffect = ev.dataTransfer.effectAllowed === 'all' ? 'copy' : 'move'; } } -function onDragenter(e) { +function onDragenter(ev) { draghover = true; } -function onDragleave(e) { +function onDragleave(ev) { draghover = false; } -function onDrop(e): void { +function onDrop(ev): void { draghover = false; // ファイルだったら - if (e.dataTransfer.files.length > 0) { - e.preventDefault(); - for (const x of Array.from(e.dataTransfer.files)) upload(x); + if (ev.dataTransfer.files.length > 0) { + ev.preventDefault(); + for (const x of Array.from(ev.dataTransfer.files)) upload(x); return; } //#region ドライブのファイル - const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); - if (driveFile != null && driveFile != '') { + const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); + if (driveFile != null && driveFile !== '') { const file = JSON.parse(driveFile); files.push(file); - e.preventDefault(); + ev.preventDefault(); } //#endregion } function saveDraft() { - const data = JSON.parse(localStorage.getItem('drafts') || '{}'); + const draftData = JSON.parse(localStorage.getItem('drafts') || '{}'); - data[draftKey] = { + draftData[draftKey] = { updatedAt: new Date(), data: { text: text, @@ -526,20 +526,20 @@ function saveDraft() { } }; - localStorage.setItem('drafts', JSON.stringify(data)); + localStorage.setItem('drafts', JSON.stringify(draftData)); } function deleteDraft() { - const data = JSON.parse(localStorage.getItem('drafts') || '{}'); + const draftData = JSON.parse(localStorage.getItem('drafts') || '{}'); - delete data[draftKey]; + delete draftData[draftKey]; - localStorage.setItem('drafts', JSON.stringify(data)); + localStorage.setItem('drafts', JSON.stringify(draftData)); } async function post() { - let data = { - text: text == '' ? undefined : text, + let postData = { + text: text === '' ? undefined : text, fileIds: files.length > 0 ? files.map(f => f.id) : undefined, replyId: props.reply ? props.reply.id : undefined, renoteId: props.renote ? props.renote.id : quoteId ? quoteId : undefined, @@ -548,18 +548,18 @@ async function post() { cw: useCw ? cw || '' : undefined, localOnly: localOnly, visibility: visibility, - visibleUserIds: visibility == 'specified' ? visibleUsers.map(u => u.id) : undefined, + visibleUserIds: visibility === 'specified' ? visibleUsers.map(u => u.id) : undefined, }; if (withHashtags && hashtags && hashtags.trim() !== '') { const hashtags_ = hashtags.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' '); - data.text = data.text ? `${data.text} ${hashtags_}` : hashtags_; + postData.text = postData.text ? `${postData.text} ${hashtags_}` : hashtags_; } // plugin if (notePostInterruptors.length > 0) { for (const interruptor of notePostInterruptors) { - data = await interruptor.handler(JSON.parse(JSON.stringify(data))); + postData = await interruptor.handler(JSON.parse(JSON.stringify(postData))); } } @@ -571,13 +571,13 @@ async function post() { } posting = true; - os.api('notes/create', data, token).then(() => { + os.api('notes/create', postData, token).then(() => { clear(); nextTick(() => { deleteDraft(); emit('posted'); - if (data.text && data.text != '') { - const hashtags_ = mfm.parse(data.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag); + if (postData.text && postData.text !== '') { + const hashtags_ = mfm.parse(postData.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag); const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[]; localStorage.setItem('hashtags', JSON.stringify(unique(hashtags_.concat(history)))); } @@ -661,7 +661,7 @@ onMounted(() => { cw = draft.data.cw; visibility = draft.data.visibility; localOnly = draft.data.localOnly; - files = (draft.data.files || []).filter(e => e); + files = (draft.data.files || []).filter(draftFile => draftFile); if (draft.data.poll) { poll = draft.data.poll; } diff --git a/packages/client/src/components/queue-chart.vue b/packages/client/src/components/queue-chart.vue index 7e0ed58cbd..7bb548cf06 100644 --- a/packages/client/src/components/queue-chart.vue +++ b/packages/client/src/components/queue-chart.vue @@ -222,7 +222,7 @@ export default defineComponent({ return { chartEl, - } + }; }, }); </script> diff --git a/packages/client/src/components/reactions-viewer.reaction.vue b/packages/client/src/components/reactions-viewer.reaction.vue index 7dc079fde6..91a90a6996 100644 --- a/packages/client/src/components/reactions-viewer.reaction.vue +++ b/packages/client/src/components/reactions-viewer.reaction.vue @@ -7,8 +7,8 @@ :class="{ reacted: note.myReaction == reaction, canToggle }" @click="toggleReaction()" > - <XReactionIcon :reaction="reaction" :custom-emojis="note.emojis"/> - <span>{{ count }}</span> + <XReactionIcon class="icon" :reaction="reaction" :custom-emojis="note.emojis"/> + <span class="count">{{ count }}</span> </button> </template> @@ -141,12 +141,16 @@ export default defineComponent({ background: var(--accent); } - > span { + > .count { color: var(--fgOnAccent); } + + > .icon { + filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.5)); + } } - > span { + > .count { font-size: 0.9em; line-height: 32px; margin: 0 0 0 4px; diff --git a/packages/client/src/components/sample.vue b/packages/client/src/components/sample.vue index 65249ff7e9..f80b9c96b7 100644 --- a/packages/client/src/components/sample.vue +++ b/packages/client/src/components/sample.vue @@ -52,7 +52,7 @@ export default defineComponent({ flag: true, radio: 'misskey', mfm: `Hello world! This is an @example mention. BTW you are @${this.$i ? this.$i.username : 'guest'}.\nAlso, here is ${config.url} and [example link](${config.url}). for more details, see https://example.com.\nAs you know #misskey is open-source software.` - } + }; }, methods: { diff --git a/packages/client/src/components/signin-dialog.vue b/packages/client/src/components/signin-dialog.vue index 5c2048e7b0..848b11fada 100644 --- a/packages/client/src/components/signin-dialog.vue +++ b/packages/client/src/components/signin-dialog.vue @@ -2,12 +2,12 @@ <XModalWindow ref="dialog" :width="370" :height="400" - @close="dialog.close()" + @close="onClose" @closed="emit('closed')" > <template #header>{{ $ts.login }}</template> - <MkSignin :auto-set="autoSet" @login="onLogin"/> + <MkSignin :auto-set="autoSet" :message="message" @login="onLogin"/> </XModalWindow> </template> @@ -18,17 +18,25 @@ import MkSignin from './signin.vue'; const props = withDefaults(defineProps<{ autoSet?: boolean; + message?: string, }>(), { autoSet: false, + message: '' }); const emit = defineEmits<{ - (e: 'done'): void; - (e: 'closed'): void; + (ev: 'done'): void; + (ev: 'closed'): void; + (ev: 'cancelled'): void; }>(); const dialog = $ref<InstanceType<typeof XModalWindow>>(); +function onClose() { + emit('cancelled'); + dialog.close(); +} + function onLogin(res) { emit('done', res); dialog.close(); diff --git a/packages/client/src/components/signin.vue b/packages/client/src/components/signin.vue index f640e948ad..b772d1479b 100644 --- a/packages/client/src/components/signin.vue +++ b/packages/client/src/components/signin.vue @@ -1,39 +1,42 @@ <template> <form class="eppvobhk _monolithic_" :class="{ signing, totpLogin }" @submit.prevent="onSubmit"> <div class="auth _section _formRoot"> - <div v-show="withAvatar" class="avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null }"></div> + <div v-show="withAvatar" class="avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null, marginBottom: message ? '1.5em' : null }"></div> + <MkInfo v-if="message"> + {{ message }} + </MkInfo> <div v-if="!totpLogin" class="normal-signin"> - <MkInput v-model="username" class="_formBlock" :placeholder="$ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange"> + <MkInput v-model="username" class="_formBlock" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange"> <template #prefix>@</template> <template #suffix>@{{ host }}</template> </MkInput> - <MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" class="_formBlock" :placeholder="$ts.password" type="password" :with-password-toggle="true" required data-cy-signin-password> + <MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" class="_formBlock" :placeholder="i18n.ts.password" type="password" :with-password-toggle="true" required data-cy-signin-password> <template #prefix><i class="fas fa-lock"></i></template> - <template #caption><button class="_textButton" type="button" @click="resetPassword">{{ $ts.forgotPassword }}</button></template> + <template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template> </MkInput> - <MkButton class="_formBlock" type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? $ts.loggingIn : $ts.login }}</MkButton> + <MkButton class="_formBlock" type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton> </div> <div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }"> <div v-if="user && user.securityKeys" class="twofa-group tap-group"> - <p>{{ $ts.tapSecurityKey }}</p> + <p>{{ i18n.ts.tapSecurityKey }}</p> <MkButton v-if="!queryingKey" @click="queryKey"> - {{ $ts.retry }} + {{ i18n.ts.retry }} </MkButton> </div> <div v-if="user && user.securityKeys" class="or-hr"> - <p class="or-msg">{{ $ts.or }}</p> + <p class="or-msg">{{ i18n.ts.or }}</p> </div> <div class="twofa-group totp-group"> - <p style="margin-bottom:0;">{{ $ts.twoStepAuthentication }}</p> + <p style="margin-bottom:0;">{{ i18n.ts.twoStepAuthentication }}</p> <MkInput v-if="user && user.usePasswordLessLogin" v-model="password" type="password" :with-password-toggle="true" required> - <template #label>{{ $ts.password }}</template> + <template #label>{{ i18n.ts.password }}</template> <template #prefix><i class="fas fa-lock"></i></template> </MkInput> <MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false" required> - <template #label>{{ $ts.token }}</template> + <template #label>{{ i18n.ts.token }}</template> <template #prefix><i class="fas fa-gavel"></i></template> </MkInput> - <MkButton type="submit" :disabled="signing" primary style="margin: 0 auto;">{{ signing ? $ts.loggingIn : $ts.login }}</MkButton> + <MkButton type="submit" :disabled="signing" primary style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton> </div> </div> </div> @@ -45,190 +48,198 @@ </form> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { defineAsyncComponent } from 'vue'; import { toUnicode } from 'punycode/'; import MkButton from '@/components/ui/button.vue'; import MkInput from '@/components/form/input.vue'; -import { apiUrl, host } from '@/config'; +import MkInfo from '@/components/ui/info.vue'; +import { apiUrl, host as configHost } from '@/config'; import { byteify, hexify } from '@/scripts/2fa'; import * as os from '@/os'; import { login } from '@/account'; import { showSuspendedDialog } from '../scripts/show-suspended-dialog'; +import { instance } from '@/instance'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkButton, - MkInput, - }, +let signing = $ref(false); +let user = $ref(null); +let username = $ref(''); +let password = $ref(''); +let token = $ref(''); +let host = $ref(toUnicode(configHost)); +let totpLogin = $ref(false); +let credential = $ref(null); +let challengeData = $ref(null); +let queryingKey = $ref(false); +let hCaptchaResponse = $ref(null); +let reCaptchaResponse = $ref(null); - props: { - withAvatar: { - type: Boolean, - required: false, - default: true - }, - autoSet: { - type: Boolean, - required: false, - default: false, - } - }, +const meta = $computed(() => instance); - emits: ['login'], +const emit = defineEmits<{ + (ev: 'login', v: any): void; +}>(); - data() { - return { - signing: false, - user: null, - username: '', - password: '', - token: '', - apiUrl, - host: toUnicode(host), - totpLogin: false, - credential: null, - challengeData: null, - queryingKey: false, - }; +const props = defineProps({ + withAvatar: { + type: Boolean, + required: false, + default: true }, - - computed: { - meta() { - return this.$instance; - }, + autoSet: { + type: Boolean, + required: false, + default: false, }, + message: { + type: String, + required: false, + default: '' + } +}); - methods: { - onUsernameChange() { - os.api('users/show', { - username: this.username - }).then(user => { - this.user = user; - }, () => { - this.user = null; - }); - }, - - onLogin(res) { - if (this.autoSet) { - return login(res.i); - } else { - return; - } - }, - - queryKey() { - this.queryingKey = true; - return navigator.credentials.get({ - publicKey: { - challenge: byteify(this.challengeData.challenge, 'base64'), - allowCredentials: this.challengeData.securityKeys.map(key => ({ - id: byteify(key.id, 'hex'), - type: 'public-key', - transports: ['usb', 'nfc', 'ble', 'internal'] - })), - timeout: 60 * 1000 - } - }).catch(() => { - this.queryingKey = false; - return Promise.reject(null); - }).then(credential => { - this.queryingKey = false; - this.signing = true; - return os.api('signin', { - username: this.username, - password: this.password, - signature: hexify(credential.response.signature), - authenticatorData: hexify(credential.response.authenticatorData), - clientDataJSON: hexify(credential.response.clientDataJSON), - credentialId: credential.id, - challengeId: this.challengeData.challengeId - }); - }).then(res => { - this.$emit('login', res); - return this.onLogin(res); - }).catch(err => { - if (err === null) return; - os.alert({ - type: 'error', - text: this.$ts.signinFailed - }); - this.signing = false; - }); - }, +function onUsernameChange() { + os.api('users/show', { + username: username + }).then(userResponse => { + user = userResponse; + }, () => { + user = null; + }); +} - onSubmit() { - this.signing = true; - if (!this.totpLogin && this.user && this.user.twoFactorEnabled) { - if (window.PublicKeyCredential && this.user.securityKeys) { - os.api('signin', { - username: this.username, - password: this.password - }).then(res => { - this.totpLogin = true; - this.signing = false; - this.challengeData = res; - return this.queryKey(); - }).catch(this.loginFailed); - } else { - this.totpLogin = true; - this.signing = false; - } - } else { - os.api('signin', { - username: this.username, - password: this.password, - token: this.user && this.user.twoFactorEnabled ? this.token : undefined - }).then(res => { - this.$emit('login', res); - this.onLogin(res); - }).catch(this.loginFailed); - } - }, +function onLogin(res) { + if (props.autoSet) { + return login(res.i); + } +} - loginFailed(err) { - switch (err.id) { - case '6cc579cc-885d-43d8-95c2-b8c7fc963280': { - os.alert({ - type: 'error', - title: this.$ts.loginFailed, - text: this.$ts.noSuchUser - }); - break; - } - case '932c904e-9460-45b7-9ce6-7ed33be7eb2c': { - os.alert({ - type: 'error', - title: this.$ts.loginFailed, - text: this.$ts.incorrectPassword, - }); - break; - } - case 'e03a5f46-d309-4865-9b69-56282d94e1eb': { - showSuspendedDialog(); - break; - } - default: { - os.alert({ - type: 'error', - title: this.$ts.loginFailed, - text: JSON.stringify(err) - }); - } - } +function queryKey() { + queryingKey = true; + return navigator.credentials.get({ + publicKey: { + challenge: byteify(challengeData.challenge, 'base64'), + allowCredentials: challengeData.securityKeys.map(key => ({ + id: byteify(key.id, 'hex'), + type: 'public-key', + transports: ['usb', 'nfc', 'ble', 'internal'] + })), + timeout: 60 * 1000 + } + }).catch(() => { + queryingKey = false; + return Promise.reject(null); + }).then(credential => { + queryingKey = false; + signing = true; + return os.api('signin', { + username, + password, + signature: hexify(credential.response.signature), + authenticatorData: hexify(credential.response.authenticatorData), + clientDataJSON: hexify(credential.response.clientDataJSON), + credentialId: credential.id, + challengeId: challengeData.challengeId, + 'hcaptcha-response': hCaptchaResponse, + 'g-recaptcha-response': reCaptchaResponse, + }); + }).then(res => { + emit('login', res); + return onLogin(res); + }).catch(err => { + if (err === null) return; + os.alert({ + type: 'error', + text: i18n.ts.signinFailed + }); + signing = false; + }); +} - this.challengeData = null; - this.totpLogin = false; - this.signing = false; - }, +function onSubmit() { + signing = true; + console.log('submit'); + if (!totpLogin && user && user.twoFactorEnabled) { + if (window.PublicKeyCredential && user.securityKeys) { + os.api('signin', { + username, + password, + 'hcaptcha-response': hCaptchaResponse, + 'g-recaptcha-response': reCaptchaResponse, + }).then(res => { + totpLogin = true; + signing = false; + challengeData = res; + return queryKey(); + }).catch(loginFailed); + } else { + totpLogin = true; + signing = false; + } + } else { + os.api('signin', { + username, + password, + 'hcaptcha-response': hCaptchaResponse, + 'g-recaptcha-response': reCaptchaResponse, + token: user && user.twoFactorEnabled ? token : undefined + }).then(res => { + emit('login', res); + onLogin(res); + }).catch(loginFailed); + } +} - resetPassword() { - os.popup(import('@/components/forgot-password.vue'), {}, { - }, 'closed'); +function loginFailed(err) { + switch (err.id) { + case '6cc579cc-885d-43d8-95c2-b8c7fc963280': { + os.alert({ + type: 'error', + title: i18n.ts.loginFailed, + text: i18n.ts.noSuchUser + }); + break; + } + case '932c904e-9460-45b7-9ce6-7ed33be7eb2c': { + os.alert({ + type: 'error', + title: i18n.ts.loginFailed, + text: i18n.ts.incorrectPassword, + }); + break; + } + case 'e03a5f46-d309-4865-9b69-56282d94e1eb': { + showSuspendedDialog(); + break; + } + case '22d05606-fbcf-421a-a2db-b32610dcfd1b': { + os.alert({ + type: 'error', + title: i18n.ts.loginFailed, + text: i18n.ts.rateLimitExceeded, + }); + break; + } + default: { + console.log(err); + os.alert({ + type: 'error', + title: i18n.ts.loginFailed, + text: JSON.stringify(err) + }); } } -}); + + challengeData = null; + totpLogin = false; + signing = false; +} + +function resetPassword() { + os.popup(defineAsyncComponent(() => import('@/components/forgot-password.vue')), {}, { + }, 'closed'); +} </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/signup-dialog.vue b/packages/client/src/components/signup-dialog.vue index bda2495ba7..6dad9257a4 100644 --- a/packages/client/src/components/signup-dialog.vue +++ b/packages/client/src/components/signup-dialog.vue @@ -27,8 +27,8 @@ const props = withDefaults(defineProps<{ }); const emit = defineEmits<{ - (e: 'done'): void; - (e: 'closed'): void; + (ev: 'done'): void; + (ev: 'closed'): void; }>(); const dialog = $ref<InstanceType<typeof XModalWindow>>(); diff --git a/packages/client/src/components/signup.vue b/packages/client/src/components/signup.vue index 38a9fd55f1..3f2af306e5 100644 --- a/packages/client/src/components/signup.vue +++ b/packages/client/src/components/signup.vue @@ -1,11 +1,11 @@ <template> -<form class="qlvuhzng _formRoot" :autocomplete="Math.random()" @submit.prevent="onSubmit"> +<form class="qlvuhzng _formRoot" autocomplete="new-password" @submit.prevent="onSubmit"> <template v-if="meta"> - <MkInput v-if="meta.disableRegistration" v-model="invitationCode" class="_formBlock" type="text" :autocomplete="Math.random()" spellcheck="false" required> + <MkInput v-if="meta.disableRegistration" v-model="invitationCode" class="_formBlock" type="text" spellcheck="false" required> <template #label>{{ $ts.invitationCode }}</template> <template #prefix><i class="fas fa-key"></i></template> </MkInput> - <MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required data-cy-signup-username @update:modelValue="onChangeUsername"> + <MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" spellcheck="false" required data-cy-signup-username @update:modelValue="onChangeUsername"> <template #label>{{ $ts.username }} <div v-tooltip:dialog="$ts.usernameInfo" class="_button _help"><i class="far fa-question-circle"></i></div></template> <template #prefix>@</template> <template #suffix>@{{ host }}</template> @@ -19,7 +19,7 @@ <span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooLong }}</span> </template> </MkInput> - <MkInput v-if="meta.emailRequiredForSignup" v-model="email" class="_formBlock" :debounce="true" type="email" :autocomplete="Math.random()" spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail"> + <MkInput v-if="meta.emailRequiredForSignup" v-model="email" class="_formBlock" :debounce="true" type="email" spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail"> <template #label>{{ $ts.emailAddress }} <div v-tooltip:dialog="$ts._signup.emailAddressInfo" class="_button _help"><i class="far fa-question-circle"></i></div></template> <template #prefix><i class="fas fa-envelope"></i></template> <template #caption> @@ -34,7 +34,7 @@ <span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.error }}</span> </template> </MkInput> - <MkInput v-model="password" class="_formBlock" type="password" :autocomplete="Math.random()" required data-cy-signup-password @update:modelValue="onChangePassword"> + <MkInput v-model="password" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password @update:modelValue="onChangePassword"> <template #label>{{ $ts.password }}</template> <template #prefix><i class="fas fa-lock"></i></template> <template #caption> @@ -43,7 +43,7 @@ <span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.strongPassword }}</span> </template> </MkInput> - <MkInput v-model="retypedPassword" class="_formBlock" type="password" :autocomplete="Math.random()" required data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype"> + <MkInput v-model="retypedPassword" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype"> <template #label>{{ $ts.password }} ({{ $ts.retype }})</template> <template #prefix><i class="fas fa-lock"></i></template> <template #caption> @@ -58,8 +58,8 @@ </template> </I18n> </MkSwitch> - <captcha v-if="meta.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="_formBlock captcha" provider="hcaptcha" :sitekey="meta.hcaptchaSiteKey"/> - <captcha v-if="meta.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="_formBlock captcha" provider="recaptcha" :sitekey="meta.recaptchaSiteKey"/> + <MkCaptcha v-if="meta.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="_formBlock captcha" provider="hcaptcha" :sitekey="meta.hcaptchaSiteKey"/> + <MkCaptcha v-if="meta.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="_formBlock captcha" provider="recaptcha" :sitekey="meta.recaptchaSiteKey"/> <MkButton class="_formBlock" type="submit" :disabled="shouldDisableSubmitting" gradate data-cy-signup-submit>{{ $ts.start }}</MkButton> </template> </form> @@ -67,7 +67,7 @@ <script lang="ts"> import { defineComponent, defineAsyncComponent } from 'vue'; -const getPasswordStrength = require('syuilo-password-strength'); +const getPasswordStrength = await import('syuilo-password-strength'); import { toUnicode } from 'punycode/'; import { host, url } from '@/config'; import MkButton from './ui/button.vue'; @@ -81,7 +81,7 @@ export default defineComponent({ MkButton, MkInput, MkSwitch, - captcha: defineAsyncComponent(() => import('./captcha.vue')), + MkCaptcha: defineAsyncComponent(() => import('./captcha.vue')), }, props: { @@ -111,7 +111,7 @@ export default defineComponent({ ToSAgreement: false, hCaptchaResponse: null, reCaptchaResponse: null, - } + }; }, computed: { @@ -124,20 +124,20 @@ export default defineComponent({ this.meta.tosUrl && !this.ToSAgreement || this.meta.enableHcaptcha && !this.hCaptchaResponse || this.meta.enableRecaptcha && !this.reCaptchaResponse || - this.passwordRetypeState == 'not-match'; + this.passwordRetypeState === 'not-match'; }, shouldShowProfileUrl(): boolean { - return (this.username != '' && - this.usernameState != 'invalid-format' && - this.usernameState != 'min-range' && - this.usernameState != 'max-range'); + return (this.username !== '' && + this.usernameState !== 'invalid-format' && + this.usernameState !== 'min-range' && + this.usernameState !== 'max-range'); } }, methods: { onChangeUsername() { - if (this.username == '') { + if (this.username === '') { this.usernameState = null; return; } @@ -165,7 +165,7 @@ export default defineComponent({ }, onChangeEmail() { - if (this.email == '') { + if (this.email === '') { this.emailState = null; return; } @@ -188,7 +188,7 @@ export default defineComponent({ }, onChangePassword() { - if (this.password == '') { + if (this.password === '') { this.passwordStrength = ''; return; } @@ -198,12 +198,12 @@ export default defineComponent({ }, onChangePasswordRetype() { - if (this.retypedPassword == '') { + if (this.retypedPassword === '') { this.passwordRetypeState = null; return; } - this.passwordRetypeState = this.password == this.retypedPassword ? 'match' : 'not-match'; + this.passwordRetypeState = this.password === this.retypedPassword ? 'match' : 'not-match'; }, onSubmit() { diff --git a/packages/client/src/components/timeline.vue b/packages/client/src/components/timeline.vue index 59956b9526..a3fa27ab78 100644 --- a/packages/client/src/components/timeline.vue +++ b/packages/client/src/components/timeline.vue @@ -19,8 +19,8 @@ const props = defineProps<{ }>(); const emit = defineEmits<{ - (e: 'note'): void; - (e: 'queue', count: number): void; + (ev: 'note'): void; + (ev: 'queue', count: number): void; }>(); provide('inChannel', computed(() => props.src === 'channel')); @@ -95,7 +95,7 @@ if (props.src === 'antenna') { visibility: 'specified' }; const onNote = note => { - if (note.visibility == 'specified') { + if (note.visibility === 'specified') { prepend(note); } }; diff --git a/packages/client/src/components/toast.vue b/packages/client/src/components/toast.vue index 99933f3846..c9fad64eb6 100644 --- a/packages/client/src/components/toast.vue +++ b/packages/client/src/components/toast.vue @@ -19,7 +19,7 @@ defineProps<{ }>(); const emit = defineEmits<{ - (e: 'closed'): void; + (ev: 'closed'): void; }>(); const zIndex = os.claimZIndex('high'); diff --git a/packages/client/src/components/ui/button.vue b/packages/client/src/components/ui/button.vue index c7b6c8ba96..e6b20d9881 100644 --- a/packages/client/src/components/ui/button.vue +++ b/packages/client/src/components/ui/button.vue @@ -90,32 +90,32 @@ export default defineComponent({ } }, methods: { - onMousedown(e: MouseEvent) { + onMousedown(evt: MouseEvent) { function distance(p, q) { return Math.hypot(p.x - q.x, p.y - q.y); } function calcCircleScale(boxW, boxH, circleCenterX, circleCenterY) { - const origin = {x: circleCenterX, y: circleCenterY}; - const dist1 = distance({x: 0, y: 0}, origin); - const dist2 = distance({x: boxW, y: 0}, origin); - const dist3 = distance({x: 0, y: boxH}, origin); - const dist4 = distance({x: boxW, y: boxH }, origin); + const origin = { x: circleCenterX, y: circleCenterY }; + const dist1 = distance({ x: 0, y: 0 }, origin); + const dist2 = distance({ x: boxW, y: 0 }, origin); + const dist3 = distance({ x: 0, y: boxH }, origin); + const dist4 = distance({ x: boxW, y: boxH }, origin); return Math.max(dist1, dist2, dist3, dist4) * 2; } - const rect = e.target.getBoundingClientRect(); + const rect = evt.target.getBoundingClientRect(); const ripple = document.createElement('div'); - ripple.style.top = (e.clientY - rect.top - 1).toString() + 'px'; - ripple.style.left = (e.clientX - rect.left - 1).toString() + 'px'; + ripple.style.top = (evt.clientY - rect.top - 1).toString() + 'px'; + ripple.style.left = (evt.clientX - rect.left - 1).toString() + 'px'; this.$refs.ripples.appendChild(ripple); - const circleCenterX = e.clientX - rect.left; - const circleCenterY = e.clientY - rect.top; + const circleCenterX = evt.clientX - rect.left; + const circleCenterY = evt.clientY - rect.top; - const scale = calcCircleScale(e.target.clientWidth, e.target.clientHeight, circleCenterX, circleCenterY); + const scale = calcCircleScale(evt.target.clientWidth, evt.target.clientHeight, circleCenterX, circleCenterY); window.setTimeout(() => { ripple.style.transform = 'scale(' + (scale / 2) + ')'; diff --git a/packages/client/src/components/ui/context-menu.vue b/packages/client/src/components/ui/context-menu.vue index f491b43b46..e637d361cf 100644 --- a/packages/client/src/components/ui/context-menu.vue +++ b/packages/client/src/components/ui/context-menu.vue @@ -19,7 +19,7 @@ const props = defineProps<{ }>(); const emit = defineEmits<{ - (e: 'closed'): void; + (ev: 'closed'): void; }>(); let rootEl = $ref<HTMLDivElement>(); @@ -63,8 +63,8 @@ onBeforeUnmount(() => { } }); -function onMousedown(e: Event) { - if (!contains(rootEl, e.target) && (rootEl != e.target)) emit('closed'); +function onMousedown(evt: Event) { + if (!contains(rootEl, evt.target) && (rootEl !== evt.target)) emit('closed'); } </script> diff --git a/packages/client/src/components/ui/folder.vue b/packages/client/src/components/ui/folder.vue index fe1602b2bb..7daa82cbd3 100644 --- a/packages/client/src/components/ui/folder.vue +++ b/packages/client/src/components/ui/folder.vue @@ -23,7 +23,7 @@ <script lang="ts"> import { defineComponent } from 'vue'; -import * as tinycolor from 'tinycolor2'; +import tinycolor from 'tinycolor2'; const localStoragePrefix = 'ui:folder:'; diff --git a/packages/client/src/components/ui/menu.vue b/packages/client/src/components/ui/menu.vue index a93cc8cda8..dad5dfa8b0 100644 --- a/packages/client/src/components/ui/menu.vue +++ b/packages/client/src/components/ui/menu.vue @@ -1,5 +1,6 @@ <template> -<div ref="itemsEl" v-hotkey="keymap" +<div + ref="itemsEl" v-hotkey="keymap" class="rrevdjwt" :class="{ center: align === 'center', asDrawer }" :style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }" @@ -60,7 +61,7 @@ const props = defineProps<{ }>(); const emit = defineEmits<{ - (e: 'close'): void; + (ev: 'close'): void; }>(); let itemsEl = $ref<HTMLDivElement>(); @@ -162,6 +163,15 @@ function focusDown() { position: relative; } + &:not(:disabled):hover { + color: var(--accent); + text-decoration: none; + + &:before { + background: var(--accentedBg); + } + } + &.danger { color: #ff2a2a; @@ -191,15 +201,6 @@ function focusDown() { } } - &:not(:disabled):hover { - color: var(--accent); - text-decoration: none; - - &:before { - background: var(--accentedBg); - } - } - &:not(:active):focus-visible { box-shadow: 0 0 0 2px var(--focus) inset; } diff --git a/packages/client/src/components/ui/modal-window.vue b/packages/client/src/components/ui/modal-window.vue index b4b8c2b965..d2b2ccff7a 100644 --- a/packages/client/src/components/ui/modal-window.vue +++ b/packages/client/src/components/ui/modal-window.vue @@ -1,7 +1,7 @@ <template> -<MkModal ref="modal" :prefer-type="'dialog'" @click="$emit('click')" @closed="$emit('closed')"> - <div class="ebkgoccj _window _narrow_" :style="{ width: `${width}px`, height: scroll ? (height ? `${height}px` : null) : (height ? `min(${height}px, 100%)` : '100%') }" @keydown="onKeydown"> - <div class="header"> +<MkModal ref="modal" :prefer-type="'dialog'" @click="onBgClick" @closed="$emit('closed')"> + <div ref="rootEl" class="ebkgoccj _window _narrow_" :style="{ width: `${width}px`, height: scroll ? (height ? `${height}px` : null) : (height ? `min(${height}px, 100%)` : '100%') }" @keydown="onKeydown"> + <div ref="headerEl" class="header"> <button v-if="withOkButton" class="_button" @click="$emit('close')"><i class="fas fa-times"></i></button> <span class="title"> <slot name="header"></slot> @@ -11,82 +11,82 @@ </div> <div v-if="padding" class="body"> <div class="_section"> - <slot></slot> + <slot :width="bodyWidth" :height="bodyHeight"></slot> </div> </div> <div v-else class="body"> - <slot></slot> + <slot :width="bodyWidth" :height="bodyHeight"></slot> </div> </div> </MkModal> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { onMounted, onUnmounted } from 'vue'; import MkModal from './modal.vue'; -export default defineComponent({ - components: { - MkModal - }, - props: { - withOkButton: { - type: Boolean, - required: false, - default: false - }, - okButtonDisabled: { - type: Boolean, - required: false, - default: false - }, - padding: { - type: Boolean, - required: false, - default: false - }, - width: { - type: Number, - required: false, - default: 400 - }, - height: { - type: Number, - required: false, - default: null - }, - canClose: { - type: Boolean, - required: false, - default: true, - }, - scroll: { - type: Boolean, - required: false, - default: true, - }, - }, +const props = withDefaults(defineProps<{ + withOkButton: boolean; + okButtonDisabled: boolean; + padding: boolean; + width: number; + height: number | null; + scroll: boolean; +}>(), { + withOkButton: false, + okButtonDisabled: false, + padding: false, + width: 400, + height: null, + scroll: true, +}); - emits: ['click', 'close', 'closed', 'ok'], +const emit = defineEmits<{ + (event: 'click'): void; + (event: 'close'): void; + (event: 'closed'): void; + (event: 'ok'): void; +}>(); - data() { - return { - }; - }, +let modal = $ref<InstanceType<typeof MkModal>>(); +let rootEl = $ref<HTMLElement>(); +let headerEl = $ref<HTMLElement>(); +let bodyWidth = $ref(0); +let bodyHeight = $ref(0); - methods: { - close() { - this.$refs.modal.close(); - }, +const close = () => { + modal.close(); +}; - onKeydown(e) { - if (e.which === 27) { // Esc - e.preventDefault(); - e.stopPropagation(); - this.close(); - } - }, +const onBgClick = () => { + emit('click'); +}; + +const onKeydown = (evt) => { + if (evt.which === 27) { // Esc + evt.preventDefault(); + evt.stopPropagation(); + close(); } +}; + +const ro = new ResizeObserver((entries, observer) => { + bodyWidth = rootEl.offsetWidth; + bodyHeight = rootEl.offsetHeight - headerEl.offsetHeight; +}); + +onMounted(() => { + bodyWidth = rootEl.offsetWidth; + bodyHeight = rootEl.offsetHeight - headerEl.offsetHeight; + ro.observe(rootEl); +}); + +onUnmounted(() => { + ro.disconnect(); +}); + +defineExpose({ + close, }); </script> diff --git a/packages/client/src/components/ui/modal.vue b/packages/client/src/components/ui/modal.vue index 1e4159055e..d6a29ec4b7 100644 --- a/packages/client/src/components/ui/modal.vue +++ b/packages/client/src/components/ui/modal.vue @@ -1,5 +1,5 @@ <template> -<transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="childRendered"> +<transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="onOpened"> <div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }"> <div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div> <div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick"> @@ -48,6 +48,7 @@ const props = withDefaults(defineProps<{ const emit = defineEmits<{ (ev: 'opening'): void; + (ev: 'opened'): void; (ev: 'click'): void; (ev: 'esc'): void; (ev: 'close'): void; @@ -212,7 +213,9 @@ const align = () => { popover.style.top = top + 'px'; }; -const childRendered = () => { +const onOpened = () => { + emit('opened'); + // モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する const el = content.value!.children[0]; el.addEventListener('mousedown', ev => { @@ -234,10 +237,10 @@ onMounted(() => { } fixed.value = (type.value === 'drawer') || (getFixedContainer(props.src) != null); - await nextTick() + await nextTick(); align(); - }, { immediate: true, }); + }, { immediate: true }); nextTick(() => { const popover = content.value; diff --git a/packages/client/src/components/ui/pagination.vue b/packages/client/src/components/ui/pagination.vue index 13f3215671..c081e06acd 100644 --- a/packages/client/src/components/ui/pagination.vue +++ b/packages/client/src/components/ui/pagination.vue @@ -14,8 +14,14 @@ </div> <div v-else ref="rootEl"> + <div v-show="pagination.reversed && more" key="_more_" class="cxiknjgy _gap"> + <MkButton v-if="!moreFetching" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMoreAhead"> + {{ $ts.loadMore }} + </MkButton> + <MkLoading v-else class="loading"/> + </div> <slot :items="items"></slot> - <div v-show="more" key="_more_" class="cxiknjgy _gap"> + <div v-show="!pagination.reversed && more" key="_more_" class="cxiknjgy _gap"> <MkButton v-if="!moreFetching" v-appear="($store.state.enableInfiniteScroll && !disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore"> {{ $ts.loadMore }} </MkButton> @@ -62,7 +68,7 @@ const props = withDefaults(defineProps<{ }); const emit = defineEmits<{ - (e: 'queue', count: number): void; + (ev: 'queue', count: number): void; }>(); type Item = { id: string; [another: string]: unknown; }; @@ -106,7 +112,7 @@ const init = async (): Promise<void> => { offset.value = res.length; error.value = false; fetching.value = false; - }, e => { + }, err => { error.value = true; fetching.value = false; }); @@ -149,7 +155,7 @@ const fetchMore = async (): Promise<void> => { } offset.value += res.length; moreFetching.value = false; - }, e => { + }, err => { moreFetching.value = false; }); }; @@ -177,7 +183,7 @@ const fetchMoreAhead = async (): Promise<void> => { } offset.value += res.length; moreFetching.value = false; - }, e => { + }, err => { moreFetching.value = false; }); }; @@ -244,6 +250,11 @@ const append = (item: Item): void => { items.value.push(item); }; +const removeItem = (finder: (item: Item) => boolean) => { + const i = items.value.findIndex(finder); + items.value.splice(i, 1); +}; + const updateItem = (id: Item['id'], replacer: (old: Item) => Item): void => { const i = items.value.findIndex(item => item.id === id); items.value[i] = replacer(items.value[i]); @@ -270,11 +281,12 @@ onDeactivated(() => { defineExpose({ items, + queue, backed, reload, - fetchMoreAhead, prepend, append, + removeItem, updateItem, }); </script> diff --git a/packages/client/src/components/ui/popup-menu.vue b/packages/client/src/components/ui/popup-menu.vue index 8d6c1b5695..2bc7030d77 100644 --- a/packages/client/src/components/ui/popup-menu.vue +++ b/packages/client/src/components/ui/popup-menu.vue @@ -19,7 +19,7 @@ defineProps<{ }>(); const emit = defineEmits<{ - (e: 'closed'): void; + (ev: 'closed'): void; }>(); let modal = $ref<InstanceType<typeof MkModal>>(); diff --git a/packages/client/src/components/ui/tooltip.vue b/packages/client/src/components/ui/tooltip.vue index ee1909554e..571d11ba3b 100644 --- a/packages/client/src/components/ui/tooltip.vue +++ b/packages/client/src/components/ui/tooltip.vue @@ -63,7 +63,7 @@ const setPosition = () => { } return [left, top]; - } + }; const calcPosWhenBottom = () => { let left: number; @@ -84,7 +84,7 @@ const setPosition = () => { } return [left, top]; - } + }; const calcPosWhenLeft = () => { let left: number; @@ -105,7 +105,7 @@ const setPosition = () => { } return [left, top]; - } + }; const calcPosWhenRight = () => { let left: number; @@ -126,7 +126,7 @@ const setPosition = () => { } return [left, top]; - } + }; const calc = (): { left: number; @@ -172,7 +172,7 @@ const setPosition = () => { } return null as never; - } + }; const { left, top, transformOrigin } = calc(); el.value.style.transformOrigin = transformOrigin; diff --git a/packages/client/src/components/ui/window.vue b/packages/client/src/components/ui/window.vue index fa32ecfdef..2066cf579d 100644 --- a/packages/client/src/components/ui/window.vue +++ b/packages/client/src/components/ui/window.vue @@ -139,10 +139,10 @@ export default defineComponent({ this.showing = false; }, - onKeydown(e) { - if (e.which === 27) { // Esc - e.preventDefault(); - e.stopPropagation(); + onKeydown(evt) { + if (evt.which === 27) { // Esc + evt.preventDefault(); + evt.stopPropagation(); this.close(); } }, @@ -162,15 +162,15 @@ export default defineComponent({ this.top(); }, - onHeaderMousedown(e) { + onHeaderMousedown(evt) { const main = this.$el as any; if (!contains(main, document.activeElement)) main.focus(); const position = main.getBoundingClientRect(); - const clickX = e.touches && e.touches.length > 0 ? e.touches[0].clientX : e.clientX; - const clickY = e.touches && e.touches.length > 0 ? e.touches[0].clientY : e.clientY; + const clickX = evt.touches && evt.touches.length > 0 ? evt.touches[0].clientX : evt.clientX; + const clickY = evt.touches && evt.touches.length > 0 ? evt.touches[0].clientY : evt.clientY; const moveBaseX = clickX - position.left; const moveBaseY = clickY - position.top; const browserWidth = window.innerWidth; @@ -204,10 +204,10 @@ export default defineComponent({ }, // 上ハンドル掴み時 - onTopHandleMousedown(e) { + onTopHandleMousedown(evt) { const main = this.$el as any; - const base = e.clientY; + const base = evt.clientY; const height = parseInt(getComputedStyle(main, '').height, 10); const top = parseInt(getComputedStyle(main, '').top, 10); @@ -230,10 +230,10 @@ export default defineComponent({ }, // 右ハンドル掴み時 - onRightHandleMousedown(e) { + onRightHandleMousedown(evt) { const main = this.$el as any; - const base = e.clientX; + const base = evt.clientX; const width = parseInt(getComputedStyle(main, '').width, 10); const left = parseInt(getComputedStyle(main, '').left, 10); const browserWidth = window.innerWidth; @@ -254,10 +254,10 @@ export default defineComponent({ }, // 下ハンドル掴み時 - onBottomHandleMousedown(e) { + onBottomHandleMousedown(evt) { const main = this.$el as any; - const base = e.clientY; + const base = evt.clientY; const height = parseInt(getComputedStyle(main, '').height, 10); const top = parseInt(getComputedStyle(main, '').top, 10); const browserHeight = window.innerHeight; @@ -278,10 +278,10 @@ export default defineComponent({ }, // 左ハンドル掴み時 - onLeftHandleMousedown(e) { + onLeftHandleMousedown(evt) { const main = this.$el as any; - const base = e.clientX; + const base = evt.clientX; const width = parseInt(getComputedStyle(main, '').width, 10); const left = parseInt(getComputedStyle(main, '').left, 10); @@ -304,27 +304,27 @@ export default defineComponent({ }, // 左上ハンドル掴み時 - onTopLeftHandleMousedown(e) { - this.onTopHandleMousedown(e); - this.onLeftHandleMousedown(e); + onTopLeftHandleMousedown(evt) { + this.onTopHandleMousedown(evt); + this.onLeftHandleMousedown(evt); }, // 右上ハンドル掴み時 - onTopRightHandleMousedown(e) { - this.onTopHandleMousedown(e); - this.onRightHandleMousedown(e); + onTopRightHandleMousedown(evt) { + this.onTopHandleMousedown(evt); + this.onRightHandleMousedown(evt); }, // 右下ハンドル掴み時 - onBottomRightHandleMousedown(e) { - this.onBottomHandleMousedown(e); - this.onRightHandleMousedown(e); + onBottomRightHandleMousedown(evt) { + this.onBottomHandleMousedown(evt); + this.onRightHandleMousedown(evt); }, // 左下ハンドル掴み時 - onBottomLeftHandleMousedown(e) { - this.onBottomHandleMousedown(e); - this.onLeftHandleMousedown(e); + onBottomLeftHandleMousedown(evt) { + this.onBottomHandleMousedown(evt); + this.onLeftHandleMousedown(evt); }, // 高さを適用 diff --git a/packages/client/src/components/url-preview.vue b/packages/client/src/components/url-preview.vue index c7bbd1fbd1..6c593c7b41 100644 --- a/packages/client/src/components/url-preview.vue +++ b/packages/client/src/components/url-preview.vue @@ -90,7 +90,7 @@ fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${requestLang}`).the sitename = info.sitename; fetching = false; player = info.player; - }) + }); }); function adjustTweetHeight(message: any) { diff --git a/packages/client/src/components/user-preview.vue b/packages/client/src/components/user-preview.vue index 51c5330564..f80947f75a 100644 --- a/packages/client/src/components/user-preview.vue +++ b/packages/client/src/components/user-preview.vue @@ -70,7 +70,7 @@ export default defineComponent({ }, mounted() { - if (typeof this.q == 'object') { + if (typeof this.q === 'object') { this.user = this.q; this.fetched = true; } else { diff --git a/packages/client/src/components/user-select-dialog.vue b/packages/client/src/components/user-select-dialog.vue index dbef34d547..b34d21af07 100644 --- a/packages/client/src/components/user-select-dialog.vue +++ b/packages/client/src/components/user-select-dialog.vue @@ -60,9 +60,9 @@ import * as os from '@/os'; import { defaultStore } from '@/store'; const emit = defineEmits<{ - (e: 'ok', selected: misskey.entities.UserDetailed): void; - (e: 'cancel'): void; - (e: 'closed'): void; + (ev: 'ok', selected: misskey.entities.UserDetailed): void; + (ev: 'cancel'): void; + (ev: 'closed'): void; }>(); let username = $ref(''); diff --git a/packages/client/src/components/visibility-picker.vue b/packages/client/src/components/visibility-picker.vue index 4b20063a51..c717c3a461 100644 --- a/packages/client/src/components/visibility-picker.vue +++ b/packages/client/src/components/visibility-picker.vue @@ -57,9 +57,9 @@ const props = withDefaults(defineProps<{ }); const emit = defineEmits<{ - (e: 'changeVisibility', v: typeof misskey.noteVisibilities[number]): void; - (e: 'changeLocalOnly', v: boolean): void; - (e: 'closed'): void; + (ev: 'changeVisibility', v: typeof misskey.noteVisibilities[number]): void; + (ev: 'changeLocalOnly', v: boolean): void; + (ev: 'closed'): void; }>(); let v = $ref(props.currentVisibility); diff --git a/packages/client/src/components/waiting-dialog.vue b/packages/client/src/components/waiting-dialog.vue index 7dfcc55695..9e631b55b1 100644 --- a/packages/client/src/components/waiting-dialog.vue +++ b/packages/client/src/components/waiting-dialog.vue @@ -21,8 +21,8 @@ const props = defineProps<{ }>(); const emit = defineEmits<{ - (e: 'done'); - (e: 'closed'); + (ev: 'done'); + (ev: 'closed'); }>(); function done() { diff --git a/packages/client/src/components/widgets.vue b/packages/client/src/components/widgets.vue index da9d935281..74dd79f733 100644 --- a/packages/client/src/components/widgets.vue +++ b/packages/client/src/components/widgets.vue @@ -2,11 +2,11 @@ <div class="vjoppmmu"> <template v-if="edit"> <header> - <MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--margin)"> + <MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--margin)" class="mk-widget-select"> <template #label>{{ $ts.selectWidget }}</template> <option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ $t(`_widgets.${widget}`) }}</option> </MkSelect> - <MkButton inline primary @click="addWidget"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton> + <MkButton inline primary class="mk-widget-add" @click="addWidget"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton> <MkButton inline @click="$emit('exit')">{{ $ts.close }}</MkButton> </header> <XDraggable @@ -19,7 +19,7 @@ <div class="customize-container"> <button class="config _button" @click.prevent.stop="configWidget(element.id)"><i class="fas fa-cog"></i></button> <button class="remove _button" @click.prevent.stop="removeWidget(element)"><i class="fas fa-times"></i></button> - <component class="handle" :ref="el => widgetRefs[element.id] = el" :is="`mkw-${element.name}`" :widget="element" @updateProps="updateWidget(element.id, $event)"/> + <component :is="`mkw-${element.name}`" :ref="el => widgetRefs[element.id] = el" class="handle" :widget="element" @updateProps="updateWidget(element.id, $event)"/> </div> </template> </XDraggable> @@ -37,7 +37,7 @@ import { widgets as widgetDefs } from '@/widgets'; export default defineComponent({ components: { - XDraggable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)), + XDraggable: defineAsyncComponent(() => import('vuedraggable')), MkSelect, MkButton, }, |