summaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2021-10-23 01:08:45 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2021-10-23 01:08:45 +0900
commitd0d5068f728e13f3ebe1dc227ddaacf380817ec4 (patch)
tree7bb95207e01bff1bee9877829c0556d3ecf62176 /src/client
parentMerge branch 'develop' (diff)
parent12.93.0 (diff)
downloadmisskey-d0d5068f728e13f3ebe1dc227ddaacf380817ec4.tar.gz
misskey-d0d5068f728e13f3ebe1dc227ddaacf380817ec4.tar.bz2
misskey-d0d5068f728e13f3ebe1dc227ddaacf380817ec4.zip
Merge branch 'develop'
Diffstat (limited to 'src/client')
-rw-r--r--src/client/components/chart.vue628
-rw-r--r--src/client/components/form/input.vue2
-rw-r--r--src/client/components/form/radios.vue12
-rw-r--r--src/client/components/form/select.vue65
-rw-r--r--src/client/components/form/textarea.vue2
-rw-r--r--src/client/components/global/emoji.vue8
-rw-r--r--src/client/components/global/header.vue6
-rw-r--r--src/client/components/instance-stats.vue487
-rw-r--r--src/client/components/mfm.ts2
-rw-r--r--src/client/components/number-diff.vue47
-rw-r--r--src/client/components/post-form.vue38
-rw-r--r--src/client/components/queue-chart.vue212
-rw-r--r--src/client/components/tab.vue2
-rw-r--r--src/client/components/ui/container.vue9
-rw-r--r--src/client/components/ui/menu.vue5
-rw-r--r--src/client/components/ui/popup-menu.vue6
-rw-r--r--src/client/components/ui/super-menu.vue2
-rw-r--r--src/client/os.ts7
-rw-r--r--src/client/pages/admin/abuses.vue (renamed from src/client/pages/instance/abuses.vue)0
-rw-r--r--src/client/pages/admin/ads.vue (renamed from src/client/pages/instance/ads.vue)0
-rw-r--r--src/client/pages/admin/announcements.vue (renamed from src/client/pages/instance/announcements.vue)0
-rw-r--r--src/client/pages/admin/bot-protection.vue (renamed from src/client/pages/instance/bot-protection.vue)0
-rw-r--r--src/client/pages/admin/database.vue (renamed from src/client/pages/instance/database.vue)0
-rw-r--r--src/client/pages/admin/email-settings.vue (renamed from src/client/pages/instance/email-settings.vue)0
-rw-r--r--src/client/pages/admin/emoji-edit-dialog.vue (renamed from src/client/pages/instance/emoji-edit-dialog.vue)0
-rw-r--r--src/client/pages/admin/emojis.vue (renamed from src/client/pages/instance/emojis.vue)0
-rw-r--r--src/client/pages/admin/file-dialog.vue (renamed from src/client/pages/instance/file-dialog.vue)0
-rw-r--r--src/client/pages/admin/files-settings.vue (renamed from src/client/pages/instance/files-settings.vue)0
-rw-r--r--src/client/pages/admin/files.vue (renamed from src/client/pages/instance/files.vue)0
-rw-r--r--src/client/pages/admin/index.vue (renamed from src/client/pages/instance/index.vue)52
-rw-r--r--src/client/pages/admin/instance-block.vue (renamed from src/client/pages/instance/instance-block.vue)0
-rw-r--r--src/client/pages/admin/instance.vue (renamed from src/client/pages/instance/instance.vue)266
-rw-r--r--src/client/pages/admin/integrations-discord.vue (renamed from src/client/pages/instance/integrations-discord.vue)0
-rw-r--r--src/client/pages/admin/integrations-github.vue (renamed from src/client/pages/instance/integrations-github.vue)0
-rw-r--r--src/client/pages/admin/integrations-twitter.vue (renamed from src/client/pages/instance/integrations-twitter.vue)0
-rw-r--r--src/client/pages/admin/integrations.vue (renamed from src/client/pages/instance/integrations.vue)6
-rw-r--r--src/client/pages/admin/metrics.vue (renamed from src/client/pages/instance/metrics.vue)83
-rw-r--r--src/client/pages/admin/object-storage.vue (renamed from src/client/pages/instance/object-storage.vue)0
-rw-r--r--src/client/pages/admin/other-settings.vue (renamed from src/client/pages/instance/other-settings.vue)0
-rw-r--r--src/client/pages/admin/overview.vue242
-rw-r--r--src/client/pages/admin/proxy-account.vue (renamed from src/client/pages/instance/proxy-account.vue)0
-rw-r--r--src/client/pages/admin/queue.chart.vue102
-rw-r--r--src/client/pages/admin/queue.vue (renamed from src/client/pages/instance/queue.vue)0
-rw-r--r--src/client/pages/admin/relays.vue (renamed from src/client/pages/instance/relays.vue)0
-rw-r--r--src/client/pages/admin/security.vue (renamed from src/client/pages/instance/security.vue)2
-rw-r--r--src/client/pages/admin/service-worker.vue (renamed from src/client/pages/instance/service-worker.vue)0
-rw-r--r--src/client/pages/admin/settings.vue (renamed from src/client/pages/instance/settings.vue)0
-rw-r--r--src/client/pages/admin/users.vue (renamed from src/client/pages/instance/users.vue)0
-rw-r--r--src/client/pages/explore.vue19
-rw-r--r--src/client/pages/instance-info.vue268
-rw-r--r--src/client/pages/instance/logs.vue97
-rw-r--r--src/client/pages/instance/overview.vue127
-rw-r--r--src/client/pages/instance/queue.chart.vue218
-rw-r--r--src/client/pages/settings/import-export.vue20
-rw-r--r--src/client/pages/settings/privacy.vue7
-rw-r--r--src/client/pages/settings/theme.manage.vue14
-rw-r--r--src/client/pages/share.vue158
-rw-r--r--src/client/pages/user/index.timeline.vue7
-rw-r--r--src/client/pages/user/index.vue9
-rw-r--r--src/client/pages/user/reactions.vue81
-rw-r--r--src/client/router.ts4
-rw-r--r--src/client/scripts/hpml/lib.ts7
-rw-r--r--src/client/scripts/theme.ts2
-rw-r--r--src/client/themes/d-astro.json52
-rw-r--r--src/client/themes/d-future.json52
-rw-r--r--src/client/themes/d-persimmon.json52
-rw-r--r--src/client/themes/d-pumpkin.json588
-rw-r--r--src/client/themes/l-apricot.json52
-rw-r--r--src/client/themes/l-rainy.json52
-rw-r--r--src/client/themes/l-sushi.json518
-rw-r--r--src/client/themes/l-vivid.json52
-rw-r--r--src/client/ui/_common_/sidebar.vue2
-rw-r--r--src/client/ui/chat/post-form.vue6
-rw-r--r--src/client/ui/default.header.vue2
-rw-r--r--src/client/ui/default.sidebar.vue2
75 files changed, 1879 insertions, 1582 deletions
diff --git a/src/client/components/chart.vue b/src/client/components/chart.vue
new file mode 100644
index 0000000000..8eb9f93f33
--- /dev/null
+++ b/src/client/components/chart.vue
@@ -0,0 +1,628 @@
+<template>
+<canvas ref="chartEl"></canvas>
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, ref, watch, PropType } from 'vue';
+import {
+ Chart,
+ ArcElement,
+ LineElement,
+ BarElement,
+ PointElement,
+ BarController,
+ LineController,
+ CategoryScale,
+ LinearScale,
+ TimeScale,
+ Legend,
+ Title,
+ Tooltip,
+ SubTitle,
+ Filler,
+} from 'chart.js';
+import 'chartjs-adapter-date-fns';
+import { enUS } from 'date-fns/locale';
+import zoomPlugin from 'chartjs-plugin-zoom';
+import * as os from '@client/os';
+import { defaultStore } from '@client/store';
+
+Chart.register(
+ ArcElement,
+ LineElement,
+ BarElement,
+ PointElement,
+ BarController,
+ LineController,
+ CategoryScale,
+ LinearScale,
+ TimeScale,
+ Legend,
+ Title,
+ Tooltip,
+ SubTitle,
+ Filler,
+ zoomPlugin,
+);
+
+const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
+const negate = arr => arr.map(x => -x);
+const alpha = (hex, a) => {
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
+ const r = parseInt(result[1], 16);
+ const g = parseInt(result[2], 16);
+ const b = parseInt(result[3], 16);
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
+};
+
+const colors = ['#008FFB', '#00E396', '#FEB019', '#FF4560'];
+const getColor = (i) => {
+ return colors[i % colors.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
+ },
+ },
+
+ setup(props) {
+ const now = new Date();
+ let chartInstance: Chart = null;
+ let data: {
+ series: {
+ name: string;
+ type: 'line' | 'area';
+ color?: string;
+ borderDash?: number[];
+ hidden?: boolean;
+ data: {
+ x: number;
+ y: number;
+ }[];
+ }[];
+ } = null;
+
+ 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();
+
+ 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 render = () => {
+ if (chartInstance) {
+ chartInstance.destroy();
+ }
+
+ const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
+
+ // フォントカラー
+ Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
+
+ chartInstance = new Chart(chartEl.value, {
+ type: '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(),
+ pointRadius: 0,
+ tension: 0,
+ borderWidth: 2,
+ borderColor: x.color ? x.color : getColor(i),
+ borderDash: x.borderDash || [],
+ borderJoinStyle: 'round',
+ backgroundColor: alpha(x.color ? x.color : getColor(i), 0.1),
+ fill: x.type === 'area',
+ hidden: !!x.hidden,
+ })),
+ },
+ options: {
+ aspectRatio: 2.5,
+ layout: {
+ padding: {
+ left: 16,
+ right: 16,
+ top: 16,
+ bottom: 8,
+ },
+ },
+ scales: {
+ x: {
+ type: 'time',
+ time: {
+ stepSize: 1,
+ unit: props.span === 'day' ? 'month' : 'day',
+ },
+ grid: {
+ display: props.detailed,
+ color: gridColor,
+ borderColor: 'rgb(0, 0, 0, 0)',
+ },
+ ticks: {
+ display: props.detailed,
+ },
+ adapters: {
+ date: {
+ locale: enUS,
+ },
+ },
+ min: getDate(props.limit).getTime(),
+ },
+ y: {
+ position: 'left',
+ grid: {
+ color: gridColor,
+ borderColor: 'rgb(0, 0, 0, 0)',
+ },
+ ticks: {
+ display: props.detailed,
+ },
+ },
+ },
+ interaction: {
+ intersect: false,
+ },
+ plugins: {
+ legend: {
+ position: 'bottom',
+ labels: {
+ boxWidth: 16,
+ },
+ },
+ tooltip: {
+ mode: 'index',
+ animation: {
+ duration: 0,
+ },
+ },
+ zoom: {
+ 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',
+ },
+ }
+ },
+ },
+ },
+ });
+ };
+
+ const exportData = () => {
+ // TODO
+ };
+
+ const fetchFederationInstancesChart = async (total: boolean): Promise<typeof data> => {
+ const raw = await os.api('charts/federation', { limit: props.limit, span: props.span });
+ return {
+ series: [{
+ name: 'Instances',
+ type: 'area',
+ data: format(total
+ ? raw.instance.total
+ : sum(raw.instance.inc, negate(raw.instance.dec))
+ ),
+ }],
+ };
+ };
+
+ 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',
+ borderDash: [5, 5],
+ 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))
+ ),
+ }, {
+ name: 'Renotes',
+ type: 'area',
+ data: format(type == 'combined'
+ ? sum(raw.local.diffs.renote, raw.remote.diffs.renote)
+ : raw[type].diffs.renote
+ ),
+ }, {
+ name: 'Replies',
+ type: 'area',
+ data: format(type == 'combined'
+ ? sum(raw.local.diffs.reply, raw.remote.diffs.reply)
+ : raw[type].diffs.reply
+ ),
+ }, {
+ name: 'Normal',
+ type: 'area',
+ data: format(type == 'combined'
+ ? sum(raw.local.diffs.normal, raw.remote.diffs.normal)
+ : raw[type].diffs.normal
+ ),
+ }],
+ };
+ };
+
+ 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 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 fetchActiveUsersChart = async (): Promise<typeof data> => {
+ const raw = await os.api('charts/active-users', { limit: props.limit, span: props.span });
+ return {
+ series: [{
+ name: 'Combined',
+ type: 'line',
+ data: format(sum(raw.local.users, raw.remote.users)),
+ }, {
+ name: 'Local',
+ type: 'area',
+ data: format(raw.local.users),
+ }, {
+ name: 'Remote',
+ type: 'area',
+ data: format(raw.remote.users),
+ }],
+ };
+ };
+
+ 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',
+ borderDash: [5, 5],
+ 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 fetchDriveTotalChart = async (): Promise<typeof data> => {
+ const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
+ return {
+ bytes: true,
+ series: [{
+ name: 'Combined',
+ type: 'line',
+ data: format(sum(raw.local.totalSize, raw.remote.totalSize)),
+ }, {
+ name: 'Local',
+ type: 'area',
+ data: format(raw.local.totalSize),
+ }, {
+ name: 'Remote',
+ type: 'area',
+ data: format(raw.remote.totalSize),
+ }],
+ };
+ };
+
+ const fetchDriveFilesChart = async (): Promise<typeof data> => {
+ const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
+ return {
+ series: [{
+ name: 'All',
+ type: 'line',
+ borderDash: [5, 5],
+ 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 fetchDriveFilesTotalChart = async (): Promise<typeof data> => {
+ const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
+ return {
+ series: [{
+ name: 'Combined',
+ type: 'line',
+ data: format(sum(raw.local.totalCount, raw.remote.totalCount)),
+ }, {
+ name: 'Local',
+ type: 'area',
+ data: format(raw.local.totalCount),
+ }, {
+ name: 'Remote',
+ type: 'area',
+ data: format(raw.remote.totalCount),
+ }],
+ };
+ };
+
+ const fetchInstanceRequestsChart = async (): Promise<typeof data> => {
+ const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
+ return {
+ 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 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 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 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 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 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 fetchAndRender = async () => {
+ const fetchData = () => {
+ switch (props.src) {
+ case 'federation-instances': return fetchFederationInstancesChart(false);
+ case 'federation-instances-total': return fetchFederationInstancesChart(true);
+ 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-total': return fetchDriveTotalChart();
+ case 'drive-files': return fetchDriveFilesChart();
+ case 'drive-files-total': return fetchDriveFilesTotalChart();
+
+ case 'instance-requests': return fetchInstanceRequestsChart();
+ case 'instance-users': return fetchInstanceUsersChart(false);
+ 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);
+ }
+ };
+ fetching.value = true;
+ data = await fetchData();
+ fetching.value = false;
+ render();
+ };
+
+ watch(() => [props.src, props.span], fetchAndRender);
+
+ onMounted(() => {
+ fetchAndRender();
+ });
+
+ return {
+ chartEl,
+ };
+ },
+});
+</script>
diff --git a/src/client/components/form/input.vue b/src/client/components/form/input.vue
index d7b6f77519..591eda9ed5 100644
--- a/src/client/components/form/input.vue
+++ b/src/client/components/form/input.vue
@@ -33,7 +33,7 @@
<script lang="ts">
import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
-import MkButton from '../ui/button.vue';
+import MkButton from '@client/components/ui/button.vue';
import { debounce } from 'throttle-debounce';
export default defineComponent({
diff --git a/src/client/components/form/radios.vue b/src/client/components/form/radios.vue
index 1d3d80172a..998a738202 100644
--- a/src/client/components/form/radios.vue
+++ b/src/client/components/form/radios.vue
@@ -22,7 +22,6 @@ export default defineComponent({
}
},
render() {
- const label = this.$slots.desc();
let options = this.$slots.default();
// なぜかFragmentになることがあるため
@@ -31,7 +30,6 @@ export default defineComponent({
return h('div', {
class: 'novjtcto'
}, [
- h('div', { class: 'label' }, label),
...options.map(option => h(MkRadio, {
key: option.key,
value: option.props.value,
@@ -45,16 +43,6 @@ export default defineComponent({
<style lang="scss">
.novjtcto {
- > .label {
- font-size: 0.85em;
- padding: 0 0 8px 12px;
- user-select: none;
-
- &:empty {
- display: none;
- }
- }
-
&:first-child {
margin-top: 0;
}
diff --git a/src/client/components/form/select.vue b/src/client/components/form/select.vue
index 257e2cc990..30ccfd312b 100644
--- a/src/client/components/form/select.vue
+++ b/src/client/components/form/select.vue
@@ -1,9 +1,9 @@
<template>
<div class="vblkjoeq">
<div class="label" @click="focus"><slot name="label"></slot></div>
- <div class="input" :class="{ inline, disabled, focused }">
+ <div class="input" :class="{ inline, disabled, focused }" @click.prevent="onClick" ref="container">
<div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div>
- <select ref="inputEl"
+ <select class="select" ref="inputEl"
v-model="v"
:disabled="disabled"
:required="required"
@@ -25,7 +25,8 @@
<script lang="ts">
import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
-import MkButton from '../ui/button.vue';
+import MkButton from '@client/components/ui/button.vue';
+import * as os from '@client/os';
export default defineComponent({
components: {
@@ -81,6 +82,7 @@ export default defineComponent({
const inputEl = ref(null);
const prefixEl = ref(null);
const suffixEl = ref(null);
+ const container = ref(null);
const focus = () => inputEl.value.focus();
const onInput = (ev) => {
@@ -132,6 +134,47 @@ export default defineComponent({
});
});
+ const onClick = (ev: MouseEvent) => {
+ focused.value = true;
+
+ const menu = [];
+ let options = context.slots.default();
+
+ for (const optionOrOptgroup of options) {
+ if (optionOrOptgroup.type === 'optgroup') {
+ const optgroup = optionOrOptgroup;
+ menu.push({
+ type: 'label',
+ text: optgroup.props.label,
+ });
+ for (const option of optgroup.children) {
+ menu.push({
+ text: option.children,
+ active: v.value === option.props.value,
+ action: () => {
+ v.value = option.props.value;
+ },
+ });
+ }
+ } else {
+ const option = optionOrOptgroup;
+ menu.push({
+ text: option.children,
+ active: v.value === option.props.value,
+ action: () => {
+ v.value = option.props.value;
+ },
+ });
+ }
+ }
+
+ os.popupMenu(menu, container.value, {
+ width: container.value.offsetWidth,
+ }).then(() => {
+ focused.value = false;
+ });
+ };
+
return {
v,
focused,
@@ -141,8 +184,10 @@ export default defineComponent({
inputEl,
prefixEl,
suffixEl,
+ container,
focus,
onInput,
+ onClick,
updated,
};
},
@@ -174,8 +219,15 @@ export default defineComponent({
> .input {
$height: 42px;
position: relative;
+ cursor: pointer;
- > select {
+ &:hover {
+ > .select {
+ border-color: var(--inputBorderHover);
+ }
+ }
+
+ > .select {
appearance: none;
-webkit-appearance: none;
display: block;
@@ -195,10 +247,7 @@ export default defineComponent({
box-sizing: border-box;
cursor: pointer;
transition: border-color 0.1s ease-out;
-
- &:hover {
- border-color: var(--inputBorderHover);
- }
+ pointer-events: none;
}
> .prefix,
diff --git a/src/client/components/form/textarea.vue b/src/client/components/form/textarea.vue
index 50be69f930..048e9032df 100644
--- a/src/client/components/form/textarea.vue
+++ b/src/client/components/form/textarea.vue
@@ -26,7 +26,7 @@
<script lang="ts">
import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
-import MkButton from '../ui/button.vue';
+import MkButton from '@client/components/ui/button.vue';
import { debounce } from 'throttle-debounce';
export default defineComponent({
diff --git a/src/client/components/global/emoji.vue b/src/client/components/global/emoji.vue
index f4ebd5f3b3..f92e35c38f 100644
--- a/src/client/components/global/emoji.vue
+++ b/src/client/components/global/emoji.vue
@@ -27,8 +27,7 @@ export default defineComponent({
default: false
},
customEmojis: {
- required: false,
- default: () => []
+ required: false
},
isReaction: {
type: Boolean,
@@ -58,10 +57,7 @@ export default defineComponent({
},
ce() {
- let ce = [];
- if (this.customEmojis) ce = ce.concat(this.customEmojis);
- if (this.$instance && this.$instance.emojis) ce = ce.concat(this.$instance.emojis);
- return ce;
+ return this.customEmojis || this.$instance?.emojis || [];
}
},
diff --git a/src/client/components/global/header.vue b/src/client/components/global/header.vue
index a4466da498..2bf490c98a 100644
--- a/src/client/components/global/header.vue
+++ b/src/client/components/global/header.vue
@@ -203,6 +203,12 @@ export default defineComponent({
&.thin {
--height: 50px;
+
+ > .buttons {
+ > .button {
+ font-size: 0.9em;
+ }
+ }
}
&.slim {
diff --git a/src/client/components/instance-stats.vue b/src/client/components/instance-stats.vue
index 5e7c71ea65..fd0b75609f 100644
--- a/src/client/components/instance-stats.vue
+++ b/src/client/components/instance-stats.vue
@@ -24,35 +24,26 @@
<option value="drive-total">{{ $ts._charts.storageUsageTotal }}</option>
</optgroup>
</MkSelect>
- <MkSelect v-model="chartSpan" style="margin: 0;">
+ <MkSelect v-model="chartSpan" style="margin: 0 0 0 10px;">
<option value="hour">{{ $ts.perHour }}</option>
<option value="day">{{ $ts.perDay }}</option>
</MkSelect>
</div>
- <canvas ref="chart"></canvas>
+ <MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="detailed"></MkChart>
</div>
</template>
<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import Chart from 'chart.js';
-import MkSelect from './form/select.vue';
-import number from '@client/filters/number';
-
-const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
-const negate = arr => arr.map(x => -x);
-const alpha = (hex, a) => {
- const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
- const r = parseInt(result[1], 16);
- const g = parseInt(result[2], 16);
- const b = parseInt(result[3], 16);
- return `rgba(${r}, ${g}, ${b}, ${a})`;
-};
+import { defineComponent, onMounted, ref, watch } from 'vue';
+import MkSelect from '@client/components/form/select.vue';
+import MkChart from '@client/components/chart.vue';
import * as os from '@client/os';
+import { defaultStore } from '@client/store';
export default defineComponent({
components: {
- MkSelect
+ MkSelect,
+ MkChart,
},
props: {
@@ -68,463 +59,15 @@ export default defineComponent({
},
},
- data() {
- return {
- notesLocalWoW: 0,
- notesLocalDoD: 0,
- notesRemoteWoW: 0,
- notesRemoteDoD: 0,
- usersLocalWoW: 0,
- usersLocalDoD: 0,
- usersRemoteWoW: 0,
- usersRemoteDoD: 0,
- now: null,
- chart: null,
- chartInstance: null,
- chartSrc: 'notes',
- chartSpan: 'hour',
- }
- },
-
- computed: {
- data(): any {
- if (this.chart == null) return null;
- switch (this.chartSrc) {
- case 'federation-instances': return this.federationInstancesChart(false);
- case 'federation-instances-total': return this.federationInstancesChart(true);
- case 'users': return this.usersChart(false);
- case 'users-total': return this.usersChart(true);
- case 'active-users': return this.activeUsersChart();
- case 'notes': return this.notesChart('combined');
- case 'local-notes': return this.notesChart('local');
- case 'remote-notes': return this.notesChart('remote');
- case 'notes-total': return this.notesTotalChart();
- case 'drive': return this.driveChart();
- case 'drive-total': return this.driveTotalChart();
- case 'drive-files': return this.driveFilesChart();
- case 'drive-files-total': return this.driveFilesTotalChart();
- }
- },
-
- stats(): any[] {
- const stats =
- this.chartSpan == 'day' ? this.chart.perDay :
- this.chartSpan == 'hour' ? this.chart.perHour :
- null;
-
- return stats;
- }
- },
-
- watch: {
- chartSrc() {
- this.renderChart();
- },
-
- chartSpan() {
- this.renderChart();
- }
- },
-
- async created() {
- this.now = new Date();
+ setup() {
+ const chartSpan = ref<'hour' | 'day'>('hour');
+ const chartSrc = ref('notes');
- this.fetchChart();
+ return {
+ chartSrc,
+ chartSpan,
+ };
},
-
- methods: {
- async fetchChart() {
- const [perHour, perDay] = await Promise.all([Promise.all([
- os.api('charts/federation', { limit: this.chartLimit, span: 'hour' }),
- os.api('charts/users', { limit: this.chartLimit, span: 'hour' }),
- os.api('charts/active-users', { limit: this.chartLimit, span: 'hour' }),
- os.api('charts/notes', { limit: this.chartLimit, span: 'hour' }),
- os.api('charts/drive', { limit: this.chartLimit, span: 'hour' }),
- ]), Promise.all([
- os.api('charts/federation', { limit: this.chartLimit, span: 'day' }),
- os.api('charts/users', { limit: this.chartLimit, span: 'day' }),
- os.api('charts/active-users', { limit: this.chartLimit, span: 'day' }),
- os.api('charts/notes', { limit: this.chartLimit, span: 'day' }),
- os.api('charts/drive', { limit: this.chartLimit, span: 'day' }),
- ])]);
-
- const chart = {
- perHour: {
- federation: perHour[0],
- users: perHour[1],
- activeUsers: perHour[2],
- notes: perHour[3],
- drive: perHour[4],
- },
- perDay: {
- federation: perDay[0],
- users: perDay[1],
- activeUsers: perDay[2],
- notes: perDay[3],
- drive: perDay[4],
- }
- };
-
- this.chart = chart;
-
- this.renderChart();
- },
-
- renderChart() {
- if (this.chartInstance) {
- this.chartInstance.destroy();
- }
-
- // TODO: var(--panel)の色が暗いか明るいかで判定する
- const gridColor = this.$store.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
-
- Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
- this.chartInstance = markRaw(new Chart(this.$refs.chart, {
- type: 'line',
- data: {
- labels: new Array(this.chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(),
- datasets: this.data.series.map(x => ({
- label: x.name,
- data: x.data.slice().reverse(),
- pointRadius: 0,
- lineTension: 0,
- borderWidth: 2,
- borderColor: x.color,
- borderDash: x.borderDash || [],
- backgroundColor: alpha(x.color, 0.1),
- fill: x.fill == null ? true : x.fill,
- hidden: !!x.hidden
- }))
- },
- options: {
- aspectRatio: 2.5,
- layout: {
- padding: {
- left: 16,
- right: 16,
- top: 16,
- bottom: 8
- }
- },
- legend: {
- position: 'bottom',
- labels: {
- boxWidth: 16,
- }
- },
- scales: {
- xAxes: [{
- type: 'time',
- time: {
- stepSize: 1,
- unit: this.chartSpan == 'day' ? 'month' : 'day',
- },
- gridLines: {
- display: this.detailed,
- color: gridColor,
- zeroLineColor: gridColor,
- },
- ticks: {
- display: this.detailed
- }
- }],
- yAxes: [{
- position: 'left',
- gridLines: {
- color: gridColor,
- zeroLineColor: gridColor,
- },
- ticks: {
- display: this.detailed
- }
- }]
- },
- tooltips: {
- intersect: false,
- mode: 'index',
- }
- }
- }));
- },
-
- getDate(ago: number) {
- const y = this.now.getFullYear();
- const m = this.now.getMonth();
- const d = this.now.getDate();
- const h = this.now.getHours();
-
- return this.chartSpan == 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago);
- },
-
- format(arr) {
- const now = Date.now();
- return arr.map((v, i) => ({
- x: new Date(now - ((this.chartSpan == 'day' ? 86400000 :3600000 ) * i)),
- y: v
- }));
- },
-
- federationInstancesChart(total: boolean): any {
- return {
- series: [{
- name: 'Instances',
- color: '#008FFB',
- data: this.format(total
- ? this.stats.federation.instance.total
- : sum(this.stats.federation.instance.inc, negate(this.stats.federation.instance.dec))
- )
- }]
- };
- },
-
- notesChart(type: string): any {
- return {
- series: [{
- name: 'All',
- type: 'line',
- color: '#008FFB',
- borderDash: [5, 5],
- fill: false,
- data: this.format(type == 'combined'
- ? sum(this.stats.notes.local.inc, negate(this.stats.notes.local.dec), this.stats.notes.remote.inc, negate(this.stats.notes.remote.dec))
- : sum(this.stats.notes[type].inc, negate(this.stats.notes[type].dec))
- )
- }, {
- name: 'Renotes',
- type: 'area',
- color: '#00E396',
- data: this.format(type == 'combined'
- ? sum(this.stats.notes.local.diffs.renote, this.stats.notes.remote.diffs.renote)
- : this.stats.notes[type].diffs.renote
- )
- }, {
- name: 'Replies',
- type: 'area',
- color: '#FEB019',
- data: this.format(type == 'combined'
- ? sum(this.stats.notes.local.diffs.reply, this.stats.notes.remote.diffs.reply)
- : this.stats.notes[type].diffs.reply
- )
- }, {
- name: 'Normal',
- type: 'area',
- color: '#FF4560',
- data: this.format(type == 'combined'
- ? sum(this.stats.notes.local.diffs.normal, this.stats.notes.remote.diffs.normal)
- : this.stats.notes[type].diffs.normal
- )
- }]
- };
- },
-
- notesTotalChart(): any {
- return {
- series: [{
- name: 'Combined',
- type: 'line',
- color: '#008FFB',
- data: this.format(sum(this.stats.notes.local.total, this.stats.notes.remote.total))
- }, {
- name: 'Local',
- type: 'area',
- color: '#008FFB',
- hidden: true,
- data: this.format(this.stats.notes.local.total)
- }, {
- name: 'Remote',
- type: 'area',
- color: '#008FFB',
- hidden: true,
- data: this.format(this.stats.notes.remote.total)
- }]
- };
- },
-
- usersChart(total: boolean): any {
- return {
- series: [{
- name: 'Combined',
- type: 'line',
- color: '#008FFB',
- data: this.format(total
- ? sum(this.stats.users.local.total, this.stats.users.remote.total)
- : sum(this.stats.users.local.inc, negate(this.stats.users.local.dec), this.stats.users.remote.inc, negate(this.stats.users.remote.dec))
- )
- }, {
- name: 'Local',
- type: 'area',
- color: '#008FFB',
- hidden: true,
- data: this.format(total
- ? this.stats.users.local.total
- : sum(this.stats.users.local.inc, negate(this.stats.users.local.dec))
- )
- }, {
- name: 'Remote',
- type: 'area',
- color: '#008FFB',
- hidden: true,
- data: this.format(total
- ? this.stats.users.remote.total
- : sum(this.stats.users.remote.inc, negate(this.stats.users.remote.dec))
- )
- }]
- };
- },
-
- activeUsersChart(): any {
- return {
- series: [{
- name: 'Combined',
- type: 'line',
- color: '#008FFB',
- data: this.format(sum(this.stats.activeUsers.local.count, this.stats.activeUsers.remote.count))
- }, {
- name: 'Local',
- type: 'area',
- color: '#008FFB',
- hidden: true,
- data: this.format(this.stats.activeUsers.local.count)
- }, {
- name: 'Remote',
- type: 'area',
- color: '#008FFB',
- hidden: true,
- data: this.format(this.stats.activeUsers.remote.count)
- }]
- };
- },
-
- driveChart(): any {
- return {
- bytes: true,
- series: [{
- name: 'All',
- type: 'line',
- color: '#09d8e2',
- borderDash: [5, 5],
- fill: false,
- data: this.format(
- sum(
- this.stats.drive.local.incSize,
- negate(this.stats.drive.local.decSize),
- this.stats.drive.remote.incSize,
- negate(this.stats.drive.remote.decSize)
- )
- )
- }, {
- name: 'Local +',
- type: 'area',
- color: '#008FFB',
- data: this.format(this.stats.drive.local.incSize)
- }, {
- name: 'Local -',
- type: 'area',
- color: '#FF4560',
- data: this.format(negate(this.stats.drive.local.decSize))
- }, {
- name: 'Remote +',
- type: 'area',
- color: '#00E396',
- data: this.format(this.stats.drive.remote.incSize)
- }, {
- name: 'Remote -',
- type: 'area',
- color: '#FEB019',
- data: this.format(negate(this.stats.drive.remote.decSize))
- }]
- };
- },
-
- driveTotalChart(): any {
- return {
- bytes: true,
- series: [{
- name: 'Combined',
- type: 'line',
- color: '#008FFB',
- data: this.format(sum(this.stats.drive.local.totalSize, this.stats.drive.remote.totalSize))
- }, {
- name: 'Local',
- type: 'area',
- color: '#008FFB',
- hidden: true,
- data: this.format(this.stats.drive.local.totalSize)
- }, {
- name: 'Remote',
- type: 'area',
- color: '#008FFB',
- hidden: true,
- data: this.format(this.stats.drive.remote.totalSize)
- }]
- };
- },
-
- driveFilesChart(): any {
- return {
- series: [{
- name: 'All',
- type: 'line',
- color: '#09d8e2',
- borderDash: [5, 5],
- fill: false,
- data: this.format(
- sum(
- this.stats.drive.local.incCount,
- negate(this.stats.drive.local.decCount),
- this.stats.drive.remote.incCount,
- negate(this.stats.drive.remote.decCount)
- )
- )
- }, {
- name: 'Local +',
- type: 'area',
- color: '#008FFB',
- data: this.format(this.stats.drive.local.incCount)
- }, {
- name: 'Local -',
- type: 'area',
- color: '#FF4560',
- data: this.format(negate(this.stats.drive.local.decCount))
- }, {
- name: 'Remote +',
- type: 'area',
- color: '#00E396',
- data: this.format(this.stats.drive.remote.incCount)
- }, {
- name: 'Remote -',
- type: 'area',
- color: '#FEB019',
- data: this.format(negate(this.stats.drive.remote.decCount))
- }]
- };
- },
-
- driveFilesTotalChart(): any {
- return {
- series: [{
- name: 'Combined',
- type: 'line',
- color: '#008FFB',
- data: this.format(sum(this.stats.drive.local.totalCount, this.stats.drive.remote.totalCount))
- }, {
- name: 'Local',
- type: 'area',
- color: '#008FFB',
- hidden: true,
- data: this.format(this.stats.drive.local.totalCount)
- }, {
- name: 'Remote',
- type: 'area',
- color: '#008FFB',
- hidden: true,
- data: this.format(this.stats.drive.remote.totalCount)
- }]
- };
- },
-
- number
- }
});
</script>
diff --git a/src/client/components/mfm.ts b/src/client/components/mfm.ts
index 2bdd7d46ee..f3411cadc3 100644
--- a/src/client/components/mfm.ts
+++ b/src/client/components/mfm.ts
@@ -185,7 +185,7 @@ export default defineComponent({
}
}
if (style == null) {
- return h('span', {}, ['[', token.props.name, ' ', ...genEl(token.children), ']']);
+ return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children), ']']);
} else {
return h('span', {
style: 'display: inline-block;' + style,
diff --git a/src/client/components/number-diff.vue b/src/client/components/number-diff.vue
new file mode 100644
index 0000000000..690f89dd59
--- /dev/null
+++ b/src/client/components/number-diff.vue
@@ -0,0 +1,47 @@
+<template>
+<span class="ceaaebcd" :class="{ isPlus, isMinus, isZero }">
+ <slot name="before"></slot>{{ isPlus ? '+' : '' }}{{ number(value) }}<slot name="after"></slot>
+</span>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent } from 'vue';
+import number from '@client/filters/number';
+
+export default defineComponent({
+ props: {
+ value: {
+ type: Number,
+ required: true
+ },
+ },
+
+ setup(props) {
+ const isPlus = computed(() => props.value > 0);
+ const isMinus = computed(() => props.value < 0);
+ const isZero = computed(() => props.value === 0);
+ return {
+ isPlus,
+ isMinus,
+ isZero,
+ number,
+ };
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.ceaaebcd {
+ &.isPlus {
+ color: var(--success);
+ }
+
+ &.isMinus {
+ color: var(--error);
+ }
+
+ &.isZero {
+ opacity: 0.5;
+ }
+}
+</style>
diff --git a/src/client/components/post-form.vue b/src/client/components/post-form.vue
index a1d89d2a2e..816a69e731 100644
--- a/src/client/components/post-form.vue
+++ b/src/client/components/post-form.vue
@@ -117,11 +117,28 @@ export default defineComponent({
type: String,
required: false
},
+ initialVisibility: {
+ type: String,
+ required: false
+ },
+ initialFiles: {
+ type: Array,
+ required: false
+ },
+ initialLocalOnly: {
+ type: Boolean,
+ required: false
+ },
+ visibleUsers: {
+ type: Array,
+ required: false,
+ default: () => []
+ },
initialNote: {
type: Object,
required: false
},
- instant: {
+ share: {
type: Boolean,
required: false,
default: false
@@ -150,8 +167,7 @@ export default defineComponent({
showPreview: false,
cw: null,
localOnly: this.$store.state.rememberNoteVisibility ? this.$store.state.localOnly : this.$store.state.defaultNoteLocalOnly,
- visibility: this.$store.state.rememberNoteVisibility ? this.$store.state.visibility : this.$store.state.defaultNoteVisibility,
- visibleUsers: [],
+ visibility: (this.$store.state.rememberNoteVisibility ? this.$store.state.visibility : this.$store.state.defaultNoteVisibility) as typeof noteVisibilities[number],
autocomplete: null,
draghover: false,
quoteId: null,
@@ -246,6 +262,18 @@ export default defineComponent({
this.text = this.initialText;
}
+ if (this.initialVisibility) {
+ this.visibility = this.initialVisibility;
+ }
+
+ if (this.initialFiles) {
+ this.files = this.initialFiles;
+ }
+
+ if (typeof this.initialLocalOnly === 'boolean') {
+ this.localOnly = this.initialLocalOnly;
+ }
+
if (this.mention) {
this.text = this.mention.host ? `@${this.mention.username}@${toASCII(this.mention.host)}` : `@${this.mention.username}`;
this.text += ' ';
@@ -321,7 +349,7 @@ export default defineComponent({
this.$nextTick(() => {
// 書きかけの投稿を復元
- if (!this.instant && !this.mention && !this.specified) {
+ if (!this.share && !this.mention && !this.specified) {
const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftKey];
if (draft) {
this.text = draft.data.text;
@@ -582,8 +610,6 @@ export default defineComponent({
},
saveDraft() {
- if (this.instant) return;
-
const data = JSON.parse(localStorage.getItem('drafts') || '{}');
data[this.draftKey] = {
diff --git a/src/client/components/queue-chart.vue b/src/client/components/queue-chart.vue
new file mode 100644
index 0000000000..59c9723f89
--- /dev/null
+++ b/src/client/components/queue-chart.vue
@@ -0,0 +1,212 @@
+<template>
+<canvas ref="chartEl"></canvas>
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
+import {
+ Chart,
+ ArcElement,
+ LineElement,
+ BarElement,
+ PointElement,
+ BarController,
+ LineController,
+ CategoryScale,
+ LinearScale,
+ TimeScale,
+ Legend,
+ Title,
+ Tooltip,
+ SubTitle,
+ Filler,
+} from 'chart.js';
+import number from '@client/filters/number';
+import * as os from '@client/os';
+import { defaultStore } from '@client/store';
+
+Chart.register(
+ ArcElement,
+ LineElement,
+ BarElement,
+ PointElement,
+ BarController,
+ LineController,
+ CategoryScale,
+ LinearScale,
+ TimeScale,
+ Legend,
+ Title,
+ Tooltip,
+ SubTitle,
+ Filler,
+);
+
+const alpha = (hex, a) => {
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
+ const r = parseInt(result[1], 16);
+ const g = parseInt(result[2], 16);
+ const b = parseInt(result[3], 16);
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
+};
+
+export default defineComponent({
+ props: {
+ domain: {
+ type: String,
+ required: true,
+ },
+ connection: {
+ required: true,
+ },
+ },
+
+ setup(props) {
+ const chartEl = ref<HTMLCanvasElement>(null);
+
+ const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
+
+ // フォントカラー
+ Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
+
+ onMounted(() => {
+ const chartInstance = new Chart(chartEl.value, {
+ type: 'line',
+ data: {
+ labels: [],
+ datasets: [{
+ label: 'Process',
+ pointRadius: 0,
+ tension: 0,
+ borderWidth: 2,
+ borderColor: '#00E396',
+ backgroundColor: alpha('#00E396', 0.1),
+ data: []
+ }, {
+ label: 'Active',
+ pointRadius: 0,
+ tension: 0,
+ borderWidth: 2,
+ borderColor: '#00BCD4',
+ backgroundColor: alpha('#00BCD4', 0.1),
+ data: []
+ }, {
+ label: 'Waiting',
+ pointRadius: 0,
+ tension: 0,
+ borderWidth: 2,
+ borderColor: '#FFB300',
+ backgroundColor: alpha('#FFB300', 0.1),
+ data: []
+ }, {
+ label: 'Delayed',
+ pointRadius: 0,
+ tension: 0,
+ borderWidth: 2,
+ borderColor: '#E53935',
+ borderDash: [5, 5],
+ fill: false,
+ data: []
+ }],
+ },
+ options: {
+ aspectRatio: 2.5,
+ layout: {
+ padding: {
+ left: 16,
+ right: 16,
+ top: 16,
+ bottom: 8,
+ },
+ },
+ scales: {
+ x: {
+ grid: {
+ display: false,
+ color: gridColor,
+ borderColor: 'rgb(0, 0, 0, 0)',
+ },
+ ticks: {
+ display: false,
+ },
+ },
+ y: {
+ grid: {
+ color: gridColor,
+ borderColor: 'rgb(0, 0, 0, 0)',
+ },
+ },
+ },
+ interaction: {
+ intersect: false,
+ },
+ plugins: {
+ legend: {
+ position: 'bottom',
+ labels: {
+ boxWidth: 16,
+ },
+ },
+ tooltip: {
+ mode: 'index',
+ animation: {
+ duration: 0,
+ },
+ },
+ },
+ },
+ });
+
+ const onStats = (stats) => {
+ chartInstance.data.labels.push('');
+ chartInstance.data.datasets[0].data.push(stats[props.domain].activeSincePrevTick);
+ chartInstance.data.datasets[1].data.push(stats[props.domain].active);
+ chartInstance.data.datasets[2].data.push(stats[props.domain].waiting);
+ chartInstance.data.datasets[3].data.push(stats[props.domain].delayed);
+ if (chartInstance.data.datasets[0].data.length > 200) {
+ chartInstance.data.labels.shift();
+ chartInstance.data.datasets[0].data.shift();
+ chartInstance.data.datasets[1].data.shift();
+ chartInstance.data.datasets[2].data.shift();
+ chartInstance.data.datasets[3].data.shift();
+ }
+ chartInstance.update();
+ };
+
+ const onStatsLog = (statsLog) => {
+ for (const stats of [...statsLog].reverse()) {
+ chartInstance.data.labels.push('');
+ chartInstance.data.datasets[0].data.push(stats[props.domain].activeSincePrevTick);
+ chartInstance.data.datasets[1].data.push(stats[props.domain].active);
+ chartInstance.data.datasets[2].data.push(stats[props.domain].waiting);
+ chartInstance.data.datasets[3].data.push(stats[props.domain].delayed);
+ if (chartInstance.data.datasets[0].data.length > 200) {
+ chartInstance.data.labels.shift();
+ chartInstance.data.datasets[0].data.shift();
+ chartInstance.data.datasets[1].data.shift();
+ chartInstance.data.datasets[2].data.shift();
+ chartInstance.data.datasets[3].data.shift();
+ }
+ }
+ chartInstance.update();
+ };
+
+ props.connection.on('stats', onStats);
+ props.connection.on('statsLog', onStatsLog);
+
+ onUnmounted(() => {
+ props.connection.off('stats', onStats);
+ props.connection.off('statsLog', onStatsLog);
+ });
+ });
+
+ return {
+ chartEl,
+ }
+ },
+});
+</script>
+
+<style lang="scss" scoped>
+
+</style>
diff --git a/src/client/components/tab.vue b/src/client/components/tab.vue
index ce86af8f95..c629727358 100644
--- a/src/client/components/tab.vue
+++ b/src/client/components/tab.vue
@@ -36,7 +36,7 @@ export default defineComponent({
> button {
flex: 1;
padding: 10px 8px;
- border-radius: 6px;
+ border-radius: var(--radius);
&:disabled {
opacity: 1 !important;
diff --git a/src/client/components/ui/container.vue b/src/client/components/ui/container.vue
index 1940099096..14673dfcd7 100644
--- a/src/client/components/ui/container.vue
+++ b/src/client/components/ui/container.vue
@@ -1,5 +1,5 @@
<template>
-<div class="ukygtjoj _block" :class="{ naked, hideHeader: !showHeader, scrollable, closed: !showBody }" v-size="{ max: [380] }">
+<div class="ukygtjoj _panel" :class="{ naked, thin, hideHeader: !showHeader, scrollable, closed: !showBody }" v-size="{ max: [380] }">
<header v-if="showHeader" ref="header">
<div class="title"><slot name="header"></slot></div>
<div class="sub">
@@ -36,6 +36,11 @@ export default defineComponent({
required: false,
default: true
},
+ thin: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
naked: {
type: Boolean,
required: false,
@@ -226,7 +231,7 @@ export default defineComponent({
}
}
- &.max-width_380px {
+ &.max-width_380px, &.thin {
> header {
> .title {
padding: 8px 10px;
diff --git a/src/client/components/ui/menu.vue b/src/client/components/ui/menu.vue
index da24d90170..aaef527f1a 100644
--- a/src/client/components/ui/menu.vue
+++ b/src/client/components/ui/menu.vue
@@ -1,5 +1,6 @@
<template>
<div class="rrevdjwt" :class="{ center: align === 'center' }"
+ :style="{ width: width ? width + 'px' : null }"
ref="items"
@contextmenu.self="e => e.preventDefault()"
v-hotkey="keymap"
@@ -59,6 +60,10 @@ export default defineComponent({
type: String,
requried: false
},
+ width: {
+ type: Number,
+ required: false
+ },
},
emits: ['close'],
data() {
diff --git a/src/client/components/ui/popup-menu.vue b/src/client/components/ui/popup-menu.vue
index 23f7c89f3b..3ff4c658b1 100644
--- a/src/client/components/ui/popup-menu.vue
+++ b/src/client/components/ui/popup-menu.vue
@@ -1,6 +1,6 @@
<template>
<MkPopup ref="popup" :src="src" @closed="$emit('closed')">
- <MkMenu :items="items" :align="align" @close="$refs.popup.close()" class="_popup _shadow"/>
+ <MkMenu :items="items" :align="align" :width="width" @close="$refs.popup.close()" class="_popup _shadow"/>
</MkPopup>
</template>
@@ -24,6 +24,10 @@ export default defineComponent({
type: String,
required: false
},
+ width: {
+ type: Number,
+ required: false
+ },
viaKeyboard: {
type: Boolean,
required: false
diff --git a/src/client/components/ui/super-menu.vue b/src/client/components/ui/super-menu.vue
index 35fc81550d..6ab94d744d 100644
--- a/src/client/components/ui/super-menu.vue
+++ b/src/client/components/ui/super-menu.vue
@@ -120,7 +120,7 @@ export default defineComponent({
> .items {
display: grid;
- grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
grid-gap: 8px;
padding: 0 16px;
diff --git a/src/client/os.ts b/src/client/os.ts
index 7ae774dd92..743d2d131f 100644
--- a/src/client/os.ts
+++ b/src/client/os.ts
@@ -372,12 +372,17 @@ export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea:
});
}
-export function popupMenu(items: any[] | Ref<any[]>, src?: HTMLElement, options?: { align?: string; viaKeyboard?: boolean }) {
+export function popupMenu(items: any[] | Ref<any[]>, src?: HTMLElement, options?: {
+ align?: string;
+ width?: number;
+ viaKeyboard?: boolean;
+}) {
return new Promise((resolve, reject) => {
let dispose;
popup(import('@client/components/ui/popup-menu.vue'), {
items,
src,
+ width: options?.width,
align: options?.align,
viaKeyboard: options?.viaKeyboard
}, {
diff --git a/src/client/pages/instance/abuses.vue b/src/client/pages/admin/abuses.vue
index 29da8cc2c5..29da8cc2c5 100644
--- a/src/client/pages/instance/abuses.vue
+++ b/src/client/pages/admin/abuses.vue
diff --git a/src/client/pages/instance/ads.vue b/src/client/pages/admin/ads.vue
index e776f99a4c..e776f99a4c 100644
--- a/src/client/pages/instance/ads.vue
+++ b/src/client/pages/admin/ads.vue
diff --git a/src/client/pages/instance/announcements.vue b/src/client/pages/admin/announcements.vue
index 78637c095a..78637c095a 100644
--- a/src/client/pages/instance/announcements.vue
+++ b/src/client/pages/admin/announcements.vue
diff --git a/src/client/pages/instance/bot-protection.vue b/src/client/pages/admin/bot-protection.vue
index 731f114cc2..731f114cc2 100644
--- a/src/client/pages/instance/bot-protection.vue
+++ b/src/client/pages/admin/bot-protection.vue
diff --git a/src/client/pages/instance/database.vue b/src/client/pages/admin/database.vue
index ffbeed8b30..ffbeed8b30 100644
--- a/src/client/pages/instance/database.vue
+++ b/src/client/pages/admin/database.vue
diff --git a/src/client/pages/instance/email-settings.vue b/src/client/pages/admin/email-settings.vue
index ebf724fcdd..ebf724fcdd 100644
--- a/src/client/pages/instance/email-settings.vue
+++ b/src/client/pages/admin/email-settings.vue
diff --git a/src/client/pages/instance/emoji-edit-dialog.vue b/src/client/pages/admin/emoji-edit-dialog.vue
index 4854c69884..4854c69884 100644
--- a/src/client/pages/instance/emoji-edit-dialog.vue
+++ b/src/client/pages/admin/emoji-edit-dialog.vue
diff --git a/src/client/pages/instance/emojis.vue b/src/client/pages/admin/emojis.vue
index 4cd34b046d..4cd34b046d 100644
--- a/src/client/pages/instance/emojis.vue
+++ b/src/client/pages/admin/emojis.vue
diff --git a/src/client/pages/instance/file-dialog.vue b/src/client/pages/admin/file-dialog.vue
index 02d83e5022..02d83e5022 100644
--- a/src/client/pages/instance/file-dialog.vue
+++ b/src/client/pages/admin/file-dialog.vue
diff --git a/src/client/pages/instance/files-settings.vue b/src/client/pages/admin/files-settings.vue
index 8aefa9e90d..8aefa9e90d 100644
--- a/src/client/pages/instance/files-settings.vue
+++ b/src/client/pages/admin/files-settings.vue
diff --git a/src/client/pages/instance/files.vue b/src/client/pages/admin/files.vue
index 55189cfd84..55189cfd84 100644
--- a/src/client/pages/instance/files.vue
+++ b/src/client/pages/admin/files.vue
diff --git a/src/client/pages/instance/index.vue b/src/client/pages/admin/index.vue
index 7b07bf2dde..416e68206c 100644
--- a/src/client/pages/instance/index.vue
+++ b/src/client/pages/admin/index.vue
@@ -7,8 +7,8 @@
<img :src="$instance.iconUrl || '/favicon.ico'" alt="" class="icon"/>
</div>
- <MkInfo v-if="noMaintainerInformation" warn class="info">{{ $ts.noMaintainerInformationWarning }} <MkA to="/instance/settings" class="_link">{{ $ts.configure }}</MkA></MkInfo>
- <MkInfo v-if="noBotProtection" warn class="info">{{ $ts.noBotProtectionWarning }} <MkA to="/instance/bot-protection" class="_link">{{ $ts.configure }}</MkA></MkInfo>
+ <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/bot-protection" class="_link">{{ $ts.configure }}</MkA></MkInfo>
<MkSuperMenu :def="menuDef" :grid="page == null"></MkSuperMenu>
</div>
@@ -93,47 +93,47 @@ export default defineComponent({
items: [{
icon: 'fas fa-tachometer-alt',
text: i18n.locale.dashboard,
- to: '/instance/overview',
+ to: '/admin/overview',
active: page.value === 'overview',
}, {
icon: 'fas fa-users',
text: i18n.locale.users,
- to: '/instance/users',
+ to: '/admin/users',
active: page.value === 'users',
}, {
icon: 'fas fa-laugh',
text: i18n.locale.customEmojis,
- to: '/instance/emojis',
+ to: '/admin/emojis',
active: page.value === 'emojis',
}, {
icon: 'fas fa-globe',
text: i18n.locale.federation,
- to: '/instance/federation',
+ to: '/admin/federation',
active: page.value === 'federation',
}, {
icon: 'fas fa-clipboard-list',
text: i18n.locale.jobQueue,
- to: '/instance/queue',
+ to: '/admin/queue',
active: page.value === 'queue',
}, {
icon: 'fas fa-cloud',
text: i18n.locale.files,
- to: '/instance/files',
+ to: '/admin/files',
active: page.value === 'files',
}, {
icon: 'fas fa-broadcast-tower',
text: i18n.locale.announcements,
- to: '/instance/announcements',
+ to: '/admin/announcements',
active: page.value === 'announcements',
}, {
icon: 'fas fa-audio-description',
text: i18n.locale.ads,
- to: '/instance/ads',
+ to: '/admin/ads',
active: page.value === 'ads',
}, {
icon: 'fas fa-exclamation-circle',
text: i18n.locale.abuseReports,
- to: '/instance/abuses',
+ to: '/admin/abuses',
active: page.value === 'abuses',
}],
}, {
@@ -141,57 +141,57 @@ export default defineComponent({
items: [{
icon: 'fas fa-cog',
text: i18n.locale.general,
- to: '/instance/settings',
+ to: '/admin/settings',
active: page.value === 'settings',
}, {
icon: 'fas fa-cloud',
text: i18n.locale.files,
- to: '/instance/files-settings',
+ to: '/admin/files-settings',
active: page.value === 'files-settings',
}, {
icon: 'fas fa-envelope',
text: i18n.locale.emailServer,
- to: '/instance/email-settings',
+ to: '/admin/email-settings',
active: page.value === 'email-settings',
}, {
icon: 'fas fa-cloud',
text: i18n.locale.objectStorage,
- to: '/instance/object-storage',
+ to: '/admin/object-storage',
active: page.value === 'object-storage',
}, {
icon: 'fas fa-lock',
text: i18n.locale.security,
- to: '/instance/security',
+ to: '/admin/security',
active: page.value === 'security',
}, {
icon: 'fas fa-bolt',
text: 'ServiceWorker',
- to: '/instance/service-worker',
+ to: '/admin/service-worker',
active: page.value === 'service-worker',
}, {
icon: 'fas fa-globe',
text: i18n.locale.relays,
- to: '/instance/relays',
+ to: '/admin/relays',
active: page.value === 'relays',
}, {
icon: 'fas fa-share-alt',
text: i18n.locale.integration,
- to: '/instance/integrations',
+ to: '/admin/integrations',
active: page.value === 'integrations',
}, {
icon: 'fas fa-ban',
text: i18n.locale.instanceBlocking,
- to: '/instance/instance-block',
+ to: '/admin/instance-block',
active: page.value === 'instance-block',
}, {
icon: 'fas fa-ghost',
text: i18n.locale.proxyAccount,
- to: '/instance/proxy-account',
+ to: '/admin/proxy-account',
active: page.value === 'proxy-account',
}, {
icon: 'fas fa-cogs',
text: i18n.locale.other,
- to: '/instance/other-settings',
+ to: '/admin/other-settings',
active: page.value === 'other-settings',
}],
}, {
@@ -199,13 +199,8 @@ export default defineComponent({
items: [{
icon: 'fas fa-database',
text: i18n.locale.database,
- to: '/instance/database',
+ to: '/admin/database',
active: page.value === 'database',
- }, {
- icon: 'fas fa-stream',
- text: i18n.locale.logs,
- to: '/instance/logs',
- active: page.value === 'logs',
}],
}]);
const component = computed(() => {
@@ -220,7 +215,6 @@ export default defineComponent({
case 'announcements': return defineAsyncComponent(() => import('./announcements.vue'));
case 'ads': return defineAsyncComponent(() => import('./ads.vue'));
case 'database': return defineAsyncComponent(() => import('./database.vue'));
- case 'logs': return defineAsyncComponent(() => import('./logs.vue'));
case 'abuses': return defineAsyncComponent(() => import('./abuses.vue'));
case 'settings': return defineAsyncComponent(() => import('./settings.vue'));
case 'files-settings': return defineAsyncComponent(() => import('./files-settings.vue'));
diff --git a/src/client/pages/instance/instance-block.vue b/src/client/pages/admin/instance-block.vue
index 105cdb4941..105cdb4941 100644
--- a/src/client/pages/instance/instance-block.vue
+++ b/src/client/pages/admin/instance-block.vue
diff --git a/src/client/pages/instance/instance.vue b/src/client/pages/admin/instance.vue
index 6117f090de..5572fbbf75 100644
--- a/src/client/pages/instance/instance.vue
+++ b/src/client/pages/admin/instance.vue
@@ -78,17 +78,17 @@
<span class="label">{{ $ts.charts }}</span>
<div class="selects">
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
- <option value="requests">{{ $ts._instanceCharts.requests }}</option>
- <option value="users">{{ $ts._instanceCharts.users }}</option>
- <option value="users-total">{{ $ts._instanceCharts.usersTotal }}</option>
- <option value="notes">{{ $ts._instanceCharts.notes }}</option>
- <option value="notes-total">{{ $ts._instanceCharts.notesTotal }}</option>
- <option value="ff">{{ $ts._instanceCharts.ff }}</option>
- <option value="ff-total">{{ $ts._instanceCharts.ffTotal }}</option>
- <option value="drive-usage">{{ $ts._instanceCharts.cacheSize }}</option>
- <option value="drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option>
- <option value="drive-files">{{ $ts._instanceCharts.files }}</option>
- <option value="drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option>
+ <option value="instance-requests">{{ $ts._instanceCharts.requests }}</option>
+ <option value="instance-users">{{ $ts._instanceCharts.users }}</option>
+ <option value="instance-users-total">{{ $ts._instanceCharts.usersTotal }}</option>
+ <option value="instance-notes">{{ $ts._instanceCharts.notes }}</option>
+ <option value="instance-notes-total">{{ $ts._instanceCharts.notesTotal }}</option>
+ <option value="instance-ff">{{ $ts._instanceCharts.ff }}</option>
+ <option value="instance-ff-total">{{ $ts._instanceCharts.ffTotal }}</option>
+ <option value="instance-drive-usage">{{ $ts._instanceCharts.cacheSize }}</option>
+ <option value="instance-drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option>
+ <option value="instance-drive-files">{{ $ts._instanceCharts.files }}</option>
+ <option value="instance-drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option>
</MkSelect>
<MkSelect v-model="chartSpan" style="margin: 0;">
<option value="hour">{{ $ts.perHour }}</option>
@@ -97,7 +97,7 @@
</div>
</div>
<div class="chart">
- <canvas :ref="setChart"></canvas>
+ <MkChart :src="chartSrc" :span="chartSpan" :limit="90" :detailed="true"></MkChart>
</div>
</div>
<div class="operations section">
@@ -124,28 +124,17 @@
<script lang="ts">
import { defineComponent, markRaw } from 'vue';
-import Chart from 'chart.js';
import XModalWindow from '@client/components/ui/modal-window.vue';
import MkUsersDialog from '@client/components/users-dialog.vue';
import MkSelect from '@client/components/form/select.vue';
import MkButton from '@client/components/ui/button.vue';
import MkSwitch from '@client/components/form/switch.vue';
import MkInfo from '@client/components/ui/info.vue';
+import MkChart from '@client/components/chart.vue';
import bytes from '@client/filters/bytes';
import number from '@client/filters/number';
import * as os from '@client/os';
-const chartLimit = 90;
-const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
-const negate = arr => arr.map(x => -x);
-const alpha = hex => {
- const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
- const r = parseInt(result[1], 16);
- const g = parseInt(result[2], 16);
- const b = parseInt(result[3], 16);
- return `rgba(${r}, ${g}, ${b}, 0.1)`;
-};
-
export default defineComponent({
components: {
XModalWindow,
@@ -153,6 +142,7 @@ export default defineComponent({
MkButton,
MkSwitch,
MkInfo,
+ MkChart,
},
props: {
@@ -167,42 +157,12 @@ export default defineComponent({
data() {
return {
isSuspended: this.instance.isSuspended,
- now: null,
- canvas: null,
- chart: null,
- chartInstance: null,
chartSrc: 'requests',
chartSpan: 'hour',
};
},
computed: {
- data(): any {
- if (this.chart == null) return null;
- switch (this.chartSrc) {
- case 'requests': return this.requestsChart();
- case 'users': return this.usersChart(false);
- case 'users-total': return this.usersChart(true);
- case 'notes': return this.notesChart(false);
- case 'notes-total': return this.notesChart(true);
- case 'ff': return this.ffChart(false);
- case 'ff-total': return this.ffChart(true);
- case 'drive-usage': return this.driveUsageChart(false);
- case 'drive-usage-total': return this.driveUsageChart(true);
- case 'drive-files': return this.driveFilesChart(false);
- case 'drive-files-total': return this.driveFilesChart(true);
- }
- },
-
- stats(): any[] {
- const stats =
- this.chartSpan == 'day' ? this.chart.perDay :
- this.chartSpan == 'hour' ? this.chart.perHour :
- null;
-
- return stats;
- },
-
meta() {
return this.$instance;
},
@@ -219,49 +179,15 @@ export default defineComponent({
isSuspended: this.isSuspended
});
},
-
- chartSrc() {
- this.renderChart();
- },
-
- chartSpan() {
- this.renderChart();
- }
- },
-
- async created() {
- this.now = new Date();
-
- const [perHour, perDay] = await Promise.all([
- os.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'hour' }),
- os.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'day' }),
- ]);
-
- const chart = {
- perHour: perHour,
- perDay: perDay
- };
-
- this.chart = chart;
-
- this.renderChart();
},
methods: {
- setChart(el) {
- this.canvas = el;
- },
-
changeBlock(e) {
os.api('admin/update-meta', {
blockedHosts: this.isBlocked ? this.meta.blockedHosts.concat([this.instance.host]) : this.meta.blockedHosts.filter(x => x !== this.instance.host)
});
},
- setSrc(src) {
- this.chartSrc = src;
- },
-
removeAllFollowing() {
os.apiWithDialog('admin/federation/remove-all-following', {
host: this.instance.host
@@ -274,170 +200,6 @@ export default defineComponent({
});
},
- renderChart() {
- if (this.chartInstance) {
- this.chartInstance.destroy();
- }
-
- Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
- this.chartInstance = markRaw(new Chart(this.canvas, {
- type: 'line',
- data: {
- labels: new Array(chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(),
- datasets: this.data.series.map(x => ({
- label: x.name,
- data: x.data.slice().reverse(),
- pointRadius: 0,
- lineTension: 0,
- borderWidth: 2,
- borderColor: x.color,
- backgroundColor: alpha(x.color),
- }))
- },
- options: {
- aspectRatio: 2.5,
- layout: {
- padding: {
- left: 16,
- right: 16,
- top: 16,
- bottom: 0
- }
- },
- legend: {
- position: 'bottom',
- labels: {
- boxWidth: 16,
- }
- },
- scales: {
- xAxes: [{
- gridLines: {
- display: false
- },
- ticks: {
- display: false
- }
- }],
- yAxes: [{
- position: 'right',
- ticks: {
- display: false
- }
- }]
- },
- tooltips: {
- intersect: false,
- mode: 'index',
- }
- }
- }));
- },
-
- getDate(ago: number) {
- const y = this.now.getFullYear();
- const m = this.now.getMonth();
- const d = this.now.getDate();
- const h = this.now.getHours();
-
- return this.chartSpan == 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago);
- },
-
- format(arr) {
- return arr;
- },
-
- requestsChart(): any {
- return {
- series: [{
- name: 'In',
- color: '#008FFB',
- data: this.format(this.stats.requests.received)
- }, {
- name: 'Out (succ)',
- color: '#00E396',
- data: this.format(this.stats.requests.succeeded)
- }, {
- name: 'Out (fail)',
- color: '#FEB019',
- data: this.format(this.stats.requests.failed)
- }]
- };
- },
-
- usersChart(total: boolean): any {
- return {
- series: [{
- name: 'Users',
- color: '#008FFB',
- data: this.format(total
- ? this.stats.users.total
- : sum(this.stats.users.inc, negate(this.stats.users.dec))
- )
- }]
- };
- },
-
- notesChart(total: boolean): any {
- return {
- series: [{
- name: 'Notes',
- color: '#008FFB',
- data: this.format(total
- ? this.stats.notes.total
- : sum(this.stats.notes.inc, negate(this.stats.notes.dec))
- )
- }]
- };
- },
-
- ffChart(total: boolean): any {
- return {
- series: [{
- name: 'Following',
- color: '#008FFB',
- data: this.format(total
- ? this.stats.following.total
- : sum(this.stats.following.inc, negate(this.stats.following.dec))
- )
- }, {
- name: 'Followers',
- color: '#00E396',
- data: this.format(total
- ? this.stats.followers.total
- : sum(this.stats.followers.inc, negate(this.stats.followers.dec))
- )
- }]
- };
- },
-
- driveUsageChart(total: boolean): any {
- return {
- bytes: true,
- series: [{
- name: 'Drive usage',
- color: '#008FFB',
- data: this.format(total
- ? this.stats.drive.totalUsage
- : sum(this.stats.drive.incUsage, negate(this.stats.drive.decUsage))
- )
- }]
- };
- },
-
- driveFilesChart(total: boolean): any {
- return {
- series: [{
- name: 'Drive files',
- color: '#008FFB',
- data: this.format(total
- ? this.stats.drive.totalFiles
- : sum(this.stats.drive.incFiles, negate(this.stats.drive.decFiles))
- )
- }]
- };
- },
-
showFollowing() {
os.modal(MkUsersDialog, {
title: this.$ts.instanceFollowing,
diff --git a/src/client/pages/instance/integrations-discord.vue b/src/client/pages/admin/integrations-discord.vue
index c33b24f17f..c33b24f17f 100644
--- a/src/client/pages/instance/integrations-discord.vue
+++ b/src/client/pages/admin/integrations-discord.vue
diff --git a/src/client/pages/instance/integrations-github.vue b/src/client/pages/admin/integrations-github.vue
index cdf85868ff..cdf85868ff 100644
--- a/src/client/pages/instance/integrations-github.vue
+++ b/src/client/pages/admin/integrations-github.vue
diff --git a/src/client/pages/instance/integrations-twitter.vue b/src/client/pages/admin/integrations-twitter.vue
index ed7d097d0a..ed7d097d0a 100644
--- a/src/client/pages/instance/integrations-twitter.vue
+++ b/src/client/pages/admin/integrations-twitter.vue
diff --git a/src/client/pages/instance/integrations.vue b/src/client/pages/admin/integrations.vue
index 6964ae5704..bdc2cec4d0 100644
--- a/src/client/pages/instance/integrations.vue
+++ b/src/client/pages/admin/integrations.vue
@@ -1,15 +1,15 @@
<template>
<FormBase>
<FormSuspense :p="init">
- <FormLink to="/instance/integrations/twitter">
+ <FormLink to="/admin/integrations/twitter">
<i class="fab fa-twitter"></i> Twitter
<template #suffix>{{ enableTwitterIntegration ? $ts.enabled : $ts.disabled }}</template>
</FormLink>
- <FormLink to="/instance/integrations/github">
+ <FormLink to="/admin/integrations/github">
<i class="fab fa-github"></i> GitHub
<template #suffix>{{ enableGithubIntegration ? $ts.enabled : $ts.disabled }}</template>
</FormLink>
- <FormLink to="/instance/integrations/discord">
+ <FormLink to="/admin/integrations/discord">
<i class="fab fa-discord"></i> Discord
<template #suffix>{{ enableDiscordIntegration ? $ts.enabled : $ts.disabled }}</template>
</FormLink>
diff --git a/src/client/pages/instance/metrics.vue b/src/client/pages/admin/metrics.vue
index 1606063aee..da36f6c688 100644
--- a/src/client/pages/instance/metrics.vue
+++ b/src/client/pages/admin/metrics.vue
@@ -52,7 +52,21 @@
<script lang="ts">
import { defineComponent, markRaw } from 'vue';
-import Chart from 'chart.js';
+import {
+ Chart,
+ ArcElement,
+ LineElement,
+ BarElement,
+ PointElement,
+ BarController,
+ LineController,
+ CategoryScale,
+ LinearScale,
+ Legend,
+ Title,
+ Tooltip,
+ SubTitle
+} from 'chart.js';
import MkButton from '@client/components/ui/button.vue';
import MkSelect from '@client/components/form/select.vue';
import MkInput from '@client/components/form/input.vue';
@@ -64,6 +78,21 @@ import bytes from '@client/filters/bytes';
import number from '@client/filters/number';
import MkInstanceInfo from './instance.vue';
+Chart.register(
+ ArcElement,
+ LineElement,
+ BarElement,
+ PointElement,
+ BarController,
+ LineController,
+ CategoryScale,
+ LinearScale,
+ Legend,
+ Title,
+ Tooltip,
+ SubTitle
+);
+
const alpha = (hex, a) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
const r = parseInt(result[1], 16);
@@ -116,7 +145,7 @@ export default defineComponent({
mounted() {
this.fetchJobs();
- Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
+ Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
os.api('admin/server-info', {}).then(res => {
this.serverInfo = res;
@@ -157,7 +186,7 @@ export default defineComponent({
datasets: [{
label: 'CPU',
pointRadius: 0,
- lineTension: 0,
+ tension: 0,
borderWidth: 2,
borderColor: '#86b300',
backgroundColor: alpha('#86b300', 0.1),
@@ -165,7 +194,7 @@ export default defineComponent({
}, {
label: 'MEM (active)',
pointRadius: 0,
- lineTension: 0,
+ tension: 0,
borderWidth: 2,
borderColor: '#935dbf',
backgroundColor: alpha('#935dbf', 0.02),
@@ -173,7 +202,7 @@ export default defineComponent({
}, {
label: 'MEM (used)',
pointRadius: 0,
- lineTension: 0,
+ tension: 0,
borderWidth: 2,
borderColor: '#935dbf',
borderDash: [5, 5],
@@ -198,7 +227,7 @@ export default defineComponent({
}
},
scales: {
- xAxes: [{
+ x: {
gridLines: {
display: false,
color: this.gridColor,
@@ -207,8 +236,8 @@ export default defineComponent({
ticks: {
display: false,
}
- }],
- yAxes: [{
+ },
+ y: {
position: 'right',
gridLines: {
display: true,
@@ -219,7 +248,7 @@ export default defineComponent({
display: false,
max: 100
}
- }]
+ }
},
tooltips: {
intersect: false,
@@ -238,7 +267,7 @@ export default defineComponent({
datasets: [{
label: 'In',
pointRadius: 0,
- lineTension: 0,
+ tension: 0,
borderWidth: 2,
borderColor: '#94a029',
backgroundColor: alpha('#94a029', 0.1),
@@ -246,7 +275,7 @@ export default defineComponent({
}, {
label: 'Out',
pointRadius: 0,
- lineTension: 0,
+ tension: 0,
borderWidth: 2,
borderColor: '#ff9156',
backgroundColor: alpha('#ff9156', 0.1),
@@ -270,7 +299,7 @@ export default defineComponent({
}
},
scales: {
- xAxes: [{
+ x: {
gridLines: {
display: false,
color: this.gridColor,
@@ -279,8 +308,8 @@ export default defineComponent({
ticks: {
display: false
}
- }],
- yAxes: [{
+ },
+ y: {
position: 'right',
gridLines: {
display: true,
@@ -290,7 +319,7 @@ export default defineComponent({
ticks: {
display: false,
}
- }]
+ }
},
tooltips: {
intersect: false,
@@ -309,7 +338,7 @@ export default defineComponent({
datasets: [{
label: 'Read',
pointRadius: 0,
- lineTension: 0,
+ tension: 0,
borderWidth: 2,
borderColor: '#94a029',
backgroundColor: alpha('#94a029', 0.1),
@@ -317,7 +346,7 @@ export default defineComponent({
}, {
label: 'Write',
pointRadius: 0,
- lineTension: 0,
+ tension: 0,
borderWidth: 2,
borderColor: '#ff9156',
backgroundColor: alpha('#ff9156', 0.1),
@@ -341,7 +370,7 @@ export default defineComponent({
}
},
scales: {
- xAxes: [{
+ x: {
gridLines: {
display: false,
color: this.gridColor,
@@ -350,8 +379,8 @@ export default defineComponent({
ticks: {
display: false
}
- }],
- yAxes: [{
+ },
+ y: {
position: 'right',
gridLines: {
display: true,
@@ -361,7 +390,7 @@ export default defineComponent({
ticks: {
display: false,
}
- }]
+ }
},
tooltips: {
intersect: false,
@@ -371,18 +400,6 @@ export default defineComponent({
}));
},
- async showInstanceInfo(q) {
- let instance = q;
- if (typeof q === 'string') {
- instance = await os.api('federation/show-instance', {
- host: q
- });
- }
- os.popup(MkInstanceInfo, {
- instance: instance
- }, {}, 'closed');
- },
-
fetchJobs() {
os.api('admin/queue/deliver-delayed', {}).then(jobs => {
this.jobs = jobs;
diff --git a/src/client/pages/instance/object-storage.vue b/src/client/pages/admin/object-storage.vue
index 2d765270e6..2d765270e6 100644
--- a/src/client/pages/instance/object-storage.vue
+++ b/src/client/pages/admin/object-storage.vue
diff --git a/src/client/pages/instance/other-settings.vue b/src/client/pages/admin/other-settings.vue
index 4e55df41fb..4e55df41fb 100644
--- a/src/client/pages/instance/other-settings.vue
+++ b/src/client/pages/admin/other-settings.vue
diff --git a/src/client/pages/admin/overview.vue b/src/client/pages/admin/overview.vue
new file mode 100644
index 0000000000..bb9c10f106
--- /dev/null
+++ b/src/client/pages/admin/overview.vue
@@ -0,0 +1,242 @@
+<template>
+<div>
+ <MkHeader :info="header"/>
+
+ <div class="edbbcaef" v-size="{ max: [880] }">
+ <div v-if="stats" class="cfcdecdf" style="margin: var(--margin)">
+ <div class="number _panel">
+ <div class="label">Users</div>
+ <div class="value _monospace">
+ {{ number(stats.originalUsersCount) }}
+ <MkNumberDiff v-if="usersComparedToThePrevDay != null" class="diff" :value="usersComparedToThePrevDay" v-tooltip="$ts.dayOverDayChanges"><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" class="diff" :value="notesComparedToThePrevDay" v-tooltip="$ts.dayOverDayChanges"><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>
+ <div style="padding-top: 12px;">
+ <MkInstanceStats :chart-limit="500" :detailed="true"/>
+ </div>
+ </MkContainer>
+
+ <div class="queue">
+ <MkContainer :foldable="true" :thin="true" class="deliver">
+ <template #header>Queue: deliver</template>
+ <MkQueueChart :connection="queueStatsConnection" domain="deliver"/>
+ </MkContainer>
+ <MkContainer :foldable="true" :thin="true" class="inbox">
+ <template #header>Queue: inbox</template>
+ <MkQueueChart :connection="queueStatsConnection" domain="inbox"/>
+ </MkContainer>
+ </div>
+
+ <!--<XMetrics/>-->
+
+ <MkFolder style="margin: var(--margin)">
+ <template #header><i class="fas fa-info-circle"></i> {{ $ts.info }}</template>
+ <div class="cfcdecdf">
+ <div class="number _panel">
+ <div class="label">Misskey</div>
+ <div class="value _monospace">{{ version }}</div>
+ </div>
+ <div class="number _panel" v-if="serverInfo">
+ <div class="label">Node.js</div>
+ <div class="value _monospace">{{ serverInfo.node }}</div>
+ </div>
+ <div class="number _panel" v-if="serverInfo">
+ <div class="label">PostgreSQL</div>
+ <div class="value _monospace">{{ serverInfo.psql }}</div>
+ </div>
+ <div class="number _panel" v-if="serverInfo">
+ <div class="label">Redis</div>
+ <div class="value _monospace">{{ serverInfo.redis }}</div>
+ </div>
+ <div class="number _panel">
+ <div class="label">Vue</div>
+ <div class="value _monospace">{{ vueVersion }}</div>
+ </div>
+ </div>
+ </MkFolder>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, markRaw, version as vueVersion } from 'vue';
+import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
+import MkInstanceStats from '@client/components/instance-stats.vue';
+import MkButton from '@client/components/ui/button.vue';
+import MkSelect from '@client/components/form/select.vue';
+import MkNumberDiff from '@client/components/number-diff.vue';
+import MkContainer from '@client/components/ui/container.vue';
+import MkFolder from '@client/components/ui/folder.vue';
+import MkQueueChart from '@client/components/queue-chart.vue';
+import { version, url } from '@client/config';
+import bytes from '@client/filters/bytes';
+import number from '@client/filters/number';
+import MkInstanceInfo from './instance.vue';
+import XMetrics from './metrics.vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+
+export default defineComponent({
+ components: {
+ MkNumberDiff,
+ FormKeyValueView,
+ MkInstanceStats,
+ MkContainer,
+ MkFolder,
+ MkQueueChart,
+ XMetrics,
+ },
+
+ emits: ['info'],
+
+ data() {
+ return {
+ [symbols.PAGE_INFO]: {
+ title: this.$ts.dashboard,
+ icon: 'fas fa-tachometer-alt',
+ bg: 'var(--bg)',
+ },
+ header: {
+ title: this.$ts.dashboard,
+ icon: 'fas fa-tachometer-alt',
+ },
+ 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(os.stream.useChannel('queueStats')),
+ }
+ },
+
+ async mounted() {
+ this.$emit('info', this[symbols.PAGE_INFO]);
+
+ os.api('meta', { detail: true }).then(meta => {
+ this.meta = meta;
+ });
+
+ 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('admin/server-info', {}).then(serverInfo => {
+ this.serverInfo = serverInfo;
+ });
+
+ this.$nextTick(() => {
+ this.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
+ });
+ }
+ os.popup(MkInstanceInfo, {
+ instance: instance
+ }, {}, 'closed');
+ },
+
+ bytes,
+
+ number,
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.edbbcaef {
+ .cfcdecdf {
+ display: grid;
+ grid-gap: 8px;
+ grid-template-columns: repeat(auto-fill,minmax(150px,1fr));
+
+ > .number {
+ padding: 12px 16px;
+
+ > .label {
+ opacity: 0.7;
+ font-size: 0.8em;
+ }
+
+ > .value {
+ font-weight: bold;
+ font-size: 1.2em;
+
+ > .diff {
+ font-size: 0.8em;
+ }
+ }
+ }
+ }
+
+ > .charts {
+ margin: var(--margin);
+ }
+
+ > .queue {
+ margin: var(--margin);
+ display: flex;
+
+ > .deliver,
+ > .inbox {
+ flex: 1;
+ width: 50%;
+
+ &:not(:first-child) {
+ margin-left: var(--margin);
+ }
+ }
+ }
+
+ &.max-width_800px {
+ > .queue {
+ display: block;
+
+ > .deliver,
+ > .inbox {
+ &:not(:first-child) {
+ margin-top: var(--margin);
+ margin-left: 0;
+ }
+ }
+ }
+ }
+}
+</style>
diff --git a/src/client/pages/instance/proxy-account.vue b/src/client/pages/admin/proxy-account.vue
index b1ece19710..b1ece19710 100644
--- a/src/client/pages/instance/proxy-account.vue
+++ b/src/client/pages/admin/proxy-account.vue
diff --git a/src/client/pages/admin/queue.chart.vue b/src/client/pages/admin/queue.chart.vue
new file mode 100644
index 0000000000..084181a606
--- /dev/null
+++ b/src/client/pages/admin/queue.chart.vue
@@ -0,0 +1,102 @@
+<template>
+<div class="_debobigegoItem">
+ <div class="_debobigegoLabel"><slot name="title"></slot></div>
+ <div class="_debobigegoPanel pumxzjhg">
+ <div class="_table status">
+ <div class="_row">
+ <div class="_cell"><div class="_label">Process</div>{{ number(activeSincePrevTick) }}</div>
+ <div class="_cell"><div class="_label">Active</div>{{ number(active) }}</div>
+ <div class="_cell"><div class="_label">Waiting</div>{{ number(waiting) }}</div>
+ <div class="_cell"><div class="_label">Delayed</div>{{ number(delayed) }}</div>
+ </div>
+ </div>
+ <div class="">
+ <MkQueueChart :domain="domain" :connection="connection"/>
+ </div>
+ <div class="jobs">
+ <div v-if="jobs.length > 0">
+ <div v-for="job in jobs" :key="job[0]">
+ <span>{{ job[0] }}</span>
+ <span style="margin-left: 8px; opacity: 0.7;">({{ number(job[1]) }} jobs)</span>
+ </div>
+ </div>
+ <span v-else style="opacity: 0.5;">{{ $ts.noJobs }}</span>
+ </div>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, markRaw, onMounted, onUnmounted, ref } from 'vue';
+import number from '@client/filters/number';
+import MkQueueChart from '@client/components/queue-chart.vue';
+import * as os from '@client/os';
+
+export default defineComponent({
+ components: {
+ MkQueueChart
+ },
+
+ props: {
+ domain: {
+ type: String,
+ required: true,
+ },
+ connection: {
+ required: true,
+ },
+ },
+
+ 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(jobs => {
+ jobs.value = jobs;
+ });
+
+ 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);
+
+ onUnmounted(() => {
+ props.connection.off('stats', onStats);
+ });
+ });
+
+ return {
+ jobs,
+ activeSincePrevTick,
+ active,
+ waiting,
+ delayed,
+ number,
+ };
+ },
+});
+</script>
+
+<style lang="scss" scoped>
+.pumxzjhg {
+ > .status {
+ padding: 16px;
+ border-bottom: solid 0.5px var(--divider);
+ }
+
+ > .jobs {
+ padding: 16px;
+ border-top: solid 0.5px var(--divider);
+ max-height: 180px;
+ overflow: auto;
+ }
+}
+</style>
diff --git a/src/client/pages/instance/queue.vue b/src/client/pages/admin/queue.vue
index f88825eb19..f88825eb19 100644
--- a/src/client/pages/instance/queue.vue
+++ b/src/client/pages/admin/queue.vue
diff --git a/src/client/pages/instance/relays.vue b/src/client/pages/admin/relays.vue
index 7d7888eaa8..7d7888eaa8 100644
--- a/src/client/pages/instance/relays.vue
+++ b/src/client/pages/admin/relays.vue
diff --git a/src/client/pages/instance/security.vue b/src/client/pages/admin/security.vue
index a854b6dbd0..4365b6800c 100644
--- a/src/client/pages/instance/security.vue
+++ b/src/client/pages/admin/security.vue
@@ -1,7 +1,7 @@
<template>
<FormBase>
<FormSuspense :p="init">
- <FormLink to="/instance/bot-protection">
+ <FormLink to="/admin/bot-protection">
<i class="fas fa-shield-alt"></i> {{ $ts.botProtection }}
<template #suffix v-if="enableHcaptcha">hCaptcha</template>
<template #suffix v-else-if="enableRecaptcha">reCAPTCHA</template>
diff --git a/src/client/pages/instance/service-worker.vue b/src/client/pages/admin/service-worker.vue
index 430e02ad2e..430e02ad2e 100644
--- a/src/client/pages/instance/service-worker.vue
+++ b/src/client/pages/admin/service-worker.vue
diff --git a/src/client/pages/instance/settings.vue b/src/client/pages/admin/settings.vue
index 7bd363e5f3..7bd363e5f3 100644
--- a/src/client/pages/instance/settings.vue
+++ b/src/client/pages/admin/settings.vue
diff --git a/src/client/pages/instance/users.vue b/src/client/pages/admin/users.vue
index f7f9306b70..f7f9306b70 100644
--- a/src/client/pages/instance/users.vue
+++ b/src/client/pages/admin/users.vue
diff --git a/src/client/pages/explore.vue b/src/client/pages/explore.vue
index 2ca0668611..6f304877b7 100644
--- a/src/client/pages/explore.vue
+++ b/src/client/pages/explore.vue
@@ -65,13 +65,18 @@
</div>
<div v-else-if="tab === 'search'">
<div class="_isolated">
- <MkInput v-model="query" :debounce="true" type="search">
+ <MkInput v-model="searchQuery" :debounce="true" type="search">
<template #prefix><i class="fas fa-search"></i></template>
<template #label>{{ $ts.searchUser }}</template>
</MkInput>
+ <MkRadios v-model="searchOrigin">
+ <option value="local">{{ $ts.local }}</option>
+ <option value="remote">{{ $ts.remote }}</option>
+ <option value="both">{{ $ts.both }}</option>
+ </MkRadios>
</div>
- <XUserList v-if="query" class="_gap" :pagination="searchPagination" ref="search"/>
+ <XUserList v-if="searchQuery" class="_gap" :pagination="searchPagination" ref="search"/>
</div>
</div>
</MkSpacer>
@@ -83,6 +88,7 @@ import { computed, defineComponent } from 'vue';
import XUserList from '@client/components/user-list.vue';
import MkFolder from '@client/components/ui/folder.vue';
import MkInput from '@client/components/form/input.vue';
+import MkRadios from '@client/components/form/radios.vue';
import number from '@client/filters/number';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
@@ -92,6 +98,7 @@ export default defineComponent({
XUserList,
MkFolder,
MkInput,
+ MkRadios,
},
props: {
@@ -158,14 +165,16 @@ export default defineComponent({
searchPagination: {
endpoint: 'users/search',
limit: 10,
- params: computed(() => (this.query && this.query !== '') ? {
- query: this.query
+ params: computed(() => (this.searchQuery && this.searchQuery !== '') ? {
+ query: this.searchQuery,
+ origin: this.searchOrigin,
} : null)
},
tagsLocal: [],
tagsRemote: [],
stats: null,
- query: null,
+ searchQuery: null,
+ searchOrigin: 'combined',
num: number,
};
},
diff --git a/src/client/pages/instance-info.vue b/src/client/pages/instance-info.vue
index 4fbf104f0c..291ceb5dfd 100644
--- a/src/client/pages/instance-info.vue
+++ b/src/client/pages/instance-info.vue
@@ -65,17 +65,17 @@
<div class="_debobigegoPanel cmhjzshl">
<div class="selects">
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
- <option value="requests">{{ $ts._instanceCharts.requests }}</option>
- <option value="users">{{ $ts._instanceCharts.users }}</option>
- <option value="users-total">{{ $ts._instanceCharts.usersTotal }}</option>
- <option value="notes">{{ $ts._instanceCharts.notes }}</option>
- <option value="notes-total">{{ $ts._instanceCharts.notesTotal }}</option>
- <option value="ff">{{ $ts._instanceCharts.ff }}</option>
- <option value="ff-total">{{ $ts._instanceCharts.ffTotal }}</option>
- <option value="drive-usage">{{ $ts._instanceCharts.cacheSize }}</option>
- <option value="drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option>
- <option value="drive-files">{{ $ts._instanceCharts.files }}</option>
- <option value="drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option>
+ <option value="instance-requests">{{ $ts._instanceCharts.requests }}</option>
+ <option value="instance-users">{{ $ts._instanceCharts.users }}</option>
+ <option value="instance-users-total">{{ $ts._instanceCharts.usersTotal }}</option>
+ <option value="instance-notes">{{ $ts._instanceCharts.notes }}</option>
+ <option value="instance-notes-total">{{ $ts._instanceCharts.notesTotal }}</option>
+ <option value="instance-ff">{{ $ts._instanceCharts.ff }}</option>
+ <option value="instance-ff-total">{{ $ts._instanceCharts.ffTotal }}</option>
+ <option value="instance-drive-usage">{{ $ts._instanceCharts.cacheSize }}</option>
+ <option value="instance-drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option>
+ <option value="instance-drive-files">{{ $ts._instanceCharts.files }}</option>
+ <option value="instance-drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option>
</MkSelect>
<MkSelect v-model="chartSpan" style="margin: 0;">
<option value="hour">{{ $ts.perHour }}</option>
@@ -83,7 +83,7 @@
</MkSelect>
</div>
<div class="chart">
- <canvas :ref="setChart"></canvas>
+ <MkChart :src="chartSrc" :span="chartSpan" :limit="90" :detailed="true"></MkChart>
</div>
</div>
</div>
@@ -135,7 +135,7 @@
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
-import Chart from 'chart.js';
+import MkChart from '@client/components/chart.vue';
import FormObjectView from '@client/components/debobigego/object-view.vue';
import FormTextarea from '@client/components/debobigego/textarea.vue';
import FormLink from '@client/components/debobigego/link.vue';
@@ -149,18 +149,7 @@ import * as os from '@client/os';
import number from '@client/filters/number';
import bytes from '@client/filters/bytes';
import * as symbols from '@client/symbols';
-import MkInstanceInfo from '@client/pages/instance/instance.vue';
-
-const chartLimit = 90;
-const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
-const negate = arr => arr.map(x => -x);
-const alpha = hex => {
- const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
- const r = parseInt(result[1], 16);
- const g = parseInt(result[2], 16);
- const b = parseInt(result[3], 16);
- return `rgba(${r}, ${g}, ${b}, 0.1)`;
-};
+import MkInstanceInfo from '@client/pages/admin/instance.vue';
export default defineComponent({
components: {
@@ -173,6 +162,7 @@ export default defineComponent({
FormKeyValueView,
FormSuspense,
MkSelect,
+ MkChart,
},
props: {
@@ -199,53 +189,11 @@ export default defineComponent({
dnsPromiseFactory: () => os.api('federation/dns', {
host: this.host
}),
- now: null,
- canvas: null,
- chart: null,
- chartInstance: null,
- chartSrc: 'requests',
+ chartSrc: 'instance-requests',
chartSpan: 'hour',
}
},
- computed: {
- data(): any {
- if (this.chart == null) return null;
- switch (this.chartSrc) {
- case 'requests': return this.requestsChart();
- case 'users': return this.usersChart(false);
- case 'users-total': return this.usersChart(true);
- case 'notes': return this.notesChart(false);
- case 'notes-total': return this.notesChart(true);
- case 'ff': return this.ffChart(false);
- case 'ff-total': return this.ffChart(true);
- case 'drive-usage': return this.driveUsageChart(false);
- case 'drive-usage-total': return this.driveUsageChart(true);
- case 'drive-files': return this.driveFilesChart(false);
- case 'drive-files-total': return this.driveFilesChart(true);
- }
- },
-
- stats(): any[] {
- const stats =
- this.chartSpan == 'day' ? this.chart.perDay :
- this.chartSpan == 'hour' ? this.chart.perHour :
- null;
-
- return stats;
- },
- },
-
- watch: {
- chartSrc() {
- this.renderChart();
- },
-
- chartSpan() {
- this.renderChart();
- }
- },
-
mounted() {
this.fetch();
},
@@ -258,190 +206,6 @@ export default defineComponent({
this.instance = await os.api('federation/show-instance', {
host: this.host
});
-
- this.now = new Date();
-
- const [perHour, perDay] = await Promise.all([
- os.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'hour' }),
- os.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'day' }),
- ]);
-
- const chart = {
- perHour: perHour,
- perDay: perDay
- };
-
- this.chart = chart;
-
- this.renderChart();
- },
-
- setChart(el) {
- this.canvas = el;
- },
-
- renderChart() {
- if (this.chartInstance) {
- this.chartInstance.destroy();
- }
-
- Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
- this.chartInstance = new Chart(this.canvas, {
- type: 'line',
- data: {
- labels: new Array(chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(),
- datasets: this.data.series.map(x => ({
- label: x.name,
- data: x.data.slice().reverse(),
- pointRadius: 0,
- lineTension: 0,
- borderWidth: 2,
- borderColor: x.color,
- backgroundColor: alpha(x.color),
- }))
- },
- options: {
- aspectRatio: 2.5,
- layout: {
- padding: {
- left: 16,
- right: 16,
- top: 16,
- bottom: 16
- }
- },
- legend: {
- position: 'bottom',
- labels: {
- boxWidth: 16,
- }
- },
- scales: {
- xAxes: [{
- gridLines: {
- display: false
- },
- ticks: {
- display: false
- }
- }],
- yAxes: [{
- position: 'right',
- ticks: {
- display: false
- }
- }]
- },
- tooltips: {
- intersect: false,
- mode: 'index',
- }
- }
- });
- },
-
- getDate(ago: number) {
- const y = this.now.getFullYear();
- const m = this.now.getMonth();
- const d = this.now.getDate();
- const h = this.now.getHours();
-
- return this.chartSpan == 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago);
- },
-
- format(arr) {
- return arr;
- },
-
- requestsChart(): any {
- return {
- series: [{
- name: 'In',
- color: '#008FFB',
- data: this.format(this.stats.requests.received)
- }, {
- name: 'Out (succ)',
- color: '#00E396',
- data: this.format(this.stats.requests.succeeded)
- }, {
- name: 'Out (fail)',
- color: '#FEB019',
- data: this.format(this.stats.requests.failed)
- }]
- };
- },
-
- usersChart(total: boolean): any {
- return {
- series: [{
- name: 'Users',
- color: '#008FFB',
- data: this.format(total
- ? this.stats.users.total
- : sum(this.stats.users.inc, negate(this.stats.users.dec))
- )
- }]
- };
- },
-
- notesChart(total: boolean): any {
- return {
- series: [{
- name: 'Notes',
- color: '#008FFB',
- data: this.format(total
- ? this.stats.notes.total
- : sum(this.stats.notes.inc, negate(this.stats.notes.dec))
- )
- }]
- };
- },
-
- ffChart(total: boolean): any {
- return {
- series: [{
- name: 'Following',
- color: '#008FFB',
- data: this.format(total
- ? this.stats.following.total
- : sum(this.stats.following.inc, negate(this.stats.following.dec))
- )
- }, {
- name: 'Followers',
- color: '#00E396',
- data: this.format(total
- ? this.stats.followers.total
- : sum(this.stats.followers.inc, negate(this.stats.followers.dec))
- )
- }]
- };
- },
-
- driveUsageChart(total: boolean): any {
- return {
- bytes: true,
- series: [{
- name: 'Drive usage',
- color: '#008FFB',
- data: this.format(total
- ? this.stats.drive.totalUsage
- : sum(this.stats.drive.incUsage, negate(this.stats.drive.decUsage))
- )
- }]
- };
- },
-
- driveFilesChart(total: boolean): any {
- return {
- series: [{
- name: 'Drive files',
- color: '#008FFB',
- data: this.format(total
- ? this.stats.drive.totalFiles
- : sum(this.stats.drive.incFiles, negate(this.stats.drive.decFiles))
- )
- }]
- };
},
info() {
diff --git a/src/client/pages/instance/logs.vue b/src/client/pages/instance/logs.vue
deleted file mode 100644
index 74aea0fc45..0000000000
--- a/src/client/pages/instance/logs.vue
+++ /dev/null
@@ -1,97 +0,0 @@
-<template>
-<div class="_section">
- <div class="_inputs">
- <MkInput v-model="logDomain" :debounce="true">
- <template #label>{{ $ts.domain }}</template>
- </MkInput>
- <MkSelect v-model="logLevel">
- <template #label>Level</template>
- <option value="all">All</option>
- <option value="info">Info</option>
- <option value="success">Success</option>
- <option value="warning">Warning</option>
- <option value="error">Error</option>
- <option value="debug">Debug</option>
- </MkSelect>
- </div>
-
- <div class="logs">
- <code v-for="log in logs" :key="log.id" :class="log.level">
- <details>
- <summary><MkTime :time="log.createdAt"/> [{{ log.domain.join('.') }}] {{ log.message }}</summary>
- <!--<vue-json-pretty v-if="log.data" :data="log.data"></vue-json-pretty>-->
- </details>
- </code>
- </div>
-
- <MkButton @click="deleteAllLogs()" primary><i class="fas fa-trash-alt"></i> {{ $ts.deleteAll }}</MkButton>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/form/input.vue';
-import MkSelect from '@client/components/form/select.vue';
-import MkTextarea from '@client/components/form/textarea.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
- components: {
- MkButton,
- MkInput,
- MkSelect,
- MkTextarea,
- },
-
- emits: ['info'],
-
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.$ts.serverLogs,
- icon: 'fas fa-stream'
- },
- logs: [],
- logLevel: 'all',
- logDomain: '',
- }
- },
-
- watch: {
- logLevel() {
- this.logs = [];
- this.fetchLogs();
- },
- logDomain() {
- this.logs = [];
- this.fetchLogs();
- }
- },
-
- created() {
- this.fetchLogs();
- },
-
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
- methods: {
- fetchLogs() {
- os.api('admin/logs', {
- level: this.logLevel === 'all' ? null : this.logLevel,
- domain: this.logDomain === '' ? null : this.logDomain,
- limit: 30
- }).then(logs => {
- this.logs = logs.reverse();
- });
- },
-
- deleteAllLogs() {
- os.apiWithDialog('admin/delete-logs');
- },
- }
-});
-</script>
diff --git a/src/client/pages/instance/overview.vue b/src/client/pages/instance/overview.vue
deleted file mode 100644
index c6db9d0c04..0000000000
--- a/src/client/pages/instance/overview.vue
+++ /dev/null
@@ -1,127 +0,0 @@
-<template>
-<FormBase>
- <FormSuspense :p="init">
- <FormSuspense :p="fetchStats" v-slot="{ result: stats }">
- <FormGroup>
- <FormKeyValueView>
- <template #key>Users</template>
- <template #value>{{ number(stats.originalUsersCount) }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
- <template #key>Notes</template>
- <template #value>{{ number(stats.originalNotesCount) }}</template>
- </FormKeyValueView>
- </FormGroup>
- </FormSuspense>
-
- <div class="_debobigegoItem">
- <div class="_debobigegoPanel">
- <MkInstanceStats :chart-limit="300" :detailed="true"/>
- </div>
- </div>
-
- <XMetrics/>
-
- <FormSuspense :p="fetchServerInfo" v-slot="{ result: serverInfo }">
- <FormGroup>
- <FormKeyValueView>
- <template #key>Node.js</template>
- <template #value>{{ serverInfo.node }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
- <template #key>PostgreSQL</template>
- <template #value>{{ serverInfo.psql }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
- <template #key>Redis</template>
- <template #value>{{ serverInfo.redis }}</template>
- </FormKeyValueView>
- </FormGroup>
- </FormSuspense>
- </FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent, markRaw } from 'vue';
-import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
-import FormInput from '@client/components/debobigego/input.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormTextarea from '@client/components/debobigego/textarea.vue';
-import FormInfo from '@client/components/debobigego/info.vue';
-import FormSuspense from '@client/components/debobigego/suspense.vue';
-import MkInstanceStats from '@client/components/instance-stats.vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkSelect from '@client/components/form/select.vue';
-import MkInput from '@client/components/form/input.vue';
-import MkContainer from '@client/components/ui/container.vue';
-import MkFolder from '@client/components/ui/folder.vue';
-import { version, url } from '@client/config';
-import bytes from '@client/filters/bytes';
-import number from '@client/filters/number';
-import MkInstanceInfo from './instance.vue';
-import XMetrics from './metrics.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
- components: {
- FormBase,
- FormSuspense,
- FormGroup,
- FormInfo,
- FormKeyValueView,
- MkInstanceStats,
- XMetrics,
- },
-
- emits: ['info'],
-
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.$ts.overview,
- icon: 'fas fa-tachometer-alt',
- bg: 'var(--bg)',
- },
- page: 'index',
- version,
- url,
- stats: null,
- meta: null,
- fetchStats: () => os.api('stats', {}),
- fetchServerInfo: () => os.api('admin/server-info', {}),
- fetchJobs: () => os.api('admin/queue/deliver-delayed', {}),
- fetchModLogs: () => os.api('admin/show-moderation-logs', {}),
- }
- },
-
- async mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
- methods: {
- async init() {
- this.meta = await os.api('meta', { detail: true });
- },
-
- async showInstanceInfo(q) {
- let instance = q;
- if (typeof q === 'string') {
- instance = await os.api('federation/show-instance', {
- host: q
- });
- }
- os.popup(MkInstanceInfo, {
- instance: instance
- }, {}, 'closed');
- },
-
- bytes,
-
- number,
- }
-});
-</script>
diff --git a/src/client/pages/instance/queue.chart.vue b/src/client/pages/instance/queue.chart.vue
deleted file mode 100644
index 887fe9a574..0000000000
--- a/src/client/pages/instance/queue.chart.vue
+++ /dev/null
@@ -1,218 +0,0 @@
-<template>
-<div class="_debobigegoItem">
- <div class="_debobigegoLabel"><slot name="title"></slot></div>
- <div class="_debobigegoPanel pumxzjhg">
- <div class="_table status">
- <div class="_row">
- <div class="_cell"><div class="_label">Process</div>{{ number(activeSincePrevTick) }}</div>
- <div class="_cell"><div class="_label">Active</div>{{ number(active) }}</div>
- <div class="_cell"><div class="_label">Waiting</div>{{ number(waiting) }}</div>
- <div class="_cell"><div class="_label">Delayed</div>{{ number(delayed) }}</div>
- </div>
- </div>
- <div class="">
- <canvas ref="chart"></canvas>
- </div>
- <div class="jobs">
- <div v-if="jobs.length > 0">
- <div v-for="job in jobs" :key="job[0]">
- <span>{{ job[0] }}</span>
- <span style="margin-left: 8px; opacity: 0.7;">({{ number(job[1]) }} jobs)</span>
- </div>
- </div>
- <span v-else style="opacity: 0.5;">{{ $ts.noJobs }}</span>
- </div>
- </div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import Chart from 'chart.js';
-import number from '@client/filters/number';
-
-const alpha = (hex, a) => {
- const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
- const r = parseInt(result[1], 16);
- const g = parseInt(result[2], 16);
- const b = parseInt(result[3], 16);
- return `rgba(${r}, ${g}, ${b}, ${a})`;
-};
-import * as os from '@client/os';
-
-export default defineComponent({
- props: {
- domain: {
- required: true
- },
- connection: {
- required: true
- },
- },
-
- data() {
- return {
- chart: null,
- jobs: [],
- activeSincePrevTick: 0,
- active: 0,
- waiting: 0,
- delayed: 0,
- }
- },
-
- mounted() {
- this.fetchJobs();
-
- // TODO: var(--panel)の色が暗いか明るいかで判定する
- const gridColor = this.$store.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
-
- Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
-
- this.chart = markRaw(new Chart(this.$refs.chart, {
- type: 'line',
- data: {
- labels: [],
- datasets: [{
- label: 'Process',
- pointRadius: 0,
- lineTension: 0,
- borderWidth: 2,
- borderColor: '#00E396',
- backgroundColor: alpha('#00E396', 0.1),
- data: []
- }, {
- label: 'Active',
- pointRadius: 0,
- lineTension: 0,
- borderWidth: 2,
- borderColor: '#00BCD4',
- backgroundColor: alpha('#00BCD4', 0.1),
- data: []
- }, {
- label: 'Waiting',
- pointRadius: 0,
- lineTension: 0,
- borderWidth: 2,
- borderColor: '#FFB300',
- backgroundColor: alpha('#FFB300', 0.1),
- data: []
- }, {
- label: 'Delayed',
- pointRadius: 0,
- lineTension: 0,
- borderWidth: 2,
- borderColor: '#E53935',
- borderDash: [5, 5],
- fill: false,
- data: []
- }]
- },
- options: {
- aspectRatio: 3,
- layout: {
- padding: {
- left: 16,
- right: 16,
- top: 16,
- bottom: 12
- }
- },
- legend: {
- position: 'bottom',
- labels: {
- boxWidth: 16,
- }
- },
- scales: {
- xAxes: [{
- gridLines: {
- display: false,
- color: gridColor,
- zeroLineColor: gridColor,
- },
- ticks: {
- display: false
- }
- }],
- yAxes: [{
- position: 'right',
- gridLines: {
- display: true,
- color: gridColor,
- zeroLineColor: gridColor,
- },
- ticks: {
- display: false,
- }
- }]
- },
- tooltips: {
- intersect: false,
- mode: 'index',
- }
- }
- }));
-
- this.connection.on('stats', this.onStats);
- this.connection.on('statsLog', this.onStatsLog);
- },
-
- beforeUnmount() {
- this.connection.off('stats', this.onStats);
- this.connection.off('statsLog', this.onStatsLog);
- },
-
- methods: {
- onStats(stats) {
- this.activeSincePrevTick = stats[this.domain].activeSincePrevTick;
- this.active = stats[this.domain].active;
- this.waiting = stats[this.domain].waiting;
- this.delayed = stats[this.domain].delayed;
- this.chart.data.labels.push('');
- this.chart.data.datasets[0].data.push(stats[this.domain].activeSincePrevTick);
- this.chart.data.datasets[1].data.push(stats[this.domain].active);
- this.chart.data.datasets[2].data.push(stats[this.domain].waiting);
- this.chart.data.datasets[3].data.push(stats[this.domain].delayed);
- if (this.chart.data.datasets[0].data.length > 200) {
- this.chart.data.labels.shift();
- this.chart.data.datasets[0].data.shift();
- this.chart.data.datasets[1].data.shift();
- this.chart.data.datasets[2].data.shift();
- this.chart.data.datasets[3].data.shift();
- }
- this.chart.update();
- },
-
- onStatsLog(statsLog) {
- for (const stats of [...statsLog].reverse()) {
- this.onStats(stats);
- }
- },
-
- fetchJobs() {
- os.api(this.domain === 'inbox' ? 'admin/queue/inbox-delayed' : this.domain === 'deliver' ? 'admin/queue/deliver-delayed' : null, {}).then(jobs => {
- this.jobs = jobs;
- });
- },
-
- number
- }
-});
-</script>
-
-<style lang="scss" scoped>
-.pumxzjhg {
- > .status {
- padding: 16px;
- border-bottom: solid 0.5px var(--divider);
- }
-
- > .jobs {
- padding: 16px;
- border-top: solid 0.5px var(--divider);
- max-height: 180px;
- overflow: auto;
- }
-}
-</style>
diff --git a/src/client/pages/settings/import-export.vue b/src/client/pages/settings/import-export.vue
index 2b49996dda..eeaa1f1602 100644
--- a/src/client/pages/settings/import-export.vue
+++ b/src/client/pages/settings/import-export.vue
@@ -16,11 +16,13 @@
</FormSection>
<FormSection>
<template #label>{{ $ts._exportOrImport.muteList }}</template>
- <MkButton :class="$style.button" inline @click="doExport('mute')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
+ <MkButton :class="$style.button" inline @click="doExport('muting')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
+ <MkButton :class="$style.button" inline @click="doImport('muting', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
</FormSection>
<FormSection>
<template #label>{{ $ts._exportOrImport.blockingList }}</template>
<MkButton :class="$style.button" inline @click="doExport('blocking')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
+ <MkButton :class="$style.button" inline @click="doImport('blocking', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
</FormSection>
</div>
</template>
@@ -58,11 +60,11 @@ export default defineComponent({
methods: {
doExport(target) {
os.api(
- target == 'notes' ? 'i/export-notes' :
- target == 'following' ? 'i/export-following' :
- target == 'blocking' ? 'i/export-blocking' :
- target == 'user-lists' ? 'i/export-user-lists' :
- target == 'mute' ? 'i/export-mute' :
+ target === 'notes' ? 'i/export-notes' :
+ target === 'following' ? 'i/export-following' :
+ target === 'blocking' ? 'i/export-blocking' :
+ target === 'user-lists' ? 'i/export-user-lists' :
+ target === 'muting' ? 'i/export-mute' :
null, {})
.then(() => {
os.dialog({
@@ -81,8 +83,10 @@ export default defineComponent({
const file = await selectFile(e.currentTarget || e.target);
os.api(
- target == 'following' ? 'i/import-following' :
- target == 'user-lists' ? 'i/import-user-lists' :
+ target === 'following' ? 'i/import-following' :
+ target === 'user-lists' ? 'i/import-user-lists' :
+ target === 'muting' ? 'i/import-muting' :
+ target === 'blocking' ? 'i/import-blocking' :
null, {
fileId: file.id
}).then(() => {
diff --git a/src/client/pages/settings/privacy.vue b/src/client/pages/settings/privacy.vue
index 7756158578..2a60ae1f46 100644
--- a/src/client/pages/settings/privacy.vue
+++ b/src/client/pages/settings/privacy.vue
@@ -5,6 +5,10 @@
<FormSwitch v-model="autoAcceptFollowed" :disabled="!isLocked" @update:modelValue="save()">{{ $ts.autoAcceptFollowed }}</FormSwitch>
<template #caption>{{ $ts.lockedAccountInfo }}</template>
</FormGroup>
+ <FormSwitch v-model="publicReactions" @update:modelValue="save()">
+ {{ $ts.makeReactionsPublic }}
+ <template #desc>{{ $ts.makeReactionsPublicDescription }}</template>
+ </FormSwitch>
<FormSwitch v-model="hideOnlineStatus" @update:modelValue="save()">
{{ $ts.hideOnlineStatus }}
<template #desc>{{ $ts.hideOnlineStatusDescription }}</template>
@@ -64,6 +68,7 @@ export default defineComponent({
noCrawle: false,
isExplorable: false,
hideOnlineStatus: false,
+ publicReactions: false,
}
},
@@ -80,6 +85,7 @@ export default defineComponent({
this.noCrawle = this.$i.noCrawle;
this.isExplorable = this.$i.isExplorable;
this.hideOnlineStatus = this.$i.hideOnlineStatus;
+ this.publicReactions = this.$i.publicReactions;
},
mounted() {
@@ -94,6 +100,7 @@ export default defineComponent({
noCrawle: !!this.noCrawle,
isExplorable: !!this.isExplorable,
hideOnlineStatus: !!this.hideOnlineStatus,
+ publicReactions: !!this.publicReactions,
});
}
}
diff --git a/src/client/pages/settings/theme.manage.vue b/src/client/pages/settings/theme.manage.vue
index da21a47a50..1a11a664f0 100644
--- a/src/client/pages/settings/theme.manage.vue
+++ b/src/client/pages/settings/theme.manage.vue
@@ -10,13 +10,13 @@
</optgroup>
</FormSelect>
<template v-if="selectedTheme">
- <FormInput readonly :value="selectedTheme.author">
+ <FormInput readonly :modelValue="selectedTheme.author">
<span>{{ $ts.author }}</span>
</FormInput>
- <FormTextarea readonly :value="selectedTheme.desc" v-if="selectedTheme.desc">
+ <FormTextarea readonly :modelValue="selectedTheme.desc" v-if="selectedTheme.desc">
<span>{{ $ts._theme.description }}</span>
</FormTextarea>
- <FormTextarea readonly tall :value="selectedThemeCode">
+ <FormTextarea readonly tall :modelValue="selectedThemeCode">
<span>{{ $ts._theme.code }}</span>
<template #desc><button @click="copyThemeCode()" class="_textButton">{{ $ts.copy }}</button></template>
</FormTextarea>
@@ -28,12 +28,12 @@
<script lang="ts">
import { defineComponent } from 'vue';
import * as JSON5 from 'json5';
-import FormTextarea from '@client/components/form/textarea.vue';
-import FormSelect from '@client/components/form/select.vue';
-import FormRadios from '@client/components/form/radios.vue';
+import FormTextarea from '@client/components/debobigego/textarea.vue';
+import FormSelect from '@client/components/debobigego/select.vue';
+import FormRadios from '@client/components/debobigego/radios.vue';
import FormBase from '@client/components/debobigego/base.vue';
import FormGroup from '@client/components/debobigego/group.vue';
-import FormInput from '@client/components/form/input.vue';
+import FormInput from '@client/components/debobigego/input.vue';
import FormButton from '@client/components/debobigego/button.vue';
import { Theme, builtinThemes } from '@client/scripts/theme';
import copyToClipboard from '@client/scripts/copy-to-clipboard';
diff --git a/src/client/pages/share.vue b/src/client/pages/share.vue
index 67e598fa8f..70a9661dd0 100644
--- a/src/client/pages/share.vue
+++ b/src/client/pages/share.vue
@@ -1,22 +1,38 @@
<template>
<div class="">
<section class="_section">
- <div class="_title" v-if="title">{{ title }}</div>
<div class="_content">
- <XPostForm v-if="!posted" fixed :instant="true" :initial-text="initialText" @posted="posted = true" class="_panel"/>
- <MkButton v-else primary @click="close()">{{ $ts.close }}</MkButton>
+ <XPostForm
+ v-if="state === 'writing'"
+ fixed
+ :share="true"
+ :initial-text="initialText"
+ :initial-visibility="visibility"
+ :initial-files="files"
+ :initial-local-only="localOnly"
+ :reply="reply"
+ :renote="renote"
+ :visible-users="visibleUsers"
+ @posted="state = 'posted'"
+ class="_panel"
+ />
+ <MkButton v-else-if="state === 'posted'" primary @click="close()" class="close">{{ $ts.close }}</MkButton>
</div>
- <div class="_footer" v-if="url">{{ url }}</div>
</section>
</div>
</template>
<script lang="ts">
+// SPECIFICATION: /src/docs/ja-JP/advanced/share-page.md
+
import { defineComponent } from 'vue';
import MkButton from '@client/components/ui/button.vue';
import XPostForm from '@client/components/post-form.vue';
import * as os from '@client/os';
+import { noteVisibilities } from '@/types';
+import { parseAcct } from '@/misc/acct';
import * as symbols from '@client/symbols';
+import * as Misskey from 'misskey-js';
export default defineComponent({
components: {
@@ -30,35 +46,139 @@ export default defineComponent({
title: this.$ts.share,
icon: 'fas fa-share-alt'
},
- title: null,
- text: null,
- url: null,
- initialText: null,
- posted: false,
+ state: 'fetching' as 'fetching' | 'writing' | 'posted',
+ title: null as string | null,
+ initialText: null as string | null,
+ reply: null as Misskey.entities.Note | null,
+ renote: null as Misskey.entities.Note | null,
+ visibility: null as string | null,
+ localOnly: null as boolean | null,
+ files: [] as Misskey.entities.DriveFile[],
+ visibleUsers: [] as Misskey.entities.User[],
}
},
- created() {
+ async created() {
const urlParams = new URLSearchParams(window.location.search);
+
this.title = urlParams.get('title');
- this.text = urlParams.get('text');
- this.url = urlParams.get('url');
-
- let text = '';
- if (this.title) text += `【${this.title}】\n`;
- if (this.text) text += `${this.text}\n`;
- if (this.url) text += `${this.url}`;
- this.initialText = text.trim();
+ const text = urlParams.get('text');
+ const url = urlParams.get('url');
+
+ let noteText = '';
+ if (this.title) noteText += `[ ${this.title} ]\n`;
+ // Googleニュース対策
+ if (text?.startsWith(`${this.title}.\n`)) noteText += text.replace(`${this.title}.\n`, '');
+ else if (text && this.title !== text) noteText += `${text}\n`;
+ if (url) noteText += `${url}`;
+ this.initialText = noteText.trim();
+
+ const visibility = urlParams.get('visibility');
+ if (noteVisibilities.includes(visibility)) {
+ this.visibility = visibility;
+ }
+
+ if (this.visibility === 'specified') {
+ const visibleUserIds = urlParams.get('visibleUserIds');
+ const visibleAccts = urlParams.get('visibleAccts');
+ await Promise.all(
+ [
+ ...(visibleUserIds ? visibleUserIds.split(',').map(userId => ({ userId })) : []),
+ ...(visibleAccts ? visibleAccts.split(',').map(parseAcct) : [])
+ ]
+ // TypeScriptの指示通りに変換する
+ .map(q => 'username' in q ? { username: q.username, host: q.host === null ? undefined : q.host } : q)
+ .map(q => os.api('users/show', q)
+ .then(user => {
+ this.visibleUsers.push(user);
+ }, () => {
+ console.error(`Invalid user query: ${JSON.stringify(q)}`);
+ })
+ )
+ );
+ }
+
+ const localOnly = urlParams.get('localOnly');
+ if (localOnly === '0') this.localOnly = false;
+ else if (localOnly === '1') this.localOnly = true;
+
+ try {
+ //#region Reply
+ const replyId = urlParams.get('replyId');
+ const replyUri = urlParams.get('replyUri');
+ if (replyId) {
+ this.reply = await os.api('notes/show', {
+ noteId: replyId
+ });
+ } else if (replyUri) {
+ const obj = await os.api('ap/show', {
+ uri: replyUri
+ });
+ if (obj.type === 'Note') {
+ this.reply = obj.object;
+ }
+ }
+ //#endregion
+
+ //#region Renote
+ const renoteId = urlParams.get('renoteId');
+ const renoteUri = urlParams.get('renoteUri');
+ if (renoteId) {
+ this.renote = await os.api('notes/show', {
+ noteId: renoteId
+ });
+ } else if (renoteUri) {
+ const obj = await os.api('ap/show', {
+ uri: renoteUri
+ });
+ if (obj.type === 'Note') {
+ this.renote = obj.object;
+ }
+ }
+ //#endregion
+
+ //#region Drive files
+ const fileIds = urlParams.get('fileIds');
+ if (fileIds) {
+ await Promise.all(
+ fileIds.split(',')
+ .map(fileId => os.api('drive/files/show', { fileId })
+ .then(file => {
+ this.files.push(file);
+ }, () => {
+ console.error(`Failed to fetch a file ${fileId}`);
+ })
+ )
+ );
+ }
+ //#endregion
+ } catch (e) {
+ os.dialog({
+ type: 'error',
+ title: e.message,
+ text: e.name
+ });
+ }
+
+ this.state = 'writing';
},
methods: {
close() {
- window.close()
+ window.close();
+
+ // 閉じなければ100ms後タイムラインに
+ setTimeout(() => {
+ this.$router.push('/');
+ }, 100);
}
}
});
</script>
<style lang="scss" scoped>
+.close {
+ margin: 16px auto;
+}
</style>
diff --git a/src/client/pages/user/index.timeline.vue b/src/client/pages/user/index.timeline.vue
index c3444f26f6..9057d90396 100644
--- a/src/client/pages/user/index.timeline.vue
+++ b/src/client/pages/user/index.timeline.vue
@@ -65,4 +65,11 @@ export default defineComponent({
background: var(--bg);
}
}
+
+._fitSide_ .yrzkoczt {
+ > .tab {
+ padding-left: var(--margin);
+ padding-right: var(--margin);
+ }
+}
</style>
diff --git a/src/client/pages/user/index.vue b/src/client/pages/user/index.vue
index 0ddf73d572..f74bf49883 100644
--- a/src/client/pages/user/index.vue
+++ b/src/client/pages/user/index.vue
@@ -181,6 +181,7 @@
</template>
<XFollowList v-else-if="page === 'following'" type="following" :user="user" class="_content _gap"/>
<XFollowList v-else-if="page === 'followers'" type="followers" :user="user" class="_content _gap"/>
+ <XReactions v-else-if="page === 'reactions'" :user="user" class="_gap"/>
<XClips v-else-if="page === 'clips'" :user="user" class="_gap"/>
<XPages v-else-if="page === 'pages'" :user="user" class="_gap"/>
<XGallery v-else-if="page === 'gallery'" :user="user" class="_gap"/>
@@ -223,6 +224,7 @@ export default defineComponent({
MkTab,
MkInfo,
XFollowList: defineAsyncComponent(() => import('./follow-list.vue')),
+ XReactions: defineAsyncComponent(() => import('./reactions.vue')),
XClips: defineAsyncComponent(() => import('./clips.vue')),
XPages: defineAsyncComponent(() => import('./pages.vue')),
XGallery: defineAsyncComponent(() => import('./gallery.vue')),
@@ -268,7 +270,12 @@ export default defineComponent({
title: this.$ts.overview,
icon: 'fas fa-home',
onClick: () => { this.$router.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'); },
+ }] : [], {
active: this.page === 'clips',
title: this.$ts.clips,
icon: 'fas fa-paperclip',
diff --git a/src/client/pages/user/reactions.vue b/src/client/pages/user/reactions.vue
new file mode 100644
index 0000000000..5ac7e01027
--- /dev/null
+++ b/src/client/pages/user/reactions.vue
@@ -0,0 +1,81 @@
+<template>
+<div>
+ <MkPagination :pagination="pagination" #default="{items}" ref="list">
+ <div v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap afdcfbfb">
+ <div class="header">
+ <MkAvatar class="avatar" :user="user"/>
+ <MkReactionIcon class="reaction" :reaction="item.type" :custom-emojis="item.note.emojis" :no-style="true"/>
+ <MkTime :time="item.createdAt" class="createdAt"/>
+ </div>
+ <MkNote :note="item.note" @update:note="updated(note, $event)" :key="item.id"/>
+ </div>
+ </MkPagination>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkPagination from '@client/components/ui/pagination.vue';
+import MkNote from '@client/components/note.vue';
+import MkReactionIcon from '@client/components/reaction-icon.vue';
+
+export default defineComponent({
+ components: {
+ MkPagination,
+ MkNote,
+ MkReactionIcon,
+ },
+
+ props: {
+ user: {
+ type: Object,
+ required: true
+ },
+ },
+
+ data() {
+ return {
+ pagination: {
+ endpoint: 'users/reactions',
+ limit: 20,
+ params: {
+ userId: this.user.id,
+ }
+ },
+ };
+ },
+
+ watch: {
+ user() {
+ this.$refs.list.reload();
+ }
+ },
+});
+</script>
+
+<style lang="scss" scoped>
+.afdcfbfb {
+ > .header {
+ display: flex;
+ align-items: center;
+ padding: 8px 16px;
+ margin-bottom: 8px;
+ border-bottom: solid 2px var(--divider);
+
+ > .avatar {
+ width: 24px;
+ height: 24px;
+ margin-right: 8px;
+ }
+
+ > .reaction {
+ width: 32px;
+ height: 32px;
+ }
+
+ > .createdAt {
+ margin-left: auto;
+ }
+ }
+}
+</style>
diff --git a/src/client/router.ts b/src/client/router.ts
index 56dc948669..f588bb04fc 100644
--- a/src/client/router.ts
+++ b/src/client/router.ts
@@ -70,8 +70,8 @@ const defaultRoutes = [
{ path: '/my/antennas/:antennaId', component: page('my-antennas/edit'), props: true },
{ path: '/my/clips', component: page('my-clips/index') },
{ path: '/scratchpad', component: page('scratchpad') },
- { path: '/instance/:page(.*)?', component: page('instance/index'), props: route => ({ initialPage: route.params.page || null }) },
- { path: '/instance', component: page('instance/index') },
+ { path: '/admin/:page(.*)?', component: page('admin/index'), props: route => ({ initialPage: route.params.page || null }) },
+ { path: '/admin', component: page('admin/index') },
{ 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/src/client/scripts/hpml/lib.ts b/src/client/scripts/hpml/lib.ts
index 150a04732f..2a1ac73a40 100644
--- a/src/client/scripts/hpml/lib.ts
+++ b/src/client/scripts/hpml/lib.ts
@@ -1,11 +1,11 @@
import * as tinycolor from 'tinycolor2';
-import Chart from 'chart.js';
import { Hpml } from './evaluator';
import { values, utils } from '@syuilo/aiscript';
import { Fn, HpmlScope } from '.';
import { Expr } from './expr';
import * as 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
Chart.pluginService.register({
beforeDraw: (chart, easing) => {
@@ -18,6 +18,7 @@ Chart.pluginService.register({
}
}
});
+*/
export function initAiLib(hpml: Hpml) {
return {
@@ -49,11 +50,12 @@ export function initAiLib(hpml: Hpml) {
]));
}),
'MkPages:chart': values.FN_NATIVE(([id, opts]) => {
+ /* TODO
utils.assertString(id);
utils.assertObject(opts);
const canvas = hpml.canvases[id.value];
const color = getComputedStyle(document.documentElement).getPropertyValue('--accent');
- Chart.defaults.global.defaultFontColor = '#555';
+ Chart.defaults.color = '#555';
const chart = new Chart(canvas, {
type: opts.value.get('type').value,
data: {
@@ -122,6 +124,7 @@ export function initAiLib(hpml: Hpml) {
})
}
});
+ */
})
};
}
diff --git a/src/client/scripts/theme.ts b/src/client/scripts/theme.ts
index e79d54fa6d..8b63821293 100644
--- a/src/client/scripts/theme.ts
+++ b/src/client/scripts/theme.ts
@@ -20,12 +20,14 @@ export const builtinThemes = [
require('@client/themes/l-apricot.json5'),
require('@client/themes/l-rainy.json5'),
require('@client/themes/l-vivid.json5'),
+ require('@client/themes/l-sushi.json5'),
require('@client/themes/d-dark.json5'),
require('@client/themes/d-persimmon.json5'),
require('@client/themes/d-astro.json5'),
require('@client/themes/d-future.json5'),
require('@client/themes/d-botanical.json5'),
+ require('@client/themes/d-pumpkin.json5'),
require('@client/themes/d-black.json5'),
] as Theme[];
diff --git a/src/client/themes/d-astro.json5 b/src/client/themes/d-astro.json5
index 2350e3d46d..c6a927ec3a 100644
--- a/src/client/themes/d-astro.json5
+++ b/src/client/themes/d-astro.json5
@@ -1,7 +1,7 @@
{
id: '080a01c5-377d-4fbb-88cc-6bb5d04977ea',
base: 'dark',
- name: 'Mi Astro',
+ name: 'Mi Astro Dark',
author: 'syuilo',
props: {
bg: '#232125',
diff --git a/src/client/themes/d-future.json5 b/src/client/themes/d-future.json5
index 1882609121..b6fa1ab0c1 100644
--- a/src/client/themes/d-future.json5
+++ b/src/client/themes/d-future.json5
@@ -1,7 +1,7 @@
{
id: '32a637ef-b47a-4775-bb7b-bacbb823f865',
- name: 'Mi Future',
+ name: 'Mi Future Dark',
author: 'syuilo',
base: 'dark',
diff --git a/src/client/themes/d-persimmon.json5 b/src/client/themes/d-persimmon.json5
index 11e9994f5e..e36265ff10 100644
--- a/src/client/themes/d-persimmon.json5
+++ b/src/client/themes/d-persimmon.json5
@@ -1,7 +1,7 @@
{
id: 'c503d768-7c70-4db2-a4e6-08264304bc8d',
- name: 'Mi Persimmon',
+ name: 'Mi Persimmon Dark',
author: 'syuilo',
base: 'dark',
diff --git a/src/client/themes/d-pumpkin.json5 b/src/client/themes/d-pumpkin.json5
new file mode 100644
index 0000000000..064ca4577b
--- /dev/null
+++ b/src/client/themes/d-pumpkin.json5
@@ -0,0 +1,88 @@
+{
+ id: '0b64fef3-02c7-20b5-dd87-b3f77e2b4301',
+
+ name: 'Mi Pumpkin Dark',
+ author: 'syuilo',
+
+ base: 'dark',
+
+ props: {
+ X2: ':darken<2<@panel',
+ X3: 'rgba(255, 255, 255, 0.05)',
+ X4: 'rgba(255, 255, 255, 0.1)',
+ X5: 'rgba(255, 255, 255, 0.05)',
+ X6: 'rgba(255, 255, 255, 0.15)',
+ X7: 'rgba(255, 255, 255, 0.05)',
+ X8: ':lighten<5<@accent',
+ X9: ':darken<5<@accent',
+ bg: 'rgb(37, 32, 47)',
+ fg: '#e0d5c0',
+ X10: ':alpha<0.4<@accent',
+ X11: 'rgba(0, 0, 0, 0.3)',
+ X12: 'rgba(255, 255, 255, 0.1)',
+ X13: 'rgba(255, 255, 255, 0.15)',
+ X14: ':alpha<0.5<@navBg',
+ X15: ':alpha<0<@panel',
+ X16: ':alpha<0.7<@panel',
+ X17: ':alpha<0.8<@bg',
+ cwBg: '#687390',
+ cwFg: '#393f4f',
+ link: 'rgb(172, 193, 68)',
+ warn: '#ecb637',
+ badge: '#31b1ce',
+ error: '#ec4137',
+ focus: ':alpha<0.3<@accent',
+ navBg: '@panel',
+ navFg: '@fg',
+ panel: ':lighten<3<@bg',
+ popup: ':lighten<3<@panel',
+ accent: 'rgb(242, 133, 36)',
+ header: ':alpha<0.7<@panel',
+ infoBg: '#253142',
+ infoFg: '#fff',
+ renote: 'rgb(110, 179, 72)',
+ shadow: 'rgba(0, 0, 0, 0.3)',
+ divider: 'rgba(255, 255, 255, 0.1)',
+ hashtag: 'rgb(188, 90, 255)',
+ mention: 'rgb(72, 179, 139)',
+ modalBg: 'rgba(0, 0, 0, 0.5)',
+ success: '#86b300',
+ buttonBg: 'rgba(255, 255, 255, 0.05)',
+ switchBg: 'rgba(255, 255, 255, 0.15)',
+ acrylicBg: ':alpha<0.5<@bg',
+ cwHoverBg: '#707b97',
+ indicator: '@accent',
+ mentionMe: '@accent',
+ messageBg: '@bg',
+ navActive: '@accent',
+ accentedBg: ':alpha<0.15<@accent',
+ fgOnAccent: '#000',
+ infoWarnBg: '#42321c',
+ infoWarnFg: '#ffbd3e',
+ navHoverFg: ':lighten<17<@fg',
+ dateLabelFg: '@fg',
+ inputBorder: 'rgba(255, 255, 255, 0.1)',
+ panelBorder: '" solid 1px var(--divider)',
+ accentDarken: ':darken<10<@accent',
+ acrylicPanel: ':alpha<0.5<@panel',
+ navIndicator: '@indicator',
+ accentLighten: ':lighten<10<@accent',
+ buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
+ driveFolderBg: ':alpha<0.3<@accent',
+ fgHighlighted: ':lighten<3<@fg',
+ fgTransparent: ':alpha<0.5<@fg',
+ panelHeaderBg: ':lighten<3<@panel',
+ panelHeaderFg: '@fg',
+ buttonGradateA: '@accent',
+ buttonGradateB: ':hue<20<@accent',
+ htmlThemeColor: '@bg',
+ panelHighlight: ':lighten<3<@panel',
+ listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
+ scrollbarHandle: 'rgba(255, 255, 255, 0.2)',
+ inputBorderHover: 'rgba(255, 255, 255, 0.2)',
+ wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
+ fgTransparentWeak: ':alpha<0.75<@fg',
+ panelHeaderDivider: 'rgba(0, 0, 0, 0)',
+ scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)',
+ },
+}
diff --git a/src/client/themes/l-apricot.json5 b/src/client/themes/l-apricot.json5
index 74cb24d407..1ed5525575 100644
--- a/src/client/themes/l-apricot.json5
+++ b/src/client/themes/l-apricot.json5
@@ -1,7 +1,7 @@
{
id: '0ff48d43-aab3-46e7-ab12-8492110d2e2b',
- name: 'Mi Apricot',
+ name: 'Mi Apricot Light',
author: 'syuilo',
base: 'light',
diff --git a/src/client/themes/l-rainy.json5 b/src/client/themes/l-rainy.json5
index 1edde1cabf..283dd74c6c 100644
--- a/src/client/themes/l-rainy.json5
+++ b/src/client/themes/l-rainy.json5
@@ -1,7 +1,7 @@
{
id: 'a58a0abb-ff8c-476a-8dec-0ad7837e7e96',
- name: 'Mi Rainy',
+ name: 'Mi Rainy Light',
author: 'syuilo',
base: 'light',
diff --git a/src/client/themes/l-sushi.json5 b/src/client/themes/l-sushi.json5
new file mode 100644
index 0000000000..5846927d65
--- /dev/null
+++ b/src/client/themes/l-sushi.json5
@@ -0,0 +1,18 @@
+{
+ id: '213273e5-7d20-d5f0-6e36-1b6a4f67115c',
+
+ name: 'Mi Sushi Light',
+ author: 'syuilo',
+
+ base: 'light',
+
+ props: {
+ accent: '#e36749',
+ bg: '#f0eee9',
+ fg: '#5f5f5f',
+ renote: '@accent',
+ link: '@accent',
+ mention: '@accent',
+ hashtag: '#229e82',
+ },
+}
diff --git a/src/client/themes/l-vivid.json5 b/src/client/themes/l-vivid.json5
index 0f4abe0a45..b3c08f38ae 100644
--- a/src/client/themes/l-vivid.json5
+++ b/src/client/themes/l-vivid.json5
@@ -1,7 +1,7 @@
{
id: '6128c2a9-5c54-43fe-a47d-17942356470b',
- name: 'Mi Vivid',
+ name: 'Mi Vivid Light',
author: 'syuilo',
base: 'light',
diff --git a/src/client/ui/_common_/sidebar.vue b/src/client/ui/_common_/sidebar.vue
index d00327b096..ec2a17ab86 100644
--- a/src/client/ui/_common_/sidebar.vue
+++ b/src/client/ui/_common_/sidebar.vue
@@ -25,7 +25,7 @@
</component>
</template>
<div class="divider"></div>
- <MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/instance" v-click-anime>
+ <MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/admin" v-click-anime>
<i class="fas fa-server fa-fw"></i><span class="text">{{ $ts.instance }}</span>
</MkA>
<button class="item _button" @click="more" v-click-anime>
diff --git a/src/client/ui/chat/post-form.vue b/src/client/ui/chat/post-form.vue
index 0cacaf77e7..64b8d08cbc 100644
--- a/src/client/ui/chat/post-form.vue
+++ b/src/client/ui/chat/post-form.vue
@@ -100,7 +100,7 @@ export default defineComponent({
type: Object,
required: false
},
- instant: {
+ share: {
type: Boolean,
required: false,
default: false
@@ -277,7 +277,7 @@ export default defineComponent({
this.$nextTick(() => {
// 書きかけの投稿を復元
- if (!this.instant && !this.mention && !this.specified) {
+ if (!this.share && !this.mention && !this.specified) {
const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftKey];
if (draft) {
this.text = draft.data.text;
@@ -507,8 +507,6 @@ export default defineComponent({
},
saveDraft() {
- if (this.instant) return;
-
const data = JSON.parse(localStorage.getItem('drafts') || '{}');
data[this.draftKey] = {
diff --git a/src/client/ui/default.header.vue b/src/client/ui/default.header.vue
index 4f6363e82d..908a4719a4 100644
--- a/src/client/ui/default.header.vue
+++ b/src/client/ui/default.header.vue
@@ -13,7 +13,7 @@
</component>
</template>
<div class="divider"></div>
- <MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/instance" :behavior="settingsWindowed ? 'modalWindow' : null" v-click-anime v-tooltip="$ts.instance">
+ <MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/admin" :behavior="settingsWindowed ? 'modalWindow' : null" v-click-anime v-tooltip="$ts.instance">
<i class="fas fa-server fa-fw"></i>
</MkA>
<button class="item _button" @click="more" v-click-anime>
diff --git a/src/client/ui/default.sidebar.vue b/src/client/ui/default.sidebar.vue
index e36febb7fa..cce74a53cd 100644
--- a/src/client/ui/default.sidebar.vue
+++ b/src/client/ui/default.sidebar.vue
@@ -20,7 +20,7 @@
</component>
</template>
<div class="divider"></div>
- <MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/instance" :behavior="settingsWindowed ? 'modalWindow' : null" v-click-anime>
+ <MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/admin" :behavior="settingsWindowed ? 'modalWindow' : null" v-click-anime>
<i class="fas fa-server fa-fw"></i><span class="text">{{ $ts.instance }}</span>
</MkA>
<button class="item _button" @click="more" v-click-anime>