diff options
Diffstat (limited to 'packages/client/src')
259 files changed, 5702 insertions, 6341 deletions
diff --git a/packages/client/src/account.ts b/packages/client/src/account.ts index f4dcab319c..ce4af61f18 100644 --- a/packages/client/src/account.ts +++ b/packages/client/src/account.ts @@ -1,5 +1,5 @@ import { del, get, set } from '@/scripts/idb-proxy'; -import { reactive } from 'vue'; +import { defineAsyncComponent, reactive } from 'vue'; import * as misskey from 'misskey-js'; import { apiUrl } from '@/config'; import { waiting, api, popup, popupMenu, success, alert } from '@/os'; @@ -11,10 +11,10 @@ import { i18n } from './i18n'; type Account = misskey.entities.MeDetailed; -const data = localStorage.getItem('account'); +const accountData = localStorage.getItem('account'); // TODO: 外部からはreadonlyに -export const $i = data ? reactive(JSON.parse(data) as Account) : null; +export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null; export const iAmModerator = $i != null && ($i.isAdmin || $i.isModerator); @@ -52,7 +52,7 @@ export async function signout() { return Promise.all(registrations.map(registration => registration.unregister())); }); } - } catch (e) {} + } catch (err) {} //#endregion document.cookie = `igi=; path=/`; @@ -104,8 +104,8 @@ function fetchAccount(token: string): Promise<Account> { }); } -export function updateAccount(data) { - for (const [key, value] of Object.entries(data)) { +export function updateAccount(accountData) { + for (const [key, value] of Object.entries(accountData)) { $i[key] = value; } localStorage.setItem('account', JSON.stringify($i)); @@ -141,7 +141,7 @@ export async function openAccountMenu(opts: { onChoose?: (account: misskey.entities.UserDetailed) => void; }, ev: MouseEvent) { function showSigninDialog() { - popup(import('@/components/signin-dialog.vue'), {}, { + popup(defineAsyncComponent(() => import('@/components/signin-dialog.vue')), {}, { done: res => { addAccount(res.id, res.i); success(); @@ -150,7 +150,7 @@ export async function openAccountMenu(opts: { } function createAccount() { - popup(import('@/components/signup-dialog.vue'), {}, { + popup(defineAsyncComponent(() => import('@/components/signup-dialog.vue')), {}, { done: res => { addAccount(res.id, res.i); switchAccountWithToken(res.i); 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, }, diff --git a/packages/client/src/directives/adaptive-border.ts b/packages/client/src/directives/adaptive-border.ts index fc426ca2cc..619c9f0b6d 100644 --- a/packages/client/src/directives/adaptive-border.ts +++ b/packages/client/src/directives/adaptive-border.ts @@ -9,7 +9,7 @@ export default { } else { return el.parentElement ? getBgColor(el.parentElement) : 'transparent'; } - } + }; const parentBg = getBgColor(src.parentElement); diff --git a/packages/client/src/directives/get-size.ts b/packages/client/src/directives/get-size.ts index 1fcd0718dc..2c4e9c188d 100644 --- a/packages/client/src/directives/get-size.ts +++ b/packages/client/src/directives/get-size.ts @@ -25,12 +25,12 @@ function calc(src: Element) { return; } if (info.intersection) { - info.intersection.disconnect() + info.intersection.disconnect(); delete info.intersection; - }; + } info.fn(width, height); -}; +} export default { mounted(src, binding, vn) { diff --git a/packages/client/src/directives/panel.ts b/packages/client/src/directives/panel.ts index 5f9158db2e..d31dc41ed4 100644 --- a/packages/client/src/directives/panel.ts +++ b/packages/client/src/directives/panel.ts @@ -9,7 +9,7 @@ export default { } else { return el.parentElement ? getBgColor(el.parentElement) : 'transparent'; } - } + }; const parentBg = getBgColor(src.parentElement); diff --git a/packages/client/src/directives/size.ts b/packages/client/src/directives/size.ts index 36f649f180..51855e0de5 100644 --- a/packages/client/src/directives/size.ts +++ b/packages/client/src/directives/size.ts @@ -60,9 +60,9 @@ function calc(el: Element) { return; } if (info.intersection) { - info.intersection.disconnect() + info.intersection.disconnect(); delete info.intersection; - }; + } mountings.set(el, Object.assign(info, { previousWidth: width })); diff --git a/packages/client/src/directives/tooltip.ts b/packages/client/src/directives/tooltip.ts index dd715227a4..0e69da954e 100644 --- a/packages/client/src/directives/tooltip.ts +++ b/packages/client/src/directives/tooltip.ts @@ -1,7 +1,7 @@ // TODO: useTooltip関数使うようにしたい // ただディレクティブ内でonUnmountedなどのcomposition api使えるのか不明 -import { Directive, ref } from 'vue'; +import { defineAsyncComponent, Directive, ref } from 'vue'; import { isTouchUsing } from '@/scripts/touch'; import { popup, alert } from '@/os'; @@ -45,7 +45,7 @@ export default { if (self.text == null) return; const showing = ref(true); - popup(import('@/components/ui/tooltip.vue'), { + popup(defineAsyncComponent(() => import('@/components/ui/tooltip.vue')), { showing, text: self.text, targetElement: el, diff --git a/packages/client/src/directives/user-preview.ts b/packages/client/src/directives/user-preview.ts index cdd2afa194..9d18a69877 100644 --- a/packages/client/src/directives/user-preview.ts +++ b/packages/client/src/directives/user-preview.ts @@ -1,4 +1,4 @@ -import { Directive, ref } from 'vue'; +import { defineAsyncComponent, Directive, ref } from 'vue'; import autobind from 'autobind-decorator'; import { popup } from '@/os'; @@ -24,7 +24,7 @@ export class UserPreview { const showing = ref(true); - popup(import('@/components/user-preview.vue'), { + popup(defineAsyncComponent(() => import('@/components/user-preview.vue')), { showing, q: this.user, source: this.el diff --git a/packages/client/src/emojilist.json b/packages/client/src/emojilist.json index 75c424ab4b..402e82e33b 100644 --- a/packages/client/src/emojilist.json +++ b/packages/client/src/emojilist.json @@ -96,6 +96,13 @@ { "category": "face", "char": "\uD83D\uDE36\u200D\uD83C\uDF2B\uFE0F", "name": "face_in_clouds", "keywords": [] }, { "category": "face", "char": "\uD83D\uDE2E\u200D\uD83D\uDCA8", "name": "face_exhaling", "keywords": [] }, { "category": "face", "char": "\uD83D\uDE35\u200D\uD83D\uDCAB", "name": "face_with_spiral_eyes", "keywords": [] }, + { "category": "face", "char": "\uD83E\uDEE0", "name": "melting_face", "keywords": ["disappear", "dissolve", "liquid", "melt", "toketa"] }, + { "category": "face", "char": "\uD83E\uDEE2", "name": "face_with_open_eyes_and_hand_over_mouth", "keywords": ["amazement", "awe", "disbelief", "embarrass", "scared", "surprise", "ohoho"] }, + { "category": "face", "char": "\uD83E\uDEE3", "name": "face_with_peeking_eye", "keywords": ["captivated", "peep", "stare", "chunibyo"] }, + { "category": "face", "char": "\uD83E\uDEE1", "name": "saluting_face", "keywords": ["ok", "salute", "sunny", "troops", "yes", "raja"] }, + { "category": "face", "char": "\uD83E\uDEE5", "name": "dotted_line_face", "keywords": ["depressed", "disappear", "hide", "introvert", "invisible", "tensen"] }, + { "category": "face", "char": "\uD83E\uDEE4", "name": "face_with_diagonal_mouth", "keywords": ["disappointed", "meh", "skeptical", "unsure"] }, + { "category": "face", "char": "\uD83E\uDD79", "name": "face_holding_back_tears", "keywords": ["angry", "cry", "proud", "resist", "sad"] }, { "category": "face", "char": "💩", "name": "poop", "keywords": ["hankey", "shitface", "fail", "turd", "shit"] }, { "category": "face", "char": "😈", "name": "smiling_imp", "keywords": ["devil", "horns"] }, { "category": "face", "char": "👿", "name": "imp", "keywords": ["devil", "angry", "horns"] }, @@ -149,11 +156,19 @@ { "category": "people", "char": "🤞", "name": "crossed_fingers", "keywords": ["good", "lucky"] }, { "category": "people", "char": "🖖", "name": "vulcan_salute", "keywords": ["hand", "fingers", "spock", "star trek"] }, { "category": "people", "char": "✍", "name": "writing_hand", "keywords": ["lower_left_ballpoint_pen", "stationery", "write", "compose"] }, + { "category": "people", "char": "\uD83E\uDEF0", "name": "hand_with_index_finger_and_thumb_crossed", "keywords": [] }, + { "category": "people", "char": "\uD83E\uDEF1", "name": "rightwards_hand", "keywords": [] }, + { "category": "people", "char": "\uD83E\uDEF2", "name": "leftwards_hand", "keywords": [] }, + { "category": "people", "char": "\uD83E\uDEF3", "name": "palm_down_hand", "keywords": [] }, + { "category": "people", "char": "\uD83E\uDEF4", "name": "palm_up_hand", "keywords": [] }, + { "category": "people", "char": "\uD83E\uDEF5", "name": "index_pointing_at_the_viewer", "keywords": [] }, + { "category": "people", "char": "\uD83E\uDEF6", "name": "heart_hands", "keywords": ["moemoekyun"] }, { "category": "people", "char": "🤏", "name": "pinching_hand", "keywords": ["hand", "fingers"] }, { "category": "people", "char": "🤌", "name": "pinched_fingers", "keywords": ["hand", "fingers"] }, { "category": "people", "char": "🤳", "name": "selfie", "keywords": ["camera", "phone"] }, { "category": "people", "char": "💅", "name": "nail_care", "keywords": ["beauty", "manicure", "finger", "fashion", "nail"] }, { "category": "people", "char": "👄", "name": "lips", "keywords": ["mouth", "kiss"] }, + { "category": "people", "char": "\uD83E\uDEE6", "name": "biting_lip", "keywords": [] }, { "category": "people", "char": "🦷", "name": "tooth", "keywords": ["teeth", "dentist"] }, { "category": "people", "char": "👅", "name": "tongue", "keywords": ["mouth", "playful"] }, { "category": "people", "char": "👂", "name": "ear", "keywords": ["face", "hear", "sound", "listen"] }, @@ -275,7 +290,11 @@ { "category": "people", "char": "🧚♀️", "name": "woman_fairy", "keywords": ["woman", "female"] }, { "category": "people", "char": "🧚♂️", "name": "man_fairy", "keywords": ["man", "male"] }, { "category": "people", "char": "👼", "name": "angel", "keywords": ["heaven", "wings", "halo"] }, + { "category": "people", "char": "\uD83E\uDDCC", "name": "troll", "keywords": [] }, { "category": "people", "char": "🤰", "name": "pregnant_woman", "keywords": ["baby"] }, + { "category": "people", "char": "\uD83E\uDEC3", "name": "pregnant_man", "keywords": [] }, + { "category": "people", "char": "\uD83E\uDEC4", "name": "pregnant_person", "keywords": [] }, + { "category": "people", "char": "\uD83E\uDEC5", "name": "person_with_crown", "keywords": [] }, { "category": "people", "char": "🤱", "name": "breastfeeding", "keywords": ["nursing", "baby"] }, { "category": "people", "char": "\uD83D\uDC69\u200D\uD83C\uDF7C", "name": "woman_feeding_baby", "keywords": [] }, { "category": "people", "char": "\uD83D\uDC68\u200D\uD83C\uDF7C", "name": "man_feeding_baby", "keywords": [] }, @@ -459,7 +478,7 @@ { "category": "animals_and_nature", "char": "🐛", "name": "bug", "keywords": ["animal", "insect", "nature", "worm"] }, { "category": "animals_and_nature", "char": "🦋", "name": "butterfly", "keywords": ["animal", "insect", "nature", "caterpillar"] }, { "category": "animals_and_nature", "char": "🐌", "name": "snail", "keywords": ["slow", "animal", "shell"] }, - { "category": "animals_and_nature", "char": "🐞", "name": "beetle", "keywords": ["animal", "insect", "nature", "ladybug"] }, + { "category": "animals_and_nature", "char": "🐞", "name": "lady_beetle", "keywords": ["animal", "insect", "nature", "ladybug"] }, { "category": "animals_and_nature", "char": "🐜", "name": "ant", "keywords": ["animal", "insect", "nature", "bug"] }, { "category": "animals_and_nature", "char": "🦗", "name": "grasshopper", "keywords": ["animal", "cricket", "chirp"] }, { "category": "animals_and_nature", "char": "🕷", "name": "spider", "keywords": ["animal", "arachnid"] }, @@ -615,6 +634,10 @@ { "category": "animals_and_nature", "char": "💧", "name": "droplet", "keywords": ["water", "drip", "faucet", "spring"] }, { "category": "animals_and_nature", "char": "💦", "name": "sweat_drops", "keywords": ["water", "drip", "oops"] }, { "category": "animals_and_nature", "char": "🌊", "name": "ocean", "keywords": ["sea", "water", "wave", "nature", "tsunami", "disaster"] }, + { "category": "animals_and_nature", "char": "\uD83E\uDEB7", "name": "lotus", "keywords": [] }, + { "category": "animals_and_nature", "char": "\uD83E\uDEB8", "name": "coral", "keywords": [] }, + { "category": "animals_and_nature", "char": "\uD83E\uDEB9", "name": "empty_nest", "keywords": [] }, + { "category": "animals_and_nature", "char": "\uD83E\uDEBA", "name": "nest_with_eggs", "keywords": [] }, { "category": "food_and_drink", "char": "🍏", "name": "green_apple", "keywords": ["fruit", "nature"] }, { "category": "food_and_drink", "char": "🍎", "name": "apple", "keywords": ["fruit", "mac", "school"] }, { "category": "food_and_drink", "char": "🍐", "name": "pear", "keywords": ["fruit", "nature", "food"] }, @@ -737,6 +760,9 @@ { "category": "food_and_drink", "char": "🥣", "name": "bowl_with_spoon", "keywords": ["food", "breakfast", "cereal", "oatmeal", "porridge"] }, { "category": "food_and_drink", "char": "🥡", "name": "takeout_box", "keywords": ["food", "leftovers"] }, { "category": "food_and_drink", "char": "🥢", "name": "chopsticks", "keywords": ["food"] }, + { "category": "food_and_drink", "char": "\uD83E\uDED7", "name": "pouring_liquid", "keywords": [] }, + { "category": "food_and_drink", "char": "\uD83E\uDED8", "name": "beans", "keywords": [] }, + { "category": "food_and_drink", "char": "\uD83E\uDED9", "name": "jar", "keywords": [] }, { "category": "activity", "char": "⚽", "name": "soccer", "keywords": ["sports", "football"] }, { "category": "activity", "char": "🏀", "name": "basketball", "keywords": ["sports", "balls", "NBA"] }, { "category": "activity", "char": "🏈", "name": "football", "keywords": ["sports", "balls", "NFL"] }, @@ -844,6 +870,8 @@ { "category": "activity", "char": "🪄", "name": "magic_wand", "keywords": [] }, { "category": "activity", "char": "🪅", "name": "pinata", "keywords": [] }, { "category": "activity", "char": "🪆", "name": "nesting_dolls", "keywords": [] }, + { "category": "activity", "char": "\uD83E\uDEAC", "name": "hamsa", "keywords": [] }, + { "category": "activity", "char": "\uD83E\uDEA9", "name": "mirror_ball", "keywords": [] }, { "category": "travel_and_places", "char": "🚗", "name": "red_car", "keywords": ["red", "transportation", "vehicle"] }, { "category": "travel_and_places", "char": "🚕", "name": "taxi", "keywords": ["uber", "vehicle", "cars", "transportation"] }, { "category": "travel_and_places", "char": "🚙", "name": "blue_car", "keywords": ["transportation", "vehicle"] }, @@ -971,11 +999,12 @@ { "category": "travel_and_places", "char": "🕋", "name": "kaaba", "keywords": ["mecca", "mosque", "islam"] }, { "category": "travel_and_places", "char": "⛩", "name": "shinto_shrine", "keywords": ["temple", "japan", "kyoto"] }, { "category": "travel_and_places", "char": "🛕", "name": "hindu_temple", "keywords": ["temple"] }, - { "category": "travel_and_places", "char": "🪨", "name": "rock", "keywords": [] }, { "category": "travel_and_places", "char": "🪵", "name": "wood", "keywords": [] }, { "category": "travel_and_places", "char": "🛖", "name": "hut", "keywords": [] }, - + { "category": "travel_and_places", "char": "\uD83D\uDEDD", "name": "playground_slide", "keywords": [] }, + { "category": "travel_and_places", "char": "\uD83D\uDEDE", "name": "wheel", "keywords": [] }, + { "category": "travel_and_places", "char": "\uD83D\uDEDF", "name": "ring_buoy", "keywords": [] }, { "category": "objects", "char": "⌚", "name": "watch", "keywords": ["time", "accessories"] }, { "category": "objects", "char": "📱", "name": "iphone", "keywords": ["technology", "apple", "gadgets", "dial"] }, { "category": "objects", "char": "📲", "name": "calling", "keywords": ["iphone", "incoming"] }, @@ -1016,6 +1045,7 @@ { "category": "objects", "char": "⌛", "name": "hourglass", "keywords": ["time", "clock", "oldschool", "limit", "exam", "quiz", "test"] }, { "category": "objects", "char": "📡", "name": "satellite", "keywords": ["communication", "future", "radio", "space"] }, { "category": "objects", "char": "🔋", "name": "battery", "keywords": ["power", "energy", "sustain"] }, + { "category": "objects", "char": "\uD83E\uDEAB", "name": "battery", "keywords": [] }, { "category": "objects", "char": "🔌", "name": "electric_plug", "keywords": ["charger", "power"] }, { "category": "objects", "char": "💡", "name": "bulb", "keywords": ["light", "electricity", "idea"] }, { "category": "objects", "char": "🔦", "name": "flashlight", "keywords": ["dark", "camping", "sight", "night"] }, @@ -1031,6 +1061,7 @@ { "category": "objects", "char": "💰", "name": "moneybag", "keywords": ["dollar", "payment", "coins", "sale"] }, { "category": "objects", "char": "🪙", "name": "coin", "keywords": ["dollar", "payment", "coins", "sale"] }, { "category": "objects", "char": "💳", "name": "credit_card", "keywords": ["money", "sales", "dollar", "bill", "payment", "shopping"] }, + { "category": "objects", "char": "\uD83E\uDEAB", "name": "identification_card", "keywords": [] }, { "category": "objects", "char": "💎", "name": "gem", "keywords": ["blue", "ruby", "diamond", "jewelry"] }, { "category": "objects", "char": "⚖", "name": "balance_scale", "keywords": ["law", "fairness", "weight"] }, { "category": "objects", "char": "🧰", "name": "toolbox", "keywords": ["tools", "diy", "fix", "maintainer", "mechanic"] }, @@ -1077,6 +1108,8 @@ { "category": "objects", "char": "🩹", "name": "adhesive_bandage", "keywords": ["health", "hospital", "medicine", "needle", "doctor", "nurse"] }, { "category": "objects", "char": "🩺", "name": "stethoscope", "keywords": ["health", "hospital", "medicine", "needle", "doctor", "nurse"] }, { "category": "objects", "char": "🪒", "name": "razor", "keywords": ["health"] }, + { "category": "objects", "char": "\uD83E\uDE7B", "name": "xray", "keywords": [] }, + { "category": "objects", "char": "\uD83E\uDE7C", "name": "crutch", "keywords": [] }, { "category": "objects", "char": "🧬", "name": "dna", "keywords": ["biologist", "genetics", "life"] }, { "category": "objects", "char": "🧫", "name": "petri_dish", "keywords": ["bacteria", "biology", "culture", "lab"] }, { "category": "objects", "char": "🧪", "name": "test_tube", "keywords": ["chemistry", "experiment", "lab", "science"] }, @@ -1111,6 +1144,7 @@ { "category": "objects", "char": "🪤", "name": "mouse_trap", "keywords": ["household"] }, { "category": "objects", "char": "🪣", "name": "bucket", "keywords": ["household"] }, { "category": "objects", "char": "🪥", "name": "toothbrush", "keywords": ["household"] }, + { "category": "objects", "char": "\uD83E\uDEE7", "name": "bubbles", "keywords": [] }, { "category": "objects", "char": "⛱", "name": "parasol_on_ground", "keywords": ["weather", "summer"] }, { "category": "objects", "char": "🗿", "name": "moyai", "keywords": ["rock", "easter island", "moai"] }, { "category": "objects", "char": "🛍", "name": "shopping", "keywords": ["mall", "buy", "purchase"] }, @@ -1404,6 +1438,7 @@ { "category": "symbols", "char": "➖", "name": "heavy_minus_sign", "keywords": ["math", "calculation", "subtract", "less"] }, { "category": "symbols", "char": "➗", "name": "heavy_division_sign", "keywords": ["divide", "math", "calculation"] }, { "category": "symbols", "char": "✖️", "name": "heavy_multiplication_x", "keywords": ["math", "calculation"] }, + { "category": "symbols", "char": "\uD83D\uDFF0", "name": "heavy_equals_sign", "keywords": [] }, { "category": "symbols", "char": "♾", "name": "infinity", "keywords": ["forever"] }, { "category": "symbols", "char": "💲", "name": "heavy_dollar_sign", "keywords": ["money", "sales", "payment", "currency", "buck"] }, { "category": "symbols", "char": "💱", "name": "currency_exchange", "keywords": ["money", "sales", "dollar", "travel"] }, @@ -1747,3 +1782,4 @@ { "category": "flags", "char": "🇺🇳", "name": "united_nations", "keywords": ["un", "flag", "banner"] }, { "category": "flags", "char": "🏴☠️", "name": "pirate_flag", "keywords": ["skull", "crossbones", "flag", "banner"] } ] + diff --git a/packages/client/src/filters/bytes.ts b/packages/client/src/filters/bytes.ts index 50e63534b6..c80f2f0ed2 100644 --- a/packages/client/src/filters/bytes.ts +++ b/packages/client/src/filters/bytes.ts @@ -1,7 +1,7 @@ export default (v, digits = 0) => { if (v == null) return '?'; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; - if (v == 0) return '0'; + if (v === 0) return '0'; const isMinus = v < 0; if (isMinus) v = -v; const i = Math.floor(Math.log(v) / Math.log(1024)); diff --git a/packages/client/src/init.ts b/packages/client/src/init.ts index ab3299d22b..bb6176e409 100644 --- a/packages/client/src/init.ts +++ b/packages/client/src/init.ts @@ -13,9 +13,9 @@ if (localStorage.getItem('accounts') != null) { } //#endregion -import { computed, createApp, watch, markRaw, version as vueVersion } from 'vue'; +import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent } from 'vue'; import compareVersions from 'compare-versions'; -import * as JSON5 from 'json5'; +import JSON5 from 'json5'; import widgets from '@/widgets'; import directives from '@/directives'; @@ -146,8 +146,7 @@ if ($i && $i.token) { try { document.body.innerHTML = '<div>Please wait...</div>'; await login(i); - location.reload(); - } catch (e) { + } catch (err) { // Render the error screen // TODO: ちゃんとしたコンポーネントをレンダリングする(v10とかのトラブルシューティングゲーム付きのやつみたいな) document.body.innerHTML = '<div id="err">Oops!</div>'; @@ -169,14 +168,14 @@ fetchInstanceMetaPromise.then(() => { initializeSw(); }); -const app = createApp(await ( - window.location.search === '?zen' ? import('@/ui/zen.vue') : - !$i ? import('@/ui/visitor.vue') : - ui === 'deck' ? import('@/ui/deck.vue') : - ui === 'desktop' ? import('@/ui/desktop.vue') : - ui === 'classic' ? import('@/ui/classic.vue') : - import('@/ui/universal.vue') -).then(x => x.default)); +const app = createApp( + window.location.search === '?zen' ? defineAsyncComponent(() => import('@/ui/zen.vue')) : + !$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) : + ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) : + ui === 'desktop' ? defineAsyncComponent(() => import('@/ui/desktop.vue')) : + ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) : + defineAsyncComponent(() => import('@/ui/universal.vue')) +); if (_DEV_) { app.config.performance = true; @@ -204,8 +203,24 @@ if (splash) splash.addEventListener('transitionend', () => { splash.remove(); }); -const rootEl = document.createElement('div'); -document.body.appendChild(rootEl); +// https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210 +// なぜかinit.tsの内容が2回実行されることがあるため、mountするdivを1つに制限する +const rootEl = (() => { + const MISSKEY_MOUNT_DIV_ID = 'misskey_app'; + + const currentEl = document.getElementById(MISSKEY_MOUNT_DIV_ID); + + if (currentEl) { + console.warn('multiple import detected'); + return currentEl; + } + + const rootEl = document.createElement('div'); + rootEl.id = MISSKEY_MOUNT_DIV_ID; + document.body.appendChild(rootEl); + return rootEl; +})(); + app.mount(rootEl); // boot.jsのやつを解除 @@ -231,10 +246,10 @@ if (lastVersion !== version) { if (lastVersion != null && compareVersions(version, lastVersion) === 1) { // ログインしてる場合だけ if ($i) { - popup(import('@/components/updated.vue'), {}, {}, 'closed'); + popup(defineAsyncComponent(() => import('@/components/updated.vue')), {}, {}, 'closed'); } } - } catch (e) { + } catch (err) { } } @@ -319,7 +334,7 @@ stream.on('_disconnected_', async () => { } }); -stream.on('emojiAdded', data => { +stream.on('emojiAdded', emojiData => { // TODO //store.commit('instance/set', ); }); diff --git a/packages/client/src/instance.ts b/packages/client/src/instance.ts index 6e912aa2e5..d24eb2419a 100644 --- a/packages/client/src/instance.ts +++ b/packages/client/src/instance.ts @@ -4,11 +4,11 @@ import { api } from './os'; // TODO: 他のタブと永続化されたstateを同期 -const data = localStorage.getItem('instance'); +const instanceData = localStorage.getItem('instance'); // TODO: instanceをリアクティブにするかは再考の余地あり -export const instance: Misskey.entities.InstanceMetadata = reactive(data ? JSON.parse(data) : { +export const instance: Misskey.entities.InstanceMetadata = reactive(instanceData ? JSON.parse(instanceData) : { // TODO: set default values }); diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts index 43c110555f..14860465fa 100644 --- a/packages/client/src/os.ts +++ b/packages/client/src/os.ts @@ -1,6 +1,6 @@ // TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する -import { Component, defineAsyncComponent, markRaw, reactive, Ref, ref } from 'vue'; +import { Component, markRaw, Ref, ref, defineAsyncComponent } from 'vue'; import { EventEmitter } from 'eventemitter3'; import insertTextAtCursor from 'insert-text-at-cursor'; import * as Misskey from 'misskey-js'; @@ -10,7 +10,6 @@ import MkWaitingDialog from '@/components/waiting-dialog.vue'; import { MenuItem } from '@/types/menu'; import { resolve } from '@/router'; import { $i } from '@/account'; -import { defaultStore } from '@/store'; export const pendingApiRequestsCount = ref(0); @@ -35,7 +34,7 @@ export const api = ((endpoint: string, data: Record<string, any> = {}, token?: s method: 'POST', body: JSON.stringify(data), credentials: 'omit', - cache: 'no-cache' + cache: 'no-cache', }).then(async (res) => { const body = res.status === 204 ? null : await res.json(); @@ -60,10 +59,10 @@ export const apiWithDialog = (( token?: string | null | undefined, ) => { const promise = api(endpoint, data, token); - promiseDialog(promise, null, (e) => { + promiseDialog(promise, null, (err) => { alert({ type: 'error', - text: e.message + '\n' + (e as any).id, + text: err.message + '\n' + (err as any).id, }); }); @@ -73,7 +72,7 @@ export const apiWithDialog = (( export function promiseDialog<T extends Promise<any>>( promise: T, onSuccess?: ((res: any) => void) | null, - onFailure?: ((e: Error) => void) | null, + onFailure?: ((err: Error) => void) | null, text?: string, ): T { const showing = ref(true); @@ -89,14 +88,14 @@ export function promiseDialog<T extends Promise<any>>( showing.value = false; }, 1000); } - }).catch(e => { + }).catch(err => { showing.value = false; if (onFailure) { - onFailure(e); + onFailure(err); } else { alert({ type: 'error', - text: e + text: err, }); } }); @@ -111,10 +110,6 @@ export function promiseDialog<T extends Promise<any>>( return promise; } -function isModule(x: any): x is typeof import('*.vue') { - return x.default != null; -} - let popupIdCount = 0; export const popups = ref([]) as Ref<{ id: any; @@ -132,10 +127,7 @@ export function claimZIndex(priority: 'low' | 'middle' | 'high' = 'low'): number return zIndexes[priority]; } -export async function popup(component: Component | typeof import('*.vue') | Promise<Component | typeof import('*.vue')>, props: Record<string, any>, events = {}, disposeEvent?: string) { - if (component.then) component = await component; - - if (isModule(component)) component = component.default; +export async function popup(component: Component, props: Record<string, any>, events = {}, disposeEvent?: string) { markRaw(component); const id = ++popupIdCount; @@ -150,7 +142,7 @@ export async function popup(component: Component | typeof import('*.vue') | Prom props, events: disposeEvent ? { ...events, - [disposeEvent]: dispose + [disposeEvent]: dispose, } : events, id, }; @@ -164,7 +156,7 @@ export async function popup(component: Component | typeof import('*.vue') | Prom export function pageWindow(path: string) { const { component, props } = resolve(path); - popup(import('@/components/page-window.vue'), { + popup(defineAsyncComponent(() => import('@/components/page-window.vue')), { initialPath: path, initialComponent: markRaw(component), initialProps: props, @@ -173,7 +165,7 @@ export function pageWindow(path: string) { export function modalPageWindow(path: string) { const { component, props } = resolve(path); - popup(import('@/components/modal-page-window.vue'), { + popup(defineAsyncComponent(() => import('@/components/modal-page-window.vue')), { initialPath: path, initialComponent: markRaw(component), initialProps: props, @@ -181,8 +173,8 @@ export function modalPageWindow(path: string) { } export function toast(message: string) { - popup(import('@/components/toast.vue'), { - message + popup(defineAsyncComponent(() => import('@/components/toast.vue')), { + message, }, {}, 'closed'); } @@ -192,7 +184,7 @@ export function alert(props: { text?: string | null; }): Promise<void> { return new Promise((resolve, reject) => { - popup(import('@/components/dialog.vue'), props, { + popup(defineAsyncComponent(() => import('@/components/dialog.vue')), props, { done: result => { resolve(); }, @@ -206,7 +198,7 @@ export function confirm(props: { text?: string | null; }): Promise<{ canceled: boolean }> { return new Promise((resolve, reject) => { - popup(import('@/components/dialog.vue'), { + popup(defineAsyncComponent(() => import('@/components/dialog.vue')), { ...props, showCancelButton: true, }, { @@ -227,14 +219,14 @@ export function inputText(props: { canceled: false; result: string; }> { return new Promise((resolve, reject) => { - popup(import('@/components/dialog.vue'), { + popup(defineAsyncComponent(() => import('@/components/dialog.vue')), { title: props.title, text: props.text, input: { type: props.type, placeholder: props.placeholder, default: props.default, - } + }, }, { done: result => { resolve(result ? result : { canceled: true }); @@ -252,14 +244,14 @@ export function inputNumber(props: { canceled: false; result: number; }> { return new Promise((resolve, reject) => { - popup(import('@/components/dialog.vue'), { + popup(defineAsyncComponent(() => import('@/components/dialog.vue')), { title: props.title, text: props.text, input: { type: 'number', placeholder: props.placeholder, default: props.default, - } + }, }, { done: result => { resolve(result ? result : { canceled: true }); @@ -277,14 +269,14 @@ export function inputDate(props: { canceled: false; result: Date; }> { return new Promise((resolve, reject) => { - popup(import('@/components/dialog.vue'), { + popup(defineAsyncComponent(() => import('@/components/dialog.vue')), { title: props.title, text: props.text, input: { type: 'date', placeholder: props.placeholder, default: props.default, - } + }, }, { done: result => { resolve(result ? { result: new Date(result.result), canceled: false } : { canceled: true }); @@ -293,7 +285,7 @@ export function inputDate(props: { }); } -export function select<C extends any = any>(props: { +export function select<C = any>(props: { title?: string | null; text?: string | null; default?: string | null; @@ -314,14 +306,14 @@ export function select<C extends any = any>(props: { canceled: false; result: C; }> { return new Promise((resolve, reject) => { - popup(import('@/components/dialog.vue'), { + popup(defineAsyncComponent(() => import('@/components/dialog.vue')), { title: props.title, text: props.text, select: { items: props.items, groupedItems: props.groupedItems, default: props.default, - } + }, }, { done: result => { resolve(result ? result : { canceled: true }); @@ -336,9 +328,9 @@ export function success() { window.setTimeout(() => { showing.value = false; }, 1000); - popup(import('@/components/waiting-dialog.vue'), { + popup(defineAsyncComponent(() => import('@/components/waiting-dialog.vue')), { success: true, - showing: showing + showing: showing, }, { done: () => resolve(), }, 'closed'); @@ -348,9 +340,9 @@ export function success() { export function waiting() { return new Promise((resolve, reject) => { const showing = ref(true); - popup(import('@/components/waiting-dialog.vue'), { + popup(defineAsyncComponent(() => import('@/components/waiting-dialog.vue')), { success: false, - showing: showing + showing: showing, }, { done: () => resolve(), }, 'closed'); @@ -359,7 +351,7 @@ export function waiting() { export function form(title, form) { return new Promise((resolve, reject) => { - popup(import('@/components/form-dialog.vue'), { title, form }, { + popup(defineAsyncComponent(() => import('@/components/form-dialog.vue')), { title, form }, { done: result => { resolve(result); }, @@ -369,7 +361,7 @@ export function form(title, form) { export async function selectUser() { return new Promise((resolve, reject) => { - popup(import('@/components/user-select-dialog.vue'), {}, { + popup(defineAsyncComponent(() => import('@/components/user-select-dialog.vue')), {}, { ok: user => { resolve(user); }, @@ -379,9 +371,9 @@ export async function selectUser() { export async function selectDriveFile(multiple: boolean) { return new Promise((resolve, reject) => { - popup(import('@/components/drive-select-dialog.vue'), { + popup(defineAsyncComponent(() => import('@/components/drive-select-dialog.vue')), { type: 'file', - multiple + multiple, }, { done: files => { if (files) { @@ -394,9 +386,9 @@ export async function selectDriveFile(multiple: boolean) { export async function selectDriveFolder(multiple: boolean) { return new Promise((resolve, reject) => { - popup(import('@/components/drive-select-dialog.vue'), { + popup(defineAsyncComponent(() => import('@/components/drive-select-dialog.vue')), { type: 'folder', - multiple + multiple, }, { done: folders => { if (folders) { @@ -409,9 +401,9 @@ export async function selectDriveFolder(multiple: boolean) { export async function pickEmoji(src: HTMLElement | null, opts) { return new Promise((resolve, reject) => { - popup(import('@/components/emoji-picker-dialog.vue'), { + popup(defineAsyncComponent(() => import('@/components/emoji-picker-dialog.vue')), { src, - ...opts + ...opts, }, { done: emoji => { resolve(emoji); @@ -420,6 +412,21 @@ export async function pickEmoji(src: HTMLElement | null, opts) { }); } +export async function cropImage(image: Misskey.entities.DriveFile, options: { + aspectRatio: number; +}): Promise<Misskey.entities.DriveFile> { + return new Promise((resolve, reject) => { + popup(defineAsyncComponent(() => import('@/components/cropper-dialog.vue')), { + file: image, + aspectRatio: options.aspectRatio, + }, { + ok: x => { + resolve(x); + }, + }, 'closed'); + }); +} + type AwaitType<T> = T extends Promise<infer U> ? U : T extends (...args: any[]) => Promise<infer V> ? V : @@ -459,9 +466,9 @@ export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea: characterData: false, }); - openingEmojiPicker = await popup(import('@/components/emoji-picker-window.vue'), { + openingEmojiPicker = await popup(defineAsyncComponent(() => import('@/components/emoji-picker-window.vue')), { src, - ...opts + ...opts, }, { chosen: emoji => { insertTextAtCursor(activeTextarea, emoji); @@ -470,7 +477,7 @@ export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea: openingEmojiPicker!.dispose(); openingEmojiPicker = null; observer.disconnect(); - } + }, }); } @@ -481,12 +488,12 @@ export function popupMenu(items: MenuItem[] | Ref<MenuItem[]>, src?: HTMLElement }) { return new Promise((resolve, reject) => { let dispose; - popup(import('@/components/ui/popup-menu.vue'), { + popup(defineAsyncComponent(() => import('@/components/ui/popup-menu.vue')), { items, src, width: options?.width, align: options?.align, - viaKeyboard: options?.viaKeyboard + viaKeyboard: options?.viaKeyboard, }, { closed: () => { resolve(); @@ -502,7 +509,7 @@ export function contextMenu(items: MenuItem[] | Ref<MenuItem[]>, ev: MouseEvent) ev.preventDefault(); return new Promise((resolve, reject) => { let dispose; - popup(import('@/components/ui/context-menu.vue'), { + popup(defineAsyncComponent(() => import('@/components/ui/context-menu.vue')), { items, ev, }, { @@ -537,78 +544,6 @@ export function post(props: Record<string, any> = {}) { export const deckGlobalEvents = new EventEmitter(); -export const uploads = ref<{ - id: string; - name: string; - progressMax: number | undefined; - progressValue: number | undefined; - img: string; -}[]>([]); - -export function upload(file: File, folder?: any, name?: string, keepOriginal: boolean = defaultStore.state.keepOriginalUploading): Promise<Misskey.entities.DriveFile> { - if (folder && typeof folder === 'object') folder = folder.id; - - return new Promise((resolve, reject) => { - const id = Math.random().toString(); - - const reader = new FileReader(); - reader.onload = (e) => { - const ctx = reactive({ - id: id, - name: name || file.name || 'untitled', - progressMax: undefined, - progressValue: undefined, - img: window.URL.createObjectURL(file) - }); - - uploads.value.push(ctx); - - console.log(keepOriginal); - - const data = new FormData(); - data.append('i', $i.token); - data.append('force', 'true'); - data.append('file', file); - - if (folder) data.append('folderId', folder); - if (name) data.append('name', name); - - const xhr = new XMLHttpRequest(); - xhr.open('POST', apiUrl + '/drive/files/create', true); - xhr.onload = (ev) => { - if (xhr.status !== 200 || ev.target == null || ev.target.response == null) { - // TODO: 消すのではなくて再送できるようにしたい - uploads.value = uploads.value.filter(x => x.id != id); - - alert({ - type: 'error', - text: 'upload failed' - }); - - reject(); - return; - } - - const driveFile = JSON.parse(ev.target.response); - - resolve(driveFile); - - uploads.value = uploads.value.filter(x => x.id != id); - }; - - xhr.upload.onprogress = e => { - if (e.lengthComputable) { - ctx.progressMax = e.total; - ctx.progressValue = e.loaded; - } - }; - - xhr.send(data); - }; - reader.readAsArrayBuffer(file); - }); -} - /* export function checkExistence(fileData: ArrayBuffer): Promise<any> { return new Promise((resolve, reject) => { diff --git a/packages/client/src/pages/about-misskey.vue b/packages/client/src/pages/about-misskey.vue index ff04ed84f2..691bc4f07b 100644 --- a/packages/client/src/pages/about-misskey.vue +++ b/packages/client/src/pages/about-misskey.vue @@ -150,6 +150,7 @@ const patrons = [ 'Weeble', '蝉暮せせせ', 'ThatOneCalculator', + 'pixeldesu', ]; let easterEggReady = false; diff --git a/packages/client/src/pages/admin/abuses.vue b/packages/client/src/pages/admin/abuses.vue index 92f93797ce..e1d0361c0b 100644 --- a/packages/client/src/pages/admin/abuses.vue +++ b/packages/client/src/pages/admin/abuses.vue @@ -24,10 +24,10 @@ </div> <!-- TODO <div class="inputs" style="display: flex; padding-top: 1.2em;"> - <MkInput v-model="searchUsername" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.reports.reload()"> + <MkInput v-model="searchUsername" style="margin: 0; flex: 1;" type="text" spellcheck="false"> <span>{{ $ts.username }}</span> </MkInput> - <MkInput v-model="searchHost" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.reports.reload()" :disabled="pagination.params().origin === 'local'"> + <MkInput v-model="searchHost" style="margin: 0; flex: 1;" type="text" spellcheck="false" :disabled="pagination.params().origin === 'local'"> <span>{{ $ts.host }}</span> </MkInput> </div> @@ -41,8 +41,8 @@ </div> </template> -<script lang="ts"> -import { computed, defineComponent } from 'vue'; +<script lang="ts" setup> +import { computed } from 'vue'; import MkInput from '@/components/form/input.vue'; import MkSelect from '@/components/form/select.vue'; @@ -50,45 +50,35 @@ import MkPagination from '@/components/ui/pagination.vue'; import XAbuseReport from '@/components/abuse-report.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkInput, - MkSelect, - MkPagination, - XAbuseReport, - }, +let reports = $ref<InstanceType<typeof MkPagination>>(); - emits: ['info'], +let state = $ref('unresolved'); +let reporterOrigin = $ref('combined'); +let targetUserOrigin = $ref('combined'); +let searchUsername = $ref(''); +let searchHost = $ref(''); - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.abuseReports, - icon: 'fas fa-exclamation-circle', - bg: 'var(--bg)', - }, - searchUsername: '', - searchHost: '', - state: 'unresolved', - reporterOrigin: 'combined', - targetUserOrigin: 'combined', - pagination: { - endpoint: 'admin/abuse-user-reports' as const, - limit: 10, - params: computed(() => ({ - state: this.state, - reporterOrigin: this.reporterOrigin, - targetUserOrigin: this.targetUserOrigin, - })), - }, - } - }, +const pagination = { + endpoint: 'admin/abuse-user-reports' as const, + limit: 10, + params: computed(() => ({ + state, + reporterOrigin, + targetUserOrigin, + })), +}; - methods: { - resolved(reportId) { - this.$refs.reports.removeItem(item => item.id === reportId); - }, +function resolved(reportId) { + reports.removeItem(item => item.id === reportId); +} + +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.abuseReports, + icon: 'fas fa-exclamation-circle', + bg: 'var(--bg)', } }); </script> diff --git a/packages/client/src/pages/admin/ads.vue b/packages/client/src/pages/admin/ads.vue index 8f164caa99..b18e08db96 100644 --- a/packages/client/src/pages/admin/ads.vue +++ b/packages/client/src/pages/admin/ads.vue @@ -7,7 +7,7 @@ <template #label>URL</template> </MkInput> <MkInput v-model="ad.imageUrl" class="_formBlock"> - <template #label>{{ $ts.imageUrl }}</template> + <template #label>{{ i18n.ts.imageUrl }}</template> </MkInput> <FormRadios v-model="ad.place" class="_formBlock"> <template #label>Form</template> @@ -17,34 +17,34 @@ </FormRadios> <!-- <div style="margin: 32px 0;"> - {{ $ts.priority }} - <MkRadio v-model="ad.priority" value="high">{{ $ts.high }}</MkRadio> - <MkRadio v-model="ad.priority" value="middle">{{ $ts.middle }}</MkRadio> - <MkRadio v-model="ad.priority" value="low">{{ $ts.low }}</MkRadio> + {{ i18n.ts.priority }} + <MkRadio v-model="ad.priority" value="high">{{ i18n.ts.high }}</MkRadio> + <MkRadio v-model="ad.priority" value="middle">{{ i18n.ts.middle }}</MkRadio> + <MkRadio v-model="ad.priority" value="low">{{ i18n.ts.low }}</MkRadio> </div> --> <FormSplit> <MkInput v-model="ad.ratio" type="number"> - <template #label>{{ $ts.ratio }}</template> + <template #label>{{ i18n.ts.ratio }}</template> </MkInput> <MkInput v-model="ad.expiresAt" type="date"> - <template #label>{{ $ts.expiration }}</template> + <template #label>{{ i18n.ts.expiration }}</template> </MkInput> </FormSplit> <MkTextarea v-model="ad.memo" class="_formBlock"> - <template #label>{{ $ts.memo }}</template> + <template #label>{{ i18n.ts.memo }}</template> </MkTextarea> <div class="buttons _formBlock"> - <MkButton class="button" inline primary style="margin-right: 12px;" @click="save(ad)"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton> - <MkButton class="button" inline danger @click="remove(ad)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton> + <MkButton class="button" inline primary style="margin-right: 12px;" @click="save(ad)"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton> + <MkButton class="button" inline danger @click="remove(ad)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton> </div> </div> </div> </MkSpacer> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import MkButton from '@/components/ui/button.vue'; import MkInput from '@/components/form/input.vue'; import MkTextarea from '@/components/form/textarea.vue'; @@ -52,81 +52,65 @@ import FormRadios from '@/components/form/radios.vue'; import FormSplit from '@/components/form/split.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkButton, - MkInput, - MkTextarea, - FormRadios, - FormSplit, - }, +let ads: any[] = $ref([]); - emits: ['info'], +os.api('admin/ad/list').then(adsResponse => { + ads = adsResponse; +}); - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.ads, - icon: 'fas fa-audio-description', - bg: 'var(--bg)', - actions: [{ - asFullButton: true, - icon: 'fas fa-plus', - text: this.$ts.add, - handler: this.add, - }], - }, - ads: [], - } - }, +function add() { + ads.unshift({ + id: null, + memo: '', + place: 'square', + priority: 'middle', + ratio: 1, + url: '', + imageUrl: null, + expiresAt: null, + }); +} - created() { - os.api('admin/ad/list').then(ads => { - this.ads = ads; +function remove(ad) { + os.confirm({ + type: 'warning', + text: i18n.t('removeAreYouSure', { x: ad.url }), + }).then(({ canceled }) => { + if (canceled) return; + ads = ads.filter(x => x !== ad); + os.apiWithDialog('admin/ad/delete', { + id: ad.id }); - }, - - methods: { - add() { - this.ads.unshift({ - id: null, - memo: '', - place: 'square', - priority: 'middle', - ratio: 1, - url: '', - imageUrl: null, - expiresAt: null, - }); - }, + }); +} - remove(ad) { - os.confirm({ - type: 'warning', - text: this.$t('removeAreYouSure', { x: ad.url }), - }).then(({ canceled }) => { - if (canceled) return; - this.ads = this.ads.filter(x => x != ad); - os.apiWithDialog('admin/ad/delete', { - id: ad.id - }); - }); - }, +function save(ad) { + if (ad.id == null) { + os.apiWithDialog('admin/ad/create', { + ...ad, + expiresAt: new Date(ad.expiresAt).getTime() + }); + } else { + os.apiWithDialog('admin/ad/update', { + ...ad, + expiresAt: new Date(ad.expiresAt).getTime() + }); + } +} - save(ad) { - if (ad.id == null) { - os.apiWithDialog('admin/ad/create', { - ...ad, - expiresAt: new Date(ad.expiresAt).getTime() - }); - } else { - os.apiWithDialog('admin/ad/update', { - ...ad, - expiresAt: new Date(ad.expiresAt).getTime() - }); - } - } +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.ads, + icon: 'fas fa-audio-description', + bg: 'var(--bg)', + actions: [{ + asFullButton: true, + icon: 'fas fa-plus', + text: i18n.ts.add, + handler: add, + }], } }); </script> diff --git a/packages/client/src/pages/admin/announcements.vue b/packages/client/src/pages/admin/announcements.vue index a0d720bb29..97774975de 100644 --- a/packages/client/src/pages/admin/announcements.vue +++ b/packages/client/src/pages/admin/announcements.vue @@ -3,112 +3,98 @@ <section v-for="announcement in announcements" class="_card _gap announcements"> <div class="_content announcement"> <MkInput v-model="announcement.title"> - <template #label>{{ $ts.title }}</template> + <template #label>{{ i18n.ts.title }}</template> </MkInput> <MkTextarea v-model="announcement.text"> - <template #label>{{ $ts.text }}</template> + <template #label>{{ i18n.ts.text }}</template> </MkTextarea> <MkInput v-model="announcement.imageUrl"> - <template #label>{{ $ts.imageUrl }}</template> + <template #label>{{ i18n.ts.imageUrl }}</template> </MkInput> - <p v-if="announcement.reads">{{ $t('nUsersRead', { n: announcement.reads }) }}</p> + <p v-if="announcement.reads">{{ i18n.t('nUsersRead', { n: announcement.reads }) }}</p> <div class="buttons"> - <MkButton class="button" inline primary @click="save(announcement)"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton> - <MkButton class="button" inline @click="remove(announcement)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton> + <MkButton class="button" inline primary @click="save(announcement)"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton> + <MkButton class="button" inline @click="remove(announcement)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton> </div> </div> </section> </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import MkButton from '@/components/ui/button.vue'; import MkInput from '@/components/form/input.vue'; import MkTextarea from '@/components/form/textarea.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkButton, - MkInput, - MkTextarea, - }, +let announcements: any[] = $ref([]); - emits: ['info'], +os.api('admin/announcements/list').then(announcementResponse => { + announcements = announcementResponse; +}); - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.announcements, - icon: 'fas fa-broadcast-tower', - bg: 'var(--bg)', - actions: [{ - asFullButton: true, - icon: 'fas fa-plus', - text: this.$ts.add, - handler: this.add, - }], - }, - announcements: [], - } - }, +function add() { + announcements.unshift({ + id: null, + title: '', + text: '', + imageUrl: null + }); +} - created() { - os.api('admin/announcements/list').then(announcements => { - this.announcements = announcements; - }); - }, +function remove(announcement) { + os.confirm({ + type: 'warning', + text: i18n.t('removeAreYouSure', { x: announcement.title }), + }).then(({ canceled }) => { + if (canceled) return; + announcements = announcements.filter(x => x !== announcement); + os.api('admin/announcements/delete', announcement); + }); +} - methods: { - add() { - this.announcements.unshift({ - id: null, - title: '', - text: '', - imageUrl: null +function save(announcement) { + if (announcement.id == null) { + os.api('admin/announcements/create', announcement).then(() => { + os.alert({ + type: 'success', + text: i18n.ts.saved }); - }, - - remove(announcement) { - os.confirm({ - type: 'warning', - text: this.$t('removeAreYouSure', { x: announcement.title }), - }).then(({ canceled }) => { - if (canceled) return; - this.announcements = this.announcements.filter(x => x != announcement); - os.api('admin/announcements/delete', announcement); + }).catch(err => { + os.alert({ + type: 'error', + text: err }); - }, + }); + } else { + os.api('admin/announcements/update', announcement).then(() => { + os.alert({ + type: 'success', + text: i18n.ts.saved + }); + }).catch(err => { + os.alert({ + type: 'error', + text: err + }); + }); + } +} - save(announcement) { - if (announcement.id == null) { - os.api('admin/announcements/create', announcement).then(() => { - os.alert({ - type: 'success', - text: this.$ts.saved - }); - }).catch(e => { - os.alert({ - type: 'error', - text: e - }); - }); - } else { - os.api('admin/announcements/update', announcement).then(() => { - os.alert({ - type: 'success', - text: this.$ts.saved - }); - }).catch(e => { - os.alert({ - type: 'error', - text: e - }); - }); - } - } +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.announcements, + icon: 'fas fa-broadcast-tower', + bg: 'var(--bg)', + actions: [{ + asFullButton: true, + icon: 'fas fa-plus', + text: i18n.ts.add, + handler: add, + }], } }); </script> diff --git a/packages/client/src/pages/admin/bot-protection.vue b/packages/client/src/pages/admin/bot-protection.vue index 5e0cdd96a5..30fee5015a 100644 --- a/packages/client/src/pages/admin/bot-protection.vue +++ b/packages/client/src/pages/admin/bot-protection.vue @@ -43,8 +43,8 @@ </div> </template> -<script lang="ts"> -import { defineAsyncComponent, defineComponent } from 'vue'; +<script lang="ts" setup> +import { defineAsyncComponent } from 'vue'; import FormRadios from '@/components/form/radios.vue'; import FormInput from '@/components/form/input.vue'; import FormButton from '@/components/ui/button.vue'; @@ -54,64 +54,39 @@ import * as os from '@/os'; import * as symbols from '@/symbols'; import { fetchInstance } from '@/instance'; -export default defineComponent({ - components: { - FormRadios, - FormInput, - FormButton, - FormSuspense, - FormSlot, - MkCaptcha: defineAsyncComponent(() => import('@/components/captcha.vue')), - }, +const MkCaptcha = defineAsyncComponent(() => import('@/components/captcha.vue')); - emits: ['info'], +let provider = $ref(null); +let hcaptchaSiteKey: string | null = $ref(null); +let hcaptchaSecretKey: string | null = $ref(null); +let recaptchaSiteKey: string | null = $ref(null); +let recaptchaSecretKey: string | null = $ref(null); - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.botProtection, - icon: 'fas fa-shield-alt' - }, - provider: null, - enableHcaptcha: false, - hcaptchaSiteKey: null, - hcaptchaSecretKey: null, - enableRecaptcha: false, - recaptchaSiteKey: null, - recaptchaSecretKey: null, - } - }, +const enableHcaptcha = $computed(() => provider === 'hcaptcha'); +const enableRecaptcha = $computed(() => provider === 'recaptcha'); - methods: { - async init() { - const meta = await os.api('admin/meta'); - this.enableHcaptcha = meta.enableHcaptcha; - this.hcaptchaSiteKey = meta.hcaptchaSiteKey; - this.hcaptchaSecretKey = meta.hcaptchaSecretKey; - this.enableRecaptcha = meta.enableRecaptcha; - this.recaptchaSiteKey = meta.recaptchaSiteKey; - this.recaptchaSecretKey = meta.recaptchaSecretKey; +async function init() { + const meta = await os.api('admin/meta'); + enableHcaptcha = meta.enableHcaptcha; + hcaptchaSiteKey = meta.hcaptchaSiteKey; + hcaptchaSecretKey = meta.hcaptchaSecretKey; + enableRecaptcha = meta.enableRecaptcha; + recaptchaSiteKey = meta.recaptchaSiteKey; + recaptchaSecretKey = meta.recaptchaSecretKey; - this.provider = this.enableHcaptcha ? 'hcaptcha' : this.enableRecaptcha ? 'recaptcha' : null; + provider = enableHcaptcha ? 'hcaptcha' : enableRecaptcha ? 'recaptcha' : null; +} - this.$watch(() => this.provider, () => { - this.enableHcaptcha = this.provider === 'hcaptcha'; - this.enableRecaptcha = this.provider === 'recaptcha'; - }); - }, - - save() { - os.apiWithDialog('admin/update-meta', { - enableHcaptcha: this.enableHcaptcha, - hcaptchaSiteKey: this.hcaptchaSiteKey, - hcaptchaSecretKey: this.hcaptchaSecretKey, - enableRecaptcha: this.enableRecaptcha, - recaptchaSiteKey: this.recaptchaSiteKey, - recaptchaSecretKey: this.recaptchaSecretKey, - }).then(() => { - fetchInstance(); - }); - } - } -}); +function save() { + os.apiWithDialog('admin/update-meta', { + enableHcaptcha, + hcaptchaSiteKey, + hcaptchaSecretKey, + enableRecaptcha, + recaptchaSiteKey, + recaptchaSecretKey, + }).then(() => { + fetchInstance(); + }); +} </script> diff --git a/packages/client/src/pages/admin/database.vue b/packages/client/src/pages/admin/database.vue index 3a835eeafa..d3519922b1 100644 --- a/packages/client/src/pages/admin/database.vue +++ b/packages/client/src/pages/admin/database.vue @@ -9,36 +9,23 @@ </MkSpacer> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import FormSuspense from '@/components/form/suspense.vue'; import MkKeyValue from '@/components/key-value.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; import bytes from '@/filters/bytes'; import number from '@/filters/number'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - FormSuspense, - MkKeyValue, - }, +const databasePromiseFactory = () => os.api('admin/get-table-stats').then(res => Object.entries(res).sort((a, b) => b[1].size - a[1].size)); - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.database, - icon: 'fas fa-database', - bg: 'var(--bg)', - }, - databasePromiseFactory: () => os.api('admin/get-table-stats', {}).then(res => Object.entries(res).sort((a, b) => b[1].size - a[1].size)), - } - }, - - methods: { - bytes, number, +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.database, + icon: 'fas fa-database', + bg: 'var(--bg)', } }); </script> diff --git a/packages/client/src/pages/admin/email-settings.vue b/packages/client/src/pages/admin/email-settings.vue index 7df0b6db1c..aa13043193 100644 --- a/packages/client/src/pages/admin/email-settings.vue +++ b/packages/client/src/pages/admin/email-settings.vue @@ -3,37 +3,37 @@ <FormSuspense :p="init"> <div class="_formRoot"> <FormSwitch v-model="enableEmail" class="_formBlock"> - <template #label>{{ $ts.enableEmail }}</template> - <template #caption>{{ $ts.emailConfigInfo }}</template> + <template #label>{{ i18n.ts.enableEmail }}</template> + <template #caption>{{ i18n.ts.emailConfigInfo }}</template> </FormSwitch> <template v-if="enableEmail"> <FormInput v-model="email" type="email" class="_formBlock"> - <template #label>{{ $ts.emailAddress }}</template> + <template #label>{{ i18n.ts.emailAddress }}</template> </FormInput> <FormSection> - <template #label>{{ $ts.smtpConfig }}</template> + <template #label>{{ i18n.ts.smtpConfig }}</template> <FormSplit :min-width="280"> <FormInput v-model="smtpHost" class="_formBlock"> - <template #label>{{ $ts.smtpHost }}</template> + <template #label>{{ i18n.ts.smtpHost }}</template> </FormInput> <FormInput v-model="smtpPort" type="number" class="_formBlock"> - <template #label>{{ $ts.smtpPort }}</template> + <template #label>{{ i18n.ts.smtpPort }}</template> </FormInput> </FormSplit> <FormSplit :min-width="280"> <FormInput v-model="smtpUser" class="_formBlock"> - <template #label>{{ $ts.smtpUser }}</template> + <template #label>{{ i18n.ts.smtpUser }}</template> </FormInput> <FormInput v-model="smtpPass" type="password" class="_formBlock"> - <template #label>{{ $ts.smtpPass }}</template> + <template #label>{{ i18n.ts.smtpPass }}</template> </FormInput> </FormSplit> - <FormInfo class="_formBlock">{{ $ts.emptyToDisableSmtpAuth }}</FormInfo> + <FormInfo class="_formBlock">{{ i18n.ts.emptyToDisableSmtpAuth }}</FormInfo> <FormSwitch v-model="smtpSecure" class="_formBlock"> - <template #label>{{ $ts.smtpSecure }}</template> - <template #caption>{{ $ts.smtpSecureInfo }}</template> + <template #label>{{ i18n.ts.smtpSecure }}</template> + <template #caption>{{ i18n.ts.smtpSecureInfo }}</template> </FormSwitch> </FormSection> </template> @@ -42,8 +42,8 @@ </MkSpacer> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import FormSwitch from '@/components/form/switch.vue'; import FormInput from '@/components/form/input.vue'; import FormInfo from '@/components/ui/info.vue'; @@ -52,86 +52,71 @@ import FormSplit from '@/components/form/split.vue'; import FormSection from '@/components/form/section.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; -import { fetchInstance } from '@/instance'; +import { fetchInstance, instance } from '@/instance'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - FormSwitch, - FormInput, - FormSplit, - FormSection, - FormInfo, - FormSuspense, - }, +let enableEmail: boolean = $ref(false); +let email: any = $ref(null); +let smtpSecure: boolean = $ref(false); +let smtpHost: string = $ref(''); +let smtpPort: number = $ref(0); +let smtpUser: string = $ref(''); +let smtpPass: string = $ref(''); - emits: ['info'], +async function init() { + const meta = await os.api('admin/meta'); + enableEmail = meta.enableEmail; + email = meta.email; + smtpSecure = meta.smtpSecure; + smtpHost = meta.smtpHost; + smtpPort = meta.smtpPort; + smtpUser = meta.smtpUser; + smtpPass = meta.smtpPass; +} - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.emailServer, - icon: 'fas fa-envelope', - bg: 'var(--bg)', - actions: [{ - asFullButton: true, - text: this.$ts.testEmail, - handler: this.testEmail, - }, { - asFullButton: true, - icon: 'fas fa-check', - text: this.$ts.save, - handler: this.save, - }], - }, - enableEmail: false, - email: null, - smtpSecure: false, - smtpHost: '', - smtpPort: 0, - smtpUser: '', - smtpPass: '', - } - }, +async function testEmail() { + const { canceled, result: destination } = await os.inputText({ + title: i18n.ts.destination, + type: 'email', + placeholder: instance.maintainerEmail + }); + if (canceled) return; + os.apiWithDialog('admin/send-email', { + to: destination, + subject: 'Test email', + text: 'Yo' + }); +} - methods: { - async init() { - const meta = await os.api('admin/meta'); - this.enableEmail = meta.enableEmail; - this.email = meta.email; - this.smtpSecure = meta.smtpSecure; - this.smtpHost = meta.smtpHost; - this.smtpPort = meta.smtpPort; - this.smtpUser = meta.smtpUser; - this.smtpPass = meta.smtpPass; - }, +function save() { + os.apiWithDialog('admin/update-meta', { + enableEmail, + email, + smtpSecure, + smtpHost, + smtpPort, + smtpUser, + smtpPass, + }).then(() => { + fetchInstance(); + }); +} - async testEmail() { - const { canceled, result: destination } = await os.inputText({ - title: this.$ts.destination, - type: 'email', - placeholder: this.$instance.maintainerEmail - }); - if (canceled) return; - os.apiWithDialog('admin/send-email', { - to: destination, - subject: 'Test email', - text: 'Yo' - }); - }, - - save() { - os.apiWithDialog('admin/update-meta', { - enableEmail: this.enableEmail, - email: this.email, - smtpSecure: this.smtpSecure, - smtpHost: this.smtpHost, - smtpPort: this.smtpPort, - smtpUser: this.smtpUser, - smtpPass: this.smtpPass, - }).then(() => { - fetchInstance(); - }); - } +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.emailServer, + icon: 'fas fa-envelope', + bg: 'var(--bg)', + actions: [{ + asFullButton: true, + text: i18n.ts.testEmail, + handler: testEmail, + }, { + asFullButton: true, + icon: 'fas fa-check', + text: i18n.ts.save, + handler: save, + }], } }); </script> diff --git a/packages/client/src/pages/admin/emoji-edit-dialog.vue b/packages/client/src/pages/admin/emoji-edit-dialog.vue index 2e3903426e..d482fa49e6 100644 --- a/packages/client/src/pages/admin/emoji-edit-dialog.vue +++ b/packages/client/src/pages/admin/emoji-edit-dialog.vue @@ -27,85 +27,71 @@ </XModalWindow> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import XModalWindow from '@/components/ui/modal-window.vue'; import MkButton from '@/components/ui/button.vue'; import MkInput from '@/components/form/input.vue'; import * as os from '@/os'; import { unique } from '@/scripts/array'; +import { i18n } from '@/i18n'; +import { emojiCategories } from '@/instance'; -export default defineComponent({ - components: { - XModalWindow, - MkButton, - MkInput, - }, +const props = defineProps<{ + emoji: any, +}>(); - props: { - emoji: { - required: true, - } - }, - - emits: ['done', 'closed'], +let dialog = $ref(null); +let name: string = $ref(props.emoji.name); +let category: string = $ref(props.emoji.category); +let aliases: string = $ref(props.emoji.aliases.join(' ')); +let categories: string[] = $ref(emojiCategories); - data() { - return { - name: this.emoji.name, - category: this.emoji.category, - aliases: this.emoji.aliases?.join(' '), - categories: [], - } - }, +const emit = defineEmits<{ + (ev: 'done', v: { deleted?: boolean, updated?: any }): void, + (ev: 'closed'): void +}>(); - created() { - os.api('meta', { detail: false }).then(({ emojis }) => { - this.categories = unique(emojis.map((x: any) => x.category || '').filter((x: string) => x !== '')); - }); - }, +function ok() { + update(); +} - methods: { - ok() { - this.update(); - }, +async function update() { + await os.apiWithDialog('admin/emoji/update', { + id: props.emoji.id, + name, + category, + aliases: aliases.split(' '), + }); - async update() { - await os.apiWithDialog('admin/emoji/update', { - id: this.emoji.id, - name: this.name, - category: this.category, - aliases: this.aliases.split(' '), - }); + emit('done', { + updated: { + id: props.emoji.id, + name, + category, + aliases: aliases.split(' '), + } + }); - this.$emit('done', { - updated: { - name: this.name, - category: this.category, - aliases: this.aliases.split(' '), - } - }); - this.$refs.dialog.close(); - }, + dialog.close(); +} - async del() { - const { canceled } = await os.confirm({ - type: 'warning', - text: this.$t('removeAreYouSure', { x: this.emoji.name }), - }); - if (canceled) return; +async function del() { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.t('removeAreYouSure', { x: name }), + }); + if (canceled) return; - os.api('admin/emoji/delete', { - id: this.emoji.id - }).then(() => { - this.$emit('done', { - deleted: true - }); - this.$refs.dialog.close(); - }); - }, - } -}); + os.api('admin/emoji/delete', { + id: props.emoji.id + }).then(() => { + emit('done', { + deleted: true + }); + dialog.close(); + }); +} </script> <style lang="scss" scoped> diff --git a/packages/client/src/pages/admin/emojis.vue b/packages/client/src/pages/admin/emojis.vue index a080ee9c23..8ca5b3d65c 100644 --- a/packages/client/src/pages/admin/emojis.vue +++ b/packages/client/src/pages/admin/emojis.vue @@ -63,7 +63,7 @@ </template> <script lang="ts" setup> -import { computed, defineComponent, ref, toRef } from 'vue'; +import { computed, defineAsyncComponent, defineComponent, ref, toRef } from 'vue'; import MkButton from '@/components/ui/button.vue'; import MkInput from '@/components/form/input.vue'; import MkPagination from '@/components/ui/pagination.vue'; @@ -130,17 +130,17 @@ const add = async (ev: MouseEvent) => { }; const edit = (emoji) => { - os.popup(import('./emoji-edit-dialog.vue'), { + os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), { emoji: emoji }, { done: result => { if (result.updated) { - emojisPaginationComponent.value.replaceItem(item => item.id === emoji.id, { - ...emoji, + emojisPaginationComponent.value.updateItem(result.updated.id, (oldEmoji: any) => ({ + ...oldEmoji, ...result.updated - }); + })); } else if (result.deleted) { - emojisPaginationComponent.value.removeItem(item => item.id === emoji.id); + emojisPaginationComponent.value.removeItem((item) => item.id === emoji.id); } }, }, 'closed'); @@ -159,7 +159,7 @@ const remoteMenu = (emoji, ev: MouseEvent) => { }, { text: i18n.ts.import, icon: 'fas fa-plus', - action: () => { im(emoji) } + action: () => { im(emoji); } }], ev.currentTarget ?? ev.target); }; @@ -175,10 +175,10 @@ const menu = (ev: MouseEvent) => { type: 'info', text: i18n.ts.exportRequested, }); - }).catch((e) => { + }).catch((err) => { os.alert({ type: 'error', - text: e.message, + text: err.message, }); }); } @@ -195,10 +195,10 @@ const menu = (ev: MouseEvent) => { type: 'info', text: i18n.ts.importRequested, }); - }).catch((e) => { + }).catch((err) => { os.alert({ type: 'error', - text: e.message, + text: err.message, }); }); } diff --git a/packages/client/src/pages/admin/file-dialog.vue b/packages/client/src/pages/admin/file-dialog.vue index 4c33f62399..0765548aab 100644 --- a/packages/client/src/pages/admin/file-dialog.vue +++ b/packages/client/src/pages/admin/file-dialog.vue @@ -34,74 +34,52 @@ </XModalWindow> </template> -<script lang="ts"> -import { computed, defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import MkButton from '@/components/ui/button.vue'; import MkSwitch from '@/components/form/switch.vue'; import XModalWindow from '@/components/ui/modal-window.vue'; import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue'; import bytes from '@/filters/bytes'; import * as os from '@/os'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkButton, - MkSwitch, - XModalWindow, - MkDriveFileThumbnail, - }, +let file: any = $ref(null); +let info: any = $ref(null); +let isSensitive: boolean = $ref(false); - props: { - fileId: { - required: true, - } - }, - - emits: ['closed'], - - data() { - return { - file: null, - info: null, - isSensitive: false, - }; - }, +const props = defineProps<{ + fileId: string, +}>(); - created() { - this.fetch(); - }, - - methods: { - async fetch() { - this.file = await os.api('drive/files/show', { fileId: this.fileId }); - this.info = await os.api('admin/drive/show-file', { fileId: this.fileId }); - this.isSensitive = this.file.isSensitive; - }, +async function fetch() { + file = await os.api('drive/files/show', { fileId: props.fileId }); + info = await os.api('admin/drive/show-file', { fileId: props.fileId }); + isSensitive = file.isSensitive; +} - showUser() { - os.pageWindow(`/user-info/${this.file.userId}`); - }, +fetch(); - async del() { - const { canceled } = await os.confirm({ - type: 'warning', - text: this.$t('removeAreYouSure', { x: this.file.name }), - }); - if (canceled) return; +function showUser() { + os.pageWindow(`/user-info/${file.userId}`); +} - os.apiWithDialog('drive/files/delete', { - fileId: this.file.id - }); - }, +async function del() { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.t('removeAreYouSure', { x: file.name }), + }); + if (canceled) return; - async toggleIsSensitive(v) { - await os.api('drive/files/update', { fileId: this.fileId, isSensitive: v }); - this.isSensitive = v; - }, + os.apiWithDialog('drive/files/delete', { + fileId: file.id + }); +} - bytes - } -}); +async function toggleIsSensitive(v) { + await os.api('drive/files/update', { fileId: props.fileId, isSensitive: v }); + isSensitive = v; +} </script> <style lang="scss" scoped> diff --git a/packages/client/src/pages/admin/files.vue b/packages/client/src/pages/admin/files.vue index c62f053092..3cda688698 100644 --- a/packages/client/src/pages/admin/files.vue +++ b/packages/client/src/pages/admin/files.vue @@ -55,7 +55,7 @@ </template> <script lang="ts" setup> -import { computed } from 'vue'; +import { computed, defineAsyncComponent } from 'vue'; import MkButton from '@/components/ui/button.vue'; import MkInput from '@/components/form/input.vue'; import MkSelect from '@/components/form/select.vue'; @@ -93,7 +93,7 @@ function clear() { } function show(file) { - os.popup(import('./file-dialog.vue'), { + os.popup(defineAsyncComponent(() => import('./file-dialog.vue')), { fileId: file.id }, {}, 'closed'); } diff --git a/packages/client/src/pages/admin/index.vue b/packages/client/src/pages/admin/index.vue index 6b11650f48..9b7fa5678e 100644 --- a/packages/client/src/pages/admin/index.vue +++ b/packages/client/src/pages/admin/index.vue @@ -1,6 +1,6 @@ <template> <div ref="el" class="hiyeyicy" :class="{ wide: !narrow }"> - <div v-if="!narrow || page == null" class="nav"> + <div v-if="!narrow || initialPage == null" class="nav"> <MkHeader :info="header"></MkHeader> <MkSpacer :content-max="700" :margin-min="16"> @@ -12,21 +12,21 @@ <MkInfo v-if="noMaintainerInformation" warn class="info">{{ $ts.noMaintainerInformationWarning }} <MkA to="/admin/settings" class="_link">{{ $ts.configure }}</MkA></MkInfo> <MkInfo v-if="noBotProtection" warn class="info">{{ $ts.noBotProtectionWarning }} <MkA to="/admin/security" class="_link">{{ $ts.configure }}</MkA></MkInfo> - <MkSuperMenu :def="menuDef" :grid="page == null"></MkSuperMenu> + <MkSuperMenu :def="menuDef" :grid="initialPage == null"></MkSuperMenu> </div> </MkSpacer> </div> - <div class="main"> + <div v-if="!(narrow && initialPage == null)" class="main"> <MkStickyContainer> <template #header><MkHeader v-if="childInfo && !childInfo.hideHeader" :info="childInfo"/></template> - <component :is="component" :ref="el => pageChanged(el)" :key="page" v-bind="pageProps"/> + <component :is="component" :ref="el => pageChanged(el)" :key="initialPage" v-bind="pageProps"/> </MkStickyContainer> </div> </div> </template> -<script lang="ts"> -import { computed, defineAsyncComponent, defineComponent, isRef, nextTick, onMounted, reactive, ref, watch } from 'vue'; +<script lang="ts" setup> +import { defineAsyncComponent, nextTick, onMounted, onUnmounted, provide, watch } from 'vue'; import { i18n } from '@/i18n'; import MkSuperMenu from '@/components/ui/super-menu.vue'; import MkInfo from '@/components/ui/info.vue'; @@ -35,292 +35,277 @@ import { instance } from '@/instance'; import * as symbols from '@/symbols'; import * as os from '@/os'; import { lookupUser } from '@/scripts/lookup-user'; +import { MisskeyNavigator } from '@/scripts/navigate'; -export default defineComponent({ - components: { - MkSuperMenu, - MkInfo, - }, +const isEmpty = (x: string | null) => x == null || x === ''; - provide: { - shouldOmitHeaderTitle: false, - }, +const nav = new MisskeyNavigator(); - props: { - initialPage: { - type: String, - required: false - } - }, +const indexInfo = { + title: i18n.ts.controlPanel, + icon: 'fas fa-cog', + bg: 'var(--bg)', + hideHeader: true, +}; - setup(props, context) { - const indexInfo = { - title: i18n.ts.controlPanel, - icon: 'fas fa-cog', - bg: 'var(--bg)', - hideHeader: true, - }; - const INFO = ref(indexInfo); - const childInfo = ref(null); - const page = ref(props.initialPage); - const narrow = ref(false); - const view = ref(null); - const el = ref(null); - const pageChanged = (page) => { - if (page == null) return; - const viewInfo = page[symbols.PAGE_INFO]; - if (isRef(viewInfo)) { - watch(viewInfo, () => { - childInfo.value = viewInfo.value; - }, { immediate: true }); - } else { - childInfo.value = viewInfo; - } - }; - const pageProps = ref({}); +const props = defineProps<{ + initialPage?: string, +}>(); - const isEmpty = (x: any) => x == null || x == ''; +provide('shouldOmitHeaderTitle', false); - const noMaintainerInformation = ref(false); - const noBotProtection = ref(false); +let INFO = $ref(indexInfo); +let childInfo = $ref(null); +let page = $ref(props.initialPage); +let narrow = $ref(false); +let view = $ref(null); +let el = $ref(null); +let pageProps = $ref({}); +let noMaintainerInformation = isEmpty(instance.maintainerName) || isEmpty(instance.maintainerEmail); +let noBotProtection = !instance.enableHcaptcha && !instance.enableRecaptcha; - os.api('meta', { detail: true }).then(meta => { - // TODO: 設定が完了しても残ったままになるので、ストリーミングでmeta更新イベントを受け取ってよしなに更新する - noMaintainerInformation.value = isEmpty(meta.maintainerName) || isEmpty(meta.maintainerEmail); - noBotProtection.value = !meta.enableHcaptcha && !meta.enableRecaptcha; - }); +const NARROW_THRESHOLD = 600; +const ro = new ResizeObserver((entries, observer) => { + if (entries.length === 0) return; + narrow = entries[0].borderBoxSize[0].inlineSize < NARROW_THRESHOLD; +}); - const menuDef = computed(() => [{ - title: i18n.ts.quickAction, - items: [{ - type: 'button', - icon: 'fas fa-search', - text: i18n.ts.lookup, - action: lookup, - }, ...(instance.disableRegistration ? [{ - type: 'button', - icon: 'fas fa-user', - text: i18n.ts.invite, - action: invite, - }] : [])], - }, { - title: i18n.ts.administration, - items: [{ - icon: 'fas fa-tachometer-alt', - text: i18n.ts.dashboard, - to: '/admin/overview', - active: page.value === 'overview', - }, { - icon: 'fas fa-users', - text: i18n.ts.users, - to: '/admin/users', - active: page.value === 'users', - }, { - icon: 'fas fa-laugh', - text: i18n.ts.customEmojis, - to: '/admin/emojis', - active: page.value === 'emojis', - }, { - icon: 'fas fa-globe', - text: i18n.ts.federation, - to: '/admin/federation', - active: page.value === 'federation', - }, { - icon: 'fas fa-clipboard-list', - text: i18n.ts.jobQueue, - to: '/admin/queue', - active: page.value === 'queue', - }, { - icon: 'fas fa-cloud', - text: i18n.ts.files, - to: '/admin/files', - active: page.value === 'files', - }, { - icon: 'fas fa-broadcast-tower', - text: i18n.ts.announcements, - to: '/admin/announcements', - active: page.value === 'announcements', - }, { - icon: 'fas fa-audio-description', - text: i18n.ts.ads, - to: '/admin/ads', - active: page.value === 'ads', - }, { - icon: 'fas fa-exclamation-circle', - text: i18n.ts.abuseReports, - to: '/admin/abuses', - active: page.value === 'abuses', - }], - }, { - title: i18n.ts.settings, - items: [{ - icon: 'fas fa-cog', - text: i18n.ts.general, - to: '/admin/settings', - active: page.value === 'settings', - }, { - icon: 'fas fa-envelope', - text: i18n.ts.emailServer, - to: '/admin/email-settings', - active: page.value === 'email-settings', - }, { - icon: 'fas fa-cloud', - text: i18n.ts.objectStorage, - to: '/admin/object-storage', - active: page.value === 'object-storage', - }, { - icon: 'fas fa-lock', - text: i18n.ts.security, - to: '/admin/security', - active: page.value === 'security', - }, { - icon: 'fas fa-globe', - text: i18n.ts.relays, - to: '/admin/relays', - active: page.value === 'relays', - }, { - icon: 'fas fa-share-alt', - text: i18n.ts.integration, - to: '/admin/integrations', - active: page.value === 'integrations', - }, { - icon: 'fas fa-ban', - text: i18n.ts.instanceBlocking, - to: '/admin/instance-block', - active: page.value === 'instance-block', - }, { - icon: 'fas fa-ghost', - text: i18n.ts.proxyAccount, - to: '/admin/proxy-account', - active: page.value === 'proxy-account', - }, { - icon: 'fas fa-cogs', - text: i18n.ts.other, - to: '/admin/other-settings', - active: page.value === 'other-settings', - }], - }, { - title: i18n.ts.info, - items: [{ - icon: 'fas fa-database', - text: i18n.ts.database, - to: '/admin/database', - active: page.value === 'database', - }], - }]); - const component = computed(() => { - if (page.value == null) return null; - switch (page.value) { - case 'overview': return defineAsyncComponent(() => import('./overview.vue')); - case 'users': return defineAsyncComponent(() => import('./users.vue')); - case 'emojis': return defineAsyncComponent(() => import('./emojis.vue')); - case 'federation': return defineAsyncComponent(() => import('../federation.vue')); - case 'queue': return defineAsyncComponent(() => import('./queue.vue')); - case 'files': return defineAsyncComponent(() => import('./files.vue')); - case 'announcements': return defineAsyncComponent(() => import('./announcements.vue')); - case 'ads': return defineAsyncComponent(() => import('./ads.vue')); - case 'database': return defineAsyncComponent(() => import('./database.vue')); - case 'abuses': return defineAsyncComponent(() => import('./abuses.vue')); - case 'settings': return defineAsyncComponent(() => import('./settings.vue')); - case 'email-settings': return defineAsyncComponent(() => import('./email-settings.vue')); - case 'object-storage': return defineAsyncComponent(() => import('./object-storage.vue')); - case 'security': return defineAsyncComponent(() => import('./security.vue')); - case 'relays': return defineAsyncComponent(() => import('./relays.vue')); - case 'integrations': return defineAsyncComponent(() => import('./integrations.vue')); - case 'instance-block': return defineAsyncComponent(() => import('./instance-block.vue')); - case 'proxy-account': return defineAsyncComponent(() => import('./proxy-account.vue')); - case 'other-settings': return defineAsyncComponent(() => import('./other-settings.vue')); - } - }); +const menuDef = $computed(() => [{ + title: i18n.ts.quickAction, + items: [{ + type: 'button', + icon: 'fas fa-search', + text: i18n.ts.lookup, + action: lookup, + }, ...(instance.disableRegistration ? [{ + type: 'button', + icon: 'fas fa-user', + text: i18n.ts.invite, + action: invite, + }] : [])], +}, { + title: i18n.ts.administration, + items: [{ + icon: 'fas fa-tachometer-alt', + text: i18n.ts.dashboard, + to: '/admin/overview', + active: props.initialPage === 'overview', + }, { + icon: 'fas fa-users', + text: i18n.ts.users, + to: '/admin/users', + active: props.initialPage === 'users', + }, { + icon: 'fas fa-laugh', + text: i18n.ts.customEmojis, + to: '/admin/emojis', + active: props.initialPage === 'emojis', + }, { + icon: 'fas fa-globe', + text: i18n.ts.federation, + to: '/admin/federation', + active: props.initialPage === 'federation', + }, { + icon: 'fas fa-clipboard-list', + text: i18n.ts.jobQueue, + to: '/admin/queue', + active: props.initialPage === 'queue', + }, { + icon: 'fas fa-cloud', + text: i18n.ts.files, + to: '/admin/files', + active: props.initialPage === 'files', + }, { + icon: 'fas fa-broadcast-tower', + text: i18n.ts.announcements, + to: '/admin/announcements', + active: props.initialPage === 'announcements', + }, { + icon: 'fas fa-audio-description', + text: i18n.ts.ads, + to: '/admin/ads', + active: props.initialPage === 'ads', + }, { + icon: 'fas fa-exclamation-circle', + text: i18n.ts.abuseReports, + to: '/admin/abuses', + active: props.initialPage === 'abuses', + }], +}, { + title: i18n.ts.settings, + items: [{ + icon: 'fas fa-cog', + text: i18n.ts.general, + to: '/admin/settings', + active: props.initialPage === 'settings', + }, { + icon: 'fas fa-envelope', + text: i18n.ts.emailServer, + to: '/admin/email-settings', + active: props.initialPage === 'email-settings', + }, { + icon: 'fas fa-cloud', + text: i18n.ts.objectStorage, + to: '/admin/object-storage', + active: props.initialPage === 'object-storage', + }, { + icon: 'fas fa-lock', + text: i18n.ts.security, + to: '/admin/security', + active: props.initialPage === 'security', + }, { + icon: 'fas fa-globe', + text: i18n.ts.relays, + to: '/admin/relays', + active: props.initialPage === 'relays', + }, { + icon: 'fas fa-share-alt', + text: i18n.ts.integration, + to: '/admin/integrations', + active: props.initialPage === 'integrations', + }, { + icon: 'fas fa-ban', + text: i18n.ts.instanceBlocking, + to: '/admin/instance-block', + active: props.initialPage === 'instance-block', + }, { + icon: 'fas fa-ghost', + text: i18n.ts.proxyAccount, + to: '/admin/proxy-account', + active: props.initialPage === 'proxy-account', + }, { + icon: 'fas fa-cogs', + text: i18n.ts.other, + to: '/admin/other-settings', + active: props.initialPage === 'other-settings', + }], +}, { + title: i18n.ts.info, + items: [{ + icon: 'fas fa-database', + text: i18n.ts.database, + to: '/admin/database', + active: props.initialPage === 'database', + }], +}]); - watch(component, () => { - pageProps.value = {}; +const component = $computed(() => { + if (props.initialPage == null) return null; + switch (props.initialPage) { + case 'overview': return defineAsyncComponent(() => import('./overview.vue')); + case 'users': return defineAsyncComponent(() => import('./users.vue')); + case 'emojis': return defineAsyncComponent(() => import('./emojis.vue')); + case 'federation': return defineAsyncComponent(() => import('../federation.vue')); + case 'queue': return defineAsyncComponent(() => import('./queue.vue')); + case 'files': return defineAsyncComponent(() => import('./files.vue')); + case 'announcements': return defineAsyncComponent(() => import('./announcements.vue')); + case 'ads': return defineAsyncComponent(() => import('./ads.vue')); + case 'database': return defineAsyncComponent(() => import('./database.vue')); + case 'abuses': return defineAsyncComponent(() => import('./abuses.vue')); + case 'settings': return defineAsyncComponent(() => import('./settings.vue')); + case 'email-settings': return defineAsyncComponent(() => import('./email-settings.vue')); + case 'object-storage': return defineAsyncComponent(() => import('./object-storage.vue')); + case 'security': return defineAsyncComponent(() => import('./security.vue')); + case 'relays': return defineAsyncComponent(() => import('./relays.vue')); + case 'integrations': return defineAsyncComponent(() => import('./integrations.vue')); + case 'instance-block': return defineAsyncComponent(() => import('./instance-block.vue')); + case 'proxy-account': return defineAsyncComponent(() => import('./proxy-account.vue')); + case 'other-settings': return defineAsyncComponent(() => import('./other-settings.vue')); + } +}); - nextTick(() => { - scroll(el.value, { top: 0 }); - }); - }, { immediate: true }); +watch(component, () => { + pageProps = {}; - watch(() => props.initialPage, () => { - if (props.initialPage == null && !narrow.value) { - page.value = 'overview'; - } else { - page.value = props.initialPage; - if (props.initialPage == null) { - INFO.value = indexInfo; - } - } - }); + nextTick(() => { + scroll(el, { top: 0 }); + }); +}, { immediate: true }); - onMounted(() => { - narrow.value = el.value.offsetWidth < 800; - if (!narrow.value) { - page.value = 'overview'; - } - }); +watch(() => props.initialPage, () => { + if (props.initialPage == null && !narrow) { + nav.push('/admin/overview'); + } else { + if (props.initialPage == null) { + INFO = indexInfo; + } + } +}); - const invite = () => { - os.api('admin/invite').then(x => { - os.alert({ - type: 'info', - text: x.code - }); - }).catch(e => { - os.alert({ - type: 'error', - text: e - }); - }); - }; +watch(narrow, () => { + if (props.initialPage == null && !narrow) { + nav.push('/admin/overview'); + } +}); - const lookup = (ev) => { - os.popupMenu([{ - text: i18n.ts.user, - icon: 'fas fa-user', - action: () => { - lookupUser(); - } - }, { - text: i18n.ts.note, - icon: 'fas fa-pencil-alt', - action: () => { - alert('TODO'); - } - }, { - text: i18n.ts.file, - icon: 'fas fa-cloud', - action: () => { - alert('TODO'); - } - }, { - text: i18n.ts.instance, - icon: 'fas fa-globe', - action: () => { - alert('TODO'); - } - }], ev.currentTarget ?? ev.target); - }; +onMounted(() => { + ro.observe(el); + + narrow = el.offsetWidth < NARROW_THRESHOLD; + if (props.initialPage == null && !narrow) { + nav.push('/admin/overview'); + } +}); + +onUnmounted(() => { + ro.disconnect(); +}); + +const pageChanged = (page) => { + if (page == null) { + childInfo = null; + } else { + childInfo = page[symbols.PAGE_INFO]; + } +}; - return { - [symbols.PAGE_INFO]: INFO, - menuDef, - header: { - title: i18n.ts.controlPanel, - }, - noMaintainerInformation, - noBotProtection, - page, - narrow, - view, - el, - pageChanged, - childInfo, - pageProps, - component, - invite, - lookup, - }; - }, +const invite = () => { + os.api('admin/invite').then(x => { + os.alert({ + type: 'info', + text: x.code + }); + }).catch(err => { + os.alert({ + type: 'error', + text: err, + }); + }); +}; + +const lookup = (ev) => { + os.popupMenu([{ + text: i18n.ts.user, + icon: 'fas fa-user', + action: () => { + lookupUser(); + } + }, { + text: i18n.ts.note, + icon: 'fas fa-pencil-alt', + action: () => { + alert('TODO'); + } + }, { + text: i18n.ts.file, + icon: 'fas fa-cloud', + action: () => { + alert('TODO'); + } + }, { + text: i18n.ts.instance, + icon: 'fas fa-globe', + action: () => { + alert('TODO'); + } + }], ev.currentTarget ?? ev.target); +}; + +defineExpose({ + [symbols.PAGE_INFO]: INFO, + header: { + title: i18n.ts.controlPanel, + } }); </script> diff --git a/packages/client/src/pages/admin/instance-block.vue b/packages/client/src/pages/admin/instance-block.vue index 4cb8dc604e..3347846a80 100644 --- a/packages/client/src/pages/admin/instance-block.vue +++ b/packages/client/src/pages/admin/instance-block.vue @@ -2,57 +2,45 @@ <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> <FormSuspense :p="init"> <FormTextarea v-model="blockedHosts" class="_formBlock"> - <span>{{ $ts.blockedInstances }}</span> - <template #caption>{{ $ts.blockedInstancesDescription }}</template> + <span>{{ i18n.ts.blockedInstances }}</span> + <template #caption>{{ i18n.ts.blockedInstancesDescription }}</template> </FormTextarea> - <FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> + <FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton> </FormSuspense> </MkSpacer> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import FormButton from '@/components/ui/button.vue'; import FormTextarea from '@/components/form/textarea.vue'; import FormSuspense from '@/components/form/suspense.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; import { fetchInstance } from '@/instance'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - FormButton, - FormTextarea, - FormSuspense, - }, +let blockedHosts: string = $ref(''); - emits: ['info'], +async function init() { + const meta = await os.api('admin/meta'); + blockedHosts = meta.blockedHosts.join('\n'); +} - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.instanceBlocking, - icon: 'fas fa-ban', - bg: 'var(--bg)', - }, - blockedHosts: '', - } - }, +function save() { + os.apiWithDialog('admin/update-meta', { + blockedHosts: blockedHosts.split('\n') || [], + }).then(() => { + fetchInstance(); + }); +} - methods: { - async init() { - const meta = await os.api('admin/meta'); - this.blockedHosts = meta.blockedHosts.join('\n'); - }, - - save() { - os.apiWithDialog('admin/update-meta', { - blockedHosts: this.blockedHosts.split('\n') || [], - }).then(() => { - fetchInstance(); - }); - } +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.instanceBlocking, + icon: 'fas fa-ban', + bg: 'var(--bg)', } }); </script> diff --git a/packages/client/src/pages/admin/integrations.discord.vue b/packages/client/src/pages/admin/integrations.discord.vue index 6b50f1b0a9..9fdc51a6ca 100644 --- a/packages/client/src/pages/admin/integrations.discord.vue +++ b/packages/client/src/pages/admin/integrations.discord.vue @@ -24,57 +24,36 @@ </FormSuspense> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import FormSwitch from '@/components/form/switch.vue'; import FormInput from '@/components/form/input.vue'; import FormButton from '@/components/ui/button.vue'; import FormInfo from '@/components/ui/info.vue'; import FormSuspense from '@/components/form/suspense.vue'; import * as os from '@/os'; -import * as symbols from '@/symbols'; import { fetchInstance } from '@/instance'; -export default defineComponent({ - components: { - FormSwitch, - FormInput, - FormInfo, - FormButton, - FormSuspense, - }, +let uri: string = $ref(''); +let enableDiscordIntegration: boolean = $ref(false); +let discordClientId: string | null = $ref(null); +let discordClientSecret: string | null = $ref(null); - emits: ['info'], +async function init() { + const meta = await os.api('admin/meta'); + uri = meta.uri; + enableDiscordIntegration = meta.enableDiscordIntegration; + discordClientId = meta.discordClientId; + discordClientSecret = meta.discordClientSecret; +} - data() { - return { - [symbols.PAGE_INFO]: { - title: 'Discord', - icon: 'fab fa-discord' - }, - enableDiscordIntegration: false, - discordClientId: null, - discordClientSecret: null, - } - }, - - methods: { - async init() { - const meta = await os.api('admin/meta'); - this.uri = meta.uri; - this.enableDiscordIntegration = meta.enableDiscordIntegration; - this.discordClientId = meta.discordClientId; - this.discordClientSecret = meta.discordClientSecret; - }, - save() { - os.apiWithDialog('admin/update-meta', { - enableDiscordIntegration: this.enableDiscordIntegration, - discordClientId: this.discordClientId, - discordClientSecret: this.discordClientSecret, - }).then(() => { - fetchInstance(); - }); - } - } -}); +function save() { + os.apiWithDialog('admin/update-meta', { + enableDiscordIntegration, + discordClientId, + discordClientSecret, + }).then(() => { + fetchInstance(); + }); +} </script> diff --git a/packages/client/src/pages/admin/integrations.github.vue b/packages/client/src/pages/admin/integrations.github.vue index 67f299e1bc..b10ccb8394 100644 --- a/packages/client/src/pages/admin/integrations.github.vue +++ b/packages/client/src/pages/admin/integrations.github.vue @@ -24,57 +24,36 @@ </FormSuspense> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import FormSwitch from '@/components/form/switch.vue'; import FormInput from '@/components/form/input.vue'; import FormButton from '@/components/ui/button.vue'; import FormInfo from '@/components/ui/info.vue'; import FormSuspense from '@/components/form/suspense.vue'; import * as os from '@/os'; -import * as symbols from '@/symbols'; import { fetchInstance } from '@/instance'; -export default defineComponent({ - components: { - FormSwitch, - FormInput, - FormInfo, - FormButton, - FormSuspense, - }, +let uri: string = $ref(''); +let enableGithubIntegration: boolean = $ref(false); +let githubClientId: string | null = $ref(null); +let githubClientSecret: string | null = $ref(null); - emits: ['info'], +async function init() { + const meta = await os.api('admin/meta'); + uri = meta.uri; + enableGithubIntegration = meta.enableGithubIntegration; + githubClientId = meta.githubClientId; + githubClientSecret = meta.githubClientSecret; +} - data() { - return { - [symbols.PAGE_INFO]: { - title: 'GitHub', - icon: 'fab fa-github' - }, - enableGithubIntegration: false, - githubClientId: null, - githubClientSecret: null, - } - }, - - methods: { - async init() { - const meta = await os.api('admin/meta'); - this.uri = meta.uri; - this.enableGithubIntegration = meta.enableGithubIntegration; - this.githubClientId = meta.githubClientId; - this.githubClientSecret = meta.githubClientSecret; - }, - save() { - os.apiWithDialog('admin/update-meta', { - enableGithubIntegration: this.enableGithubIntegration, - githubClientId: this.githubClientId, - githubClientSecret: this.githubClientSecret, - }).then(() => { - fetchInstance(); - }); - } - } -}); +function save() { + os.apiWithDialog('admin/update-meta', { + enableGithubIntegration, + githubClientId, + githubClientSecret, + }).then(() => { + fetchInstance(); + }); +} </script> diff --git a/packages/client/src/pages/admin/integrations.twitter.vue b/packages/client/src/pages/admin/integrations.twitter.vue index a389c71506..11b5fd86b2 100644 --- a/packages/client/src/pages/admin/integrations.twitter.vue +++ b/packages/client/src/pages/admin/integrations.twitter.vue @@ -24,7 +24,7 @@ </FormSuspense> </template> -<script lang="ts"> +<script lang="ts" setup> import { defineComponent } from 'vue'; import FormSwitch from '@/components/form/switch.vue'; import FormInput from '@/components/form/input.vue'; @@ -32,49 +32,28 @@ import FormButton from '@/components/ui/button.vue'; import FormInfo from '@/components/ui/info.vue'; import FormSuspense from '@/components/form/suspense.vue'; import * as os from '@/os'; -import * as symbols from '@/symbols'; import { fetchInstance } from '@/instance'; -export default defineComponent({ - components: { - FormSwitch, - FormInput, - FormInfo, - FormButton, - FormSuspense, - }, +let uri: string = $ref(''); +let enableTwitterIntegration: boolean = $ref(false); +let twitterConsumerKey: string | null = $ref(null); +let twitterConsumerSecret: string | null = $ref(null); - emits: ['info'], +async function init() { + const meta = await os.api('admin/meta'); + uri = meta.uri; + enableTwitterIntegration = meta.enableTwitterIntegration; + twitterConsumerKey = meta.twitterConsumerKey; + twitterConsumerSecret = meta.twitterConsumerSecret; +} - data() { - return { - [symbols.PAGE_INFO]: { - title: 'Twitter', - icon: 'fab fa-twitter' - }, - enableTwitterIntegration: false, - twitterConsumerKey: null, - twitterConsumerSecret: null, - } - }, - - methods: { - async init() { - const meta = await os.api('admin/meta'); - this.uri = meta.uri; - this.enableTwitterIntegration = meta.enableTwitterIntegration; - this.twitterConsumerKey = meta.twitterConsumerKey; - this.twitterConsumerSecret = meta.twitterConsumerSecret; - }, - save() { - os.apiWithDialog('admin/update-meta', { - enableTwitterIntegration: this.enableTwitterIntegration, - twitterConsumerKey: this.twitterConsumerKey, - twitterConsumerSecret: this.twitterConsumerSecret, - }).then(() => { - fetchInstance(); - }); - } - } -}); +function save() { + os.apiWithDialog('admin/update-meta', { + enableTwitterIntegration, + twitterConsumerKey, + twitterConsumerSecret, + }).then(() => { + fetchInstance(); + }); +} </script> diff --git a/packages/client/src/pages/admin/integrations.vue b/packages/client/src/pages/admin/integrations.vue index 4db8a9e0a9..d6061d0e51 100644 --- a/packages/client/src/pages/admin/integrations.vue +++ b/packages/client/src/pages/admin/integrations.vue @@ -4,69 +4,52 @@ <FormFolder class="_formBlock"> <template #icon><i class="fab fa-twitter"></i></template> <template #label>Twitter</template> - <template #suffix>{{ enableTwitterIntegration ? $ts.enabled : $ts.disabled }}</template> + <template #suffix>{{ enableTwitterIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template> <XTwitter/> </FormFolder> - <FormFolder to="/admin/integrations/github" class="_formBlock"> + <FormFolder class="_formBlock"> <template #icon><i class="fab fa-github"></i></template> <template #label>GitHub</template> - <template #suffix>{{ enableGithubIntegration ? $ts.enabled : $ts.disabled }}</template> + <template #suffix>{{ enableGithubIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template> <XGithub/> </FormFolder> - <FormFolder to="/admin/integrations/discord" class="_formBlock"> + <FormFolder class="_formBlock"> <template #icon><i class="fab fa-discord"></i></template> <template #label>Discord</template> - <template #suffix>{{ enableDiscordIntegration ? $ts.enabled : $ts.disabled }}</template> + <template #suffix>{{ enableDiscordIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template> <XDiscord/> </FormFolder> </FormSuspense> </MkSpacer> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import FormFolder from '@/components/form/folder.vue'; -import FormSecion from '@/components/form/section.vue'; import FormSuspense from '@/components/form/suspense.vue'; import XTwitter from './integrations.twitter.vue'; import XGithub from './integrations.github.vue'; import XDiscord from './integrations.discord.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; -import { fetchInstance } from '@/instance'; - -export default defineComponent({ - components: { - FormFolder, - FormSecion, - FormSuspense, - XTwitter, - XGithub, - XDiscord, - }, +import { i18n } from '@/i18n'; - emits: ['info'], +let enableTwitterIntegration: boolean = $ref(false); +let enableGithubIntegration: boolean = $ref(false); +let enableDiscordIntegration: boolean = $ref(false); - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.integration, - icon: 'fas fa-share-alt', - bg: 'var(--bg)', - }, - enableTwitterIntegration: false, - enableGithubIntegration: false, - enableDiscordIntegration: false, - } - }, +async function init() { + const meta = await os.api('admin/meta'); + enableTwitterIntegration = meta.enableTwitterIntegration; + enableGithubIntegration = meta.enableGithubIntegration; + enableDiscordIntegration = meta.enableDiscordIntegration; +} - methods: { - async init() { - const meta = await os.api('admin/meta'); - this.enableTwitterIntegration = meta.enableTwitterIntegration; - this.enableGithubIntegration = meta.enableGithubIntegration; - this.enableDiscordIntegration = meta.enableDiscordIntegration; - }, +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.integration, + icon: 'fas fa-share-alt', + bg: 'var(--bg)', } }); </script> diff --git a/packages/client/src/pages/admin/metrics.vue b/packages/client/src/pages/admin/metrics.vue index 1de297fd93..7e5f5bb094 100644 --- a/packages/client/src/pages/admin/metrics.vue +++ b/packages/client/src/pages/admin/metrics.vue @@ -132,7 +132,7 @@ export default defineComponent({ overviewHeight: '1fr', queueHeight: '1fr', paused: false, - } + }; }, computed: { diff --git a/packages/client/src/pages/admin/object-storage.vue b/packages/client/src/pages/admin/object-storage.vue index a1ee0761c8..d109db9c38 100644 --- a/packages/client/src/pages/admin/object-storage.vue +++ b/packages/client/src/pages/admin/object-storage.vue @@ -2,32 +2,32 @@ <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> <FormSuspense :p="init"> <div class="_formRoot"> - <FormSwitch v-model="useObjectStorage" class="_formBlock">{{ $ts.useObjectStorage }}</FormSwitch> + <FormSwitch v-model="useObjectStorage" class="_formBlock">{{ i18n.ts.useObjectStorage }}</FormSwitch> <template v-if="useObjectStorage"> <FormInput v-model="objectStorageBaseUrl" class="_formBlock"> - <template #label>{{ $ts.objectStorageBaseUrl }}</template> - <template #caption>{{ $ts.objectStorageBaseUrlDesc }}</template> + <template #label>{{ i18n.ts.objectStorageBaseUrl }}</template> + <template #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template> </FormInput> <FormInput v-model="objectStorageBucket" class="_formBlock"> - <template #label>{{ $ts.objectStorageBucket }}</template> - <template #caption>{{ $ts.objectStorageBucketDesc }}</template> + <template #label>{{ i18n.ts.objectStorageBucket }}</template> + <template #caption>{{ i18n.ts.objectStorageBucketDesc }}</template> </FormInput> <FormInput v-model="objectStoragePrefix" class="_formBlock"> - <template #label>{{ $ts.objectStoragePrefix }}</template> - <template #caption>{{ $ts.objectStoragePrefixDesc }}</template> + <template #label>{{ i18n.ts.objectStoragePrefix }}</template> + <template #caption>{{ i18n.ts.objectStoragePrefixDesc }}</template> </FormInput> <FormInput v-model="objectStorageEndpoint" class="_formBlock"> - <template #label>{{ $ts.objectStorageEndpoint }}</template> - <template #caption>{{ $ts.objectStorageEndpointDesc }}</template> + <template #label>{{ i18n.ts.objectStorageEndpoint }}</template> + <template #caption>{{ i18n.ts.objectStorageEndpointDesc }}</template> </FormInput> <FormInput v-model="objectStorageRegion" class="_formBlock"> - <template #label>{{ $ts.objectStorageRegion }}</template> - <template #caption>{{ $ts.objectStorageRegionDesc }}</template> + <template #label>{{ i18n.ts.objectStorageRegion }}</template> + <template #caption>{{ i18n.ts.objectStorageRegionDesc }}</template> </FormInput> <FormSplit :min-width="280"> @@ -43,17 +43,17 @@ </FormSplit> <FormSwitch v-model="objectStorageUseSSL" class="_formBlock"> - <template #label>{{ $ts.objectStorageUseSSL }}</template> - <template #caption>{{ $ts.objectStorageUseSSLDesc }}</template> + <template #label>{{ i18n.ts.objectStorageUseSSL }}</template> + <template #caption>{{ i18n.ts.objectStorageUseSSLDesc }}</template> </FormSwitch> <FormSwitch v-model="objectStorageUseProxy" class="_formBlock"> - <template #label>{{ $ts.objectStorageUseProxy }}</template> - <template #caption>{{ $ts.objectStorageUseProxyDesc }}</template> + <template #label>{{ i18n.ts.objectStorageUseProxy }}</template> + <template #caption>{{ i18n.ts.objectStorageUseProxyDesc }}</template> </FormSwitch> <FormSwitch v-model="objectStorageSetPublicRead" class="_formBlock"> - <template #label>{{ $ts.objectStorageSetPublicRead }}</template> + <template #label>{{ i18n.ts.objectStorageSetPublicRead }}</template> </FormSwitch> <FormSwitch v-model="objectStorageS3ForcePathStyle" class="_formBlock"> @@ -65,8 +65,8 @@ </MkSpacer> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import FormSwitch from '@/components/form/switch.vue'; import FormInput from '@/components/form/input.vue'; import FormGroup from '@/components/form/group.vue'; @@ -76,84 +76,70 @@ import FormSection from '@/components/form/section.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; import { fetchInstance } from '@/instance'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - FormSwitch, - FormInput, - FormGroup, - FormSuspense, - FormSplit, - FormSection, - }, +let useObjectStorage: boolean = $ref(false); +let objectStorageBaseUrl: string | null = $ref(null); +let objectStorageBucket: string | null = $ref(null); +let objectStoragePrefix: string | null = $ref(null); +let objectStorageEndpoint: string | null = $ref(null); +let objectStorageRegion: string | null = $ref(null); +let objectStoragePort: number | null = $ref(null); +let objectStorageAccessKey: string | null = $ref(null); +let objectStorageSecretKey: string | null = $ref(null); +let objectStorageUseSSL: boolean = $ref(false); +let objectStorageUseProxy: boolean = $ref(false); +let objectStorageSetPublicRead: boolean = $ref(false); +let objectStorageS3ForcePathStyle: boolean = $ref(true); - emits: ['info'], +async function init() { + const meta = await os.api('admin/meta'); + useObjectStorage = meta.useObjectStorage; + objectStorageBaseUrl = meta.objectStorageBaseUrl; + objectStorageBucket = meta.objectStorageBucket; + objectStoragePrefix = meta.objectStoragePrefix; + objectStorageEndpoint = meta.objectStorageEndpoint; + objectStorageRegion = meta.objectStorageRegion; + objectStoragePort = meta.objectStoragePort; + objectStorageAccessKey = meta.objectStorageAccessKey; + objectStorageSecretKey = meta.objectStorageSecretKey; + objectStorageUseSSL = meta.objectStorageUseSSL; + objectStorageUseProxy = meta.objectStorageUseProxy; + objectStorageSetPublicRead = meta.objectStorageSetPublicRead; + objectStorageS3ForcePathStyle = meta.objectStorageS3ForcePathStyle; +} - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.objectStorage, - icon: 'fas fa-cloud', - bg: 'var(--bg)', - actions: [{ - asFullButton: true, - icon: 'fas fa-check', - text: this.$ts.save, - handler: this.save, - }], - }, - useObjectStorage: false, - objectStorageBaseUrl: null, - objectStorageBucket: null, - objectStoragePrefix: null, - objectStorageEndpoint: null, - objectStorageRegion: null, - objectStoragePort: null, - objectStorageAccessKey: null, - objectStorageSecretKey: null, - objectStorageUseSSL: false, - objectStorageUseProxy: false, - objectStorageSetPublicRead: false, - objectStorageS3ForcePathStyle: true, - } - }, +function save() { + os.apiWithDialog('admin/update-meta', { + useObjectStorage, + objectStorageBaseUrl, + objectStorageBucket, + objectStoragePrefix, + objectStorageEndpoint, + objectStorageRegion, + objectStoragePort, + objectStorageAccessKey, + objectStorageSecretKey, + objectStorageUseSSL, + objectStorageUseProxy, + objectStorageSetPublicRead, + objectStorageS3ForcePathStyle, + }).then(() => { + fetchInstance(); + }); +} - methods: { - async init() { - const meta = await os.api('admin/meta'); - this.useObjectStorage = meta.useObjectStorage; - this.objectStorageBaseUrl = meta.objectStorageBaseUrl; - this.objectStorageBucket = meta.objectStorageBucket; - this.objectStoragePrefix = meta.objectStoragePrefix; - this.objectStorageEndpoint = meta.objectStorageEndpoint; - this.objectStorageRegion = meta.objectStorageRegion; - this.objectStoragePort = meta.objectStoragePort; - this.objectStorageAccessKey = meta.objectStorageAccessKey; - this.objectStorageSecretKey = meta.objectStorageSecretKey; - this.objectStorageUseSSL = meta.objectStorageUseSSL; - this.objectStorageUseProxy = meta.objectStorageUseProxy; - this.objectStorageSetPublicRead = meta.objectStorageSetPublicRead; - this.objectStorageS3ForcePathStyle = meta.objectStorageS3ForcePathStyle; - }, - save() { - os.apiWithDialog('admin/update-meta', { - useObjectStorage: this.useObjectStorage, - objectStorageBaseUrl: this.objectStorageBaseUrl ? this.objectStorageBaseUrl : null, - objectStorageBucket: this.objectStorageBucket ? this.objectStorageBucket : null, - objectStoragePrefix: this.objectStoragePrefix ? this.objectStoragePrefix : null, - objectStorageEndpoint: this.objectStorageEndpoint ? this.objectStorageEndpoint : null, - objectStorageRegion: this.objectStorageRegion ? this.objectStorageRegion : null, - objectStoragePort: this.objectStoragePort ? this.objectStoragePort : null, - objectStorageAccessKey: this.objectStorageAccessKey ? this.objectStorageAccessKey : null, - objectStorageSecretKey: this.objectStorageSecretKey ? this.objectStorageSecretKey : null, - objectStorageUseSSL: this.objectStorageUseSSL, - objectStorageUseProxy: this.objectStorageUseProxy, - objectStorageSetPublicRead: this.objectStorageSetPublicRead, - objectStorageS3ForcePathStyle: this.objectStorageS3ForcePathStyle, - }).then(() => { - fetchInstance(); - }); - } +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.objectStorage, + icon: 'fas fa-cloud', + bg: 'var(--bg)', + actions: [{ + asFullButton: true, + icon: 'fas fa-check', + text: i18n.ts.save, + handler: save, + }], } }); </script> diff --git a/packages/client/src/pages/admin/other-settings.vue b/packages/client/src/pages/admin/other-settings.vue index 99ea6a5f32..552b05f347 100644 --- a/packages/client/src/pages/admin/other-settings.vue +++ b/packages/client/src/pages/admin/other-settings.vue @@ -6,52 +6,35 @@ </MkSpacer> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; -import FormSwitch from '@/components/form/switch.vue'; -import FormInput from '@/components/form/input.vue'; -import FormSection from '@/components/form/section.vue'; +<script lang="ts" setup> +import { } from 'vue'; import FormSuspense from '@/components/form/suspense.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; import { fetchInstance } from '@/instance'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - FormSwitch, - FormInput, - FormSection, - FormSuspense, - }, +async function init() { + await os.api('admin/meta'); +} - emits: ['info'], +function save() { + os.apiWithDialog('admin/update-meta').then(() => { + fetchInstance(); + }); +} - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.other, - icon: 'fas fa-cogs', - bg: 'var(--bg)', - actions: [{ - asFullButton: true, - icon: 'fas fa-check', - text: this.$ts.save, - handler: this.save, - }], - }, - } - }, - - methods: { - async init() { - const meta = await os.api('admin/meta'); - }, - save() { - os.apiWithDialog('admin/update-meta', { - }).then(() => { - fetchInstance(); - }); - } +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.other, + icon: 'fas fa-cogs', + bg: 'var(--bg)', + actions: [{ + asFullButton: true, + icon: 'fas fa-check', + text: i18n.ts.save, + handler: save, + }], } }); </script> diff --git a/packages/client/src/pages/admin/overview.vue b/packages/client/src/pages/admin/overview.vue index b8ae8ad9e1..cc69424c3b 100644 --- a/packages/client/src/pages/admin/overview.vue +++ b/packages/client/src/pages/admin/overview.vue @@ -5,20 +5,20 @@ <div class="label">Users</div> <div class="value _monospace"> {{ number(stats.originalUsersCount) }} - <MkNumberDiff v-if="usersComparedToThePrevDay != null" v-tooltip="$ts.dayOverDayChanges" class="diff" :value="usersComparedToThePrevDay"><template #before>(</template><template #after>)</template></MkNumberDiff> + <MkNumberDiff v-if="usersComparedToThePrevDay != null" v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="usersComparedToThePrevDay"><template #before>(</template><template #after>)</template></MkNumberDiff> </div> </div> <div class="number _panel"> <div class="label">Notes</div> <div class="value _monospace"> {{ number(stats.originalNotesCount) }} - <MkNumberDiff v-if="notesComparedToThePrevDay != null" v-tooltip="$ts.dayOverDayChanges" class="diff" :value="notesComparedToThePrevDay"><template #before>(</template><template #after>)</template></MkNumberDiff> + <MkNumberDiff v-if="notesComparedToThePrevDay != null" v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="notesComparedToThePrevDay"><template #before>(</template><template #after>)</template></MkNumberDiff> </div> </div> </div> <MkContainer :foldable="true" class="charts"> - <template #header><i class="fas fa-chart-bar"></i>{{ $ts.charts }}</template> + <template #header><i class="fas fa-chart-bar"></i>{{ i18n.ts.charts }}</template> <div style="padding: 12px;"> <MkInstanceStats :chart-limit="500" :detailed="true"/> </div> @@ -38,7 +38,7 @@ <!--<XMetrics/>--> <MkFolder style="margin: var(--margin)"> - <template #header><i class="fas fa-info-circle"></i> {{ $ts.info }}</template> + <template #header><i class="fas fa-info-circle"></i> {{ i18n.ts.info }}</template> <div class="cfcdecdf"> <div class="number _panel"> <div class="label">Misskey</div> @@ -65,103 +65,61 @@ </div> </template> -<script lang="ts"> -import { computed, defineComponent, markRaw, version as vueVersion } from 'vue'; +<script lang="ts" setup> +import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue'; import MkInstanceStats from '@/components/instance-stats.vue'; -import MkButton from '@/components/ui/button.vue'; -import MkSelect from '@/components/form/select.vue'; import MkNumberDiff from '@/components/number-diff.vue'; import MkContainer from '@/components/ui/container.vue'; import MkFolder from '@/components/ui/folder.vue'; import MkQueueChart from '@/components/queue-chart.vue'; import { version, url } from '@/config'; -import bytes from '@/filters/bytes'; import number from '@/filters/number'; import XMetrics from './metrics.vue'; import * as os from '@/os'; import { stream } from '@/stream'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkNumberDiff, - MkInstanceStats, - MkContainer, - MkFolder, - MkQueueChart, - XMetrics, - }, +let stats: any = $ref(null); +let serverInfo: any = $ref(null); +let usersComparedToThePrevDay: any = $ref(null); +let notesComparedToThePrevDay: any = $ref(null); +const queueStatsConnection = markRaw(stream.useChannel('queueStats')); - emits: ['info'], +onMounted(async () => { + os.api('stats', {}).then(statsResponse => { + stats = statsResponse; - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.dashboard, - icon: 'fas fa-tachometer-alt', - bg: 'var(--bg)', - }, - version, - vueVersion, - url, - stats: null, - meta: null, - serverInfo: null, - usersComparedToThePrevDay: null, - notesComparedToThePrevDay: null, - fetchJobs: () => os.api('admin/queue/deliver-delayed', {}), - fetchModLogs: () => os.api('admin/show-moderation-logs', {}), - queueStatsConnection: markRaw(stream.useChannel('queueStats')), - } - }, - - async mounted() { - os.api('meta', { detail: true }).then(meta => { - this.meta = meta; + os.api('charts/users', { limit: 2, span: 'day' }).then(chart => { + usersComparedToThePrevDay = stats.originalUsersCount - chart.local.total[1]; }); - - os.api('stats', {}).then(stats => { - this.stats = stats; - - os.api('charts/users', { limit: 2, span: 'day' }).then(chart => { - this.usersComparedToThePrevDay = this.stats.originalUsersCount - chart.local.total[1]; - }); - os.api('charts/notes', { limit: 2, span: 'day' }).then(chart => { - this.notesComparedToThePrevDay = this.stats.originalNotesCount - chart.local.total[1]; - }); + os.api('charts/notes', { limit: 2, span: 'day' }).then(chart => { + notesComparedToThePrevDay = stats.originalNotesCount - chart.local.total[1]; }); + }); - os.api('admin/server-info', {}).then(serverInfo => { - this.serverInfo = serverInfo; - }); + os.api('admin/server-info').then(serverInfoResponse => { + serverInfo = serverInfoResponse; + }); - this.$nextTick(() => { - this.queueStatsConnection.send('requestLog', { - id: Math.random().toString().substr(2, 8), - length: 200 - }); + nextTick(() => { + queueStatsConnection.send('requestLog', { + id: Math.random().toString().substr(2, 8), + length: 200 }); - }, - - beforeUnmount() { - this.queueStatsConnection.dispose(); - }, - - methods: { - async showInstanceInfo(q) { - let instance = q; - if (typeof q === 'string') { - instance = await os.api('federation/show-instance', { - host: q - }); - } - // TODO - }, + }); +}); - bytes, +onBeforeUnmount(() => { + queueStatsConnection.dispose(); +}); - number, +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.dashboard, + icon: 'fas fa-tachometer-alt', + bg: 'var(--bg)', } }); </script> diff --git a/packages/client/src/pages/admin/proxy-account.vue b/packages/client/src/pages/admin/proxy-account.vue index 00f14a176f..727e20e7e5 100644 --- a/packages/client/src/pages/admin/proxy-account.vue +++ b/packages/client/src/pages/admin/proxy-account.vue @@ -1,19 +1,19 @@ <template> <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> <FormSuspense :p="init"> - <MkInfo class="_formBlock">{{ $ts.proxyAccountDescription }}</MkInfo> + <MkInfo class="_formBlock">{{ i18n.ts.proxyAccountDescription }}</MkInfo> <MkKeyValue class="_formBlock"> - <template #key>{{ $ts.proxyAccount }}</template> - <template #value>{{ proxyAccount ? `@${proxyAccount.username}` : $ts.none }}</template> + <template #key>{{ i18n.ts.proxyAccount }}</template> + <template #value>{{ proxyAccount ? `@${proxyAccount.username}` : i18n.ts.none }}</template> </MkKeyValue> - <FormButton primary class="_formBlock" @click="chooseProxyAccount">{{ $ts.selectAccount }}</FormButton> + <FormButton primary class="_formBlock" @click="chooseProxyAccount">{{ i18n.ts.selectAccount }}</FormButton> </FormSuspense> </MkSpacer> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import MkKeyValue from '@/components/key-value.vue'; import FormButton from '@/components/ui/button.vue'; import MkInfo from '@/components/ui/info.vue'; @@ -21,53 +21,40 @@ import FormSuspense from '@/components/form/suspense.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; import { fetchInstance } from '@/instance'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkKeyValue, - FormButton, - MkInfo, - FormSuspense, - }, +let proxyAccount: any = $ref(null); +let proxyAccountId: any = $ref(null); - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.proxyAccount, - icon: 'fas fa-ghost', - bg: 'var(--bg)', - }, - proxyAccount: null, - proxyAccountId: null, - } - }, +async function init() { + const meta = await os.api('admin/meta'); + proxyAccountId = meta.proxyAccountId; + if (proxyAccountId) { + proxyAccount = await os.api('users/show', { userId: proxyAccountId }); + } +} - methods: { - async init() { - const meta = await os.api('admin/meta'); - this.proxyAccountId = meta.proxyAccountId; - if (this.proxyAccountId) { - this.proxyAccount = await os.api('users/show', { userId: this.proxyAccountId }); - } - }, +function chooseProxyAccount() { + os.selectUser().then(user => { + proxyAccount = user; + proxyAccountId = user.id; + save(); + }); +} - chooseProxyAccount() { - os.selectUser().then(user => { - this.proxyAccount = user; - this.proxyAccountId = user.id; - this.save(); - }); - }, +function save() { + os.apiWithDialog('admin/update-meta', { + proxyAccountId: proxyAccountId, + }).then(() => { + fetchInstance(); + }); +} - save() { - os.apiWithDialog('admin/update-meta', { - proxyAccountId: this.proxyAccountId, - }).then(() => { - fetchInstance(); - }); - } +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.proxyAccount, + icon: 'fas fa-ghost', + bg: 'var(--bg)', } }); </script> diff --git a/packages/client/src/pages/admin/queue.chart.vue b/packages/client/src/pages/admin/queue.chart.vue index 136fb63bb6..be63830bdd 100644 --- a/packages/client/src/pages/admin/queue.chart.vue +++ b/packages/client/src/pages/admin/queue.chart.vue @@ -26,62 +26,40 @@ </div> </template> -<script lang="ts"> -import { defineComponent, markRaw, onMounted, onUnmounted, ref } from 'vue'; +<script lang="ts" setup> +import { onMounted, onUnmounted, ref } from 'vue'; import number from '@/filters/number'; import MkQueueChart from '@/components/queue-chart.vue'; import * as os from '@/os'; -export default defineComponent({ - components: { - MkQueueChart - }, +const activeSincePrevTick = ref(0); +const active = ref(0); +const waiting = ref(0); +const delayed = ref(0); +const jobs = ref([]); - props: { - domain: { - type: String, - required: true, - }, - connection: { - required: true, - }, - }, +const props = defineProps<{ + domain: string, + connection: any, +}>(); - setup(props) { - const activeSincePrevTick = ref(0); - const active = ref(0); - const waiting = ref(0); - const delayed = ref(0); - const jobs = ref([]); +onMounted(() => { + os.api(props.domain === 'inbox' ? 'admin/queue/inbox-delayed' : props.domain === 'deliver' ? 'admin/queue/deliver-delayed' : null, {}).then(result => { + jobs.value = result; + }); - onMounted(() => { - os.api(props.domain === 'inbox' ? 'admin/queue/inbox-delayed' : props.domain === 'deliver' ? 'admin/queue/deliver-delayed' : null, {}).then(result => { - jobs.value = result; - }); + const onStats = (stats) => { + activeSincePrevTick.value = stats[props.domain].activeSincePrevTick; + active.value = stats[props.domain].active; + waiting.value = stats[props.domain].waiting; + delayed.value = stats[props.domain].delayed; + }; - const onStats = (stats) => { - activeSincePrevTick.value = stats[props.domain].activeSincePrevTick; - active.value = stats[props.domain].active; - waiting.value = stats[props.domain].waiting; - delayed.value = stats[props.domain].delayed; - }; + props.connection.on('stats', onStats); - props.connection.on('stats', onStats); - - onUnmounted(() => { - props.connection.off('stats', onStats); - }); - }); - - return { - jobs, - activeSincePrevTick, - active, - waiting, - delayed, - number, - }; - }, + onUnmounted(() => { + props.connection.off('stats', onStats); + }); }); </script> diff --git a/packages/client/src/pages/admin/queue.vue b/packages/client/src/pages/admin/queue.vue index 35fd618c82..656b18199f 100644 --- a/packages/client/src/pages/admin/queue.vue +++ b/packages/client/src/pages/admin/queue.vue @@ -6,71 +6,60 @@ <XQueue :connection="connection" domain="deliver"> <template #title>Out</template> </XQueue> - <MkButton danger @click="clear()"><i class="fas fa-trash-alt"></i> {{ $ts.clearQueue }}</MkButton> + <MkButton danger @click="clear()"><i class="fas fa-trash-alt"></i> {{ i18n.ts.clearQueue }}</MkButton> </MkSpacer> </template> -<script lang="ts"> -import { defineComponent, markRaw } from 'vue'; +<script lang="ts" setup> +import { markRaw, onMounted, onBeforeUnmount, nextTick } from 'vue'; import MkButton from '@/components/ui/button.vue'; import XQueue from './queue.chart.vue'; import * as os from '@/os'; import { stream } from '@/stream'; import * as symbols from '@/symbols'; import * as config from '@/config'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkButton, - XQueue, - }, +const connection = markRaw(stream.useChannel('queueStats')); - emits: ['info'], +function clear() { + os.confirm({ + type: 'warning', + title: i18n.ts.clearQueueConfirmTitle, + text: i18n.ts.clearQueueConfirmText, + }).then(({ canceled }) => { + if (canceled) return; - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.jobQueue, - icon: 'fas fa-clipboard-list', - bg: 'var(--bg)', - actions: [{ - asFullButton: true, - icon: 'fas fa-up-right-from-square', - text: this.$ts.dashboard, - handler: () => { - window.open(config.url + '/queue', '_blank'); - }, - }], - }, - connection: markRaw(stream.useChannel('queueStats')), - } - }, + os.apiWithDialog('admin/queue/clear'); + }); +} - mounted() { - this.$nextTick(() => { - this.connection.send('requestLog', { - id: Math.random().toString().substr(2, 8), - length: 200 - }); +onMounted(() => { + nextTick(() => { + connection.send('requestLog', { + id: Math.random().toString().substr(2, 8), + length: 200 }); - }, - - beforeUnmount() { - this.connection.dispose(); - }, + }); +}); - methods: { - clear() { - os.confirm({ - type: 'warning', - title: this.$ts.clearQueueConfirmTitle, - text: this.$ts.clearQueueConfirmText, - }).then(({ canceled }) => { - if (canceled) return; +onBeforeUnmount(() => { + connection.dispose(); +}); - os.apiWithDialog('admin/queue/clear', {}); - }); - } +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.jobQueue, + icon: 'fas fa-clipboard-list', + bg: 'var(--bg)', + actions: [{ + asFullButton: true, + icon: 'fas fa-up-right-from-square', + text: i18n.ts.dashboard, + handler: () => { + window.open(config.url + '/queue', '_blank'); + }, + }], } }); </script> diff --git a/packages/client/src/pages/admin/relays.vue b/packages/client/src/pages/admin/relays.vue index bb840db0a2..1a36bb4753 100644 --- a/packages/client/src/pages/admin/relays.vue +++ b/packages/client/src/pages/admin/relays.vue @@ -8,84 +8,71 @@ <i v-else class="fas fa-clock icon requesting"></i> <span>{{ $t(`_relayStatus.${relay.status}`) }}</span> </div> - <MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton> + <MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton> </div> </MkSpacer> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import MkButton from '@/components/ui/button.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkButton, - }, +let relays: any[] = $ref([]); - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.relays, - icon: 'fas fa-globe', - bg: 'var(--bg)', - actions: [{ - asFullButton: true, - icon: 'fas fa-plus', - text: this.$ts.addRelay, - handler: this.addRelay, - }], - }, - relays: [], - inbox: '', - } - }, +async function addRelay() { + const { canceled, result: inbox } = await os.inputText({ + title: i18n.ts.addRelay, + type: 'url', + placeholder: i18n.ts.inboxUrl + }); + if (canceled) return; + os.api('admin/relays/add', { + inbox + }).then((relay: any) => { + refresh(); + }).catch((err: any) => { + os.alert({ + type: 'error', + text: err.message || err + }); + }); +} - created() { - this.refresh(); - }, +function remove(inbox: string) { + os.api('admin/relays/remove', { + inbox + }).then(() => { + refresh(); + }).catch((err: any) => { + os.alert({ + type: 'error', + text: err.message || err + }); + }); +} - methods: { - async addRelay() { - const { canceled, result: inbox } = await os.inputText({ - title: this.$ts.addRelay, - type: 'url', - placeholder: this.$ts.inboxUrl - }); - if (canceled) return; - os.api('admin/relays/add', { - inbox - }).then((relay: any) => { - this.refresh(); - }).catch((e: any) => { - os.alert({ - type: 'error', - text: e.message || e - }); - }); - }, +function refresh() { + os.api('admin/relays/list').then((relayList: any) => { + relays = relayList; + }); +} - remove(inbox: string) { - os.api('admin/relays/remove', { - inbox - }).then(() => { - this.refresh(); - }).catch((e: any) => { - os.alert({ - type: 'error', - text: e.message || e - }); - }); - }, +refresh(); - refresh() { - os.api('admin/relays/list').then((relays: any) => { - this.relays = relays; - }); - } +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.relays, + icon: 'fas fa-globe', + bg: 'var(--bg)', + actions: [{ + asFullButton: true, + icon: 'fas fa-plus', + text: i18n.ts.addRelay, + handler: addRelay, + }], } }); </script> diff --git a/packages/client/src/pages/admin/security.vue b/packages/client/src/pages/admin/security.vue index d1c979b3e0..6b8f70cca5 100644 --- a/packages/client/src/pages/admin/security.vue +++ b/packages/client/src/pages/admin/security.vue @@ -4,10 +4,10 @@ <div class="_formRoot"> <FormFolder class="_formBlock"> <template #icon><i class="fas fa-shield-alt"></i></template> - <template #label>{{ $ts.botProtection }}</template> + <template #label>{{ i18n.ts.botProtection }}</template> <template v-if="enableHcaptcha" #suffix>hCaptcha</template> <template v-else-if="enableRecaptcha" #suffix>reCAPTCHA</template> - <template v-else #suffix>{{ $ts.none }} ({{ $ts.notRecommended }})</template> + <template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template> <XBotProtection/> </FormFolder> @@ -21,7 +21,7 @@ <template #label>Summaly Proxy URL</template> </FormInput> - <FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> + <FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton> </div> </FormFolder> </div> @@ -29,8 +29,8 @@ </MkSpacer> </template> -<script lang="ts"> -import { defineAsyncComponent, defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import FormFolder from '@/components/form/folder.vue'; import FormSwitch from '@/components/form/switch.vue'; import FormInfo from '@/components/ui/info.vue'; @@ -42,49 +42,32 @@ import XBotProtection from './bot-protection.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; import { fetchInstance } from '@/instance'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - FormFolder, - FormSwitch, - FormInfo, - FormSection, - FormSuspense, - FormButton, - FormInput, - XBotProtection, - }, +let summalyProxy: string = $ref(''); +let enableHcaptcha: boolean = $ref(false); +let enableRecaptcha: boolean = $ref(false); - emits: ['info'], +async function init() { + const meta = await os.api('admin/meta'); + summalyProxy = meta.summalyProxy; + enableHcaptcha = meta.enableHcaptcha; + enableRecaptcha = meta.enableRecaptcha; +} - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.security, - icon: 'fas fa-lock', - bg: 'var(--bg)', - }, - summalyProxy: '', - enableHcaptcha: false, - enableRecaptcha: false, - } - }, +function save() { + os.apiWithDialog('admin/update-meta', { + summalyProxy, + }).then(() => { + fetchInstance(); + }); +} - methods: { - async init() { - const meta = await os.api('admin/meta'); - this.summalyProxy = meta.summalyProxy; - this.enableHcaptcha = meta.enableHcaptcha; - this.enableRecaptcha = meta.enableRecaptcha; - }, - - save() { - os.apiWithDialog('admin/update-meta', { - summalyProxy: this.summalyProxy, - }).then(() => { - fetchInstance(); - }); - } +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.security, + icon: 'fas fa-lock', + bg: 'var(--bg)', } }); </script> diff --git a/packages/client/src/pages/admin/settings.vue b/packages/client/src/pages/admin/settings.vue index f2970d0459..6dc30fe50b 100644 --- a/packages/client/src/pages/admin/settings.vue +++ b/packages/client/src/pages/admin/settings.vue @@ -3,104 +3,104 @@ <FormSuspense :p="init"> <div class="_formRoot"> <FormInput v-model="name" class="_formBlock"> - <template #label>{{ $ts.instanceName }}</template> + <template #label>{{ i18n.ts.instanceName }}</template> </FormInput> <FormTextarea v-model="description" class="_formBlock"> - <template #label>{{ $ts.instanceDescription }}</template> + <template #label>{{ i18n.ts.instanceDescription }}</template> </FormTextarea> <FormInput v-model="tosUrl" class="_formBlock"> <template #prefix><i class="fas fa-link"></i></template> - <template #label>{{ $ts.tosUrl }}</template> + <template #label>{{ i18n.ts.tosUrl }}</template> </FormInput> <FormSplit :min-width="300"> <FormInput v-model="maintainerName" class="_formBlock"> - <template #label>{{ $ts.maintainerName }}</template> + <template #label>{{ i18n.ts.maintainerName }}</template> </FormInput> <FormInput v-model="maintainerEmail" type="email" class="_formBlock"> <template #prefix><i class="fas fa-envelope"></i></template> - <template #label>{{ $ts.maintainerEmail }}</template> + <template #label>{{ i18n.ts.maintainerEmail }}</template> </FormInput> </FormSplit> <FormTextarea v-model="pinnedUsers" class="_formBlock"> - <template #label>{{ $ts.pinnedUsers }}</template> - <template #caption>{{ $ts.pinnedUsersDescription }}</template> + <template #label>{{ i18n.ts.pinnedUsers }}</template> + <template #caption>{{ i18n.ts.pinnedUsersDescription }}</template> </FormTextarea> <FormSection> <FormSwitch v-model="enableRegistration" class="_formBlock"> - <template #label>{{ $ts.enableRegistration }}</template> + <template #label>{{ i18n.ts.enableRegistration }}</template> </FormSwitch> <FormSwitch v-model="emailRequiredForSignup" class="_formBlock"> - <template #label>{{ $ts.emailRequiredForSignup }}</template> + <template #label>{{ i18n.ts.emailRequiredForSignup }}</template> </FormSwitch> </FormSection> <FormSection> - <FormSwitch v-model="enableLocalTimeline" class="_formBlock">{{ $ts.enableLocalTimeline }}</FormSwitch> - <FormSwitch v-model="enableGlobalTimeline" class="_formBlock">{{ $ts.enableGlobalTimeline }}</FormSwitch> - <FormInfo class="_formBlock">{{ $ts.disablingTimelinesInfo }}</FormInfo> + <FormSwitch v-model="enableLocalTimeline" class="_formBlock">{{ i18n.ts.enableLocalTimeline }}</FormSwitch> + <FormSwitch v-model="enableGlobalTimeline" class="_formBlock">{{ i18n.ts.enableGlobalTimeline }}</FormSwitch> + <FormInfo class="_formBlock">{{ i18n.ts.disablingTimelinesInfo }}</FormInfo> </FormSection> <FormSection> - <template #label>{{ $ts.theme }}</template> + <template #label>{{ i18n.ts.theme }}</template> <FormInput v-model="iconUrl" class="_formBlock"> <template #prefix><i class="fas fa-link"></i></template> - <template #label>{{ $ts.iconUrl }}</template> + <template #label>{{ i18n.ts.iconUrl }}</template> </FormInput> <FormInput v-model="bannerUrl" class="_formBlock"> <template #prefix><i class="fas fa-link"></i></template> - <template #label>{{ $ts.bannerUrl }}</template> + <template #label>{{ i18n.ts.bannerUrl }}</template> </FormInput> <FormInput v-model="backgroundImageUrl" class="_formBlock"> <template #prefix><i class="fas fa-link"></i></template> - <template #label>{{ $ts.backgroundImageUrl }}</template> + <template #label>{{ i18n.ts.backgroundImageUrl }}</template> </FormInput> <FormInput v-model="themeColor" class="_formBlock"> <template #prefix><i class="fas fa-palette"></i></template> - <template #label>{{ $ts.themeColor }}</template> + <template #label>{{ i18n.ts.themeColor }}</template> <template #caption>#RRGGBB</template> </FormInput> <FormTextarea v-model="defaultLightTheme" class="_formBlock"> - <template #label>{{ $ts.instanceDefaultLightTheme }}</template> - <template #caption>{{ $ts.instanceDefaultThemeDescription }}</template> + <template #label>{{ i18n.ts.instanceDefaultLightTheme }}</template> + <template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template> </FormTextarea> <FormTextarea v-model="defaultDarkTheme" class="_formBlock"> - <template #label>{{ $ts.instanceDefaultDarkTheme }}</template> - <template #caption>{{ $ts.instanceDefaultThemeDescription }}</template> + <template #label>{{ i18n.ts.instanceDefaultDarkTheme }}</template> + <template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template> </FormTextarea> </FormSection> <FormSection> - <template #label>{{ $ts.files }}</template> + <template #label>{{ i18n.ts.files }}</template> <FormSwitch v-model="cacheRemoteFiles" class="_formBlock"> - <template #label>{{ $ts.cacheRemoteFiles }}</template> - <template #caption>{{ $ts.cacheRemoteFilesDescription }}</template> + <template #label>{{ i18n.ts.cacheRemoteFiles }}</template> + <template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}</template> </FormSwitch> <FormSplit :min-width="280"> <FormInput v-model="localDriveCapacityMb" type="number" class="_formBlock"> - <template #label>{{ $ts.driveCapacityPerLocalAccount }}</template> + <template #label>{{ i18n.ts.driveCapacityPerLocalAccount }}</template> <template #suffix>MB</template> - <template #caption>{{ $ts.inMb }}</template> + <template #caption>{{ i18n.ts.inMb }}</template> </FormInput> <FormInput v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles" class="_formBlock"> - <template #label>{{ $ts.driveCapacityPerRemoteAccount }}</template> + <template #label>{{ i18n.ts.driveCapacityPerRemoteAccount }}</template> <template #suffix>MB</template> - <template #caption>{{ $ts.inMb }}</template> + <template #caption>{{ i18n.ts.inMb }}</template> </FormInput> </FormSplit> </FormSection> @@ -109,8 +109,8 @@ <template #label>ServiceWorker</template> <FormSwitch v-model="enableServiceWorker" class="_formBlock"> - <template #label>{{ $ts.enableServiceworker }}</template> - <template #caption>{{ $ts.serviceworkerInfo }}</template> + <template #label>{{ i18n.ts.enableServiceworker }}</template> + <template #caption>{{ i18n.ts.serviceworkerInfo }}</template> </FormSwitch> <template v-if="enableServiceWorker"> @@ -142,8 +142,8 @@ </MkSpacer> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import FormSwitch from '@/components/form/switch.vue'; import FormInput from '@/components/form/input.vue'; import FormTextarea from '@/components/form/textarea.vue'; @@ -154,119 +154,103 @@ import FormSuspense from '@/components/form/suspense.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; import { fetchInstance } from '@/instance'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - FormSwitch, - FormInput, - FormSuspense, - FormTextarea, - FormInfo, - FormSection, - FormSplit, - }, +let name: string | null = $ref(null); +let description: string | null = $ref(null); +let tosUrl: string | null = $ref(null); +let maintainerName: string | null = $ref(null); +let maintainerEmail: string | null = $ref(null); +let iconUrl: string | null = $ref(null); +let bannerUrl: string | null = $ref(null); +let backgroundImageUrl: string | null = $ref(null); +let themeColor: any = $ref(null); +let defaultLightTheme: any = $ref(null); +let defaultDarkTheme: any = $ref(null); +let enableLocalTimeline: boolean = $ref(false); +let enableGlobalTimeline: boolean = $ref(false); +let pinnedUsers: string = $ref(''); +let cacheRemoteFiles: boolean = $ref(false); +let localDriveCapacityMb: any = $ref(0); +let remoteDriveCapacityMb: any = $ref(0); +let enableRegistration: boolean = $ref(false); +let emailRequiredForSignup: boolean = $ref(false); +let enableServiceWorker: boolean = $ref(false); +let swPublicKey: any = $ref(null); +let swPrivateKey: any = $ref(null); +let deeplAuthKey: string = $ref(''); +let deeplIsPro: boolean = $ref(false); - emits: ['info'], +async function init() { + const meta = await os.api('admin/meta'); + name = meta.name; + description = meta.description; + tosUrl = meta.tosUrl; + iconUrl = meta.iconUrl; + bannerUrl = meta.bannerUrl; + backgroundImageUrl = meta.backgroundImageUrl; + themeColor = meta.themeColor; + defaultLightTheme = meta.defaultLightTheme; + defaultDarkTheme = meta.defaultDarkTheme; + maintainerName = meta.maintainerName; + maintainerEmail = meta.maintainerEmail; + enableLocalTimeline = !meta.disableLocalTimeline; + enableGlobalTimeline = !meta.disableGlobalTimeline; + pinnedUsers = meta.pinnedUsers.join('\n'); + cacheRemoteFiles = meta.cacheRemoteFiles; + localDriveCapacityMb = meta.driveCapacityPerLocalUserMb; + remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb; + enableRegistration = !meta.disableRegistration; + emailRequiredForSignup = meta.emailRequiredForSignup; + enableServiceWorker = meta.enableServiceWorker; + swPublicKey = meta.swPublickey; + swPrivateKey = meta.swPrivateKey; + deeplAuthKey = meta.deeplAuthKey; + deeplIsPro = meta.deeplIsPro; +} - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.general, - icon: 'fas fa-cog', - bg: 'var(--bg)', - actions: [{ - asFullButton: true, - icon: 'fas fa-check', - text: this.$ts.save, - handler: this.save, - }], - }, - name: null, - description: null, - tosUrl: null as string | null, - maintainerName: null, - maintainerEmail: null, - iconUrl: null, - bannerUrl: null, - backgroundImageUrl: null, - themeColor: null, - defaultLightTheme: null, - defaultDarkTheme: null, - enableLocalTimeline: false, - enableGlobalTimeline: false, - pinnedUsers: '', - cacheRemoteFiles: false, - localDriveCapacityMb: 0, - remoteDriveCapacityMb: 0, - enableRegistration: false, - emailRequiredForSignup: false, - enableServiceWorker: false, - swPublicKey: null, - swPrivateKey: null, - deeplAuthKey: '', - deeplIsPro: false, - } - }, +function save() { + os.apiWithDialog('admin/update-meta', { + name, + description, + tosUrl, + iconUrl, + bannerUrl, + backgroundImageUrl, + themeColor: themeColor === '' ? null : themeColor, + defaultLightTheme: defaultLightTheme === '' ? null : defaultLightTheme, + defaultDarkTheme: defaultDarkTheme === '' ? null : defaultDarkTheme, + maintainerName, + maintainerEmail, + disableLocalTimeline: !enableLocalTimeline, + disableGlobalTimeline: !enableGlobalTimeline, + pinnedUsers: pinnedUsers.split('\n'), + cacheRemoteFiles, + localDriveCapacityMb: parseInt(localDriveCapacityMb, 10), + remoteDriveCapacityMb: parseInt(remoteDriveCapacityMb, 10), + disableRegistration: !enableRegistration, + emailRequiredForSignup, + enableServiceWorker, + swPublicKey, + swPrivateKey, + deeplAuthKey, + deeplIsPro, + }).then(() => { + fetchInstance(); + }); +} - methods: { - async init() { - const meta = await os.api('admin/meta'); - this.name = meta.name; - this.description = meta.description; - this.tosUrl = meta.tosUrl; - this.iconUrl = meta.iconUrl; - this.bannerUrl = meta.bannerUrl; - this.backgroundImageUrl = meta.backgroundImageUrl; - this.themeColor = meta.themeColor; - this.defaultLightTheme = meta.defaultLightTheme; - this.defaultDarkTheme = meta.defaultDarkTheme; - this.maintainerName = meta.maintainerName; - this.maintainerEmail = meta.maintainerEmail; - this.enableLocalTimeline = !meta.disableLocalTimeline; - this.enableGlobalTimeline = !meta.disableGlobalTimeline; - this.pinnedUsers = meta.pinnedUsers.join('\n'); - this.cacheRemoteFiles = meta.cacheRemoteFiles; - this.localDriveCapacityMb = meta.driveCapacityPerLocalUserMb; - this.remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb; - this.enableRegistration = !meta.disableRegistration; - this.emailRequiredForSignup = meta.emailRequiredForSignup; - this.enableServiceWorker = meta.enableServiceWorker; - this.swPublicKey = meta.swPublickey; - this.swPrivateKey = meta.swPrivateKey; - this.deeplAuthKey = meta.deeplAuthKey; - this.deeplIsPro = meta.deeplIsPro; - }, - - save() { - os.apiWithDialog('admin/update-meta', { - name: this.name, - description: this.description, - tosUrl: this.tosUrl, - iconUrl: this.iconUrl, - bannerUrl: this.bannerUrl, - backgroundImageUrl: this.backgroundImageUrl, - themeColor: this.themeColor === '' ? null : this.themeColor, - defaultLightTheme: this.defaultLightTheme === '' ? null : this.defaultLightTheme, - defaultDarkTheme: this.defaultDarkTheme === '' ? null : this.defaultDarkTheme, - maintainerName: this.maintainerName, - maintainerEmail: this.maintainerEmail, - disableLocalTimeline: !this.enableLocalTimeline, - disableGlobalTimeline: !this.enableGlobalTimeline, - pinnedUsers: this.pinnedUsers.split('\n'), - cacheRemoteFiles: this.cacheRemoteFiles, - localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10), - remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10), - disableRegistration: !this.enableRegistration, - emailRequiredForSignup: this.emailRequiredForSignup, - enableServiceWorker: this.enableServiceWorker, - swPublicKey: this.swPublicKey, - swPrivateKey: this.swPrivateKey, - deeplAuthKey: this.deeplAuthKey, - deeplIsPro: this.deeplIsPro, - }).then(() => { - fetchInstance(); - }); - } +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.general, + icon: 'fas fa-cog', + bg: 'var(--bg)', + actions: [{ + asFullButton: true, + icon: 'fas fa-check', + text: i18n.ts.save, + handler: save, + }], } }); </script> diff --git a/packages/client/src/pages/api-console.vue b/packages/client/src/pages/api-console.vue index 142a3bee2e..88acbcd3a3 100644 --- a/packages/client/src/pages/api-console.vue +++ b/packages/client/src/pages/api-console.vue @@ -25,72 +25,60 @@ </MkSpacer> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; -import * as JSON5 from 'json5'; +<script lang="ts" setup> +import { ref } from 'vue'; +import JSON5 from 'json5'; import MkButton from '@/components/ui/button.vue'; import MkInput from '@/components/form/input.vue'; import MkTextarea from '@/components/form/textarea.vue'; import MkSwitch from '@/components/form/switch.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; +import { Endpoints } from 'misskey-js'; -export default defineComponent({ - components: { - MkButton, MkInput, MkTextarea, MkSwitch, - }, +const body = ref('{}'); +const endpoint = ref(''); +const endpoints = ref<any[]>([]); +const sending = ref(false); +const res = ref(''); +const withCredential = ref(true); - data() { - return { - [symbols.PAGE_INFO]: { - title: 'API console', - icon: 'fas fa-terminal' - }, +os.api('endpoints').then(endpointResponse => { + endpoints.value = endpointResponse; +}); - endpoint: '', - body: '{}', - res: null, - sending: false, - endpoints: [], - withCredential: true, +function send() { + sending.value = true; + const requestBody = JSON5.parse(body.value); + os.api(endpoint.value as keyof Endpoints, requestBody, requestBody.i || (withCredential.value ? undefined : null)).then(resp => { + sending.value = false; + res.value = JSON5.stringify(resp, null, 2); + }, err => { + sending.value = false; + res.value = JSON5.stringify(err, null, 2); + }); +} - }; - }, +function onEndpointChange() { + os.api('endpoint', { endpoint: endpoint.value }, withCredential.value ? undefined : null).then(resp => { + const endpointBody = {}; + for (const p of resp.params) { + endpointBody[p.name] = + p.type === 'String' ? '' : + p.type === 'Number' ? 0 : + p.type === 'Boolean' ? false : + p.type === 'Array' ? [] : + p.type === 'Object' ? {} : + null; + } + body.value = JSON5.stringify(endpointBody, null, 2); + }); +} - created() { - os.api('endpoints').then(endpoints => { - this.endpoints = endpoints; - }); +defineExpose({ + [symbols.PAGE_INFO]: { + title: 'API console', + icon: 'fas fa-terminal' }, - - methods: { - send() { - this.sending = true; - const body = JSON5.parse(this.body); - os.api(this.endpoint, body, body.i || (this.withCredential ? undefined : null)).then(res => { - this.sending = false; - this.res = JSON5.stringify(res, null, 2); - }, err => { - this.sending = false; - this.res = JSON5.stringify(err, null, 2); - }); - }, - - onEndpointChange() { - os.api('endpoint', { endpoint: this.endpoint }, this.withCredential ? undefined : null).then(endpoint => { - const body = {}; - for (const p of endpoint.params) { - body[p.name] = - p.type === 'String' ? '' : - p.type === 'Number' ? 0 : - p.type === 'Boolean' ? false : - p.type === 'Array' ? [] : - p.type === 'Object' ? {} : - null; - } - this.body = JSON5.stringify(body, null, 2); - }); - } - } }); </script> diff --git a/packages/client/src/pages/auth.form.vue b/packages/client/src/pages/auth.form.vue index bc719aebd3..5feff0149a 100644 --- a/packages/client/src/pages/auth.form.vue +++ b/packages/client/src/pages/auth.form.vue @@ -32,7 +32,7 @@ export default defineComponent({ computed: { name(): string { const el = document.createElement('div'); - el.textContent = this.app.name + el.textContent = this.app.name; return el.innerHTML; }, app(): any { diff --git a/packages/client/src/pages/channel-editor.vue b/packages/client/src/pages/channel-editor.vue index 3818c7481a..ea3a5dab76 100644 --- a/packages/client/src/pages/channel-editor.vue +++ b/packages/client/src/pages/channel-editor.vue @@ -111,8 +111,8 @@ export default defineComponent({ } }, - setBannerImage(e) { - selectFile(e.currentTarget ?? e.target, null).then(file => { + setBannerImage(evt) { + selectFile(evt.currentTarget ?? evt.target, null).then(file => { this.bannerId = file.id; }); }, diff --git a/packages/client/src/pages/emojis.category.vue b/packages/client/src/pages/emojis.category.vue index 9a317418be..c47870f4d4 100644 --- a/packages/client/src/pages/emojis.category.vue +++ b/packages/client/src/pages/emojis.category.vue @@ -58,7 +58,7 @@ export default defineComponent({ tags: emojiTags, selectedTags: new Set(), searchEmojis: null, - } + }; }, watch: { @@ -79,9 +79,9 @@ export default defineComponent({ } if (this.selectedTags.size === 0) { - this.searchEmojis = this.customEmojis.filter(e => e.name.includes(this.q) || e.aliases.includes(this.q)); + this.searchEmojis = this.customEmojis.filter(emoji => emoji.name.includes(this.q) || emoji.aliases.includes(this.q)); } else { - this.searchEmojis = this.customEmojis.filter(e => (e.name.includes(this.q) || e.aliases.includes(this.q)) && [...this.selectedTags].every(t => e.aliases.includes(t))); + this.searchEmojis = this.customEmojis.filter(emoji => (emoji.name.includes(this.q) || emoji.aliases.includes(this.q)) && [...this.selectedTags].every(t => emoji.aliases.includes(t))); } }, diff --git a/packages/client/src/pages/emojis.vue b/packages/client/src/pages/emojis.vue index 886b5f7119..f44b29df04 100644 --- a/packages/client/src/pages/emojis.vue +++ b/packages/client/src/pages/emojis.vue @@ -25,10 +25,10 @@ function menu(ev) { type: 'info', text: i18n.ts.exportRequested, }); - }).catch((e) => { + }).catch((err) => { os.alert({ type: 'error', - text: e.message, + text: err.message, }); }); } diff --git a/packages/client/src/pages/federation.vue b/packages/client/src/pages/federation.vue index 5add2b5324..447918905b 100644 --- a/packages/client/src/pages/federation.vue +++ b/packages/client/src/pages/federation.vue @@ -127,7 +127,7 @@ function getStatus(instance) { if (instance.isSuspended) return 'suspended'; if (instance.isNotResponding) return 'error'; return 'alive'; -}; +} defineExpose({ [symbols.PAGE_INFO]: { diff --git a/packages/client/src/pages/follow.vue b/packages/client/src/pages/follow.vue index d8a6824dca..e69e0481e0 100644 --- a/packages/client/src/pages/follow.vue +++ b/packages/client/src/pages/follow.vue @@ -20,7 +20,7 @@ export default defineComponent({ uri: acct }); promise.then(res => { - if (res.type == 'User') { + if (res.type === 'User') { this.follow(res.object); } else if (res.type === 'Note') { this.$router.push(`/notes/${res.object.id}`); diff --git a/packages/client/src/pages/gallery/edit.vue b/packages/client/src/pages/gallery/edit.vue index 25ee513186..bc87160c44 100644 --- a/packages/client/src/pages/gallery/edit.vue +++ b/packages/client/src/pages/gallery/edit.vue @@ -71,7 +71,7 @@ export default defineComponent({ description: null, title: null, isSensitive: false, - } + }; }, watch: { @@ -91,8 +91,8 @@ export default defineComponent({ }, methods: { - selectFile(e) { - selectFiles(e.currentTarget ?? e.target, null).then(files => { + selectFile(evt) { + selectFiles(evt.currentTarget ?? evt.target, null).then(files => { this.files = this.files.concat(files); }); }, diff --git a/packages/client/src/pages/gallery/post.vue b/packages/client/src/pages/gallery/post.vue index 1755c23286..1ca3443e56 100644 --- a/packages/client/src/pages/gallery/post.vue +++ b/packages/client/src/pages/gallery/post.vue @@ -119,8 +119,8 @@ export default defineComponent({ postId: this.postId }).then(post => { this.post = post; - }).catch(e => { - this.error = e; + }).catch(err => { + this.error = err; }); }, diff --git a/packages/client/src/pages/messaging/index.vue b/packages/client/src/pages/messaging/index.vue index 88a1e07afc..7c1d3e3cbe 100644 --- a/packages/client/src/pages/messaging/index.vue +++ b/packages/client/src/pages/messaging/index.vue @@ -90,14 +90,14 @@ export default defineComponent({ getAcct: Acct.toString, isMe(message) { - return message.userId == this.$i.id; + return message.userId === this.$i.id; }, onMessage(message) { if (message.recipientId) { this.messages = this.messages.filter(m => !( - (m.recipientId == message.recipientId && m.userId == message.userId) || - (m.recipientId == message.userId && m.userId == message.recipientId))); + (m.recipientId === message.recipientId && m.userId === message.userId) || + (m.recipientId === message.userId && m.userId === message.recipientId))); this.messages.unshift(message); } else if (message.groupId) { @@ -108,7 +108,7 @@ export default defineComponent({ onRead(ids) { for (const id of ids) { - const found = this.messages.find(m => m.id == id); + const found = this.messages.find(m => m.id === id); if (found) { if (found.recipientId) { found.isRead = true; @@ -123,11 +123,11 @@ export default defineComponent({ os.popupMenu([{ text: this.$ts.messagingWithUser, icon: 'fas fa-user', - action: () => { this.startUser() } + action: () => { this.startUser(); } }, { text: this.$ts.messagingWithGroup, icon: 'fas fa-users', - action: () => { this.startGroup() } + action: () => { this.startGroup(); } }], ev.currentTarget ?? ev.target); }, diff --git a/packages/client/src/pages/messaging/messaging-room.form.vue b/packages/client/src/pages/messaging/messaging-room.form.vue index 3863c8f82b..8e779c4f39 100644 --- a/packages/client/src/pages/messaging/messaging-room.form.vue +++ b/packages/client/src/pages/messaging/messaging-room.form.vue @@ -31,6 +31,7 @@ import * as os from '@/os'; import { stream } from '@/stream'; import { Autocomplete } from '@/scripts/autocomplete'; import { throttle } from 'throttle-debounce'; +import { uploadFile } from '@/scripts/upload'; export default defineComponent({ props: { @@ -58,7 +59,7 @@ export default defineComponent({ return this.user ? 'user:' + this.user.id : 'group:' + this.group.id; }, canSend(): boolean { - return (this.text != null && this.text != '') || this.file != null; + return (this.text != null && this.text !== '') || this.file != null; }, room(): any { return this.$parent; @@ -87,12 +88,11 @@ export default defineComponent({ } }, methods: { - async onPaste(e: ClipboardEvent) { - const data = e.clipboardData; - const items = data.items; + async onPaste(evt: ClipboardEvent) { + const items = evt.clipboardData.items; - if (items.length == 1) { - if (items[0].kind == 'file') { + if (items.length === 1) { + if (items[0].kind === 'file') { const file = items[0].getAsFile(); const lio = file.name.lastIndexOf('.'); const ext = lio >= 0 ? file.name.slice(lio) : ''; @@ -100,7 +100,7 @@ export default defineComponent({ if (formatted) this.upload(file, formatted); } } else { - if (items[0].kind == 'file') { + if (items[0].kind === 'file') { os.alert({ type: 'error', text: this.$ts.onlyOneFileCanBeAttached @@ -109,23 +109,23 @@ export default defineComponent({ } }, - onDragover(e) { - const isFile = e.dataTransfer.items[0].kind == 'file'; - const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_; + onDragover(evt) { + const isFile = evt.dataTransfer.items[0].kind === 'file'; + const isDriveFile = evt.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_; if (isFile || isDriveFile) { - e.preventDefault(); - e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; + evt.preventDefault(); + evt.dataTransfer.dropEffect = evt.dataTransfer.effectAllowed === 'all' ? 'copy' : 'move'; } }, - onDrop(e): void { + onDrop(evt): void { // ファイルだったら - if (e.dataTransfer.files.length == 1) { - e.preventDefault(); - this.upload(e.dataTransfer.files[0]); + if (evt.dataTransfer.files.length === 1) { + evt.preventDefault(); + this.upload(evt.dataTransfer.files[0]); return; - } else if (e.dataTransfer.files.length > 1) { - e.preventDefault(); + } else if (evt.dataTransfer.files.length > 1) { + evt.preventDefault(); os.alert({ type: 'error', text: this.$ts.onlyOneFileCanBeAttached @@ -134,17 +134,17 @@ export default defineComponent({ } //#region ドライブのファイル - const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); - if (driveFile != null && driveFile != '') { + const driveFile = evt.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); + if (driveFile != null && driveFile !== '') { this.file = JSON.parse(driveFile); - e.preventDefault(); + evt.preventDefault(); } //#endregion }, - onKeydown(e) { + onKeydown(evt) { this.typing(); - if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey) && this.canSend) { + if ((evt.which === 10 || evt.which === 13) && (evt.ctrlKey || evt.metaKey) && this.canSend) { this.send(); } }, @@ -153,8 +153,8 @@ export default defineComponent({ this.typing(); }, - chooseFile(e) { - selectFile(e.currentTarget ?? e.target, this.$ts.selectFile).then(file => { + chooseFile(evt) { + selectFile(evt.currentTarget ?? evt.target, this.$ts.selectFile).then(file => { this.file = file; }); }, @@ -164,7 +164,7 @@ export default defineComponent({ }, upload(file: File, name?: string) { - os.upload(file, this.$store.state.uploadFolder, name).then(res => { + uploadFile(file, this.$store.state.uploadFolder, name).then(res => { this.file = res; }); }, @@ -192,25 +192,25 @@ export default defineComponent({ }, saveDraft() { - const data = JSON.parse(localStorage.getItem('message_drafts') || '{}'); + const drafts = JSON.parse(localStorage.getItem('message_drafts') || '{}'); - data[this.draftKey] = { + drafts[this.draftKey] = { updatedAt: new Date(), data: { text: this.text, file: this.file } - } + }; - localStorage.setItem('message_drafts', JSON.stringify(data)); + localStorage.setItem('message_drafts', JSON.stringify(drafts)); }, deleteDraft() { - const data = JSON.parse(localStorage.getItem('message_drafts') || '{}'); + const drafts = JSON.parse(localStorage.getItem('message_drafts') || '{}'); - delete data[this.draftKey]; + delete drafts[this.draftKey]; - localStorage.setItem('message_drafts', JSON.stringify(data)); + localStorage.setItem('message_drafts', JSON.stringify(drafts)); }, async insertEmoji(ev) { diff --git a/packages/client/src/pages/messaging/messaging-room.vue b/packages/client/src/pages/messaging/messaging-room.vue index 2ecc68eb54..fd1962218a 100644 --- a/packages/client/src/pages/messaging/messaging-room.vue +++ b/packages/client/src/pages/messaging/messaging-room.vue @@ -166,23 +166,23 @@ const Component = defineComponent({ }); }, - onDragover(e) { - const isFile = e.dataTransfer.items[0].kind == 'file'; - const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_; + onDragover(evt) { + const isFile = evt.dataTransfer.items[0].kind === 'file'; + const isDriveFile = evt.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_; if (isFile || isDriveFile) { - e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; + evt.dataTransfer.dropEffect = evt.dataTransfer.effectAllowed === 'all' ? 'copy' : 'move'; } else { - e.dataTransfer.dropEffect = 'none'; + evt.dataTransfer.dropEffect = 'none'; } }, - onDrop(e): void { + onDrop(evt): void { // ファイルだったら - if (e.dataTransfer.files.length == 1) { - this.form.upload(e.dataTransfer.files[0]); + if (evt.dataTransfer.files.length === 1) { + this.form.upload(evt.dataTransfer.files[0]); return; - } else if (e.dataTransfer.files.length > 1) { + } else if (evt.dataTransfer.files.length > 1) { os.alert({ type: 'error', text: this.$ts.onlyOneFileCanBeAttached @@ -191,8 +191,8 @@ const Component = defineComponent({ } //#region ドライブのファイル - const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); - if (driveFile != null && driveFile != '') { + const driveFile = evt.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); + if (driveFile != null && driveFile !== '') { const file = JSON.parse(driveFile); this.form.file = file; } @@ -209,7 +209,7 @@ const Component = defineComponent({ limit: max + 1, untilId: this.existMoreMessages ? this.messages[0].id : undefined }).then(messages => { - if (messages.length == max + 1) { + if (messages.length === max + 1) { this.existMoreMessages = true; messages.pop(); } else { @@ -235,7 +235,7 @@ const Component = defineComponent({ const _isBottom = isBottom(this.$el, 64); this.messages.push(message); - if (message.userId != this.$i.id && !document.hidden) { + if (message.userId !== this.$i.id && !document.hidden) { this.connection.send('read', { id: message.id }); @@ -246,7 +246,7 @@ const Component = defineComponent({ this.$nextTick(() => { this.scrollToBottom(); }); - } else if (message.userId != this.$i.id) { + } else if (message.userId !== this.$i.id) { // Notify this.notifyNewMessage(); } @@ -256,7 +256,7 @@ const Component = defineComponent({ if (this.user) { if (!Array.isArray(x)) x = [x]; for (const id of x) { - if (this.messages.some(x => x.id == id)) { + if (this.messages.some(x => x.id === id)) { const exist = this.messages.map(x => x.id).indexOf(id); this.messages[exist] = { ...this.messages[exist], @@ -266,7 +266,7 @@ const Component = defineComponent({ } } else if (this.group) { for (const id of x.ids) { - if (this.messages.some(x => x.id == id)) { + if (this.messages.some(x => x.id === id)) { const exist = this.messages.map(x => x.id).indexOf(id); this.messages[exist] = { ...this.messages[exist], diff --git a/packages/client/src/pages/mfm-cheat-sheet.vue b/packages/client/src/pages/mfm-cheat-sheet.vue index 83ae5741c3..2c10494ede 100644 --- a/packages/client/src/pages/mfm-cheat-sheet.vue +++ b/packages/client/src/pages/mfm-cheat-sheet.vue @@ -325,23 +325,23 @@ export default defineComponent({ preview_inlineMath: '\\(x= \\frac{-b\' \\pm \\sqrt{(b\')^2-ac}}{a}\\)', preview_quote: `> ${this.$ts._mfm.dummy}`, preview_search: `${this.$ts._mfm.dummy} 検索`, - preview_jelly: `$[jelly 🍮]`, - preview_tada: `$[tada 🍮]`, - preview_jump: `$[jump 🍮]`, - preview_bounce: `$[bounce 🍮]`, - preview_shake: `$[shake 🍮]`, - preview_twitch: `$[twitch 🍮]`, - preview_spin: `$[spin 🍮] $[spin.left 🍮] $[spin.alternate 🍮]\n$[spin.x 🍮] $[spin.x,left 🍮] $[spin.x,alternate 🍮]\n$[spin.y 🍮] $[spin.y,left 🍮] $[spin.y,alternate 🍮]`, + preview_jelly: `$[jelly 🍮] $[jelly.speed=5s 🍮]`, + preview_tada: `$[tada 🍮] $[tada.speed=5s 🍮]`, + preview_jump: `$[jump 🍮] $[jump.speed=5s 🍮]`, + preview_bounce: `$[bounce 🍮] $[bounce.speed=5s 🍮]`, + preview_shake: `$[shake 🍮] $[shake.speed=5s 🍮]`, + preview_twitch: `$[twitch 🍮] $[twitch.speed=5s 🍮]`, + preview_spin: `$[spin 🍮] $[spin.left 🍮] $[spin.alternate 🍮]\n$[spin.x 🍮] $[spin.x,left 🍮] $[spin.x,alternate 🍮]\n$[spin.y 🍮] $[spin.y,left 🍮] $[spin.y,alternate 🍮]\n\n$[spin.speed=5s 🍮]`, preview_flip: `$[flip ${this.$ts._mfm.dummy}]\n$[flip.v ${this.$ts._mfm.dummy}]\n$[flip.h,v ${this.$ts._mfm.dummy}]`, preview_font: `$[font.serif ${this.$ts._mfm.dummy}]\n$[font.monospace ${this.$ts._mfm.dummy}]\n$[font.cursive ${this.$ts._mfm.dummy}]\n$[font.fantasy ${this.$ts._mfm.dummy}]`, preview_x2: `$[x2 🍮]`, preview_x3: `$[x3 🍮]`, preview_x4: `$[x4 🍮]`, preview_blur: `$[blur ${this.$ts._mfm.dummy}]`, - preview_rainbow: `$[rainbow 🍮]`, + preview_rainbow: `$[rainbow 🍮] $[rainbow.speed=5s 🍮]`, preview_sparkle: `$[sparkle 🍮]`, preview_rotate: `$[rotate 🍮]`, - } + }; }, }); </script> diff --git a/packages/client/src/pages/miauth.vue b/packages/client/src/pages/miauth.vue index 6e85b784ff..4032d7723e 100644 --- a/packages/client/src/pages/miauth.vue +++ b/packages/client/src/pages/miauth.vue @@ -42,6 +42,7 @@ import MkSignin from '@/components/signin.vue'; import MkButton from '@/components/ui/button.vue'; import * as os from '@/os'; import { login } from '@/account'; +import { appendQuery, query } from '@/scripts/url'; export default defineComponent({ components: { @@ -82,7 +83,9 @@ export default defineComponent({ this.state = 'accepted'; if (this.callback) { - location.href = `${this.callback}?session=${this.session}`; + location.href = appendQuery(this.callback, query({ + session: this.session + })); } }, deny() { diff --git a/packages/client/src/pages/my-antennas/edit.vue b/packages/client/src/pages/my-antennas/edit.vue index 04928c81a3..38e56ce35d 100644 --- a/packages/client/src/pages/my-antennas/edit.vue +++ b/packages/client/src/pages/my-antennas/edit.vue @@ -4,49 +4,34 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkButton from '@/components/ui/button.vue'; +<script lang="ts" setup> +import { watch } from 'vue'; import XAntenna from './editor.vue'; import * as symbols from '@/symbols'; import * as os from '@/os'; +import { MisskeyNavigator } from '@/scripts/navigate'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkButton, - XAntenna, - }, +const nav = new MisskeyNavigator(); - props: { - antennaId: { - type: String, - required: true, - } - }, +let antenna: any = $ref(null); - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.manageAntennas, - icon: 'fas fa-satellite', - }, - antenna: null, - }; - }, +const props = defineProps<{ + antennaId: string +}>(); - watch: { - antennaId: { - async handler() { - this.antenna = await os.api('antennas/show', { antennaId: this.antennaId }); - }, - immediate: true, - } - }, +function onAntennaUpdated() { + nav.push('/my/antennas'); +} - methods: { - onAntennaUpdated() { - this.$router.push('/my/antennas'); - }, +os.api('antennas/show', { antennaId: props.antennaId }).then((antennaResponse) => { + antenna = antennaResponse; +}); + +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.manageAntennas, + icon: 'fas fa-satellite', } }); </script> diff --git a/packages/client/src/pages/my-antennas/editor.vue b/packages/client/src/pages/my-antennas/editor.vue index 8c1d6148fe..6f3c4afbfe 100644 --- a/packages/client/src/pages/my-antennas/editor.vue +++ b/packages/client/src/pages/my-antennas/editor.vue @@ -44,135 +44,100 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { watch } from 'vue'; import MkButton from '@/components/ui/button.vue'; import MkInput from '@/components/form/input.vue'; import MkTextarea from '@/components/form/textarea.vue'; import MkSelect from '@/components/form/select.vue'; import MkSwitch from '@/components/form/switch.vue'; -import * as Acct from 'misskey-js/built/acct'; import * as os from '@/os'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkButton, MkInput, MkTextarea, MkSelect, MkSwitch - }, +const props = defineProps<{ + antenna: any +}>(); - props: { - antenna: { - type: Object, - required: true - } - }, +const emit = defineEmits<{ + (ev: 'created'): void, + (ev: 'updated'): void, + (ev: 'deleted'): void, +}>(); - data() { - return { - name: '', - src: '', - userListId: null, - userGroupId: null, - users: '', - keywords: '', - excludeKeywords: '', - caseSensitive: false, - withReplies: false, - withFile: false, - notify: false, - userLists: null, - userGroups: null, - }; - }, +let name: string = $ref(props.antenna.name); +let src: string = $ref(props.antenna.src); +let userListId: any = $ref(props.antenna.userListId); +let userGroupId: any = $ref(props.antenna.userGroupId); +let users: string = $ref(props.antenna.users.join('\n')); +let keywords: string = $ref(props.antenna.keywords.map(x => x.join(' ')).join('\n')); +let excludeKeywords: string = $ref(props.antenna.excludeKeywords.map(x => x.join(' ')).join('\n')); +let caseSensitive: boolean = $ref(props.antenna.caseSensitive); +let withReplies: boolean = $ref(props.antenna.withReplies); +let withFile: boolean = $ref(props.antenna.withFile); +let notify: boolean = $ref(props.antenna.notify); +let userLists: any = $ref(null); +let userGroups: any = $ref(null); - watch: { - async src() { - if (this.src === 'list' && this.userLists === null) { - this.userLists = await os.api('users/lists/list'); - } +watch(() => src, async () => { + if (src === 'list' && userLists === null) { + userLists = await os.api('users/lists/list'); + } - if (this.src === 'group' && this.userGroups === null) { - const groups1 = await os.api('users/groups/owned'); - const groups2 = await os.api('users/groups/joined'); + if (src === 'group' && userGroups === null) { + const groups1 = await os.api('users/groups/owned'); + const groups2 = await os.api('users/groups/joined'); - this.userGroups = [...groups1, ...groups2]; - } - } - }, + userGroups = [...groups1, ...groups2]; + } +}); - created() { - this.name = this.antenna.name; - this.src = this.antenna.src; - this.userListId = this.antenna.userListId; - this.userGroupId = this.antenna.userGroupId; - this.users = this.antenna.users.join('\n'); - this.keywords = this.antenna.keywords.map(x => x.join(' ')).join('\n'); - this.excludeKeywords = this.antenna.excludeKeywords.map(x => x.join(' ')).join('\n'); - this.caseSensitive = this.antenna.caseSensitive; - this.withReplies = this.antenna.withReplies; - this.withFile = this.antenna.withFile; - this.notify = this.antenna.notify; - }, +async function saveAntenna() { + const antennaData = { + name, + src, + userListId, + userGroupId, + withReplies, + withFile, + notify, + caseSensitive, + users: users.trim().split('\n').map(x => x.trim()), + keywords: keywords.trim().split('\n').map(x => x.trim().split(' ')), + excludeKeywords: excludeKeywords.trim().split('\n').map(x => x.trim().split(' ')), + }; - methods: { - async saveAntenna() { - if (this.antenna.id == null) { - await os.apiWithDialog('antennas/create', { - name: this.name, - src: this.src, - userListId: this.userListId, - userGroupId: this.userGroupId, - withReplies: this.withReplies, - withFile: this.withFile, - notify: this.notify, - caseSensitive: this.caseSensitive, - users: this.users.trim().split('\n').map(x => x.trim()), - keywords: this.keywords.trim().split('\n').map(x => x.trim().split(' ')), - excludeKeywords: this.excludeKeywords.trim().split('\n').map(x => x.trim().split(' ')), - }); - this.$emit('created'); - } else { - await os.apiWithDialog('antennas/update', { - antennaId: this.antenna.id, - name: this.name, - src: this.src, - userListId: this.userListId, - userGroupId: this.userGroupId, - withReplies: this.withReplies, - withFile: this.withFile, - notify: this.notify, - caseSensitive: this.caseSensitive, - users: this.users.trim().split('\n').map(x => x.trim()), - keywords: this.keywords.trim().split('\n').map(x => x.trim().split(' ')), - excludeKeywords: this.excludeKeywords.trim().split('\n').map(x => x.trim().split(' ')), - }); - this.$emit('updated'); - } - }, + if (props.antenna.id == null) { + await os.apiWithDialog('antennas/create', antennaData); + emit('created'); + } else { + antennaData['antennaId'] = props.antenna.id; + await os.apiWithDialog('antennas/update', antennaData); + emit('updated'); + } +} - async deleteAntenna() { - const { canceled } = await os.confirm({ - type: 'warning', - text: this.$t('removeAreYouSure', { x: this.antenna.name }), - }); - if (canceled) return; +async function deleteAntenna() { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.t('removeAreYouSure', { x: props.antenna.name }), + }); + if (canceled) return; - await os.api('antennas/delete', { - antennaId: this.antenna.id, - }); + await os.api('antennas/delete', { + antennaId: props.antenna.id, + }); - os.success(); - this.$emit('deleted'); - }, + os.success(); + emit('deleted'); +} - addUser() { - os.selectUser().then(user => { - this.users = this.users.trim(); - this.users += '\n@' + Acct.toString(user); - this.users = this.users.trim(); - }); - } - } -}); +function addUser() { + os.selectUser().then(user => { + users = users.trim(); + users += '\n@' + Acct.toString(user as any); + users = users.trim(); + }); +} </script> <style lang="scss" scoped> diff --git a/packages/client/src/pages/my-antennas/index.vue b/packages/client/src/pages/my-antennas/index.vue index 7138d269a9..a568f64c52 100644 --- a/packages/client/src/pages/my-antennas/index.vue +++ b/packages/client/src/pages/my-antennas/index.vue @@ -1,7 +1,7 @@ <template> <MkSpacer :content-max="700"> <div class="ieepwinx"> - <MkButton :link="true" to="/my/antennas/create" primary class="add"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton> + <MkButton :link="true" to="/my/antennas/create" primary class="add"><i class="fas fa-plus"></i> {{ i18n.ts.add }}</MkButton> <div class=""> <MkPagination v-slot="{items}" ref="list" :pagination="pagination"> @@ -14,35 +14,24 @@ </MkSpacer> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import MkPagination from '@/components/ui/pagination.vue'; import MkButton from '@/components/ui/button.vue'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkPagination, - MkButton, - }, +const pagination = { + endpoint: 'antennas/list' as const, + limit: 10, +}; - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.manageAntennas, - icon: 'fas fa-satellite', - bg: 'var(--bg)', - action: { - icon: 'fas fa-plus', - handler: this.create - } - }, - pagination: { - endpoint: 'antennas/list' as const, - limit: 10, - }, - }; - }, +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.manageAntennas, + icon: 'fas fa-satellite', + bg: 'var(--bg)' + } }); </script> diff --git a/packages/client/src/pages/note.vue b/packages/client/src/pages/note.vue index 29261ec484..f0a18ecc36 100644 --- a/packages/client/src/pages/note.vue +++ b/packages/client/src/pages/note.vue @@ -108,6 +108,10 @@ export default defineComponent({ }, methods: { fetch() { + this.hasPrev = false; + this.hasNext = false; + this.showPrev = false; + this.showNext = false; this.note = null; os.api('notes/show', { noteId: this.noteId @@ -132,8 +136,8 @@ export default defineComponent({ this.hasPrev = prev.length !== 0; this.hasNext = next.length !== 0; }); - }).catch(e => { - this.error = e; + }).catch(err => { + this.error = err; }); } } diff --git a/packages/client/src/pages/page-editor/page-editor.vue b/packages/client/src/pages/page-editor/page-editor.vue index f302ac4f90..9566592618 100644 --- a/packages/client/src/pages/page-editor/page-editor.vue +++ b/packages/client/src/pages/page-editor/page-editor.vue @@ -114,7 +114,7 @@ export default defineComponent({ readonly: this.readonly, getScriptBlockList: this.getScriptBlockList, getPageBlockList: this.getPageBlockList - } + }; }, props: { diff --git a/packages/client/src/pages/page.vue b/packages/client/src/pages/page.vue index b2c039a269..5bca971438 100644 --- a/packages/client/src/pages/page.vue +++ b/packages/client/src/pages/page.vue @@ -139,8 +139,8 @@ export default defineComponent({ username: this.username, }).then(page => { this.page = page; - }).catch(e => { - this.error = e; + }).catch(err => { + this.error = err; }); }, diff --git a/packages/client/src/pages/reset-password.vue b/packages/client/src/pages/reset-password.vue index 7d008ae75c..b3e2ca8d6f 100644 --- a/packages/client/src/pages/reset-password.vue +++ b/packages/client/src/pages/reset-password.vue @@ -12,7 +12,7 @@ </template> <script lang="ts" setup> -import { onMounted } from 'vue'; +import { defineAsyncComponent, onMounted } from 'vue'; import FormInput from '@/components/form/input.vue'; import FormButton from '@/components/ui/button.vue'; import * as os from '@/os'; @@ -36,7 +36,7 @@ async function save() { onMounted(() => { if (props.token == null) { - os.popup(import('@/components/forgot-password.vue'), {}, {}, 'closed'); + os.popup(defineAsyncComponent(() => import('@/components/forgot-password.vue')), {}, {}, 'closed'); router.push('/'); } }); diff --git a/packages/client/src/pages/scratchpad.vue b/packages/client/src/pages/scratchpad.vue index f871dc48e8..34a41b81a5 100644 --- a/packages/client/src/pages/scratchpad.vue +++ b/packages/client/src/pages/scratchpad.vue @@ -6,20 +6,20 @@ </div> <MkContainer :foldable="true" class="_gap"> - <template #header>{{ $ts.output }}</template> + <template #header>{{ i18n.ts.output }}</template> <div class="bepmlvbi"> <div v-for="log in logs" :key="log.id" class="log" :class="{ print: log.print }">{{ log.text }}</div> </div> </MkContainer> <div class="_gap"> - {{ $ts.scratchpadDescription }} + {{ i18n.ts.scratchpadDescription }} </div> </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { defineExpose, ref, watch } from 'vue'; import 'prismjs'; import { highlight, languages } from 'prismjs/components/prism-core'; import 'prismjs/components/prism-clike'; @@ -27,103 +27,90 @@ import 'prismjs/components/prism-javascript'; import 'prismjs/themes/prism-okaidia.css'; import { PrismEditor } from 'vue-prism-editor'; import 'vue-prism-editor/dist/prismeditor.min.css'; -import { AiScript, parse, utils, values } from '@syuilo/aiscript'; +import { AiScript, parse, utils } from '@syuilo/aiscript'; import MkContainer from '@/components/ui/container.vue'; import MkButton from '@/components/ui/button.vue'; import { createAiScriptEnv } from '@/scripts/aiscript/api'; import * as os from '@/os'; import * as symbols from '@/symbols'; +import { $i } from '@/account'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkContainer, - MkButton, - PrismEditor, - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.scratchpad, - icon: 'fas fa-terminal', - }, - code: '', - logs: [], - } - }, - - watch: { - code() { - localStorage.setItem('scratchpad', this.code); - } - }, +const code = ref(''); +const logs = ref<any[]>([]); - created() { - const saved = localStorage.getItem('scratchpad'); - if (saved) { - this.code = saved; - } - }, +const saved = localStorage.getItem('scratchpad'); +if (saved) { + code.value = saved; +} - methods: { - async run() { - this.logs = []; - const aiscript = new AiScript(createAiScriptEnv({ - storageKey: 'scratchpad', - token: this.$i?.token, - }), { - in: (q) => { - return new Promise(ok => { - os.inputText({ - title: q, - }).then(({ canceled, result: a }) => { - ok(a); - }); - }); - }, - out: (value) => { - this.logs.push({ - id: Math.random(), - text: value.type === 'str' ? value.value : utils.valToString(value), - print: true - }); - }, - log: (type, params) => { - switch (type) { - case 'end': this.logs.push({ - id: Math.random(), - text: utils.valToString(params.val, true), - print: false - }); break; - default: break; - } - } - }); +watch(code, () => { + localStorage.setItem('scratchpad', code.value); +}); - let ast; - try { - ast = parse(this.code); - } catch (e) { - os.alert({ - type: 'error', - text: 'Syntax error :(' - }); - return; - } - try { - await aiscript.exec(ast); - } catch (e) { - os.alert({ - type: 'error', - text: e +async function run() { + logs.value = []; + const aiscript = new AiScript(createAiScriptEnv({ + storageKey: 'scratchpad', + token: $i?.token, + }), { + in: (q) => { + return new Promise(ok => { + os.inputText({ + title: q, + }).then(({ canceled, result: a }) => { + ok(a); }); - } + }); }, - - highlighter(code) { - return highlight(code, languages.js, 'javascript'); + out: (value) => { + logs.value.push({ + id: Math.random(), + text: value.type === 'str' ? value.value : utils.valToString(value), + print: true + }); }, + log: (type, params) => { + switch (type) { + case 'end': logs.value.push({ + id: Math.random(), + text: utils.valToString(params.val, true), + print: false + }); break; + default: break; + } + } + }); + + let ast; + try { + ast = parse(code.value); + } catch (error) { + os.alert({ + type: 'error', + text: 'Syntax error :(' + }); + return; } + try { + await aiscript.exec(ast); + } catch (error: any) { + os.alert({ + type: 'error', + text: error.message + }); + } +} + +function highlighter(code) { + return highlight(code, languages.js, 'javascript'); +} + +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.scratchpad, + icon: 'fas fa-terminal', + }, }); </script> diff --git a/packages/client/src/pages/settings/2fa.vue b/packages/client/src/pages/settings/2fa.vue index 10599d99ff..fb3a7a17f3 100644 --- a/packages/client/src/pages/settings/2fa.vue +++ b/packages/client/src/pages/settings/2fa.vue @@ -1,49 +1,49 @@ <template> <div> - <MkButton v-if="!data && !$i.twoFactorEnabled" @click="register">{{ $ts._2fa.registerDevice }}</MkButton> + <MkButton v-if="!twoFactorData && !$i.twoFactorEnabled" @click="register">{{ i18n.ts._2fa.registerDevice }}</MkButton> <template v-if="$i.twoFactorEnabled"> - <p>{{ $ts._2fa.alreadyRegistered }}</p> - <MkButton @click="unregister">{{ $ts.unregister }}</MkButton> + <p>{{ i18n.ts._2fa.alreadyRegistered }}</p> + <MkButton @click="unregister">{{ i18n.ts.unregister }}</MkButton> <template v-if="supportsCredentials"> <hr class="totp-method-sep"> - <h2 class="heading">{{ $ts.securityKey }}</h2> - <p>{{ $ts._2fa.securityKeyInfo }}</p> + <h2 class="heading">{{ i18n.ts.securityKey }}</h2> + <p>{{ i18n.ts._2fa.securityKeyInfo }}</p> <div class="key-list"> <div v-for="key in $i.securityKeysList" class="key"> <h3>{{ key.name }}</h3> - <div class="last-used">{{ $ts.lastUsed }}<MkTime :time="key.lastUsed"/></div> - <MkButton @click="unregisterKey(key)">{{ $ts.unregister }}</MkButton> + <div class="last-used">{{ i18n.ts.lastUsed }}<MkTime :time="key.lastUsed"/></div> + <MkButton @click="unregisterKey(key)">{{ i18n.ts.unregister }}</MkButton> </div> </div> - <MkSwitch v-if="$i.securityKeysList.length > 0" v-model="usePasswordLessLogin" @update:modelValue="updatePasswordLessLogin">{{ $ts.passwordLessLogin }}</MkSwitch> + <MkSwitch v-if="$i.securityKeysList.length > 0" v-model="usePasswordLessLogin" @update:modelValue="updatePasswordLessLogin">{{ i18n.ts.passwordLessLogin }}</MkSwitch> - <MkInfo v-if="registration && registration.error" warn>{{ $ts.error }} {{ registration.error }}</MkInfo> - <MkButton v-if="!registration || registration.error" @click="addSecurityKey">{{ $ts._2fa.registerKey }}</MkButton> + <MkInfo v-if="registration && registration.error" warn>{{ i18n.ts.error }} {{ registration.error }}</MkInfo> + <MkButton v-if="!registration || registration.error" @click="addSecurityKey">{{ i18n.ts._2fa.registerKey }}</MkButton> <ol v-if="registration && !registration.error"> <li v-if="registration.stage >= 0"> - {{ $ts.tapSecurityKey }} + {{ i18n.ts.tapSecurityKey }} <i v-if="registration.saving && registration.stage == 0" class="fas fa-spinner fa-pulse fa-fw"></i> </li> <li v-if="registration.stage >= 1"> <MkForm :disabled="registration.stage != 1 || registration.saving"> <MkInput v-model="keyName" :max="30"> - <template #label>{{ $ts.securityKeyName }}</template> + <template #label>{{ i18n.ts.securityKeyName }}</template> </MkInput> - <MkButton :disabled="keyName.length == 0" @click="registerKey">{{ $ts.registerSecurityKey }}</MkButton> + <MkButton :disabled="keyName.length == 0" @click="registerKey">{{ i18n.ts.registerSecurityKey }}</MkButton> <i v-if="registration.saving && registration.stage == 1" class="fas fa-spinner fa-pulse fa-fw"></i> </MkForm> </li> </ol> </template> </template> - <div v-if="data && !$i.twoFactorEnabled"> + <div v-if="twoFactorData && !$i.twoFactorEnabled"> <ol style="margin: 0; padding: 0 0 0 1em;"> <li> - <I18n :src="$ts._2fa.step1" tag="span"> + <I18n :src="i18n.ts._2fa.step1" tag="span"> <template #a> <a href="https://authy.com/" rel="noopener" target="_blank" class="_link">Authy</a> </template> @@ -52,19 +52,20 @@ </template> </I18n> </li> - <li>{{ $ts._2fa.step2 }}<br><img :src="data.qr"></li> - <li>{{ $ts._2fa.step3 }}<br> - <MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false"><template #label>{{ $ts.token }}</template></MkInput> - <MkButton primary @click="submit">{{ $ts.done }}</MkButton> + <li>{{ i18n.ts._2fa.step2 }}<br><img :src="twoFactorData.qr"><p>{{ $ts._2fa.step2Url }}<br>{{ twoFactorData.url }}</p></li> + <li> + {{ i18n.ts._2fa.step3 }}<br> + <MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false"><template #label>{{ i18n.ts.token }}</template></MkInput> + <MkButton primary @click="submit">{{ i18n.ts.done }}</MkButton> </li> </ol> - <MkInfo>{{ $ts._2fa.step4 }}</MkInfo> + <MkInfo>{{ i18n.ts._2fa.step4 }}</MkInfo> </div> </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { ref } from 'vue'; import { hostname } from '@/config'; import { byteify, hexify, stringify } from '@/scripts/2fa'; import MkButton from '@/components/ui/button.vue'; @@ -72,155 +73,144 @@ import MkInfo from '@/components/ui/info.vue'; import MkInput from '@/components/form/input.vue'; import MkSwitch from '@/components/form/switch.vue'; import * as os from '@/os'; -import * as symbols from '@/symbols'; +import { $i } from '@/account'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkButton, MkInfo, MkInput, MkSwitch - }, +const twoFactorData = ref<any>(null); +const supportsCredentials = ref(!!navigator.credentials); +const usePasswordLessLogin = ref($i!.usePasswordLessLogin); +const registration = ref<any>(null); +const keyName = ref(''); +const token = ref(null); - data() { - return { - data: null, - supportsCredentials: !!navigator.credentials, - usePasswordLessLogin: this.$i.usePasswordLessLogin, - registration: null, - keyName: '', - token: null, - }; - }, +function register() { + os.inputText({ + title: i18n.ts.password, + type: 'password' + }).then(({ canceled, result: password }) => { + if (canceled) return; + os.api('i/2fa/register', { + password: password + }).then(data => { + twoFactorData.value = data; + }); + }); +} - methods: { - register() { - os.inputText({ - title: this.$ts.password, - type: 'password' - }).then(({ canceled, result: password }) => { - if (canceled) return; - os.api('i/2fa/register', { - password: password - }).then(data => { - this.data = data; - }); - }); - }, +function unregister() { + os.inputText({ + title: i18n.ts.password, + type: 'password' + }).then(({ canceled, result: password }) => { + if (canceled) return; + os.api('i/2fa/unregister', { + password: password + }).then(() => { + usePasswordLessLogin.value = false; + updatePasswordLessLogin(); + }).then(() => { + os.success(); + $i!.twoFactorEnabled = false; + }); + }); +} - unregister() { - os.inputText({ - title: this.$ts.password, - type: 'password' - }).then(({ canceled, result: password }) => { - if (canceled) return; - os.api('i/2fa/unregister', { - password: password - }).then(() => { - this.usePasswordLessLogin = false; - this.updatePasswordLessLogin(); - }).then(() => { - os.success(); - this.$i.twoFactorEnabled = false; - }); - }); - }, +function submit() { + os.api('i/2fa/done', { + token: token.value + }).then(() => { + os.success(); + $i!.twoFactorEnabled = true; + }).catch(err => { + os.alert({ + type: 'error', + text: err, + }); + }); +} - submit() { - os.api('i/2fa/done', { - token: this.token - }).then(() => { - os.success(); - this.$i.twoFactorEnabled = true; - }).catch(e => { - os.alert({ - type: 'error', - text: e - }); - }); - }, +function registerKey() { + registration.value.saving = true; + os.api('i/2fa/key-done', { + password: registration.value.password, + name: keyName.value, + challengeId: registration.value.challengeId, + // we convert each 16 bits to a string to serialise + clientDataJSON: stringify(registration.value.credential.response.clientDataJSON), + attestationObject: hexify(registration.value.credential.response.attestationObject) + }).then(key => { + registration.value = null; + key.lastUsed = new Date(); + os.success(); + }); +} - registerKey() { - this.registration.saving = true; - os.api('i/2fa/key-done', { - password: this.registration.password, - name: this.keyName, - challengeId: this.registration.challengeId, - // we convert each 16 bits to a string to serialise - clientDataJSON: stringify(this.registration.credential.response.clientDataJSON), - attestationObject: hexify(this.registration.credential.response.attestationObject) - }).then(key => { - this.registration = null; - key.lastUsed = new Date(); - os.success(); - }) - }, +function unregisterKey(key) { + os.inputText({ + title: i18n.ts.password, + type: 'password' + }).then(({ canceled, result: password }) => { + if (canceled) return; + return os.api('i/2fa/remove-key', { + password, + credentialId: key.id + }).then(() => { + usePasswordLessLogin.value = false; + updatePasswordLessLogin(); + }).then(() => { + os.success(); + }); + }); +} - unregisterKey(key) { - os.inputText({ - title: this.$ts.password, - type: 'password' - }).then(({ canceled, result: password }) => { - if (canceled) return; - return os.api('i/2fa/remove-key', { - password, - credentialId: key.id - }).then(() => { - this.usePasswordLessLogin = false; - this.updatePasswordLessLogin(); - }).then(() => { - os.success(); - }); +function addSecurityKey() { + os.inputText({ + title: i18n.ts.password, + type: 'password' + }).then(({ canceled, result: password }) => { + if (canceled) return; + os.api('i/2fa/register-key', { + password + }).then(reg => { + registration.value = { + password, + challengeId: reg!.challengeId, + stage: 0, + publicKeyOptions: { + challenge: byteify(reg!.challenge, 'base64'), + rp: { + id: hostname, + name: 'Misskey' + }, + user: { + id: byteify($i!.id, 'ascii'), + name: $i!.username, + displayName: $i!.name, + }, + pubKeyCredParams: [{ alg: -7, type: 'public-key' }], + timeout: 60000, + attestation: 'direct' + }, + saving: true + }; + return navigator.credentials.create({ + publicKey: registration.value.publicKeyOptions }); - }, + }).then(credential => { + registration.value.credential = credential; + registration.value.saving = false; + registration.value.stage = 1; + }).catch(err => { + console.warn('Error while registering?', err); + registration.value.error = err.message; + registration.value.stage = -1; + }); + }); +} - addSecurityKey() { - os.inputText({ - title: this.$ts.password, - type: 'password' - }).then(({ canceled, result: password }) => { - if (canceled) return; - os.api('i/2fa/register-key', { - password - }).then(registration => { - this.registration = { - password, - challengeId: registration.challengeId, - stage: 0, - publicKeyOptions: { - challenge: byteify(registration.challenge, 'base64'), - rp: { - id: hostname, - name: 'Misskey' - }, - user: { - id: byteify(this.$i.id, 'ascii'), - name: this.$i.username, - displayName: this.$i.name, - }, - pubKeyCredParams: [{ alg: -7, type: 'public-key' }], - timeout: 60000, - attestation: 'direct' - }, - saving: true - }; - return navigator.credentials.create({ - publicKey: this.registration.publicKeyOptions - }); - }).then(credential => { - this.registration.credential = credential; - this.registration.saving = false; - this.registration.stage = 1; - }).catch(err => { - console.warn('Error while registering?', err); - this.registration.error = err.message; - this.registration.stage = -1; - }); - }); - }, - - updatePasswordLessLogin() { - os.api('i/2fa/password-less', { - value: !!this.usePasswordLessLogin - }); - } - } -}); +async function updatePasswordLessLogin() { + await os.api('i/2fa/password-less', { + value: !!usePasswordLessLogin.value + }); +} </script> diff --git a/packages/client/src/pages/settings/account-info.vue b/packages/client/src/pages/settings/account-info.vue index c98ad056f6..12142b4dc1 100644 --- a/packages/client/src/pages/settings/account-info.vue +++ b/packages/client/src/pages/settings/account-info.vue @@ -7,163 +7,150 @@ <FormSection> <MkKeyValue> - <template #key>{{ $ts.registeredDate }}</template> + <template #key>{{ i18n.ts.registeredDate }}</template> <template #value><MkTime :time="$i.createdAt" mode="detail"/></template> </MkKeyValue> </FormSection> <FormSection v-if="stats"> - <template #label>{{ $ts.statistics }}</template> + <template #label>{{ i18n.ts.statistics }}</template> <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ $ts.notesCount }}</template> + <template #key>{{ i18n.ts.notesCount }}</template> <template #value>{{ number(stats.notesCount) }}</template> </MkKeyValue> <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ $ts.repliesCount }}</template> + <template #key>{{ i18n.ts.repliesCount }}</template> <template #value>{{ number(stats.repliesCount) }}</template> </MkKeyValue> <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ $ts.renotesCount }}</template> + <template #key>{{ i18n.ts.renotesCount }}</template> <template #value>{{ number(stats.renotesCount) }}</template> </MkKeyValue> <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ $ts.repliedCount }}</template> + <template #key>{{ i18n.ts.repliedCount }}</template> <template #value>{{ number(stats.repliedCount) }}</template> </MkKeyValue> <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ $ts.renotedCount }}</template> + <template #key>{{ i18n.ts.renotedCount }}</template> <template #value>{{ number(stats.renotedCount) }}</template> </MkKeyValue> <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ $ts.pollVotesCount }}</template> + <template #key>{{ i18n.ts.pollVotesCount }}</template> <template #value>{{ number(stats.pollVotesCount) }}</template> </MkKeyValue> <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ $ts.pollVotedCount }}</template> + <template #key>{{ i18n.ts.pollVotedCount }}</template> <template #value>{{ number(stats.pollVotedCount) }}</template> </MkKeyValue> <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ $ts.sentReactionsCount }}</template> + <template #key>{{ i18n.ts.sentReactionsCount }}</template> <template #value>{{ number(stats.sentReactionsCount) }}</template> </MkKeyValue> <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ $ts.receivedReactionsCount }}</template> + <template #key>{{ i18n.ts.receivedReactionsCount }}</template> <template #value>{{ number(stats.receivedReactionsCount) }}</template> </MkKeyValue> <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ $ts.noteFavoritesCount }}</template> + <template #key>{{ i18n.ts.noteFavoritesCount }}</template> <template #value>{{ number(stats.noteFavoritesCount) }}</template> </MkKeyValue> <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ $ts.followingCount }}</template> + <template #key>{{ i18n.ts.followingCount }}</template> <template #value>{{ number(stats.followingCount) }}</template> </MkKeyValue> <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ $ts.followingCount }} ({{ $ts.local }})</template> + <template #key>{{ i18n.ts.followingCount }} ({{ i18n.ts.local }})</template> <template #value>{{ number(stats.localFollowingCount) }}</template> </MkKeyValue> <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ $ts.followingCount }} ({{ $ts.remote }})</template> + <template #key>{{ i18n.ts.followingCount }} ({{ i18n.ts.remote }})</template> <template #value>{{ number(stats.remoteFollowingCount) }}</template> </MkKeyValue> <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ $ts.followersCount }}</template> + <template #key>{{ i18n.ts.followersCount }}</template> <template #value>{{ number(stats.followersCount) }}</template> </MkKeyValue> <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ $ts.followersCount }} ({{ $ts.local }})</template> + <template #key>{{ i18n.ts.followersCount }} ({{ i18n.ts.local }})</template> <template #value>{{ number(stats.localFollowersCount) }}</template> </MkKeyValue> <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ $ts.followersCount }} ({{ $ts.remote }})</template> + <template #key>{{ i18n.ts.followersCount }} ({{ i18n.ts.remote }})</template> <template #value>{{ number(stats.remoteFollowersCount) }}</template> </MkKeyValue> <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ $ts.pageLikesCount }}</template> + <template #key>{{ i18n.ts.pageLikesCount }}</template> <template #value>{{ number(stats.pageLikesCount) }}</template> </MkKeyValue> <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ $ts.pageLikedCount }}</template> + <template #key>{{ i18n.ts.pageLikedCount }}</template> <template #value>{{ number(stats.pageLikedCount) }}</template> </MkKeyValue> <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ $ts.driveFilesCount }}</template> + <template #key>{{ i18n.ts.driveFilesCount }}</template> <template #value>{{ number(stats.driveFilesCount) }}</template> </MkKeyValue> <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ $ts.driveUsage }}</template> + <template #key>{{ i18n.ts.driveUsage }}</template> <template #value>{{ bytes(stats.driveUsage) }}</template> </MkKeyValue> </FormSection> <FormSection> - <template #label>{{ $ts.other }}</template> + <template #label>{{ i18n.ts.other }}</template> <MkKeyValue oneline style="margin: 1em 0;"> <template #key>emailVerified</template> - <template #value>{{ $i.emailVerified ? $ts.yes : $ts.no }}</template> + <template #value>{{ $i.emailVerified ? i18n.ts.yes : i18n.ts.no }}</template> </MkKeyValue> <MkKeyValue oneline style="margin: 1em 0;"> <template #key>twoFactorEnabled</template> - <template #value>{{ $i.twoFactorEnabled ? $ts.yes : $ts.no }}</template> + <template #value>{{ $i.twoFactorEnabled ? i18n.ts.yes : i18n.ts.no }}</template> </MkKeyValue> <MkKeyValue oneline style="margin: 1em 0;"> <template #key>securityKeys</template> - <template #value>{{ $i.securityKeys ? $ts.yes : $ts.no }}</template> + <template #value>{{ $i.securityKeys ? i18n.ts.yes : i18n.ts.no }}</template> </MkKeyValue> <MkKeyValue oneline style="margin: 1em 0;"> <template #key>usePasswordLessLogin</template> - <template #value>{{ $i.usePasswordLessLogin ? $ts.yes : $ts.no }}</template> + <template #value>{{ $i.usePasswordLessLogin ? i18n.ts.yes : i18n.ts.no }}</template> </MkKeyValue> <MkKeyValue oneline style="margin: 1em 0;"> <template #key>isModerator</template> - <template #value>{{ $i.isModerator ? $ts.yes : $ts.no }}</template> + <template #value>{{ $i.isModerator ? i18n.ts.yes : i18n.ts.no }}</template> </MkKeyValue> <MkKeyValue oneline style="margin: 1em 0;"> <template #key>isAdmin</template> - <template #value>{{ $i.isAdmin ? $ts.yes : $ts.no }}</template> + <template #value>{{ $i.isAdmin ? i18n.ts.yes : i18n.ts.no }}</template> </MkKeyValue> </FormSection> </div> </template> -<script lang="ts"> -import { defineAsyncComponent, defineComponent } from 'vue'; +<script lang="ts" setup> +import { defineExpose, onMounted, ref } from 'vue'; import FormSection from '@/components/form/section.vue'; import MkKeyValue from '@/components/key-value.vue'; import * as os from '@/os'; import number from '@/filters/number'; import bytes from '@/filters/bytes'; import * as symbols from '@/symbols'; +import { $i } from '@/account'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - FormSection, - MkKeyValue, - }, +const stats = ref<any>({}); - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.accountInfo, - icon: 'fas fa-info-circle' - }, - stats: null - } - }, - - mounted() { - os.api('users/stats', { - userId: this.$i.id - }).then(stats => { - this.stats = stats; - }); - }, +onMounted(() => { + os.api('users/stats', { + userId: $i!.id + }).then(response => { + stats.value = response; + }); +}); - methods: { - number, - bytes, +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.accountInfo, + icon: 'fas fa-info-circle' } }); </script> diff --git a/packages/client/src/pages/settings/accounts.vue b/packages/client/src/pages/settings/accounts.vue index a744a031d4..5e75639c55 100644 --- a/packages/client/src/pages/settings/accounts.vue +++ b/packages/client/src/pages/settings/accounts.vue @@ -1,7 +1,7 @@ <template> <div class="_formRoot"> <FormSuspense :p="init"> - <FormButton primary @click="addAccount"><i class="fas fa-plus"></i> {{ $ts.addAccount }}</FormButton> + <FormButton primary @click="addAccount"><i class="fas fa-plus"></i> {{ i18n.ts.addAccount }}</FormButton> <div v-for="account in accounts" :key="account.id" class="_panel _button lcjjdxlm" @click="menu(account, $event)"> <div class="avatar"> @@ -20,90 +20,89 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { defineAsyncComponent, defineExpose, ref } from 'vue'; import FormSuspense from '@/components/form/suspense.vue'; import FormButton from '@/components/ui/button.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; -import { getAccounts, addAccount, login } from '@/account'; +import { getAccounts, addAccount as addAccounts, login, $i } from '@/account'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - FormSuspense, - FormButton, - }, +const storedAccounts = ref<any>(null); +const accounts = ref<any>(null); - emits: ['info'], +const init = async () => { + getAccounts().then(accounts => { + storedAccounts.value = accounts.filter(x => x.id !== $i!.id); - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.accounts, - icon: 'fas fa-users', - bg: 'var(--bg)', - }, - storedAccounts: getAccounts().then(accounts => accounts.filter(x => x.id !== this.$i.id)), - accounts: null, - init: async () => os.api('users/show', { - userIds: (await this.storedAccounts).map(x => x.id) - }).then(accounts => { - this.accounts = accounts; - }), - }; - }, + console.log(storedAccounts.value); - methods: { - menu(account, ev) { - os.popupMenu([{ - text: this.$ts.switch, - icon: 'fas fa-exchange-alt', - action: () => this.switchAccount(account), - }, { - text: this.$ts.remove, - icon: 'fas fa-trash-alt', - danger: true, - action: () => this.removeAccount(account), - }], ev.currentTarget ?? ev.target); - }, + return os.api('users/show', { + userIds: storedAccounts.value.map(x => x.id) + }); + }).then(response => { + accounts.value = response; + console.log(accounts.value); + }); +}; - addAccount(ev) { - os.popupMenu([{ - text: this.$ts.existingAccount, - action: () => { this.addExistingAccount(); }, - }, { - text: this.$ts.createAccount, - action: () => { this.createAccount(); }, - }], ev.currentTarget ?? ev.target); - }, +function menu(account, ev) { + os.popupMenu([{ + text: i18n.ts.switch, + icon: 'fas fa-exchange-alt', + action: () => switchAccount(account), + }, { + text: i18n.ts.remove, + icon: 'fas fa-trash-alt', + danger: true, + action: () => removeAccount(account), + }], ev.currentTarget ?? ev.target); +} - addExistingAccount() { - os.popup(import('@/components/signin-dialog.vue'), {}, { - done: res => { - addAccount(res.id, res.i); - os.success(); - }, - }, 'closed'); - }, +function addAccount(ev) { + os.popupMenu([{ + text: i18n.ts.existingAccount, + action: () => { addExistingAccount(); }, + }, { + text: i18n.ts.createAccount, + action: () => { createAccount(); }, + }], ev.currentTarget ?? ev.target); +} - createAccount() { - os.popup(import('@/components/signup-dialog.vue'), {}, { - done: res => { - addAccount(res.id, res.i); - this.switchAccountWithToken(res.i); - }, - }, 'closed'); +function addExistingAccount() { + os.popup(defineAsyncComponent(() => import('@/components/signin-dialog.vue')), {}, { + done: res => { + addAccounts(res.id, res.i); + os.success(); }, + }, 'closed'); +} - async switchAccount(account: any) { - const storedAccounts = await getAccounts(); - const token = storedAccounts.find(x => x.id === account.id).token; - this.switchAccountWithToken(token); +function createAccount() { + os.popup(defineAsyncComponent(() => import('@/components/signup-dialog.vue')), {}, { + done: res => { + addAccounts(res.id, res.i); + switchAccountWithToken(res.i); }, + }, 'closed'); +} - switchAccountWithToken(token: string) { - login(token); - }, +async function switchAccount(account: any) { + const fetchedAccounts: any[] = await getAccounts(); + const token = fetchedAccounts.find(x => x.id === account.id).token; + switchAccountWithToken(token); +} + +function switchAccountWithToken(token: string) { + login(token); +} + +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.accounts, + icon: 'fas fa-users', + bg: 'var(--bg)', } }); </script> diff --git a/packages/client/src/pages/settings/api.vue b/packages/client/src/pages/settings/api.vue index 20ff2a8d96..e6375763f1 100644 --- a/packages/client/src/pages/settings/api.vue +++ b/packages/client/src/pages/settings/api.vue @@ -1,56 +1,45 @@ <template> <div class="_formRoot"> - <FormButton primary class="_formBlock" @click="generateToken">{{ $ts.generateAccessToken }}</FormButton> - <FormLink to="/settings/apps" class="_formBlock">{{ $ts.manageAccessTokens }}</FormLink> + <FormButton primary class="_formBlock" @click="generateToken">{{ i18n.ts.generateAccessToken }}</FormButton> + <FormLink to="/settings/apps" class="_formBlock">{{ i18n.ts.manageAccessTokens }}</FormLink> <FormLink to="/api-console" :behavior="isDesktop ? 'window' : null" class="_formBlock">API console</FormLink> </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { defineAsyncComponent, defineExpose, ref } from 'vue'; import FormLink from '@/components/form/link.vue'; import FormButton from '@/components/ui/button.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - FormButton, - FormLink, - }, +const isDesktop = ref(window.innerWidth >= 1100); - emits: ['info'], +function generateToken() { + os.popup(defineAsyncComponent(() => import('@/components/token-generate-window.vue')), {}, { + done: async result => { + const { name, permissions } = result; + const { token } = await os.api('miauth/gen-token', { + session: null, + name: name, + permission: permissions, + }); - data() { - return { - [symbols.PAGE_INFO]: { - title: 'API', - icon: 'fas fa-key', - bg: 'var(--bg)', - }, - isDesktop: window.innerWidth >= 1100, - }; - }, - - methods: { - generateToken() { - os.popup(import('@/components/token-generate-window.vue'), {}, { - done: async result => { - const { name, permissions } = result; - const { token } = await os.api('miauth/gen-token', { - session: null, - name: name, - permission: permissions, - }); - - os.alert({ - type: 'success', - title: this.$ts.token, - text: token - }); - }, - }, 'closed'); + os.alert({ + type: 'success', + title: i18n.ts.token, + text: token + }); }, + }, 'closed'); +} + +defineExpose({ + [symbols.PAGE_INFO]: { + title: 'API', + icon: 'fas fa-key', + bg: 'var(--bg)', } }); </script> diff --git a/packages/client/src/pages/settings/apps.vue b/packages/client/src/pages/settings/apps.vue index 9c0fa8a54d..7b0b5548d5 100644 --- a/packages/client/src/pages/settings/apps.vue +++ b/packages/client/src/pages/settings/apps.vue @@ -4,7 +4,7 @@ <template #empty> <div class="_fullinfo"> <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> - <div>{{ $ts.nothing }}</div> + <div>{{ i18n.ts.nothing }}</div> </div> </template> <template v-slot="{items}"> @@ -14,18 +14,18 @@ <div class="name">{{ token.name }}</div> <div class="description">{{ token.description }}</div> <div class="_keyValue"> - <div>{{ $ts.installedDate }}:</div> + <div>{{ i18n.ts.installedDate }}:</div> <div><MkTime :time="token.createdAt"/></div> </div> <div class="_keyValue"> - <div>{{ $ts.lastUsedDate }}:</div> + <div>{{ i18n.ts.lastUsedDate }}:</div> <div><MkTime :time="token.lastUsedAt"/></div> </div> <div class="actions"> <button class="_button" @click="revoke(token)"><i class="fas fa-trash-alt"></i></button> </div> <details> - <summary>{{ $ts.details }}</summary> + <summary>{{ i18n.ts.details }}</summary> <ul> <li v-for="p in token.permission" :key="p">{{ $t(`_permissions.${p}`) }}</li> </ul> @@ -37,42 +37,34 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { defineExpose, ref } from 'vue'; import FormPagination from '@/components/ui/pagination.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - FormPagination, - }, +const list = ref<any>(null); - emits: ['info'], +const pagination = { + endpoint: 'i/apps' as const, + limit: 100, + params: { + sort: '+lastUsedAt' + } +}; - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.installedApps, - icon: 'fas fa-plug', - bg: 'var(--bg)', - }, - pagination: { - endpoint: 'i/apps' as const, - limit: 100, - params: { - sort: '+lastUsedAt' - } - }, - }; - }, +function revoke(token) { + os.api('i/revoke-token', { tokenId: token.id }).then(() => { + list.value.reload(); + }); +} - methods: { - revoke(token) { - os.api('i/revoke-token', { tokenId: token.id }).then(() => { - this.$refs.list.reload(); - }); - } +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.installedApps, + icon: 'fas fa-plug', + bg: 'var(--bg)', } }); </script> diff --git a/packages/client/src/pages/settings/custom-css.vue b/packages/client/src/pages/settings/custom-css.vue index 556ee30c1d..20db077ceb 100644 --- a/packages/client/src/pages/settings/custom-css.vue +++ b/packages/client/src/pages/settings/custom-css.vue @@ -1,6 +1,6 @@ <template> <div class="_formRoot"> - <FormInfo warn class="_formBlock">{{ $ts.customCssWarn }}</FormInfo> + <FormInfo warn class="_formBlock">{{ i18n.ts.customCssWarn }}</FormInfo> <FormTextarea v-model="localCustomCss" manual-save tall class="_monospace _formBlock" style="tab-size: 2;"> <template #label>CSS</template> @@ -8,50 +8,38 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { defineExpose, ref, watch } from 'vue'; import FormTextarea from '@/components/form/textarea.vue'; import FormInfo from '@/components/ui/info.vue'; import * as os from '@/os'; import { unisonReload } from '@/scripts/unison-reload'; import * as symbols from '@/symbols'; -import { defaultStore } from '@/store'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - FormTextarea, - FormInfo, - }, +const localCustomCss = ref(localStorage.getItem('customCss') ?? ''); - emits: ['info'], +async function apply() { + localStorage.setItem('customCss', localCustomCss.value); - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.customCss, - icon: 'fas fa-code', - bg: 'var(--bg)', - }, - localCustomCss: localStorage.getItem('customCss') - } - }, + const { canceled } = await os.confirm({ + type: 'info', + text: i18n.ts.reloadToApplySetting, + }); + if (canceled) return; - mounted() { - this.$watch('localCustomCss', this.apply); - }, + unisonReload(); +} - methods: { - async apply() { - localStorage.setItem('customCss', this.localCustomCss); - - const { canceled } = await os.confirm({ - type: 'info', - text: this.$ts.reloadToApplySetting, - }); - if (canceled) return; +watch(localCustomCss, async () => { + await apply(); +}); - unisonReload(); - } +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.customCss, + icon: 'fas fa-code', + bg: 'var(--bg)', } }); </script> diff --git a/packages/client/src/pages/settings/deck.vue b/packages/client/src/pages/settings/deck.vue index 46b90d3d1a..2d868aa0a7 100644 --- a/packages/client/src/pages/settings/deck.vue +++ b/packages/client/src/pages/settings/deck.vue @@ -1,36 +1,36 @@ <template> <div class="_formRoot"> <FormGroup> - <template #label>{{ $ts.defaultNavigationBehaviour }}</template> - <FormSwitch v-model="navWindow">{{ $ts.openInWindow }}</FormSwitch> + <template #label>{{ i18n.ts.defaultNavigationBehaviour }}</template> + <FormSwitch v-model="navWindow">{{ i18n.ts.openInWindow }}</FormSwitch> </FormGroup> - <FormSwitch v-model="alwaysShowMainColumn" class="_formBlock">{{ $ts._deck.alwaysShowMainColumn }}</FormSwitch> + <FormSwitch v-model="alwaysShowMainColumn" class="_formBlock">{{ i18n.ts._deck.alwaysShowMainColumn }}</FormSwitch> <FormRadios v-model="columnAlign" class="_formBlock"> - <template #label>{{ $ts._deck.columnAlign }}</template> - <option value="left">{{ $ts.left }}</option> - <option value="center">{{ $ts.center }}</option> + <template #label>{{ i18n.ts._deck.columnAlign }}</template> + <option value="left">{{ i18n.ts.left }}</option> + <option value="center">{{ i18n.ts.center }}</option> </FormRadios> <FormRadios v-model="columnHeaderHeight" class="_formBlock"> - <template #label>{{ $ts._deck.columnHeaderHeight }}</template> - <option :value="42">{{ $ts.narrow }}</option> - <option :value="45">{{ $ts.medium }}</option> - <option :value="48">{{ $ts.wide }}</option> + <template #label>{{ i18n.ts._deck.columnHeaderHeight }}</template> + <option :value="42">{{ i18n.ts.narrow }}</option> + <option :value="45">{{ i18n.ts.medium }}</option> + <option :value="48">{{ i18n.ts.wide }}</option> </FormRadios> <FormInput v-model="columnMargin" type="number" class="_formBlock"> - <template #label>{{ $ts._deck.columnMargin }}</template> + <template #label>{{ i18n.ts._deck.columnMargin }}</template> <template #suffix>px</template> </FormInput> - <FormLink class="_formBlock" @click="setProfile">{{ $ts._deck.profile }}<template #suffix>{{ profile }}</template></FormLink> + <FormLink class="_formBlock" @click="setProfile">{{ i18n.ts._deck.profile }}<template #suffix>{{ profile }}</template></FormLink> </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { computed, defineExpose, watch } from 'vue'; import FormSwitch from '@/components/form/switch.vue'; import FormLink from '@/components/form/link.vue'; import FormRadios from '@/components/form/radios.vue'; @@ -40,59 +40,41 @@ import { deckStore } from '@/ui/deck/deck-store'; import * as os from '@/os'; import { unisonReload } from '@/scripts/unison-reload'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - FormSwitch, - FormLink, - FormInput, - FormRadios, - FormGroup, - }, +const navWindow = computed(deckStore.makeGetterSetter('navWindow')); +const alwaysShowMainColumn = computed(deckStore.makeGetterSetter('alwaysShowMainColumn')); +const columnAlign = computed(deckStore.makeGetterSetter('columnAlign')); +const columnMargin = computed(deckStore.makeGetterSetter('columnMargin')); +const columnHeaderHeight = computed(deckStore.makeGetterSetter('columnHeaderHeight')); +const profile = computed(deckStore.makeGetterSetter('profile')); - emits: ['info'], +watch(navWindow, async () => { + const { canceled } = await os.confirm({ + type: 'info', + text: i18n.ts.reloadToApplySetting, + }); + if (canceled) return; - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.deck, - icon: 'fas fa-columns', - bg: 'var(--bg)', - }, - } - }, - - computed: { - navWindow: deckStore.makeGetterSetter('navWindow'), - alwaysShowMainColumn: deckStore.makeGetterSetter('alwaysShowMainColumn'), - columnAlign: deckStore.makeGetterSetter('columnAlign'), - columnMargin: deckStore.makeGetterSetter('columnMargin'), - columnHeaderHeight: deckStore.makeGetterSetter('columnHeaderHeight'), - profile: deckStore.makeGetterSetter('profile'), - }, - - watch: { - async navWindow() { - const { canceled } = await os.confirm({ - type: 'info', - text: this.$ts.reloadToApplySetting, - }); - if (canceled) return; + unisonReload(); +}); - unisonReload(); - } - }, +async function setProfile() { + const { canceled, result: name } = await os.inputText({ + title: i18n.ts._deck.profile, + allowEmpty: false + }); + if (canceled) return; + + profile.value = name; + unisonReload(); +} - methods: { - async setProfile() { - const { canceled, result: name } = await os.inputText({ - title: this.$ts._deck.profile, - allowEmpty: false - }); - if (canceled) return; - this.profile = name; - unisonReload(); - } +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.deck, + icon: 'fas fa-columns', + bg: 'var(--bg)', } }); </script> diff --git a/packages/client/src/pages/settings/delete-account.vue b/packages/client/src/pages/settings/delete-account.vue index 7edc81a309..e9f19aaf0b 100644 --- a/packages/client/src/pages/settings/delete-account.vue +++ b/packages/client/src/pages/settings/delete-account.vue @@ -1,64 +1,52 @@ <template> <div class="_formRoot"> - <FormInfo warn class="_formBlock">{{ $ts._accountDelete.mayTakeTime }}</FormInfo> - <FormInfo class="_formBlock">{{ $ts._accountDelete.sendEmail }}</FormInfo> - <FormButton v-if="!$i.isDeleted" danger class="_formBlock" @click="deleteAccount">{{ $ts._accountDelete.requestAccountDelete }}</FormButton> - <FormButton v-else disabled>{{ $ts._accountDelete.inProgress }}</FormButton> + <FormInfo warn class="_formBlock">{{ i18n.ts._accountDelete.mayTakeTime }}</FormInfo> + <FormInfo class="_formBlock">{{ i18n.ts._accountDelete.sendEmail }}</FormInfo> + <FormButton v-if="!$i.isDeleted" danger class="_formBlock" @click="deleteAccount">{{ i18n.ts._accountDelete.requestAccountDelete }}</FormButton> + <FormButton v-else disabled>{{ i18n.ts._accountDelete.inProgress }}</FormButton> </div> </template> -<script lang="ts"> -import { defineAsyncComponent, defineComponent } from 'vue'; +<script lang="ts" setup> +import { defineExpose } from 'vue'; import FormInfo from '@/components/ui/info.vue'; import FormButton from '@/components/ui/button.vue'; import * as os from '@/os'; import { signout } from '@/account'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - FormButton, - FormInfo, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts._accountDelete.accountDelete, - icon: 'fas fa-exclamation-triangle', - bg: 'var(--bg)', - }, - } - }, +async function deleteAccount() { + { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.ts.deleteAccountConfirm, + }); + if (canceled) return; + } - methods: { - async deleteAccount() { - { - const { canceled } = await os.confirm({ - type: 'warning', - text: this.$ts.deleteAccountConfirm, - }); - if (canceled) return; - } + const { canceled, result: password } = await os.inputText({ + title: i18n.ts.password, + type: 'password' + }); + if (canceled) return; - const { canceled, result: password } = await os.inputText({ - title: this.$ts.password, - type: 'password' - }); - if (canceled) return; + await os.apiWithDialog('i/delete-account', { + password: password + }); - await os.apiWithDialog('i/delete-account', { - password: password - }); + await os.alert({ + title: i18n.ts._accountDelete.started, + }); - await os.alert({ - title: this.$ts._accountDelete.started, - }); + await signout(); +} - signout(); - } +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts._accountDelete.accountDelete, + icon: 'fas fa-exclamation-triangle', + bg: 'var(--bg)', } }); </script> diff --git a/packages/client/src/pages/settings/drive.vue b/packages/client/src/pages/settings/drive.vue index 9309eb5ec7..09a2537ed5 100644 --- a/packages/client/src/pages/settings/drive.vue +++ b/packages/client/src/pages/settings/drive.vue @@ -1,41 +1,41 @@ <template> <div class="_formRoot"> <FormSection v-if="!fetching"> - <template #label>{{ $ts.usageAmount }}</template> + <template #label>{{ i18n.ts.usageAmount }}</template> <div class="_formBlock uawsfosz"> <div class="meter"><div :style="meterStyle"></div></div> </div> <FormSplit> <MkKeyValue class="_formBlock"> - <template #key>{{ $ts.capacity }}</template> + <template #key>{{ i18n.ts.capacity }}</template> <template #value>{{ bytes(capacity, 1) }}</template> </MkKeyValue> <MkKeyValue class="_formBlock"> - <template #key>{{ $ts.inUse }}</template> + <template #key>{{ i18n.ts.inUse }}</template> <template #value>{{ bytes(usage, 1) }}</template> </MkKeyValue> </FormSplit> </FormSection> <FormSection> - <template #label>{{ $ts.statistics }}</template> + <template #label>{{ i18n.ts.statistics }}</template> <MkChart src="per-user-drive" :args="{ user: $i }" span="day" :limit="7 * 5" :bar="true" :stacked="true" :detailed="false" :aspect-ratio="6"/> </FormSection> <FormSection> <FormLink @click="chooseUploadFolder()"> - {{ $ts.uploadFolder }} + {{ i18n.ts.uploadFolder }} <template #suffix>{{ uploadFolder ? uploadFolder.name : '-' }}</template> <template #suffixIcon><i class="fas fa-folder-open"></i></template> </FormLink> - <FormSwitch v-model="keepOriginalUploading" class="_formBlock">{{ $ts.keepOriginalUploading }}<template #caption>{{ $ts.keepOriginalUploadingDescription }}</template></FormSwitch> + <FormSwitch v-model="keepOriginalUploading" class="_formBlock">{{ i18n.ts.keepOriginalUploading }}<template #caption>{{ i18n.ts.keepOriginalUploadingDescription }}</template></FormSwitch> </FormSection> </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; -import * as tinycolor from 'tinycolor2'; +<script lang="ts" setup> +import { computed, defineExpose, ref } from 'vue'; +import tinycolor from 'tinycolor2'; import FormLink from '@/components/form/link.vue'; import FormSwitch from '@/components/form/switch.vue'; import FormSection from '@/components/form/section.vue'; @@ -46,80 +46,59 @@ import bytes from '@/filters/bytes'; import * as symbols from '@/symbols'; import { defaultStore } from '@/store'; import MkChart from '@/components/chart.vue'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - FormLink, - FormSwitch, - FormSection, - MkKeyValue, - FormSplit, - MkChart, - }, +const fetching = ref(true); +const usage = ref<any>(null); +const capacity = ref<any>(null); +const uploadFolder = ref<any>(null); - emits: ['info'], +const meterStyle = computed(() => { + return { + width: `${usage.value / capacity.value * 100}%`, + background: tinycolor({ + h: 180 - (usage.value / capacity.value * 180), + s: 0.7, + l: 0.5 + }) + }; +}); - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.drive, - icon: 'fas fa-cloud', - bg: 'var(--bg)', - }, - fetching: true, - usage: null, - capacity: null, - uploadFolder: null, - } - }, +const keepOriginalUploading = computed(defaultStore.makeGetterSetter('keepOriginalUploading')); - computed: { - meterStyle(): any { - return { - width: `${this.usage / this.capacity * 100}%`, - background: tinycolor({ - h: 180 - (this.usage / this.capacity * 180), - s: 0.7, - l: 0.5 - }) - }; - }, - keepOriginalUploading: defaultStore.makeGetterSetter('keepOriginalUploading'), - }, +os.api('drive').then(info => { + capacity.value = info.capacity; + usage.value = info.usage; + fetching.value = false; +}); - async created() { - os.api('drive').then(info => { - this.capacity = info.capacity; - this.usage = info.usage; - this.fetching = false; - this.$nextTick(() => { - this.renderChart(); - }); - }); +if (defaultStore.state.uploadFolder) { + os.api('drive/folders/show', { + folderId: defaultStore.state.uploadFolder + }).then(response => { + uploadFolder.value = response; + }); +} - if (this.$store.state.uploadFolder) { - this.uploadFolder = await os.api('drive/folders/show', { - folderId: this.$store.state.uploadFolder +function chooseUploadFolder() { + os.selectDriveFolder(false).then(async folder => { + defaultStore.set('uploadFolder', folder ? folder.id : null); + os.success(); + if (defaultStore.state.uploadFolder) { + uploadFolder.value = await os.api('drive/folders/show', { + folderId: defaultStore.state.uploadFolder }); + } else { + uploadFolder.value = null; } - }, - - methods: { - chooseUploadFolder() { - os.selectDriveFolder(false).then(async folder => { - this.$store.set('uploadFolder', folder ? folder.id : null); - os.success(); - if (this.$store.state.uploadFolder) { - this.uploadFolder = await os.api('drive/folders/show', { - folderId: this.$store.state.uploadFolder - }); - } else { - this.uploadFolder = null; - } - }); - }, + }); +} - bytes +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.drive, + icon: 'fas fa-cloud', + bg: 'var(--bg)', } }); </script> diff --git a/packages/client/src/pages/settings/email.vue b/packages/client/src/pages/settings/email.vue index 4697fec9b7..37f14068e2 100644 --- a/packages/client/src/pages/settings/email.vue +++ b/packages/client/src/pages/settings/email.vue @@ -39,8 +39,8 @@ </div> </template> -<script lang="ts"> -import { defineComponent, onMounted, ref, watch } from 'vue'; +<script lang="ts" setup> +import { defineExpose, onMounted, ref, watch } from 'vue'; import FormSection from '@/components/form/section.vue'; import FormInput from '@/components/form/input.vue'; import FormSwitch from '@/components/form/switch.vue'; @@ -49,79 +49,62 @@ import * as symbols from '@/symbols'; import { $i } from '@/account'; import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - FormSection, - FormSwitch, - FormInput, - }, +const emailAddress = ref($i!.email); - emits: ['info'], +const onChangeReceiveAnnouncementEmail = (v) => { + os.api('i/update', { + receiveAnnouncementEmail: v + }); +}; - setup(props, context) { - const emailAddress = ref($i.email); - - const INFO = { - title: i18n.ts.email, - icon: 'fas fa-envelope', - bg: 'var(--bg)', - }; - - const onChangeReceiveAnnouncementEmail = (v) => { - os.api('i/update', { - receiveAnnouncementEmail: v - }); - }; - - const saveEmailAddress = () => { - os.inputText({ - title: i18n.ts.password, - type: 'password' - }).then(({ canceled, result: password }) => { - if (canceled) return; - os.apiWithDialog('i/update-email', { - password: password, - email: emailAddress.value, - }); - }); - }; +const saveEmailAddress = () => { + os.inputText({ + title: i18n.ts.password, + type: 'password' + }).then(({ canceled, result: password }) => { + if (canceled) return; + os.apiWithDialog('i/update-email', { + password: password, + email: emailAddress.value, + }); + }); +}; - const emailNotification_mention = ref($i.emailNotificationTypes.includes('mention')); - const emailNotification_reply = ref($i.emailNotificationTypes.includes('reply')); - const emailNotification_quote = ref($i.emailNotificationTypes.includes('quote')); - const emailNotification_follow = ref($i.emailNotificationTypes.includes('follow')); - const emailNotification_receiveFollowRequest = ref($i.emailNotificationTypes.includes('receiveFollowRequest')); - const emailNotification_groupInvited = ref($i.emailNotificationTypes.includes('groupInvited')); +const emailNotification_mention = ref($i!.emailNotificationTypes.includes('mention')); +const emailNotification_reply = ref($i!.emailNotificationTypes.includes('reply')); +const emailNotification_quote = ref($i!.emailNotificationTypes.includes('quote')); +const emailNotification_follow = ref($i!.emailNotificationTypes.includes('follow')); +const emailNotification_receiveFollowRequest = ref($i!.emailNotificationTypes.includes('receiveFollowRequest')); +const emailNotification_groupInvited = ref($i!.emailNotificationTypes.includes('groupInvited')); - const saveNotificationSettings = () => { - os.api('i/update', { - emailNotificationTypes: [ - ...[emailNotification_mention.value ? 'mention' : null], - ...[emailNotification_reply.value ? 'reply' : null], - ...[emailNotification_quote.value ? 'quote' : null], - ...[emailNotification_follow.value ? 'follow' : null], - ...[emailNotification_receiveFollowRequest.value ? 'receiveFollowRequest' : null], - ...[emailNotification_groupInvited.value ? 'groupInvited' : null], - ].filter(x => x != null) - }); - }; +const saveNotificationSettings = () => { + os.api('i/update', { + emailNotificationTypes: [ + ...[emailNotification_mention.value ? 'mention' : null], + ...[emailNotification_reply.value ? 'reply' : null], + ...[emailNotification_quote.value ? 'quote' : null], + ...[emailNotification_follow.value ? 'follow' : null], + ...[emailNotification_receiveFollowRequest.value ? 'receiveFollowRequest' : null], + ...[emailNotification_groupInvited.value ? 'groupInvited' : null], + ].filter(x => x != null) + }); +}; - watch([emailNotification_mention, emailNotification_reply, emailNotification_quote, emailNotification_follow, emailNotification_receiveFollowRequest, emailNotification_groupInvited], () => { - saveNotificationSettings(); - }); +watch([emailNotification_mention, emailNotification_reply, emailNotification_quote, emailNotification_follow, emailNotification_receiveFollowRequest, emailNotification_groupInvited], () => { + saveNotificationSettings(); +}); - onMounted(() => { - watch(emailAddress, () => { - saveEmailAddress(); - }); - }); +onMounted(() => { + watch(emailAddress, () => { + saveEmailAddress(); + }); +}); - return { - [symbols.PAGE_INFO]: INFO, - emailAddress, - onChangeReceiveAnnouncementEmail, - emailNotification_mention, emailNotification_reply, emailNotification_quote, emailNotification_follow, emailNotification_receiveFollowRequest, emailNotification_groupInvited, - }; - }, +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.email, + icon: 'fas fa-envelope', + bg: 'var(--bg)', + } }); </script> diff --git a/packages/client/src/pages/settings/general.vue b/packages/client/src/pages/settings/general.vue index c8f6f58322..64b8cc3106 100644 --- a/packages/client/src/pages/settings/general.vue +++ b/packages/client/src/pages/settings/general.vue @@ -1,10 +1,10 @@ <template> <div class="_formRoot"> <FormSelect v-model="lang" class="_formBlock"> - <template #label>{{ $ts.uiLanguage }}</template> + <template #label>{{ i18n.ts.uiLanguage }}</template> <option v-for="x in langs" :key="x[0]" :value="x[0]">{{ x[1] }}</option> <template #caption> - <I18n :src="$ts.i18nInfo" tag="span"> + <I18n :src="i18n.ts.i18nInfo" tag="span"> <template #link> <MkLink url="https://crowdin.com/project/misskey">Crowdin</MkLink> </template> @@ -13,48 +13,48 @@ </FormSelect> <FormRadios v-model="overridedDeviceKind" class="_formBlock"> - <template #label>{{ $ts.overridedDeviceKind }}</template> - <option :value="null">{{ $ts.auto }}</option> - <option value="smartphone"><i class="fas fa-mobile-alt"/> {{ $ts.smartphone }}</option> - <option value="tablet"><i class="fas fa-tablet-alt"/> {{ $ts.tablet }}</option> - <option value="desktop"><i class="fas fa-desktop"/> {{ $ts.desktop }}</option> + <template #label>{{ i18n.ts.overridedDeviceKind }}</template> + <option :value="null">{{ i18n.ts.auto }}</option> + <option value="smartphone"><i class="fas fa-mobile-alt"/> {{ i18n.ts.smartphone }}</option> + <option value="tablet"><i class="fas fa-tablet-alt"/> {{ i18n.ts.tablet }}</option> + <option value="desktop"><i class="fas fa-desktop"/> {{ i18n.ts.desktop }}</option> </FormRadios> - <FormSwitch v-model="showFixedPostForm" class="_formBlock">{{ $ts.showFixedPostForm }}</FormSwitch> + <FormSwitch v-model="showFixedPostForm" class="_formBlock">{{ i18n.ts.showFixedPostForm }}</FormSwitch> <FormSection> - <template #label>{{ $ts.behavior }}</template> - <FormSwitch v-model="imageNewTab" class="_formBlock">{{ $ts.openImageInNewTab }}</FormSwitch> - <FormSwitch v-model="enableInfiniteScroll" class="_formBlock">{{ $ts.enableInfiniteScroll }}</FormSwitch> - <FormSwitch v-model="useReactionPickerForContextMenu" class="_formBlock">{{ $ts.useReactionPickerForContextMenu }}</FormSwitch> - <FormSwitch v-model="disablePagesScript" class="_formBlock">{{ $ts.disablePagesScript }}</FormSwitch> + <template #label>{{ i18n.ts.behavior }}</template> + <FormSwitch v-model="imageNewTab" class="_formBlock">{{ i18n.ts.openImageInNewTab }}</FormSwitch> + <FormSwitch v-model="enableInfiniteScroll" class="_formBlock">{{ i18n.ts.enableInfiniteScroll }}</FormSwitch> + <FormSwitch v-model="useReactionPickerForContextMenu" class="_formBlock">{{ i18n.ts.useReactionPickerForContextMenu }}</FormSwitch> + <FormSwitch v-model="disablePagesScript" class="_formBlock">{{ i18n.ts.disablePagesScript }}</FormSwitch> <FormSelect v-model="serverDisconnectedBehavior" class="_formBlock"> - <template #label>{{ $ts.whenServerDisconnected }}</template> - <option value="reload">{{ $ts._serverDisconnectedBehavior.reload }}</option> - <option value="dialog">{{ $ts._serverDisconnectedBehavior.dialog }}</option> - <option value="quiet">{{ $ts._serverDisconnectedBehavior.quiet }}</option> + <template #label>{{ i18n.ts.whenServerDisconnected }}</template> + <option value="reload">{{ i18n.ts._serverDisconnectedBehavior.reload }}</option> + <option value="dialog">{{ i18n.ts._serverDisconnectedBehavior.dialog }}</option> + <option value="quiet">{{ i18n.ts._serverDisconnectedBehavior.quiet }}</option> </FormSelect> </FormSection> <FormSection> - <template #label>{{ $ts.appearance }}</template> - <FormSwitch v-model="disableAnimatedMfm" class="_formBlock">{{ $ts.disableAnimatedMfm }}</FormSwitch> - <FormSwitch v-model="reduceAnimation" class="_formBlock">{{ $ts.reduceUiAnimation }}</FormSwitch> - <FormSwitch v-model="useBlurEffect" class="_formBlock">{{ $ts.useBlurEffect }}</FormSwitch> - <FormSwitch v-model="useBlurEffectForModal" class="_formBlock">{{ $ts.useBlurEffectForModal }}</FormSwitch> - <FormSwitch v-model="showGapBetweenNotesInTimeline" class="_formBlock">{{ $ts.showGapBetweenNotesInTimeline }}</FormSwitch> - <FormSwitch v-model="loadRawImages" class="_formBlock">{{ $ts.loadRawImages }}</FormSwitch> - <FormSwitch v-model="disableShowingAnimatedImages" class="_formBlock">{{ $ts.disableShowingAnimatedImages }}</FormSwitch> - <FormSwitch v-model="squareAvatars" class="_formBlock">{{ $ts.squareAvatars }}</FormSwitch> - <FormSwitch v-model="useSystemFont" class="_formBlock">{{ $ts.useSystemFont }}</FormSwitch> - <FormSwitch v-model="useOsNativeEmojis" class="_formBlock">{{ $ts.useOsNativeEmojis }} + <template #label>{{ i18n.ts.appearance }}</template> + <FormSwitch v-model="disableAnimatedMfm" class="_formBlock">{{ i18n.ts.disableAnimatedMfm }}</FormSwitch> + <FormSwitch v-model="reduceAnimation" class="_formBlock">{{ i18n.ts.reduceUiAnimation }}</FormSwitch> + <FormSwitch v-model="useBlurEffect" class="_formBlock">{{ i18n.ts.useBlurEffect }}</FormSwitch> + <FormSwitch v-model="useBlurEffectForModal" class="_formBlock">{{ i18n.ts.useBlurEffectForModal }}</FormSwitch> + <FormSwitch v-model="showGapBetweenNotesInTimeline" class="_formBlock">{{ i18n.ts.showGapBetweenNotesInTimeline }}</FormSwitch> + <FormSwitch v-model="loadRawImages" class="_formBlock">{{ i18n.ts.loadRawImages }}</FormSwitch> + <FormSwitch v-model="disableShowingAnimatedImages" class="_formBlock">{{ i18n.ts.disableShowingAnimatedImages }}</FormSwitch> + <FormSwitch v-model="squareAvatars" class="_formBlock">{{ i18n.ts.squareAvatars }}</FormSwitch> + <FormSwitch v-model="useSystemFont" class="_formBlock">{{ i18n.ts.useSystemFont }}</FormSwitch> + <FormSwitch v-model="useOsNativeEmojis" class="_formBlock">{{ i18n.ts.useOsNativeEmojis }} <div><Mfm :key="useOsNativeEmojis" text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></div> </FormSwitch> - <FormSwitch v-model="disableDrawer" class="_formBlock">{{ $ts.disableDrawer }}</FormSwitch> + <FormSwitch v-model="disableDrawer" class="_formBlock">{{ i18n.ts.disableDrawer }}</FormSwitch> <FormRadios v-model="fontSize" class="_formBlock"> - <template #label>{{ $ts.fontSize }}</template> + <template #label>{{ i18n.ts.fontSize }}</template> <option value="small"><span style="font-size: 14px;">Aa</span></option> <option :value="null"><span style="font-size: 16px;">Aa</span></option> <option value="large"><span style="font-size: 18px;">Aa</span></option> @@ -63,36 +63,36 @@ </FormSection> <FormSection> - <FormSwitch v-model="aiChanMode">{{ $ts.aiChanMode }}</FormSwitch> + <FormSwitch v-model="aiChanMode">{{ i18n.ts.aiChanMode }}</FormSwitch> </FormSection> <FormSelect v-model="instanceTicker" class="_formBlock"> - <template #label>{{ $ts.instanceTicker }}</template> - <option value="none">{{ $ts._instanceTicker.none }}</option> - <option value="remote">{{ $ts._instanceTicker.remote }}</option> - <option value="always">{{ $ts._instanceTicker.always }}</option> + <template #label>{{ i18n.ts.instanceTicker }}</template> + <option value="none">{{ i18n.ts._instanceTicker.none }}</option> + <option value="remote">{{ i18n.ts._instanceTicker.remote }}</option> + <option value="always">{{ i18n.ts._instanceTicker.always }}</option> </FormSelect> <FormSelect v-model="nsfw" class="_formBlock"> - <template #label>{{ $ts.nsfw }}</template> - <option value="respect">{{ $ts._nsfw.respect }}</option> - <option value="ignore">{{ $ts._nsfw.ignore }}</option> - <option value="force">{{ $ts._nsfw.force }}</option> + <template #label>{{ i18n.ts.nsfw }}</template> + <option value="respect">{{ i18n.ts._nsfw.respect }}</option> + <option value="ignore">{{ i18n.ts._nsfw.ignore }}</option> + <option value="force">{{ i18n.ts._nsfw.force }}</option> </FormSelect> <FormGroup> - <template #label>{{ $ts.defaultNavigationBehaviour }}</template> - <FormSwitch v-model="defaultSideView">{{ $ts.openInSideView }}</FormSwitch> + <template #label>{{ i18n.ts.defaultNavigationBehaviour }}</template> + <FormSwitch v-model="defaultSideView">{{ i18n.ts.openInSideView }}</FormSwitch> </FormGroup> - <FormLink to="/settings/deck" class="_formBlock">{{ $ts.deck }}</FormLink> + <FormLink to="/settings/deck" class="_formBlock">{{ i18n.ts.deck }}</FormLink> - <FormLink to="/settings/custom-css" class="_formBlock"><template #icon><i class="fas fa-code"></i></template>{{ $ts.customCss }}</FormLink> + <FormLink to="/settings/custom-css" class="_formBlock"><template #icon><i class="fas fa-code"></i></template>{{ i18n.ts.customCss }}</FormLink> </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { computed, defineExpose, ref, watch } from 'vue'; import FormSwitch from '@/components/form/switch.vue'; import FormSelect from '@/components/form/select.vue'; import FormRadios from '@/components/form/radios.vue'; @@ -102,122 +102,87 @@ import FormLink from '@/components/form/link.vue'; import MkLink from '@/components/link.vue'; import { langs } from '@/config'; import { defaultStore } from '@/store'; -import { ColdDeviceStorage } from '@/store'; import * as os from '@/os'; import { unisonReload } from '@/scripts/unison-reload'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkLink, - FormSwitch, - FormSelect, - FormRadios, - FormGroup, - FormLink, - FormSection, - }, +const lang = ref(localStorage.getItem('lang')); +const fontSize = ref(localStorage.getItem('fontSize')); +const useSystemFont = ref(localStorage.getItem('useSystemFont') != null); - emits: ['info'], +async function reloadAsk() { + const { canceled } = await os.confirm({ + type: 'info', + text: i18n.ts.reloadToApplySetting, + }); + if (canceled) return; - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.general, - icon: 'fas fa-cogs', - bg: 'var(--bg)' - }, - langs, - lang: localStorage.getItem('lang'), - fontSize: localStorage.getItem('fontSize'), - useSystemFont: localStorage.getItem('useSystemFont') != null, - } - }, + unisonReload(); +} - computed: { - overridedDeviceKind: defaultStore.makeGetterSetter('overridedDeviceKind'), - serverDisconnectedBehavior: defaultStore.makeGetterSetter('serverDisconnectedBehavior'), - reduceAnimation: defaultStore.makeGetterSetter('animation', v => !v, v => !v), - useBlurEffectForModal: defaultStore.makeGetterSetter('useBlurEffectForModal'), - useBlurEffect: defaultStore.makeGetterSetter('useBlurEffect'), - showGapBetweenNotesInTimeline: defaultStore.makeGetterSetter('showGapBetweenNotesInTimeline'), - disableAnimatedMfm: defaultStore.makeGetterSetter('animatedMfm', v => !v, v => !v), - useOsNativeEmojis: defaultStore.makeGetterSetter('useOsNativeEmojis'), - disableDrawer: defaultStore.makeGetterSetter('disableDrawer'), - disableShowingAnimatedImages: defaultStore.makeGetterSetter('disableShowingAnimatedImages'), - loadRawImages: defaultStore.makeGetterSetter('loadRawImages'), - imageNewTab: defaultStore.makeGetterSetter('imageNewTab'), - nsfw: defaultStore.makeGetterSetter('nsfw'), - disablePagesScript: defaultStore.makeGetterSetter('disablePagesScript'), - showFixedPostForm: defaultStore.makeGetterSetter('showFixedPostForm'), - defaultSideView: defaultStore.makeGetterSetter('defaultSideView'), - instanceTicker: defaultStore.makeGetterSetter('instanceTicker'), - enableInfiniteScroll: defaultStore.makeGetterSetter('enableInfiniteScroll'), - useReactionPickerForContextMenu: defaultStore.makeGetterSetter('useReactionPickerForContextMenu'), - squareAvatars: defaultStore.makeGetterSetter('squareAvatars'), - aiChanMode: defaultStore.makeGetterSetter('aiChanMode'), - }, +const overridedDeviceKind = computed(defaultStore.makeGetterSetter('overridedDeviceKind')); +const serverDisconnectedBehavior = computed(defaultStore.makeGetterSetter('serverDisconnectedBehavior')); +const reduceAnimation = computed(defaultStore.makeGetterSetter('animation', v => !v, v => !v)); +const useBlurEffectForModal = computed(defaultStore.makeGetterSetter('useBlurEffectForModal')); +const useBlurEffect = computed(defaultStore.makeGetterSetter('useBlurEffect')); +const showGapBetweenNotesInTimeline = computed(defaultStore.makeGetterSetter('showGapBetweenNotesInTimeline')); +const disableAnimatedMfm = computed(defaultStore.makeGetterSetter('animatedMfm', v => !v, v => !v)); +const useOsNativeEmojis = computed(defaultStore.makeGetterSetter('useOsNativeEmojis')); +const disableDrawer = computed(defaultStore.makeGetterSetter('disableDrawer')); +const disableShowingAnimatedImages = computed(defaultStore.makeGetterSetter('disableShowingAnimatedImages')); +const loadRawImages = computed(defaultStore.makeGetterSetter('loadRawImages')); +const imageNewTab = computed(defaultStore.makeGetterSetter('imageNewTab')); +const nsfw = computed(defaultStore.makeGetterSetter('nsfw')); +const disablePagesScript = computed(defaultStore.makeGetterSetter('disablePagesScript')); +const showFixedPostForm = computed(defaultStore.makeGetterSetter('showFixedPostForm')); +const defaultSideView = computed(defaultStore.makeGetterSetter('defaultSideView')); +const instanceTicker = computed(defaultStore.makeGetterSetter('instanceTicker')); +const enableInfiniteScroll = computed(defaultStore.makeGetterSetter('enableInfiniteScroll')); +const useReactionPickerForContextMenu = computed(defaultStore.makeGetterSetter('useReactionPickerForContextMenu')); +const squareAvatars = computed(defaultStore.makeGetterSetter('squareAvatars')); +const aiChanMode = computed(defaultStore.makeGetterSetter('aiChanMode')); - watch: { - lang() { - localStorage.setItem('lang', this.lang); - localStorage.removeItem('locale'); - this.reloadAsk(); - }, - - fontSize() { - if (this.fontSize == null) { - localStorage.removeItem('fontSize'); - } else { - localStorage.setItem('fontSize', this.fontSize); - } - this.reloadAsk(); - }, - - useSystemFont() { - if (this.useSystemFont) { - localStorage.setItem('useSystemFont', 't'); - } else { - localStorage.removeItem('useSystemFont'); - } - this.reloadAsk(); - }, - - enableInfiniteScroll() { - this.reloadAsk(); - }, - - squareAvatars() { - this.reloadAsk(); - }, - - aiChanMode() { - this.reloadAsk(); - }, - - showGapBetweenNotesInTimeline() { - this.reloadAsk(); - }, +watch(lang, () => { + localStorage.setItem('lang', lang.value as string); + localStorage.removeItem('locale'); +}); - instanceTicker() { - this.reloadAsk(); - }, +watch(fontSize, () => { + if (fontSize.value == null) { + localStorage.removeItem('fontSize'); + } else { + localStorage.setItem('fontSize', fontSize.value); + } +}); - overridedDeviceKind() { - this.reloadAsk(); - }, - }, +watch(useSystemFont, () => { + if (useSystemFont.value) { + localStorage.setItem('useSystemFont', 't'); + } else { + localStorage.removeItem('useSystemFont'); + } +}); - methods: { - async reloadAsk() { - const { canceled } = await os.confirm({ - type: 'info', - text: this.$ts.reloadToApplySetting, - }); - if (canceled) return; +watch([ + lang, + fontSize, + useSystemFont, + enableInfiniteScroll, + squareAvatars, + aiChanMode, + showGapBetweenNotesInTimeline, + instanceTicker, + overridedDeviceKind +], async () => { + await reloadAsk(); +}); - unisonReload(); - } +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.general, + icon: 'fas fa-cogs', + bg: 'var(--bg)' } }); </script> diff --git a/packages/client/src/pages/settings/import-export.vue b/packages/client/src/pages/settings/import-export.vue index c153b4d28c..127cbcd4c1 100644 --- a/packages/client/src/pages/settings/import-export.vue +++ b/packages/client/src/pages/settings/import-export.vue @@ -37,8 +37,8 @@ </div> </template> -<script lang="ts"> -import { defineComponent, onMounted, ref } from 'vue'; +<script lang="ts" setup> +import { defineExpose, ref } from 'vue'; import MkButton from '@/components/ui/button.vue'; import FormSection from '@/components/form/section.vue'; import FormGroup from '@/components/form/group.vue'; @@ -48,108 +48,80 @@ import { selectFile } from '@/scripts/select-file'; import * as symbols from '@/symbols'; import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - FormSection, - FormGroup, - FormSwitch, - MkButton, - }, +const excludeMutingUsers = ref(false); +const excludeInactiveUsers = ref(false); - emits: ['info'], +const onExportSuccess = () => { + os.alert({ + type: 'info', + text: i18n.ts.exportRequested, + }); +}; - setup(props, context) { - const INFO = { - title: i18n.ts.importAndExport, - icon: 'fas fa-boxes', - bg: 'var(--bg)', - }; +const onImportSuccess = () => { + os.alert({ + type: 'info', + text: i18n.ts.importRequested, + }); +}; - const excludeMutingUsers = ref(false); - const excludeInactiveUsers = ref(false); +const onError = (ev) => { + os.alert({ + type: 'error', + text: ev.message, + }); +}; - const onExportSuccess = () => { - os.alert({ - type: 'info', - text: i18n.ts.exportRequested, - }); - }; +const exportNotes = () => { + os.api('i/export-notes', {}).then(onExportSuccess).catch(onError); +}; - const onImportSuccess = () => { - os.alert({ - type: 'info', - text: i18n.ts.importRequested, - }); - }; +const exportFollowing = () => { + os.api('i/export-following', { + excludeMuting: excludeMutingUsers.value, + excludeInactive: excludeInactiveUsers.value, + }) + .then(onExportSuccess).catch(onError); +}; - const onError = (e) => { - os.alert({ - type: 'error', - text: e.message, - }); - }; +const exportBlocking = () => { + os.api('i/export-blocking', {}).then(onExportSuccess).catch(onError); +}; - const exportNotes = () => { - os.api('i/export-notes', {}).then(onExportSuccess).catch(onError); - }; +const exportUserLists = () => { + os.api('i/export-user-lists', {}).then(onExportSuccess).catch(onError); +}; - const exportFollowing = () => { - os.api('i/export-following', { - excludeMuting: excludeMutingUsers.value, - excludeInactive: excludeInactiveUsers.value, - }) - .then(onExportSuccess).catch(onError); - }; +const exportMuting = () => { + os.api('i/export-mute', {}).then(onExportSuccess).catch(onError); +}; - const exportBlocking = () => { - os.api('i/export-blocking', {}).then(onExportSuccess).catch(onError); - }; +const importFollowing = async (ev) => { + const file = await selectFile(ev.currentTarget ?? ev.target); + os.api('i/import-following', { fileId: file.id }).then(onImportSuccess).catch(onError); +}; - const exportUserLists = () => { - os.api('i/export-user-lists', {}).then(onExportSuccess).catch(onError); - }; +const importUserLists = async (ev) => { + const file = await selectFile(ev.currentTarget ?? ev.target); + os.api('i/import-user-lists', { fileId: file.id }).then(onImportSuccess).catch(onError); +}; - const exportMuting = () => { - os.api('i/export-mute', {}).then(onExportSuccess).catch(onError); - }; +const importMuting = async (ev) => { + const file = await selectFile(ev.currentTarget ?? ev.target); + os.api('i/import-muting', { fileId: file.id }).then(onImportSuccess).catch(onError); +}; - const importFollowing = async (ev) => { - const file = await selectFile(ev.currentTarget ?? ev.target); - os.api('i/import-following', { fileId: file.id }).then(onImportSuccess).catch(onError); - }; +const importBlocking = async (ev) => { + const file = await selectFile(ev.currentTarget ?? ev.target); + os.api('i/import-blocking', { fileId: file.id }).then(onImportSuccess).catch(onError); +}; - const importUserLists = async (ev) => { - const file = await selectFile(ev.currentTarget ?? ev.target); - os.api('i/import-user-lists', { fileId: file.id }).then(onImportSuccess).catch(onError); - }; - - const importMuting = async (ev) => { - const file = await selectFile(ev.currentTarget ?? ev.target); - os.api('i/import-muting', { fileId: file.id }).then(onImportSuccess).catch(onError); - }; - - const importBlocking = async (ev) => { - const file = await selectFile(ev.currentTarget ?? ev.target); - os.api('i/import-blocking', { fileId: file.id }).then(onImportSuccess).catch(onError); - }; - - return { - [symbols.PAGE_INFO]: INFO, - excludeMutingUsers, - excludeInactiveUsers, - - exportNotes, - exportFollowing, - exportBlocking, - exportUserLists, - exportMuting, - - importFollowing, - importUserLists, - importMuting, - importBlocking, - }; - }, +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.importAndExport, + icon: 'fas fa-boxes', + bg: 'var(--bg)', + } }); </script> diff --git a/packages/client/src/pages/settings/index.vue b/packages/client/src/pages/settings/index.vue index 44c3be62fe..e6670ea930 100644 --- a/packages/client/src/pages/settings/index.vue +++ b/packages/client/src/pages/settings/index.vue @@ -2,19 +2,22 @@ <MkSpacer :content-max="900" :margin-min="20" :margin-max="32"> <div ref="el" class="vvcocwet" :class="{ wide: !narrow }"> <div class="header"> - <div class="title">{{ $ts.settings }}</div> + <div class="title"> + <MkA v-if="narrow" to="/settings">{{ $ts.settings }}</MkA> + <template v-else>{{ $ts.settings }}</template> + </div> <div v-if="childInfo" class="subtitle">{{ childInfo.title }}</div> </div> <div class="body"> - <div v-if="!narrow || page == null" class="nav"> + <div v-if="!narrow || initialPage == null" class="nav"> <div class="baaadecd"> <MkInfo v-if="emailNotConfigured" warn class="info">{{ $ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ $ts.configure }}</MkA></MkInfo> - <MkSuperMenu :def="menuDef" :grid="page == null"></MkSuperMenu> + <MkSuperMenu :def="menuDef" :grid="initialPage == null"></MkSuperMenu> </div> </div> - <div class="main"> + <div v-if="!(narrow && initialPage == null)" class="main"> <div class="bkzroven"> - <component :is="component" :ref="el => pageChanged(el)" :key="page" v-bind="pageProps"/> + <component :is="component" :ref="el => pageChanged(el)" :key="initialPage" v-bind="pageProps"/> </div> </div> </div> @@ -23,7 +26,7 @@ </template> <script setup lang="ts"> -import { computed, defineAsyncComponent, nextTick, onMounted, ref, watch } from 'vue'; +import { computed, defineAsyncComponent, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'; import { i18n } from '@/i18n'; import MkInfo from '@/components/ui/info.vue'; import MkSuperMenu from '@/components/ui/super-menu.vue'; @@ -33,6 +36,7 @@ import { unisonReload } from '@/scripts/unison-reload'; import * as symbols from '@/symbols'; import { instance } from '@/instance'; import { $i } from '@/account'; +import { MisskeyNavigator } from '@/scripts/navigate'; const props = defineProps<{ initialPage?: string @@ -45,53 +49,61 @@ const indexInfo = { hideHeader: true, }; const INFO = ref(indexInfo); -const page = ref(props.initialPage); -const narrow = ref(false); -const view = ref(null); const el = ref<HTMLElement | null>(null); const childInfo = ref(null); + +const nav = new MisskeyNavigator(); + +const narrow = ref(false); +const NARROW_THRESHOLD = 600; + +const ro = new ResizeObserver((entries, observer) => { + if (entries.length === 0) return; + narrow.value = entries[0].borderBoxSize[0].inlineSize < NARROW_THRESHOLD; +}); + const menuDef = computed(() => [{ title: i18n.ts.basicSettings, items: [{ icon: 'fas fa-user', text: i18n.ts.profile, to: '/settings/profile', - active: page.value === 'profile', + active: props.initialPage === 'profile', }, { icon: 'fas fa-lock-open', text: i18n.ts.privacy, to: '/settings/privacy', - active: page.value === 'privacy', + active: props.initialPage === 'privacy', }, { icon: 'fas fa-laugh', text: i18n.ts.reaction, to: '/settings/reaction', - active: page.value === 'reaction', + active: props.initialPage === 'reaction', }, { icon: 'fas fa-cloud', text: i18n.ts.drive, to: '/settings/drive', - active: page.value === 'drive', + active: props.initialPage === 'drive', }, { icon: 'fas fa-bell', text: i18n.ts.notifications, to: '/settings/notifications', - active: page.value === 'notifications', + active: props.initialPage === 'notifications', }, { icon: 'fas fa-envelope', text: i18n.ts.email, to: '/settings/email', - active: page.value === 'email', + active: props.initialPage === 'email', }, { icon: 'fas fa-share-alt', text: i18n.ts.integration, to: '/settings/integration', - active: page.value === 'integration', + active: props.initialPage === 'integration', }, { icon: 'fas fa-lock', text: i18n.ts.security, to: '/settings/security', - active: page.value === 'security', + active: props.initialPage === 'security', }], }, { title: i18n.ts.clientSettings, @@ -99,27 +111,27 @@ const menuDef = computed(() => [{ icon: 'fas fa-cogs', text: i18n.ts.general, to: '/settings/general', - active: page.value === 'general', + active: props.initialPage === 'general', }, { icon: 'fas fa-palette', text: i18n.ts.theme, to: '/settings/theme', - active: page.value === 'theme', + active: props.initialPage === 'theme', }, { icon: 'fas fa-list-ul', text: i18n.ts.menu, to: '/settings/menu', - active: page.value === 'menu', + active: props.initialPage === 'menu', }, { icon: 'fas fa-music', text: i18n.ts.sounds, to: '/settings/sounds', - active: page.value === 'sounds', + active: props.initialPage === 'sounds', }, { icon: 'fas fa-plug', text: i18n.ts.plugins, to: '/settings/plugin', - active: page.value === 'plugin', + active: props.initialPage === 'plugin', }], }, { title: i18n.ts.otherSettings, @@ -127,37 +139,37 @@ const menuDef = computed(() => [{ icon: 'fas fa-boxes', text: i18n.ts.importAndExport, to: '/settings/import-export', - active: page.value === 'import-export', + active: props.initialPage === 'import-export', }, { icon: 'fas fa-volume-mute', text: i18n.ts.instanceMute, to: '/settings/instance-mute', - active: page.value === 'instance-mute', + active: props.initialPage === 'instance-mute', }, { icon: 'fas fa-ban', text: i18n.ts.muteAndBlock, to: '/settings/mute-block', - active: page.value === 'mute-block', + active: props.initialPage === 'mute-block', }, { icon: 'fas fa-comment-slash', text: i18n.ts.wordMute, to: '/settings/word-mute', - active: page.value === 'word-mute', + active: props.initialPage === 'word-mute', }, { icon: 'fas fa-key', text: 'API', to: '/settings/api', - active: page.value === 'api', + active: props.initialPage === 'api', }, { icon: 'fas fa-bolt', text: 'Webhook', to: '/settings/webhook', - active: page.value === 'webhook', + active: props.initialPage === 'webhook', }, { icon: 'fas fa-ellipsis-h', text: i18n.ts.other, to: '/settings/other', - active: page.value === 'other', + active: props.initialPage === 'other', }], }, { items: [{ @@ -182,8 +194,8 @@ const menuDef = computed(() => [{ const pageProps = ref({}); const component = computed(() => { - if (page.value == null) return null; - switch (page.value) { + if (props.initialPage == null) return null; + switch (props.initialPage) { case 'accounts': return defineAsyncComponent(() => import('./accounts.vue')); case 'profile': return defineAsyncComponent(() => import('./profile.vue')); case 'privacy': return defineAsyncComponent(() => import('./privacy.vue')); @@ -230,27 +242,41 @@ watch(component, () => { watch(() => props.initialPage, () => { if (props.initialPage == null && !narrow.value) { - page.value = 'profile'; + nav.push('/settings/profile'); } else { - page.value = props.initialPage; if (props.initialPage == null) { INFO.value = indexInfo; } } }); +watch(narrow, () => { + if (props.initialPage == null && !narrow.value) { + nav.push('/settings/profile'); + } +}); + onMounted(() => { - narrow.value = el.value.offsetWidth < 800; - if (!narrow.value) { - page.value = 'profile'; + ro.observe(el.value); + + narrow.value = el.value.offsetWidth < NARROW_THRESHOLD; + if (props.initialPage == null && !narrow.value) { + nav.push('/settings/profile'); } }); +onUnmounted(() => { + ro.disconnect(); +}); + const emailNotConfigured = computed(() => instance.enableEmail && ($i.email == null || !$i.emailVerified)); const pageChanged = (page) => { - if (page == null) return; - childInfo.value = page[symbols.PAGE_INFO]; + if (page == null) { + childInfo.value = null; + } else { + childInfo.value = page[symbols.PAGE_INFO]; + } }; defineExpose({ @@ -267,6 +293,7 @@ defineExpose({ font-weight: bold; > .title { + display: block; width: 34%; } diff --git a/packages/client/src/pages/settings/instance-mute.vue b/packages/client/src/pages/settings/instance-mute.vue index f84a209b60..bcc2ee85ad 100644 --- a/packages/client/src/pages/settings/instance-mute.vue +++ b/packages/client/src/pages/settings/instance-mute.vue @@ -1,67 +1,51 @@ <template> <div class="_formRoot"> - <MkInfo>{{ $ts._instanceMute.title }}</MkInfo> + <MkInfo>{{ i18n.ts._instanceMute.title }}</MkInfo> <FormTextarea v-model="instanceMutes" class="_formBlock"> - <template #label>{{ $ts._instanceMute.heading }}</template> - <template #caption>{{ $ts._instanceMute.instanceMuteDescription }}<br>{{ $ts._instanceMute.instanceMuteDescription2 }}</template> + <template #label>{{ i18n.ts._instanceMute.heading }}</template> + <template #caption>{{ i18n.ts._instanceMute.instanceMuteDescription }}<br>{{ i18n.ts._instanceMute.instanceMuteDescription2 }}</template> </FormTextarea> - <MkButton primary :disabled="!changed" class="_formBlock" @click="save()"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton> + <MkButton primary :disabled="!changed" class="_formBlock" @click="save()"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton> </div> </template> -<script> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { defineExpose, ref, watch } from 'vue'; import FormTextarea from '@/components/form/textarea.vue'; import MkInfo from '@/components/ui/info.vue'; import MkButton from '@/components/ui/button.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; +import { $i } from '@/account'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkButton, - FormTextarea, - MkInfo, - }, +const instanceMutes = ref($i!.mutedInstances.join('\n')); +const changed = ref(false); - emits: ['info'], +async function save() { + let mutes = instanceMutes.value + .trim().split('\n') + .map(el => el.trim()) + .filter(el => el); - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.instanceMute, - icon: 'fas fa-volume-mute' - }, - tab: 'soft', - instanceMutes: '', - changed: false, - } - }, + await os.api('i/update', { + mutedInstances: mutes, + }); - watch: { - instanceMutes: { - handler() { - this.changed = true; - }, - deep: true - }, - }, + changed.value = false; - async created() { - this.instanceMutes = this.$i.mutedInstances.join('\n'); - }, + // Refresh filtered list to signal to the user how they've been saved + instanceMutes.value = mutes.join('\n'); +} - methods: { - async save() { - let mutes = this.instanceMutes.trim().split('\n').map(el => el.trim()).filter(el => el); - await os.api('i/update', { - mutedInstances: mutes, - }); - this.changed = false; +watch(instanceMutes, () => { + changed.value = true; +}); - // Refresh filtered list to signal to the user how they've been saved - this.instanceMutes = mutes.join('\n'); - }, +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.instanceMute, + icon: 'fas fa-volume-mute' } -}) +}); </script> diff --git a/packages/client/src/pages/settings/integration.vue b/packages/client/src/pages/settings/integration.vue index ca36c91665..75c6200944 100644 --- a/packages/client/src/pages/settings/integration.vue +++ b/packages/client/src/pages/settings/integration.vue @@ -1,133 +1,98 @@ <template> <div class="_formRoot"> - <FormSection v-if="enableTwitterIntegration"> + <FormSection v-if="instance.enableTwitterIntegration"> <template #label><i class="fab fa-twitter"></i> Twitter</template> - <p v-if="integrations.twitter">{{ $ts.connectedTo }}: <a :href="`https://twitter.com/${integrations.twitter.screenName}`" rel="nofollow noopener" target="_blank">@{{ integrations.twitter.screenName }}</a></p> - <MkButton v-if="integrations.twitter" danger @click="disconnectTwitter">{{ $ts.disconnectService }}</MkButton> - <MkButton v-else primary @click="connectTwitter">{{ $ts.connectService }}</MkButton> + <p v-if="integrations.twitter">{{ i18n.ts.connectedTo }}: <a :href="`https://twitter.com/${integrations.twitter.screenName}`" rel="nofollow noopener" target="_blank">@{{ integrations.twitter.screenName }}</a></p> + <MkButton v-if="integrations.twitter" danger @click="disconnectTwitter">{{ i18n.ts.disconnectService }}</MkButton> + <MkButton v-else primary @click="connectTwitter">{{ i18n.ts.connectService }}</MkButton> </FormSection> - <FormSection v-if="enableDiscordIntegration"> + <FormSection v-if="instance.enableDiscordIntegration"> <template #label><i class="fab fa-discord"></i> Discord</template> - <p v-if="integrations.discord">{{ $ts.connectedTo }}: <a :href="`https://discord.com/users/${integrations.discord.id}`" rel="nofollow noopener" target="_blank">@{{ integrations.discord.username }}#{{ integrations.discord.discriminator }}</a></p> - <MkButton v-if="integrations.discord" danger @click="disconnectDiscord">{{ $ts.disconnectService }}</MkButton> - <MkButton v-else primary @click="connectDiscord">{{ $ts.connectService }}</MkButton> + <p v-if="integrations.discord">{{ i18n.ts.connectedTo }}: <a :href="`https://discord.com/users/${integrations.discord.id}`" rel="nofollow noopener" target="_blank">@{{ integrations.discord.username }}#{{ integrations.discord.discriminator }}</a></p> + <MkButton v-if="integrations.discord" danger @click="disconnectDiscord">{{ i18n.ts.disconnectService }}</MkButton> + <MkButton v-else primary @click="connectDiscord">{{ i18n.ts.connectService }}</MkButton> </FormSection> - <FormSection v-if="enableGithubIntegration"> + <FormSection v-if="instance.enableGithubIntegration"> <template #label><i class="fab fa-github"></i> GitHub</template> - <p v-if="integrations.github">{{ $ts.connectedTo }}: <a :href="`https://github.com/${integrations.github.login}`" rel="nofollow noopener" target="_blank">@{{ integrations.github.login }}</a></p> - <MkButton v-if="integrations.github" danger @click="disconnectGithub">{{ $ts.disconnectService }}</MkButton> - <MkButton v-else primary @click="connectGithub">{{ $ts.connectService }}</MkButton> + <p v-if="integrations.github">{{ i18n.ts.connectedTo }}: <a :href="`https://github.com/${integrations.github.login}`" rel="nofollow noopener" target="_blank">@{{ integrations.github.login }}</a></p> + <MkButton v-if="integrations.github" danger @click="disconnectGithub">{{ i18n.ts.disconnectService }}</MkButton> + <MkButton v-else primary @click="connectGithub">{{ i18n.ts.connectService }}</MkButton> </FormSection> </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { computed, defineExpose, onMounted, ref, watch } from 'vue'; import { apiUrl } from '@/config'; import FormSection from '@/components/form/section.vue'; import MkButton from '@/components/ui/button.vue'; -import * as os from '@/os'; import * as symbols from '@/symbols'; +import { $i } from '@/account'; +import { instance } from '@/instance'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - FormSection, - MkButton - }, +const twitterForm = ref<Window | null>(null); +const discordForm = ref<Window | null>(null); +const githubForm = ref<Window | null>(null); - emits: ['info'], +const integrations = computed(() => $i!.integrations); - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.integration, - icon: 'fas fa-share-alt', - bg: 'var(--bg)', - }, - apiUrl, - twitterForm: null, - discordForm: null, - githubForm: null, - enableTwitterIntegration: false, - enableDiscordIntegration: false, - enableGithubIntegration: false, - }; - }, +function openWindow(service: string, type: string) { + return window.open(`${apiUrl}/${type}/${service}`, + `${service}_${type}_window`, + 'height=570, width=520' + ); +} - computed: { - integrations() { - return this.$i.integrations; - }, - - meta() { - return this.$instance; - }, - }, +function connectTwitter() { + twitterForm.value = openWindow('twitter', 'connect'); +} - created() { - this.enableTwitterIntegration = this.meta.enableTwitterIntegration; - this.enableDiscordIntegration = this.meta.enableDiscordIntegration; - this.enableGithubIntegration = this.meta.enableGithubIntegration; - }, +function disconnectTwitter() { + openWindow('twitter', 'disconnect'); +} - mounted() { - document.cookie = `igi=${this.$i.token}; path=/;` + - ` max-age=31536000;` + - (document.location.protocol.startsWith('https') ? ' secure' : ''); +function connectDiscord() { + discordForm.value = openWindow('discord', 'connect'); +} - this.$watch('integrations', () => { - if (this.integrations.twitter) { - if (this.twitterForm) this.twitterForm.close(); - } - if (this.integrations.discord) { - if (this.discordForm) this.discordForm.close(); - } - if (this.integrations.github) { - if (this.githubForm) this.githubForm.close(); - } - }, { - deep: true - }); - }, +function disconnectDiscord() { + openWindow('discord', 'disconnect'); +} - methods: { - connectTwitter() { - this.twitterForm = window.open(apiUrl + '/connect/twitter', - 'twitter_connect_window', - 'height=570, width=520'); - }, +function connectGithub() { + githubForm.value = openWindow('github', 'connect'); +} - disconnectTwitter() { - window.open(apiUrl + '/disconnect/twitter', - 'twitter_disconnect_window', - 'height=570, width=520'); - }, +function disconnectGithub() { + openWindow('github', 'disconnect'); +} - connectDiscord() { - this.discordForm = window.open(apiUrl + '/connect/discord', - 'discord_connect_window', - 'height=570, width=520'); - }, +onMounted(() => { + document.cookie = `igi=${$i!.token}; path=/;` + + ` max-age=31536000;` + + (document.location.protocol.startsWith('https') ? ' secure' : ''); - disconnectDiscord() { - window.open(apiUrl + '/disconnect/discord', - 'discord_disconnect_window', - 'height=570, width=520'); - }, - - connectGithub() { - this.githubForm = window.open(apiUrl + '/connect/github', - 'github_connect_window', - 'height=570, width=520'); - }, + watch(integrations, () => { + if (integrations.value.twitter) { + if (twitterForm.value) twitterForm.value.close(); + } + if (integrations.value.discord) { + if (discordForm.value) discordForm.value.close(); + } + if (integrations.value.github) { + if (githubForm.value) githubForm.value.close(); + } + }); +}); - disconnectGithub() { - window.open(apiUrl + '/disconnect/github', - 'github_disconnect_window', - 'height=570, width=520'); - }, +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.integration, + icon: 'fas fa-share-alt', + bg: 'var(--bg)', } }); </script> diff --git a/packages/client/src/pages/settings/menu.vue b/packages/client/src/pages/settings/menu.vue index 6e38cd5dfe..2288c3f718 100644 --- a/packages/client/src/pages/settings/menu.vue +++ b/packages/client/src/pages/settings/menu.vue @@ -1,24 +1,24 @@ <template> <div class="_formRoot"> <FormTextarea v-model="items" tall manual-save class="_formBlock"> - <template #label>{{ $ts.menu }}</template> - <template #caption><button class="_textButton" @click="addItem">{{ $ts.addItem }}</button></template> + <template #label>{{ i18n.ts.menu }}</template> + <template #caption><button class="_textButton" @click="addItem">{{ i18n.ts.addItem }}</button></template> </FormTextarea> <FormRadios v-model="menuDisplay" class="_formBlock"> - <template #label>{{ $ts.display }}</template> - <option value="sideFull">{{ $ts._menuDisplay.sideFull }}</option> - <option value="sideIcon">{{ $ts._menuDisplay.sideIcon }}</option> - <option value="top">{{ $ts._menuDisplay.top }}</option> - <!-- <MkRadio v-model="menuDisplay" value="hide" disabled>{{ $ts._menuDisplay.hide }}</MkRadio>--> <!-- TODO: サイドバーを完全に隠せるようにすると、別途ハンバーガーボタンのようなものをUIに表示する必要があり面倒 --> + <template #label>{{ i18n.ts.display }}</template> + <option value="sideFull">{{ i18n.ts._menuDisplay.sideFull }}</option> + <option value="sideIcon">{{ i18n.ts._menuDisplay.sideIcon }}</option> + <option value="top">{{ i18n.ts._menuDisplay.top }}</option> + <!-- <MkRadio v-model="menuDisplay" value="hide" disabled>{{ i18n.ts._menuDisplay.hide }}</MkRadio>--> <!-- TODO: サイドバーを完全に隠せるようにすると、別途ハンバーガーボタンのようなものをUIに表示する必要があり面倒 --> </FormRadios> - <FormButton danger class="_formBlock" @click="reset()"><i class="fas fa-redo"></i> {{ $ts.default }}</FormButton> + <FormButton danger class="_formBlock" @click="reset()"><i class="fas fa-redo"></i> {{ i18n.ts.default }}</FormButton> </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { computed, defineExpose, ref, watch } from 'vue'; import FormTextarea from '@/components/form/textarea.vue'; import FormRadios from '@/components/form/radios.vue'; import FormButton from '@/components/ui/button.vue'; @@ -27,81 +27,60 @@ import { menuDef } from '@/menu'; import { defaultStore } from '@/store'; import * as symbols from '@/symbols'; import { unisonReload } from '@/scripts/unison-reload'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - FormButton, - FormTextarea, - FormRadios, - }, +const items = ref(defaultStore.state.menu.join('\n')); - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.menu, - icon: 'fas fa-list-ul', - bg: 'var(--bg)', - }, - menuDef: menuDef, - items: defaultStore.state.menu.join('\n'), - } - }, +const split = computed(() => items.value.trim().split('\n').filter(x => x.trim() !== '')); +const menuDisplay = computed(defaultStore.makeGetterSetter('menuDisplay')); - computed: { - splited(): string[] { - return this.items.trim().split('\n').filter(x => x.trim() !== ''); - }, +async function reloadAsk() { + const { canceled } = await os.confirm({ + type: 'info', + text: i18n.ts.reloadToApplySetting + }); + if (canceled) return; - menuDisplay: defaultStore.makeGetterSetter('menuDisplay') - }, + unisonReload(); +} - watch: { - menuDisplay() { - this.reloadAsk(); - }, +async function addItem() { + const menu = Object.keys(menuDef).filter(k => !defaultStore.state.menu.includes(k)); + const { canceled, result: item } = await os.select({ + title: i18n.ts.addItem, + items: [...menu.map(k => ({ + value: k, text: i18n.ts[menuDef[k].title] + })), { + value: '-', text: i18n.ts.divider + }] + }); + if (canceled) return; + items.value = [...split.value, item].join('\n'); +} - items() { - this.save(); - }, - }, +async function save() { + defaultStore.set('menu', split.value); + await reloadAsk(); +} - methods: { - async addItem() { - const menu = Object.keys(this.menuDef).filter(k => !this.$store.state.menu.includes(k)); - const { canceled, result: item } = await os.select({ - title: this.$ts.addItem, - items: [...menu.map(k => ({ - value: k, text: this.$ts[this.menuDef[k].title] - })), ...[{ - value: '-', text: this.$ts.divider - }]] - }); - if (canceled) return; - this.items = [...this.splited, item].join('\n'); - }, +function reset() { + defaultStore.reset('menu'); + items.value = defaultStore.state.menu.join('\n'); +} - save() { - this.$store.set('menu', this.splited); - this.reloadAsk(); - }, - - reset() { - this.$store.reset('menu'); - this.items = this.$store.state.menu.join('\n'); - }, +watch(items, async () => { + await save(); +}); - async reloadAsk() { - const { canceled } = await os.confirm({ - type: 'info', - text: this.$ts.reloadToApplySetting, - showCancelButton: true - }); - if (canceled) return; +watch(menuDisplay, async () => { + await reloadAsk(); +}); - unisonReload(); - } - }, +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.menu, + icon: 'fas fa-list-ul', + bg: 'var(--bg)', + } }); </script> diff --git a/packages/client/src/pages/settings/notifications.vue b/packages/client/src/pages/settings/notifications.vue index 12171530bb..b8fff95a8d 100644 --- a/packages/client/src/pages/settings/notifications.vue +++ b/packages/client/src/pages/settings/notifications.vue @@ -1,71 +1,59 @@ <template> <div class="_formRoot"> - <FormLink class="_formBlock" @click="configure"><template #icon><i class="fas fa-cog"></i></template>{{ $ts.notificationSetting }}</FormLink> + <FormLink class="_formBlock" @click="configure"><template #icon><i class="fas fa-cog"></i></template>{{ i18n.ts.notificationSetting }}</FormLink> <FormSection> - <FormLink class="_formBlock" @click="readAllNotifications">{{ $ts.markAsReadAllNotifications }}</FormLink> - <FormLink class="_formBlock" @click="readAllUnreadNotes">{{ $ts.markAsReadAllUnreadNotes }}</FormLink> - <FormLink class="_formBlock" @click="readAllMessagingMessages">{{ $ts.markAsReadAllTalkMessages }}</FormLink> + <FormLink class="_formBlock" @click="readAllNotifications">{{ i18n.ts.markAsReadAllNotifications }}</FormLink> + <FormLink class="_formBlock" @click="readAllUnreadNotes">{{ i18n.ts.markAsReadAllUnreadNotes }}</FormLink> + <FormLink class="_formBlock" @click="readAllMessagingMessages">{{ i18n.ts.markAsReadAllTalkMessages }}</FormLink> </FormSection> </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { defineAsyncComponent, defineExpose } from 'vue'; import FormButton from '@/components/ui/button.vue'; import FormLink from '@/components/form/link.vue'; import FormSection from '@/components/form/section.vue'; import { notificationTypes } from 'misskey-js'; import * as os from '@/os'; import * as symbols from '@/symbols'; +import { $i } from '@/account'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - FormLink, - FormButton, - FormSection, - }, +async function readAllUnreadNotes() { + await os.api('i/read-all-unread-notes'); +} - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.notifications, - icon: 'fas fa-bell', - bg: 'var(--bg)', - }, - } - }, - - methods: { - readAllUnreadNotes() { - os.api('i/read-all-unread-notes'); - }, +async function readAllMessagingMessages() { + await os.api('i/read-all-messaging-messages'); +} - readAllMessagingMessages() { - os.api('i/read-all-messaging-messages'); - }, +async function readAllNotifications() { + await os.api('notifications/mark-all-as-read'); +} - readAllNotifications() { - os.api('notifications/mark-all-as-read'); - }, +function configure() { + const includingTypes = notificationTypes.filter(x => !$i!.mutingNotificationTypes.includes(x)); + os.popup(defineAsyncComponent(() => import('@/components/notification-setting-window.vue')), { + includingTypes, + showGlobalToggle: false, + }, { + done: async (res) => { + const { includingTypes: value } = res; + await os.apiWithDialog('i/update', { + mutingNotificationTypes: notificationTypes.filter(x => !value.includes(x)), + }).then(i => { + $i!.mutingNotificationTypes = i.mutingNotificationTypes; + }); + } + }, 'closed'); +} - configure() { - const includingTypes = notificationTypes.filter(x => !this.$i.mutingNotificationTypes.includes(x)); - os.popup(import('@/components/notification-setting-window.vue'), { - includingTypes, - showGlobalToggle: false, - }, { - done: async (res) => { - const { includingTypes: value } = res; - await os.apiWithDialog('i/update', { - mutingNotificationTypes: notificationTypes.filter(x => !value.includes(x)), - }).then(i => { - this.$i.mutingNotificationTypes = i.mutingNotificationTypes; - }); - } - }, 'closed'); - }, +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.notifications, + icon: 'fas fa-bell', + bg: 'var(--bg)', } }); </script> diff --git a/packages/client/src/pages/settings/other.vue b/packages/client/src/pages/settings/other.vue index a9903acc7e..82e174a5b4 100644 --- a/packages/client/src/pages/settings/other.vue +++ b/packages/client/src/pages/settings/other.vue @@ -1,66 +1,44 @@ <template> <div class="_formRoot"> - <FormSwitch :value="$i.injectFeaturedNote" class="_formBlock" @update:modelValue="onChangeInjectFeaturedNote"> - {{ $ts.showFeaturedNotesInTimeline }} + <FormSwitch v-model="$i.injectFeaturedNote" class="_formBlock" @update:modelValue="onChangeInjectFeaturedNote"> + {{ i18n.ts.showFeaturedNotesInTimeline }} </FormSwitch> <!-- - <FormSwitch v-model="reportError" class="_formBlock">{{ $ts.sendErrorReports }}<template #caption>{{ $ts.sendErrorReportsDescription }}</template></FormSwitch> + <FormSwitch v-model="reportError" class="_formBlock">{{ i18n.ts.sendErrorReports }}<template #caption>{{ i18n.ts.sendErrorReportsDescription }}</template></FormSwitch> --> - <FormLink to="/settings/account-info" class="_formBlock">{{ $ts.accountInfo }}</FormLink> + <FormLink to="/settings/account-info" class="_formBlock">{{ i18n.ts.accountInfo }}</FormLink> - <FormLink to="/settings/delete-account" class="_formBlock"><template #icon><i class="fas fa-exclamation-triangle"></i></template>{{ $ts.closeAccount }}</FormLink> + <FormLink to="/settings/delete-account" class="_formBlock"><template #icon><i class="fas fa-exclamation-triangle"></i></template>{{ i18n.ts.closeAccount }}</FormLink> </div> </template> -<script lang="ts"> -import { defineAsyncComponent, defineComponent } from 'vue'; +<script lang="ts" setup> +import { computed, defineExpose } from 'vue'; import FormSwitch from '@/components/form/switch.vue'; -import FormSection from '@/components/form/section.vue'; import FormLink from '@/components/form/link.vue'; import * as os from '@/os'; -import { debug } from '@/config'; import { defaultStore } from '@/store'; -import { unisonReload } from '@/scripts/unison-reload'; import * as symbols from '@/symbols'; +import { $i } from '@/account'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - FormSection, - FormSwitch, - FormLink, - }, +const reportError = computed(defaultStore.makeGetterSetter('reportError')); - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.other, - icon: 'fas fa-ellipsis-h', - bg: 'var(--bg)', - }, - debug, - } - }, +function onChangeInjectFeaturedNote(v) { + os.api('i/update', { + injectFeaturedNote: v + }).then((i) => { + $i!.injectFeaturedNote = i.injectFeaturedNote; + }); +} - computed: { - reportError: defaultStore.makeGetterSetter('reportError'), - }, - - methods: { - changeDebug(v) { - console.log(v); - localStorage.setItem('debug', v.toString()); - unisonReload(); - }, - - onChangeInjectFeaturedNote(v) { - os.api('i/update', { - injectFeaturedNote: v - }); - }, +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.other, + icon: 'fas fa-ellipsis-h', + bg: 'var(--bg)', } }); </script> diff --git a/packages/client/src/pages/settings/plugin.install.vue b/packages/client/src/pages/settings/plugin.install.vue index d35d20d17a..96c0abfd99 100644 --- a/packages/client/src/pages/settings/plugin.install.vue +++ b/packages/client/src/pages/settings/plugin.install.vue @@ -1,19 +1,19 @@ <template> <div class="_formRoot"> - <FormInfo warn class="_formBlock">{{ $ts._plugin.installWarn }}</FormInfo> + <FormInfo warn class="_formBlock">{{ i18n.ts._plugin.installWarn }}</FormInfo> <FormTextarea v-model="code" tall class="_formBlock"> - <template #label>{{ $ts.code }}</template> + <template #label>{{ i18n.ts.code }}</template> </FormTextarea> <div class="_formBlock"> - <FormButton :disabled="code == null" primary inline @click="install"><i class="fas fa-check"></i> {{ $ts.install }}</FormButton> + <FormButton :disabled="code == null" primary inline @click="install"><i class="fas fa-check"></i> {{ i18n.ts.install }}</FormButton> </div> </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { defineExpose, defineAsyncComponent, nextTick, ref } from 'vue'; import { AiScript, parse } from '@syuilo/aiscript'; import { serialize } from '@syuilo/aiscript/built/serializer'; import { v4 as uuid } from 'uuid'; @@ -23,111 +23,101 @@ import FormInfo from '@/components/ui/info.vue'; import * as os from '@/os'; import { ColdDeviceStorage } from '@/store'; import { unisonReload } from '@/scripts/unison-reload'; +import { i18n } from '@/i18n'; import * as symbols from '@/symbols'; -export default defineComponent({ - components: { - FormTextarea, - FormButton, - FormInfo, - }, +const code = ref(null); - emits: ['info'], +function installPlugin({ id, meta, ast, token }) { + ColdDeviceStorage.set('plugins', ColdDeviceStorage.get('plugins').concat({ + ...meta, + id, + active: true, + configData: {}, + token: token, + ast: ast + })); +} - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts._plugin.install, - icon: 'fas fa-download', - bg: 'var(--bg)', - }, - code: null, - } - }, +async function install() { + let ast; + try { + ast = parse(code.value); + } catch (err) { + os.alert({ + type: 'error', + text: 'Syntax error :(' + }); + return; + } - methods: { - installPlugin({ id, meta, ast, token }) { - ColdDeviceStorage.set('plugins', ColdDeviceStorage.get('plugins').concat({ - ...meta, - id, - active: true, - configData: {}, - token: token, - ast: ast - })); - }, + const meta = AiScript.collectMetadata(ast); + if (meta == null) { + os.alert({ + type: 'error', + text: 'No metadata found :(' + }); + return; + } - async install() { - let ast; - try { - ast = parse(this.code); - } catch (e) { - os.alert({ - type: 'error', - text: 'Syntax error :(' - }); - return; - } - const meta = AiScript.collectMetadata(ast); - if (meta == null) { - os.alert({ - type: 'error', - text: 'No metadata found :(' - }); - return; - } - const data = meta.get(null); - if (data == null) { - os.alert({ - type: 'error', - text: 'No metadata found :(' - }); - return; - } - const { name, version, author, description, permissions, config } = data; - if (name == null || version == null || author == null) { - os.alert({ - type: 'error', - text: 'Required property not found :(' + const metadata = meta.get(null); + if (metadata == null) { + os.alert({ + type: 'error', + text: 'No metadata found :(' + }); + return; + } + + const { name, version, author, description, permissions, config } = metadata; + if (name == null || version == null || author == null) { + os.alert({ + type: 'error', + text: 'Required property not found :(' + }); + return; + } + + const token = permissions == null || permissions.length === 0 ? null : await new Promise((res, rej) => { + os.popup(defineAsyncComponent(() => import('@/components/token-generate-window.vue')), { + title: i18n.ts.tokenRequested, + information: i18n.ts.pluginTokenRequestedDescription, + initialName: name, + initialPermissions: permissions + }, { + done: async result => { + const { name, permissions } = result; + const { token } = await os.api('miauth/gen-token', { + session: null, + name: name, + permission: permissions, }); - return; + res(token); } + }, 'closed'); + }); - const token = permissions == null || permissions.length === 0 ? null : await new Promise((res, rej) => { - os.popup(import('@/components/token-generate-window.vue'), { - title: this.$ts.tokenRequested, - information: this.$ts.pluginTokenRequestedDescription, - initialName: name, - initialPermissions: permissions - }, { - done: async result => { - const { name, permissions } = result; - const { token } = await os.api('miauth/gen-token', { - session: null, - name: name, - permission: permissions, - }); - - res(token); - } - }, 'closed'); - }); + installPlugin({ + id: uuid(), + meta: { + name, version, author, description, permissions, config + }, + token, + ast: serialize(ast) + }); - this.installPlugin({ - id: uuid(), - meta: { - name, version, author, description, permissions, config - }, - token, - ast: serialize(ast) - }); + os.success(); - os.success(); + nextTick(() => { + unisonReload(); + }); +} - this.$nextTick(() => { - unisonReload(); - }); - }, +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts._plugin.install, + icon: 'fas fa-download', + bg: 'var(--bg)', } }); </script> diff --git a/packages/client/src/pages/settings/plugin.vue b/packages/client/src/pages/settings/plugin.vue index 7a3ab9d152..873a022cbc 100644 --- a/packages/client/src/pages/settings/plugin.vue +++ b/packages/client/src/pages/settings/plugin.vue @@ -1,38 +1,38 @@ <template> <div class="_formRoot"> - <FormLink to="/settings/plugin/install"><template #icon><i class="fas fa-download"></i></template>{{ $ts._plugin.install }}</FormLink> + <FormLink to="/settings/plugin/install"><template #icon><i class="fas fa-download"></i></template>{{ i18n.ts._plugin.install }}</FormLink> <FormSection> - <template #label>{{ $ts.manage }}</template> + <template #label>{{ i18n.ts.manage }}</template> <div v-for="plugin in plugins" :key="plugin.id" class="_formBlock _panel" style="padding: 20px;"> <span style="display: flex;"><b>{{ plugin.name }}</b><span style="margin-left: auto;">v{{ plugin.version }}</span></span> - <FormSwitch class="_formBlock" :modelValue="plugin.active" @update:modelValue="changeActive(plugin, $event)">{{ $ts.makeActive }}</FormSwitch> + <FormSwitch class="_formBlock" :modelValue="plugin.active" @update:modelValue="changeActive(plugin, $event)">{{ i18n.ts.makeActive }}</FormSwitch> <MkKeyValue class="_formBlock"> - <template #key>{{ $ts.author }}</template> + <template #key>{{ i18n.ts.author }}</template> <template #value>{{ plugin.author }}</template> </MkKeyValue> <MkKeyValue class="_formBlock"> - <template #key>{{ $ts.description }}</template> + <template #key>{{ i18n.ts.description }}</template> <template #value>{{ plugin.description }}</template> </MkKeyValue> <MkKeyValue class="_formBlock"> - <template #key>{{ $ts.permission }}</template> + <template #key>{{ i18n.ts.permission }}</template> <template #value>{{ plugin.permission }}</template> </MkKeyValue> <div style="display: flex; gap: var(--margin); flex-wrap: wrap;"> - <MkButton v-if="plugin.config" inline @click="config(plugin)"><i class="fas fa-cog"></i> {{ $ts.settings }}</MkButton> - <MkButton inline danger @click="uninstall(plugin)"><i class="fas fa-trash-alt"></i> {{ $ts.uninstall }}</MkButton> + <MkButton v-if="plugin.config" inline @click="config(plugin)"><i class="fas fa-cog"></i> {{ i18n.ts.settings }}</MkButton> + <MkButton inline danger @click="uninstall(plugin)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.uninstall }}</MkButton> </div> </div> </FormSection> </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { defineExpose, nextTick, ref } from 'vue'; import FormLink from '@/components/form/link.vue'; import FormSwitch from '@/components/form/switch.vue'; import FormSection from '@/components/form/section.vue'; @@ -41,67 +41,54 @@ import MkKeyValue from '@/components/key-value.vue'; import * as os from '@/os'; import { ColdDeviceStorage } from '@/store'; import * as symbols from '@/symbols'; +import { unisonReload } from '@/scripts/unison-reload'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - FormLink, - FormSwitch, - FormSection, - MkButton, - MkKeyValue, - }, +const plugins = ref(ColdDeviceStorage.get('plugins')); - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.plugins, - icon: 'fas fa-plug', - bg: 'var(--bg)', - }, - plugins: ColdDeviceStorage.get('plugins'), - } - }, +function uninstall(plugin) { + ColdDeviceStorage.set('plugins', plugins.value.filter(x => x.id !== plugin.id)); + os.success(); + nextTick(() => { + unisonReload(); + }); +} - methods: { - uninstall(plugin) { - ColdDeviceStorage.set('plugins', this.plugins.filter(x => x.id !== plugin.id)); - os.success(); - this.$nextTick(() => { - unisonReload(); - }); - }, +// TODO: この処理をstore側にactionとして移動し、設定画面を開くAiScriptAPIを実装できるようにする +async function config(plugin) { + const config = plugin.config; + for (const key in plugin.configData) { + config[key].default = plugin.configData[key]; + } - // TODO: この処理をstore側にactionとして移動し、設定画面を開くAiScriptAPIを実装できるようにする - async config(plugin) { - const config = plugin.config; - for (const key in plugin.configData) { - config[key].default = plugin.configData[key]; - } + const { canceled, result } = await os.form(plugin.name, config); + if (canceled) return; - const { canceled, result } = await os.form(plugin.name, config); - if (canceled) return; + const coldPlugins = ColdDeviceStorage.get('plugins'); + coldPlugins.find(p => p.id === plugin.id)!.configData = result; + ColdDeviceStorage.set('plugins', coldPlugins); - const plugins = ColdDeviceStorage.get('plugins'); - plugins.find(p => p.id === plugin.id).configData = result; - ColdDeviceStorage.set('plugins', plugins); + nextTick(() => { + location.reload(); + }); +} - this.$nextTick(() => { - location.reload(); - }); - }, +function changeActive(plugin, active) { + const coldPlugins = ColdDeviceStorage.get('plugins'); + coldPlugins.find(p => p.id === plugin.id)!.active = active; + ColdDeviceStorage.set('plugins', coldPlugins); - changeActive(plugin, active) { - const plugins = ColdDeviceStorage.get('plugins'); - plugins.find(p => p.id === plugin.id).active = active; - ColdDeviceStorage.set('plugins', plugins); + nextTick(() => { + location.reload(); + }); +} - this.$nextTick(() => { - location.reload(); - }); - } - }, +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.plugins, + icon: 'fas fa-plug', + bg: 'var(--bg)', + } }); </script> diff --git a/packages/client/src/pages/settings/profile.vue b/packages/client/src/pages/settings/profile.vue index e991d725b6..b64dc93cc7 100644 --- a/packages/client/src/pages/settings/profile.vue +++ b/packages/client/src/pages/settings/profile.vue @@ -62,7 +62,7 @@ </template> <script lang="ts" setup> -import { defineComponent, reactive, watch } from 'vue'; +import { reactive, watch } from 'vue'; import MkButton from '@/components/ui/button.vue'; import FormInput from '@/components/form/input.vue'; import FormTextarea from '@/components/form/textarea.vue'; @@ -132,8 +132,21 @@ function save() { function changeAvatar(ev) { selectFile(ev.currentTarget ?? ev.target, i18n.ts.avatar).then(async (file) => { + let originalOrCropped = file; + + const { canceled } = await os.confirm({ + type: 'question', + text: i18n.t('cropImageAsk'), + }); + + if (!canceled) { + originalOrCropped = await os.cropImage(file, { + aspectRatio: 1, + }); + } + const i = await os.apiWithDialog('i/update', { - avatarId: file.id, + avatarId: originalOrCropped.id, }); $i.avatarId = i.avatarId; $i.avatarUrl = i.avatarUrl; @@ -142,8 +155,21 @@ function changeAvatar(ev) { function changeBanner(ev) { selectFile(ev.currentTarget ?? ev.target, i18n.ts.banner).then(async (file) => { + let originalOrCropped = file; + + const { canceled } = await os.confirm({ + type: 'question', + text: i18n.t('cropImageAsk'), + }); + + if (!canceled) { + originalOrCropped = await os.cropImage(file, { + aspectRatio: 2, + }); + } + const i = await os.apiWithDialog('i/update', { - bannerId: file.id, + bannerId: originalOrCropped.id, }); $i.bannerId = i.bannerId; $i.bannerUrl = i.bannerUrl; diff --git a/packages/client/src/pages/settings/reaction.vue b/packages/client/src/pages/settings/reaction.vue index a188ba353d..963ac81dfa 100644 --- a/packages/client/src/pages/settings/reaction.vue +++ b/packages/client/src/pages/settings/reaction.vue @@ -54,7 +54,7 @@ </template> <script lang="ts" setup> -import { watch } from 'vue'; +import { defineAsyncComponent, watch } from 'vue'; import XDraggable from 'vuedraggable'; import FormInput from '@/components/form/input.vue'; import FormRadios from '@/components/form/radios.vue'; @@ -88,7 +88,7 @@ function remove(reaction, ev: MouseEvent) { } function preview(ev: MouseEvent) { - os.popup(import('@/components/emoji-picker-dialog.vue'), { + os.popup(defineAsyncComponent(() => import('@/components/emoji-picker-dialog.vue')), { asReactionPicker: true, src: ev.currentTarget ?? ev.target, }, {}, 'closed'); diff --git a/packages/client/src/pages/settings/security.vue b/packages/client/src/pages/settings/security.vue index 6fb3f1c413..401648790a 100644 --- a/packages/client/src/pages/settings/security.vue +++ b/packages/client/src/pages/settings/security.vue @@ -1,17 +1,17 @@ <template> <div class="_formRoot"> <FormSection> - <template #label>{{ $ts.password }}</template> - <FormButton primary @click="change()">{{ $ts.changePassword }}</FormButton> + <template #label>{{ i18n.ts.password }}</template> + <FormButton primary @click="change()">{{ i18n.ts.changePassword }}</FormButton> </FormSection> <FormSection> - <template #label>{{ $ts.twoStepAuthentication }}</template> + <template #label>{{ i18n.ts.twoStepAuthentication }}</template> <X2fa/> </FormSection> <FormSection> - <template #label>{{ $ts.signinHistory }}</template> + <template #label>{{ i18n.ts.signinHistory }}</template> <MkPagination :pagination="pagination"> <template v-slot="{items}"> <div> @@ -30,15 +30,15 @@ <FormSection> <FormSlot> - <FormButton danger @click="regenerateToken"><i class="fas fa-sync-alt"></i> {{ $ts.regenerateLoginToken }}</FormButton> - <template #caption>{{ $ts.regenerateLoginTokenDescription }}</template> + <FormButton danger @click="regenerateToken"><i class="fas fa-sync-alt"></i> {{ i18n.ts.regenerateLoginToken }}</FormButton> + <template #caption>{{ i18n.ts.regenerateLoginTokenDescription }}</template> </FormSlot> </FormSection> </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { defineExpose } from 'vue'; import FormSection from '@/components/form/section.vue'; import FormSlot from '@/components/form/slot.vue'; import FormButton from '@/components/ui/button.vue'; @@ -46,77 +46,63 @@ import MkPagination from '@/components/ui/pagination.vue'; import X2fa from './2fa.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - FormSection, - FormButton, - MkPagination, - FormSlot, - X2fa, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.security, - icon: 'fas fa-lock', - bg: 'var(--bg)', - }, - pagination: { - endpoint: 'i/signin-history' as const, - limit: 5, - }, - } - }, +const pagination = { + endpoint: 'i/signin-history' as const, + limit: 5, +}; - methods: { - async change() { - const { canceled: canceled1, result: currentPassword } = await os.inputText({ - title: this.$ts.currentPassword, - type: 'password' - }); - if (canceled1) return; +async function change() { + const { canceled: canceled1, result: currentPassword } = await os.inputText({ + title: i18n.ts.currentPassword, + type: 'password' + }); + if (canceled1) return; - const { canceled: canceled2, result: newPassword } = await os.inputText({ - title: this.$ts.newPassword, - type: 'password' - }); - if (canceled2) return; + const { canceled: canceled2, result: newPassword } = await os.inputText({ + title: i18n.ts.newPassword, + type: 'password' + }); + if (canceled2) return; - const { canceled: canceled3, result: newPassword2 } = await os.inputText({ - title: this.$ts.newPasswordRetype, - type: 'password' - }); - if (canceled3) return; + const { canceled: canceled3, result: newPassword2 } = await os.inputText({ + title: i18n.ts.newPasswordRetype, + type: 'password' + }); + if (canceled3) return; - if (newPassword !== newPassword2) { - os.alert({ - type: 'error', - text: this.$ts.retypedNotMatch - }); - return; - } - - os.apiWithDialog('i/change-password', { - currentPassword, - newPassword - }); - }, + if (newPassword !== newPassword2) { + os.alert({ + type: 'error', + text: i18n.ts.retypedNotMatch + }); + return; + } + + os.apiWithDialog('i/change-password', { + currentPassword, + newPassword + }); +} + +function regenerateToken() { + os.inputText({ + title: i18n.ts.password, + type: 'password' + }).then(({ canceled, result: password }) => { + if (canceled) return; + os.api('i/regenerate_token', { + password: password + }); + }); +} - regenerateToken() { - os.inputText({ - title: this.$ts.password, - type: 'password' - }).then(({ canceled, result: password }) => { - if (canceled) return; - os.api('i/regenerate_token', { - password: password - }); - }); - }, +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.security, + icon: 'fas fa-lock', + bg: 'var(--bg)', } }); </script> diff --git a/packages/client/src/pages/settings/sounds.vue b/packages/client/src/pages/settings/sounds.vue index 490a1b5514..d01e87c1f8 100644 --- a/packages/client/src/pages/settings/sounds.vue +++ b/packages/client/src/pages/settings/sounds.vue @@ -1,24 +1,24 @@ <template> <div class="_formRoot"> <FormRange v-model="masterVolume" :min="0" :max="1" :step="0.05" :text-converter="(v) => `${Math.floor(v * 100)}%`" class="_formBlock"> - <template #label>{{ $ts.masterVolume }}</template> + <template #label>{{ i18n.ts.masterVolume }}</template> </FormRange> <FormSection> - <template #label>{{ $ts.sounds }}</template> + <template #label>{{ i18n.ts.sounds }}</template> <FormLink v-for="type in Object.keys(sounds)" :key="type" style="margin-bottom: 8px;" @click="edit(type)"> {{ $t('_sfx.' + type) }} - <template #suffix>{{ sounds[type].type || $ts.none }}</template> + <template #suffix>{{ sounds[type].type || i18n.ts.none }}</template> <template #suffixIcon><i class="fas fa-chevron-down"></i></template> </FormLink> </FormSection> - <FormButton danger class="_formBlock" @click="reset()"><i class="fas fa-redo"></i> {{ $ts.default }}</FormButton> + <FormButton danger class="_formBlock" @click="reset()"><i class="fas fa-redo"></i> {{ i18n.ts.default }}</FormButton> </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { computed, defineExpose, ref } from 'vue'; import FormRange from '@/components/form/range.vue'; import FormButton from '@/components/ui/button.vue'; import FormLink from '@/components/form/link.vue'; @@ -27,6 +27,28 @@ import * as os from '@/os'; import { ColdDeviceStorage } from '@/store'; import { playFile } from '@/scripts/sound'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; + +const masterVolume = computed({ + get: () => { + return ColdDeviceStorage.get('sound_masterVolume'); + }, + set: (value) => { + ColdDeviceStorage.set('sound_masterVolume', value); + } +}); + +const volumeIcon = computed(() => masterVolume.value === 0 ? 'fas fa-volume-mute' : 'fas fa-volume-up'); + +const sounds = ref({ + note: ColdDeviceStorage.get('sound_note'), + noteMy: ColdDeviceStorage.get('sound_noteMy'), + notification: ColdDeviceStorage.get('sound_notification'), + chat: ColdDeviceStorage.get('sound_chat'), + chatBg: ColdDeviceStorage.get('sound_chatBg'), + antenna: ColdDeviceStorage.get('sound_antenna'), + channel: ColdDeviceStorage.get('sound_channel'), +}); const soundsTypes = [ null, @@ -55,94 +77,58 @@ const soundsTypes = [ 'noizenecio/kick_gaba2', ]; -export default defineComponent({ - components: { - FormLink, - FormButton, - FormRange, - FormSection, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.sounds, - icon: 'fas fa-music', - bg: 'var(--bg)', - }, - sounds: {}, - } - }, - - computed: { - masterVolume: { // TODO: (外部)関数にcomputedを使うのはアレなので直す - get() { return ColdDeviceStorage.get('sound_masterVolume'); }, - set(value) { ColdDeviceStorage.set('sound_masterVolume', value); } +async function edit(type) { + const { canceled, result } = await os.form(i18n.t('_sfx.' + type), { + type: { + type: 'enum', + enum: soundsTypes.map(x => ({ + value: x, + label: x == null ? i18n.ts.none : x, + })), + label: i18n.ts.sound, + default: sounds.value[type].type, + }, + volume: { + type: 'range', + mim: 0, + max: 1, + step: 0.05, + textConverter: (v) => `${Math.floor(v * 100)}%`, + label: i18n.ts.volume, + default: sounds.value[type].volume }, - volumeIcon() { - return this.masterVolume === 0 ? 'fas fa-volume-mute' : 'fas fa-volume-up'; + listen: { + type: 'button', + content: i18n.ts.listen, + action: (_, values) => { + playFile(values.type, values.volume); + } } - }, + }); + if (canceled) return; - created() { - this.sounds.note = ColdDeviceStorage.get('sound_note'); - this.sounds.noteMy = ColdDeviceStorage.get('sound_noteMy'); - this.sounds.notification = ColdDeviceStorage.get('sound_notification'); - this.sounds.chat = ColdDeviceStorage.get('sound_chat'); - this.sounds.chatBg = ColdDeviceStorage.get('sound_chatBg'); - this.sounds.antenna = ColdDeviceStorage.get('sound_antenna'); - this.sounds.channel = ColdDeviceStorage.get('sound_channel'); - }, + const v = { + type: result.type, + volume: result.volume, + }; - methods: { - async edit(type) { - const { canceled, result } = await os.form(this.$t('_sfx.' + type), { - type: { - type: 'enum', - enum: soundsTypes.map(x => ({ - value: x, - label: x == null ? this.$ts.none : x, - })), - label: this.$ts.sound, - default: this.sounds[type].type, - }, - volume: { - type: 'range', - mim: 0, - max: 1, - step: 0.05, - textConverter: (v) => `${Math.floor(v * 100)}%`, - label: this.$ts.volume, - default: this.sounds[type].volume - }, - listen: { - type: 'button', - content: this.$ts.listen, - action: (_, values) => { - playFile(values.type, values.volume); - } - } - }); - if (canceled) return; + ColdDeviceStorage.set('sound_' + type, v); + sounds.value[type] = v; +} - const v = { - type: result.type, - volume: result.volume, - }; - - ColdDeviceStorage.set('sound_' + type, v); - this.sounds[type] = v; - }, +function reset() { + for (const sound of Object.keys(sounds.value)) { + const v = ColdDeviceStorage.default['sound_' + sound]; + ColdDeviceStorage.set('sound_' + sound, v); + sounds.value[sound] = v; + } +} - reset() { - for (const sound of Object.keys(this.sounds)) { - const v = ColdDeviceStorage.default['sound_' + sound]; - ColdDeviceStorage.set('sound_' + sound, v); - this.sounds[sound] = v; - } - } +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.sounds, + icon: 'fas fa-music', + bg: 'var(--bg)', } }); </script> diff --git a/packages/client/src/pages/settings/theme.install.vue b/packages/client/src/pages/settings/theme.install.vue index 2d3514342e..25fa6c012b 100644 --- a/packages/client/src/pages/settings/theme.install.vue +++ b/packages/client/src/pages/settings/theme.install.vue @@ -13,7 +13,7 @@ <script lang="ts" setup> import { } from 'vue'; -import * as JSON5 from 'json5'; +import JSON5 from 'json5'; import FormTextarea from '@/components/form/textarea.vue'; import FormButton from '@/components/ui/button.vue'; import { applyTheme, validateTheme } from '@/scripts/theme'; @@ -29,7 +29,7 @@ function parseThemeCode(code: string) { try { theme = JSON5.parse(code); - } catch (e) { + } catch (err) { os.alert({ type: 'error', text: i18n.ts._theme.invalid diff --git a/packages/client/src/pages/settings/theme.manage.vue b/packages/client/src/pages/settings/theme.manage.vue index a1e849b540..94b2d24455 100644 --- a/packages/client/src/pages/settings/theme.manage.vue +++ b/packages/client/src/pages/settings/theme.manage.vue @@ -1,95 +1,77 @@ <template> <div class="_formRoot"> <FormSelect v-model="selectedThemeId" class="_formBlock"> - <template #label>{{ $ts.theme }}</template> - <optgroup :label="$ts._theme.installedThemes"> + <template #label>{{ i18n.ts.theme }}</template> + <optgroup :label="i18n.ts._theme.installedThemes"> <option v-for="x in installedThemes" :key="x.id" :value="x.id">{{ x.name }}</option> </optgroup> - <optgroup :label="$ts._theme.builtinThemes"> + <optgroup :label="i18n.ts._theme.builtinThemes"> <option v-for="x in builtinThemes" :key="x.id" :value="x.id">{{ x.name }}</option> </optgroup> </FormSelect> <template v-if="selectedTheme"> - <FormInput readonly :modelValue="selectedTheme.author" class="_formBlock"> - <template #label>{{ $ts.author }}</template> + <FormInput readonly :model-value="selectedTheme.author" class="_formBlock"> + <template #label>{{ i18n.ts.author }}</template> </FormInput> - <FormTextarea v-if="selectedTheme.desc" readonly :modelValue="selectedTheme.desc" class="_formBlock"> - <template #label>{{ $ts._theme.description }}</template> + <FormTextarea v-if="selectedTheme.desc" readonly :model-value="selectedTheme.desc" class="_formBlock"> + <template #label>{{ i18n.ts._theme.description }}</template> </FormTextarea> - <FormTextarea readonly tall :modelValue="selectedThemeCode" class="_formBlock"> - <template #label>{{ $ts._theme.code }}</template> - <template #caption><button class="_textButton" @click="copyThemeCode()">{{ $ts.copy }}</button></template> + <FormTextarea readonly tall :model-value="selectedThemeCode" class="_formBlock"> + <template #label>{{ i18n.ts._theme.code }}</template> + <template #caption><button class="_textButton" @click="copyThemeCode()">{{ i18n.ts.copy }}</button></template> </FormTextarea> - <FormButton v-if="!builtinThemes.some(t => t.id == selectedTheme.id)" class="_formBlock" danger @click="uninstall()"><i class="fas fa-trash-alt"></i> {{ $ts.uninstall }}</FormButton> + <FormButton v-if="!builtinThemes.some(t => t.id == selectedTheme.id)" class="_formBlock" danger @click="uninstall()"><i class="fas fa-trash-alt"></i> {{ i18n.ts.uninstall }}</FormButton> </template> </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; -import * as JSON5 from 'json5'; +<script lang="ts" setup> +import { computed, defineExpose, ref } from 'vue'; +import JSON5 from 'json5'; import FormTextarea from '@/components/form/textarea.vue'; import FormSelect from '@/components/form/select.vue'; import FormInput from '@/components/form/input.vue'; import FormButton from '@/components/ui/button.vue'; -import { Theme, builtinThemes } from '@/scripts/theme'; +import { Theme, getBuiltinThemesRef } from '@/scripts/theme'; import copyToClipboard from '@/scripts/copy-to-clipboard'; import * as os from '@/os'; -import { ColdDeviceStorage } from '@/store'; import { getThemes, removeTheme } from '@/theme-store'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - FormTextarea, - FormSelect, - FormInput, - FormButton, - }, +const installedThemes = ref(getThemes()); +const builtinThemes = getBuiltinThemesRef(); +const selectedThemeId = ref(null); - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts._theme.manage, - icon: 'fas fa-folder-open', - bg: 'var(--bg)', - }, - installedThemes: getThemes(), - builtinThemes, - selectedThemeId: null, - } - }, +const themes = computed(() => [ ...installedThemes.value, ...builtinThemes.value ]); - computed: { - themes(): Theme[] { - return this.builtinThemes.concat(this.installedThemes); - }, - - selectedTheme() { - if (this.selectedThemeId == null) return null; - return this.themes.find(x => x.id === this.selectedThemeId); - }, +const selectedTheme = computed(() => { + if (selectedThemeId.value == null) return null; + return themes.value.find(x => x.id === selectedThemeId.value); +}); + +const selectedThemeCode = computed(() => { + if (selectedTheme.value == null) return null; + return JSON5.stringify(selectedTheme.value, null, '\t'); +}); - selectedThemeCode() { - if (this.selectedTheme == null) return null; - return JSON5.stringify(this.selectedTheme, null, '\t'); - }, - }, +function copyThemeCode() { + copyToClipboard(selectedThemeCode.value); + os.success(); +} - methods: { - copyThemeCode() { - copyToClipboard(this.selectedThemeCode); - os.success(); - }, +function uninstall() { + removeTheme(selectedTheme.value as Theme); + installedThemes.value = installedThemes.value.filter(t => t.id !== selectedThemeId.value); + selectedThemeId.value = null; + os.success(); +} - uninstall() { - removeTheme(this.selectedTheme); - this.installedThemes = this.installedThemes.filter(t => t.id !== this.selectedThemeId); - this.selectedThemeId = null; - os.success(); - }, +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts._theme.manage, + icon: 'fas fa-folder-open', + bg: 'var(--bg)', } }); </script> diff --git a/packages/client/src/pages/settings/theme.vue b/packages/client/src/pages/settings/theme.vue index d134a092b6..5e7ffcff4b 100644 --- a/packages/client/src/pages/settings/theme.vue +++ b/packages/client/src/pages/settings/theme.vue @@ -85,116 +85,94 @@ </div> </template> -<script lang="ts"> -import { computed, defineComponent, onActivated, onMounted, ref, watch } from 'vue'; -import * as JSON5 from 'json5'; +<script lang="ts" setup> +import { computed, onActivated, ref, watch } from 'vue'; +import JSON5 from 'json5'; import FormSwitch from '@/components/form/switch.vue'; import FormSelect from '@/components/form/select.vue'; -import FormGroup from '@/components/form/group.vue'; import FormSection from '@/components/form/section.vue'; import FormLink from '@/components/form/link.vue'; import FormButton from '@/components/ui/button.vue'; -import { builtinThemes } from '@/scripts/theme'; +import { getBuiltinThemesRef } from '@/scripts/theme'; import { selectFile } from '@/scripts/select-file'; import { isDeviceDarkmode } from '@/scripts/is-device-darkmode'; import { ColdDeviceStorage } from '@/store'; import { i18n } from '@/i18n'; import { defaultStore } from '@/store'; import { instance } from '@/instance'; -import { concat, uniqueBy } from '@/scripts/array'; +import { uniqueBy } from '@/scripts/array'; import { fetchThemes, getThemes } from '@/theme-store'; import * as symbols from '@/symbols'; -export default defineComponent({ - components: { - FormSwitch, - FormSelect, - FormGroup, - FormSection, - FormLink, - FormButton, - }, +const installedThemes = ref(getThemes()); +const builtinThemes = getBuiltinThemesRef(); +const instanceThemes = []; - emits: ['info'], +if (instance.defaultLightTheme != null) instanceThemes.push(JSON5.parse(instance.defaultLightTheme)); +if (instance.defaultDarkTheme != null) instanceThemes.push(JSON5.parse(instance.defaultDarkTheme)); - setup(props, { emit }) { - const INFO = { - title: i18n.ts.theme, - icon: 'fas fa-palette', - bg: 'var(--bg)', - }; +const themes = computed(() => uniqueBy([ ...instanceThemes, ...builtinThemes.value, ...installedThemes.value ], theme => theme.id)); +const darkThemes = computed(() => themes.value.filter(t => t.base === 'dark' || t.kind === 'dark')); +const lightThemes = computed(() => themes.value.filter(t => t.base === 'light' || t.kind === 'light')); +const darkTheme = ColdDeviceStorage.ref('darkTheme'); +const darkThemeId = computed({ + get() { + return darkTheme.value.id; + }, + set(id) { + ColdDeviceStorage.set('darkTheme', themes.value.find(x => x.id === id)); + } +}); +const lightTheme = ColdDeviceStorage.ref('lightTheme'); +const lightThemeId = computed({ + get() { + return lightTheme.value.id; + }, + set(id) { + ColdDeviceStorage.set('lightTheme', themes.value.find(x => x.id === id)); + } +}); +const darkMode = computed(defaultStore.makeGetterSetter('darkMode')); +const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode')); +const wallpaper = ref(localStorage.getItem('wallpaper')); +const themesCount = installedThemes.value.length; - const installedThemes = ref(getThemes()); - const instanceThemes = []; - if (instance.defaultLightTheme != null) instanceThemes.push(JSON5.parse(instance.defaultLightTheme)); - if (instance.defaultDarkTheme != null) instanceThemes.push(JSON5.parse(instance.defaultDarkTheme)); - const themes = computed(() => uniqueBy(instanceThemes.concat(builtinThemes.concat(installedThemes.value)), theme => theme.id)); - const darkThemes = computed(() => themes.value.filter(t => t.base === 'dark' || t.kind === 'dark')); - const lightThemes = computed(() => themes.value.filter(t => t.base === 'light' || t.kind === 'light')); - const darkTheme = ColdDeviceStorage.ref('darkTheme'); - const darkThemeId = computed({ - get() { - return darkTheme.value.id; - }, - set(id) { - ColdDeviceStorage.set('darkTheme', themes.value.find(x => x.id === id)) - } - }); - const lightTheme = ColdDeviceStorage.ref('lightTheme'); - const lightThemeId = computed({ - get() { - return lightTheme.value.id; - }, - set(id) { - ColdDeviceStorage.set('lightTheme', themes.value.find(x => x.id === id)) - } - }); - const darkMode = computed(defaultStore.makeGetterSetter('darkMode')); - const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode')); - const wallpaper = ref(localStorage.getItem('wallpaper')); - const themesCount = installedThemes.value.length; +watch(syncDeviceDarkMode, () => { + if (syncDeviceDarkMode.value) { + defaultStore.set('darkMode', isDeviceDarkmode()); + } +}); - watch(syncDeviceDarkMode, () => { - if (syncDeviceDarkMode.value) { - defaultStore.set('darkMode', isDeviceDarkmode()); - } - }); +watch(wallpaper, () => { + if (wallpaper.value == null) { + localStorage.removeItem('wallpaper'); + } else { + localStorage.setItem('wallpaper', wallpaper.value); + } + location.reload(); +}); - watch(wallpaper, () => { - if (wallpaper.value == null) { - localStorage.removeItem('wallpaper'); - } else { - localStorage.setItem('wallpaper', wallpaper.value); - } - location.reload(); - }); +onActivated(() => { + fetchThemes().then(() => { + installedThemes.value = getThemes(); + }); +}); - onActivated(() => { - fetchThemes().then(() => { - installedThemes.value = getThemes(); - }); - }); +fetchThemes().then(() => { + installedThemes.value = getThemes(); +}); - fetchThemes().then(() => { - installedThemes.value = getThemes(); - }); +function setWallpaper(event) { + selectFile(event.currentTarget ?? event.target, null).then(file => { + wallpaper.value = file.url; + }); +} - return { - [symbols.PAGE_INFO]: INFO, - darkThemes, - lightThemes, - darkThemeId, - lightThemeId, - darkMode, - syncDeviceDarkMode, - themesCount, - wallpaper, - setWallpaper(e) { - selectFile(e.currentTarget ?? e.target, null).then(file => { - wallpaper.value = file.url; - }); - }, - }; +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.theme, + icon: 'fas fa-palette', + bg: 'var(--bg)', } }); </script> diff --git a/packages/client/src/pages/settings/webhook.edit.vue b/packages/client/src/pages/settings/webhook.edit.vue index bb3a25407e..3690526b41 100644 --- a/packages/client/src/pages/settings/webhook.edit.vue +++ b/packages/client/src/pages/settings/webhook.edit.vue @@ -43,6 +43,14 @@ import * as os from '@/os'; import * as symbols from '@/symbols'; import { i18n } from '@/i18n'; +defineExpose({ + [symbols.PAGE_INFO]: { + title: 'Edit webhook', + icon: 'fas fa-bolt', + bg: 'var(--bg)', + }, +}); + const webhook = await os.api('i/webhooks/show', { webhookId: new URLSearchParams(window.location.search).get('id') }); @@ -78,12 +86,4 @@ async function save(): Promise<void> { active, }); } - -defineExpose({ - [symbols.PAGE_INFO]: { - title: 'Edit webhook', - icon: 'fas fa-bolt', - bg: 'var(--bg)', - }, -}); </script> diff --git a/packages/client/src/pages/settings/word-mute.vue b/packages/client/src/pages/settings/word-mute.vue index c11707b6cf..6e1a4b2ccb 100644 --- a/packages/client/src/pages/settings/word-mute.vue +++ b/packages/client/src/pages/settings/word-mute.vue @@ -1,35 +1,35 @@ <template> <div class="_formRoot"> <MkTab v-model="tab" class="_formBlock"> - <option value="soft">{{ $ts._wordMute.soft }}</option> - <option value="hard">{{ $ts._wordMute.hard }}</option> + <option value="soft">{{ i18n.ts._wordMute.soft }}</option> + <option value="hard">{{ i18n.ts._wordMute.hard }}</option> </MkTab> <div class="_formBlock"> <div v-show="tab === 'soft'"> - <MkInfo class="_formBlock">{{ $ts._wordMute.softDescription }}</MkInfo> + <MkInfo class="_formBlock">{{ i18n.ts._wordMute.softDescription }}</MkInfo> <FormTextarea v-model="softMutedWords" class="_formBlock"> - <span>{{ $ts._wordMute.muteWords }}</span> - <template #caption>{{ $ts._wordMute.muteWordsDescription }}<br>{{ $ts._wordMute.muteWordsDescription2 }}</template> + <span>{{ i18n.ts._wordMute.muteWords }}</span> + <template #caption>{{ i18n.ts._wordMute.muteWordsDescription }}<br>{{ i18n.ts._wordMute.muteWordsDescription2 }}</template> </FormTextarea> </div> <div v-show="tab === 'hard'"> - <MkInfo class="_formBlock">{{ $ts._wordMute.hardDescription }} {{ $ts.reflectMayTakeTime }}</MkInfo> + <MkInfo class="_formBlock">{{ i18n.ts._wordMute.hardDescription }} {{ i18n.ts.reflectMayTakeTime }}</MkInfo> <FormTextarea v-model="hardMutedWords" class="_formBlock"> - <span>{{ $ts._wordMute.muteWords }}</span> - <template #caption>{{ $ts._wordMute.muteWordsDescription }}<br>{{ $ts._wordMute.muteWordsDescription2 }}</template> + <span>{{ i18n.ts._wordMute.muteWords }}</span> + <template #caption>{{ i18n.ts._wordMute.muteWordsDescription }}<br>{{ i18n.ts._wordMute.muteWordsDescription2 }}</template> </FormTextarea> <MkKeyValue v-if="hardWordMutedNotesCount != null" class="_formBlock"> - <template #key>{{ $ts._wordMute.mutedNotes }}</template> + <template #key>{{ i18n.ts._wordMute.mutedNotes }}</template> <template #value>{{ number(hardWordMutedNotesCount) }}</template> </MkKeyValue> </div> </div> - <MkButton primary inline :disabled="!changed" @click="save()"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton> + <MkButton primary inline :disabled="!changed" @click="save()"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton> </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { defineExpose, ref, watch } from 'vue'; import FormTextarea from '@/components/form/textarea.vue'; import MkKeyValue from '@/components/key-value.vue'; import MkButton from '@/components/ui/button.vue'; @@ -38,114 +38,90 @@ import MkTab from '@/components/tab.vue'; import * as os from '@/os'; import number from '@/filters/number'; import * as symbols from '@/symbols'; +import { defaultStore } from '@/store'; +import { $i } from '@/account'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkButton, - FormTextarea, - MkKeyValue, - MkTab, - MkInfo, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.wordMute, - icon: 'fas fa-comment-slash', - bg: 'var(--bg)', - }, - tab: 'soft', - softMutedWords: '', - hardMutedWords: '', - hardWordMutedNotesCount: null, - changed: false, - } - }, +const render = (mutedWords) => mutedWords.map(x => { + if (Array.isArray(x)) { + return x.join(' '); + } else { + return x; + } +}).join('\n'); - watch: { - softMutedWords: { - handler() { - this.changed = true; - }, - deep: true - }, - hardMutedWords: { - handler() { - this.changed = true; - }, - deep: true - }, - }, +const tab = ref('soft'); +const softMutedWords = ref(render(defaultStore.state.mutedWords)); +const hardMutedWords = ref(render($i!.mutedWords)); +const hardWordMutedNotesCount = ref(null); +const changed = ref(false); - async created() { - const render = (mutedWords) => mutedWords.map(x => { - if (Array.isArray(x)) { - return x.join(' '); - } else { - return x; - } - }).join('\n'); +os.api('i/get-word-muted-notes-count', {}).then(response => { + hardWordMutedNotesCount.value = response?.count; +}); - this.softMutedWords = render(this.$store.state.mutedWords); - this.hardMutedWords = render(this.$i.mutedWords); +watch(softMutedWords, () => { + changed.value = true; +}); - this.hardWordMutedNotesCount = (await os.api('i/get-word-muted-notes-count', {})).count; - }, +watch(hardMutedWords, () => { + changed.value = true; +}); - methods: { - async save() { - const parseMutes = (mutes, tab) => { - // split into lines, remove empty lines and unnecessary whitespace - let lines = mutes.trim().split('\n').map(line => line.trim()).filter(line => line != ''); +async function save() { + const parseMutes = (mutes, tab) => { + // split into lines, remove empty lines and unnecessary whitespace + let lines = mutes.trim().split('\n').map(line => line.trim()).filter(line => line !== ''); - // check each line if it is a RegExp or not - for (let i = 0; i < lines.length; i++) { - const line = lines[i] - const regexp = line.match(/^\/(.+)\/(.*)$/); - if (regexp) { - // check that the RegExp is valid - try { - new RegExp(regexp[1], regexp[2]); - // note that regex lines will not be split by spaces! - } catch (err) { - // invalid syntax: do not save, do not reset changed flag - os.alert({ - type: 'error', - title: this.$ts.regexpError, - text: this.$t('regexpErrorDescription', { tab, line: i + 1 }) + "\n" + err.toString() - }); - // re-throw error so these invalid settings are not saved - throw err; - } - } else { - lines[i] = line.split(' '); - } + // check each line if it is a RegExp or not + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const regexp = line.match(/^\/(.+)\/(.*)$/); + if (regexp) { + // check that the RegExp is valid + try { + new RegExp(regexp[1], regexp[2]); + // note that regex lines will not be split by spaces! + } catch (err: any) { + // invalid syntax: do not save, do not reset changed flag + os.alert({ + type: 'error', + title: i18n.ts.regexpError, + text: i18n.t('regexpErrorDescription', { tab, line: i + 1 }) + "\n" + err.toString() + }); + // re-throw error so these invalid settings are not saved + throw err; } + } else { + lines[i] = line.split(' '); + } + } - return lines; - }; + return lines; + }; - let softMutes, hardMutes; - try { - softMutes = parseMutes(this.softMutedWords, this.$ts._wordMute.soft); - hardMutes = parseMutes(this.hardMutedWords, this.$ts._wordMute.hard); - } catch (err) { - // already displayed error message in parseMutes - return; - } + let softMutes, hardMutes; + try { + softMutes = parseMutes(softMutedWords.value, i18n.ts._wordMute.soft); + hardMutes = parseMutes(hardMutedWords.value, i18n.ts._wordMute.hard); + } catch (err) { + // already displayed error message in parseMutes + return; + } - this.$store.set('mutedWords', softMutes); - await os.api('i/update', { - mutedWords: hardMutes, - }); + defaultStore.set('mutedWords', softMutes); + await os.api('i/update', { + mutedWords: hardMutes, + }); - this.changed = false; - }, + changed.value = false; +} - number +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.wordMute, + icon: 'fas fa-comment-slash', + bg: 'var(--bg)', } }); </script> diff --git a/packages/client/src/pages/share.vue b/packages/client/src/pages/share.vue index 4d77de5819..1700944f82 100644 --- a/packages/client/src/pages/share.vue +++ b/packages/client/src/pages/share.vue @@ -56,7 +56,7 @@ export default defineComponent({ localOnly: null as boolean | null, files: [] as Misskey.entities.DriveFile[], visibleUsers: [] as Misskey.entities.User[], - } + }; }, async created() { @@ -153,11 +153,11 @@ export default defineComponent({ ); } //#endregion - } catch (e) { + } catch (err) { os.alert({ type: 'error', - title: e.message, - text: e.name + title: err.message, + text: err.name }); } diff --git a/packages/client/src/pages/theme-editor.vue b/packages/client/src/pages/theme-editor.vue index a53e23c1c5..2a11c07fd2 100644 --- a/packages/client/src/pages/theme-editor.vue +++ b/packages/client/src/pages/theme-editor.vue @@ -67,15 +67,17 @@ <script lang="ts" setup> import { watch } from 'vue'; import { toUnicode } from 'punycode/'; -import * as tinycolor from 'tinycolor2'; -import { v4 as uuid} from 'uuid'; -import * as JSON5 from 'json5'; +import tinycolor from 'tinycolor2'; +import { v4 as uuid } from 'uuid'; +import JSON5 from 'json5'; import FormButton from '@/components/ui/button.vue'; import FormTextarea from '@/components/form/textarea.vue'; import FormFolder from '@/components/form/folder.vue'; -import { Theme, applyTheme, darkTheme, lightTheme } from '@/scripts/theme'; +import { Theme, applyTheme } from '@/scripts/theme'; +import lightTheme from '@/themes/_light.json5'; +import darkTheme from '@/themes/_dark.json5'; import { host } from '@/config'; import * as os from '@/os'; import { ColdDeviceStorage, defaultStore } from '@/store'; @@ -124,11 +126,11 @@ let changed = $ref(false); useLeaveGuard($$(changed)); function showPreview() { - os.pageWindow('preview'); + os.pageWindow('/preview'); } function setBgColor(color: typeof bgColors[number]) { - if (theme.base != color.kind) { + if (theme.base !== color.kind) { const base = color.kind === 'dark' ? darkTheme : lightTheme; for (const prop of Object.keys(base.props)) { if (prop === 'accent') continue; diff --git a/packages/client/src/pages/timeline.vue b/packages/client/src/pages/timeline.vue index 79f00c4b44..fe3dbc3cff 100644 --- a/packages/client/src/pages/timeline.vue +++ b/packages/client/src/pages/timeline.vue @@ -20,7 +20,7 @@ <script lang="ts"> export default { name: 'MkTimelinePage', -} +}; </script> <script lang="ts" setup> diff --git a/packages/client/src/pages/user-info.vue b/packages/client/src/pages/user-info.vue index 516ab4d440..54e1f13021 100644 --- a/packages/client/src/pages/user-info.vue +++ b/packages/client/src/pages/user-info.vue @@ -54,6 +54,9 @@ <FormButton v-if="user.host != null" class="_formBlock" @click="updateRemoteUser"><i class="fas fa-sync"></i> {{ $ts.updateRemoteUser }}</FormButton> </FormSection> + <MkObjectView v-if="info && $i.isAdmin" tall :value="info"> + </MkObjectView> + <MkObjectView tall :value="user"> </MkObjectView> </div> @@ -232,10 +235,10 @@ export default defineComponent({ await os.api('admin/delete-all-files-of-a-user', { userId: this.user.id }); os.success(); }; - await process().catch(e => { + await process().catch(err => { os.alert({ type: 'error', - text: e.toString() + text: err.toString(), }); }); await this.refreshUser(); diff --git a/packages/client/src/pages/user/index.vue b/packages/client/src/pages/user/index.vue index 10a86243f9..a024dd28bc 100644 --- a/packages/client/src/pages/user/index.vue +++ b/packages/client/src/pages/user/index.vue @@ -125,7 +125,7 @@ <script lang="ts"> import { defineComponent, defineAsyncComponent, computed } from 'vue'; -import * as age from 's-age'; +import age from 's-age'; import XUserTimeline from './index.timeline.vue'; import XNote from '@/components/note.vue'; import MkFollowButton from '@/components/follow-button.vue'; @@ -141,6 +141,7 @@ import number from '@/filters/number'; import { userPage, acct as getAcct } from '@/filters/user'; import * as os from '@/os'; import * as symbols from '@/symbols'; +import { MisskeyNavigator } from '@/scripts/navigate'; export default defineComponent({ components: { @@ -190,33 +191,34 @@ export default defineComponent({ active: this.page === 'index', title: this.$ts.overview, icon: 'fas fa-home', - onClick: () => { this.$router.push('/@' + getAcct(this.user)); }, + onClick: () => { this.mkNav.push('/@' + getAcct(this.user)); }, }, ...(this.$i && (this.$i.id === this.user.id)) || this.user.publicReactions ? [{ active: this.page === 'reactions', title: this.$ts.reaction, icon: 'fas fa-laugh', - onClick: () => { this.$router.push('/@' + getAcct(this.user) + '/reactions'); }, + onClick: () => { this.mkNav.push('/@' + getAcct(this.user) + '/reactions'); }, }] : [], { active: this.page === 'clips', title: this.$ts.clips, icon: 'fas fa-paperclip', - onClick: () => { this.$router.push('/@' + getAcct(this.user) + '/clips'); }, + onClick: () => { this.mkNav.push('/@' + getAcct(this.user) + '/clips'); }, }, { active: this.page === 'pages', title: this.$ts.pages, icon: 'fas fa-file-alt', - onClick: () => { this.$router.push('/@' + getAcct(this.user) + '/pages'); }, + onClick: () => { this.mkNav.push('/@' + getAcct(this.user) + '/pages'); }, }, { active: this.page === 'gallery', title: this.$ts.gallery, icon: 'fas fa-icons', - onClick: () => { this.$router.push('/@' + getAcct(this.user) + '/gallery'); }, + onClick: () => { this.mkNav.push('/@' + getAcct(this.user) + '/gallery'); }, }], } : null), user: null, error: null, parallaxAnimationId: null, narrow: null, + mkNav: new MisskeyNavigator(), }; }, @@ -258,8 +260,8 @@ export default defineComponent({ this.user = null; os.api('users/show', Acct.parse(this.acct)).then(user => { this.user = user; - }).catch(e => { - this.error = e; + }).catch(err => { + this.error = err; }); }, diff --git a/packages/client/src/pages/welcome.setup.vue b/packages/client/src/pages/welcome.setup.vue index ec23b76e29..1a2f460283 100644 --- a/packages/client/src/pages/welcome.setup.vue +++ b/packages/client/src/pages/welcome.setup.vue @@ -41,7 +41,7 @@ export default defineComponent({ password: '', submitting: false, host, - } + }; }, methods: { diff --git a/packages/client/src/pages/welcome.timeline.vue b/packages/client/src/pages/welcome.timeline.vue index 38a85f67b1..bec9481ffd 100644 --- a/packages/client/src/pages/welcome.timeline.vue +++ b/packages/client/src/pages/welcome.timeline.vue @@ -39,7 +39,7 @@ export default defineComponent({ return { notes: [], isScrolling: false, - } + }; }, created() { diff --git a/packages/client/src/router.ts b/packages/client/src/router.ts index 839841f0fe..db39dd741c 100644 --- a/packages/client/src/router.ts +++ b/packages/client/src/router.ts @@ -1,4 +1,4 @@ -import { defineAsyncComponent, markRaw } from 'vue'; +import { AsyncComponentLoader, defineAsyncComponent, markRaw } from 'vue'; import { createRouter, createWebHistory } from 'vue-router'; import MkLoading from '@/pages/_loading_.vue'; import MkError from '@/pages/_error_.vue'; @@ -6,8 +6,9 @@ import MkTimeline from '@/pages/timeline.vue'; import { $i, iAmModerator } from './account'; import { ui } from '@/config'; -const page = (path: string, ui?: string) => defineAsyncComponent({ - loader: ui ? () => import(`./ui/${ui}/pages/${path}.vue`) : () => import(`./pages/${path}.vue`), +// pathに/が入るとrollupが解決してくれないので、() => import('*.vue')を指定すること +const page = (path: string | AsyncComponentLoader<any>, uiName?: string) => defineAsyncComponent({ + loader: typeof path === 'string' ? uiName ? () => import(`./ui/${ui}/pages/${path}.vue`) : () => import(`./pages/${path}.vue`) : path, loadingComponent: MkLoading, errorComponent: MkError, }); @@ -17,10 +18,10 @@ let indexScrollPos = 0; const defaultRoutes = [ // NOTE: MkTimelineをdynamic importするとAsyncComponentWrapperが間に入るせいでkeep-aliveのコンポーネント指定が効かなくなる { path: '/', name: 'index', component: $i ? MkTimeline : page('welcome') }, - { path: '/@:acct/:page?', name: 'user', component: page('user/index'), props: route => ({ acct: route.params.acct, page: route.params.page || 'index' }) }, + { path: '/@:acct/:page?', name: 'user', component: page(() => import('./pages/user/index.vue')), props: route => ({ acct: route.params.acct, page: route.params.page || 'index' }) }, { path: '/@:user/pages/:page', component: page('page'), props: route => ({ pageName: route.params.page, username: route.params.user }) }, - { path: '/@:user/pages/:pageName/view-source', component: page('page-editor/page-editor'), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) }, - { path: '/settings/:page(.*)?', name: 'settings', component: page('settings/index'), props: route => ({ initialPage: route.params.page || null }) }, + { path: '/@:user/pages/:pageName/view-source', component: page(() => import('./pages/page-editor/page-editor.vue')), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) }, + { path: '/settings/:page(.*)?', name: 'settings', component: page(() => import('./pages/settings/index.vue')), props: route => ({ initialPage: route.params.page || null }) }, { path: '/reset-password/:token?', component: page('reset-password'), props: route => ({ token: route.params.token }) }, { path: '/signup-complete/:code', component: page('signup-complete'), props: route => ({ code: route.params.code }) }, { path: '/announcements', component: page('announcements') }, @@ -35,12 +36,12 @@ const defaultRoutes = [ { path: '/emojis', component: page('emojis') }, { path: '/search', component: page('search'), props: route => ({ query: route.query.q, channel: route.query.channel }) }, { path: '/pages', name: 'pages', component: page('pages') }, - { path: '/pages/new', component: page('page-editor/page-editor') }, - { path: '/pages/edit/:pageId', component: page('page-editor/page-editor'), props: route => ({ initPageId: route.params.pageId }) }, - { path: '/gallery', component: page('gallery/index') }, - { path: '/gallery/new', component: page('gallery/edit') }, - { path: '/gallery/:postId/edit', component: page('gallery/edit'), props: route => ({ postId: route.params.postId }) }, - { path: '/gallery/:postId', component: page('gallery/post'), props: route => ({ postId: route.params.postId }) }, + { path: '/pages/new', component: page(() => import('./pages/page-editor/page-editor.vue')) }, + { path: '/pages/edit/:pageId', component: page(() => import('./pages/page-editor/page-editor.vue')), props: route => ({ initPageId: route.params.pageId }) }, + { path: '/gallery', component: page(() => import('./pages/gallery/index.vue')) }, + { path: '/gallery/new', component: page(() => import('./pages/gallery/edit.vue')) }, + { path: '/gallery/:postId/edit', component: page(() => import('./pages/gallery/edit.vue')), props: route => ({ postId: route.params.postId }) }, + { path: '/gallery/:postId', component: page(() => import('./pages/gallery/edit.vue')), props: route => ({ postId: route.params.postId }) }, { path: '/channels', component: page('channels') }, { path: '/channels/new', component: page('channel-editor') }, { path: '/channels/:channelId/edit', component: page('channel-editor'), props: true }, @@ -52,23 +53,23 @@ const defaultRoutes = [ { path: '/my/favorites', component: page('favorites') }, { path: '/my/messages', component: page('messages') }, { path: '/my/mentions', component: page('mentions') }, - { path: '/my/messaging', name: 'messaging', component: page('messaging/index') }, - { path: '/my/messaging/:user', component: page('messaging/messaging-room'), props: route => ({ userAcct: route.params.user }) }, - { path: '/my/messaging/group/:group', component: page('messaging/messaging-room'), props: route => ({ groupId: route.params.group }) }, + { path: '/my/messaging', name: 'messaging', component: page(() => import('./pages/messaging/index.vue')) }, + { path: '/my/messaging/:user', component: page(() => import('./pages/messaging/messaging-room.vue')), props: route => ({ userAcct: route.params.user }) }, + { path: '/my/messaging/group/:group', component: page(() => import('./pages/messaging/messaging-room.vue')), props: route => ({ groupId: route.params.group }) }, { path: '/my/drive', name: 'drive', component: page('drive') }, { path: '/my/drive/folder/:folder', component: page('drive') }, { path: '/my/follow-requests', component: page('follow-requests') }, - { path: '/my/lists', component: page('my-lists/index') }, - { path: '/my/lists/:list', component: page('my-lists/list') }, - { path: '/my/groups', component: page('my-groups/index') }, - { path: '/my/groups/:group', component: page('my-groups/group'), props: route => ({ groupId: route.params.group }) }, - { path: '/my/antennas', component: page('my-antennas/index') }, - { path: '/my/antennas/create', component: page('my-antennas/create') }, - { path: '/my/antennas/:antennaId', component: page('my-antennas/edit'), props: true }, - { path: '/my/clips', component: page('my-clips/index') }, + { path: '/my/lists', component: page(() => import('./pages/my-lists/index.vue')) }, + { path: '/my/lists/:list', component: page(() => import('./pages/my-lists/list.vue')) }, + { path: '/my/groups', component: page(() => import('./pages/my-groups/index.vue')) }, + { path: '/my/groups/:group', component: page(() => import('./pages/my-groups/group.vue')), props: route => ({ groupId: route.params.group }) }, + { path: '/my/antennas', component: page(() => import('./pages/my-antennas/index.vue')) }, + { path: '/my/antennas/create', component: page(() => import('./pages/my-antennas/create.vue')) }, + { path: '/my/antennas/:antennaId', component: page(() => import('./pages/my-antennas/edit.vue')), props: true }, + { path: '/my/clips', component: page(() => import('./pages/my-clips/index.vue')) }, { path: '/scratchpad', component: page('scratchpad') }, - { path: '/admin/:page(.*)?', component: iAmModerator ? page('admin/index') : page('not-found'), props: route => ({ initialPage: route.params.page || null }) }, - { path: '/admin', component: iAmModerator ? page('admin/index') : page('not-found') }, + { path: '/admin/:page(.*)?', component: iAmModerator ? page(() => import('./pages/admin/index.vue')) : page('not-found'), props: route => ({ initialPage: route.params.page || null }) }, + { path: '/admin', component: iAmModerator ? page(() => import('./pages/admin/index.vue')) : page('not-found') }, { path: '/notes/:note', name: 'note', component: page('note'), props: route => ({ noteId: route.params.note }) }, { path: '/tags/:tag', component: page('tag'), props: route => ({ tag: route.params.tag }) }, { path: '/user-info/:user', component: page('user-info'), props: route => ({ userId: route.params.user }) }, diff --git a/packages/client/src/scripts/2fa.ts b/packages/client/src/scripts/2fa.ts index 00363cffa6..d1b9581e72 100644 --- a/packages/client/src/scripts/2fa.ts +++ b/packages/client/src/scripts/2fa.ts @@ -1,11 +1,11 @@ -export function byteify(data: string, encoding: 'ascii' | 'base64' | 'hex') { +export function byteify(string: string, encoding: 'ascii' | 'base64' | 'hex') { switch (encoding) { case 'ascii': - return Uint8Array.from(data, c => c.charCodeAt(0)); + return Uint8Array.from(string, c => c.charCodeAt(0)); case 'base64': return Uint8Array.from( atob( - data + string .replace(/-/g, '+') .replace(/_/g, '/') ), @@ -13,7 +13,7 @@ export function byteify(data: string, encoding: 'ascii' | 'base64' | 'hex') { ); case 'hex': return new Uint8Array( - data + string .match(/.{1,2}/g) .map(byte => parseInt(byte, 16)) ); diff --git a/packages/client/src/scripts/autocomplete.ts b/packages/client/src/scripts/autocomplete.ts index f4a3a4c0fc..8d9bdee8f5 100644 --- a/packages/client/src/scripts/autocomplete.ts +++ b/packages/client/src/scripts/autocomplete.ts @@ -1,5 +1,5 @@ -import { nextTick, Ref, ref } from 'vue'; -import * as getCaretCoordinates from 'textarea-caret'; +import { nextTick, Ref, ref, defineAsyncComponent } from 'vue'; +import getCaretCoordinates from 'textarea-caret'; import { toASCII } from 'punycode/'; import { popup } from '@/os'; @@ -74,21 +74,21 @@ export class Autocomplete { emojiIndex, mfmTagIndex); - if (max == -1) { + if (max === -1) { this.close(); return; } - const isMention = mentionIndex != -1; - const isHashtag = hashtagIndex != -1; - const isMfmTag = mfmTagIndex != -1; - const isEmoji = emojiIndex != -1 && text.split(/:[a-z0-9_+\-]+:/).pop()!.includes(':'); + const isMention = mentionIndex !== -1; + const isHashtag = hashtagIndex !== -1; + const isMfmTag = mfmTagIndex !== -1; + const isEmoji = emojiIndex !== -1 && text.split(/:[a-z0-9_+\-]+:/).pop()!.includes(':'); let opened = false; if (isMention) { const username = text.substr(mentionIndex + 1); - if (username != '' && username.match(/^[a-zA-Z0-9_]+$/)) { + if (username !== '' && username.match(/^[a-zA-Z0-9_]+$/)) { this.open('user', username); opened = true; } else if (username === '') { @@ -130,7 +130,7 @@ export class Autocomplete { * サジェストを提示します。 */ private async open(type: string, q: string | null) { - if (type != this.currentType) { + if (type !== this.currentType) { this.close(); } if (this.opening) return; @@ -157,7 +157,7 @@ export class Autocomplete { const _y = ref(y); const _q = ref(q); - const { dispose } = await popup(import('@/components/autocomplete.vue'), { + const { dispose } = await popup(defineAsyncComponent(() => import('@/components/autocomplete.vue')), { textarea: this.textarea, close: this.close, type: type, @@ -201,7 +201,7 @@ export class Autocomplete { const caret = this.textarea.selectionStart; - if (type == 'user') { + if (type === 'user') { const source = this.text; const before = source.substr(0, caret); @@ -219,7 +219,7 @@ export class Autocomplete { const pos = trimmedBefore.length + (acct.length + 2); this.textarea.setSelectionRange(pos, pos); }); - } else if (type == 'hashtag') { + } else if (type === 'hashtag') { const source = this.text; const before = source.substr(0, caret); @@ -235,7 +235,7 @@ export class Autocomplete { const pos = trimmedBefore.length + (value.length + 2); this.textarea.setSelectionRange(pos, pos); }); - } else if (type == 'emoji') { + } else if (type === 'emoji') { const source = this.text; const before = source.substr(0, caret); @@ -251,7 +251,7 @@ export class Autocomplete { const pos = trimmedBefore.length + value.length; this.textarea.setSelectionRange(pos, pos); }); - } else if (type == 'mfmTag') { + } else if (type === 'mfmTag') { const source = this.text; const before = source.substr(0, caret); diff --git a/packages/client/src/scripts/contains.ts b/packages/client/src/scripts/contains.ts index 770bda63bb..256e09d293 100644 --- a/packages/client/src/scripts/contains.ts +++ b/packages/client/src/scripts/contains.ts @@ -2,7 +2,7 @@ export default (parent, child, checkSame = true) => { if (checkSame && parent === child) return true; let node = child.parentNode; while (node) { - if (node == parent) return true; + if (node === parent) return true; node = node.parentNode; } return false; diff --git a/packages/client/src/scripts/emojilist.ts b/packages/client/src/scripts/emojilist.ts index bd8689e4f8..4196170d24 100644 --- a/packages/client/src/scripts/emojilist.ts +++ b/packages/client/src/scripts/emojilist.ts @@ -8,4 +8,4 @@ export type UnicodeEmojiDef = { } // initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb -export const emojilist = require('../emojilist.json') as UnicodeEmojiDef[]; +export const emojilist = (await import('../emojilist.json')).default as UnicodeEmojiDef[]; diff --git a/packages/client/src/scripts/extract-avg-color-from-blurhash.ts b/packages/client/src/scripts/extract-avg-color-from-blurhash.ts index 123ab7a06d..af517f2672 100644 --- a/packages/client/src/scripts/extract-avg-color-from-blurhash.ts +++ b/packages/client/src/scripts/extract-avg-color-from-blurhash.ts @@ -1,5 +1,5 @@ export function extractAvgColorFromBlurhash(hash: string) { - return typeof hash == 'string' + return typeof hash === 'string' ? '#' + [...hash.slice(2, 6)] .map(x => '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~'.indexOf(x)) .reduce((a, c) => a * 83 + c, 0) diff --git a/packages/client/src/scripts/format-time-string.ts b/packages/client/src/scripts/format-time-string.ts index bfb2c397ae..fb4718c007 100644 --- a/packages/client/src/scripts/format-time-string.ts +++ b/packages/client/src/scripts/format-time-string.ts @@ -13,7 +13,7 @@ const defaultLocaleStringFormats: {[index: string]: string} = { function formatLocaleString(date: Date, format: string): string { return format.replace(/\{\{(\w+)(:(\w+))?\}\}/g, (match: string, kind: string, unused?, option?: string) => { if (['weekday', 'era', 'year', 'month', 'day', 'hour', 'minute', 'second', 'timeZoneName'].includes(kind)) { - return date.toLocaleString(window.navigator.language, {[kind]: option ? option : defaultLocaleStringFormats[kind]}); + return date.toLocaleString(window.navigator.language, { [kind]: option ? option : defaultLocaleStringFormats[kind] }); } else { return match; } @@ -24,8 +24,8 @@ export function formatDateTimeString(date: Date, format: string): string { return format .replace(/yyyy/g, date.getFullYear().toString()) .replace(/yy/g, date.getFullYear().toString().slice(-2)) - .replace(/MMMM/g, date.toLocaleString(window.navigator.language, { month: 'long'})) - .replace(/MMM/g, date.toLocaleString(window.navigator.language, { month: 'short'})) + .replace(/MMMM/g, date.toLocaleString(window.navigator.language, { month: 'long' })) + .replace(/MMM/g, date.toLocaleString(window.navigator.language, { month: 'short' })) .replace(/MM/g, (`0${date.getMonth() + 1}`).slice(-2)) .replace(/M/g, (date.getMonth() + 1).toString()) .replace(/dd/g, (`0${date.getDate()}`).slice(-2)) diff --git a/packages/client/src/scripts/get-account-from-id.ts b/packages/client/src/scripts/get-account-from-id.ts index ba3adceecc..1da897f176 100644 --- a/packages/client/src/scripts/get-account-from-id.ts +++ b/packages/client/src/scripts/get-account-from-id.ts @@ -3,5 +3,5 @@ import { get } from '@/scripts/idb-proxy'; export async function getAccountFromId(id: string) { const accounts = await get('accounts') as { token: string; id: string; }[]; if (!accounts) console.log('Accounts are not recorded'); - return accounts.find(e => e.id === id); + return accounts.find(account => account.id === id); } diff --git a/packages/client/src/scripts/get-md5.ts b/packages/client/src/scripts/get-md5.ts deleted file mode 100644 index b002d762b1..0000000000 --- a/packages/client/src/scripts/get-md5.ts +++ /dev/null @@ -1,10 +0,0 @@ -// スクリプトサイズがデカい -//import * as crypto from 'crypto'; - -export default (data: ArrayBuffer) => { - //const buf = new Buffer(data); - //const hash = crypto.createHash('md5'); - //hash.update(buf); - //return hash.digest('hex'); - return ''; -}; diff --git a/packages/client/src/scripts/get-note-menu.ts b/packages/client/src/scripts/get-note-menu.ts index b19656d3cc..78749ad6bb 100644 --- a/packages/client/src/scripts/get-note-menu.ts +++ b/packages/client/src/scripts/get-note-menu.ts @@ -1,4 +1,4 @@ -import { Ref } from 'vue'; +import { defineAsyncComponent, Ref } from 'vue'; import * as misskey from 'misskey-js'; import { $i } from '@/account'; import { i18n } from '@/i18n'; @@ -22,7 +22,7 @@ export function getNoteMenu(props: { props.note.poll == null ); - let appearNote = isRenote ? props.note.renote as misskey.entities.Note : props.note; + const appearNote = isRenote ? props.note.renote as misskey.entities.Note : props.note; function del(): void { os.confirm({ @@ -83,8 +83,8 @@ export function getNoteMenu(props: { function togglePin(pin: boolean): void { os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', { noteId: appearNote.id - }, undefined, null, e => { - if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') { + }, undefined, null, res => { + if (res.id === '72dab508-c64d-498f-8740-a8eec1ba385a') { os.alert({ type: 'error', text: i18n.ts.pinLimitExceeded @@ -209,7 +209,7 @@ export function getNoteMenu(props: { text: i18n.ts.clip, action: () => clip() }, - (appearNote.userId != $i.id) ? statePromise.then(state => state.isWatching ? { + (appearNote.userId !== $i.id) ? statePromise.then(state => state.isWatching ? { icon: 'fas fa-eye-slash', text: i18n.ts.unwatch, action: () => toggleWatch(false) @@ -227,7 +227,7 @@ export function getNoteMenu(props: { text: i18n.ts.muteThread, action: () => toggleThreadMute(true) }), - appearNote.userId == $i.id ? ($i.pinnedNoteIds || []).includes(appearNote.id) ? { + appearNote.userId === $i.id ? ($i.pinnedNoteIds || []).includes(appearNote.id) ? { icon: 'fas fa-thumbtack', text: i18n.ts.unpin, action: () => togglePin(false) @@ -246,14 +246,14 @@ export function getNoteMenu(props: { }] : [] ),*/ - ...(appearNote.userId != $i.id ? [ + ...(appearNote.userId !== $i.id ? [ null, { icon: 'fas fa-exclamation-circle', text: i18n.ts.reportAbuse, action: () => { const u = appearNote.url || appearNote.uri || `${url}/notes/${appearNote.id}`; - os.popup(import('@/components/abuse-report-window.vue'), { + os.popup(defineAsyncComponent(() => import('@/components/abuse-report-window.vue')), { user: appearNote.user, initialComment: `Note: ${u}\n-----\n` }, {}, 'closed'); @@ -261,9 +261,9 @@ export function getNoteMenu(props: { }] : [] ), - ...(appearNote.userId == $i.id || $i.isModerator || $i.isAdmin ? [ + ...(appearNote.userId === $i.id || $i.isModerator || $i.isAdmin ? [ null, - appearNote.userId == $i.id ? { + appearNote.userId === $i.id ? { icon: 'fas fa-edit', text: i18n.ts.deleteAndEdit, action: delEdit diff --git a/packages/client/src/scripts/get-note-summary.ts b/packages/client/src/scripts/get-note-summary.ts index 54b8d109d6..d57e1c3029 100644 --- a/packages/client/src/scripts/get-note-summary.ts +++ b/packages/client/src/scripts/get-note-summary.ts @@ -24,7 +24,7 @@ export const getNoteSummary = (note: misskey.entities.Note): string => { } // ファイルが添付されているとき - if ((note.files || []).length != 0) { + if ((note.files || []).length !== 0) { summary += ` (${i18n.t('withNFiles', { n: note.files.length })})`; } diff --git a/packages/client/src/scripts/get-user-menu.ts b/packages/client/src/scripts/get-user-menu.ts index 192d14b83e..091338efd6 100644 --- a/packages/client/src/scripts/get-user-menu.ts +++ b/packages/client/src/scripts/get-user-menu.ts @@ -6,6 +6,7 @@ import * as os from '@/os'; import { userActions } from '@/store'; import { router } from '@/router'; import { $i, iAmModerator } from '@/account'; +import { defineAsyncComponent } from 'vue'; export function getUserMenu(user) { const meId = $i ? $i.id : null; @@ -127,7 +128,7 @@ export function getUserMenu(user) { } function reportAbuse() { - os.popup(import('@/components/abuse-report-window.vue'), { + os.popup(defineAsyncComponent(() => import('@/components/abuse-report-window.vue')), { user: user, }, {}, 'closed'); } @@ -147,7 +148,7 @@ export function getUserMenu(user) { userId: user.id }).then(() => { user.isFollowed = !user.isFollowed; - }) + }); } let menu = [{ @@ -168,7 +169,7 @@ export function getUserMenu(user) { action: () => { os.post({ specified: user }); } - }, meId != user.id ? { + }, meId !== user.id ? { type: 'link', icon: 'fas fa-comments', text: i18n.ts.startMessaging, @@ -177,13 +178,13 @@ export function getUserMenu(user) { icon: 'fas fa-list-ul', text: i18n.ts.addToList, action: pushList - }, meId != user.id ? { + }, meId !== user.id ? { icon: 'fas fa-users', text: i18n.ts.inviteToGroup, action: inviteGroup } : undefined] as any; - if ($i && meId != user.id) { + if ($i && meId !== user.id) { menu = menu.concat([null, { icon: user.isMuted ? 'fas fa-eye' : 'fas fa-eye-slash', text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute, diff --git a/packages/client/src/scripts/get-user-name.ts b/packages/client/src/scripts/get-user-name.ts new file mode 100644 index 0000000000..d499ea0203 --- /dev/null +++ b/packages/client/src/scripts/get-user-name.ts @@ -0,0 +1,3 @@ +export default function(user: { name?: string | null, username: string }): string { + return user.name || user.username; +} diff --git a/packages/client/src/scripts/hotkey.ts b/packages/client/src/scripts/hotkey.ts index 2b3f491fd8..fd9c74f6c8 100644 --- a/packages/client/src/scripts/hotkey.ts +++ b/packages/client/src/scripts/hotkey.ts @@ -53,34 +53,34 @@ const parseKeymap = (keymap: Keymap) => Object.entries(keymap).map(([patterns, c const ignoreElemens = ['input', 'textarea']; -function match(e: KeyboardEvent, patterns: Action['patterns']): boolean { - const key = e.code.toLowerCase(); +function match(ev: KeyboardEvent, patterns: Action['patterns']): boolean { + const key = ev.code.toLowerCase(); return patterns.some(pattern => pattern.which.includes(key) && - pattern.ctrl === e.ctrlKey && - pattern.shift === e.shiftKey && - pattern.alt === e.altKey && - !e.metaKey + pattern.ctrl === ev.ctrlKey && + pattern.shift === ev.shiftKey && + pattern.alt === ev.altKey && + !ev.metaKey ); } export const makeHotkey = (keymap: Keymap) => { const actions = parseKeymap(keymap); - return (e: KeyboardEvent) => { + return (ev: KeyboardEvent) => { if (document.activeElement) { if (ignoreElemens.some(el => document.activeElement!.matches(el))) return; if (document.activeElement.attributes['contenteditable']) return; } for (const action of actions) { - const matched = match(e, action.patterns); + const matched = match(ev, action.patterns); if (matched) { - if (!action.allowRepeat && e.repeat) return; + if (!action.allowRepeat && ev.repeat) return; - e.preventDefault(); - e.stopPropagation(); - action.callback(e); + ev.preventDefault(); + ev.stopPropagation(); + action.callback(ev); break; } } diff --git a/packages/client/src/scripts/hpml/evaluator.ts b/packages/client/src/scripts/hpml/evaluator.ts index 6329c0860e..8106687b61 100644 --- a/packages/client/src/scripts/hpml/evaluator.ts +++ b/packages/client/src/scripts/hpml/evaluator.ts @@ -36,7 +36,7 @@ export class Hpml { if (this.opts.enableAiScript) { this.aiscript = markRaw(new AiScript({ ...createAiScriptEnv({ storageKey: 'pages:' + this.page.id - }), ...initAiLib(this)}, { + }), ...initAiLib(this) }, { in: (q) => { return new Promise(ok => { os.inputText({ @@ -85,7 +85,7 @@ export class Hpml { public eval() { try { this.vars.value = this.evaluateVars(); - } catch (e) { + } catch (err) { //this.onError(e); } } @@ -103,7 +103,7 @@ export class Hpml { public callAiScript(fn: string) { try { if (this.aiscript) this.aiscript.execFn(this.aiscript.scope.get(fn), []); - } catch (e) {} + } catch (err) {} } @autobind @@ -185,7 +185,7 @@ export class Hpml { if (this.aiscript) { try { return utils.valToJs(this.aiscript.scope.get(expr.value)); - } catch (e) { + } catch (err) { return null; } } else { @@ -194,7 +194,7 @@ export class Hpml { } // Define user function - if (expr.type == 'fn') { + if (expr.type === 'fn') { return { slots: expr.value.slots.map(x => x.name), exec: (slotArg: Record<string, any>) => { diff --git a/packages/client/src/scripts/hpml/lib.ts b/packages/client/src/scripts/hpml/lib.ts index 2a1ac73a40..01a44ffcdf 100644 --- a/packages/client/src/scripts/hpml/lib.ts +++ b/packages/client/src/scripts/hpml/lib.ts @@ -1,9 +1,9 @@ -import * as tinycolor from 'tinycolor2'; +import tinycolor from 'tinycolor2'; import { Hpml } from './evaluator'; import { values, utils } from '@syuilo/aiscript'; import { Fn, HpmlScope } from '.'; import { Expr } from './expr'; -import * as seedrandom from 'seedrandom'; +import seedrandom from 'seedrandom'; /* TODO: https://www.chartjs.org/docs/latest/configuration/canvas-background.html#color // https://stackoverflow.com/questions/38493564/chart-area-background-color-chartjs diff --git a/packages/client/src/scripts/idb-proxy.ts b/packages/client/src/scripts/idb-proxy.ts index 5f76ae30bb..d462a0d7ce 100644 --- a/packages/client/src/scripts/idb-proxy.ts +++ b/packages/client/src/scripts/idb-proxy.ts @@ -13,8 +13,8 @@ let idbAvailable = typeof window !== 'undefined' ? !!window.indexedDB : true; if (idbAvailable) { try { await iset('idb-test', 'test'); - } catch (e) { - console.error('idb error', e); + } catch (err) { + console.error('idb error', err); idbAvailable = false; } } diff --git a/packages/client/src/scripts/initialize-sw.ts b/packages/client/src/scripts/initialize-sw.ts index d6dbd5dbd4..7bacfbdf00 100644 --- a/packages/client/src/scripts/initialize-sw.ts +++ b/packages/client/src/scripts/initialize-sw.ts @@ -4,26 +4,26 @@ import { api } from '@/os'; import { lang } from '@/config'; export async function initializeSw() { - if (instance.swPublickey && - ('serviceWorker' in navigator) && - ('PushManager' in window) && - $i && $i.token) { - navigator.serviceWorker.register(`/sw.js`); + if (!('serviceWorker' in navigator)) return; - navigator.serviceWorker.ready.then(registration => { - registration.active?.postMessage({ - msg: 'initialize', - lang, - }); + navigator.serviceWorker.register(`/sw.js`, { scope: '/', type: 'classic' }); + navigator.serviceWorker.ready.then(registration => { + registration.active?.postMessage({ + msg: 'initialize', + lang, + }); + + if (instance.swPublickey && ('PushManager' in window) && $i && $i.token) { // SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(instance.swPublickey) - }).then(subscription => { + }) + .then(subscription => { function encode(buffer: ArrayBuffer | null) { return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer))); } - + // Register api('sw/register', { endpoint: subscription.endpoint, @@ -37,15 +37,15 @@ export async function initializeSw() { if (err.name === 'NotAllowedError') { return; } - + // 違うapplicationServerKey (または gcm_sender_id)のサブスクリプションが // 既に存在していることが原因でエラーになった可能性があるので、 // そのサブスクリプションを解除しておく const subscription = await registration.pushManager.getSubscription(); if (subscription) subscription.unsubscribe(); }); - }); - } + } + }); } /** diff --git a/packages/client/src/scripts/lookup-user.ts b/packages/client/src/scripts/lookup-user.ts index 8de5c84ce8..2d00e51621 100644 --- a/packages/client/src/scripts/lookup-user.ts +++ b/packages/client/src/scripts/lookup-user.ts @@ -25,12 +25,12 @@ export async function lookupUser() { _notFound = true; } }; - usernamePromise.then(show).catch(e => { - if (e.code === 'NO_SUCH_USER') { + usernamePromise.then(show).catch(err => { + if (err.code === 'NO_SUCH_USER') { notFound(); } }); - idPromise.then(show).catch(e => { + idPromise.then(show).catch(err => { notFound(); }); } diff --git a/packages/client/src/scripts/navigate.ts b/packages/client/src/scripts/navigate.ts new file mode 100644 index 0000000000..08b891ec5b --- /dev/null +++ b/packages/client/src/scripts/navigate.ts @@ -0,0 +1,34 @@ +import { inject } from 'vue'; +import { router } from '@/router'; +import { defaultStore } from '@/store'; + +export type Navigate = (path: string, record?: boolean) => void; + +export class MisskeyNavigator { + public readonly navHook: Navigate | null = null; + public readonly sideViewHook: Navigate | null = null; + + // It should be constructed during vue creating in order for inject function to work + constructor() { + this.navHook = inject<Navigate | null>('navHook', null); + this.sideViewHook = inject<Navigate | null>('sideViewHook', null); + } + + // Use this method instead of router.push() + public push(path: string, record = true) { + if (this.navHook) { + this.navHook(path, record); + } else { + if (defaultStore.state.defaultSideView && this.sideViewHook && path !== '/') { + return this.sideViewHook(path, record); + } + + if (router.currentRoute.value.path === path) { + window.scroll({ top: 0, behavior: 'smooth' }); + } else { + if (record) router.push(path); + else router.replace(path); + } + } + } +} diff --git a/packages/client/src/scripts/physics.ts b/packages/client/src/scripts/physics.ts index 36e476b6f9..9e657906c2 100644 --- a/packages/client/src/scripts/physics.ts +++ b/packages/client/src/scripts/physics.ts @@ -41,9 +41,9 @@ export function physics(container: HTMLElement) { const groundThickness = 1024; const ground = Matter.Bodies.rectangle(containerCenterX, containerHeight + (groundThickness / 2), containerWidth, groundThickness, { - isStatic: true, - restitution: 0.1, - friction: 2 + isStatic: true, + restitution: 0.1, + friction: 2 }); //const wallRight = Matter.Bodies.rectangle(window.innerWidth+50, window.innerHeight/2, 100, window.innerHeight, wallopts); diff --git a/packages/client/src/scripts/please-login.ts b/packages/client/src/scripts/please-login.ts index aeaafa124b..e21a6d2ed3 100644 --- a/packages/client/src/scripts/please-login.ts +++ b/packages/client/src/scripts/please-login.ts @@ -1,14 +1,21 @@ +import { defineAsyncComponent } from 'vue'; import { $i } from '@/account'; import { i18n } from '@/i18n'; -import { alert } from '@/os'; +import { popup } from '@/os'; -export function pleaseLogin() { +export function pleaseLogin(path?: string) { if ($i) return; - alert({ - title: i18n.ts.signinRequired, - text: null - }); + popup(defineAsyncComponent(() => import('@/components/signin-dialog.vue')), { + autoSet: true, + message: i18n.ts.signinRequired + }, { + cancelled: () => { + if (path) { + window.location.href = path; + } + }, + }, 'closed'); throw new Error('signin required'); } diff --git a/packages/client/src/scripts/popout.ts b/packages/client/src/scripts/popout.ts index b8286a2a76..580031d0a3 100644 --- a/packages/client/src/scripts/popout.ts +++ b/packages/client/src/scripts/popout.ts @@ -1,8 +1,9 @@ import * as config from '@/config'; +import { appendQuery } from './url'; export function popout(path: string, w?: HTMLElement) { - let url = path.startsWith('http://') || path.startsWith('https://') ? path : config.url + "/" + path; - url += '?zen'; + let url = path.startsWith('http://') || path.startsWith('https://') ? path : config.url + path; + url = appendQuery(url, 'zen'); if (w) { const position = w.getBoundingClientRect(); const width = parseInt(getComputedStyle(w, '').width, 10); diff --git a/packages/client/src/scripts/reaction-picker.ts b/packages/client/src/scripts/reaction-picker.ts index 3ac1f63430..b7699cae4a 100644 --- a/packages/client/src/scripts/reaction-picker.ts +++ b/packages/client/src/scripts/reaction-picker.ts @@ -1,4 +1,4 @@ -import { Ref, ref } from 'vue'; +import { defineAsyncComponent, Ref, ref } from 'vue'; import { popup } from '@/os'; class ReactionPicker { @@ -12,7 +12,7 @@ class ReactionPicker { } public async init() { - await popup(import('@/components/emoji-picker-dialog.vue'), { + await popup(defineAsyncComponent(() => import('@/components/emoji-picker-dialog.vue')), { src: this.src, asReactionPicker: true, manualShowing: this.manualShowing diff --git a/packages/client/src/scripts/select-file.ts b/packages/client/src/scripts/select-file.ts index 23df4edf54..461d613b42 100644 --- a/packages/client/src/scripts/select-file.ts +++ b/packages/client/src/scripts/select-file.ts @@ -4,6 +4,7 @@ import { stream } from '@/stream'; import { i18n } from '@/i18n'; import { defaultStore } from '@/store'; import { DriveFile } from 'misskey-js/built/entities'; +import { uploadFile } from '@/scripts/upload'; function select(src: any, label: string | null, multiple: boolean): Promise<DriveFile | DriveFile[]> { return new Promise((res, rej) => { @@ -14,14 +15,14 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv input.type = 'file'; input.multiple = multiple; input.onchange = () => { - const promises = Array.from(input.files).map(file => os.upload(file, defaultStore.state.uploadFolder, undefined, keepOriginal.value)); + const promises = Array.from(input.files).map(file => uploadFile(file, defaultStore.state.uploadFolder, undefined, keepOriginal.value)); Promise.all(promises).then(driveFiles => { res(multiple ? driveFiles : driveFiles[0]); - }).catch(e => { + }).catch(err => { os.alert({ type: 'error', - text: e + text: err }); }); @@ -53,9 +54,9 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv const marker = Math.random().toString(); // TODO: UUIDとか使う const connection = stream.useChannel('main'); - connection.on('urlUploadFinished', data => { - if (data.marker === marker) { - res(multiple ? [data.file] : data.file); + connection.on('urlUploadFinished', urlResponse => { + if (urlResponse.marker === marker) { + res(multiple ? [urlResponse.file] : urlResponse.file); connection.dispose(); } }); diff --git a/packages/client/src/scripts/theme-editor.ts b/packages/client/src/scripts/theme-editor.ts index 3d69d2836a..2c917e280d 100644 --- a/packages/client/src/scripts/theme-editor.ts +++ b/packages/client/src/scripts/theme-editor.ts @@ -1,4 +1,4 @@ -import { v4 as uuid} from 'uuid'; +import { v4 as uuid } from 'uuid'; import { themeProps, Theme } from './theme'; diff --git a/packages/client/src/scripts/theme.ts b/packages/client/src/scripts/theme.ts index 2cb78fae5c..dec9fb355c 100644 --- a/packages/client/src/scripts/theme.ts +++ b/packages/client/src/scripts/theme.ts @@ -1,5 +1,6 @@ +import { ref } from 'vue'; import { globalEvents } from '@/events'; -import * as tinycolor from 'tinycolor2'; +import tinycolor from 'tinycolor2'; export type Theme = { id: string; @@ -10,30 +11,38 @@ export type Theme = { props: Record<string, string>; }; -export const lightTheme: Theme = require('@/themes/_light.json5'); -export const darkTheme: Theme = require('@/themes/_dark.json5'); +import lightTheme from '@/themes/_light.json5'; +import darkTheme from '@/themes/_dark.json5'; export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X')); -export const builtinThemes = [ - require('@/themes/l-light.json5'), - require('@/themes/l-coffee.json5'), - require('@/themes/l-apricot.json5'), - require('@/themes/l-rainy.json5'), - require('@/themes/l-vivid.json5'), - require('@/themes/l-cherry.json5'), - require('@/themes/l-sushi.json5'), +export const getBuiltinThemes = () => Promise.all( + [ + 'l-light', + 'l-coffee', + 'l-apricot', + 'l-rainy', + 'l-vivid', + 'l-cherry', + 'l-sushi', - require('@/themes/d-dark.json5'), - require('@/themes/d-persimmon.json5'), - require('@/themes/d-astro.json5'), - require('@/themes/d-future.json5'), - require('@/themes/d-botanical.json5'), - require('@/themes/d-cherry.json5'), - require('@/themes/d-ice.json5'), - require('@/themes/d-pumpkin.json5'), - require('@/themes/d-black.json5'), -] as Theme[]; + 'd-dark', + 'd-persimmon', + 'd-astro', + 'd-future', + 'd-botanical', + 'd-cherry', + 'd-ice', + 'd-pumpkin', + 'd-black', + ].map(name => import(`../themes/${name}.json5`).then(({ default: _default }): Theme => _default)) +); + +export const getBuiltinThemesRef = () => { + const builtinThemes = ref<Theme[]>([]); + getBuiltinThemes().then(themes => builtinThemes.value = themes); + return builtinThemes; +}; let timeout = null; diff --git a/packages/client/src/scripts/upload.ts b/packages/client/src/scripts/upload.ts new file mode 100644 index 0000000000..2f7b30b58d --- /dev/null +++ b/packages/client/src/scripts/upload.ts @@ -0,0 +1,114 @@ +import { reactive, ref } from 'vue'; +import { defaultStore } from '@/store'; +import { apiUrl } from '@/config'; +import * as Misskey from 'misskey-js'; +import { $i } from '@/account'; +import { readAndCompressImage } from 'browser-image-resizer'; +import { alert } from '@/os'; + +type Uploading = { + id: string; + name: string; + progressMax: number | undefined; + progressValue: number | undefined; + img: string; +}; +export const uploads = ref<Uploading[]>([]); + +const compressTypeMap = { + 'image/jpeg': { quality: 0.85, mimeType: 'image/jpeg' }, + 'image/webp': { quality: 0.85, mimeType: 'image/jpeg' }, + 'image/svg+xml': { quality: 1, mimeType: 'image/png' }, +} as const; + +const mimeTypeMap = { + 'image/webp': 'webp', + 'image/jpeg': 'jpg', + 'image/png': 'png', +} as const; + +export function uploadFile( + file: File, + folder?: any, + name?: string, + keepOriginal: boolean = defaultStore.state.keepOriginalUploading +): Promise<Misskey.entities.DriveFile> { + if (folder && typeof folder === 'object') folder = folder.id; + + return new Promise((resolve, reject) => { + const id = Math.random().toString(); + + const reader = new FileReader(); + reader.onload = async (ev) => { + const ctx = reactive<Uploading>({ + id: id, + name: name || file.name || 'untitled', + progressMax: undefined, + progressValue: undefined, + img: window.URL.createObjectURL(file) + }); + + uploads.value.push(ctx); + + let resizedImage: any; + if (!keepOriginal && file.type in compressTypeMap) { + const imgConfig = compressTypeMap[file.type]; + + const config = { + maxWidth: 2048, + maxHeight: 2048, + debug: true, + ...imgConfig, + }; + + try { + resizedImage = await readAndCompressImage(file, config); + ctx.name = file.type !== imgConfig.mimeType ? `${ctx.name}.${mimeTypeMap[compressTypeMap[file.type].mimeType]}` : ctx.name; + } catch (err) { + console.error('Failed to resize image', err); + } + } + + const formData = new FormData(); + formData.append('i', $i.token); + formData.append('force', 'true'); + formData.append('file', resizedImage || file); + formData.append('name', ctx.name); + if (folder) formData.append('folderId', folder); + + const xhr = new XMLHttpRequest(); + xhr.open('POST', apiUrl + '/drive/files/create', true); + xhr.onload = (ev) => { + if (xhr.status !== 200 || ev.target == null || ev.target.response == null) { + // TODO: 消すのではなくて再送できるようにしたい + uploads.value = uploads.value.filter(x => x.id !== id); + + alert({ + type: 'error', + title: 'Failed to upload', + text: `${JSON.stringify(ev.target?.response)}, ${JSON.stringify(xhr.response)}` + }); + + reject(); + return; + } + + const driveFile = JSON.parse(ev.target.response); + + resolve(driveFile); + + uploads.value = uploads.value.filter(x => x.id !== id); + }; + + xhr.upload.onprogress = ev => { + if (ev.lengthComputable) { + ctx.progressMax = ev.total; + ctx.progressValue = ev.loaded; + } + }; + + xhr.send(formData); + }; + reader.readAsArrayBuffer(file); + }); +} diff --git a/packages/client/src/scripts/url.ts b/packages/client/src/scripts/url.ts index c7f2b7c1e7..542b00e0f0 100644 --- a/packages/client/src/scripts/url.ts +++ b/packages/client/src/scripts/url.ts @@ -4,7 +4,7 @@ export function query(obj: {}): string { .reduce((a, [k, v]) => (a[k] = v, a), {} as Record<string, any>); return Object.entries(params) - .map((e) => `${e[0]}=${encodeURIComponent(e[1])}`) + .map((p) => `${p[0]}=${encodeURIComponent(p[1])}`) .join('&'); } diff --git a/packages/client/src/scripts/use-note-capture.ts b/packages/client/src/scripts/use-note-capture.ts index b2a96062c7..f1f976693e 100644 --- a/packages/client/src/scripts/use-note-capture.ts +++ b/packages/client/src/scripts/use-note-capture.ts @@ -11,8 +11,8 @@ export function useNoteCapture(props: { const note = props.note; const connection = $i ? stream : null; - function onStreamNoteUpdated(data): void { - const { type, id, body } = data; + function onStreamNoteUpdated(noteData): void { + const { type, id, body } = noteData; if (id !== note.value.id) return; diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts index b9800ec607..deee23951e 100644 --- a/packages/client/src/store.ts +++ b/packages/client/src/store.ts @@ -255,10 +255,13 @@ type Plugin = { /** * 常にメモリにロードしておく必要がないような設定情報を保管するストレージ(非リアクティブ) */ +import lightTheme from '@/themes/l-light.json5'; +import darkTheme from '@/themes/d-dark.json5'; + export class ColdDeviceStorage { public static default = { - lightTheme: require('@/themes/l-light.json5') as Theme, - darkTheme: require('@/themes/d-dark.json5') as Theme, + lightTheme, + darkTheme, syncDeviceDarkMode: true, plugins: [] as Plugin[], mediaVolume: 0.5, diff --git a/packages/client/src/sw/compose-notification.ts b/packages/client/src/sw/compose-notification.ts deleted file mode 100644 index e271d30949..0000000000 --- a/packages/client/src/sw/compose-notification.ts +++ /dev/null @@ -1,107 +0,0 @@ -/** - * Notification composer of Service Worker - */ -declare var self: ServiceWorkerGlobalScope; - -import * as misskey from 'misskey-js'; - -function getUserName(user: misskey.entities.User): string { - return user.name || user.username; -} - -export default async function(type, data, i18n): Promise<[string, NotificationOptions] | null | undefined> { - if (!i18n) { - console.log('no i18n'); - return; - } - - switch (type) { - case 'driveFileCreated': // TODO (Server Side) - return [i18n.t('_notification.fileUploaded'), { - body: data.name, - icon: data.url - }]; - case 'notification': - switch (data.type) { - case 'mention': - return [i18n.t('_notification.youGotMention', { name: getUserName(data.user) }), { - body: data.note.text, - icon: data.user.avatarUrl - }]; - - case 'reply': - return [i18n.t('_notification.youGotReply', { name: getUserName(data.user) }), { - body: data.note.text, - icon: data.user.avatarUrl - }]; - - case 'renote': - return [i18n.t('_notification.youRenoted', { name: getUserName(data.user) }), { - body: data.note.text, - icon: data.user.avatarUrl - }]; - - case 'quote': - return [i18n.t('_notification.youGotQuote', { name: getUserName(data.user) }), { - body: data.note.text, - icon: data.user.avatarUrl - }]; - - case 'reaction': - return [`${data.reaction} ${getUserName(data.user)}`, { - body: data.note.text, - icon: data.user.avatarUrl - }]; - - case 'pollVote': - return [i18n.t('_notification.youGotPoll', { name: getUserName(data.user) }), { - body: data.note.text, - icon: data.user.avatarUrl - }]; - - case 'pollEnded': - return [i18n.t('_notification.pollEnded'), { - body: data.note.text, - }]; - - case 'follow': - return [i18n.t('_notification.youWereFollowed'), { - body: getUserName(data.user), - icon: data.user.avatarUrl - }]; - - case 'receiveFollowRequest': - return [i18n.t('_notification.youReceivedFollowRequest'), { - body: getUserName(data.user), - icon: data.user.avatarUrl - }]; - - case 'followRequestAccepted': - return [i18n.t('_notification.yourFollowRequestAccepted'), { - body: getUserName(data.user), - icon: data.user.avatarUrl - }]; - - case 'groupInvited': - return [i18n.t('_notification.youWereInvitedToGroup'), { - body: data.group.name - }]; - - default: - return null; - } - case 'unreadMessagingMessage': - if (data.groupId === null) { - return [i18n.t('_notification.youGotMessagingMessageFromUser', { name: getUserName(data.user) }), { - icon: data.user.avatarUrl, - tag: `messaging:user:${data.user.id}` - }]; - } - return [i18n.t('_notification.youGotMessagingMessageFromGroup', { name: data.group.name }), { - icon: data.user.avatarUrl, - tag: `messaging:group:${data.group.id}` - }]; - default: - return null; - } -} diff --git a/packages/client/src/sw/sw.ts b/packages/client/src/sw/sw.ts deleted file mode 100644 index 68c650c771..0000000000 --- a/packages/client/src/sw/sw.ts +++ /dev/null @@ -1,123 +0,0 @@ -/** - * Service Worker - */ -declare var self: ServiceWorkerGlobalScope; - -import { get, set } from 'idb-keyval'; -import composeNotification from '@/sw/compose-notification'; -import { I18n } from '@/scripts/i18n'; - -//#region Variables -const version = _VERSION_; -const cacheName = `mk-cache-${version}`; - -let lang: string; -let i18n: I18n<any>; -let pushesPool: any[] = []; -//#endregion - -//#region Startup -get('lang').then(async prelang => { - if (!prelang) return; - lang = prelang; - return fetchLocale(); -}); -//#endregion - -//#region Lifecycle: Install -self.addEventListener('install', ev => { - self.skipWaiting(); -}); -//#endregion - -//#region Lifecycle: Activate -self.addEventListener('activate', ev => { - ev.waitUntil( - caches.keys() - .then(cacheNames => Promise.all( - cacheNames - .filter((v) => v !== cacheName) - .map(name => caches.delete(name)) - )) - .then(() => self.clients.claim()) - ); -}); -//#endregion - -//#region When: Fetching -self.addEventListener('fetch', ev => { - // Nothing to do -}); -//#endregion - -//#region When: Caught Notification -self.addEventListener('push', ev => { - // クライアント取得 - ev.waitUntil(self.clients.matchAll({ - includeUncontrolled: true - }).then(async clients => { - // クライアントがあったらストリームに接続しているということなので通知しない - if (clients.length != 0) return; - - const { type, body } = ev.data?.json(); - - // localeを読み込めておらずi18nがundefinedだった場合はpushesPoolにためておく - if (!i18n) return pushesPool.push({ type, body }); - - const n = await composeNotification(type, body, i18n); - if (n) return self.registration.showNotification(...n); - })); -}); -//#endregion - -//#region When: Caught a message from the client -self.addEventListener('message', ev => { - switch(ev.data) { - case 'clear': - return; // TODO - default: - break; - } - - if (typeof ev.data === 'object') { - // E.g. '[object Array]' → 'array' - const otype = Object.prototype.toString.call(ev.data).slice(8, -1).toLowerCase(); - - if (otype === 'object') { - if (ev.data.msg === 'initialize') { - lang = ev.data.lang; - set('lang', lang); - fetchLocale(); - } - } - } -}); -//#endregion - -//#region Function: (Re)Load i18n instance -async function fetchLocale() { - //#region localeファイルの読み込み - // Service Workerは何度も起動しそのたびにlocaleを読み込むので、CacheStorageを使う - const localeUrl = `/assets/locales/${lang}.${version}.json`; - let localeRes = await caches.match(localeUrl); - - if (!localeRes) { - localeRes = await fetch(localeUrl); - const clone = localeRes?.clone(); - if (!clone?.clone().ok) return; - - caches.open(cacheName).then(cache => cache.put(localeUrl, clone)); - } - - i18n = new I18n(await localeRes.json()); - //#endregion - - //#region i18nをきちんと読み込んだ後にやりたい処理 - for (const { type, body } of pushesPool) { - const n = await composeNotification(type, body, i18n); - if (n) self.registration.showNotification(...n); - } - pushesPool = []; - //#endregion -} -//#endregion diff --git a/packages/client/src/theme-store.ts b/packages/client/src/theme-store.ts index e7962e7e8e..fdc92ed793 100644 --- a/packages/client/src/theme-store.ts +++ b/packages/client/src/theme-store.ts @@ -14,9 +14,9 @@ export async function fetchThemes(): Promise<void> { try { const themes = await api('i/registry/get', { scope: ['client'], key: 'themes' }); localStorage.setItem(lsCacheKey, JSON.stringify(themes)); - } catch (e) { - if (e.code === 'NO_SUCH_KEY') return; - throw e; + } catch (err) { + if (err.code === 'NO_SUCH_KEY') return; + throw err; } } @@ -28,7 +28,7 @@ export async function addTheme(theme: Theme): Promise<void> { } export async function removeTheme(theme: Theme): Promise<void> { - const themes = getThemes().filter(t => t.id != theme.id); + const themes = getThemes().filter(t => t.id !== theme.id); await api('i/registry/set', { scope: ['client'], key: 'themes', value: themes }); localStorage.setItem(lsCacheKey, JSON.stringify(themes)); } diff --git a/packages/client/src/ui/_common_/common.vue b/packages/client/src/ui/_common_/common.vue index 05688d7c53..9f7388db53 100644 --- a/packages/client/src/ui/_common_/common.vue +++ b/packages/client/src/ui/_common_/common.vue @@ -17,9 +17,11 @@ <script lang="ts"> import { defineAsyncComponent, defineComponent } from 'vue'; -import { popup, popups, uploads, pendingApiRequestsCount } from '@/os'; +import { popup, popups, pendingApiRequestsCount } from '@/os'; +import { uploads } from '@/scripts/upload'; import * as sound from '@/scripts/sound'; import { $i } from '@/account'; +import { swInject } from './sw-inject'; import { stream } from '@/stream'; export default defineComponent({ @@ -37,7 +39,7 @@ export default defineComponent({ id: notification.id }); - popup(import('@/components/notification-toast.vue'), { + popup(defineAsyncComponent(() => import('@/components/notification-toast.vue')), { notification }, {}, 'closed'); } @@ -48,6 +50,11 @@ export default defineComponent({ if ($i) { const connection = stream.useChannel('main', null, 'UI'); connection.on('notification', onNotification); + + //#region Listen message from SW + if ('serviceWorker' in navigator) { + swInject(); + } } return { diff --git a/packages/client/src/ui/_common_/sidebar-for-mobile.vue b/packages/client/src/ui/_common_/sidebar-for-mobile.vue index afcc50725b..41d0837233 100644 --- a/packages/client/src/ui/_common_/sidebar-for-mobile.vue +++ b/packages/client/src/ui/_common_/sidebar-for-mobile.vue @@ -33,7 +33,7 @@ </template> <script lang="ts"> -import { computed, defineComponent, ref, toRef, watch } from 'vue'; +import { computed, defineAsyncComponent, defineComponent, ref, toRef, watch } from 'vue'; import { host } from '@/config'; import { search } from '@/scripts/search'; import * as os from '@/os'; @@ -61,13 +61,13 @@ export default defineComponent({ otherMenuItemIndicated, post: os.post, search, - openAccountMenu:(ev) => { + openAccountMenu: (ev) => { openAccountMenu({ withExtraOperation: true, }, ev); }, more: () => { - os.popup(import('@/components/launch-pad.vue'), {}, { + os.popup(defineAsyncComponent(() => import('@/components/launch-pad.vue')), {}, { }, 'closed'); }, }; diff --git a/packages/client/src/ui/_common_/sidebar.vue b/packages/client/src/ui/_common_/sidebar.vue index a23b7d4152..d65e776d86 100644 --- a/packages/client/src/ui/_common_/sidebar.vue +++ b/packages/client/src/ui/_common_/sidebar.vue @@ -33,7 +33,7 @@ </template> <script lang="ts" setup> -import { computed, ref, watch } from 'vue'; +import { computed, defineAsyncComponent, ref, watch } from 'vue'; import * as os from '@/os'; import { menuDef } from '@/menu'; import { $i, openAccountMenu as openAccountMenu_ } from '@/account'; @@ -69,7 +69,7 @@ function openAccountMenu(ev: MouseEvent) { } function more(ev: MouseEvent) { - os.popup(import('@/components/launch-pad.vue'), { + os.popup(defineAsyncComponent(() => import('@/components/launch-pad.vue')), { src: ev.currentTarget ?? ev.target, }, { }, 'closed'); diff --git a/packages/client/src/ui/_common_/sw-inject.ts b/packages/client/src/ui/_common_/sw-inject.ts new file mode 100644 index 0000000000..371f80ca15 --- /dev/null +++ b/packages/client/src/ui/_common_/sw-inject.ts @@ -0,0 +1,44 @@ +import { inject } from 'vue'; +import { post } from '@/os'; +import { $i, login } from '@/account'; +import { defaultStore } from '@/store'; +import { getAccountFromId } from '@/scripts/get-account-from-id'; +import { router } from '@/router'; + +export function swInject() { + const navHook = inject('navHook', null); + const sideViewHook = inject('sideViewHook', null); + + navigator.serviceWorker.addEventListener('message', ev => { + if (_DEV_) { + console.log('sw msg', ev.data); + } + + if (ev.data.type !== 'order') return; + + if (ev.data.loginId !== $i?.id) { + return getAccountFromId(ev.data.loginId).then(account => { + if (!account) return; + return login(account.token, ev.data.url); + }); + } + + switch (ev.data.order) { + case 'post': + return post(ev.data.options); + case 'push': + if (router.currentRoute.value.path === ev.data.url) { + return window.scroll({ top: 0, behavior: 'smooth' }); + } + if (navHook) { + return navHook(ev.data.url); + } + if (sideViewHook && defaultStore.state.defaultSideView && ev.data.url !== '/') { + return sideViewHook(ev.data.url); + } + return router.push(ev.data.url); + default: + return; + } + }); +} diff --git a/packages/client/src/ui/_common_/upload.vue b/packages/client/src/ui/_common_/upload.vue index ab7678a505..f3703d0e8f 100644 --- a/packages/client/src/ui/_common_/upload.vue +++ b/packages/client/src/ui/_common_/upload.vue @@ -20,8 +20,8 @@ <script lang="ts" setup> import { } from 'vue'; import * as os from '@/os'; +import { uploads } from '@/scripts/upload'; -const uploads = os.uploads; const zIndex = os.claimZIndex('high'); </script> diff --git a/packages/client/src/ui/classic.header.vue b/packages/client/src/ui/classic.header.vue index c7fddbc491..57008aeaed 100644 --- a/packages/client/src/ui/classic.header.vue +++ b/packages/client/src/ui/classic.header.vue @@ -39,7 +39,7 @@ </template> <script lang="ts"> -import { defineComponent } from 'vue'; +import { defineAsyncComponent, defineComponent } from 'vue'; import { host } from '@/config'; import { search } from '@/scripts/search'; import * as os from '@/os'; @@ -101,7 +101,7 @@ export default defineComponent({ }, more(ev) { - os.popup(import('@/components/launch-pad.vue'), { + os.popup(defineAsyncComponent(() => import('@/components/launch-pad.vue')), { src: ev.currentTarget ?? ev.target, anchor: { x: 'center', y: 'bottom' }, }, { diff --git a/packages/client/src/ui/classic.sidebar.vue b/packages/client/src/ui/classic.sidebar.vue index 3364ee39be..6c0ce023e4 100644 --- a/packages/client/src/ui/classic.sidebar.vue +++ b/packages/client/src/ui/classic.sidebar.vue @@ -41,7 +41,7 @@ </template> <script lang="ts"> -import { defineComponent } from 'vue'; +import { defineAsyncComponent, defineComponent } from 'vue'; import { host } from '@/config'; import { search } from '@/scripts/search'; import * as os from '@/os'; @@ -121,12 +121,12 @@ export default defineComponent({ }, more(ev) { - os.popup(import('@/components/launch-pad.vue'), { + os.popup(defineAsyncComponent(() => import('@/components/launch-pad.vue')), { src: ev.currentTarget ?? ev.target, }, {}, 'closed'); }, - openAccountMenu:(ev) => { + openAccountMenu: (ev) => { openAccountMenu({ withExtraOperation: true, }, ev); diff --git a/packages/client/src/ui/classic.widgets.vue b/packages/client/src/ui/classic.widgets.vue index f42f27e926..6f9d18bde5 100644 --- a/packages/client/src/ui/classic.widgets.vue +++ b/packages/client/src/ui/classic.widgets.vue @@ -44,13 +44,13 @@ export default defineComponent({ }, removeWidget(widget) { - this.$store.set('widgets', this.$store.state.widgets.filter(w => w.id != widget.id)); + this.$store.set('widgets', this.$store.state.widgets.filter(w => w.id !== widget.id)); }, updateWidget({ id, data }) { this.$store.set('widgets', this.$store.state.widgets.map(w => w.id === id ? { ...w, - data: data + data, } : w)); }, diff --git a/packages/client/src/ui/deck.vue b/packages/client/src/ui/deck.vue index 1e0d9a1652..e538a93f06 100644 --- a/packages/client/src/ui/deck.vue +++ b/packages/client/src/ui/deck.vue @@ -17,7 +17,7 @@ :key="ids[0]" class="column" :column="columns.find(c => c.id === ids[0])" - :is-stacked="false" + :is-stacked="false" :style="columns.find(c => c.id === ids[0])!.flexible ? { flex: 1, minWidth: '350px' } : { width: columns.find(c => c.id === ids[0])!.width + 'px' }" @parent-focus="moveFocus(ids[0], $event)" /> diff --git a/packages/client/src/ui/deck/antenna-column.vue b/packages/client/src/ui/deck/antenna-column.vue index e0f56c2800..f12f5c6b25 100644 --- a/packages/client/src/ui/deck/antenna-column.vue +++ b/packages/client/src/ui/deck/antenna-column.vue @@ -22,8 +22,8 @@ const props = defineProps<{ }>(); const emit = defineEmits<{ - (e: 'loaded'): void; - (e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void; + (ev: 'loaded'): void; + (ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void; }>(); let timeline = $ref<InstanceType<typeof XTimeline>>(); diff --git a/packages/client/src/ui/deck/column-core.vue b/packages/client/src/ui/deck/column-core.vue index 485e89a062..2667b6d745 100644 --- a/packages/client/src/ui/deck/column-core.vue +++ b/packages/client/src/ui/deck/column-core.vue @@ -29,7 +29,7 @@ defineProps<{ }>(); const emit = defineEmits<{ - (e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void; + (ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void; }>(); /* diff --git a/packages/client/src/ui/deck/column.vue b/packages/client/src/ui/deck/column.vue index 5f8da8cf8f..6db3549fbb 100644 --- a/packages/client/src/ui/deck/column.vue +++ b/packages/client/src/ui/deck/column.vue @@ -61,8 +61,8 @@ const props = withDefaults(defineProps<{ }); const emit = defineEmits<{ - (e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void; - (e: 'change-active-state', v: boolean): void; + (ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void; + (ev: 'change-active-state', v: boolean): void; }>(); let body = $ref<HTMLDivElement>(); @@ -94,7 +94,6 @@ onBeforeUnmount(() => { os.deckGlobalEvents.off('column.dragEnd', onOtherDragEnd); }); - function onOtherDragStart() { dropready = true; } @@ -193,9 +192,9 @@ function goTop() { }); } -function onDragstart(e) { - e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.setData(_DATA_TRANSFER_DECK_COLUMN_, props.column.id); +function onDragstart(ev) { + ev.dataTransfer.effectAllowed = 'move'; + ev.dataTransfer.setData(_DATA_TRANSFER_DECK_COLUMN_, props.column.id); // Chromeのバグで、Dragstartハンドラ内ですぐにDOMを変更する(=リアクティブなプロパティを変更する)とDragが終了してしまう // SEE: https://stackoverflow.com/questions/19639969/html5-dragend-event-firing-immediately @@ -204,35 +203,34 @@ function onDragstart(e) { }, 10); } -function onDragend(e) { +function onDragend(ev) { dragging = false; } -function onDragover(e) { +function onDragover(ev) { // 自分自身がドラッグされている場合 if (dragging) { // 自分自身にはドロップさせない - e.dataTransfer.dropEffect = 'none'; - return; - } + ev.dataTransfer.dropEffect = 'none'; + } else { + const isDeckColumn = ev.dataTransfer.types[0] === _DATA_TRANSFER_DECK_COLUMN_; - const isDeckColumn = e.dataTransfer.types[0] == _DATA_TRANSFER_DECK_COLUMN_; + ev.dataTransfer.dropEffect = isDeckColumn ? 'move' : 'none'; - e.dataTransfer.dropEffect = isDeckColumn ? 'move' : 'none'; - - if (!dragging && isDeckColumn) draghover = true; + if (isDeckColumn) draghover = true; + } } function onDragleave() { draghover = false; } -function onDrop(e) { +function onDrop(ev) { draghover = false; os.deckGlobalEvents.emit('column.dragEnd'); - const id = e.dataTransfer.getData(_DATA_TRANSFER_DECK_COLUMN_); - if (id != null && id != '') { + const id = ev.dataTransfer.getData(_DATA_TRANSFER_DECK_COLUMN_); + if (id != null && id !== '') { swapColumn(props.column.id, id); } } diff --git a/packages/client/src/ui/deck/deck-store.ts b/packages/client/src/ui/deck/deck-store.ts index f7c39ad8fd..c847bf2b43 100644 --- a/packages/client/src/ui/deck/deck-store.ts +++ b/packages/client/src/ui/deck/deck-store.ts @@ -72,8 +72,8 @@ export const loadDeck = async () => { scope: ['client', 'deck', 'profiles'], key: deckStore.state.profile, }); - } catch (e) { - if (e.code === 'NO_SUCH_KEY') { + } catch (err) { + if (err.code === 'NO_SUCH_KEY') { // 後方互換性のため if (deckStore.state.profile === 'default') { saveDeck(); @@ -94,7 +94,7 @@ export const loadDeck = async () => { deckStore.set('layout', [['a'], ['b']]); return; } - throw e; + throw err; } deckStore.set('columns', deck.columns); @@ -114,7 +114,7 @@ export const saveDeck = throttle(1000, () => { }); export function addColumn(column: Column) { - if (column.name == undefined) column.name = null; + if (column.name === undefined) column.name = null; deckStore.push('columns', column); deckStore.push('layout', [column.id]); saveDeck(); @@ -129,10 +129,10 @@ export function removeColumn(id: Column['id']) { } export function swapColumn(a: Column['id'], b: Column['id']) { - const aX = deckStore.state.layout.findIndex(ids => ids.indexOf(a) != -1); - const aY = deckStore.state.layout[aX].findIndex(id => id == a); - const bX = deckStore.state.layout.findIndex(ids => ids.indexOf(b) != -1); - const bY = deckStore.state.layout[bX].findIndex(id => id == b); + const aX = deckStore.state.layout.findIndex(ids => ids.indexOf(a) !== -1); + const aY = deckStore.state.layout[aX].findIndex(id => id === a); + const bX = deckStore.state.layout.findIndex(ids => ids.indexOf(b) !== -1); + const bY = deckStore.state.layout[bX].findIndex(id => id === b); const layout = copy(deckStore.state.layout); layout[aX][aY] = b; layout[bX][bY] = a; @@ -259,7 +259,7 @@ export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) { const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); const column = copy(deckStore.state.columns[columnIndex]); if (column == null) return; - column.widgets = column.widgets.filter(w => w.id != widget.id); + column.widgets = column.widgets.filter(w => w.id !== widget.id); columns[columnIndex] = column; deckStore.set('columns', columns); saveDeck(); @@ -276,14 +276,14 @@ export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) { saveDeck(); } -export function updateColumnWidget(id: Column['id'], widgetId: string, data: any) { +export function updateColumnWidget(id: Column['id'], widgetId: string, WidgetData: any) { const columns = copy(deckStore.state.columns); const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); const column = copy(deckStore.state.columns[columnIndex]); if (column == null) return; column.widgets = column.widgets.map(w => w.id === widgetId ? { ...w, - data: data + data: widgetData, } : w); columns[columnIndex] = column; deckStore.set('columns', columns); diff --git a/packages/client/src/ui/deck/direct-column.vue b/packages/client/src/ui/deck/direct-column.vue index ebaba574f4..4837c0ce38 100644 --- a/packages/client/src/ui/deck/direct-column.vue +++ b/packages/client/src/ui/deck/direct-column.vue @@ -18,7 +18,7 @@ defineProps<{ }>(); const emit = defineEmits<{ - (e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void; + (ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void; }>(); const pagination = { diff --git a/packages/client/src/ui/deck/list-column.vue b/packages/client/src/ui/deck/list-column.vue index b990516d05..843a3bd1cb 100644 --- a/packages/client/src/ui/deck/list-column.vue +++ b/packages/client/src/ui/deck/list-column.vue @@ -22,8 +22,8 @@ const props = defineProps<{ }>(); const emit = defineEmits<{ - (e: 'loaded'): void; - (e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void; + (ev: 'loaded'): void; + (ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void; }>(); let timeline = $ref<InstanceType<typeof XTimeline>>(); diff --git a/packages/client/src/ui/deck/main-column.vue b/packages/client/src/ui/deck/main-column.vue index 57caab44cb..3c97cd4867 100644 --- a/packages/client/src/ui/deck/main-column.vue +++ b/packages/client/src/ui/deck/main-column.vue @@ -35,7 +35,7 @@ defineProps<{ }>(); const emit = defineEmits<{ - (e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void; + (ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void; }>(); let pageInfo = $ref<Record<string, any> | null>(null); diff --git a/packages/client/src/ui/deck/mentions-column.vue b/packages/client/src/ui/deck/mentions-column.vue index a7a012a7fb..0b6ca3a239 100644 --- a/packages/client/src/ui/deck/mentions-column.vue +++ b/packages/client/src/ui/deck/mentions-column.vue @@ -18,7 +18,7 @@ defineProps<{ }>(); const emit = defineEmits<{ - (e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void; + (ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void; }>(); const pagination = { diff --git a/packages/client/src/ui/deck/notifications-column.vue b/packages/client/src/ui/deck/notifications-column.vue index 50ee12a275..6dd040cb8d 100644 --- a/packages/client/src/ui/deck/notifications-column.vue +++ b/packages/client/src/ui/deck/notifications-column.vue @@ -7,7 +7,7 @@ </template> <script lang="ts" setup> -import { } from 'vue'; +import { defineAsyncComponent } from 'vue'; import XColumn from './column.vue'; import XNotifications from '@/components/notifications.vue'; import * as os from '@/os'; @@ -20,11 +20,11 @@ const props = defineProps<{ }>(); const emit = defineEmits<{ - (e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void; + (ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void; }>(); function func() { - os.popup(import('@/components/notification-setting-window.vue'), { + os.popup(defineAsyncComponent(() => import('@/components/notification-setting-window.vue')), { includingTypes: props.column.includingTypes, }, { done: async (res) => { diff --git a/packages/client/src/ui/deck/tl-column.vue b/packages/client/src/ui/deck/tl-column.vue index 02b9ef83a1..f3ecda5aa4 100644 --- a/packages/client/src/ui/deck/tl-column.vue +++ b/packages/client/src/ui/deck/tl-column.vue @@ -35,8 +35,8 @@ const props = defineProps<{ }>(); const emit = defineEmits<{ - (e: 'loaded'): void; - (e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void; + (ev: 'loaded'): void; + (ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void; }>(); let disabled = $ref(false); diff --git a/packages/client/src/ui/deck/widgets-column.vue b/packages/client/src/ui/deck/widgets-column.vue index a2edc38357..10c6f5adf6 100644 --- a/packages/client/src/ui/deck/widgets-column.vue +++ b/packages/client/src/ui/deck/widgets-column.vue @@ -3,7 +3,7 @@ <template #header><i class="fas fa-window-maximize" style="margin-right: 8px;"></i>{{ column.name }}</template> <div class="wtdtxvec"> - <XWidgets v-if="column.widgets" :edit="edit" :widgets="column.widgets" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="edit = false"/> + <XWidgets :edit="edit" :widgets="column.widgets" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="edit = false"/> </div> </XColumn> </template> @@ -20,7 +20,7 @@ const props = defineProps<{ }>(); const emit = defineEmits<{ - (e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void; + (ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void; }>(); let edit = $ref(false); diff --git a/packages/client/src/ui/universal.widgets.vue b/packages/client/src/ui/universal.widgets.vue index 2660e80368..7aed083886 100644 --- a/packages/client/src/ui/universal.widgets.vue +++ b/packages/client/src/ui/universal.widgets.vue @@ -3,7 +3,7 @@ <XWidgets :edit="editMode" :widgets="defaultStore.reactiveState.widgets.value" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="editMode = false"/> <button v-if="editMode" class="_textButton" style="font-size: 0.9em;" @click="editMode = false"><i class="fas fa-check"></i> {{ i18n.ts.editWidgetsExit }}</button> - <button v-else class="_textButton" style="font-size: 0.9em;" @click="editMode = true"><i class="fas fa-pencil-alt"></i> {{ i18n.ts.editWidgets }}</button> + <button v-else class="_textButton mk-widget-edit" style="font-size: 0.9em;" @click="editMode = true"><i class="fas fa-pencil-alt"></i> {{ i18n.ts.editWidgets }}</button> </div> </template> @@ -14,7 +14,7 @@ import { i18n } from '@/i18n'; import { defaultStore } from '@/store'; const emit = defineEmits<{ - (e: 'mounted', el: Element): void; + (ev: 'mounted', el: Element): void; }>(); let editMode = $ref(false); @@ -32,13 +32,13 @@ function addWidget(widget) { } function removeWidget(widget) { - defaultStore.set('widgets', defaultStore.state.widgets.filter(w => w.id != widget.id)); + defaultStore.set('widgets', defaultStore.state.widgets.filter(w => w.id !== widget.id)); } function updateWidget({ id, data }) { defaultStore.set('widgets', defaultStore.state.widgets.map(w => w.id === id ? { ...w, - data: data + data, } : w)); } diff --git a/packages/client/src/widgets/activity.calendar.vue b/packages/client/src/widgets/activity.calendar.vue index b833bd65ca..33b95b00db 100644 --- a/packages/client/src/widgets/activity.calendar.vue +++ b/packages/client/src/widgets/activity.calendar.vue @@ -1,13 +1,13 @@ <template> <svg viewBox="0 0 21 7"> - <rect v-for="record in data" class="day" + <rect v-for="record in activity" class="day" width="1" height="1" :x="record.x" :y="record.date.weekday" rx="1" ry="1" fill="transparent"> <title>{{ record.date.year }}/{{ record.date.month + 1 }}/{{ record.date.day }}</title> </rect> - <rect v-for="record in data" class="day" + <rect v-for="record in activity" class="day" :width="record.v" :height="record.v" :x="record.x + ((1 - record.v) / 2)" :y="record.date.weekday + ((1 - record.v) / 2)" rx="1" ry="1" @@ -15,7 +15,7 @@ style="pointer-events: none;"/> <rect class="today" width="1" height="1" - :x="data[0].x" :y="data[0].date.weekday" + :x="activity[0].x" :y="activity[0].date.weekday" rx="1" ry="1" fill="none" stroke-width="0.1" @@ -23,45 +23,41 @@ </svg> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; -import * as os from '@/os'; +<script lang="ts" setup> +const props = defineProps<{ + activity: any[] +}>(); -export default defineComponent({ - props: ['data'], - created() { - for (const d of this.data) { - d.total = d.notes + d.replies + d.renotes; - } - const peak = Math.max.apply(null, this.data.map(d => d.total)); +for (const d of props.activity) { + d.total = d.notes + d.replies + d.renotes; +} +const peak = Math.max(...props.activity.map(d => d.total)); - const now = new Date(); - const year = now.getFullYear(); - const month = now.getMonth(); - const day = now.getDate(); +const now = new Date(); +const year = now.getFullYear(); +const month = now.getMonth(); +const day = now.getDate(); - let x = 20; - this.data.slice().forEach((d, i) => { - d.x = x; +let x = 20; +props.activity.slice().forEach((d, i) => { + d.x = x; - const date = new Date(year, month, day - i); - d.date = { - year: date.getFullYear(), - month: date.getMonth(), - day: date.getDate(), - weekday: date.getDay() - }; + const date = new Date(year, month, day - i); + d.date = { + year: date.getFullYear(), + month: date.getMonth(), + day: date.getDate(), + weekday: date.getDay() + }; - d.v = peak === 0 ? 0 : d.total / (peak / 2); - if (d.v > 1) d.v = 1; - const ch = d.date.weekday === 0 || d.date.weekday === 6 ? 275 : 170; - const cs = d.v * 100; - const cl = 15 + ((1 - d.v) * 80); - d.color = `hsl(${ch}, ${cs}%, ${cl}%)`; + d.v = peak === 0 ? 0 : d.total / (peak / 2); + if (d.v > 1) d.v = 1; + const ch = d.date.weekday === 0 || d.date.weekday === 6 ? 275 : 170; + const cs = d.v * 100; + const cl = 15 + ((1 - d.v) * 80); + d.color = `hsl(${ch}, ${cs}%, ${cl}%)`; - if (d.date.weekday === 0) x--; - }); - } + if (d.date.weekday === 0) x--; }); </script> diff --git a/packages/client/src/widgets/activity.chart.vue b/packages/client/src/widgets/activity.chart.vue index 9702d66663..b7db2af580 100644 --- a/packages/client/src/widgets/activity.chart.vue +++ b/packages/client/src/widgets/activity.chart.vue @@ -24,9 +24,19 @@ </svg> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; -import * as os from '@/os'; +<script lang="ts" setup> +const props = defineProps<{ + activity: any[] +}>(); + +let viewBoxX: number = $ref(147); +let viewBoxY: number = $ref(60); +let zoom: number = $ref(1); +let pos: number = $ref(0); +let pointsNote: any = $ref(null); +let pointsReply: any = $ref(null); +let pointsRenote: any = $ref(null); +let pointsTotal: any = $ref(null); function dragListen(fn) { window.addEventListener('mousemove', fn); @@ -40,60 +50,35 @@ function dragClear(fn) { window.removeEventListener('mouseup', dragClear); } -export default defineComponent({ - props: ['data'], - data() { - return { - viewBoxX: 147, - viewBoxY: 60, - zoom: 1, - pos: 0, - pointsNote: null, - pointsReply: null, - pointsRenote: null, - pointsTotal: null - }; - }, - created() { - for (const d of this.data) { - d.total = d.notes + d.replies + d.renotes; - } +function onMousedown(ev) { + const clickX = ev.clientX; + const clickY = ev.clientY; + const baseZoom = zoom; + const basePos = pos; - this.render(); - }, - methods: { - render() { - const peak = Math.max.apply(null, this.data.map(d => d.total)); - if (peak != 0) { - const data = this.data.slice().reverse(); - this.pointsNote = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.notes / peak)) * this.viewBoxY}`).join(' '); - this.pointsReply = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' '); - this.pointsRenote = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.renotes / peak)) * this.viewBoxY}`).join(' '); - this.pointsTotal = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' '); - } - }, - onMousedown(e) { - const clickX = e.clientX; - const clickY = e.clientY; - const baseZoom = this.zoom; - const basePos = this.pos; + // 動かした時 + dragListen(me => { + let moveLeft = me.clientX - clickX; + let moveTop = me.clientY - clickY; - // 動かした時 - dragListen(me => { - let moveLeft = me.clientX - clickX; - let moveTop = me.clientY - clickY; + zoom = Math.max(1, baseZoom + (-moveTop / 20)); + pos = Math.min(0, basePos + moveLeft); + if (pos < -(((props.activity.length - 1) * zoom) - viewBoxX)) pos = -(((props.activity.length - 1) * zoom) - viewBoxX); - this.zoom = baseZoom + (-moveTop / 20); - this.pos = basePos + moveLeft; - if (this.zoom < 1) this.zoom = 1; - if (this.pos > 0) this.pos = 0; - if (this.pos < -(((this.data.length - 1) * this.zoom) - this.viewBoxX)) this.pos = -(((this.data.length - 1) * this.zoom) - this.viewBoxX); + render(); + }); +} - this.render(); - }); - } +function render() { + const peak = Math.max(...props.activity.map(d => d.total)); + if (peak !== 0) { + const activity = props.activity.slice().reverse(); + pointsNote = activity.map((d, i) => `${(i * zoom) + pos},${(1 - (d.notes / peak)) * viewBoxY}`).join(' '); + pointsReply = activity.map((d, i) => `${(i * zoom) + pos},${(1 - (d.replies / peak)) * viewBoxY}`).join(' '); + pointsRenote = activity.map((d, i) => `${(i * zoom) + pos},${(1 - (d.renotes / peak)) * viewBoxY}`).join(' '); + pointsTotal = activity.map((d, i) => `${(i * zoom) + pos},${(1 - (d.total / peak)) * viewBoxY}`).join(' '); } -}); +} </script> <style lang="scss" scoped> diff --git a/packages/client/src/widgets/activity.vue b/packages/client/src/widgets/activity.vue index acbbb7a97a..7fb9f5894c 100644 --- a/packages/client/src/widgets/activity.vue +++ b/packages/client/src/widgets/activity.vue @@ -1,13 +1,13 @@ <template> -<MkContainer :show-header="widgetProps.showHeader" :naked="widgetProps.transparent"> +<MkContainer :show-header="widgetProps.showHeader" :naked="widgetProps.transparent" class="mkw-activity"> <template #header><i class="fas fa-chart-bar"></i>{{ $ts._widgets.activity }}</template> <template #func><button class="_button" @click="toggleView()"><i class="fas fa-sort"></i></button></template> <div> <MkLoading v-if="fetching"/> <template v-else> - <XCalendar v-show="widgetProps.view === 0" :data="[].concat(activity)"/> - <XChart v-show="widgetProps.view === 1" :data="[].concat(activity)"/> + <XCalendar v-show="widgetProps.view === 0" :activity="[].concat(activity)"/> + <XChart v-show="widgetProps.view === 1" :activity="[].concat(activity)"/> </template> </div> </MkContainer> @@ -47,7 +47,7 @@ type WidgetProps = GetFormResultType<typeof widgetPropsDef>; //const props = defineProps<WidgetComponentProps<WidgetProps>>(); //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); +const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); const { widgetProps, configure, save } = useWidgetPropsManager(name, widgetPropsDef, diff --git a/packages/client/src/widgets/aichan.vue b/packages/client/src/widgets/aichan.vue index 03e394b976..cdd367cc84 100644 --- a/packages/client/src/widgets/aichan.vue +++ b/packages/client/src/widgets/aichan.vue @@ -1,5 +1,5 @@ <template> -<MkContainer :naked="widgetProps.transparent" :show-header="false"> +<MkContainer :naked="widgetProps.transparent" :show-header="false" class="mkw-aichan"> <iframe ref="live2d" class="dedjhjmo" src="https://misskey-dev.github.io/mascot-web/?scale=1.5&y=1.1&eyeY=100" @click="touched"></iframe> </MkContainer> </template> @@ -24,7 +24,7 @@ type WidgetProps = GetFormResultType<typeof widgetPropsDef>; //const props = defineProps<WidgetComponentProps<WidgetProps>>(); //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); +const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); const { widgetProps, configure } = useWidgetPropsManager(name, widgetPropsDef, diff --git a/packages/client/src/widgets/aiscript.vue b/packages/client/src/widgets/aiscript.vue index 0a5c0d614d..9fed292a69 100644 --- a/packages/client/src/widgets/aiscript.vue +++ b/packages/client/src/widgets/aiscript.vue @@ -1,5 +1,5 @@ <template> -<MkContainer :show-header="widgetProps.showHeader"> +<MkContainer :show-header="widgetProps.showHeader" class="mkw-aiscript"> <template #header><i class="fas fa-terminal"></i>{{ $ts._widgets.aiscript }}</template> <div class="uylguesu _monospace"> @@ -43,7 +43,7 @@ type WidgetProps = GetFormResultType<typeof widgetPropsDef>; //const props = defineProps<WidgetComponentProps<WidgetProps>>(); //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); +const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); const { widgetProps, configure } = useWidgetPropsManager(name, widgetPropsDef, @@ -94,7 +94,7 @@ const run = async () => { let ast; try { ast = parse(widgetProps.script); - } catch (e) { + } catch (err) { os.alert({ type: 'error', text: 'Syntax error :(', @@ -103,10 +103,10 @@ const run = async () => { } try { await aiscript.exec(ast); - } catch (e) { + } catch (err) { os.alert({ type: 'error', - text: e, + text: err, }); } }; diff --git a/packages/client/src/widgets/button.vue b/packages/client/src/widgets/button.vue index a33afd6e7a..ee4e9c6423 100644 --- a/packages/client/src/widgets/button.vue +++ b/packages/client/src/widgets/button.vue @@ -40,7 +40,7 @@ type WidgetProps = GetFormResultType<typeof widgetPropsDef>; //const props = defineProps<WidgetComponentProps<WidgetProps>>(); //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); +const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); const { widgetProps, configure } = useWidgetPropsManager(name, widgetPropsDef, @@ -73,7 +73,7 @@ const run = async () => { let ast; try { ast = parse(widgetProps.script); - } catch (e) { + } catch (err) { os.alert({ type: 'error', text: 'Syntax error :(', @@ -82,10 +82,10 @@ const run = async () => { } try { await aiscript.exec(ast); - } catch (e) { + } catch (err) { os.alert({ type: 'error', - text: e, + text: err, }); } }; diff --git a/packages/client/src/widgets/calendar.vue b/packages/client/src/widgets/calendar.vue index c6a69b3fb8..2a2b035541 100644 --- a/packages/client/src/widgets/calendar.vue +++ b/packages/client/src/widgets/calendar.vue @@ -53,7 +53,7 @@ type WidgetProps = GetFormResultType<typeof widgetPropsDef>; //const props = defineProps<WidgetComponentProps<WidgetProps>>(); //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); +const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); const { widgetProps, configure } = useWidgetPropsManager(name, widgetPropsDef, diff --git a/packages/client/src/widgets/clock.vue b/packages/client/src/widgets/clock.vue index 6acb10d74d..fbd2f9e899 100644 --- a/packages/client/src/widgets/clock.vue +++ b/packages/client/src/widgets/clock.vue @@ -1,5 +1,5 @@ <template> -<MkContainer :naked="widgetProps.transparent" :show-header="false"> +<MkContainer :naked="widgetProps.transparent" :show-header="false" class="mkw-clock"> <div class="vubelbmv"> <MkAnalogClock class="clock" :thickness="widgetProps.thickness"/> </div> @@ -39,7 +39,7 @@ type WidgetProps = GetFormResultType<typeof widgetPropsDef>; //const props = defineProps<WidgetComponentProps<WidgetProps>>(); //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); +const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); const { widgetProps, configure } = useWidgetPropsManager(name, widgetPropsDef, diff --git a/packages/client/src/widgets/digital-clock.vue b/packages/client/src/widgets/digital-clock.vue index 62f052a692..a17ed040c9 100644 --- a/packages/client/src/widgets/digital-clock.vue +++ b/packages/client/src/widgets/digital-clock.vue @@ -41,7 +41,7 @@ type WidgetProps = GetFormResultType<typeof widgetPropsDef>; //const props = defineProps<WidgetComponentProps<WidgetProps>>(); //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); +const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); const { widgetProps, configure } = useWidgetPropsManager(name, widgetPropsDef, diff --git a/packages/client/src/widgets/federation.vue b/packages/client/src/widgets/federation.vue index 5f1131dce1..a3862077bb 100644 --- a/packages/client/src/widgets/federation.vue +++ b/packages/client/src/widgets/federation.vue @@ -1,5 +1,5 @@ <template> -<MkContainer :show-header="widgetProps.showHeader" :foldable="foldable" :scrollable="scrollable"> +<MkContainer :show-header="widgetProps.showHeader" :foldable="foldable" :scrollable="scrollable" class="mkw-federation"> <template #header><i class="fas fa-globe"></i>{{ $ts._widgets.federation }}</template> <div class="wbrkwalb"> @@ -41,7 +41,7 @@ type WidgetProps = GetFormResultType<typeof widgetPropsDef>; //const props = defineProps<WidgetComponentProps<WidgetProps> & { foldable?: boolean; scrollable?: boolean; }>(); //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const props = defineProps<{ widget?: Widget<WidgetProps>; foldable?: boolean; scrollable?: boolean; }>(); -const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); +const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); const { widgetProps, configure } = useWidgetPropsManager(name, widgetPropsDef, diff --git a/packages/client/src/widgets/job-queue.vue b/packages/client/src/widgets/job-queue.vue index 4a2a3cf233..8897f240bd 100644 --- a/packages/client/src/widgets/job-queue.vue +++ b/packages/client/src/widgets/job-queue.vue @@ -73,7 +73,7 @@ type WidgetProps = GetFormResultType<typeof widgetPropsDef>; //const props = defineProps<WidgetComponentProps<WidgetProps>>(); //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); +const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); const { widgetProps, configure } = useWidgetPropsManager(name, widgetPropsDef, diff --git a/packages/client/src/widgets/memo.vue b/packages/client/src/widgets/memo.vue index 450598f65a..8670cb2bac 100644 --- a/packages/client/src/widgets/memo.vue +++ b/packages/client/src/widgets/memo.vue @@ -1,5 +1,5 @@ <template> -<MkContainer :show-header="widgetProps.showHeader"> +<MkContainer :show-header="widgetProps.showHeader" class="mkw-memo"> <template #header><i class="fas fa-sticky-note"></i>{{ $ts._widgets.memo }}</template> <div class="otgbylcu"> @@ -32,7 +32,7 @@ type WidgetProps = GetFormResultType<typeof widgetPropsDef>; //const props = defineProps<WidgetComponentProps<WidgetProps>>(); //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); +const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); const { widgetProps, configure } = useWidgetPropsManager(name, widgetPropsDef, diff --git a/packages/client/src/widgets/notifications.vue b/packages/client/src/widgets/notifications.vue index 8cf29c9271..18c546ee74 100644 --- a/packages/client/src/widgets/notifications.vue +++ b/packages/client/src/widgets/notifications.vue @@ -1,5 +1,5 @@ <template> -<MkContainer :style="`height: ${widgetProps.height}px;`" :show-header="widgetProps.showHeader" :scrollable="true"> +<MkContainer :style="`height: ${widgetProps.height}px;`" :show-header="widgetProps.showHeader" :scrollable="true" class="mkw-notifications"> <template #header><i class="fas fa-bell"></i>{{ $ts.notifications }}</template> <template #func><button class="_button" @click="configureNotification()"><i class="fas fa-cog"></i></button></template> @@ -15,6 +15,7 @@ import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExp import MkContainer from '@/components/ui/container.vue'; import XNotifications from '@/components/notifications.vue'; import * as os from '@/os'; +import { defineAsyncComponent } from 'vue'; const name = 'notifications'; @@ -40,7 +41,7 @@ type WidgetProps = GetFormResultType<typeof widgetPropsDef>; //const props = defineProps<WidgetComponentProps<WidgetProps>>(); //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); +const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); const { widgetProps, configure, save } = useWidgetPropsManager(name, widgetPropsDef, @@ -49,7 +50,7 @@ const { widgetProps, configure, save } = useWidgetPropsManager(name, ); const configureNotification = () => { - os.popup(import('@/components/notification-setting-window.vue'), { + os.popup(defineAsyncComponent(() => import('@/components/notification-setting-window.vue')), { includingTypes: widgetProps.includingTypes, }, { done: async (res) => { diff --git a/packages/client/src/widgets/online-users.vue b/packages/client/src/widgets/online-users.vue index 1746a8314e..eb3184fe9d 100644 --- a/packages/client/src/widgets/online-users.vue +++ b/packages/client/src/widgets/online-users.vue @@ -27,7 +27,7 @@ type WidgetProps = GetFormResultType<typeof widgetPropsDef>; //const props = defineProps<WidgetComponentProps<WidgetProps>>(); //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); +const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); const { widgetProps, configure } = useWidgetPropsManager(name, widgetPropsDef, diff --git a/packages/client/src/widgets/photos.vue b/packages/client/src/widgets/photos.vue index 8f948dc643..5d9b9e2984 100644 --- a/packages/client/src/widgets/photos.vue +++ b/packages/client/src/widgets/photos.vue @@ -1,5 +1,5 @@ <template> -<MkContainer :show-header="widgetProps.showHeader" :naked="widgetProps.transparent" :class="$style.root" :data-transparent="widgetProps.transparent ? true : null"> +<MkContainer :show-header="widgetProps.showHeader" :naked="widgetProps.transparent" :class="$style.root" :data-transparent="widgetProps.transparent ? true : null" class="mkw-photos"> <template #header><i class="fas fa-camera"></i>{{ $ts._widgets.photos }}</template> <div class=""> @@ -43,7 +43,7 @@ type WidgetProps = GetFormResultType<typeof widgetPropsDef>; //const props = defineProps<WidgetComponentProps<WidgetProps>>(); //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); +const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); const { widgetProps, configure } = useWidgetPropsManager(name, widgetPropsDef, diff --git a/packages/client/src/widgets/post-form.vue b/packages/client/src/widgets/post-form.vue index 51aa8fcf6b..b542913357 100644 --- a/packages/client/src/widgets/post-form.vue +++ b/packages/client/src/widgets/post-form.vue @@ -1,5 +1,5 @@ <template> -<XPostForm class="_panel" :fixed="true" :autofocus="false"/> +<XPostForm class="_panel mkw-postForm" :fixed="true" :autofocus="false"/> </template> <script lang="ts" setup> @@ -19,7 +19,7 @@ type WidgetProps = GetFormResultType<typeof widgetPropsDef>; //const props = defineProps<WidgetComponentProps<WidgetProps>>(); //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); +const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); const { widgetProps, configure } = useWidgetPropsManager(name, widgetPropsDef, diff --git a/packages/client/src/widgets/rss.vue b/packages/client/src/widgets/rss.vue index 9e2e503602..fc65f11813 100644 --- a/packages/client/src/widgets/rss.vue +++ b/packages/client/src/widgets/rss.vue @@ -1,5 +1,5 @@ <template> -<MkContainer :show-header="widgetProps.showHeader"> +<MkContainer :show-header="widgetProps.showHeader" class="mkw-rss"> <template #header><i class="fas fa-rss-square"></i>RSS</template> <template #func><button class="_button" @click="configure"><i class="fas fa-cog"></i></button></template> @@ -38,7 +38,7 @@ type WidgetProps = GetFormResultType<typeof widgetPropsDef>; //const props = defineProps<WidgetComponentProps<WidgetProps>>(); //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); +const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); const { widgetProps, configure } = useWidgetPropsManager(name, widgetPropsDef, diff --git a/packages/client/src/widgets/server-metric/cpu-mem.vue b/packages/client/src/widgets/server-metric/cpu-mem.vue index ad9e6a8b0f..00c3a10c9b 100644 --- a/packages/client/src/widgets/server-metric/cpu-mem.vue +++ b/packages/client/src/widgets/server-metric/cpu-mem.vue @@ -69,79 +69,72 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { onMounted, onBeforeUnmount } from 'vue'; import { v4 as uuid } from 'uuid'; -export default defineComponent({ - props: { - connection: { - required: true, - }, - meta: { - required: true, - } - }, - data() { - return { - viewBoxX: 50, - viewBoxY: 30, - stats: [], - cpuGradientId: uuid(), - cpuMaskId: uuid(), - memGradientId: uuid(), - memMaskId: uuid(), - cpuPolylinePoints: '', - memPolylinePoints: '', - cpuPolygonPoints: '', - memPolygonPoints: '', - cpuHeadX: null, - cpuHeadY: null, - memHeadX: null, - memHeadY: null, - cpuP: '', - memP: '' - }; - }, - mounted() { - this.connection.on('stats', this.onStats); - this.connection.on('statsLog', this.onStatsLog); - this.connection.send('requestLog', { - id: Math.random().toString().substr(2, 8) - }); - }, - beforeUnmount() { - this.connection.off('stats', this.onStats); - this.connection.off('statsLog', this.onStatsLog); - }, - methods: { - onStats(stats) { - this.stats.push(stats); - if (this.stats.length > 50) this.stats.shift(); +const props = defineProps<{ + connection: any, + meta: any +}>(); - const cpuPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - s.cpu) * this.viewBoxY]); - const memPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - (s.mem.active / this.meta.mem.total)) * this.viewBoxY]); - this.cpuPolylinePoints = cpuPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' '); - this.memPolylinePoints = memPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' '); +let viewBoxX: number = $ref(50); +let viewBoxY: number = $ref(30); +let stats: any[] = $ref([]); +const cpuGradientId = uuid(); +const cpuMaskId = uuid(); +const memGradientId = uuid(); +const memMaskId = uuid(); +let cpuPolylinePoints: string = $ref(''); +let memPolylinePoints: string = $ref(''); +let cpuPolygonPoints: string = $ref(''); +let memPolygonPoints: string = $ref(''); +let cpuHeadX: any = $ref(null); +let cpuHeadY: any = $ref(null); +let memHeadX: any = $ref(null); +let memHeadY: any = $ref(null); +let cpuP: string = $ref(''); +let memP: string = $ref(''); - this.cpuPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.cpuPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`; - this.memPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.memPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`; +onMounted(() => { + props.connection.on('stats', onStats); + props.connection.on('statsLog', onStatsLog); + props.connection.send('requestLog', { + id: Math.random().toString().substr(2, 8) + }); +}); - this.cpuHeadX = cpuPolylinePoints[cpuPolylinePoints.length - 1][0]; - this.cpuHeadY = cpuPolylinePoints[cpuPolylinePoints.length - 1][1]; - this.memHeadX = memPolylinePoints[memPolylinePoints.length - 1][0]; - this.memHeadY = memPolylinePoints[memPolylinePoints.length - 1][1]; +onBeforeUnmount(() => { + props.connection.off('stats', onStats); + props.connection.off('statsLog', onStatsLog); +}); - this.cpuP = (stats.cpu * 100).toFixed(0); - this.memP = (stats.mem.active / this.meta.mem.total * 100).toFixed(0); - }, - onStatsLog(statsLog) { - for (const stats of [...statsLog].reverse()) { - this.onStats(stats); - } - } +function onStats(connStats) { + stats.push(connStats); + if (stats.length > 50) stats.shift(); + + let cpuPolylinePointsStats = stats.map((s, i) => [viewBoxX - ((stats.length - 1) - i), (1 - s.cpu) * viewBoxY]); + let memPolylinePointsStats = stats.map((s, i) => [viewBoxX - ((stats.length - 1) - i), (1 - (s.mem.active / props.meta.mem.total)) * viewBoxY]); + cpuPolylinePoints = cpuPolylinePointsStats.map(xy => `${xy[0]},${xy[1]}`).join(' '); + memPolylinePoints = memPolylinePointsStats.map(xy => `${xy[0]},${xy[1]}`).join(' '); + + cpuPolygonPoints = `${viewBoxX - (stats.length - 1)},${viewBoxY} ${cpuPolylinePoints} ${viewBoxX},${viewBoxY}`; + memPolygonPoints = `${viewBoxX - (stats.length - 1)},${viewBoxY} ${memPolylinePoints} ${viewBoxX},${viewBoxY}`; + + cpuHeadX = cpuPolylinePointsStats[cpuPolylinePointsStats.length - 1][0]; + cpuHeadY = cpuPolylinePointsStats[cpuPolylinePointsStats.length - 1][1]; + memHeadX = memPolylinePointsStats[memPolylinePointsStats.length - 1][0]; + memHeadY = memPolylinePointsStats[memPolylinePointsStats.length - 1][1]; + + cpuP = (connStats.cpu * 100).toFixed(0); + memP = (connStats.mem.active / props.meta.mem.total * 100).toFixed(0); +} + +function onStatsLog(statsLog) { + for (const revStats of [...statsLog].reverse()) { + onStats(revStats); } -}); +} </script> <style lang="scss" scoped> diff --git a/packages/client/src/widgets/server-metric/cpu.vue b/packages/client/src/widgets/server-metric/cpu.vue index 4478ee3065..baf802cb8f 100644 --- a/packages/client/src/widgets/server-metric/cpu.vue +++ b/packages/client/src/widgets/server-metric/cpu.vue @@ -9,38 +9,27 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { onMounted, onBeforeUnmount } from 'vue'; import XPie from './pie.vue'; -export default defineComponent({ - components: { - XPie - }, - props: { - connection: { - required: true, - }, - meta: { - required: true, - } - }, - data() { - return { - usage: 0, - }; - }, - mounted() { - this.connection.on('stats', this.onStats); - }, - beforeUnmount() { - this.connection.off('stats', this.onStats); - }, - methods: { - onStats(stats) { - this.usage = stats.cpu; - } - } +const props = defineProps<{ + connection: any, + meta: any +}>(); + +let usage: number = $ref(0); + +function onStats(stats) { + usage = stats.cpu; +} + +onMounted(() => { + props.connection.on('stats', onStats); +}); + +onBeforeUnmount(() => { + props.connection.off('stats', onStats); }); </script> diff --git a/packages/client/src/widgets/server-metric/index.vue b/packages/client/src/widgets/server-metric/index.vue index 2caa73fa74..9e86b811d1 100644 --- a/packages/client/src/widgets/server-metric/index.vue +++ b/packages/client/src/widgets/server-metric/index.vue @@ -50,7 +50,7 @@ type WidgetProps = GetFormResultType<typeof widgetPropsDef>; //const props = defineProps<WidgetComponentProps<WidgetProps>>(); //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); +const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); const { widgetProps, configure, save } = useWidgetPropsManager(name, widgetPropsDef, @@ -65,7 +65,7 @@ os.api('server-info', {}).then(res => { }); const toggleView = () => { - if (widgetProps.view == 4) { + if (widgetProps.view === 4) { widgetProps.view = 0; } else { widgetProps.view++; diff --git a/packages/client/src/widgets/server-metric/mem.vue b/packages/client/src/widgets/server-metric/mem.vue index a6ca7b1175..6018eb4265 100644 --- a/packages/client/src/widgets/server-metric/mem.vue +++ b/packages/client/src/widgets/server-metric/mem.vue @@ -10,46 +10,34 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { onMounted, onBeforeUnmount } from 'vue'; import XPie from './pie.vue'; import bytes from '@/filters/bytes'; -export default defineComponent({ - components: { - XPie - }, - props: { - connection: { - required: true, - }, - meta: { - required: true, - } - }, - data() { - return { - usage: 0, - total: 0, - used: 0, - free: 0, - }; - }, - mounted() { - this.connection.on('stats', this.onStats); - }, - beforeUnmount() { - this.connection.off('stats', this.onStats); - }, - methods: { - onStats(stats) { - this.usage = stats.mem.active / this.meta.mem.total; - this.total = this.meta.mem.total; - this.used = stats.mem.active; - this.free = this.meta.mem.total - stats.mem.active; - }, - bytes - } +const props = defineProps<{ + connection: any, + meta: any +}>(); + +let usage: number = $ref(0); +let total: number = $ref(0); +let used: number = $ref(0); +let free: number = $ref(0); + +function onStats(stats) { + usage = stats.mem.active / props.meta.mem.total; + total = props.meta.mem.total; + used = stats.mem.active; + free = total - used; +} + +onMounted(() => { + props.connection.on('stats', onStats); +}); + +onBeforeUnmount(() => { + props.connection.off('stats', onStats); }); </script> diff --git a/packages/client/src/widgets/server-metric/net.vue b/packages/client/src/widgets/server-metric/net.vue index 23c148eeb6..b698953f97 100644 --- a/packages/client/src/widgets/server-metric/net.vue +++ b/packages/client/src/widgets/server-metric/net.vue @@ -43,79 +43,71 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { onMounted, onBeforeUnmount } from 'vue'; import bytes from '@/filters/bytes'; -export default defineComponent({ - props: { - connection: { - required: true, - }, - meta: { - required: true, - } - }, - data() { - return { - viewBoxX: 50, - viewBoxY: 30, - stats: [], - inPolylinePoints: '', - outPolylinePoints: '', - inPolygonPoints: '', - outPolygonPoints: '', - inHeadX: null, - inHeadY: null, - outHeadX: null, - outHeadY: null, - inRecent: 0, - outRecent: 0 - }; - }, - mounted() { - this.connection.on('stats', this.onStats); - this.connection.on('statsLog', this.onStatsLog); - this.connection.send('requestLog', { - id: Math.random().toString().substr(2, 8) - }); - }, - beforeUnmount() { - this.connection.off('stats', this.onStats); - this.connection.off('statsLog', this.onStatsLog); - }, - methods: { - onStats(stats) { - this.stats.push(stats); - if (this.stats.length > 50) this.stats.shift(); +const props = defineProps<{ + connection: any, + meta: any +}>(); + +let viewBoxX: number = $ref(50); +let viewBoxY: number = $ref(30); +let stats: any[] = $ref([]); +let inPolylinePoints: string = $ref(''); +let outPolylinePoints: string = $ref(''); +let inPolygonPoints: string = $ref(''); +let outPolygonPoints: string = $ref(''); +let inHeadX: any = $ref(null); +let inHeadY: any = $ref(null); +let outHeadX: any = $ref(null); +let outHeadY: any = $ref(null); +let inRecent: number = $ref(0); +let outRecent: number = $ref(0); + +onMounted(() => { + props.connection.on('stats', onStats); + props.connection.on('statsLog', onStatsLog); + props.connection.send('requestLog', { + id: Math.random().toString().substr(2, 8) + }); +}); - const inPeak = Math.max(1024 * 64, Math.max(...this.stats.map(s => s.net.rx))); - const outPeak = Math.max(1024 * 64, Math.max(...this.stats.map(s => s.net.tx))); +onBeforeUnmount(() => { + props.connection.off('stats', onStats); + props.connection.off('statsLog', onStatsLog); +}); - const inPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - (s.net.rx / inPeak)) * this.viewBoxY]); - const outPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - (s.net.tx / outPeak)) * this.viewBoxY]); - this.inPolylinePoints = inPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' '); - this.outPolylinePoints = outPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' '); +function onStats(connStats) { + stats.push(connStats); + if (stats.length > 50) stats.shift(); - this.inPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.inPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`; - this.outPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.outPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`; + const inPeak = Math.max(1024 * 64, Math.max(...stats.map(s => s.net.rx))); + const outPeak = Math.max(1024 * 64, Math.max(...stats.map(s => s.net.tx))); - this.inHeadX = inPolylinePoints[inPolylinePoints.length - 1][0]; - this.inHeadY = inPolylinePoints[inPolylinePoints.length - 1][1]; - this.outHeadX = outPolylinePoints[outPolylinePoints.length - 1][0]; - this.outHeadY = outPolylinePoints[outPolylinePoints.length - 1][1]; + let inPolylinePointsStats = stats.map((s, i) => [viewBoxX - ((stats.length - 1) - i), (1 - (s.net.rx / inPeak)) * viewBoxY]); + let outPolylinePointsStats = stats.map((s, i) => [viewBoxX - ((stats.length - 1) - i), (1 - (s.net.tx / outPeak)) * viewBoxY]); + inPolylinePoints = inPolylinePointsStats.map(xy => `${xy[0]},${xy[1]}`).join(' '); + outPolylinePoints = outPolylinePointsStats.map(xy => `${xy[0]},${xy[1]}`).join(' '); - this.inRecent = stats.net.rx; - this.outRecent = stats.net.tx; - }, - onStatsLog(statsLog) { - for (const stats of [...statsLog].reverse()) { - this.onStats(stats); - } - }, - bytes + inPolygonPoints = `${viewBoxX - (stats.length - 1)},${viewBoxY} ${inPolylinePoints} ${viewBoxX},${viewBoxY}`; + outPolygonPoints = `${viewBoxX - (stats.length - 1)},${viewBoxY} ${outPolylinePoints} ${viewBoxX},${viewBoxY}`; + + inHeadX = inPolylinePointsStats[inPolylinePointsStats.length - 1][0]; + inHeadY = inPolylinePointsStats[inPolylinePointsStats.length - 1][1]; + outHeadX = outPolylinePointsStats[outPolylinePointsStats.length - 1][0]; + outHeadY = outPolylinePointsStats[outPolylinePointsStats.length - 1][1]; + + inRecent = connStats.net.rx; + outRecent = connStats.net.tx; +} + +function onStatsLog(statsLog) { + for (const revStats of [...statsLog].reverse()) { + onStats(revStats); } -}); +} </script> <style lang="scss" scoped> diff --git a/packages/client/src/widgets/slideshow.vue b/packages/client/src/widgets/slideshow.vue index 7b2e539685..fd78edbe40 100644 --- a/packages/client/src/widgets/slideshow.vue +++ b/packages/client/src/widgets/slideshow.vue @@ -1,5 +1,5 @@ <template> -<div class="kvausudm _panel" :style="{ height: widgetProps.height + 'px' }"> +<div class="kvausudm _panel mkw-slideshow" :style="{ height: widgetProps.height + 'px' }"> <div @click="choose"> <p v-if="widgetProps.folderId == null"> {{ $ts.folder }} @@ -37,7 +37,7 @@ type WidgetProps = GetFormResultType<typeof widgetPropsDef>; //const props = defineProps<WidgetComponentProps<WidgetProps>>(); //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); +const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); const { widgetProps, configure, save } = useWidgetPropsManager(name, widgetPropsDef, @@ -51,7 +51,7 @@ const slideA = ref<HTMLElement>(); const slideB = ref<HTMLElement>(); const change = () => { - if (images.value.length == 0) return; + if (images.value.length === 0) return; const index = Math.floor(Math.random() * images.value.length); const img = `url(${ images.value[index].url })`; diff --git a/packages/client/src/widgets/timeline.vue b/packages/client/src/widgets/timeline.vue index 34e3b20e36..3bcad1ae29 100644 --- a/packages/client/src/widgets/timeline.vue +++ b/packages/client/src/widgets/timeline.vue @@ -1,5 +1,5 @@ <template> -<MkContainer :show-header="widgetProps.showHeader" :style="`height: ${widgetProps.height}px;`" :scrollable="true"> +<MkContainer :show-header="widgetProps.showHeader" :style="`height: ${widgetProps.height}px;`" :scrollable="true" class="mkw-timeline"> <template #header> <button class="_button" @click="choose"> <i v-if="widgetProps.src === 'home'" class="fas fa-home"></i> @@ -63,7 +63,7 @@ type WidgetProps = GetFormResultType<typeof widgetPropsDef>; //const props = defineProps<WidgetComponentProps<WidgetProps>>(); //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); +const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); const { widgetProps, configure, save } = useWidgetPropsManager(name, widgetPropsDef, @@ -103,19 +103,19 @@ const choose = async (ev) => { os.popupMenu([{ text: i18n.ts._timelines.home, icon: 'fas fa-home', - action: () => { setSrc('home') } + action: () => { setSrc('home'); } }, { text: i18n.ts._timelines.local, icon: 'fas fa-comments', - action: () => { setSrc('local') } + action: () => { setSrc('local'); } }, { text: i18n.ts._timelines.social, icon: 'fas fa-share-alt', - action: () => { setSrc('social') } + action: () => { setSrc('social'); } }, { text: i18n.ts._timelines.global, icon: 'fas fa-globe', - action: () => { setSrc('global') } + action: () => { setSrc('global'); } }, antennaItems.length > 0 ? null : undefined, ...antennaItems, listItems.length > 0 ? null : undefined, ...listItems], ev.currentTarget ?? ev.target).then(() => { menuOpened.value = false; }); diff --git a/packages/client/src/widgets/trends.vue b/packages/client/src/widgets/trends.vue index a34710eae7..9680f1c892 100644 --- a/packages/client/src/widgets/trends.vue +++ b/packages/client/src/widgets/trends.vue @@ -1,5 +1,5 @@ <template> -<MkContainer :show-header="widgetProps.showHeader"> +<MkContainer :show-header="widgetProps.showHeader" class="mkw-trends"> <template #header><i class="fas fa-hashtag"></i>{{ $ts._widgets.trends }}</template> <div class="wbrkwala"> @@ -40,7 +40,7 @@ type WidgetProps = GetFormResultType<typeof widgetPropsDef>; //const props = defineProps<WidgetComponentProps<WidgetProps>>(); //const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>(); +const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); const { widgetProps, configure } = useWidgetPropsManager(name, widgetPropsDef, diff --git a/packages/client/src/widgets/widget.ts b/packages/client/src/widgets/widget.ts index 81239bfb3b..9626d01619 100644 --- a/packages/client/src/widgets/widget.ts +++ b/packages/client/src/widgets/widget.ts @@ -13,7 +13,7 @@ export type WidgetComponentProps<P extends Record<string, unknown>> = { }; export type WidgetComponentEmits<P extends Record<string, unknown>> = { - (e: 'updateProps', props: P); + (ev: 'updateProps', props: P); }; export type WidgetComponentExpose = { @@ -45,7 +45,7 @@ export const useWidgetPropsManager = <F extends Form & Record<string, { default: }, { deep: true, immediate: true, }); const save = throttle(3000, () => { - emit('updateProps', widgetProps) + emit('updateProps', widgetProps); }); const configure = async () => { |