mirror of
https://iceshrimp.dev/crimekillz/iceshrimp-161sh.git
synced 2024-11-22 12:13:48 +01:00
refactor(client): Refine routing (#8846)
This commit is contained in:
parent
e548abb13f
commit
10a18cd514
@ -77,7 +77,6 @@
|
||||
"vite": "2.9.10",
|
||||
"vue": "3.2.37",
|
||||
"vue-prism-editor": "2.0.0-alpha.2",
|
||||
"vue-router": "4.0.16",
|
||||
"vuedraggable": "4.0.1",
|
||||
"websocket": "1.0.34",
|
||||
"ws": "8.8.0"
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { del, get, set } from '@/scripts/idb-proxy';
|
||||
import { defineAsyncComponent, reactive } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import { showSuspendedDialog } from './scripts/show-suspended-dialog';
|
||||
import { i18n } from './i18n';
|
||||
import { del, get, set } from '@/scripts/idb-proxy';
|
||||
import { apiUrl } from '@/config';
|
||||
import { waiting, api, popup, popupMenu, success, alert } from '@/os';
|
||||
import { unisonReload, reloadChannel } from '@/scripts/unison-reload';
|
||||
import { showSuspendedDialog } from './scripts/show-suspended-dialog';
|
||||
import { i18n } from './i18n';
|
||||
|
||||
// TODO: 他のタブと永続化されたstateを同期
|
||||
|
||||
@ -22,13 +22,7 @@ export async function signout() {
|
||||
waiting();
|
||||
localStorage.removeItem('account');
|
||||
|
||||
//#region Remove account
|
||||
const accounts = await getAccounts();
|
||||
accounts.splice(accounts.findIndex(x => x.id === $i.id), 1);
|
||||
|
||||
if (accounts.length > 0) await set('accounts', accounts);
|
||||
else await del('accounts');
|
||||
//#endregion
|
||||
await removeAccount($i.id);
|
||||
|
||||
//#region Remove service worker registration
|
||||
try {
|
||||
@ -55,7 +49,7 @@ export async function signout() {
|
||||
} catch (err) {}
|
||||
//#endregion
|
||||
|
||||
document.cookie = `igi=; path=/`;
|
||||
document.cookie = 'igi=; path=/';
|
||||
|
||||
if (accounts.length > 0) login(accounts[0].token);
|
||||
else unisonReload('/');
|
||||
@ -72,14 +66,22 @@ export async function addAccount(id: Account['id'], token: Account['token']) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeAccount(id: Account['id']) {
|
||||
const accounts = await getAccounts();
|
||||
accounts.splice(accounts.findIndex(x => x.id === id), 1);
|
||||
|
||||
if (accounts.length > 0) await set('accounts', accounts);
|
||||
else await del('accounts');
|
||||
}
|
||||
|
||||
function fetchAccount(token: string): Promise<Account> {
|
||||
return new Promise((done, fail) => {
|
||||
// Fetch user
|
||||
fetch(`${apiUrl}/i`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
i: token
|
||||
})
|
||||
i: token,
|
||||
}),
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
@ -216,13 +218,13 @@ export async function openAccountMenu(opts: {
|
||||
type: 'link',
|
||||
icon: 'fas fa-users',
|
||||
text: i18n.ts.manageAccounts,
|
||||
to: `/settings/accounts`,
|
||||
to: '/settings/accounts',
|
||||
}]], ev.currentTarget ?? ev.target, {
|
||||
align: 'left'
|
||||
align: 'left',
|
||||
});
|
||||
} else {
|
||||
popupMenu([...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises], ev.currentTarget ?? ev.target, {
|
||||
align: 'left'
|
||||
align: 'left',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@
|
||||
id-denylist violation when setting it. This is causing about 60+ lint issues.
|
||||
As this is part of Chart.js's API it makes sense to disable the check here.
|
||||
*/
|
||||
import { defineProps, onMounted, ref, watch, PropType, onUnmounted } from 'vue';
|
||||
import { onMounted, ref, watch, PropType, onUnmounted } from 'vue';
|
||||
import {
|
||||
Chart,
|
||||
ArcElement,
|
||||
@ -53,7 +53,7 @@ const props = defineProps({
|
||||
limit: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 90
|
||||
default: 90,
|
||||
},
|
||||
span: {
|
||||
type: String as PropType<'hour' | 'day'>,
|
||||
@ -62,22 +62,22 @@ const props = defineProps({
|
||||
detailed: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
stacked: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
bar: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
aspectRatio: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
@ -156,7 +156,7 @@ const getDate = (ago: number) => {
|
||||
const format = (arr) => {
|
||||
return arr.map((v, i) => ({
|
||||
x: getDate(i).getTime(),
|
||||
y: v
|
||||
y: v,
|
||||
}));
|
||||
};
|
||||
|
||||
@ -343,7 +343,7 @@ const render = () => {
|
||||
min: 'original',
|
||||
max: 'original',
|
||||
},
|
||||
}
|
||||
},
|
||||
} : undefined,
|
||||
//gradient,
|
||||
},
|
||||
@ -367,8 +367,8 @@ const render = () => {
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
}]
|
||||
},
|
||||
}],
|
||||
});
|
||||
};
|
||||
|
||||
@ -433,18 +433,18 @@ const fetchApRequestChart = async (): Promise<typeof chartData> => {
|
||||
name: 'In',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
data: format(raw.inboxReceived)
|
||||
data: format(raw.inboxReceived),
|
||||
}, {
|
||||
name: 'Out (succ)',
|
||||
type: 'area',
|
||||
color: '#00E396',
|
||||
data: format(raw.deliverSucceeded)
|
||||
data: format(raw.deliverSucceeded),
|
||||
}, {
|
||||
name: 'Out (fail)',
|
||||
type: 'area',
|
||||
color: '#FEB019',
|
||||
data: format(raw.deliverFailed)
|
||||
}]
|
||||
data: format(raw.deliverFailed),
|
||||
}],
|
||||
};
|
||||
};
|
||||
|
||||
@ -456,7 +456,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => {
|
||||
type: 'line',
|
||||
data: format(type === 'combined'
|
||||
? sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec))
|
||||
: sum(raw[type].inc, negate(raw[type].dec))
|
||||
: sum(raw[type].inc, negate(raw[type].dec)),
|
||||
),
|
||||
color: '#888888',
|
||||
}, {
|
||||
@ -464,7 +464,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => {
|
||||
type: 'area',
|
||||
data: format(type === 'combined'
|
||||
? sum(raw.local.diffs.renote, raw.remote.diffs.renote)
|
||||
: raw[type].diffs.renote
|
||||
: raw[type].diffs.renote,
|
||||
),
|
||||
color: colors.green,
|
||||
}, {
|
||||
@ -472,7 +472,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => {
|
||||
type: 'area',
|
||||
data: format(type === 'combined'
|
||||
? sum(raw.local.diffs.reply, raw.remote.diffs.reply)
|
||||
: raw[type].diffs.reply
|
||||
: raw[type].diffs.reply,
|
||||
),
|
||||
color: colors.yellow,
|
||||
}, {
|
||||
@ -480,7 +480,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => {
|
||||
type: 'area',
|
||||
data: format(type === 'combined'
|
||||
? sum(raw.local.diffs.normal, raw.remote.diffs.normal)
|
||||
: raw[type].diffs.normal
|
||||
: raw[type].diffs.normal,
|
||||
),
|
||||
color: colors.blue,
|
||||
}, {
|
||||
@ -488,7 +488,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => {
|
||||
type: 'area',
|
||||
data: format(type === 'combined'
|
||||
? sum(raw.local.diffs.withFile, raw.remote.diffs.withFile)
|
||||
: raw[type].diffs.withFile
|
||||
: raw[type].diffs.withFile,
|
||||
),
|
||||
color: colors.purple,
|
||||
}],
|
||||
@ -522,21 +522,21 @@ const fetchUsersChart = async (total: boolean): Promise<typeof chartData> => {
|
||||
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))
|
||||
: 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))
|
||||
: 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))
|
||||
: sum(raw.remote.inc, negate(raw.remote.dec)),
|
||||
),
|
||||
}],
|
||||
};
|
||||
@ -607,8 +607,8 @@ const fetchDriveChart = async (): Promise<typeof chartData> => {
|
||||
raw.local.incSize,
|
||||
negate(raw.local.decSize),
|
||||
raw.remote.incSize,
|
||||
negate(raw.remote.decSize)
|
||||
)
|
||||
negate(raw.remote.decSize),
|
||||
),
|
||||
),
|
||||
}, {
|
||||
name: 'Local +',
|
||||
@ -642,8 +642,8 @@ const fetchDriveFilesChart = async (): Promise<typeof chartData> => {
|
||||
raw.local.incCount,
|
||||
negate(raw.local.decCount),
|
||||
raw.remote.incCount,
|
||||
negate(raw.remote.decCount)
|
||||
)
|
||||
negate(raw.remote.decCount),
|
||||
),
|
||||
),
|
||||
}, {
|
||||
name: 'Local +',
|
||||
@ -672,18 +672,18 @@ const fetchInstanceRequestsChart = async (): Promise<typeof chartData> => {
|
||||
name: 'In',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
data: format(raw.requests.received)
|
||||
data: format(raw.requests.received),
|
||||
}, {
|
||||
name: 'Out (succ)',
|
||||
type: 'area',
|
||||
color: '#00E396',
|
||||
data: format(raw.requests.succeeded)
|
||||
data: format(raw.requests.succeeded),
|
||||
}, {
|
||||
name: 'Out (fail)',
|
||||
type: 'area',
|
||||
color: '#FEB019',
|
||||
data: format(raw.requests.failed)
|
||||
}]
|
||||
data: format(raw.requests.failed),
|
||||
}],
|
||||
};
|
||||
};
|
||||
|
||||
@ -696,9 +696,9 @@ const fetchInstanceUsersChart = async (total: boolean): Promise<typeof chartData
|
||||
color: '#008FFB',
|
||||
data: format(total
|
||||
? raw.users.total
|
||||
: sum(raw.users.inc, negate(raw.users.dec))
|
||||
)
|
||||
}]
|
||||
: sum(raw.users.inc, negate(raw.users.dec)),
|
||||
),
|
||||
}],
|
||||
};
|
||||
};
|
||||
|
||||
@ -711,9 +711,9 @@ const fetchInstanceNotesChart = async (total: boolean): Promise<typeof chartData
|
||||
color: '#008FFB',
|
||||
data: format(total
|
||||
? raw.notes.total
|
||||
: sum(raw.notes.inc, negate(raw.notes.dec))
|
||||
)
|
||||
}]
|
||||
: sum(raw.notes.inc, negate(raw.notes.dec)),
|
||||
),
|
||||
}],
|
||||
};
|
||||
};
|
||||
|
||||
@ -726,17 +726,17 @@ const fetchInstanceFfChart = async (total: boolean): Promise<typeof chartData> =
|
||||
color: '#008FFB',
|
||||
data: format(total
|
||||
? raw.following.total
|
||||
: sum(raw.following.inc, negate(raw.following.dec))
|
||||
)
|
||||
: 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))
|
||||
)
|
||||
}]
|
||||
: sum(raw.followers.inc, negate(raw.followers.dec)),
|
||||
),
|
||||
}],
|
||||
};
|
||||
};
|
||||
|
||||
@ -750,9 +750,9 @@ const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof char
|
||||
color: '#008FFB',
|
||||
data: format(total
|
||||
? raw.drive.totalUsage
|
||||
: sum(raw.drive.incUsage, negate(raw.drive.decUsage))
|
||||
)
|
||||
}]
|
||||
: sum(raw.drive.incUsage, negate(raw.drive.decUsage)),
|
||||
),
|
||||
}],
|
||||
};
|
||||
};
|
||||
|
||||
@ -765,9 +765,9 @@ const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof char
|
||||
color: '#008FFB',
|
||||
data: format(total
|
||||
? raw.drive.totalFiles
|
||||
: sum(raw.drive.incFiles, negate(raw.drive.decFiles))
|
||||
)
|
||||
}]
|
||||
: sum(raw.drive.incFiles, negate(raw.drive.decFiles)),
|
||||
),
|
||||
}],
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -57,7 +57,7 @@ const isThumbnailAvailable = computed(() => {
|
||||
.zdjebgpv {
|
||||
position: relative;
|
||||
display: flex;
|
||||
background: #e1e1e1;
|
||||
background: var(--panel);
|
||||
border-radius: 8px;
|
||||
overflow: clip;
|
||||
|
||||
|
@ -9,13 +9,13 @@
|
||||
<i v-else class="fas fa-angle-down icon"></i>
|
||||
</span>
|
||||
</div>
|
||||
<keep-alive>
|
||||
<KeepAlive>
|
||||
<div v-if="openedAtLeastOnce" v-show="opened" class="body">
|
||||
<MkSpacer :margin-min="14" :margin-max="22">
|
||||
<slot></slot>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</keep-alive>
|
||||
</KeepAlive>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -5,13 +5,13 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { inject } from 'vue';
|
||||
import * as os from '@/os';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
||||
import { router } from '@/router';
|
||||
import { url } from '@/config';
|
||||
import { popout as popout_ } from '@/scripts/popout';
|
||||
import { i18n } from '@/i18n';
|
||||
import { MisskeyNavigator } from '@/scripts/navigate';
|
||||
import { useRouter } from '@/router';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
to: string;
|
||||
@ -22,15 +22,16 @@ const props = withDefaults(defineProps<{
|
||||
behavior: null,
|
||||
});
|
||||
|
||||
const mkNav = new MisskeyNavigator();
|
||||
const router = useRouter();
|
||||
|
||||
const active = $computed(() => {
|
||||
if (props.activeClass == null) return false;
|
||||
const resolved = router.resolve(props.to);
|
||||
if (resolved.path === router.currentRoute.value.path) return true;
|
||||
if (resolved.name == null) return false;
|
||||
if (resolved == null) return false;
|
||||
if (resolved.route.path === router.currentRoute.value.path) return true;
|
||||
if (resolved.route.name == null) return false;
|
||||
if (router.currentRoute.value.name == null) return false;
|
||||
return resolved.name === router.currentRoute.value.name;
|
||||
return resolved.route.name === router.currentRoute.value.name;
|
||||
});
|
||||
|
||||
function onContextmenu(ev) {
|
||||
@ -44,31 +45,25 @@ function onContextmenu(ev) {
|
||||
text: i18n.ts.openInWindow,
|
||||
action: () => {
|
||||
os.pageWindow(props.to);
|
||||
}
|
||||
}, mkNav.sideViewHook ? {
|
||||
icon: 'fas fa-columns',
|
||||
text: i18n.ts.openInSideView,
|
||||
action: () => {
|
||||
if (mkNav.sideViewHook) mkNav.sideViewHook(props.to);
|
||||
}
|
||||
} : undefined, {
|
||||
},
|
||||
}, {
|
||||
icon: 'fas fa-expand-alt',
|
||||
text: i18n.ts.showInPage,
|
||||
action: () => {
|
||||
router.push(props.to);
|
||||
}
|
||||
},
|
||||
}, null, {
|
||||
icon: 'fas fa-external-link-alt',
|
||||
text: i18n.ts.openInNewTab,
|
||||
action: () => {
|
||||
window.open(props.to, '_blank');
|
||||
}
|
||||
},
|
||||
}, {
|
||||
icon: 'fas fa-link',
|
||||
text: i18n.ts.copyLink,
|
||||
action: () => {
|
||||
copyToClipboard(`${url}${props.to}`);
|
||||
}
|
||||
},
|
||||
}], ev);
|
||||
}
|
||||
|
||||
@ -98,6 +93,6 @@ function nav() {
|
||||
}
|
||||
}
|
||||
|
||||
mkNav.push(props.to);
|
||||
router.push(props.to);
|
||||
}
|
||||
</script>
|
||||
|
@ -1,361 +0,0 @@
|
||||
<template>
|
||||
<div ref="el" class="fdidabkb" :class="{ slim: narrow, thin: thin_ }" :style="{ background: bg }" @click="onClick">
|
||||
<template v-if="info">
|
||||
<div v-if="!hideTitle" class="titleContainer" @click="showTabsPopup">
|
||||
<MkAvatar v-if="info.avatar" class="avatar" :user="info.avatar" :disable-preview="true" :show-indicator="true"/>
|
||||
<i v-else-if="info.icon" class="icon" :class="info.icon"></i>
|
||||
|
||||
<div class="title">
|
||||
<MkUserName v-if="info.userName" :user="info.userName" :nowrap="true" class="title"/>
|
||||
<div v-else-if="info.title" class="title">{{ info.title }}</div>
|
||||
<div v-if="!narrow && info.subtitle" class="subtitle">
|
||||
{{ info.subtitle }}
|
||||
</div>
|
||||
<div v-if="narrow && hasTabs" class="subtitle activeTab">
|
||||
{{ info.tabs.find(tab => tab.active)?.title }}
|
||||
<i class="chevron fas fa-chevron-down"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!narrow || hideTitle" class="tabs">
|
||||
<button v-for="tab in info.tabs" v-tooltip="tab.title" class="tab _button" :class="{ active: tab.active }" @click="tab.onClick">
|
||||
<i v-if="tab.icon" class="icon" :class="tab.icon"></i>
|
||||
<span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="buttons right">
|
||||
<template v-if="info && info.actions && !narrow">
|
||||
<template v-for="action in info.actions">
|
||||
<MkButton v-if="action.asFullButton" class="fullButton" primary @click.stop="action.handler"><i :class="action.icon" style="margin-right: 6px;"></i>{{ action.text }}</MkButton>
|
||||
<button v-else v-tooltip="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button>
|
||||
</template>
|
||||
</template>
|
||||
<button v-if="shouldShowMenu" v-tooltip="$ts.menu" class="_button button" @click.stop="showMenu" @touchstart="preventDrag"><i class="fas fa-ellipsis-h"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, onMounted, onUnmounted, PropType, ref, inject } from 'vue';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { popupMenu } from '@/os';
|
||||
import { url } from '@/config';
|
||||
import { scrollToTop } from '@/scripts/scroll';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { globalEvents } from '@/events';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton
|
||||
},
|
||||
|
||||
props: {
|
||||
info: {
|
||||
type: Object as PropType<{
|
||||
actions?: {}[];
|
||||
tabs?: {}[];
|
||||
}>,
|
||||
required: true
|
||||
},
|
||||
menu: {
|
||||
required: false
|
||||
},
|
||||
thin: {
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
const el = ref<HTMLElement>(null);
|
||||
const bg = ref(null);
|
||||
const narrow = ref(false);
|
||||
const height = ref(0);
|
||||
const hasTabs = computed(() => {
|
||||
return props.info.tabs && props.info.tabs.length > 0;
|
||||
});
|
||||
const shouldShowMenu = computed(() => {
|
||||
if (props.info == null) return false;
|
||||
if (props.info.actions != null && narrow.value) return true;
|
||||
if (props.info.menu != null) return true;
|
||||
if (props.info.share != null) return true;
|
||||
if (props.menu != null) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
const share = () => {
|
||||
navigator.share({
|
||||
url: url + props.info.path,
|
||||
...props.info.share,
|
||||
});
|
||||
};
|
||||
|
||||
const showMenu = (ev: MouseEvent) => {
|
||||
let menu = props.info.menu ? props.info.menu() : [];
|
||||
if (narrow.value && props.info.actions) {
|
||||
menu = [...props.info.actions.map(x => ({
|
||||
text: x.text,
|
||||
icon: x.icon,
|
||||
action: x.handler
|
||||
})), menu.length > 0 ? null : undefined, ...menu];
|
||||
}
|
||||
if (props.info.share) {
|
||||
if (menu.length > 0) menu.push(null);
|
||||
menu.push({
|
||||
text: i18n.ts.share,
|
||||
icon: 'fas fa-share-alt',
|
||||
action: share
|
||||
});
|
||||
}
|
||||
if (props.menu) {
|
||||
if (menu.length > 0) menu.push(null);
|
||||
menu = menu.concat(props.menu);
|
||||
}
|
||||
popupMenu(menu, ev.currentTarget ?? ev.target);
|
||||
};
|
||||
|
||||
const showTabsPopup = (ev: MouseEvent) => {
|
||||
if (!hasTabs.value) return;
|
||||
if (!narrow.value) return;
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const menu = props.info.tabs.map(tab => ({
|
||||
text: tab.title,
|
||||
icon: tab.icon,
|
||||
action: tab.onClick,
|
||||
}));
|
||||
popupMenu(menu, ev.currentTarget ?? ev.target);
|
||||
};
|
||||
|
||||
const preventDrag = (ev: TouchEvent) => {
|
||||
ev.stopPropagation();
|
||||
};
|
||||
|
||||
const onClick = () => {
|
||||
scrollToTop(el.value, { behavior: 'smooth' });
|
||||
};
|
||||
|
||||
const calcBg = () => {
|
||||
const rawBg = props.info?.bg || 'var(--bg)';
|
||||
const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
|
||||
tinyBg.setAlpha(0.85);
|
||||
bg.value = tinyBg.toRgbString();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
calcBg();
|
||||
globalEvents.on('themeChanged', calcBg);
|
||||
onUnmounted(() => {
|
||||
globalEvents.off('themeChanged', calcBg);
|
||||
});
|
||||
|
||||
if (el.value.parentElement) {
|
||||
narrow.value = el.value.parentElement.offsetWidth < 500;
|
||||
const ro = new ResizeObserver((entries, observer) => {
|
||||
if (el.value) {
|
||||
narrow.value = el.value.parentElement.offsetWidth < 500;
|
||||
}
|
||||
});
|
||||
ro.observe(el.value.parentElement);
|
||||
onUnmounted(() => {
|
||||
ro.disconnect();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
el,
|
||||
bg,
|
||||
narrow,
|
||||
height,
|
||||
hasTabs,
|
||||
shouldShowMenu,
|
||||
share,
|
||||
showMenu,
|
||||
showTabsPopup,
|
||||
preventDrag,
|
||||
onClick,
|
||||
hideTitle: inject('shouldOmitHeaderTitle', false),
|
||||
thin_: props.thin || inject('shouldHeaderThin', false)
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.fdidabkb {
|
||||
--height: 60px;
|
||||
display: flex;
|
||||
position: sticky;
|
||||
top: var(--stickyTop, 0);
|
||||
z-index: 1000;
|
||||
width: 100%;
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
border-bottom: solid 0.5px var(--divider);
|
||||
|
||||
&.thin {
|
||||
--height: 50px;
|
||||
|
||||
> .buttons {
|
||||
> .button {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.slim {
|
||||
text-align: center;
|
||||
|
||||
> .titleContainer {
|
||||
flex: 1;
|
||||
margin: 0 auto;
|
||||
margin-left: var(--height);
|
||||
|
||||
> *:first-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
> *:last-child {
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .buttons {
|
||||
--margin: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--height);
|
||||
margin: 0 var(--margin);
|
||||
|
||||
&.right {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
&:empty {
|
||||
width: var(--height);
|
||||
}
|
||||
|
||||
> .button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: calc(var(--height) - (var(--margin) * 2));
|
||||
width: calc(var(--height) - (var(--margin) * 2));
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
border-radius: 5px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&.highlighted {
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
> .fullButton {
|
||||
& + .fullButton {
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .titleContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 400px;
|
||||
overflow: auto;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
flex-shrink: 0;
|
||||
margin-left: 24px;
|
||||
|
||||
> .avatar {
|
||||
$size: 32px;
|
||||
display: inline-block;
|
||||
width: $size;
|
||||
height: $size;
|
||||
vertical-align: bottom;
|
||||
margin: 0 8px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
> .icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
> .title {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.1;
|
||||
|
||||
> .subtitle {
|
||||
opacity: 0.6;
|
||||
font-size: 0.8em;
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&.activeTab {
|
||||
text-align: center;
|
||||
|
||||
> .chevron {
|
||||
display: inline-block;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .tabs {
|
||||
margin-left: 16px;
|
||||
font-size: 0.8em;
|
||||
overflow: auto;
|
||||
white-space: nowrap;
|
||||
|
||||
> .tab {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
padding: 0 10px;
|
||||
height: 100%;
|
||||
font-weight: normal;
|
||||
opacity: 0.7;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
> .icon + .title {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
300
packages/client/src/components/global/page-header.vue
Normal file
300
packages/client/src/components/global/page-header.vue
Normal file
@ -0,0 +1,300 @@
|
||||
<template>
|
||||
<div v-if="show" ref="el" class="fdidabkb" :class="{ slim: narrow, thin: thin_ }" :style="{ background: bg }" @click="onClick">
|
||||
<template v-if="metadata">
|
||||
<div v-if="!hideTitle" class="titleContainer" @click="showTabsPopup">
|
||||
<MkAvatar v-if="metadata.avatar" class="avatar" :user="metadata.avatar" :disable-preview="true" :show-indicator="true"/>
|
||||
<i v-else-if="metadata.icon" class="icon" :class="metadata.icon"></i>
|
||||
|
||||
<div class="title">
|
||||
<MkUserName v-if="metadata.userName" :user="metadata.userName" :nowrap="true" class="title"/>
|
||||
<div v-else-if="metadata.title" class="title">{{ metadata.title }}</div>
|
||||
<div v-if="!narrow && metadata.subtitle" class="subtitle">
|
||||
{{ metadata.subtitle }}
|
||||
</div>
|
||||
<div v-if="narrow && hasTabs" class="subtitle activeTab">
|
||||
{{ tabs.find(tab => tab.active)?.title }}
|
||||
<i class="chevron fas fa-chevron-down"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!narrow || hideTitle" class="tabs">
|
||||
<button v-for="tab in tabs" v-tooltip="tab.title" class="tab _button" :class="{ active: tab.active }" @click="tab.onClick">
|
||||
<i v-if="tab.icon" class="icon" :class="tab.icon"></i>
|
||||
<span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="buttons right">
|
||||
<template v-for="action in actions">
|
||||
<button v-tooltip="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, onUnmounted, ref, inject } from 'vue';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { popupMenu } from '@/os';
|
||||
import { scrollToTop } from '@/scripts/scroll';
|
||||
import { i18n } from '@/i18n';
|
||||
import { globalEvents } from '@/events';
|
||||
import { injectPageMetadata, PageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const props = defineProps<{
|
||||
tabs?: {
|
||||
title: string;
|
||||
active: boolean;
|
||||
icon?: string;
|
||||
iconOnly?: boolean;
|
||||
onClick: () => void;
|
||||
}[];
|
||||
actions?: {
|
||||
text: string;
|
||||
icon: string;
|
||||
handler: (ev: MouseEvent) => void;
|
||||
}[];
|
||||
thin?: boolean;
|
||||
}>();
|
||||
|
||||
const metadata = injectPageMetadata();
|
||||
|
||||
const hideTitle = inject('shouldOmitHeaderTitle', false);
|
||||
const thin_ = props.thin || inject('shouldHeaderThin', false);
|
||||
|
||||
const el = $ref<HTMLElement | null>(null);
|
||||
const bg = ref(null);
|
||||
let narrow = $ref(false);
|
||||
const height = ref(0);
|
||||
const hasTabs = $computed(() => props.tabs && props.tabs.length > 0);
|
||||
const hasActions = $computed(() => props.actions && props.actions.length > 0);
|
||||
const show = $computed(() => {
|
||||
return !hideTitle || hasTabs || hasActions;
|
||||
});
|
||||
|
||||
const showTabsPopup = (ev: MouseEvent) => {
|
||||
if (!hasTabs) return;
|
||||
if (!narrow) return;
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const menu = props.tabs.map(tab => ({
|
||||
text: tab.title,
|
||||
icon: tab.icon,
|
||||
action: tab.onClick,
|
||||
}));
|
||||
popupMenu(menu, ev.currentTarget ?? ev.target);
|
||||
};
|
||||
|
||||
const preventDrag = (ev: TouchEvent) => {
|
||||
ev.stopPropagation();
|
||||
};
|
||||
|
||||
const onClick = () => {
|
||||
scrollToTop(el, { behavior: 'smooth' });
|
||||
};
|
||||
|
||||
const calcBg = () => {
|
||||
const rawBg = metadata?.bg || 'var(--bg)';
|
||||
const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
|
||||
tinyBg.setAlpha(0.85);
|
||||
bg.value = tinyBg.toRgbString();
|
||||
};
|
||||
|
||||
let ro: ResizeObserver | null;
|
||||
|
||||
onMounted(() => {
|
||||
calcBg();
|
||||
globalEvents.on('themeChanged', calcBg);
|
||||
|
||||
if (el && el.parentElement) {
|
||||
narrow = el.parentElement.offsetWidth < 500;
|
||||
ro = new ResizeObserver((entries, observer) => {
|
||||
if (el.parentElement) {
|
||||
narrow = el.parentElement.offsetWidth < 500;
|
||||
}
|
||||
});
|
||||
ro.observe(el.parentElement);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
globalEvents.off('themeChanged', calcBg);
|
||||
if (ro) ro.disconnect();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.fdidabkb {
|
||||
--height: 60px;
|
||||
display: flex;
|
||||
position: sticky;
|
||||
top: var(--stickyTop, 0);
|
||||
z-index: 1000;
|
||||
width: 100%;
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
border-bottom: solid 0.5px var(--divider);
|
||||
|
||||
&.thin {
|
||||
--height: 50px;
|
||||
|
||||
> .buttons {
|
||||
> .button {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.slim {
|
||||
text-align: center;
|
||||
|
||||
> .titleContainer {
|
||||
flex: 1;
|
||||
margin: 0 auto;
|
||||
margin-left: var(--height);
|
||||
|
||||
> *:first-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
> *:last-child {
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .buttons {
|
||||
--margin: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--height);
|
||||
margin: 0 var(--margin);
|
||||
|
||||
&.right {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
&:empty {
|
||||
width: var(--height);
|
||||
}
|
||||
|
||||
> .button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: calc(var(--height) - (var(--margin) * 2));
|
||||
width: calc(var(--height) - (var(--margin) * 2));
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
border-radius: 5px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&.highlighted {
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
> .fullButton {
|
||||
& + .fullButton {
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .titleContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 400px;
|
||||
overflow: auto;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
flex-shrink: 0;
|
||||
margin-left: 24px;
|
||||
|
||||
> .avatar {
|
||||
$size: 32px;
|
||||
display: inline-block;
|
||||
width: $size;
|
||||
height: $size;
|
||||
vertical-align: bottom;
|
||||
margin: 0 8px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
> .icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
> .title {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.1;
|
||||
|
||||
> .subtitle {
|
||||
opacity: 0.6;
|
||||
font-size: 0.8em;
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&.activeTab {
|
||||
text-align: center;
|
||||
|
||||
> .chevron {
|
||||
display: inline-block;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .tabs {
|
||||
margin-left: 16px;
|
||||
font-size: 0.8em;
|
||||
overflow: auto;
|
||||
white-space: nowrap;
|
||||
|
||||
> .tab {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
padding: 0 10px;
|
||||
height: 100%;
|
||||
font-weight: normal;
|
||||
opacity: 0.7;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
> .icon + .title {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
39
packages/client/src/components/global/router-view.vue
Normal file
39
packages/client/src/components/global/router-view.vue
Normal file
@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<KeepAlive max="5">
|
||||
<component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)"/>
|
||||
</KeepAlive>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { inject, nextTick, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { Router } from '@/nirax';
|
||||
|
||||
const props = defineProps<{
|
||||
router?: Router;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const router = props.router ?? inject('router');
|
||||
|
||||
if (router == null) {
|
||||
throw new Error('no router provided');
|
||||
}
|
||||
|
||||
let currentPageComponent = $ref(router.getCurrentComponent());
|
||||
let currentPageProps = $ref(router.getCurrentProps());
|
||||
let key = $ref(router.getCurrentKey());
|
||||
|
||||
function onChange({ route, props: newProps, key: newKey }) {
|
||||
currentPageComponent = route.component;
|
||||
currentPageProps = newProps;
|
||||
key = newKey;
|
||||
}
|
||||
|
||||
router.addListener('change', onChange);
|
||||
|
||||
onUnmounted(() => {
|
||||
router.removeListener('change', onChange);
|
||||
});
|
||||
</script>
|
@ -10,15 +10,17 @@ import MkEllipsis from './global/ellipsis.vue';
|
||||
import MkTime from './global/time.vue';
|
||||
import MkUrl from './global/url.vue';
|
||||
import I18n from './global/i18n';
|
||||
import RouterView from './global/router-view.vue';
|
||||
import MkLoading from './global/loading.vue';
|
||||
import MkError from './global/error.vue';
|
||||
import MkAd from './global/ad.vue';
|
||||
import MkHeader from './global/header.vue';
|
||||
import MkPageHeader from './global/page-header.vue';
|
||||
import MkSpacer from './global/spacer.vue';
|
||||
import MkStickyContainer from './global/sticky-container.vue';
|
||||
|
||||
export default function(app: App) {
|
||||
app.component('I18n', I18n);
|
||||
app.component('RouterView', RouterView);
|
||||
app.component('Mfm', Mfm);
|
||||
app.component('MkA', MkA);
|
||||
app.component('MkAcct', MkAcct);
|
||||
@ -31,7 +33,7 @@ export default function(app: App) {
|
||||
app.component('MkLoading', MkLoading);
|
||||
app.component('MkError', MkError);
|
||||
app.component('MkAd', MkAd);
|
||||
app.component('MkHeader', MkHeader);
|
||||
app.component('MkPageHeader', MkPageHeader);
|
||||
app.component('MkSpacer', MkSpacer);
|
||||
app.component('MkStickyContainer', MkStickyContainer);
|
||||
}
|
||||
@ -39,6 +41,7 @@ export default function(app: App) {
|
||||
declare module '@vue/runtime-core' {
|
||||
export interface GlobalComponents {
|
||||
I18n: typeof I18n;
|
||||
RouterView: typeof RouterView;
|
||||
Mfm: typeof Mfm;
|
||||
MkA: typeof MkA;
|
||||
MkAcct: typeof MkAcct;
|
||||
@ -51,7 +54,7 @@ declare module '@vue/runtime-core' {
|
||||
MkLoading: typeof MkLoading;
|
||||
MkError: typeof MkError;
|
||||
MkAd: typeof MkAd;
|
||||
MkHeader: typeof MkHeader;
|
||||
MkPageHeader: typeof MkPageHeader;
|
||||
MkSpacer: typeof MkSpacer;
|
||||
MkStickyContainer: typeof MkStickyContainer;
|
||||
}
|
||||
|
@ -1,163 +1,118 @@
|
||||
<template>
|
||||
<MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')">
|
||||
<div class="hrmcaedk _window _narrow_" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }">
|
||||
<div ref="rootEl" class="hrmcaedk _window _narrow_" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }">
|
||||
<div class="header" @contextmenu="onContextmenu">
|
||||
<button v-if="history.length > 0" v-tooltip="$ts.goBack" class="_button" @click="back()"><i class="fas fa-arrow-left"></i></button>
|
||||
<span v-else style="display: inline-block; width: 20px"></span>
|
||||
<span v-if="pageInfo" class="title">
|
||||
<i v-if="pageInfo.icon" class="icon" :class="pageInfo.icon"></i>
|
||||
<span>{{ pageInfo.title }}</span>
|
||||
<span v-if="pageMetadata?.value" class="title">
|
||||
<i v-if="pageMetadata?.value.icon" class="icon" :class="pageMetadata?.value.icon"></i>
|
||||
<span>{{ pageMetadata?.value.title }}</span>
|
||||
</span>
|
||||
<button class="_button" @click="$refs.modal.close()"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<div class="body">
|
||||
<MkStickyContainer>
|
||||
<template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template>
|
||||
<keep-alive>
|
||||
<component :is="component" v-bind="props" :ref="changePage"/>
|
||||
</keep-alive>
|
||||
<template #header><MkPageHeader v-if="pageMetadata?.value && !pageMetadata?.value.hideHeader" :info="pageMetadata?.value"/></template>
|
||||
<RouterView :router="router"/>
|
||||
</MkStickyContainer>
|
||||
</div>
|
||||
</div>
|
||||
</MkModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { ComputedRef, provide } from 'vue';
|
||||
import MkModal from '@/components/ui/modal.vue';
|
||||
import { popout } from '@/scripts/popout';
|
||||
import { popout as _popout } from '@/scripts/popout';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
||||
import { resolve } from '@/router';
|
||||
import { url } from '@/config';
|
||||
import * as symbols from '@/symbols';
|
||||
import * as os from '@/os';
|
||||
import { mainRouter, routes } from '@/router';
|
||||
import { i18n } from '@/i18n';
|
||||
import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
|
||||
import { Router } from '@/nirax';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkModal,
|
||||
},
|
||||
const props = defineProps<{
|
||||
initialPath: string;
|
||||
}>();
|
||||
|
||||
inject: {
|
||||
sideViewHook: {
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
defineEmits<{
|
||||
(ev: 'closed'): void;
|
||||
(ev: 'click'): void;
|
||||
}>();
|
||||
|
||||
provide() {
|
||||
return {
|
||||
navHook: (path) => {
|
||||
this.navigate(path);
|
||||
},
|
||||
shouldHeaderThin: true,
|
||||
};
|
||||
},
|
||||
const router = new Router(routes, props.initialPath);
|
||||
|
||||
props: {
|
||||
initialPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
initialComponent: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
initialProps: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['closed'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
width: 860,
|
||||
height: 660,
|
||||
pageInfo: null,
|
||||
path: this.initialPath,
|
||||
component: this.initialComponent,
|
||||
props: this.initialProps,
|
||||
history: [],
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
url(): string {
|
||||
return url + this.path;
|
||||
},
|
||||
|
||||
contextmenu() {
|
||||
return [{
|
||||
type: 'label',
|
||||
text: this.path,
|
||||
}, {
|
||||
icon: 'fas fa-expand-alt',
|
||||
text: this.$ts.showInPage,
|
||||
action: this.expand,
|
||||
}, this.sideViewHook ? {
|
||||
icon: 'fas fa-columns',
|
||||
text: this.$ts.openInSideView,
|
||||
action: () => {
|
||||
this.sideViewHook(this.path);
|
||||
this.$refs.window.close();
|
||||
},
|
||||
} : undefined, {
|
||||
icon: 'fas fa-external-link-alt',
|
||||
text: this.$ts.popout,
|
||||
action: this.popout,
|
||||
}, null, {
|
||||
icon: 'fas fa-external-link-alt',
|
||||
text: this.$ts.openInNewTab,
|
||||
action: () => {
|
||||
window.open(this.url, '_blank');
|
||||
this.$refs.window.close();
|
||||
},
|
||||
}, {
|
||||
icon: 'fas fa-link',
|
||||
text: this.$ts.copyLink,
|
||||
action: () => {
|
||||
copyToClipboard(this.url);
|
||||
},
|
||||
}];
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
changePage(page) {
|
||||
if (page == null) return;
|
||||
if (page[symbols.PAGE_INFO]) {
|
||||
this.pageInfo = page[symbols.PAGE_INFO];
|
||||
}
|
||||
},
|
||||
|
||||
navigate(path, record = true) {
|
||||
if (record) this.history.push(this.path);
|
||||
this.path = path;
|
||||
const { component, props } = resolve(path);
|
||||
this.component = component;
|
||||
this.props = props;
|
||||
},
|
||||
|
||||
back() {
|
||||
this.navigate(this.history.pop(), false);
|
||||
},
|
||||
|
||||
expand() {
|
||||
this.$router.push(this.path);
|
||||
this.$refs.window.close();
|
||||
},
|
||||
|
||||
popout() {
|
||||
popout(this.path, this.$el);
|
||||
this.$refs.window.close();
|
||||
},
|
||||
|
||||
onContextmenu(ev: MouseEvent) {
|
||||
os.contextMenu(this.contextmenu, ev);
|
||||
},
|
||||
},
|
||||
router.addListener('push', ctx => {
|
||||
|
||||
});
|
||||
|
||||
let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
|
||||
let rootEl = $ref();
|
||||
let modal = $ref<InstanceType<typeof MkModal>>();
|
||||
let path = $ref(props.initialPath);
|
||||
let width = $ref(860);
|
||||
let height = $ref(660);
|
||||
const history = [];
|
||||
|
||||
provide('router', router);
|
||||
provideMetadataReceiver((info) => {
|
||||
pageMetadata = info;
|
||||
});
|
||||
provide('shouldOmitHeaderTitle', true);
|
||||
provide('shouldHeaderThin', true);
|
||||
|
||||
const pageUrl = $computed(() => url + path);
|
||||
const contextmenu = $computed(() => {
|
||||
return [{
|
||||
type: 'label',
|
||||
text: path,
|
||||
}, {
|
||||
icon: 'fas fa-expand-alt',
|
||||
text: i18n.ts.showInPage,
|
||||
action: expand,
|
||||
}, {
|
||||
icon: 'fas fa-external-link-alt',
|
||||
text: i18n.ts.popout,
|
||||
action: popout,
|
||||
}, null, {
|
||||
icon: 'fas fa-external-link-alt',
|
||||
text: i18n.ts.openInNewTab,
|
||||
action: () => {
|
||||
window.open(pageUrl, '_blank');
|
||||
modal.close();
|
||||
},
|
||||
}, {
|
||||
icon: 'fas fa-link',
|
||||
text: i18n.ts.copyLink,
|
||||
action: () => {
|
||||
copyToClipboard(pageUrl);
|
||||
},
|
||||
}];
|
||||
});
|
||||
|
||||
function navigate(path, record = true) {
|
||||
if (record) history.push(router.getCurrentPath());
|
||||
router.push(path);
|
||||
}
|
||||
|
||||
function back() {
|
||||
navigate(history.pop(), false);
|
||||
}
|
||||
|
||||
function expand() {
|
||||
mainRouter.push(path);
|
||||
modal.close();
|
||||
}
|
||||
|
||||
function popout() {
|
||||
_popout(path, rootEl);
|
||||
modal.close();
|
||||
}
|
||||
|
||||
function onContextmenu(ev: MouseEvent) {
|
||||
os.contextMenu(contextmenu, ev);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -225,7 +225,7 @@ function undoReact(note): void {
|
||||
});
|
||||
}
|
||||
|
||||
const currentClipPage = inject<Ref<misskey.entities.Clip>>('currentClipPage');
|
||||
const currentClipPage = inject<Ref<misskey.entities.Clip> | null>('currentClipPage', null);
|
||||
|
||||
function onContextmenu(ev: MouseEvent): void {
|
||||
const isLink = (el: HTMLElement) => {
|
||||
|
@ -1,186 +1,135 @@
|
||||
<template>
|
||||
<XWindow ref="window"
|
||||
<XWindow
|
||||
ref="windowEl"
|
||||
:initial-width="500"
|
||||
:initial-height="500"
|
||||
:can-resize="true"
|
||||
:close-button="true"
|
||||
:buttons-left="buttonsLeft"
|
||||
:buttons-right="buttonsRight"
|
||||
:contextmenu="contextmenu"
|
||||
@closed="$emit('closed')"
|
||||
>
|
||||
<template #header>
|
||||
<template v-if="pageInfo">
|
||||
<i v-if="pageInfo.icon" class="icon" :class="pageInfo.icon" style="margin-right: 0.5em;"></i>
|
||||
<span>{{ pageInfo.title }}</span>
|
||||
<template v-if="pageMetadata?.value">
|
||||
<i v-if="pageMetadata.value.icon" class="icon" :class="pageMetadata.value.icon" style="margin-right: 0.5em;"></i>
|
||||
<span>{{ pageMetadata.value.title }}</span>
|
||||
</template>
|
||||
</template>
|
||||
<template #headerLeft>
|
||||
<button v-if="history.length > 0" v-tooltip="$ts.goBack" class="_button" @click="back()"><i class="fas fa-arrow-left"></i></button>
|
||||
</template>
|
||||
<template #headerRight>
|
||||
<button v-tooltip="$ts.showInPage" class="_button" @click="expand()"><i class="fas fa-expand-alt"></i></button>
|
||||
<button v-tooltip="$ts.popout" class="_button" @click="popout()"><i class="fas fa-external-link-alt"></i></button>
|
||||
<button class="_button" @click="menu"><i class="fas fa-ellipsis-h"></i></button>
|
||||
</template>
|
||||
|
||||
<div class="yrolvcoq" :style="{ background: pageInfo?.bg }">
|
||||
<MkStickyContainer>
|
||||
<template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template>
|
||||
<component :is="component" v-bind="props" :ref="changePage"/>
|
||||
</MkStickyContainer>
|
||||
<div class="yrolvcoq" :style="{ background: pageMetadata?.value?.bg }">
|
||||
<RouterView :router="router"/>
|
||||
</div>
|
||||
</XWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { ComputedRef, inject, provide } from 'vue';
|
||||
import RouterView from './global/router-view.vue';
|
||||
import XWindow from '@/components/ui/window.vue';
|
||||
import { popout } from '@/scripts/popout';
|
||||
import { popout as _popout } from '@/scripts/popout';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
||||
import { resolve } from '@/router';
|
||||
import { url } from '@/config';
|
||||
import * as symbols from '@/symbols';
|
||||
import * as os from '@/os';
|
||||
import { mainRouter, routes } from '@/router';
|
||||
import { Router } from '@/nirax';
|
||||
import { i18n } from '@/i18n';
|
||||
import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XWindow,
|
||||
const props = defineProps<{
|
||||
initialPath: string;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const router = new Router(routes, props.initialPath);
|
||||
|
||||
let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
|
||||
let windowEl = $ref<InstanceType<typeof XWindow>>();
|
||||
const history = $ref<string[]>([props.initialPath]);
|
||||
const buttonsLeft = $computed(() => {
|
||||
const buttons = [];
|
||||
|
||||
if (history.length > 1) {
|
||||
buttons.push({
|
||||
icon: 'fas fa-arrow-left',
|
||||
onClick: back,
|
||||
});
|
||||
}
|
||||
|
||||
return buttons;
|
||||
});
|
||||
const buttonsRight = $computed(() => {
|
||||
const buttons = [{
|
||||
icon: 'fas fa-expand-alt',
|
||||
title: i18n.ts.showInPage,
|
||||
onClick: expand,
|
||||
}];
|
||||
|
||||
return buttons;
|
||||
});
|
||||
|
||||
router.addListener('push', ctx => {
|
||||
history.push(router.getCurrentPath());
|
||||
});
|
||||
|
||||
provide('router', router);
|
||||
provideMetadataReceiver((info) => {
|
||||
pageMetadata = info;
|
||||
});
|
||||
provide('shouldOmitHeaderTitle', true);
|
||||
provide('shouldHeaderThin', true);
|
||||
|
||||
const contextmenu = $computed(() => ([{
|
||||
icon: 'fas fa-expand-alt',
|
||||
text: i18n.ts.showInPage,
|
||||
action: expand,
|
||||
}, {
|
||||
icon: 'fas fa-external-link-alt',
|
||||
text: i18n.ts.popout,
|
||||
action: popout,
|
||||
}, {
|
||||
icon: 'fas fa-external-link-alt',
|
||||
text: i18n.ts.openInNewTab,
|
||||
action: () => {
|
||||
window.open(url + router.getCurrentPath(), '_blank');
|
||||
windowEl.close();
|
||||
},
|
||||
|
||||
inject: {
|
||||
sideViewHook: {
|
||||
default: null
|
||||
}
|
||||
}, {
|
||||
icon: 'fas fa-link',
|
||||
text: i18n.ts.copyLink,
|
||||
action: () => {
|
||||
copyToClipboard(url + router.getCurrentPath());
|
||||
},
|
||||
}]));
|
||||
|
||||
provide() {
|
||||
return {
|
||||
navHook: (path) => {
|
||||
this.navigate(path);
|
||||
},
|
||||
shouldHeaderThin: true,
|
||||
};
|
||||
},
|
||||
function menu(ev) {
|
||||
os.popupMenu(contextmenu, ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
props: {
|
||||
initialPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
initialComponent: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
initialProps: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
function back() {
|
||||
history.pop();
|
||||
router.change(history[history.length - 1]);
|
||||
}
|
||||
|
||||
emits: ['closed'],
|
||||
function close() {
|
||||
windowEl.close();
|
||||
}
|
||||
|
||||
data() {
|
||||
return {
|
||||
pageInfo: null,
|
||||
path: this.initialPath,
|
||||
component: this.initialComponent,
|
||||
props: this.initialProps,
|
||||
history: [],
|
||||
};
|
||||
},
|
||||
function expand() {
|
||||
mainRouter.push(router.getCurrentPath());
|
||||
windowEl.close();
|
||||
}
|
||||
|
||||
computed: {
|
||||
url(): string {
|
||||
return url + this.path;
|
||||
},
|
||||
function popout() {
|
||||
_popout(router.getCurrentPath(), windowEl.$el);
|
||||
windowEl.close();
|
||||
}
|
||||
|
||||
contextmenu() {
|
||||
return [{
|
||||
type: 'label',
|
||||
text: this.path,
|
||||
}, {
|
||||
icon: 'fas fa-expand-alt',
|
||||
text: this.$ts.showInPage,
|
||||
action: this.expand
|
||||
}, this.sideViewHook ? {
|
||||
icon: 'fas fa-columns',
|
||||
text: this.$ts.openInSideView,
|
||||
action: () => {
|
||||
this.sideViewHook(this.path);
|
||||
this.$refs.window.close();
|
||||
}
|
||||
} : undefined, {
|
||||
icon: 'fas fa-external-link-alt',
|
||||
text: this.$ts.popout,
|
||||
action: this.popout
|
||||
}, null, {
|
||||
icon: 'fas fa-external-link-alt',
|
||||
text: this.$ts.openInNewTab,
|
||||
action: () => {
|
||||
window.open(this.url, '_blank');
|
||||
this.$refs.window.close();
|
||||
}
|
||||
}, {
|
||||
icon: 'fas fa-link',
|
||||
text: this.$ts.copyLink,
|
||||
action: () => {
|
||||
copyToClipboard(this.url);
|
||||
}
|
||||
}];
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
changePage(page) {
|
||||
if (page == null) return;
|
||||
if (page[symbols.PAGE_INFO]) {
|
||||
this.pageInfo = page[symbols.PAGE_INFO];
|
||||
}
|
||||
},
|
||||
|
||||
navigate(path, record = true) {
|
||||
if (record) this.history.push(this.path);
|
||||
this.path = path;
|
||||
const { component, props } = resolve(path);
|
||||
this.component = component;
|
||||
this.props = props;
|
||||
},
|
||||
|
||||
menu(ev) {
|
||||
os.popupMenu([{
|
||||
icon: 'fas fa-external-link-alt',
|
||||
text: this.$ts.openInNewTab,
|
||||
action: () => {
|
||||
window.open(this.url, '_blank');
|
||||
this.$refs.window.close();
|
||||
}
|
||||
}, {
|
||||
icon: 'fas fa-link',
|
||||
text: this.$ts.copyLink,
|
||||
action: () => {
|
||||
copyToClipboard(this.url);
|
||||
}
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
},
|
||||
|
||||
back() {
|
||||
this.navigate(this.history.pop(), false);
|
||||
},
|
||||
|
||||
close() {
|
||||
this.$refs.window.close();
|
||||
},
|
||||
|
||||
expand() {
|
||||
this.$router.push(this.path);
|
||||
this.$refs.window.close();
|
||||
},
|
||||
|
||||
popout() {
|
||||
popout(this.path, this.$el);
|
||||
this.$refs.window.close();
|
||||
},
|
||||
},
|
||||
defineExpose({
|
||||
close,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -4,14 +4,14 @@
|
||||
<div class="body _window _shadow _narrow_" @mousedown="onBodyMousedown" @keydown="onKeydown">
|
||||
<div class="header" :class="{ mini }" @contextmenu.prevent.stop="onContextmenu">
|
||||
<span class="left">
|
||||
<slot name="headerLeft"></slot>
|
||||
<button v-for="button in buttonsLeft" v-tooltip="button.title" class="button _button" :class="{ highlighted: button.highlighted }" @click="button.onClick"><i :class="button.icon"></i></button>
|
||||
</span>
|
||||
<span class="title" @mousedown.prevent="onHeaderMousedown" @touchstart.prevent="onHeaderMousedown">
|
||||
<slot name="header"></slot>
|
||||
</span>
|
||||
<span class="right">
|
||||
<slot name="headerRight"></slot>
|
||||
<button v-if="closeButton" class="_button" @click="close()"><i class="fas fa-times"></i></button>
|
||||
<button v-for="button in buttonsRight" v-tooltip="button.title" class="button _button" :class="{ highlighted: button.highlighted }" @click="button.onClick"><i :class="button.icon"></i></button>
|
||||
<button v-if="closeButton" class="button _button" @click="close()"><i class="fas fa-times"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="padding" class="body">
|
||||
@ -46,41 +46,41 @@ const minHeight = 50;
|
||||
const minWidth = 250;
|
||||
|
||||
function dragListen(fn) {
|
||||
window.addEventListener('mousemove', fn);
|
||||
window.addEventListener('touchmove', fn);
|
||||
window.addEventListener('mousemove', fn);
|
||||
window.addEventListener('touchmove', fn);
|
||||
window.addEventListener('mouseleave', dragClear.bind(null, fn));
|
||||
window.addEventListener('mouseup', dragClear.bind(null, fn));
|
||||
window.addEventListener('touchend', dragClear.bind(null, fn));
|
||||
window.addEventListener('mouseup', dragClear.bind(null, fn));
|
||||
window.addEventListener('touchend', dragClear.bind(null, fn));
|
||||
}
|
||||
|
||||
function dragClear(fn) {
|
||||
window.removeEventListener('mousemove', fn);
|
||||
window.removeEventListener('touchmove', fn);
|
||||
window.removeEventListener('mousemove', fn);
|
||||
window.removeEventListener('touchmove', fn);
|
||||
window.removeEventListener('mouseleave', dragClear);
|
||||
window.removeEventListener('mouseup', dragClear);
|
||||
window.removeEventListener('touchend', dragClear);
|
||||
window.removeEventListener('mouseup', dragClear);
|
||||
window.removeEventListener('touchend', dragClear);
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
provide: {
|
||||
inWindow: true
|
||||
inWindow: true,
|
||||
},
|
||||
|
||||
props: {
|
||||
padding: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
initialWidth: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 400
|
||||
default: 400,
|
||||
},
|
||||
initialHeight: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
canResize: {
|
||||
type: Boolean,
|
||||
@ -105,7 +105,17 @@ export default defineComponent({
|
||||
contextmenu: {
|
||||
type: Array,
|
||||
required: false,
|
||||
}
|
||||
},
|
||||
buttonsLeft: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: [],
|
||||
},
|
||||
buttonsRight: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: [],
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['closed'],
|
||||
@ -162,7 +172,10 @@ export default defineComponent({
|
||||
this.top();
|
||||
},
|
||||
|
||||
onHeaderMousedown(evt) {
|
||||
onHeaderMousedown(evt: MouseEvent) {
|
||||
// 右クリックはコンテキストメニューを開こうとした可能性が高いため無視
|
||||
if (evt.button === 2) return;
|
||||
|
||||
const main = this.$el as any;
|
||||
|
||||
if (!contains(main, document.activeElement)) main.focus();
|
||||
@ -356,12 +369,12 @@ export default defineComponent({
|
||||
const browserHeight = window.innerHeight;
|
||||
const windowWidth = main.offsetWidth;
|
||||
const windowHeight = main.offsetHeight;
|
||||
if (position.left < 0) main.style.left = 0; // 左はみ出し
|
||||
if (position.top + windowHeight > browserHeight) main.style.top = browserHeight - windowHeight + 'px'; // 下はみ出し
|
||||
if (position.left + windowWidth > browserWidth) main.style.left = browserWidth - windowWidth + 'px'; // 右はみ出し
|
||||
if (position.top < 0) main.style.top = 0; // 上はみ出し
|
||||
}
|
||||
}
|
||||
if (position.left < 0) main.style.left = 0; // 左はみ出し
|
||||
if (position.top + windowHeight > browserHeight) main.style.top = browserHeight - windowHeight + 'px'; // 下はみ出し
|
||||
if (position.left + windowWidth > browserWidth) main.style.left = browserWidth - windowWidth + 'px'; // 右はみ出し
|
||||
if (position.top < 0) main.style.top = 0; // 上はみ出し
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -404,17 +417,25 @@ export default defineComponent({
|
||||
border-bottom: solid 1px var(--divider);
|
||||
|
||||
> .left, > .right {
|
||||
> ::v-deep(button) {
|
||||
> .button {
|
||||
height: var(--height);
|
||||
width: var(--height);
|
||||
|
||||
&:hover {
|
||||
color: var(--fgHighlighted);
|
||||
}
|
||||
|
||||
&.highlighted {
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .left {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
> .right {
|
||||
min-width: 16px;
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,6 @@ import widgets from '@/widgets';
|
||||
import directives from '@/directives';
|
||||
import components from '@/components';
|
||||
import { version, ui, lang, host } from '@/config';
|
||||
import { router } from '@/router';
|
||||
import { applyTheme } from '@/scripts/theme';
|
||||
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
|
||||
import { i18n } from '@/i18n';
|
||||
@ -170,11 +169,10 @@ fetchInstanceMetaPromise.then(() => {
|
||||
|
||||
const app = createApp(
|
||||
window.location.search === '?zen' ? defineAsyncComponent(() => import('@/ui/zen.vue')) :
|
||||
!$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) :
|
||||
ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) :
|
||||
ui === 'desktop' ? defineAsyncComponent(() => import('@/ui/desktop.vue')) :
|
||||
ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) :
|
||||
defineAsyncComponent(() => import('@/ui/universal.vue'))
|
||||
!$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) :
|
||||
ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) :
|
||||
ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) :
|
||||
defineAsyncComponent(() => import('@/ui/universal.vue')),
|
||||
);
|
||||
|
||||
if (_DEV_) {
|
||||
@ -189,14 +187,10 @@ app.config.globalProperties = {
|
||||
$ts: i18n.ts,
|
||||
};
|
||||
|
||||
app.use(router);
|
||||
|
||||
widgets(app);
|
||||
directives(app);
|
||||
components(app);
|
||||
|
||||
await router.isReady();
|
||||
|
||||
const splash = document.getElementById('splash');
|
||||
// 念のためnullチェック(HTMLが古い場合があるため(そのうち消す))
|
||||
if (splash) splash.addEventListener('transitionend', () => {
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { computed, ref, reactive } from 'vue';
|
||||
import { $i } from './account';
|
||||
import { mainRouter } from '@/router';
|
||||
import { search } from '@/scripts/search';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { ui } from '@/config';
|
||||
import { $i } from './account';
|
||||
import { unisonReload } from '@/scripts/unison-reload';
|
||||
import { router } from './router';
|
||||
|
||||
export const menuDef = reactive({
|
||||
notifications: {
|
||||
@ -60,16 +60,16 @@ export const menuDef = reactive({
|
||||
title: 'lists',
|
||||
icon: 'fas fa-list-ul',
|
||||
show: computed(() => $i != null),
|
||||
active: computed(() => router.currentRoute.value.path.startsWith('/timeline/list/') || router.currentRoute.value.path === '/my/lists' || router.currentRoute.value.path.startsWith('/my/lists/')),
|
||||
active: computed(() => mainRouter.currentRoute.value.path.startsWith('/timeline/list/') || mainRouter.currentRoute.value.path === '/my/lists' || mainRouter.currentRoute.value.path.startsWith('/my/lists/')),
|
||||
action: (ev) => {
|
||||
const items = ref([{
|
||||
type: 'pending'
|
||||
type: 'pending',
|
||||
}]);
|
||||
os.api('users/lists/list').then(lists => {
|
||||
const _items = [...lists.map(list => ({
|
||||
type: 'link',
|
||||
text: list.name,
|
||||
to: `/timeline/list/${list.id}`
|
||||
to: `/timeline/list/${list.id}`,
|
||||
})), null, {
|
||||
type: 'link',
|
||||
to: '/my/lists',
|
||||
@ -91,16 +91,16 @@ export const menuDef = reactive({
|
||||
title: 'antennas',
|
||||
icon: 'fas fa-satellite',
|
||||
show: computed(() => $i != null),
|
||||
active: computed(() => router.currentRoute.value.path.startsWith('/timeline/antenna/') || router.currentRoute.value.path === '/my/antennas' || router.currentRoute.value.path.startsWith('/my/antennas/')),
|
||||
active: computed(() => mainRouter.currentRoute.value.path.startsWith('/timeline/antenna/') || mainRouter.currentRoute.value.path === '/my/antennas' || mainRouter.currentRoute.value.path.startsWith('/my/antennas/')),
|
||||
action: (ev) => {
|
||||
const items = ref([{
|
||||
type: 'pending'
|
||||
type: 'pending',
|
||||
}]);
|
||||
os.api('antennas/list').then(antennas => {
|
||||
const _items = [...antennas.map(antenna => ({
|
||||
type: 'link',
|
||||
text: antenna.name,
|
||||
to: `/timeline/antenna/${antenna.id}`
|
||||
to: `/timeline/antenna/${antenna.id}`,
|
||||
})), null, {
|
||||
type: 'link',
|
||||
to: '/my/antennas',
|
||||
@ -178,29 +178,22 @@ export const menuDef = reactive({
|
||||
action: () => {
|
||||
localStorage.setItem('ui', 'default');
|
||||
unisonReload();
|
||||
}
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.deck,
|
||||
active: ui === 'deck',
|
||||
action: () => {
|
||||
localStorage.setItem('ui', 'deck');
|
||||
unisonReload();
|
||||
}
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.classic,
|
||||
active: ui === 'classic',
|
||||
action: () => {
|
||||
localStorage.setItem('ui', 'classic');
|
||||
unisonReload();
|
||||
}
|
||||
}, /*{
|
||||
text: i18n.ts.desktop + ' (β)',
|
||||
active: ui === 'desktop',
|
||||
action: () => {
|
||||
localStorage.setItem('ui', 'desktop');
|
||||
unisonReload();
|
||||
}
|
||||
}*/], ev.currentTarget ?? ev.target);
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
200
packages/client/src/nirax.ts
Normal file
200
packages/client/src/nirax.ts
Normal file
@ -0,0 +1,200 @@
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { Ref, Component, ref, shallowRef, ShallowRef } from 'vue';
|
||||
|
||||
type RouteDef = {
|
||||
path: string;
|
||||
component: Component;
|
||||
query?: Record<string, string>;
|
||||
name?: string;
|
||||
globalCacheKey?: string;
|
||||
};
|
||||
|
||||
type ParsedPath = (string | {
|
||||
name: string;
|
||||
startsWith?: string;
|
||||
wildcard?: boolean;
|
||||
optional?: boolean;
|
||||
})[];
|
||||
|
||||
function parsePath(path: string): ParsedPath {
|
||||
const res = [] as ParsedPath;
|
||||
|
||||
path = path.substring(1);
|
||||
|
||||
for (const part of path.split('/')) {
|
||||
if (part.includes(':')) {
|
||||
const prefix = part.substring(0, part.indexOf(':'));
|
||||
const placeholder = part.substring(part.indexOf(':') + 1);
|
||||
const wildcard = placeholder.includes('(*)');
|
||||
const optional = placeholder.endsWith('?');
|
||||
res.push({
|
||||
name: placeholder.replace('(*)', '').replace('?', ''),
|
||||
startsWith: prefix !== '' ? prefix : undefined,
|
||||
wildcard,
|
||||
optional,
|
||||
});
|
||||
} else {
|
||||
res.push(part);
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export class Router extends EventEmitter<{
|
||||
change: (ctx: {
|
||||
beforePath: string;
|
||||
path: string;
|
||||
route: RouteDef | null;
|
||||
props: Map<string, string> | null;
|
||||
key: string;
|
||||
}) => void;
|
||||
push: (ctx: {
|
||||
beforePath: string;
|
||||
path: string;
|
||||
route: RouteDef | null;
|
||||
props: Map<string, string> | null;
|
||||
key: string;
|
||||
}) => void;
|
||||
}> {
|
||||
private routes: RouteDef[];
|
||||
private currentPath: string;
|
||||
private currentComponent: Component | null = null;
|
||||
private currentProps: Map<string, string> | null = null;
|
||||
private currentKey = Date.now().toString();
|
||||
|
||||
public currentRoute: ShallowRef<RouteDef | null> = shallowRef(null);
|
||||
|
||||
constructor(routes: Router['routes'], currentPath: Router['currentPath']) {
|
||||
super();
|
||||
|
||||
this.routes = routes;
|
||||
this.currentPath = currentPath;
|
||||
this.navigate(currentPath, null, true);
|
||||
}
|
||||
|
||||
public resolve(path: string): { route: RouteDef; props: Map<string, string>; } | null {
|
||||
let queryString: string | null = null;
|
||||
if (path[0] === '/') path = path.substring(1);
|
||||
if (path.includes('?')) {
|
||||
queryString = path.substring(path.indexOf('?') + 1);
|
||||
path = path.substring(0, path.indexOf('?'));
|
||||
}
|
||||
|
||||
if (_DEV_) console.log('Routing: ', path, queryString);
|
||||
|
||||
forEachRouteLoop:
|
||||
for (const route of this.routes) {
|
||||
let parts = path.split('/');
|
||||
const props = new Map<string, string>();
|
||||
|
||||
pathMatchLoop:
|
||||
for (const p of parsePath(route.path)) {
|
||||
if (typeof p === 'string') {
|
||||
if (p === parts[0]) {
|
||||
parts.shift();
|
||||
} else {
|
||||
continue forEachRouteLoop;
|
||||
}
|
||||
} else {
|
||||
if (parts[0] == null && !p.optional) {
|
||||
continue forEachRouteLoop;
|
||||
}
|
||||
if (p.wildcard) {
|
||||
if (parts.length !== 0) {
|
||||
props.set(p.name, parts.join('/'));
|
||||
parts = [];
|
||||
}
|
||||
break pathMatchLoop;
|
||||
} else {
|
||||
if (p.startsWith && (parts[0] == null || !parts[0].startsWith(p.startsWith))) continue forEachRouteLoop;
|
||||
|
||||
props.set(p.name, parts[0]);
|
||||
parts.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (parts.length !== 0) continue forEachRouteLoop;
|
||||
|
||||
if (route.query != null && queryString != null) {
|
||||
const queryObject = [...new URLSearchParams(queryString).entries()]
|
||||
.reduce((obj, entry) => ({ ...obj, [entry[0]]: entry[1] }), {});
|
||||
|
||||
for (const q in route.query) {
|
||||
const as = route.query[q];
|
||||
if (queryObject[q]) {
|
||||
props.set(as, queryObject[q]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
route,
|
||||
props,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private navigate(path: string, key: string | null | undefined, initial = false) {
|
||||
const beforePath = this.currentPath;
|
||||
const beforeRoute = this.currentRoute.value;
|
||||
this.currentPath = path;
|
||||
|
||||
const res = this.resolve(this.currentPath);
|
||||
|
||||
if (res == null) {
|
||||
throw new Error('no route found for: ' + path);
|
||||
}
|
||||
|
||||
const isSamePath = beforePath === path;
|
||||
if (isSamePath && key == null) key = this.currentKey;
|
||||
this.currentComponent = res.route.component;
|
||||
this.currentProps = res.props;
|
||||
this.currentRoute.value = res.route;
|
||||
this.currentKey = this.currentRoute.value.globalCacheKey ?? key ?? Date.now().toString();
|
||||
|
||||
if (!initial) {
|
||||
this.emit('change', {
|
||||
beforePath,
|
||||
path,
|
||||
route: this.currentRoute.value,
|
||||
props: this.currentProps,
|
||||
key: this.currentKey,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public getCurrentComponent() {
|
||||
return this.currentComponent;
|
||||
}
|
||||
|
||||
public getCurrentProps() {
|
||||
return this.currentProps;
|
||||
}
|
||||
|
||||
public getCurrentPath() {
|
||||
return this.currentPath;
|
||||
}
|
||||
|
||||
public getCurrentKey() {
|
||||
return this.currentKey;
|
||||
}
|
||||
|
||||
public push(path: string) {
|
||||
const beforePath = this.currentPath;
|
||||
this.navigate(path, null);
|
||||
this.emit('push', {
|
||||
beforePath,
|
||||
path,
|
||||
route: this.currentRoute.value,
|
||||
props: this.currentProps,
|
||||
key: this.currentKey,
|
||||
});
|
||||
}
|
||||
|
||||
public change(path: string, key?: string | null) {
|
||||
this.navigate(path, key);
|
||||
}
|
||||
}
|
@ -8,7 +8,6 @@ import { apiUrl, url } from '@/config';
|
||||
import MkPostFormDialog from '@/components/post-form-dialog.vue';
|
||||
import MkWaitingDialog from '@/components/waiting-dialog.vue';
|
||||
import { MenuItem } from '@/types/menu';
|
||||
import { resolve } from '@/router';
|
||||
import { $i } from '@/account';
|
||||
|
||||
export const pendingApiRequestsCount = ref(0);
|
||||
@ -155,20 +154,14 @@ export async function popup(component: Component, props: Record<string, any>, ev
|
||||
}
|
||||
|
||||
export function pageWindow(path: string) {
|
||||
const { component, props } = resolve(path);
|
||||
popup(defineAsyncComponent(() => import('@/components/page-window.vue')), {
|
||||
initialPath: path,
|
||||
initialComponent: markRaw(component),
|
||||
initialProps: props,
|
||||
}, {}, 'closed');
|
||||
}
|
||||
|
||||
export function modalPageWindow(path: string) {
|
||||
const { component, props } = resolve(path);
|
||||
popup(defineAsyncComponent(() => import('@/components/modal-page-window.vue')), {
|
||||
initialPath: path,
|
||||
initialComponent: markRaw(component),
|
||||
initialProps: props,
|
||||
}, {}, 'closed');
|
||||
}
|
||||
|
||||
|
@ -21,11 +21,11 @@
|
||||
import { } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import * as symbols from '@/symbols';
|
||||
import { version } from '@/config';
|
||||
import * as os from '@/os';
|
||||
import { unisonReload } from '@/scripts/unison-reload';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
error?: Error;
|
||||
@ -52,11 +52,13 @@ function reload() {
|
||||
unisonReload();
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.error,
|
||||
icon: 'fas fa-exclamation-triangle',
|
||||
},
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.error,
|
||||
icon: 'fas fa-exclamation-triangle',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -1,62 +1,65 @@
|
||||
<template>
|
||||
<div style="overflow: clip;">
|
||||
<MkSpacer :content-max="600" :margin-min="20">
|
||||
<div class="_formRoot znqjceqz">
|
||||
<div id="debug"></div>
|
||||
<div ref="containerEl" v-panel class="_formBlock about" :class="{ playing: easterEggEngine != null }">
|
||||
<img src="/client-assets/about-icon.png" alt="" class="icon" draggable="false" @load="iconLoaded" @click="gravity"/>
|
||||
<div class="misskey">Misskey</div>
|
||||
<div class="version">v{{ version }}</div>
|
||||
<span v-for="emoji in easterEggEmojis" :key="emoji.id" class="emoji" :data-physics-x="emoji.left" :data-physics-y="emoji.top" :class="{ _physics_circle_: !emoji.emoji.startsWith(':') }"><MkEmoji class="emoji" :emoji="emoji.emoji" :custom-emojis="$instance.emojis" :is-reaction="false" :normal="true" :no-style="true"/></span>
|
||||
</div>
|
||||
<div class="_formBlock" style="text-align: center;">
|
||||
{{ i18n.ts._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ i18n.ts.learnMore }}</a>
|
||||
</div>
|
||||
<div class="_formBlock" style="text-align: center;">
|
||||
<MkButton primary rounded inline @click="iLoveMisskey">I <Mfm text="$[jelly ❤]"/> #Misskey</MkButton>
|
||||
</div>
|
||||
<FormSection>
|
||||
<div class="_formLinks">
|
||||
<FormLink to="https://github.com/misskey-dev/misskey" external>
|
||||
<template #icon><i class="fas fa-code"></i></template>
|
||||
{{ i18n.ts._aboutMisskey.source }}
|
||||
<template #suffix>GitHub</template>
|
||||
</FormLink>
|
||||
<FormLink to="https://crowdin.com/project/misskey" external>
|
||||
<template #icon><i class="fas fa-language"></i></template>
|
||||
{{ i18n.ts._aboutMisskey.translation }}
|
||||
<template #suffix>Crowdin</template>
|
||||
</FormLink>
|
||||
<FormLink to="https://www.patreon.com/syuilo" external>
|
||||
<template #icon><i class="fas fa-hand-holding-medical"></i></template>
|
||||
{{ i18n.ts._aboutMisskey.donate }}
|
||||
<template #suffix>Patreon</template>
|
||||
</FormLink>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<div style="overflow: clip;">
|
||||
<MkSpacer :content-max="600" :margin-min="20">
|
||||
<div class="_formRoot znqjceqz">
|
||||
<div id="debug"></div>
|
||||
<div ref="containerEl" v-panel class="_formBlock about" :class="{ playing: easterEggEngine != null }">
|
||||
<img src="/client-assets/about-icon.png" alt="" class="icon" draggable="false" @load="iconLoaded" @click="gravity"/>
|
||||
<div class="misskey">Misskey</div>
|
||||
<div class="version">v{{ version }}</div>
|
||||
<span v-for="emoji in easterEggEmojis" :key="emoji.id" class="emoji" :data-physics-x="emoji.left" :data-physics-y="emoji.top" :class="{ _physics_circle_: !emoji.emoji.startsWith(':') }"><MkEmoji class="emoji" :emoji="emoji.emoji" :custom-emojis="$instance.emojis" :is-reaction="false" :normal="true" :no-style="true"/></span>
|
||||
</div>
|
||||
</FormSection>
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts._aboutMisskey.contributors }}</template>
|
||||
<div class="_formLinks">
|
||||
<FormLink to="https://github.com/syuilo" external>@syuilo</FormLink>
|
||||
<FormLink to="https://github.com/AyaMorisawa" external>@AyaMorisawa</FormLink>
|
||||
<FormLink to="https://github.com/mei23" external>@mei23</FormLink>
|
||||
<FormLink to="https://github.com/acid-chicken" external>@acid-chicken</FormLink>
|
||||
<FormLink to="https://github.com/tamaina" external>@tamaina</FormLink>
|
||||
<FormLink to="https://github.com/rinsuki" external>@rinsuki</FormLink>
|
||||
<FormLink to="https://github.com/Xeltica" external>@Xeltica</FormLink>
|
||||
<FormLink to="https://github.com/u1-liquid" external>@u1-liquid</FormLink>
|
||||
<FormLink to="https://github.com/marihachi" external>@marihachi</FormLink>
|
||||
<div class="_formBlock" style="text-align: center;">
|
||||
{{ i18n.ts._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ i18n.ts.learnMore }}</a>
|
||||
</div>
|
||||
<template #caption><MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ i18n.ts._aboutMisskey.allContributors }}</MkLink></template>
|
||||
</FormSection>
|
||||
<FormSection>
|
||||
<template #label><Mfm text="$[jelly ❤]"/> {{ i18n.ts._aboutMisskey.patrons }}</template>
|
||||
<div v-for="patron in patrons" :key="patron">{{ patron }}</div>
|
||||
<template #caption>{{ i18n.ts._aboutMisskey.morePatrons }}</template>
|
||||
</FormSection>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
<div class="_formBlock" style="text-align: center;">
|
||||
<MkButton primary rounded inline @click="iLoveMisskey">I <Mfm text="$[jelly ❤]"/> #Misskey</MkButton>
|
||||
</div>
|
||||
<FormSection>
|
||||
<div class="_formLinks">
|
||||
<FormLink to="https://github.com/misskey-dev/misskey" external>
|
||||
<template #icon><i class="fas fa-code"></i></template>
|
||||
{{ i18n.ts._aboutMisskey.source }}
|
||||
<template #suffix>GitHub</template>
|
||||
</FormLink>
|
||||
<FormLink to="https://crowdin.com/project/misskey" external>
|
||||
<template #icon><i class="fas fa-language"></i></template>
|
||||
{{ i18n.ts._aboutMisskey.translation }}
|
||||
<template #suffix>Crowdin</template>
|
||||
</FormLink>
|
||||
<FormLink to="https://www.patreon.com/syuilo" external>
|
||||
<template #icon><i class="fas fa-hand-holding-medical"></i></template>
|
||||
{{ i18n.ts._aboutMisskey.donate }}
|
||||
<template #suffix>Patreon</template>
|
||||
</FormLink>
|
||||
</div>
|
||||
</FormSection>
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts._aboutMisskey.contributors }}</template>
|
||||
<div class="_formLinks">
|
||||
<FormLink to="https://github.com/syuilo" external>@syuilo</FormLink>
|
||||
<FormLink to="https://github.com/AyaMorisawa" external>@AyaMorisawa</FormLink>
|
||||
<FormLink to="https://github.com/mei23" external>@mei23</FormLink>
|
||||
<FormLink to="https://github.com/acid-chicken" external>@acid-chicken</FormLink>
|
||||
<FormLink to="https://github.com/tamaina" external>@tamaina</FormLink>
|
||||
<FormLink to="https://github.com/rinsuki" external>@rinsuki</FormLink>
|
||||
<FormLink to="https://github.com/Xeltica" external>@Xeltica</FormLink>
|
||||
<FormLink to="https://github.com/u1-liquid" external>@u1-liquid</FormLink>
|
||||
<FormLink to="https://github.com/marihachi" external>@marihachi</FormLink>
|
||||
</div>
|
||||
<template #caption><MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ i18n.ts._aboutMisskey.allContributors }}</MkLink></template>
|
||||
</FormSection>
|
||||
<FormSection>
|
||||
<template #label><Mfm text="$[jelly ❤]"/> {{ i18n.ts._aboutMisskey.patrons }}</template>
|
||||
<div v-for="patron in patrons" :key="patron">{{ patron }}</div>
|
||||
<template #caption>{{ i18n.ts._aboutMisskey.morePatrons }}</template>
|
||||
</FormSection>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@ -67,10 +70,10 @@ import FormSection from '@/components/form/section.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkLink from '@/components/link.vue';
|
||||
import { physics } from '@/scripts/physics';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
import { defaultStore } from '@/store';
|
||||
import * as os from '@/os';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const patrons = [
|
||||
'まっちゃとーにゅ',
|
||||
@ -194,12 +197,14 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.aboutMisskey,
|
||||
icon: null,
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.aboutMisskey,
|
||||
icon: null,
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -1,78 +1,81 @@
|
||||
<template>
|
||||
<MkSpacer v-if="tab === 'overview'" :content-max="600" :margin-min="20">
|
||||
<div class="_formRoot">
|
||||
<div class="_formBlock fwhjspax" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }">
|
||||
<div class="content">
|
||||
<img :src="$instance.iconUrl || $instance.faviconUrl || '/favicon.ico'" alt="" class="icon"/>
|
||||
<div class="name">
|
||||
<b>{{ $instance.name || host }}</b>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer v-if="tab === 'overview'" :content-max="600" :margin-min="20">
|
||||
<div class="_formRoot">
|
||||
<div class="_formBlock fwhjspax" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }">
|
||||
<div class="content">
|
||||
<img :src="$instance.iconUrl || $instance.faviconUrl || '/favicon.ico'" alt="" class="icon"/>
|
||||
<div class="name">
|
||||
<b>{{ $instance.name || host }}</b>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MkKeyValue class="_formBlock">
|
||||
<template #key>{{ $ts.description }}</template>
|
||||
<template #value>{{ $instance.description }}</template>
|
||||
</MkKeyValue>
|
||||
|
||||
<FormSection>
|
||||
<MkKeyValue class="_formBlock" :copy="version">
|
||||
<template #key>Misskey</template>
|
||||
<template #value>{{ version }}</template>
|
||||
<MkKeyValue class="_formBlock">
|
||||
<template #key>{{ $ts.description }}</template>
|
||||
<template #value>{{ $instance.description }}</template>
|
||||
</MkKeyValue>
|
||||
<FormLink to="/about-misskey">{{ $ts.aboutMisskey }}</FormLink>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<FormSplit>
|
||||
<MkKeyValue class="_formBlock">
|
||||
<template #key>{{ $ts.administrator }}</template>
|
||||
<template #value>{{ $instance.maintainerName }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue class="_formBlock">
|
||||
<template #key>{{ $ts.contact }}</template>
|
||||
<template #value>{{ $instance.maintainerEmail }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSplit>
|
||||
<FormLink v-if="$instance.tosUrl" :to="$instance.tosUrl" class="_formBlock" external>{{ $ts.tos }}</FormLink>
|
||||
</FormSection>
|
||||
|
||||
<FormSuspense :p="initStats">
|
||||
<FormSection>
|
||||
<template #label>{{ $ts.statistics }}</template>
|
||||
<MkKeyValue class="_formBlock" :copy="version">
|
||||
<template #key>Misskey</template>
|
||||
<template #value>{{ version }}</template>
|
||||
</MkKeyValue>
|
||||
<FormLink to="/about-misskey">{{ $ts.aboutMisskey }}</FormLink>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<FormSplit>
|
||||
<MkKeyValue class="_formBlock">
|
||||
<template #key>{{ $ts.users }}</template>
|
||||
<template #value>{{ number(stats.originalUsersCount) }}</template>
|
||||
<template #key>{{ $ts.administrator }}</template>
|
||||
<template #value>{{ $instance.maintainerName }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue class="_formBlock">
|
||||
<template #key>{{ $ts.notes }}</template>
|
||||
<template #value>{{ number(stats.originalNotesCount) }}</template>
|
||||
<template #key>{{ $ts.contact }}</template>
|
||||
<template #value>{{ $instance.maintainerEmail }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSplit>
|
||||
<FormLink v-if="$instance.tosUrl" :to="$instance.tosUrl" class="_formBlock" external>{{ $ts.tos }}</FormLink>
|
||||
</FormSection>
|
||||
</FormSuspense>
|
||||
|
||||
<FormSection>
|
||||
<template #label>Well-known resources</template>
|
||||
<div class="_formLinks">
|
||||
<FormLink :to="`/.well-known/host-meta`" external>host-meta</FormLink>
|
||||
<FormLink :to="`/.well-known/host-meta.json`" external>host-meta.json</FormLink>
|
||||
<FormLink :to="`/.well-known/nodeinfo`" external>nodeinfo</FormLink>
|
||||
<FormLink :to="`/robots.txt`" external>robots.txt</FormLink>
|
||||
<FormLink :to="`/manifest.json`" external>manifest.json</FormLink>
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
<MkSpacer v-else-if="tab === 'charts'" :content-max="1200" :margin-min="20">
|
||||
<MkInstanceStats :chart-limit="500" :detailed="true"/>
|
||||
</MkSpacer>
|
||||
<FormSuspense :p="initStats">
|
||||
<FormSection>
|
||||
<template #label>{{ $ts.statistics }}</template>
|
||||
<FormSplit>
|
||||
<MkKeyValue class="_formBlock">
|
||||
<template #key>{{ $ts.users }}</template>
|
||||
<template #value>{{ number(stats.originalUsersCount) }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue class="_formBlock">
|
||||
<template #key>{{ $ts.notes }}</template>
|
||||
<template #value>{{ number(stats.originalNotesCount) }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSplit>
|
||||
</FormSection>
|
||||
</FormSuspense>
|
||||
|
||||
<FormSection>
|
||||
<template #label>Well-known resources</template>
|
||||
<div class="_formLinks">
|
||||
<FormLink :to="`/.well-known/host-meta`" external>host-meta</FormLink>
|
||||
<FormLink :to="`/.well-known/host-meta.json`" external>host-meta.json</FormLink>
|
||||
<FormLink :to="`/.well-known/nodeinfo`" external>nodeinfo</FormLink>
|
||||
<FormLink :to="`/robots.txt`" external>robots.txt</FormLink>
|
||||
<FormLink :to="`/manifest.json`" external>manifest.json</FormLink>
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
<MkSpacer v-else-if="tab === 'charts'" :content-max="1200" :margin-min="20">
|
||||
<MkInstanceStats :chart-limit="500" :detailed="true"/>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { version, instanceName } from '@/config';
|
||||
import { version, instanceName , host } from '@/config';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
@ -81,9 +84,8 @@ import MkKeyValue from '@/components/key-value.vue';
|
||||
import MkInstanceStats from '@/components/instance-stats.vue';
|
||||
import * as os from '@/os';
|
||||
import number from '@/filters/number';
|
||||
import * as symbols from '@/symbols';
|
||||
import { host } from '@/config';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
let stats = $ref(null);
|
||||
let tab = $ref('overview');
|
||||
@ -93,23 +95,24 @@ const initStats = () => os.api('stats', {
|
||||
stats = res;
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: computed(() => ({
|
||||
title: i18n.ts.instanceInfo,
|
||||
icon: 'fas fa-info-circle',
|
||||
bg: 'var(--bg)',
|
||||
tabs: [{
|
||||
active: tab === 'overview',
|
||||
title: i18n.ts.overview,
|
||||
onClick: () => { tab = 'overview'; },
|
||||
}, {
|
||||
active: tab === 'charts',
|
||||
title: i18n.ts.charts,
|
||||
icon: 'fas fa-chart-bar',
|
||||
onClick: () => { tab = 'charts'; },
|
||||
},],
|
||||
})),
|
||||
});
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => [{
|
||||
active: tab === 'overview',
|
||||
title: i18n.ts.overview,
|
||||
onClick: () => { tab = 'overview'; },
|
||||
}, {
|
||||
active: tab === 'charts',
|
||||
title: i18n.ts.charts,
|
||||
icon: 'fas fa-chart-bar',
|
||||
onClick: () => { tab = 'charts'; },
|
||||
}]);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
title: i18n.ts.instanceInfo,
|
||||
icon: 'fas fa-info-circle',
|
||||
bg: 'var(--bg)',
|
||||
})));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -1,30 +1,33 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="500" :margin-min="16" :margin-max="32">
|
||||
<div v-if="file" class="cxqhhsmd _formRoot">
|
||||
<div class="_formBlock">
|
||||
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
|
||||
<div class="info">
|
||||
<span style="margin-right: 1em;">{{ file.type }}</span>
|
||||
<span>{{ bytes(file.size) }}</span>
|
||||
<MkTime :time="file.createdAt" mode="detail" style="display: block;"/>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="500" :margin-min="16" :margin-max="32">
|
||||
<div v-if="file" class="cxqhhsmd _formRoot">
|
||||
<div class="_formBlock">
|
||||
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
|
||||
<div class="info">
|
||||
<span style="margin-right: 1em;">{{ file.type }}</span>
|
||||
<span>{{ bytes(file.size) }}</span>
|
||||
<MkTime :time="file.createdAt" mode="detail" style="display: block;"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_formBlock">
|
||||
<MkSwitch v-model="isSensitive" @update:modelValue="toggleIsSensitive">NSFW</MkSwitch>
|
||||
</div>
|
||||
<div class="_formBlock">
|
||||
<MkButton full @click="showUser"><i class="fas fa-external-link-square-alt"></i> {{ $ts.user }}</MkButton>
|
||||
</div>
|
||||
<div class="_formBlock">
|
||||
<MkButton full danger @click="del"><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</MkButton>
|
||||
</div>
|
||||
<div v-if="info" class="_formBlock">
|
||||
<details class="_content rawdata">
|
||||
<pre><code>{{ JSON.stringify(info, null, 2) }}</code></pre>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_formBlock">
|
||||
<MkSwitch v-model="isSensitive" @update:modelValue="toggleIsSensitive">NSFW</MkSwitch>
|
||||
</div>
|
||||
<div class="_formBlock">
|
||||
<MkButton full @click="showUser"><i class="fas fa-external-link-square-alt"></i> {{ $ts.user }}</MkButton>
|
||||
</div>
|
||||
<div class="_formBlock">
|
||||
<MkButton full danger @click="del"><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</MkButton>
|
||||
</div>
|
||||
<div v-if="info" class="_formBlock">
|
||||
<details class="_content rawdata">
|
||||
<pre><code>{{ JSON.stringify(info, null, 2) }}</code></pre>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@ -35,7 +38,7 @@ import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue';
|
||||
import bytes from '@/filters/bytes';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import * as symbols from '@/symbols';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
let file: any = $ref(null);
|
||||
let info: any = $ref(null);
|
||||
@ -74,13 +77,15 @@ async function toggleIsSensitive(v) {
|
||||
isSensitive = v;
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: computed(() => ({
|
||||
title: file ? i18n.ts.file + ': ' + file.name : i18n.ts.file,
|
||||
icon: 'fas fa-file',
|
||||
bg: 'var(--bg)',
|
||||
})),
|
||||
});
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
title: file ? i18n.ts.file + ': ' + file.name : i18n.ts.file,
|
||||
icon: 'fas fa-file',
|
||||
bg: 'var(--bg)',
|
||||
})));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
249
packages/client/src/pages/admin/_header_.vue
Normal file
249
packages/client/src/pages/admin/_header_.vue
Normal file
@ -0,0 +1,249 @@
|
||||
<template>
|
||||
<div ref="el" class="fdidabkc" :style="{ background: bg }" @click="onClick">
|
||||
<template v-if="metadata">
|
||||
<div class="titleContainer" @click="showTabsPopup">
|
||||
<i v-if="metadata.icon" class="icon" :class="metadata.icon"></i>
|
||||
|
||||
<div class="title">
|
||||
<div class="title">{{ metadata.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tabs">
|
||||
<button v-for="tab in tabs" v-tooltip="tab.title" class="tab _button" :class="{ active: tab.active }" @click="tab.onClick">
|
||||
<i v-if="tab.icon" class="icon" :class="tab.icon"></i>
|
||||
<span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="buttons right">
|
||||
<template v-if="actions">
|
||||
<template v-for="action in actions">
|
||||
<MkButton v-if="action.asFullButton" class="fullButton" primary @click.stop="action.handler"><i :class="action.icon" style="margin-right: 6px;"></i>{{ action.text }}</MkButton>
|
||||
<button v-else v-tooltip="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, onUnmounted, ref, inject } from 'vue';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { popupMenu } from '@/os';
|
||||
import { url } from '@/config';
|
||||
import { scrollToTop } from '@/scripts/scroll';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { globalEvents } from '@/events';
|
||||
import { injectPageMetadata, PageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const props = defineProps<{
|
||||
tabs?: {
|
||||
title: string;
|
||||
active: boolean;
|
||||
icon?: string;
|
||||
iconOnly?: boolean;
|
||||
onClick: () => void;
|
||||
}[];
|
||||
actions?: {
|
||||
text: string;
|
||||
icon: string;
|
||||
asFullButton?: boolean;
|
||||
handler: (ev: MouseEvent) => void;
|
||||
}[];
|
||||
thin?: boolean;
|
||||
}>();
|
||||
|
||||
const metadata = injectPageMetadata();
|
||||
|
||||
const el = ref<HTMLElement>(null);
|
||||
const bg = ref(null);
|
||||
const height = ref(0);
|
||||
const hasTabs = computed(() => {
|
||||
return props.tabs && props.tabs.length > 0;
|
||||
});
|
||||
|
||||
const showTabsPopup = (ev: MouseEvent) => {
|
||||
if (!hasTabs.value) return;
|
||||
if (!narrow.value) return;
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const menu = props.tabs.map(tab => ({
|
||||
text: tab.title,
|
||||
icon: tab.icon,
|
||||
action: tab.onClick,
|
||||
}));
|
||||
popupMenu(menu, ev.currentTarget ?? ev.target);
|
||||
};
|
||||
|
||||
const preventDrag = (ev: TouchEvent) => {
|
||||
ev.stopPropagation();
|
||||
};
|
||||
|
||||
const onClick = () => {
|
||||
scrollToTop(el.value, { behavior: 'smooth' });
|
||||
};
|
||||
|
||||
const calcBg = () => {
|
||||
const rawBg = metadata?.bg || 'var(--bg)';
|
||||
const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
|
||||
tinyBg.setAlpha(0.85);
|
||||
bg.value = tinyBg.toRgbString();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
calcBg();
|
||||
globalEvents.on('themeChanged', calcBg);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
globalEvents.off('themeChanged', calcBg);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.fdidabkc {
|
||||
--height: 60px;
|
||||
display: flex;
|
||||
position: sticky;
|
||||
top: var(--stickyTop, 0);
|
||||
z-index: 1000;
|
||||
width: 100%;
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
|
||||
> .buttons {
|
||||
--margin: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--height);
|
||||
margin: 0 var(--margin);
|
||||
|
||||
&.right {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
&:empty {
|
||||
width: var(--height);
|
||||
}
|
||||
|
||||
> .button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: calc(var(--height) - (var(--margin) * 2));
|
||||
width: calc(var(--height) - (var(--margin) * 2));
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
border-radius: 5px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&.highlighted {
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
> .fullButton {
|
||||
& + .fullButton {
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .titleContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 400px;
|
||||
overflow: auto;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
flex-shrink: 0;
|
||||
margin-left: 24px;
|
||||
|
||||
> .avatar {
|
||||
$size: 32px;
|
||||
display: inline-block;
|
||||
width: $size;
|
||||
height: $size;
|
||||
vertical-align: bottom;
|
||||
margin: 0 8px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
> .icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
> .title {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.1;
|
||||
|
||||
> .subtitle {
|
||||
opacity: 0.6;
|
||||
font-size: 0.8em;
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&.activeTab {
|
||||
text-align: center;
|
||||
|
||||
> .chevron {
|
||||
display: inline-block;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .tabs {
|
||||
margin-left: 16px;
|
||||
font-size: 0.8em;
|
||||
overflow: auto;
|
||||
white-space: nowrap;
|
||||
|
||||
> .tab {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
padding: 0 10px;
|
||||
height: 100%;
|
||||
font-weight: normal;
|
||||
opacity: 0.7;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
> .icon + .title {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,28 +1,31 @@
|
||||
<template>
|
||||
<div class="lcixvhis">
|
||||
<div class="_section reports">
|
||||
<div class="_content">
|
||||
<div class="inputs" style="display: flex;">
|
||||
<MkSelect v-model="state" style="margin: 0; flex: 1;">
|
||||
<template #label>{{ $ts.state }}</template>
|
||||
<option value="all">{{ $ts.all }}</option>
|
||||
<option value="unresolved">{{ $ts.unresolved }}</option>
|
||||
<option value="resolved">{{ $ts.resolved }}</option>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="targetUserOrigin" style="margin: 0; flex: 1;">
|
||||
<template #label>{{ $ts.reporteeOrigin }}</template>
|
||||
<option value="combined">{{ $ts.all }}</option>
|
||||
<option value="local">{{ $ts.local }}</option>
|
||||
<option value="remote">{{ $ts.remote }}</option>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="reporterOrigin" style="margin: 0; flex: 1;">
|
||||
<template #label>{{ $ts.reporterOrigin }}</template>
|
||||
<option value="combined">{{ $ts.all }}</option>
|
||||
<option value="local">{{ $ts.local }}</option>
|
||||
<option value="remote">{{ $ts.remote }}</option>
|
||||
</MkSelect>
|
||||
</div>
|
||||
<!-- TODO
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="900">
|
||||
<div class="lcixvhis">
|
||||
<div class="_section reports">
|
||||
<div class="_content">
|
||||
<div class="inputs" style="display: flex;">
|
||||
<MkSelect v-model="state" style="margin: 0; flex: 1;">
|
||||
<template #label>{{ $ts.state }}</template>
|
||||
<option value="all">{{ $ts.all }}</option>
|
||||
<option value="unresolved">{{ $ts.unresolved }}</option>
|
||||
<option value="resolved">{{ $ts.resolved }}</option>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="targetUserOrigin" style="margin: 0; flex: 1;">
|
||||
<template #label>{{ $ts.reporteeOrigin }}</template>
|
||||
<option value="combined">{{ $ts.all }}</option>
|
||||
<option value="local">{{ $ts.local }}</option>
|
||||
<option value="remote">{{ $ts.remote }}</option>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="reporterOrigin" style="margin: 0; flex: 1;">
|
||||
<template #label>{{ $ts.reporterOrigin }}</template>
|
||||
<option value="combined">{{ $ts.all }}</option>
|
||||
<option value="local">{{ $ts.local }}</option>
|
||||
<option value="remote">{{ $ts.remote }}</option>
|
||||
</MkSelect>
|
||||
</div>
|
||||
<!-- TODO
|
||||
<div class="inputs" style="display: flex; padding-top: 1.2em;">
|
||||
<MkInput v-model="searchUsername" style="margin: 0; flex: 1;" type="text" spellcheck="false">
|
||||
<span>{{ $ts.username }}</span>
|
||||
@ -33,24 +36,27 @@
|
||||
</div>
|
||||
-->
|
||||
|
||||
<MkPagination v-slot="{items}" ref="reports" :pagination="pagination" style="margin-top: var(--margin);">
|
||||
<XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/>
|
||||
</MkPagination>
|
||||
<MkPagination v-slot="{items}" ref="reports" :pagination="pagination" style="margin-top: var(--margin);">
|
||||
<XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
import XHeader from './_header_.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkSelect from '@/components/form/select.vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import XAbuseReport from '@/components/abuse-report.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
let reports = $ref<InstanceType<typeof MkPagination>>();
|
||||
|
||||
@ -74,12 +80,14 @@ function resolved(reportId) {
|
||||
reports.removeItem(item => item.id === reportId);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.abuseReports,
|
||||
icon: 'fas fa-exclamation-circle',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.abuseReports,
|
||||
icon: 'fas fa-exclamation-circle',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -1,21 +1,23 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="900">
|
||||
<div class="uqshojas">
|
||||
<div v-for="ad in ads" class="_panel _formRoot ad">
|
||||
<MkAd v-if="ad.url" :specify="ad"/>
|
||||
<MkInput v-model="ad.url" type="url" class="_formBlock">
|
||||
<template #label>URL</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="ad.imageUrl" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.imageUrl }}</template>
|
||||
</MkInput>
|
||||
<FormRadios v-model="ad.place" class="_formBlock">
|
||||
<template #label>Form</template>
|
||||
<option value="square">square</option>
|
||||
<option value="horizontal">horizontal</option>
|
||||
<option value="horizontal-big">horizontal-big</option>
|
||||
</FormRadios>
|
||||
<!--
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="900">
|
||||
<div class="uqshojas">
|
||||
<div v-for="ad in ads" class="_panel _formRoot ad">
|
||||
<MkAd v-if="ad.url" :specify="ad"/>
|
||||
<MkInput v-model="ad.url" type="url" class="_formBlock">
|
||||
<template #label>URL</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="ad.imageUrl" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.imageUrl }}</template>
|
||||
</MkInput>
|
||||
<FormRadios v-model="ad.place" class="_formBlock">
|
||||
<template #label>Form</template>
|
||||
<option value="square">square</option>
|
||||
<option value="horizontal">horizontal</option>
|
||||
<option value="horizontal-big">horizontal-big</option>
|
||||
</FormRadios>
|
||||
<!--
|
||||
<div style="margin: 32px 0;">
|
||||
{{ i18n.ts.priority }}
|
||||
<MkRadio v-model="ad.priority" value="high">{{ i18n.ts.high }}</MkRadio>
|
||||
@ -23,36 +25,38 @@
|
||||
<MkRadio v-model="ad.priority" value="low">{{ i18n.ts.low }}</MkRadio>
|
||||
</div>
|
||||
-->
|
||||
<FormSplit>
|
||||
<MkInput v-model="ad.ratio" type="number">
|
||||
<template #label>{{ i18n.ts.ratio }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="ad.expiresAt" type="date">
|
||||
<template #label>{{ i18n.ts.expiration }}</template>
|
||||
</MkInput>
|
||||
</FormSplit>
|
||||
<MkTextarea v-model="ad.memo" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.memo }}</template>
|
||||
</MkTextarea>
|
||||
<div class="buttons _formBlock">
|
||||
<MkButton class="button" inline primary style="margin-right: 12px;" @click="save(ad)"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton>
|
||||
<MkButton class="button" inline danger @click="remove(ad)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton>
|
||||
<FormSplit>
|
||||
<MkInput v-model="ad.ratio" type="number">
|
||||
<template #label>{{ i18n.ts.ratio }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="ad.expiresAt" type="date">
|
||||
<template #label>{{ i18n.ts.expiration }}</template>
|
||||
</MkInput>
|
||||
</FormSplit>
|
||||
<MkTextarea v-model="ad.memo" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.memo }}</template>
|
||||
</MkTextarea>
|
||||
<div class="buttons _formBlock">
|
||||
<MkButton class="button" inline primary style="margin-right: 12px;" @click="save(ad)"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton>
|
||||
<MkButton class="button" inline danger @click="remove(ad)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkTextarea from '@/components/form/textarea.vue';
|
||||
import FormRadios from '@/components/form/radios.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
let ads: any[] = $ref([]);
|
||||
|
||||
@ -81,7 +85,7 @@ function remove(ad) {
|
||||
if (canceled) return;
|
||||
ads = ads.filter(x => x !== ad);
|
||||
os.apiWithDialog('admin/ad/delete', {
|
||||
id: ad.id
|
||||
id: ad.id,
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -90,28 +94,29 @@ function save(ad) {
|
||||
if (ad.id == null) {
|
||||
os.apiWithDialog('admin/ad/create', {
|
||||
...ad,
|
||||
expiresAt: new Date(ad.expiresAt).getTime()
|
||||
expiresAt: new Date(ad.expiresAt).getTime(),
|
||||
});
|
||||
} else {
|
||||
os.apiWithDialog('admin/ad/update', {
|
||||
...ad,
|
||||
expiresAt: new Date(ad.expiresAt).getTime()
|
||||
expiresAt: new Date(ad.expiresAt).getTime(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.ads,
|
||||
icon: 'fas fa-audio-description',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
asFullButton: true,
|
||||
icon: 'fas fa-plus',
|
||||
text: i18n.ts.add,
|
||||
handler: add,
|
||||
}],
|
||||
}
|
||||
const headerActions = $computed(() => [{
|
||||
asFullButton: true,
|
||||
icon: 'fas fa-plus',
|
||||
text: i18n.ts.add,
|
||||
handler: add,
|
||||
}]);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.ads,
|
||||
icon: 'fas fa-audio-description',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -1,34 +1,40 @@
|
||||
<template>
|
||||
<div class="ztgjmzrw">
|
||||
<section v-for="announcement in announcements" class="_card _gap announcements">
|
||||
<div class="_content announcement">
|
||||
<MkInput v-model="announcement.title">
|
||||
<template #label>{{ i18n.ts.title }}</template>
|
||||
</MkInput>
|
||||
<MkTextarea v-model="announcement.text">
|
||||
<template #label>{{ i18n.ts.text }}</template>
|
||||
</MkTextarea>
|
||||
<MkInput v-model="announcement.imageUrl">
|
||||
<template #label>{{ i18n.ts.imageUrl }}</template>
|
||||
</MkInput>
|
||||
<p v-if="announcement.reads">{{ i18n.t('nUsersRead', { n: announcement.reads }) }}</p>
|
||||
<div class="buttons">
|
||||
<MkButton class="button" inline primary @click="save(announcement)"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton>
|
||||
<MkButton class="button" inline @click="remove(announcement)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton>
|
||||
</div>
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="900">
|
||||
<div class="ztgjmzrw">
|
||||
<section v-for="announcement in announcements" class="_card _gap announcements">
|
||||
<div class="_content announcement">
|
||||
<MkInput v-model="announcement.title">
|
||||
<template #label>{{ i18n.ts.title }}</template>
|
||||
</MkInput>
|
||||
<MkTextarea v-model="announcement.text">
|
||||
<template #label>{{ i18n.ts.text }}</template>
|
||||
</MkTextarea>
|
||||
<MkInput v-model="announcement.imageUrl">
|
||||
<template #label>{{ i18n.ts.imageUrl }}</template>
|
||||
</MkInput>
|
||||
<p v-if="announcement.reads">{{ i18n.t('nUsersRead', { n: announcement.reads }) }}</p>
|
||||
<div class="buttons">
|
||||
<MkButton class="button" inline primary @click="save(announcement)"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton>
|
||||
<MkButton class="button" inline @click="remove(announcement)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkTextarea from '@/components/form/textarea.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
let announcements: any[] = $ref([]);
|
||||
|
||||
@ -41,7 +47,7 @@ function add() {
|
||||
id: null,
|
||||
title: '',
|
||||
text: '',
|
||||
imageUrl: null
|
||||
imageUrl: null,
|
||||
});
|
||||
}
|
||||
|
||||
@ -61,41 +67,42 @@ function save(announcement) {
|
||||
os.api('admin/announcements/create', announcement).then(() => {
|
||||
os.alert({
|
||||
type: 'success',
|
||||
text: i18n.ts.saved
|
||||
text: i18n.ts.saved,
|
||||
});
|
||||
}).catch(err => {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: err
|
||||
text: err,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
os.api('admin/announcements/update', announcement).then(() => {
|
||||
os.alert({
|
||||
type: 'success',
|
||||
text: i18n.ts.saved
|
||||
text: i18n.ts.saved,
|
||||
});
|
||||
}).catch(err => {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: err
|
||||
text: err,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.announcements,
|
||||
icon: 'fas fa-broadcast-tower',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
asFullButton: true,
|
||||
icon: 'fas fa-plus',
|
||||
text: i18n.ts.add,
|
||||
handler: add,
|
||||
}],
|
||||
}
|
||||
const headerActions = $computed(() => [{
|
||||
asFullButton: true,
|
||||
icon: 'fas fa-plus',
|
||||
text: i18n.ts.add,
|
||||
handler: add,
|
||||
}]);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.announcements,
|
||||
icon: 'fas fa-broadcast-tower',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -51,7 +51,6 @@ import FormButton from '@/components/ui/button.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import FormSlot from '@/components/form/slot.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { fetchInstance } from '@/instance';
|
||||
|
||||
const MkCaptcha = defineAsyncComponent(() => import('@/components/captcha.vue'));
|
||||
|
@ -1,12 +1,13 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="800" :margin-min="16" :margin-max="32">
|
||||
<template><MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="800" :margin-min="16" :margin-max="32">
|
||||
<FormSuspense v-slot="{ result: database }" :p="databasePromiseFactory">
|
||||
<MkKeyValue v-for="table in database" :key="table[0]" oneline style="margin: 1em 0;">
|
||||
<template #key>{{ table[0] }}</template>
|
||||
<template #value>{{ bytes(table[1].size) }} ({{ number(table[1].count) }} recs)</template>
|
||||
</MkKeyValue>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
</MkSpacer></MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@ -14,18 +15,20 @@ import { } from 'vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import MkKeyValue from '@/components/key-value.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import bytes from '@/filters/bytes';
|
||||
import number from '@/filters/number';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const databasePromiseFactory = () => os.api('admin/get-table-stats').then(res => Object.entries(res).sort((a, b) => b[1].size - a[1].size));
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.database,
|
||||
icon: 'fas fa-database',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.database,
|
||||
icon: 'fas fa-database',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
@ -1,49 +1,53 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<FormSuspense :p="init">
|
||||
<div class="_formRoot">
|
||||
<FormSwitch v-model="enableEmail" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.enableEmail }}</template>
|
||||
<template #caption>{{ i18n.ts.emailConfigInfo }}</template>
|
||||
</FormSwitch>
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<FormSuspense :p="init">
|
||||
<div class="_formRoot">
|
||||
<FormSwitch v-model="enableEmail" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.enableEmail }}</template>
|
||||
<template #caption>{{ i18n.ts.emailConfigInfo }}</template>
|
||||
</FormSwitch>
|
||||
|
||||
<template v-if="enableEmail">
|
||||
<FormInput v-model="email" type="email" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.emailAddress }}</template>
|
||||
</FormInput>
|
||||
<template v-if="enableEmail">
|
||||
<FormInput v-model="email" type="email" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.emailAddress }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts.smtpConfig }}</template>
|
||||
<FormSplit :min-width="280">
|
||||
<FormInput v-model="smtpHost" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.smtpHost }}</template>
|
||||
</FormInput>
|
||||
<FormInput v-model="smtpPort" type="number" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.smtpPort }}</template>
|
||||
</FormInput>
|
||||
</FormSplit>
|
||||
<FormSplit :min-width="280">
|
||||
<FormInput v-model="smtpUser" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.smtpUser }}</template>
|
||||
</FormInput>
|
||||
<FormInput v-model="smtpPass" type="password" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.smtpPass }}</template>
|
||||
</FormInput>
|
||||
</FormSplit>
|
||||
<FormInfo class="_formBlock">{{ i18n.ts.emptyToDisableSmtpAuth }}</FormInfo>
|
||||
<FormSwitch v-model="smtpSecure" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.smtpSecure }}</template>
|
||||
<template #caption>{{ i18n.ts.smtpSecureInfo }}</template>
|
||||
</FormSwitch>
|
||||
</FormSection>
|
||||
</template>
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts.smtpConfig }}</template>
|
||||
<FormSplit :min-width="280">
|
||||
<FormInput v-model="smtpHost" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.smtpHost }}</template>
|
||||
</FormInput>
|
||||
<FormInput v-model="smtpPort" type="number" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.smtpPort }}</template>
|
||||
</FormInput>
|
||||
</FormSplit>
|
||||
<FormSplit :min-width="280">
|
||||
<FormInput v-model="smtpUser" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.smtpUser }}</template>
|
||||
</FormInput>
|
||||
<FormInput v-model="smtpPass" type="password" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.smtpPass }}</template>
|
||||
</FormInput>
|
||||
</FormSplit>
|
||||
<FormInfo class="_formBlock">{{ i18n.ts.emptyToDisableSmtpAuth }}</FormInfo>
|
||||
<FormSwitch v-model="smtpSecure" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.smtpSecure }}</template>
|
||||
<template #caption>{{ i18n.ts.smtpSecureInfo }}</template>
|
||||
</FormSwitch>
|
||||
</FormSection>
|
||||
</template>
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import FormInfo from '@/components/ui/info.vue';
|
||||
@ -51,9 +55,9 @@ import FormSuspense from '@/components/form/suspense.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { fetchInstance, instance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
let enableEmail: boolean = $ref(false);
|
||||
let email: any = $ref(null);
|
||||
@ -78,13 +82,13 @@ async function testEmail() {
|
||||
const { canceled, result: destination } = await os.inputText({
|
||||
title: i18n.ts.destination,
|
||||
type: 'email',
|
||||
placeholder: instance.maintainerEmail
|
||||
placeholder: instance.maintainerEmail,
|
||||
});
|
||||
if (canceled) return;
|
||||
os.apiWithDialog('admin/send-email', {
|
||||
to: destination,
|
||||
subject: 'Test email',
|
||||
text: 'Yo'
|
||||
text: 'Yo',
|
||||
});
|
||||
}
|
||||
|
||||
@ -102,21 +106,22 @@ function save() {
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.emailServer,
|
||||
icon: 'fas fa-envelope',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
asFullButton: true,
|
||||
text: i18n.ts.testEmail,
|
||||
handler: testEmail,
|
||||
}, {
|
||||
asFullButton: true,
|
||||
icon: 'fas fa-check',
|
||||
text: i18n.ts.save,
|
||||
handler: save,
|
||||
}],
|
||||
}
|
||||
const headerActions = $computed(() => [{
|
||||
asFullButton: true,
|
||||
text: i18n.ts.testEmail,
|
||||
handler: testEmail,
|
||||
}, {
|
||||
asFullButton: true,
|
||||
icon: 'fas fa-check',
|
||||
text: i18n.ts.save,
|
||||
handler: save,
|
||||
}]);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.emailServer,
|
||||
icon: 'fas fa-envelope',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
@ -1,69 +1,75 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="900">
|
||||
<div class="ogwlenmc">
|
||||
<div v-if="tab === 'local'" class="local">
|
||||
<MkInput v-model="query" :debounce="true" type="search">
|
||||
<template #prefix><i class="fas fa-search"></i></template>
|
||||
<template #label>{{ $ts.search }}</template>
|
||||
</MkInput>
|
||||
<MkSwitch v-model="selectMode" style="margin: 8px 0;">
|
||||
<template #label>Select mode</template>
|
||||
</MkSwitch>
|
||||
<div v-if="selectMode" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
|
||||
<MkButton inline @click="selectAll">Select all</MkButton>
|
||||
<MkButton inline @click="setCategoryBulk">Set category</MkButton>
|
||||
<MkButton inline @click="addTagBulk">Add tag</MkButton>
|
||||
<MkButton inline @click="removeTagBulk">Remove tag</MkButton>
|
||||
<MkButton inline @click="setTagBulk">Set tag</MkButton>
|
||||
<MkButton inline danger @click="delBulk">Delete</MkButton>
|
||||
</div>
|
||||
<MkPagination ref="emojisPaginationComponent" :pagination="pagination">
|
||||
<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
|
||||
<template v-slot="{items}">
|
||||
<div class="ldhfsamy">
|
||||
<button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" :class="{ selected: selectedEmojis.includes(emoji.id) }" @click="selectMode ? toggleSelect(emoji) : edit(emoji)">
|
||||
<img :src="emoji.url" class="img" :alt="emoji.name"/>
|
||||
<div class="body">
|
||||
<div class="name _monospace">{{ emoji.name }}</div>
|
||||
<div class="info">{{ emoji.category }}</div>
|
||||
</div>
|
||||
</button>
|
||||
<div>
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="900">
|
||||
<div class="ogwlenmc">
|
||||
<div v-if="tab === 'local'" class="local">
|
||||
<MkInput v-model="query" :debounce="true" type="search">
|
||||
<template #prefix><i class="fas fa-search"></i></template>
|
||||
<template #label>{{ $ts.search }}</template>
|
||||
</MkInput>
|
||||
<MkSwitch v-model="selectMode" style="margin: 8px 0;">
|
||||
<template #label>Select mode</template>
|
||||
</MkSwitch>
|
||||
<div v-if="selectMode" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
|
||||
<MkButton inline @click="selectAll">Select all</MkButton>
|
||||
<MkButton inline @click="setCategoryBulk">Set category</MkButton>
|
||||
<MkButton inline @click="addTagBulk">Add tag</MkButton>
|
||||
<MkButton inline @click="removeTagBulk">Remove tag</MkButton>
|
||||
<MkButton inline @click="setTagBulk">Set tag</MkButton>
|
||||
<MkButton inline danger @click="delBulk">Delete</MkButton>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<MkPagination ref="emojisPaginationComponent" :pagination="pagination">
|
||||
<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
|
||||
<template #default="{items}">
|
||||
<div class="ldhfsamy">
|
||||
<button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" :class="{ selected: selectedEmojis.includes(emoji.id) }" @click="selectMode ? toggleSelect(emoji) : edit(emoji)">
|
||||
<img :src="emoji.url" class="img" :alt="emoji.name"/>
|
||||
<div class="body">
|
||||
<div class="name _monospace">{{ emoji.name }}</div>
|
||||
<div class="info">{{ emoji.category }}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tab === 'remote'" class="remote">
|
||||
<FormSplit>
|
||||
<MkInput v-model="queryRemote" :debounce="true" type="search">
|
||||
<template #prefix><i class="fas fa-search"></i></template>
|
||||
<template #label>{{ $ts.search }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="host" :debounce="true">
|
||||
<template #label>{{ $ts.host }}</template>
|
||||
</MkInput>
|
||||
</FormSplit>
|
||||
<MkPagination :pagination="remotePagination">
|
||||
<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
|
||||
<template v-slot="{items}">
|
||||
<div class="ldhfsamy">
|
||||
<div v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="remoteMenu(emoji, $event)">
|
||||
<img :src="emoji.url" class="img" :alt="emoji.name"/>
|
||||
<div class="body">
|
||||
<div class="name _monospace">{{ emoji.name }}</div>
|
||||
<div class="info">{{ emoji.host }}</div>
|
||||
<div v-else-if="tab === 'remote'" class="remote">
|
||||
<FormSplit>
|
||||
<MkInput v-model="queryRemote" :debounce="true" type="search">
|
||||
<template #prefix><i class="fas fa-search"></i></template>
|
||||
<template #label>{{ $ts.search }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="host" :debounce="true">
|
||||
<template #label>{{ $ts.host }}</template>
|
||||
</MkInput>
|
||||
</FormSplit>
|
||||
<MkPagination :pagination="remotePagination">
|
||||
<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
|
||||
<template #default="{items}">
|
||||
<div class="ldhfsamy">
|
||||
<div v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="remoteMenu(emoji, $event)">
|
||||
<img :src="emoji.url" class="img" :alt="emoji.name"/>
|
||||
<div class="body">
|
||||
<div class="name _monospace">{{ emoji.name }}</div>
|
||||
<div class="info">{{ emoji.host }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent, defineComponent, ref, toRef } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
@ -72,8 +78,8 @@ import MkSwitch from '@/components/form/switch.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import { selectFile, selectFiles } from '@/scripts/select-file';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const emojisPaginationComponent = ref<InstanceType<typeof MkPagination>>();
|
||||
|
||||
@ -131,13 +137,13 @@ const add = async (ev: MouseEvent) => {
|
||||
|
||||
const edit = (emoji) => {
|
||||
os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), {
|
||||
emoji: emoji
|
||||
emoji: emoji,
|
||||
}, {
|
||||
done: result => {
|
||||
if (result.updated) {
|
||||
emojisPaginationComponent.value.updateItem(result.updated.id, (oldEmoji: any) => ({
|
||||
...oldEmoji,
|
||||
...result.updated
|
||||
...result.updated,
|
||||
}));
|
||||
} else if (result.deleted) {
|
||||
emojisPaginationComponent.value.removeItem((item) => item.id === emoji.id);
|
||||
@ -159,7 +165,7 @@ const remoteMenu = (emoji, ev: MouseEvent) => {
|
||||
}, {
|
||||
text: i18n.ts.import,
|
||||
icon: 'fas fa-plus',
|
||||
action: () => { im(emoji); }
|
||||
action: () => { im(emoji); },
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
};
|
||||
|
||||
@ -181,7 +187,7 @@ const menu = (ev: MouseEvent) => {
|
||||
text: err.message,
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
}, {
|
||||
icon: 'fas fa-upload',
|
||||
text: i18n.ts.import,
|
||||
@ -201,7 +207,7 @@ const menu = (ev: MouseEvent) => {
|
||||
text: err.message,
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
};
|
||||
|
||||
@ -265,31 +271,31 @@ const delBulk = async () => {
|
||||
emojisPaginationComponent.value.reload();
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: computed(() => ({
|
||||
title: i18n.ts.customEmojis,
|
||||
icon: 'fas fa-laugh',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
asFullButton: true,
|
||||
icon: 'fas fa-plus',
|
||||
text: i18n.ts.addEmoji,
|
||||
handler: add,
|
||||
}, {
|
||||
icon: 'fas fa-ellipsis-h',
|
||||
handler: menu,
|
||||
}],
|
||||
tabs: [{
|
||||
active: tab.value === 'local',
|
||||
title: i18n.ts.local,
|
||||
onClick: () => { tab.value = 'local'; },
|
||||
}, {
|
||||
active: tab.value === 'remote',
|
||||
title: i18n.ts.remote,
|
||||
onClick: () => { tab.value = 'remote'; },
|
||||
},]
|
||||
})),
|
||||
});
|
||||
const headerActions = $computed(() => [{
|
||||
asFullButton: true,
|
||||
icon: 'fas fa-plus',
|
||||
text: i18n.ts.addEmoji,
|
||||
handler: add,
|
||||
}, {
|
||||
icon: 'fas fa-ellipsis-h',
|
||||
handler: menu,
|
||||
}]);
|
||||
|
||||
const headerTabs = $computed(() => [{
|
||||
active: tab.value === 'local',
|
||||
title: i18n.ts.local,
|
||||
onClick: () => { tab.value = 'local'; },
|
||||
}, {
|
||||
active: tab.value === 'remote',
|
||||
title: i18n.ts.remote,
|
||||
onClick: () => { tab.value = 'remote'; },
|
||||
}]);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
title: i18n.ts.customEmojis,
|
||||
icon: 'fas fa-laugh',
|
||||
bg: 'var(--bg)',
|
||||
})));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -1,50 +1,58 @@
|
||||
<template>
|
||||
<div class="xrmjdkdw">
|
||||
<div>
|
||||
<div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
|
||||
<MkSelect v-model="origin" style="margin: 0; flex: 1;">
|
||||
<template #label>{{ $ts.instance }}</template>
|
||||
<option value="combined">{{ $ts.all }}</option>
|
||||
<option value="local">{{ $ts.local }}</option>
|
||||
<option value="remote">{{ $ts.remote }}</option>
|
||||
</MkSelect>
|
||||
<MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="pagination.params.origin === 'local'">
|
||||
<template #label>{{ $ts.host }}</template>
|
||||
</MkInput>
|
||||
</div>
|
||||
<div class="inputs" style="display: flex; padding-top: 1.2em;">
|
||||
<MkInput v-model="type" :debounce="true" type="search" style="margin: 0; flex: 1;">
|
||||
<template #label>MIME type</template>
|
||||
</MkInput>
|
||||
</div>
|
||||
<MkPagination v-slot="{items}" :pagination="pagination" class="urempief" :class="{ grid: viewMode === 'grid' }">
|
||||
<button v-for="file in items" :key="file.id" v-tooltip.mfm="`${file.type}\n${bytes(file.size)}\n${new Date(file.createdAt).toLocaleString()}\nby ${file.user ? '@' + Acct.toString(file.user) : 'system'}`" class="file _panel _button" @click="show(file, $event)">
|
||||
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
|
||||
<div v-if="viewMode === 'list'" class="body">
|
||||
<div>
|
||||
<small style="opacity: 0.7;">{{ file.name }}</small>
|
||||
<div>
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions"/></template>
|
||||
<MkSpacer :content-max="900">
|
||||
<div class="xrmjdkdw">
|
||||
<div>
|
||||
<div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
|
||||
<MkSelect v-model="origin" style="margin: 0; flex: 1;">
|
||||
<template #label>{{ $ts.instance }}</template>
|
||||
<option value="combined">{{ $ts.all }}</option>
|
||||
<option value="local">{{ $ts.local }}</option>
|
||||
<option value="remote">{{ $ts.remote }}</option>
|
||||
</MkSelect>
|
||||
<MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="pagination.params.origin === 'local'">
|
||||
<template #label>{{ $ts.host }}</template>
|
||||
</MkInput>
|
||||
</div>
|
||||
<div>
|
||||
<MkAcct v-if="file.user" :user="file.user"/>
|
||||
<div v-else>{{ $ts.system }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span style="margin-right: 1em;">{{ file.type }}</span>
|
||||
<span>{{ bytes(file.size) }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ $ts.registeredDate }}: <MkTime :time="file.createdAt" mode="detail"/></span>
|
||||
<div class="inputs" style="display: flex; padding-top: 1.2em;">
|
||||
<MkInput v-model="type" :debounce="true" type="search" style="margin: 0; flex: 1;">
|
||||
<template #label>MIME type</template>
|
||||
</MkInput>
|
||||
</div>
|
||||
<MkPagination v-slot="{items}" :pagination="pagination" class="urempief" :class="{ grid: viewMode === 'grid' }">
|
||||
<button v-for="file in items" :key="file.id" v-tooltip.mfm="`${file.type}\n${bytes(file.size)}\n${new Date(file.createdAt).toLocaleString()}\nby ${file.user ? '@' + Acct.toString(file.user) : 'system'}`" class="file _button" @click="show(file, $event)">
|
||||
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
|
||||
<div v-if="viewMode === 'list'" class="body">
|
||||
<div>
|
||||
<small style="opacity: 0.7;">{{ file.name }}</small>
|
||||
</div>
|
||||
<div>
|
||||
<MkAcct v-if="file.user" :user="file.user"/>
|
||||
<div v-else>{{ $ts.system }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span style="margin-right: 1em;">{{ file.type }}</span>
|
||||
<span>{{ bytes(file.size) }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ $ts.registeredDate }}: <MkTime :time="file.createdAt" mode="detail"/></span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</button>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent } from 'vue';
|
||||
import * as Acct from 'misskey-js/built/acct';
|
||||
import XHeader from './_header_.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkSelect from '@/components/form/select.vue';
|
||||
@ -53,8 +61,8 @@ import MkContainer from '@/components/ui/container.vue';
|
||||
import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue';
|
||||
import bytes from '@/filters/bytes';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
let origin = $ref('local');
|
||||
let type = $ref(null);
|
||||
@ -82,7 +90,7 @@ function clear() {
|
||||
}
|
||||
|
||||
function show(file) {
|
||||
os.pageWindow(`/admin-file/${file.id}`);
|
||||
os.pageWindow(`/admin/file/${file.id}`);
|
||||
}
|
||||
|
||||
async function find() {
|
||||
@ -104,22 +112,23 @@ async function find() {
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: computed(() => ({
|
||||
title: i18n.ts.files,
|
||||
icon: 'fas fa-cloud',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
text: i18n.ts.lookup,
|
||||
icon: 'fas fa-search',
|
||||
handler: find,
|
||||
}, {
|
||||
text: i18n.ts.clearCachedFiles,
|
||||
icon: 'fas fa-trash-alt',
|
||||
handler: clear,
|
||||
}],
|
||||
})),
|
||||
});
|
||||
const headerActions = $computed(() => [{
|
||||
text: i18n.ts.lookup,
|
||||
icon: 'fas fa-search',
|
||||
handler: find,
|
||||
}, {
|
||||
text: i18n.ts.clearCachedFiles,
|
||||
icon: 'fas fa-trash-alt',
|
||||
handler: clear,
|
||||
}]);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
title: i18n.ts.files,
|
||||
icon: 'fas fa-cloud',
|
||||
bg: 'var(--bg)',
|
||||
})));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -1,8 +1,6 @@
|
||||
<template>
|
||||
<div ref="el" class="hiyeyicy" :class="{ wide: !narrow }">
|
||||
<div v-if="!narrow || initialPage == null" class="nav">
|
||||
<MkHeader :info="header"></MkHeader>
|
||||
|
||||
<div v-if="!narrow || initialPage == null" class="nav">
|
||||
<MkSpacer :content-max="700" :margin-min="16">
|
||||
<div class="lxpfedzu">
|
||||
<div class="banner">
|
||||
@ -17,29 +15,26 @@
|
||||
</MkSpacer>
|
||||
</div>
|
||||
<div v-if="!(narrow && initialPage == null)" class="main">
|
||||
<MkStickyContainer>
|
||||
<template #header><MkHeader v-if="childInfo && !childInfo.hideHeader" :info="childInfo"/></template>
|
||||
<component :is="component" :ref="el => pageChanged(el)" :key="initialPage" v-bind="pageProps"/>
|
||||
</MkStickyContainer>
|
||||
<component :is="component" :key="initialPage" v-bind="pageProps"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, nextTick, onMounted, onUnmounted, provide, watch } from 'vue';
|
||||
import { defineAsyncComponent, inject, nextTick, onMounted, onUnmounted, provide, watch } from 'vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import MkSuperMenu from '@/components/ui/super-menu.vue';
|
||||
import MkInfo from '@/components/ui/info.vue';
|
||||
import { scroll } from '@/scripts/scroll';
|
||||
import { instance } from '@/instance';
|
||||
import * as symbols from '@/symbols';
|
||||
import * as os from '@/os';
|
||||
import { lookupUser } from '@/scripts/lookup-user';
|
||||
import { MisskeyNavigator } from '@/scripts/navigate';
|
||||
import { useRouter } from '@/router';
|
||||
import { definePageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const isEmpty = (x: string | null) => x == null || x === '';
|
||||
|
||||
const nav = new MisskeyNavigator();
|
||||
const router = useRouter();
|
||||
|
||||
const indexInfo = {
|
||||
title: i18n.ts.controlPanel,
|
||||
@ -224,7 +219,7 @@ watch(component, () => {
|
||||
|
||||
watch(() => props.initialPage, () => {
|
||||
if (props.initialPage == null && !narrow) {
|
||||
nav.push('/admin/overview');
|
||||
router.push('/admin/overview');
|
||||
} else {
|
||||
if (props.initialPage == null) {
|
||||
INFO = indexInfo;
|
||||
@ -234,7 +229,7 @@ watch(() => props.initialPage, () => {
|
||||
|
||||
watch(narrow, () => {
|
||||
if (props.initialPage == null && !narrow) {
|
||||
nav.push('/admin/overview');
|
||||
router.push('/admin/overview');
|
||||
}
|
||||
});
|
||||
|
||||
@ -243,7 +238,7 @@ onMounted(() => {
|
||||
|
||||
narrow = el.offsetWidth < NARROW_THRESHOLD;
|
||||
if (props.initialPage == null && !narrow) {
|
||||
nav.push('/admin/overview');
|
||||
router.push('/admin/overview');
|
||||
}
|
||||
});
|
||||
|
||||
@ -251,19 +246,19 @@ onUnmounted(() => {
|
||||
ro.disconnect();
|
||||
});
|
||||
|
||||
const pageChanged = (page) => {
|
||||
if (page == null) {
|
||||
provideMetadataReceiver((info) => {
|
||||
if (info == null) {
|
||||
childInfo = null;
|
||||
} else {
|
||||
childInfo = page[symbols.PAGE_INFO];
|
||||
childInfo = info;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const invite = () => {
|
||||
os.api('admin/invite').then(x => {
|
||||
os.alert({
|
||||
type: 'info',
|
||||
text: x.code
|
||||
text: x.code,
|
||||
});
|
||||
}).catch(err => {
|
||||
os.alert({
|
||||
@ -279,33 +274,38 @@ const lookup = (ev) => {
|
||||
icon: 'fas fa-user',
|
||||
action: () => {
|
||||
lookupUser();
|
||||
}
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.note,
|
||||
icon: 'fas fa-pencil-alt',
|
||||
action: () => {
|
||||
alert('TODO');
|
||||
}
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.file,
|
||||
icon: 'fas fa-cloud',
|
||||
action: () => {
|
||||
alert('TODO');
|
||||
}
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.instance,
|
||||
icon: 'fas fa-globe',
|
||||
action: () => {
|
||||
alert('TODO');
|
||||
}
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
};
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata(INFO);
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: INFO,
|
||||
header: {
|
||||
title: i18n.ts.controlPanel,
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -1,25 +1,29 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<FormSuspense :p="init">
|
||||
<FormTextarea v-model="blockedHosts" class="_formBlock">
|
||||
<span>{{ i18n.ts.blockedInstances }}</span>
|
||||
<template #caption>{{ i18n.ts.blockedInstancesDescription }}</template>
|
||||
</FormTextarea>
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<FormSuspense :p="init">
|
||||
<FormTextarea v-model="blockedHosts" class="_formBlock">
|
||||
<span>{{ i18n.ts.blockedInstances }}</span>
|
||||
<template #caption>{{ i18n.ts.blockedInstancesDescription }}</template>
|
||||
</FormTextarea>
|
||||
|
||||
<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
import FormTextarea from '@/components/form/textarea.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { fetchInstance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
let blockedHosts: string = $ref('');
|
||||
|
||||
@ -36,11 +40,13 @@ function save() {
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.instanceBlocking,
|
||||
icon: 'fas fa-ban',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.instanceBlocking,
|
||||
icon: 'fas fa-ban',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<template><MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<FormSuspense :p="init">
|
||||
<FormFolder class="_formBlock">
|
||||
<template #icon><i class="fab fa-twitter"></i></template>
|
||||
@ -20,19 +21,19 @@
|
||||
<XDiscord/>
|
||||
</FormFolder>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
</MkSpacer></MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import FormFolder from '@/components/form/folder.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import XTwitter from './integrations.twitter.vue';
|
||||
import XGithub from './integrations.github.vue';
|
||||
import XDiscord from './integrations.discord.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import FormFolder from '@/components/form/folder.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
let enableTwitterIntegration: boolean = $ref(false);
|
||||
let enableGithubIntegration: boolean = $ref(false);
|
||||
@ -45,11 +46,13 @@ async function init() {
|
||||
enableDiscordIntegration = meta.enableDiscordIntegration;
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.integration,
|
||||
icon: 'fas fa-share-alt',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.integration,
|
||||
icon: 'fas fa-share-alt',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
@ -1,72 +1,76 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<FormSuspense :p="init">
|
||||
<div class="_formRoot">
|
||||
<FormSwitch v-model="useObjectStorage" class="_formBlock">{{ i18n.ts.useObjectStorage }}</FormSwitch>
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<FormSuspense :p="init">
|
||||
<div class="_formRoot">
|
||||
<FormSwitch v-model="useObjectStorage" class="_formBlock">{{ i18n.ts.useObjectStorage }}</FormSwitch>
|
||||
|
||||
<template v-if="useObjectStorage">
|
||||
<FormInput v-model="objectStorageBaseUrl" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.objectStorageBaseUrl }}</template>
|
||||
<template #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormInput v-model="objectStorageBucket" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.objectStorageBucket }}</template>
|
||||
<template #caption>{{ i18n.ts.objectStorageBucketDesc }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormInput v-model="objectStoragePrefix" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.objectStoragePrefix }}</template>
|
||||
<template #caption>{{ i18n.ts.objectStoragePrefixDesc }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormInput v-model="objectStorageEndpoint" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.objectStorageEndpoint }}</template>
|
||||
<template #caption>{{ i18n.ts.objectStorageEndpointDesc }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormInput v-model="objectStorageRegion" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.objectStorageRegion }}</template>
|
||||
<template #caption>{{ i18n.ts.objectStorageRegionDesc }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormSplit :min-width="280">
|
||||
<FormInput v-model="objectStorageAccessKey" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-key"></i></template>
|
||||
<template #label>Access key</template>
|
||||
<template v-if="useObjectStorage">
|
||||
<FormInput v-model="objectStorageBaseUrl" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.objectStorageBaseUrl }}</template>
|
||||
<template #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormInput v-model="objectStorageSecretKey" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-key"></i></template>
|
||||
<template #label>Secret key</template>
|
||||
<FormInput v-model="objectStorageBucket" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.objectStorageBucket }}</template>
|
||||
<template #caption>{{ i18n.ts.objectStorageBucketDesc }}</template>
|
||||
</FormInput>
|
||||
</FormSplit>
|
||||
|
||||
<FormSwitch v-model="objectStorageUseSSL" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.objectStorageUseSSL }}</template>
|
||||
<template #caption>{{ i18n.ts.objectStorageUseSSLDesc }}</template>
|
||||
</FormSwitch>
|
||||
<FormInput v-model="objectStoragePrefix" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.objectStoragePrefix }}</template>
|
||||
<template #caption>{{ i18n.ts.objectStoragePrefixDesc }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormSwitch v-model="objectStorageUseProxy" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.objectStorageUseProxy }}</template>
|
||||
<template #caption>{{ i18n.ts.objectStorageUseProxyDesc }}</template>
|
||||
</FormSwitch>
|
||||
<FormInput v-model="objectStorageEndpoint" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.objectStorageEndpoint }}</template>
|
||||
<template #caption>{{ i18n.ts.objectStorageEndpointDesc }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormSwitch v-model="objectStorageSetPublicRead" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.objectStorageSetPublicRead }}</template>
|
||||
</FormSwitch>
|
||||
<FormInput v-model="objectStorageRegion" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.objectStorageRegion }}</template>
|
||||
<template #caption>{{ i18n.ts.objectStorageRegionDesc }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormSwitch v-model="objectStorageS3ForcePathStyle" class="_formBlock">
|
||||
<template #label>s3ForcePathStyle</template>
|
||||
</FormSwitch>
|
||||
</template>
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
<FormSplit :min-width="280">
|
||||
<FormInput v-model="objectStorageAccessKey" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-key"></i></template>
|
||||
<template #label>Access key</template>
|
||||
</FormInput>
|
||||
|
||||
<FormInput v-model="objectStorageSecretKey" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-key"></i></template>
|
||||
<template #label>Secret key</template>
|
||||
</FormInput>
|
||||
</FormSplit>
|
||||
|
||||
<FormSwitch v-model="objectStorageUseSSL" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.objectStorageUseSSL }}</template>
|
||||
<template #caption>{{ i18n.ts.objectStorageUseSSLDesc }}</template>
|
||||
</FormSwitch>
|
||||
|
||||
<FormSwitch v-model="objectStorageUseProxy" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.objectStorageUseProxy }}</template>
|
||||
<template #caption>{{ i18n.ts.objectStorageUseProxyDesc }}</template>
|
||||
</FormSwitch>
|
||||
|
||||
<FormSwitch v-model="objectStorageSetPublicRead" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.objectStorageSetPublicRead }}</template>
|
||||
</FormSwitch>
|
||||
|
||||
<FormSwitch v-model="objectStorageS3ForcePathStyle" class="_formBlock">
|
||||
<template #label>s3ForcePathStyle</template>
|
||||
</FormSwitch>
|
||||
</template>
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import FormGroup from '@/components/form/group.vue';
|
||||
@ -74,9 +78,9 @@ import FormSuspense from '@/components/form/suspense.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { fetchInstance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
let useObjectStorage: boolean = $ref(false);
|
||||
let objectStorageBaseUrl: string | null = $ref(null);
|
||||
@ -129,17 +133,18 @@ function save() {
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.objectStorage,
|
||||
icon: 'fas fa-cloud',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
asFullButton: true,
|
||||
icon: 'fas fa-check',
|
||||
text: i18n.ts.save,
|
||||
handler: save,
|
||||
}],
|
||||
}
|
||||
const headerActions = $computed(() => [{
|
||||
asFullButton: true,
|
||||
icon: 'fas fa-check',
|
||||
text: i18n.ts.save,
|
||||
handler: save,
|
||||
}]);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.objectStorage,
|
||||
icon: 'fas fa-cloud',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
@ -1,18 +1,22 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<FormSuspense :p="init">
|
||||
none
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<FormSuspense :p="init">
|
||||
none
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { fetchInstance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
async function init() {
|
||||
await os.api('admin/meta');
|
||||
@ -24,17 +28,18 @@ function save() {
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.other,
|
||||
icon: 'fas fa-cogs',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
asFullButton: true,
|
||||
icon: 'fas fa-check',
|
||||
text: i18n.ts.save,
|
||||
handler: save,
|
||||
}],
|
||||
}
|
||||
const headerActions = $computed(() => [{
|
||||
asFullButton: true,
|
||||
icon: 'fas fa-check',
|
||||
text: i18n.ts.save,
|
||||
handler: save,
|
||||
}]);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.other,
|
||||
icon: 'fas fa-cogs',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
@ -35,7 +35,7 @@
|
||||
</MkContainer>
|
||||
</div>
|
||||
|
||||
<!--<XMetrics/>-->
|
||||
<!--<XMetrics/>-->
|
||||
|
||||
<MkFolder style="margin: var(--margin)">
|
||||
<template #header><i class="fas fa-info-circle"></i> {{ i18n.ts.info }}</template>
|
||||
@ -67,6 +67,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||
import XMetrics from './metrics.vue';
|
||||
import MkInstanceStats from '@/components/instance-stats.vue';
|
||||
import MkNumberDiff from '@/components/number-diff.vue';
|
||||
import MkContainer from '@/components/ui/container.vue';
|
||||
@ -74,11 +75,10 @@ import MkFolder from '@/components/ui/folder.vue';
|
||||
import MkQueueChart from '@/components/queue-chart.vue';
|
||||
import { version, url } from '@/config';
|
||||
import number from '@/filters/number';
|
||||
import XMetrics from './metrics.vue';
|
||||
import * as os from '@/os';
|
||||
import { stream } from '@/stream';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
let stats: any = $ref(null);
|
||||
let serverInfo: any = $ref(null);
|
||||
@ -106,7 +106,7 @@ onMounted(async () => {
|
||||
nextTick(() => {
|
||||
queueStatsConnection.send('requestLog', {
|
||||
id: Math.random().toString().substr(2, 8),
|
||||
length: 200
|
||||
length: 200,
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -115,12 +115,14 @@ onBeforeUnmount(() => {
|
||||
queueStatsConnection.dispose();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.dashboard,
|
||||
icon: 'fas fa-tachometer-alt',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.dashboard,
|
||||
icon: 'fas fa-tachometer-alt',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<template><MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<FormSuspense :p="init">
|
||||
<MkInfo class="_formBlock">{{ i18n.ts.proxyAccountDescription }}</MkInfo>
|
||||
<MkKeyValue class="_formBlock">
|
||||
@ -9,7 +10,7 @@
|
||||
|
||||
<FormButton primary class="_formBlock" @click="chooseProxyAccount">{{ i18n.ts.selectAccount }}</FormButton>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
</MkSpacer></MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@ -19,9 +20,9 @@ import FormButton from '@/components/ui/button.vue';
|
||||
import MkInfo from '@/components/ui/info.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { fetchInstance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
let proxyAccount: any = $ref(null);
|
||||
let proxyAccountId: any = $ref(null);
|
||||
@ -50,11 +51,13 @@ function save() {
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.proxyAccount,
|
||||
icon: 'fas fa-ghost',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.proxyAccount,
|
||||
icon: 'fas fa-ghost',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
@ -1,24 +1,28 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="800">
|
||||
<XQueue :connection="connection" domain="inbox">
|
||||
<template #title>In</template>
|
||||
</XQueue>
|
||||
<XQueue :connection="connection" domain="deliver">
|
||||
<template #title>Out</template>
|
||||
</XQueue>
|
||||
<MkButton danger @click="clear()"><i class="fas fa-trash-alt"></i> {{ i18n.ts.clearQueue }}</MkButton>
|
||||
</MkSpacer>
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="800">
|
||||
<XQueue :connection="connection" domain="inbox">
|
||||
<template #title>In</template>
|
||||
</XQueue>
|
||||
<XQueue :connection="connection" domain="deliver">
|
||||
<template #title>Out</template>
|
||||
</XQueue>
|
||||
<MkButton danger @click="clear()"><i class="fas fa-trash-alt"></i> {{ i18n.ts.clearQueue }}</MkButton>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { markRaw, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import XQueue from './queue.chart.vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import * as os from '@/os';
|
||||
import { stream } from '@/stream';
|
||||
import * as symbols from '@/symbols';
|
||||
import * as config from '@/config';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const connection = markRaw(stream.useChannel('queueStats'));
|
||||
|
||||
@ -38,7 +42,7 @@ onMounted(() => {
|
||||
nextTick(() => {
|
||||
connection.send('requestLog', {
|
||||
id: Math.random().toString().substr(2, 8),
|
||||
length: 200
|
||||
length: 200,
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -47,19 +51,20 @@ onBeforeUnmount(() => {
|
||||
connection.dispose();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.jobQueue,
|
||||
icon: 'fas fa-clipboard-list',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
asFullButton: true,
|
||||
icon: 'fas fa-up-right-from-square',
|
||||
text: i18n.ts.dashboard,
|
||||
handler: () => {
|
||||
window.open(config.url + '/queue', '_blank');
|
||||
},
|
||||
}],
|
||||
}
|
||||
const headerActions = $computed(() => [{
|
||||
asFullButton: true,
|
||||
icon: 'fas fa-up-right-from-square',
|
||||
text: i18n.ts.dashboard,
|
||||
handler: () => {
|
||||
window.open(config.url + '/queue', '_blank');
|
||||
},
|
||||
}]);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.jobQueue,
|
||||
icon: 'fas fa-clipboard-list',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
@ -1,24 +1,28 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="800">
|
||||
<div v-for="relay in relays" :key="relay.inbox" class="relaycxt _panel _block" style="padding: 16px;">
|
||||
<div>{{ relay.inbox }}</div>
|
||||
<div class="status">
|
||||
<i v-if="relay.status === 'accepted'" class="fas fa-check icon accepted"></i>
|
||||
<i v-else-if="relay.status === 'rejected'" class="fas fa-ban icon rejected"></i>
|
||||
<i v-else class="fas fa-clock icon requesting"></i>
|
||||
<span>{{ $t(`_relayStatus.${relay.status}`) }}</span>
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="800">
|
||||
<div v-for="relay in relays" :key="relay.inbox" class="relaycxt _panel _block" style="padding: 16px;">
|
||||
<div>{{ relay.inbox }}</div>
|
||||
<div class="status">
|
||||
<i v-if="relay.status === 'accepted'" class="fas fa-check icon accepted"></i>
|
||||
<i v-else-if="relay.status === 'rejected'" class="fas fa-ban icon rejected"></i>
|
||||
<i v-else class="fas fa-clock icon requesting"></i>
|
||||
<span>{{ $t(`_relayStatus.${relay.status}`) }}</span>
|
||||
</div>
|
||||
<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton>
|
||||
</div>
|
||||
<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
let relays: any[] = $ref([]);
|
||||
|
||||
@ -26,30 +30,30 @@ async function addRelay() {
|
||||
const { canceled, result: inbox } = await os.inputText({
|
||||
title: i18n.ts.addRelay,
|
||||
type: 'url',
|
||||
placeholder: i18n.ts.inboxUrl
|
||||
placeholder: i18n.ts.inboxUrl,
|
||||
});
|
||||
if (canceled) return;
|
||||
os.api('admin/relays/add', {
|
||||
inbox
|
||||
inbox,
|
||||
}).then((relay: any) => {
|
||||
refresh();
|
||||
}).catch((err: any) => {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: err.message || err
|
||||
text: err.message || err,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function remove(inbox: string) {
|
||||
os.api('admin/relays/remove', {
|
||||
inbox
|
||||
inbox,
|
||||
}).then(() => {
|
||||
refresh();
|
||||
}).catch((err: any) => {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: err.message || err
|
||||
text: err.message || err,
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -62,18 +66,19 @@ function refresh() {
|
||||
|
||||
refresh();
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.relays,
|
||||
icon: 'fas fa-globe',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
asFullButton: true,
|
||||
icon: 'fas fa-plus',
|
||||
text: i18n.ts.addRelay,
|
||||
handler: addRelay,
|
||||
}],
|
||||
}
|
||||
const headerActions = $computed(() => [{
|
||||
asFullButton: true,
|
||||
icon: 'fas fa-plus',
|
||||
text: i18n.ts.addRelay,
|
||||
handler: addRelay,
|
||||
}]);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.relays,
|
||||
icon: 'fas fa-globe',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -1,36 +1,41 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<FormSuspense :p="init">
|
||||
<div class="_formRoot">
|
||||
<FormFolder class="_formBlock">
|
||||
<template #icon><i class="fas fa-shield-alt"></i></template>
|
||||
<template #label>{{ i18n.ts.botProtection }}</template>
|
||||
<template v-if="enableHcaptcha" #suffix>hCaptcha</template>
|
||||
<template v-else-if="enableRecaptcha" #suffix>reCAPTCHA</template>
|
||||
<template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template>
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<FormSuspense :p="init">
|
||||
<div class="_formRoot">
|
||||
<FormFolder class="_formBlock">
|
||||
<template #icon><i class="fas fa-shield-alt"></i></template>
|
||||
<template #label>{{ i18n.ts.botProtection }}</template>
|
||||
<template v-if="enableHcaptcha" #suffix>hCaptcha</template>
|
||||
<template v-else-if="enableRecaptcha" #suffix>reCAPTCHA</template>
|
||||
<template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template>
|
||||
|
||||
<XBotProtection/>
|
||||
</FormFolder>
|
||||
<XBotProtection/>
|
||||
</FormFolder>
|
||||
|
||||
<FormFolder class="_formBlock">
|
||||
<template #label>Summaly Proxy</template>
|
||||
<FormFolder class="_formBlock">
|
||||
<template #label>Summaly Proxy</template>
|
||||
|
||||
<div class="_formRoot">
|
||||
<FormInput v-model="summalyProxy" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-link"></i></template>
|
||||
<template #label>Summaly Proxy URL</template>
|
||||
</FormInput>
|
||||
<div class="_formRoot">
|
||||
<FormInput v-model="summalyProxy" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-link"></i></template>
|
||||
<template #label>Summaly Proxy URL</template>
|
||||
</FormInput>
|
||||
|
||||
<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton>
|
||||
</div>
|
||||
</FormFolder>
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton>
|
||||
</div>
|
||||
</FormFolder>
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import XBotProtection from './bot-protection.vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import FormFolder from '@/components/form/folder.vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormInfo from '@/components/ui/info.vue';
|
||||
@ -38,11 +43,10 @@ import FormSuspense from '@/components/form/suspense.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
import XBotProtection from './bot-protection.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { fetchInstance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
let summalyProxy: string = $ref('');
|
||||
let enableHcaptcha: boolean = $ref(false);
|
||||
@ -63,11 +67,13 @@ function save() {
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.security,
|
||||
icon: 'fas fa-lock',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.security,
|
||||
icon: 'fas fa-lock',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
@ -1,149 +1,155 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<FormSuspense :p="init">
|
||||
<div class="_formRoot">
|
||||
<FormInput v-model="name" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.instanceName }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormTextarea v-model="description" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.instanceDescription }}</template>
|
||||
</FormTextarea>
|
||||
|
||||
<FormInput v-model="tosUrl" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-link"></i></template>
|
||||
<template #label>{{ i18n.ts.tosUrl }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormSplit :min-width="300">
|
||||
<FormInput v-model="maintainerName" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.maintainerName }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormInput v-model="maintainerEmail" type="email" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-envelope"></i></template>
|
||||
<template #label>{{ i18n.ts.maintainerEmail }}</template>
|
||||
</FormInput>
|
||||
</FormSplit>
|
||||
|
||||
<FormTextarea v-model="pinnedUsers" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.pinnedUsers }}</template>
|
||||
<template #caption>{{ i18n.ts.pinnedUsersDescription }}</template>
|
||||
</FormTextarea>
|
||||
|
||||
<FormSection>
|
||||
<FormSwitch v-model="enableRegistration" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.enableRegistration }}</template>
|
||||
</FormSwitch>
|
||||
|
||||
<FormSwitch v-model="emailRequiredForSignup" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.emailRequiredForSignup }}</template>
|
||||
</FormSwitch>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<FormSwitch v-model="enableLocalTimeline" class="_formBlock">{{ i18n.ts.enableLocalTimeline }}</FormSwitch>
|
||||
<FormSwitch v-model="enableGlobalTimeline" class="_formBlock">{{ i18n.ts.enableGlobalTimeline }}</FormSwitch>
|
||||
<FormInfo class="_formBlock">{{ i18n.ts.disablingTimelinesInfo }}</FormInfo>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts.theme }}</template>
|
||||
|
||||
<FormInput v-model="iconUrl" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-link"></i></template>
|
||||
<template #label>{{ i18n.ts.iconUrl }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormInput v-model="bannerUrl" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-link"></i></template>
|
||||
<template #label>{{ i18n.ts.bannerUrl }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormInput v-model="backgroundImageUrl" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-link"></i></template>
|
||||
<template #label>{{ i18n.ts.backgroundImageUrl }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormInput v-model="themeColor" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-palette"></i></template>
|
||||
<template #label>{{ i18n.ts.themeColor }}</template>
|
||||
<template #caption>#RRGGBB</template>
|
||||
</FormInput>
|
||||
|
||||
<FormTextarea v-model="defaultLightTheme" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.instanceDefaultLightTheme }}</template>
|
||||
<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
|
||||
</FormTextarea>
|
||||
|
||||
<FormTextarea v-model="defaultDarkTheme" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.instanceDefaultDarkTheme }}</template>
|
||||
<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
|
||||
</FormTextarea>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts.files }}</template>
|
||||
|
||||
<FormSwitch v-model="cacheRemoteFiles" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.cacheRemoteFiles }}</template>
|
||||
<template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}</template>
|
||||
</FormSwitch>
|
||||
|
||||
<FormSplit :min-width="280">
|
||||
<FormInput v-model="localDriveCapacityMb" type="number" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.driveCapacityPerLocalAccount }}</template>
|
||||
<template #suffix>MB</template>
|
||||
<template #caption>{{ i18n.ts.inMb }}</template>
|
||||
<div>
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<FormSuspense :p="init">
|
||||
<div class="_formRoot">
|
||||
<FormInput v-model="name" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.instanceName }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormInput v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.driveCapacityPerRemoteAccount }}</template>
|
||||
<template #suffix>MB</template>
|
||||
<template #caption>{{ i18n.ts.inMb }}</template>
|
||||
</FormInput>
|
||||
</FormSplit>
|
||||
</FormSection>
|
||||
<FormTextarea v-model="description" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.instanceDescription }}</template>
|
||||
</FormTextarea>
|
||||
|
||||
<FormSection>
|
||||
<template #label>ServiceWorker</template>
|
||||
|
||||
<FormSwitch v-model="enableServiceWorker" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.enableServiceworker }}</template>
|
||||
<template #caption>{{ i18n.ts.serviceworkerInfo }}</template>
|
||||
</FormSwitch>
|
||||
|
||||
<template v-if="enableServiceWorker">
|
||||
<FormInput v-model="swPublicKey" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-key"></i></template>
|
||||
<template #label>Public key</template>
|
||||
<FormInput v-model="tosUrl" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-link"></i></template>
|
||||
<template #label>{{ i18n.ts.tosUrl }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormInput v-model="swPrivateKey" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-key"></i></template>
|
||||
<template #label>Private key</template>
|
||||
</FormInput>
|
||||
</template>
|
||||
</FormSection>
|
||||
<FormSplit :min-width="300">
|
||||
<FormInput v-model="maintainerName" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.maintainerName }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormSection>
|
||||
<template #label>DeepL Translation</template>
|
||||
<FormInput v-model="maintainerEmail" type="email" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-envelope"></i></template>
|
||||
<template #label>{{ i18n.ts.maintainerEmail }}</template>
|
||||
</FormInput>
|
||||
</FormSplit>
|
||||
|
||||
<FormInput v-model="deeplAuthKey" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-key"></i></template>
|
||||
<template #label>DeepL Auth Key</template>
|
||||
</FormInput>
|
||||
<FormSwitch v-model="deeplIsPro" class="_formBlock">
|
||||
<template #label>Pro account</template>
|
||||
</FormSwitch>
|
||||
</FormSection>
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
<FormTextarea v-model="pinnedUsers" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.pinnedUsers }}</template>
|
||||
<template #caption>{{ i18n.ts.pinnedUsersDescription }}</template>
|
||||
</FormTextarea>
|
||||
|
||||
<FormSection>
|
||||
<FormSwitch v-model="enableRegistration" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.enableRegistration }}</template>
|
||||
</FormSwitch>
|
||||
|
||||
<FormSwitch v-model="emailRequiredForSignup" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.emailRequiredForSignup }}</template>
|
||||
</FormSwitch>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<FormSwitch v-model="enableLocalTimeline" class="_formBlock">{{ i18n.ts.enableLocalTimeline }}</FormSwitch>
|
||||
<FormSwitch v-model="enableGlobalTimeline" class="_formBlock">{{ i18n.ts.enableGlobalTimeline }}</FormSwitch>
|
||||
<FormInfo class="_formBlock">{{ i18n.ts.disablingTimelinesInfo }}</FormInfo>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts.theme }}</template>
|
||||
|
||||
<FormInput v-model="iconUrl" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-link"></i></template>
|
||||
<template #label>{{ i18n.ts.iconUrl }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormInput v-model="bannerUrl" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-link"></i></template>
|
||||
<template #label>{{ i18n.ts.bannerUrl }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormInput v-model="backgroundImageUrl" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-link"></i></template>
|
||||
<template #label>{{ i18n.ts.backgroundImageUrl }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormInput v-model="themeColor" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-palette"></i></template>
|
||||
<template #label>{{ i18n.ts.themeColor }}</template>
|
||||
<template #caption>#RRGGBB</template>
|
||||
</FormInput>
|
||||
|
||||
<FormTextarea v-model="defaultLightTheme" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.instanceDefaultLightTheme }}</template>
|
||||
<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
|
||||
</FormTextarea>
|
||||
|
||||
<FormTextarea v-model="defaultDarkTheme" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.instanceDefaultDarkTheme }}</template>
|
||||
<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
|
||||
</FormTextarea>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts.files }}</template>
|
||||
|
||||
<FormSwitch v-model="cacheRemoteFiles" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.cacheRemoteFiles }}</template>
|
||||
<template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}</template>
|
||||
</FormSwitch>
|
||||
|
||||
<FormSplit :min-width="280">
|
||||
<FormInput v-model="localDriveCapacityMb" type="number" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.driveCapacityPerLocalAccount }}</template>
|
||||
<template #suffix>MB</template>
|
||||
<template #caption>{{ i18n.ts.inMb }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormInput v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.driveCapacityPerRemoteAccount }}</template>
|
||||
<template #suffix>MB</template>
|
||||
<template #caption>{{ i18n.ts.inMb }}</template>
|
||||
</FormInput>
|
||||
</FormSplit>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<template #label>ServiceWorker</template>
|
||||
|
||||
<FormSwitch v-model="enableServiceWorker" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.enableServiceworker }}</template>
|
||||
<template #caption>{{ i18n.ts.serviceworkerInfo }}</template>
|
||||
</FormSwitch>
|
||||
|
||||
<template v-if="enableServiceWorker">
|
||||
<FormInput v-model="swPublicKey" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-key"></i></template>
|
||||
<template #label>Public key</template>
|
||||
</FormInput>
|
||||
|
||||
<FormInput v-model="swPrivateKey" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-key"></i></template>
|
||||
<template #label>Private key</template>
|
||||
</FormInput>
|
||||
</template>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<template #label>DeepL Translation</template>
|
||||
|
||||
<FormInput v-model="deeplAuthKey" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-key"></i></template>
|
||||
<template #label>DeepL Auth Key</template>
|
||||
</FormInput>
|
||||
<FormSwitch v-model="deeplIsPro" class="_formBlock">
|
||||
<template #label>Pro account</template>
|
||||
</FormSwitch>
|
||||
</FormSection>
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import FormTextarea from '@/components/form/textarea.vue';
|
||||
@ -152,9 +158,9 @@ import FormSection from '@/components/form/section.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { fetchInstance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
let name: string | null = $ref(null);
|
||||
let description: string | null = $ref(null);
|
||||
@ -240,17 +246,18 @@ function save() {
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.general,
|
||||
icon: 'fas fa-cog',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
asFullButton: true,
|
||||
icon: 'fas fa-check',
|
||||
text: i18n.ts.save,
|
||||
handler: save,
|
||||
}],
|
||||
}
|
||||
const headerActions = $computed(() => [{
|
||||
asFullButton: true,
|
||||
icon: 'fas fa-check',
|
||||
text: i18n.ts.save,
|
||||
handler: save,
|
||||
}]);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.general,
|
||||
icon: 'fas fa-cog',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
@ -1,76 +1,84 @@
|
||||
<template>
|
||||
<div class="lknzcolw">
|
||||
<div class="users">
|
||||
<div class="inputs">
|
||||
<MkSelect v-model="sort" style="flex: 1;">
|
||||
<template #label>{{ $ts.sort }}</template>
|
||||
<option value="-createdAt">{{ $ts.registeredDate }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+createdAt">{{ $ts.registeredDate }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-updatedAt">{{ $ts.lastUsed }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+updatedAt">{{ $ts.lastUsed }} ({{ $ts.descendingOrder }})</option>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="state" style="flex: 1;">
|
||||
<template #label>{{ $ts.state }}</template>
|
||||
<option value="all">{{ $ts.all }}</option>
|
||||
<option value="available">{{ $ts.normal }}</option>
|
||||
<option value="admin">{{ $ts.administrator }}</option>
|
||||
<option value="moderator">{{ $ts.moderator }}</option>
|
||||
<option value="silenced">{{ $ts.silence }}</option>
|
||||
<option value="suspended">{{ $ts.suspend }}</option>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="origin" style="flex: 1;">
|
||||
<template #label>{{ $ts.instance }}</template>
|
||||
<option value="combined">{{ $ts.all }}</option>
|
||||
<option value="local">{{ $ts.local }}</option>
|
||||
<option value="remote">{{ $ts.remote }}</option>
|
||||
</MkSelect>
|
||||
</div>
|
||||
<div class="inputs">
|
||||
<MkInput v-model="searchUsername" style="flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.users.reload()">
|
||||
<template #prefix>@</template>
|
||||
<template #label>{{ $ts.username }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="searchHost" style="flex: 1;" type="text" spellcheck="false" :disabled="pagination.params.origin === 'local'" @update:modelValue="$refs.users.reload()">
|
||||
<template #prefix>@</template>
|
||||
<template #label>{{ $ts.host }}</template>
|
||||
</MkInput>
|
||||
</div>
|
||||
<div>
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="900">
|
||||
<div class="lknzcolw">
|
||||
<div class="users">
|
||||
<div class="inputs">
|
||||
<MkSelect v-model="sort" style="flex: 1;">
|
||||
<template #label>{{ $ts.sort }}</template>
|
||||
<option value="-createdAt">{{ $ts.registeredDate }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+createdAt">{{ $ts.registeredDate }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-updatedAt">{{ $ts.lastUsed }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+updatedAt">{{ $ts.lastUsed }} ({{ $ts.descendingOrder }})</option>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="state" style="flex: 1;">
|
||||
<template #label>{{ $ts.state }}</template>
|
||||
<option value="all">{{ $ts.all }}</option>
|
||||
<option value="available">{{ $ts.normal }}</option>
|
||||
<option value="admin">{{ $ts.administrator }}</option>
|
||||
<option value="moderator">{{ $ts.moderator }}</option>
|
||||
<option value="silenced">{{ $ts.silence }}</option>
|
||||
<option value="suspended">{{ $ts.suspend }}</option>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="origin" style="flex: 1;">
|
||||
<template #label>{{ $ts.instance }}</template>
|
||||
<option value="combined">{{ $ts.all }}</option>
|
||||
<option value="local">{{ $ts.local }}</option>
|
||||
<option value="remote">{{ $ts.remote }}</option>
|
||||
</MkSelect>
|
||||
</div>
|
||||
<div class="inputs">
|
||||
<MkInput v-model="searchUsername" style="flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.users.reload()">
|
||||
<template #prefix>@</template>
|
||||
<template #label>{{ $ts.username }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="searchHost" style="flex: 1;" type="text" spellcheck="false" :disabled="pagination.params.origin === 'local'" @update:modelValue="$refs.users.reload()">
|
||||
<template #prefix>@</template>
|
||||
<template #label>{{ $ts.host }}</template>
|
||||
</MkInput>
|
||||
</div>
|
||||
|
||||
<MkPagination v-slot="{items}" ref="paginationComponent" :pagination="pagination" class="users">
|
||||
<button v-for="user in items" :key="user.id" class="user _panel _button _gap" @click="show(user)">
|
||||
<MkAvatar class="avatar" :user="user" :disable-link="true" :show-indicator="true"/>
|
||||
<div class="body">
|
||||
<header>
|
||||
<MkUserName class="name" :user="user"/>
|
||||
<span class="acct">@{{ acct(user) }}</span>
|
||||
<span v-if="user.isAdmin" class="staff"><i class="fas fa-bookmark"></i></span>
|
||||
<span v-if="user.isModerator" class="staff"><i class="far fa-bookmark"></i></span>
|
||||
<span v-if="user.isSilenced" class="punished"><i class="fas fa-microphone-slash"></i></span>
|
||||
<span v-if="user.isSuspended" class="punished"><i class="fas fa-snowflake"></i></span>
|
||||
</header>
|
||||
<div>
|
||||
<span>{{ $ts.lastUsed }}: <MkTime v-if="user.updatedAt" :time="user.updatedAt" mode="detail"/></span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ $ts.registeredDate }}: <MkTime :time="user.createdAt" mode="detail"/></span>
|
||||
</div>
|
||||
<MkPagination v-slot="{items}" ref="paginationComponent" :pagination="pagination" class="users">
|
||||
<button v-for="user in items" :key="user.id" class="user _panel _button _gap" @click="show(user)">
|
||||
<MkAvatar class="avatar" :user="user" :disable-link="true" :show-indicator="true"/>
|
||||
<div class="body">
|
||||
<header>
|
||||
<MkUserName class="name" :user="user"/>
|
||||
<span class="acct">@{{ acct(user) }}</span>
|
||||
<span v-if="user.isAdmin" class="staff"><i class="fas fa-bookmark"></i></span>
|
||||
<span v-if="user.isModerator" class="staff"><i class="far fa-bookmark"></i></span>
|
||||
<span v-if="user.isSilenced" class="punished"><i class="fas fa-microphone-slash"></i></span>
|
||||
<span v-if="user.isSuspended" class="punished"><i class="fas fa-snowflake"></i></span>
|
||||
</header>
|
||||
<div>
|
||||
<span>{{ $ts.lastUsed }}: <MkTime v-if="user.updatedAt" :time="user.updatedAt" mode="detail"/></span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ $ts.registeredDate }}: <MkTime :time="user.createdAt" mode="detail"/></span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</button>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkSelect from '@/components/form/select.vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import { acct } from '@/filters/user';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { lookupUser } from '@/scripts/lookup-user';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
let paginationComponent = $ref<InstanceType<typeof MkPagination>>();
|
||||
|
||||
@ -89,7 +97,7 @@ const pagination = {
|
||||
username: searchUsername,
|
||||
hostname: searchHost,
|
||||
})),
|
||||
offsetMode: true
|
||||
offsetMode: true,
|
||||
};
|
||||
|
||||
function searchUser() {
|
||||
@ -106,7 +114,7 @@ async function addUser() {
|
||||
|
||||
const { canceled: canceled2, result: password } = await os.inputText({
|
||||
title: i18n.ts.password,
|
||||
type: 'password'
|
||||
type: 'password',
|
||||
});
|
||||
if (canceled2) return;
|
||||
|
||||
@ -122,34 +130,34 @@ function show(user) {
|
||||
os.pageWindow(`/user-info/${user.id}`);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: computed(() => ({
|
||||
title: i18n.ts.users,
|
||||
icon: 'fas fa-users',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
icon: 'fas fa-search',
|
||||
text: i18n.ts.search,
|
||||
handler: searchUser
|
||||
}, {
|
||||
asFullButton: true,
|
||||
icon: 'fas fa-plus',
|
||||
text: i18n.ts.addUser,
|
||||
handler: addUser
|
||||
}, {
|
||||
asFullButton: true,
|
||||
icon: 'fas fa-search',
|
||||
text: i18n.ts.lookup,
|
||||
handler: lookupUser
|
||||
}],
|
||||
})),
|
||||
});
|
||||
const headerActions = $computed(() => [{
|
||||
icon: 'fas fa-search',
|
||||
text: i18n.ts.search,
|
||||
handler: searchUser,
|
||||
}, {
|
||||
asFullButton: true,
|
||||
icon: 'fas fa-plus',
|
||||
text: i18n.ts.addUser,
|
||||
handler: addUser,
|
||||
}, {
|
||||
asFullButton: true,
|
||||
icon: 'fas fa-search',
|
||||
text: i18n.ts.lookup,
|
||||
handler: lookupUser,
|
||||
}]);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
title: i18n.ts.users,
|
||||
icon: 'fas fa-users',
|
||||
bg: 'var(--bg)',
|
||||
})));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.lknzcolw {
|
||||
> .users {
|
||||
margin: var(--margin);
|
||||
|
||||
> .inputs {
|
||||
display: flex;
|
||||
|
@ -1,57 +1,53 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="800">
|
||||
<MkPagination v-slot="{items}" :pagination="pagination" class="ruryvtyk _content">
|
||||
<section v-for="(announcement, i) in items" :key="announcement.id" class="_card announcement">
|
||||
<div class="_title"><span v-if="$i && !announcement.isRead">🆕 </span>{{ announcement.title }}</div>
|
||||
<div class="_content">
|
||||
<Mfm :text="announcement.text"/>
|
||||
<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
|
||||
</div>
|
||||
<div v-if="$i && !announcement.isRead" class="_footer">
|
||||
<MkButton primary @click="read(items, announcement, i)"><i class="fas fa-check"></i> {{ $ts.gotIt }}</MkButton>
|
||||
</div>
|
||||
</section>
|
||||
</MkPagination>
|
||||
</MkSpacer>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="800">
|
||||
<MkPagination v-slot="{items}" :pagination="pagination" class="ruryvtyk _content">
|
||||
<section v-for="(announcement, i) in items" :key="announcement.id" class="_card announcement">
|
||||
<div class="_title"><span v-if="$i && !announcement.isRead">🆕 </span>{{ announcement.title }}</div>
|
||||
<div class="_content">
|
||||
<Mfm :text="announcement.text"/>
|
||||
<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
|
||||
</div>
|
||||
<div v-if="$i && !announcement.isRead" class="_footer">
|
||||
<MkButton primary @click="read(items, announcement, i)"><i class="fas fa-check"></i> {{ $ts.gotIt }}</MkButton>
|
||||
</div>
|
||||
</section>
|
||||
</MkPagination>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkPagination,
|
||||
MkButton
|
||||
},
|
||||
const pagination = {
|
||||
endpoint: 'announcements' as const,
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.announcements,
|
||||
icon: 'fas fa-broadcast-tower',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
pagination: {
|
||||
endpoint: 'announcements' as const,
|
||||
limit: 10,
|
||||
},
|
||||
};
|
||||
},
|
||||
// TODO: これは実質的に親コンポーネントから子コンポーネントのプロパティを変更してるのでなんとかしたい
|
||||
function read(items, announcement, i) {
|
||||
items[i] = {
|
||||
...announcement,
|
||||
isRead: true,
|
||||
};
|
||||
os.api('i/read-announcement', { announcementId: announcement.id });
|
||||
}
|
||||
|
||||
methods: {
|
||||
// TODO: これは実質的に親コンポーネントから子コンポーネントのプロパティを変更してるのでなんとかしたい
|
||||
read(items, announcement, i) {
|
||||
items[i] = {
|
||||
...announcement,
|
||||
isRead: true,
|
||||
};
|
||||
os.api('i/read-announcement', { announcementId: announcement.id });
|
||||
},
|
||||
}
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.announcements,
|
||||
icon: 'fas fa-broadcast-tower',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -1,8 +1,9 @@
|
||||
<template>
|
||||
<div v-hotkey.global="keymap" v-size="{ min: [800] }" class="tqmomfks">
|
||||
<div ref="rootEl" v-hotkey.global="keymap" v-size="{ min: [800] }" class="tqmomfks">
|
||||
<div v-if="queue > 0" class="new"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div>
|
||||
<div class="tl _block">
|
||||
<XTimeline ref="tl" :key="antennaId"
|
||||
<XTimeline
|
||||
ref="tlEl" :key="antennaId"
|
||||
class="tl"
|
||||
src="antenna"
|
||||
:antenna="antennaId"
|
||||
@ -13,92 +14,78 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, defineAsyncComponent, computed } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, watch } from 'vue';
|
||||
import XTimeline from '@/components/timeline.vue';
|
||||
import { scroll } from '@/scripts/scroll';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { useRouter } from '@/router';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import i18n from '@/components/global/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XTimeline,
|
||||
},
|
||||
const router = useRouter();
|
||||
|
||||
props: {
|
||||
antennaId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
const props = defineProps<{
|
||||
antennaId: string;
|
||||
}>();
|
||||
|
||||
data() {
|
||||
return {
|
||||
antenna: null,
|
||||
queue: 0,
|
||||
[symbols.PAGE_INFO]: computed(() => this.antenna ? {
|
||||
title: this.antenna.name,
|
||||
icon: 'fas fa-satellite',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
icon: 'fas fa-calendar-alt',
|
||||
text: this.$ts.jumpToSpecifiedDate,
|
||||
handler: this.timetravel
|
||||
}, {
|
||||
icon: 'fas fa-cog',
|
||||
text: this.$ts.settings,
|
||||
handler: this.settings
|
||||
}],
|
||||
} : null),
|
||||
};
|
||||
},
|
||||
let antenna = $ref(null);
|
||||
let queue = $ref(0);
|
||||
let rootEl = $ref<HTMLElement>();
|
||||
let tlEl = $ref<InstanceType<typeof XTimeline>>();
|
||||
const keymap = $computed(() => ({
|
||||
't': focus,
|
||||
}));
|
||||
|
||||
computed: {
|
||||
keymap(): any {
|
||||
return {
|
||||
't': this.focus
|
||||
};
|
||||
},
|
||||
},
|
||||
function queueUpdated(q) {
|
||||
queue = q;
|
||||
}
|
||||
|
||||
watch: {
|
||||
antennaId: {
|
||||
async handler() {
|
||||
this.antenna = await os.api('antennas/show', {
|
||||
antennaId: this.antennaId
|
||||
});
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
function top() {
|
||||
scroll(rootEl, { top: 0 });
|
||||
}
|
||||
|
||||
methods: {
|
||||
queueUpdated(q) {
|
||||
this.queue = q;
|
||||
},
|
||||
async function timetravel() {
|
||||
const { canceled, result: date } = await os.inputDate({
|
||||
title: i18n.ts.date,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
top() {
|
||||
scroll(this.$el, { top: 0 });
|
||||
},
|
||||
tlEl.timetravel(date);
|
||||
}
|
||||
|
||||
async timetravel() {
|
||||
const { canceled, result: date } = await os.inputDate({
|
||||
title: this.$ts.date,
|
||||
});
|
||||
if (canceled) return;
|
||||
function settings() {
|
||||
router.push(`/my/antennas/${props.antennaId}`);
|
||||
}
|
||||
|
||||
this.$refs.tl.timetravel(date);
|
||||
},
|
||||
function focus() {
|
||||
tlEl.focus();
|
||||
}
|
||||
|
||||
settings() {
|
||||
this.$router.push(`/my/antennas/${this.antennaId}`);
|
||||
},
|
||||
watch(() => props.antennaId, async () => {
|
||||
antenna = await os.api('antennas/show', {
|
||||
antennaId: props.antennaId,
|
||||
});
|
||||
}, { immediate: true });
|
||||
|
||||
focus() {
|
||||
(this.$refs.tl as any).focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => antenna ? {
|
||||
title: antenna.name,
|
||||
icon: 'fas fa-satellite',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
icon: 'fas fa-calendar-alt',
|
||||
text: i18n.ts.jumpToSpecifiedDate,
|
||||
handler: timetravel,
|
||||
}, {
|
||||
icon: 'fas fa-cog',
|
||||
text: i18n.ts.settings,
|
||||
handler: settings,
|
||||
}],
|
||||
} : null));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -1,40 +1,43 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="700">
|
||||
<div class="_formRoot">
|
||||
<div class="_formBlock">
|
||||
<MkInput v-model="endpoint" :datalist="endpoints" class="_formBlock" @update:modelValue="onEndpointChange()">
|
||||
<template #label>Endpoint</template>
|
||||
</MkInput>
|
||||
<MkTextarea v-model="body" class="_formBlock" code>
|
||||
<template #label>Params (JSON or JSON5)</template>
|
||||
</MkTextarea>
|
||||
<MkSwitch v-model="withCredential" class="_formBlock">
|
||||
With credential
|
||||
</MkSwitch>
|
||||
<MkButton class="_formBlock" primary :disabled="sending" @click="send">
|
||||
<template v-if="sending"><MkEllipsis/></template>
|
||||
<template v-else><i class="fas fa-paper-plane"></i> Send</template>
|
||||
</MkButton>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700">
|
||||
<div class="_formRoot">
|
||||
<div class="_formBlock">
|
||||
<MkInput v-model="endpoint" :datalist="endpoints" class="_formBlock" @update:modelValue="onEndpointChange()">
|
||||
<template #label>Endpoint</template>
|
||||
</MkInput>
|
||||
<MkTextarea v-model="body" class="_formBlock" code>
|
||||
<template #label>Params (JSON or JSON5)</template>
|
||||
</MkTextarea>
|
||||
<MkSwitch v-model="withCredential" class="_formBlock">
|
||||
With credential
|
||||
</MkSwitch>
|
||||
<MkButton class="_formBlock" primary :disabled="sending" @click="send">
|
||||
<template v-if="sending"><MkEllipsis/></template>
|
||||
<template v-else><i class="fas fa-paper-plane"></i> Send</template>
|
||||
</MkButton>
|
||||
</div>
|
||||
<div v-if="res" class="_formBlock">
|
||||
<MkTextarea v-model="res" code readonly tall>
|
||||
<template #label>Response</template>
|
||||
</MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="res" class="_formBlock">
|
||||
<MkTextarea v-model="res" code readonly tall>
|
||||
<template #label>Response</template>
|
||||
</MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import JSON5 from 'json5';
|
||||
import { Endpoints } from 'misskey-js';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkTextarea from '@/components/form/textarea.vue';
|
||||
import MkSwitch from '@/components/form/switch.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { Endpoints } from 'misskey-js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const body = ref('{}');
|
||||
const endpoint = ref('');
|
||||
@ -75,10 +78,12 @@ function onEndpointChange() {
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: 'API console',
|
||||
icon: 'fas fa-terminal'
|
||||
},
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: 'API console',
|
||||
icon: 'fas fa-terminal',
|
||||
});
|
||||
</script>
|
||||
|
@ -15,7 +15,7 @@
|
||||
<h1>{{ $ts._auth.denied }}</h1>
|
||||
</div>
|
||||
<div v-if="state == 'accepted'" class="accepted">
|
||||
<h1>{{ session.app.isAuthorized ? this.$t('already-authorized') : this.$ts.allowed }}</h1>
|
||||
<h1>{{ session.app.isAuthorized ? $t('already-authorized') : $ts.allowed }}</h1>
|
||||
<p v-if="session.app.callbackUrl">{{ $ts._auth.callback }}<MkEllipsis/></p>
|
||||
<p v-if="!session.app.callbackUrl">{{ $ts._auth.pleaseGoBack }}</p>
|
||||
</div>
|
||||
@ -40,24 +40,20 @@ export default defineComponent({
|
||||
XForm,
|
||||
MkSignin,
|
||||
},
|
||||
props: ['token'],
|
||||
data() {
|
||||
return {
|
||||
state: null,
|
||||
session: null,
|
||||
fetching: true
|
||||
fetching: true,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
token(): string {
|
||||
return this.$route.params.token;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (!this.$i) return;
|
||||
|
||||
// Fetch session
|
||||
os.api('auth/session/show', {
|
||||
token: this.token
|
||||
token: this.token,
|
||||
}).then(session => {
|
||||
this.session = session;
|
||||
this.fetching = false;
|
||||
@ -65,7 +61,7 @@ export default defineComponent({
|
||||
// 既に連携していた場合
|
||||
if (this.session.app.isAuthorized) {
|
||||
os.api('auth/accept', {
|
||||
token: this.session.token
|
||||
token: this.session.token,
|
||||
}).then(() => {
|
||||
this.accepted();
|
||||
});
|
||||
@ -85,8 +81,8 @@ export default defineComponent({
|
||||
}
|
||||
}, onLogin(res) {
|
||||
login(res.i);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -1,127 +1,122 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="700">
|
||||
<div class="_formRoot">
|
||||
<MkInput v-model="name" class="_formBlock">
|
||||
<template #label>{{ $ts.name }}</template>
|
||||
</MkInput>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700">
|
||||
<div class="_formRoot">
|
||||
<MkInput v-model="name" class="_formBlock">
|
||||
<template #label>{{ $ts.name }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkTextarea v-model="description" class="_formBlock">
|
||||
<template #label>{{ $ts.description }}</template>
|
||||
</MkTextarea>
|
||||
<MkTextarea v-model="description" class="_formBlock">
|
||||
<template #label>{{ $ts.description }}</template>
|
||||
</MkTextarea>
|
||||
|
||||
<div class="banner">
|
||||
<MkButton v-if="bannerId == null" @click="setBannerImage"><i class="fas fa-plus"></i> {{ $ts._channel.setBanner }}</MkButton>
|
||||
<div v-else-if="bannerUrl">
|
||||
<img :src="bannerUrl" style="width: 100%;"/>
|
||||
<MkButton @click="removeBannerImage()"><i class="fas fa-trash-alt"></i> {{ $ts._channel.removeBanner }}</MkButton>
|
||||
<div class="banner">
|
||||
<MkButton v-if="bannerId == null" @click="setBannerImage"><i class="fas fa-plus"></i> {{ $ts._channel.setBanner }}</MkButton>
|
||||
<div v-else-if="bannerUrl">
|
||||
<img :src="bannerUrl" style="width: 100%;"/>
|
||||
<MkButton @click="removeBannerImage()"><i class="fas fa-trash-alt"></i> {{ $ts._channel.removeBanner }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_formBlock">
|
||||
<MkButton primary @click="save()"><i class="fas fa-save"></i> {{ channelId ? $ts.save : $ts.create }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_formBlock">
|
||||
<MkButton primary @click="save()"><i class="fas fa-save"></i> {{ channelId ? $ts.save : $ts.create }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, watch } from 'vue';
|
||||
import MkTextarea from '@/components/form/textarea.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import { selectFile } from '@/scripts/select-file';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { useRouter } from '@/router';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkTextarea, MkButton, MkInput,
|
||||
},
|
||||
const router = useRouter();
|
||||
|
||||
props: {
|
||||
channelId: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
},
|
||||
const props = defineProps<{
|
||||
channelId?: string;
|
||||
}>();
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: computed(() => this.channelId ? {
|
||||
title: this.$ts._channel.edit,
|
||||
icon: 'fas fa-satellite-dish',
|
||||
bg: 'var(--bg)',
|
||||
} : {
|
||||
title: this.$ts._channel.create,
|
||||
icon: 'fas fa-satellite-dish',
|
||||
bg: 'var(--bg)',
|
||||
}),
|
||||
channel: null,
|
||||
name: null,
|
||||
description: null,
|
||||
bannerUrl: null,
|
||||
bannerId: null,
|
||||
};
|
||||
},
|
||||
let channel = $ref(null);
|
||||
let name = $ref(null);
|
||||
let description = $ref(null);
|
||||
let bannerUrl = $ref<string | null>(null);
|
||||
let bannerId = $ref<string | null>(null);
|
||||
|
||||
watch: {
|
||||
async bannerId() {
|
||||
if (this.bannerId == null) {
|
||||
this.bannerUrl = null;
|
||||
} else {
|
||||
this.bannerUrl = (await os.api('drive/files/show', {
|
||||
fileId: this.bannerId,
|
||||
})).url;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
async created() {
|
||||
if (this.channelId) {
|
||||
this.channel = await os.api('channels/show', {
|
||||
channelId: this.channelId,
|
||||
});
|
||||
|
||||
this.name = this.channel.name;
|
||||
this.description = this.channel.description;
|
||||
this.bannerId = this.channel.bannerId;
|
||||
this.bannerUrl = this.channel.bannerUrl;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
save() {
|
||||
const params = {
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
bannerId: this.bannerId,
|
||||
};
|
||||
|
||||
if (this.channelId) {
|
||||
params.channelId = this.channelId;
|
||||
os.api('channels/update', params)
|
||||
.then(channel => {
|
||||
os.success();
|
||||
});
|
||||
} else {
|
||||
os.api('channels/create', params)
|
||||
.then(channel => {
|
||||
os.success();
|
||||
this.$router.push(`/channels/${channel.id}`);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setBannerImage(evt) {
|
||||
selectFile(evt.currentTarget ?? evt.target, null).then(file => {
|
||||
this.bannerId = file.id;
|
||||
});
|
||||
},
|
||||
|
||||
removeBannerImage() {
|
||||
this.bannerId = null;
|
||||
}
|
||||
watch(() => bannerId, async () => {
|
||||
if (bannerId == null) {
|
||||
bannerUrl = null;
|
||||
} else {
|
||||
bannerUrl = (await os.api('drive/files/show', {
|
||||
fileId: bannerId,
|
||||
})).url;
|
||||
}
|
||||
});
|
||||
|
||||
async function fetchChannel() {
|
||||
if (props.channelId == null) return;
|
||||
|
||||
channel = await os.api('channels/show', {
|
||||
channelId: props.channelId,
|
||||
});
|
||||
|
||||
name = channel.name;
|
||||
description = channel.description;
|
||||
bannerId = channel.bannerId;
|
||||
bannerUrl = channel.bannerUrl;
|
||||
}
|
||||
|
||||
fetchChannel();
|
||||
|
||||
function save() {
|
||||
const params = {
|
||||
name: name,
|
||||
description: description,
|
||||
bannerId: bannerId,
|
||||
};
|
||||
|
||||
if (props.channelId) {
|
||||
params.channelId = props.channelId;
|
||||
os.api('channels/update', params).then(() => {
|
||||
os.success();
|
||||
});
|
||||
} else {
|
||||
os.api('channels/create', params).then(created => {
|
||||
os.success();
|
||||
router.push(`/channels/${created.id}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function setBannerImage(evt) {
|
||||
selectFile(evt.currentTarget ?? evt.target, null).then(file => {
|
||||
bannerId = file.id;
|
||||
});
|
||||
}
|
||||
|
||||
function removeBannerImage() {
|
||||
bannerId = null;
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => props.channelId ? {
|
||||
title: i18n.ts._channel.edit,
|
||||
icon: 'fas fa-satellite-dish',
|
||||
bg: 'var(--bg)',
|
||||
} : {
|
||||
title: i18n.ts._channel.create,
|
||||
icon: 'fas fa-satellite-dish',
|
||||
bg: 'var(--bg)',
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -1,98 +1,87 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="700">
|
||||
<div v-if="channel">
|
||||
<div class="wpgynlbz _panel _gap" :class="{ hide: !showBanner }">
|
||||
<XChannelFollowButton :channel="channel" :full="true" class="subscribe"/>
|
||||
<button class="_button toggle" @click="() => showBanner = !showBanner">
|
||||
<template v-if="showBanner"><i class="fas fa-angle-up"></i></template>
|
||||
<template v-else><i class="fas fa-angle-down"></i></template>
|
||||
</button>
|
||||
<div v-if="!showBanner" class="hideOverlay">
|
||||
</div>
|
||||
<div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : null }" class="banner">
|
||||
<div class="status">
|
||||
<div><i class="fas fa-users fa-fw"></i><I18n :src="$ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div>
|
||||
<div><i class="fas fa-pencil-alt fa-fw"></i><I18n :src="$ts._channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700">
|
||||
<div v-if="channel">
|
||||
<div class="wpgynlbz _panel _gap" :class="{ hide: !showBanner }">
|
||||
<XChannelFollowButton :channel="channel" :full="true" class="subscribe"/>
|
||||
<button class="_button toggle" @click="() => showBanner = !showBanner">
|
||||
<template v-if="showBanner"><i class="fas fa-angle-up"></i></template>
|
||||
<template v-else><i class="fas fa-angle-down"></i></template>
|
||||
</button>
|
||||
<div v-if="!showBanner" class="hideOverlay">
|
||||
</div>
|
||||
<div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : null }" class="banner">
|
||||
<div class="status">
|
||||
<div><i class="fas fa-users fa-fw"></i><I18n :src="$ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div>
|
||||
<div><i class="fas fa-pencil-alt fa-fw"></i><I18n :src="$ts._channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div>
|
||||
</div>
|
||||
<div class="fade"></div>
|
||||
</div>
|
||||
<div v-if="channel.description" class="description">
|
||||
<Mfm :text="channel.description" :is-note="false" :i="$i"/>
|
||||
</div>
|
||||
<div class="fade"></div>
|
||||
</div>
|
||||
<div v-if="channel.description" class="description">
|
||||
<Mfm :text="channel.description" :is-note="false" :i="$i"/>
|
||||
</div>
|
||||
|
||||
<XPostForm v-if="$i" :channel="channel" class="post-form _panel _gap" fixed/>
|
||||
|
||||
<XTimeline :key="channelId" class="_gap" src="channel" :channel="channelId" @before="before" @after="after"/>
|
||||
</div>
|
||||
|
||||
<XPostForm v-if="$i" :channel="channel" class="post-form _panel _gap" fixed/>
|
||||
|
||||
<XTimeline :key="channelId" class="_gap" src="channel" :channel="channelId" @before="before" @after="after"/>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, watch } from 'vue';
|
||||
import MkContainer from '@/components/ui/container.vue';
|
||||
import XPostForm from '@/components/post-form.vue';
|
||||
import XTimeline from '@/components/timeline.vue';
|
||||
import XChannelFollowButton from '@/components/channel-follow-button.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { useRouter } from '@/router';
|
||||
import { $i } from '@/account';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkContainer,
|
||||
XPostForm,
|
||||
XTimeline,
|
||||
XChannelFollowButton
|
||||
},
|
||||
const router = useRouter();
|
||||
|
||||
props: {
|
||||
channelId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
const props = defineProps<{
|
||||
channelId: string;
|
||||
}>();
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: computed(() => this.channel ? {
|
||||
title: this.channel.name,
|
||||
icon: 'fas fa-satellite-dish',
|
||||
bg: 'var(--bg)',
|
||||
actions: [...(this.$i && this.$i.id === this.channel.userId ? [{
|
||||
icon: 'fas fa-cog',
|
||||
text: this.$ts.edit,
|
||||
handler: this.edit,
|
||||
}] : [])],
|
||||
} : null),
|
||||
channel: null,
|
||||
showBanner: true,
|
||||
pagination: {
|
||||
endpoint: 'channels/timeline' as const,
|
||||
limit: 10,
|
||||
params: computed(() => ({
|
||||
channelId: this.channelId,
|
||||
}))
|
||||
},
|
||||
};
|
||||
},
|
||||
let channel = $ref(null);
|
||||
let showBanner = $ref(true);
|
||||
const pagination = {
|
||||
endpoint: 'channels/timeline' as const,
|
||||
limit: 10,
|
||||
params: computed(() => ({
|
||||
channelId: props.channelId,
|
||||
})),
|
||||
};
|
||||
|
||||
watch: {
|
||||
channelId: {
|
||||
async handler() {
|
||||
this.channel = await os.api('channels/show', {
|
||||
channelId: this.channelId,
|
||||
});
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
watch(() => props.channelId, async () => {
|
||||
channel = await os.api('channels/show', {
|
||||
channelId: props.channelId,
|
||||
});
|
||||
}, { immediate: true });
|
||||
|
||||
methods: {
|
||||
edit() {
|
||||
this.$router.push(`/channels/${this.channel.id}/edit`);
|
||||
}
|
||||
},
|
||||
});
|
||||
function edit() {
|
||||
router.push(`/channels/${channel.id}/edit`);
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => channel && channel.userId ? [{
|
||||
icon: 'fas fa-cog',
|
||||
text: i18n.ts.edit,
|
||||
handler: edit,
|
||||
}] : null);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => channel ? {
|
||||
title: channel.name,
|
||||
icon: 'fas fa-satellite-dish',
|
||||
bg: 'var(--bg)',
|
||||
} : null));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -1,82 +1,83 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="700">
|
||||
<div v-if="tab === 'featured'" class="_content grwlizim featured">
|
||||
<MkPagination v-slot="{items}" :pagination="featuredPagination">
|
||||
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div v-else-if="tab === 'following'" class="_content grwlizim following">
|
||||
<MkPagination v-slot="{items}" :pagination="followingPagination">
|
||||
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div v-else-if="tab === 'owned'" class="_content grwlizim owned">
|
||||
<MkButton class="new" @click="create()"><i class="fas fa-plus"></i></MkButton>
|
||||
<MkPagination v-slot="{items}" :pagination="ownedPagination">
|
||||
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700">
|
||||
<div v-if="tab === 'featured'" class="_content grwlizim featured">
|
||||
<MkPagination v-slot="{items}" :pagination="featuredPagination">
|
||||
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div v-else-if="tab === 'following'" class="_content grwlizim following">
|
||||
<MkPagination v-slot="{items}" :pagination="followingPagination">
|
||||
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div v-else-if="tab === 'owned'" class="_content grwlizim owned">
|
||||
<MkButton class="new" @click="create()"><i class="fas fa-plus"></i></MkButton>
|
||||
<MkPagination v-slot="{items}" :pagination="ownedPagination">
|
||||
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineComponent, inject } from 'vue';
|
||||
import MkChannelPreview from '@/components/channel-preview.vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import * as symbols from '@/symbols';
|
||||
import { useRouter } from '@/router';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkChannelPreview, MkPagination, MkButton,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: computed(() => ({
|
||||
title: this.$ts.channel,
|
||||
icon: 'fas fa-satellite-dish',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
icon: 'fas fa-plus',
|
||||
text: this.$ts.create,
|
||||
handler: this.create,
|
||||
}],
|
||||
tabs: [{
|
||||
active: this.tab === 'featured',
|
||||
title: this.$ts._channel.featured,
|
||||
icon: 'fas fa-fire-alt',
|
||||
onClick: () => { this.tab = 'featured'; },
|
||||
}, {
|
||||
active: this.tab === 'following',
|
||||
title: this.$ts._channel.following,
|
||||
icon: 'fas fa-heart',
|
||||
onClick: () => { this.tab = 'following'; },
|
||||
}, {
|
||||
active: this.tab === 'owned',
|
||||
title: this.$ts._channel.owned,
|
||||
icon: 'fas fa-edit',
|
||||
onClick: () => { this.tab = 'owned'; },
|
||||
},]
|
||||
})),
|
||||
tab: 'featured',
|
||||
featuredPagination: {
|
||||
endpoint: 'channels/featured' as const,
|
||||
noPaging: true,
|
||||
},
|
||||
followingPagination: {
|
||||
endpoint: 'channels/followed' as const,
|
||||
limit: 5,
|
||||
},
|
||||
ownedPagination: {
|
||||
endpoint: 'channels/owned' as const,
|
||||
limit: 5,
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
create() {
|
||||
this.$router.push(`/channels/new`);
|
||||
}
|
||||
}
|
||||
});
|
||||
const router = useRouter();
|
||||
|
||||
let tab = $ref('featured');
|
||||
|
||||
const featuredPagination = {
|
||||
endpoint: 'channels/featured' as const,
|
||||
noPaging: true,
|
||||
};
|
||||
const followingPagination = {
|
||||
endpoint: 'channels/followed' as const,
|
||||
limit: 5,
|
||||
};
|
||||
const ownedPagination = {
|
||||
endpoint: 'channels/owned' as const,
|
||||
limit: 5,
|
||||
};
|
||||
|
||||
function create() {
|
||||
router.push('/channels/new');
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => [{
|
||||
icon: 'fas fa-plus',
|
||||
text: i18n.ts.create,
|
||||
handler: create,
|
||||
}]);
|
||||
|
||||
const headerTabs = $computed(() => [{
|
||||
active: tab === 'featured',
|
||||
title: i18n.ts._channel.featured,
|
||||
icon: 'fas fa-fire-alt',
|
||||
onClick: () => { tab = 'featured'; },
|
||||
}, {
|
||||
active: tab === 'following',
|
||||
title: i18n.ts._channel.following,
|
||||
icon: 'fas fa-heart',
|
||||
onClick: () => { tab = 'following'; },
|
||||
}, {
|
||||
active: tab === 'owned',
|
||||
title: i18n.ts._channel.owned,
|
||||
icon: 'fas fa-edit',
|
||||
onClick: () => { tab = 'owned'; },
|
||||
}]);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
title: i18n.ts.channel,
|
||||
icon: 'fas fa-satellite-dish',
|
||||
bg: 'var(--bg)',
|
||||
})));
|
||||
</script>
|
||||
|
@ -1,18 +1,21 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="800">
|
||||
<div v-if="clip">
|
||||
<div class="okzinsic _panel">
|
||||
<div v-if="clip.description" class="description">
|
||||
<Mfm :text="clip.description" :is-note="false" :i="$i"/>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions"/></template>
|
||||
<MkSpacer :content-max="800">
|
||||
<div v-if="clip">
|
||||
<div class="okzinsic _panel">
|
||||
<div v-if="clip.description" class="description">
|
||||
<Mfm :text="clip.description" :is-note="false" :i="$i"/>
|
||||
</div>
|
||||
<div class="user">
|
||||
<MkAvatar :user="clip.user" class="avatar" :show-indicator="true"/> <MkUserName :user="clip.user" :nowrap="false"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user">
|
||||
<MkAvatar :user="clip.user" class="avatar" :show-indicator="true"/> <MkUserName :user="clip.user" :nowrap="false"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<XNotes :pagination="pagination" :detail="true"/>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
<XNotes :pagination="pagination" :detail="true"/>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@ -22,7 +25,7 @@ import XNotes from '@/components/notes.vue';
|
||||
import { $i } from '@/account';
|
||||
import { i18n } from '@/i18n';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const props = defineProps<{
|
||||
clipId: string,
|
||||
@ -49,59 +52,58 @@ watch(() => props.clipId, async () => {
|
||||
|
||||
provide('currentClipPage', $$(clip));
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: computed(() => clip ? {
|
||||
title: clip.name,
|
||||
icon: 'fas fa-paperclip',
|
||||
bg: 'var(--bg)',
|
||||
actions: isOwned ? [{
|
||||
icon: 'fas fa-pencil-alt',
|
||||
text: i18n.ts.edit,
|
||||
handler: async (): Promise<void> => {
|
||||
const { canceled, result } = await os.form(clip.name, {
|
||||
name: {
|
||||
type: 'string',
|
||||
label: i18n.ts.name,
|
||||
default: clip.name,
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
multiline: true,
|
||||
label: i18n.ts.description,
|
||||
default: clip.description,
|
||||
},
|
||||
isPublic: {
|
||||
type: 'boolean',
|
||||
label: i18n.ts.public,
|
||||
default: clip.isPublic,
|
||||
},
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
os.apiWithDialog('clips/update', {
|
||||
clipId: clip.id,
|
||||
...result,
|
||||
});
|
||||
const headerActions = $computed(() => clip && isOwned ? [{
|
||||
icon: 'fas fa-pencil-alt',
|
||||
text: i18n.ts.edit,
|
||||
handler: async (): Promise<void> => {
|
||||
const { canceled, result } = await os.form(clip.name, {
|
||||
name: {
|
||||
type: 'string',
|
||||
label: i18n.ts.name,
|
||||
default: clip.name,
|
||||
},
|
||||
}, {
|
||||
icon: 'fas fa-trash-alt',
|
||||
text: i18n.ts.delete,
|
||||
danger: true,
|
||||
handler: async (): Promise<void> => {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('deleteAreYouSure', { x: clip.name }),
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
await os.apiWithDialog('clips/delete', {
|
||||
clipId: clip.id,
|
||||
});
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
multiline: true,
|
||||
label: i18n.ts.description,
|
||||
default: clip.description,
|
||||
},
|
||||
}] : [],
|
||||
} : null),
|
||||
});
|
||||
isPublic: {
|
||||
type: 'boolean',
|
||||
label: i18n.ts.public,
|
||||
default: clip.isPublic,
|
||||
},
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
os.apiWithDialog('clips/update', {
|
||||
clipId: clip.id,
|
||||
...result,
|
||||
});
|
||||
},
|
||||
}, {
|
||||
icon: 'fas fa-trash-alt',
|
||||
text: i18n.ts.delete,
|
||||
danger: true,
|
||||
handler: async (): Promise<void> => {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('deleteAreYouSure', { x: clip.name }),
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
await os.apiWithDialog('clips/delete', {
|
||||
clipId: clip.id,
|
||||
});
|
||||
},
|
||||
}] : null);
|
||||
|
||||
definePageMetadata(computed(() => clip ? {
|
||||
title: clip.name,
|
||||
icon: 'fas fa-paperclip',
|
||||
bg: 'var(--bg)',
|
||||
} : null));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -8,17 +8,19 @@
|
||||
import { computed } from 'vue';
|
||||
import XDrive from '@/components/drive.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
let folder = $ref(null);
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: computed(() => ({
|
||||
title: folder ? folder.name : i18n.ts.drive,
|
||||
icon: 'fas fa-cloud',
|
||||
bg: 'var(--bg)',
|
||||
hideHeader: true,
|
||||
})),
|
||||
});
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
title: folder ? folder.name : i18n.ts.drive,
|
||||
icon: 'fas fa-cloud',
|
||||
bg: 'var(--bg)',
|
||||
hideHeader: true,
|
||||
})));
|
||||
</script>
|
||||
|
@ -36,7 +36,6 @@ import MkSelect from '@/components/form/select.vue';
|
||||
import MkFolder from '@/components/ui/folder.vue';
|
||||
import MkTab from '@/components/tab.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { emojiCategories, emojiTags } from '@/instance';
|
||||
import XEmoji from './emojis.emoji.vue';
|
||||
|
||||
|
@ -1,15 +1,18 @@
|
||||
<template>
|
||||
<div :class="$style.root">
|
||||
<XCategory v-if="tab === 'category'"/>
|
||||
</div>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<div :class="$style.root">
|
||||
<XCategory v-if="tab === 'category'"/>
|
||||
</div>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import XCategory from './emojis.category.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const tab = ref('category');
|
||||
|
||||
@ -31,20 +34,21 @@ function menu(ev) {
|
||||
text: err.message,
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.customEmojis,
|
||||
icon: 'fas fa-laugh',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
icon: 'fas fa-ellipsis-h',
|
||||
handler: menu,
|
||||
}],
|
||||
},
|
||||
const headerActions = $computed(() => [{
|
||||
icon: 'fas fa-ellipsis-h',
|
||||
handler: menu,
|
||||
}]);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.customEmojis,
|
||||
icon: 'fas fa-laugh',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -1,11 +1,12 @@
|
||||
<template>
|
||||
<div>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="1200">
|
||||
<div class="lznhrdub">
|
||||
<div v-if="tab === 'local'">
|
||||
<div v-if="meta && stats && tag == null" class="localfedi7 _block _isolated" :style="{ backgroundImage: meta.bannerUrl ? `url(${meta.bannerUrl})` : null }">
|
||||
<header><span>{{ $t('explore', { host: meta.name || 'Misskey' }) }}</span></header>
|
||||
<div><span>{{ $t('exploreUsersCount', { count: num(stats.originalUsersCount) }) }}</span></div>
|
||||
<div v-if="instance && stats && tag == null" class="localfedi7 _block _isolated" :style="{ backgroundImage: instance.bannerUrl ? `url(${instance.bannerUrl})` : null }">
|
||||
<header><span>{{ $t('explore', { host: instance.name || 'Misskey' }) }}</span></header>
|
||||
<div><span>{{ $t('exploreUsersCount', { count: number(stats.originalUsersCount) }) }}</span></div>
|
||||
</div>
|
||||
|
||||
<template v-if="tag == null">
|
||||
@ -32,7 +33,7 @@
|
||||
<header><span>{{ $ts.exploreFediverse }}</span></header>
|
||||
</div>
|
||||
|
||||
<MkFolder ref="tags" :foldable="true" :expanded="false" class="_gap">
|
||||
<MkFolder ref="tagsEl" :foldable="true" :expanded="false" class="_gap">
|
||||
<template #header><i class="fas fa-hashtag fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularTags }}</template>
|
||||
|
||||
<div class="vxjfqztj">
|
||||
@ -74,147 +75,127 @@
|
||||
</MkRadios>
|
||||
</div>
|
||||
|
||||
<XUserList v-if="searchQuery" ref="search" class="_gap" :pagination="searchPagination"/>
|
||||
<XUserList v-if="searchQuery" ref="searchEl" class="_gap" :pagination="searchPagination"/>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineComponent, watch } from 'vue';
|
||||
import XUserList from '@/components/user-list.vue';
|
||||
import MkFolder from '@/components/ui/folder.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkRadios from '@/components/form/radios.vue';
|
||||
import number from '@/filters/number';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { i18n } from '@/i18n';
|
||||
import { instance } from '@/instance';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XUserList,
|
||||
MkFolder,
|
||||
MkInput,
|
||||
MkRadios,
|
||||
},
|
||||
const props = defineProps<{
|
||||
tag?: string;
|
||||
}>();
|
||||
|
||||
props: {
|
||||
tag: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
let tab = $ref('local');
|
||||
let tagsEl = $ref<InstanceType<typeof MkFolder>>();
|
||||
let tagsLocal = $ref([]);
|
||||
let tagsRemote = $ref([]);
|
||||
let stats = $ref(null);
|
||||
let searchQuery = $ref(null);
|
||||
let searchOrigin = $ref('combined');
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: computed(() => ({
|
||||
title: this.$ts.explore,
|
||||
icon: 'fas fa-hashtag',
|
||||
bg: 'var(--bg)',
|
||||
tabs: [{
|
||||
active: this.tab === 'local',
|
||||
title: this.$ts.local,
|
||||
onClick: () => { this.tab = 'local'; },
|
||||
}, {
|
||||
active: this.tab === 'remote',
|
||||
title: this.$ts.remote,
|
||||
onClick: () => { this.tab = 'remote'; },
|
||||
}, {
|
||||
active: this.tab === 'search',
|
||||
title: this.$ts.search,
|
||||
onClick: () => { this.tab = 'search'; },
|
||||
},]
|
||||
})),
|
||||
tab: 'local',
|
||||
pinnedUsers: { endpoint: 'pinned-users' },
|
||||
popularUsers: { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
state: 'alive',
|
||||
origin: 'local',
|
||||
sort: '+follower',
|
||||
} },
|
||||
recentlyUpdatedUsers: { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
origin: 'local',
|
||||
sort: '+updatedAt',
|
||||
} },
|
||||
recentlyRegisteredUsers: { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
origin: 'local',
|
||||
state: 'alive',
|
||||
sort: '+createdAt',
|
||||
} },
|
||||
popularUsersF: { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
state: 'alive',
|
||||
origin: 'remote',
|
||||
sort: '+follower',
|
||||
} },
|
||||
recentlyUpdatedUsersF: { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
origin: 'combined',
|
||||
sort: '+updatedAt',
|
||||
} },
|
||||
recentlyRegisteredUsersF: { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
origin: 'combined',
|
||||
sort: '+createdAt',
|
||||
} },
|
||||
searchPagination: {
|
||||
endpoint: 'users/search' as const,
|
||||
limit: 10,
|
||||
params: computed(() => (this.searchQuery && this.searchQuery !== '') ? {
|
||||
query: this.searchQuery,
|
||||
origin: this.searchOrigin,
|
||||
} : null)
|
||||
},
|
||||
tagsLocal: [],
|
||||
tagsRemote: [],
|
||||
stats: null,
|
||||
searchQuery: null,
|
||||
searchOrigin: 'combined',
|
||||
num: number,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
meta() {
|
||||
return this.$instance;
|
||||
},
|
||||
tagUsers(): any {
|
||||
return {
|
||||
endpoint: 'hashtags/users' as const,
|
||||
limit: 30,
|
||||
params: {
|
||||
tag: this.tag,
|
||||
origin: 'combined',
|
||||
sort: '+follower',
|
||||
}
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
tag() {
|
||||
if (this.$refs.tags) this.$refs.tags.toggleContent(this.tag == null);
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
os.api('hashtags/list', {
|
||||
sort: '+attachedLocalUsers',
|
||||
attachedToLocalUserOnly: true,
|
||||
limit: 30
|
||||
}).then(tags => {
|
||||
this.tagsLocal = tags;
|
||||
});
|
||||
os.api('hashtags/list', {
|
||||
sort: '+attachedRemoteUsers',
|
||||
attachedToRemoteUserOnly: true,
|
||||
limit: 30
|
||||
}).then(tags => {
|
||||
this.tagsRemote = tags;
|
||||
});
|
||||
os.api('stats').then(stats => {
|
||||
this.stats = stats;
|
||||
});
|
||||
},
|
||||
watch(() => props.tag, () => {
|
||||
if (tagsEl) tagsEl.toggleContent(props.tag == null);
|
||||
});
|
||||
|
||||
const tagUsers = $computed(() => ({
|
||||
endpoint: 'hashtags/users' as const,
|
||||
limit: 30,
|
||||
params: {
|
||||
tag: props.tag,
|
||||
origin: 'combined',
|
||||
sort: '+follower',
|
||||
},
|
||||
}));
|
||||
|
||||
const pinnedUsers = { endpoint: 'pinned-users' };
|
||||
const popularUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
state: 'alive',
|
||||
origin: 'local',
|
||||
sort: '+follower',
|
||||
} };
|
||||
const recentlyUpdatedUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
origin: 'local',
|
||||
sort: '+updatedAt',
|
||||
} };
|
||||
const recentlyRegisteredUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
origin: 'local',
|
||||
state: 'alive',
|
||||
sort: '+createdAt',
|
||||
} };
|
||||
const popularUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
state: 'alive',
|
||||
origin: 'remote',
|
||||
sort: '+follower',
|
||||
} };
|
||||
const recentlyUpdatedUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
origin: 'combined',
|
||||
sort: '+updatedAt',
|
||||
} };
|
||||
const recentlyRegisteredUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
origin: 'combined',
|
||||
sort: '+createdAt',
|
||||
} };
|
||||
const searchPagination = {
|
||||
endpoint: 'users/search' as const,
|
||||
limit: 10,
|
||||
params: computed(() => (searchQuery && searchQuery !== '') ? {
|
||||
query: searchQuery,
|
||||
origin: searchOrigin,
|
||||
} : null),
|
||||
};
|
||||
|
||||
os.api('hashtags/list', {
|
||||
sort: '+attachedLocalUsers',
|
||||
attachedToLocalUserOnly: true,
|
||||
limit: 30,
|
||||
}).then(tags => {
|
||||
tagsLocal = tags;
|
||||
});
|
||||
os.api('hashtags/list', {
|
||||
sort: '+attachedRemoteUsers',
|
||||
attachedToRemoteUserOnly: true,
|
||||
limit: 30,
|
||||
}).then(tags => {
|
||||
tagsRemote = tags;
|
||||
});
|
||||
os.api('stats').then(_stats => {
|
||||
stats = _stats;
|
||||
});
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => [{
|
||||
active: tab === 'local',
|
||||
title: i18n.ts.local,
|
||||
onClick: () => { tab = 'local'; },
|
||||
}, {
|
||||
active: tab === 'remote',
|
||||
title: i18n.ts.remote,
|
||||
onClick: () => { tab = 'remote'; },
|
||||
}, {
|
||||
active: tab === 'search',
|
||||
title: i18n.ts.search,
|
||||
onClick: () => { tab = 'search'; },
|
||||
}]);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
title: i18n.ts.explore,
|
||||
icon: 'fas fa-hashtag',
|
||||
bg: 'var(--bg)',
|
||||
})));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -1,20 +1,23 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="800">
|
||||
<MkPagination ref="pagingComponent" :pagination="pagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
||||
<div>{{ $ts.noNotes }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader/></template>
|
||||
<MkSpacer :content-max="800">
|
||||
<MkPagination ref="pagingComponent" :pagination="pagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
||||
<div>{{ $ts.noNotes }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #default="{ items }">
|
||||
<XList v-slot="{ item }" :items="items" :direction="'down'" :no-gap="false" :ad="false">
|
||||
<XNote :key="item.id" :note="item.note" :class="$style.note"/>
|
||||
</XList>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</MkSpacer>
|
||||
<template #default="{ items }">
|
||||
<XList v-slot="{ item }" :items="items" :direction="'down'" :no-gap="false" :ad="false">
|
||||
<XNote :key="item.id" :note="item.note" :class="$style.note"/>
|
||||
</XList>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@ -22,8 +25,8 @@ import { ref } from 'vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import XNote from '@/components/note.vue';
|
||||
import XList from '@/components/date-separated-list.vue';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'i/favorites' as const,
|
||||
@ -32,12 +35,10 @@ const pagination = {
|
||||
|
||||
const pagingComponent = ref<InstanceType<typeof MkPagination>>();
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.favorites,
|
||||
icon: 'fas fa-star',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
definePageMetadata({
|
||||
title: i18n.ts.favorites,
|
||||
icon: 'fas fa-star',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -1,13 +1,16 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="800">
|
||||
<XNotes ref="notes" :pagination="pagination"/>
|
||||
</MkSpacer>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader/></template>
|
||||
<MkSpacer :content-max="800">
|
||||
<XNotes ref="notes" :pagination="pagination"/>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import XNotes from '@/components/notes.vue';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'notes/featured' as const,
|
||||
@ -15,11 +18,9 @@ const pagination = {
|
||||
offsetMode: true,
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.featured,
|
||||
icon: 'fas fa-fire-alt',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
definePageMetadata({
|
||||
title: i18n.ts.featured,
|
||||
icon: 'fas fa-fire-alt',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
@ -1,94 +1,97 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="1000">
|
||||
<div class="taeiyria">
|
||||
<div class="query">
|
||||
<MkInput v-model="host" :debounce="true" class="">
|
||||
<template #prefix><i class="fas fa-search"></i></template>
|
||||
<template #label>{{ $ts.host }}</template>
|
||||
</MkInput>
|
||||
<FormSplit style="margin-top: var(--margin);">
|
||||
<MkSelect v-model="state">
|
||||
<template #label>{{ $ts.state }}</template>
|
||||
<option value="all">{{ $ts.all }}</option>
|
||||
<option value="federating">{{ $ts.federating }}</option>
|
||||
<option value="subscribing">{{ $ts.subscribing }}</option>
|
||||
<option value="publishing">{{ $ts.publishing }}</option>
|
||||
<option value="suspended">{{ $ts.suspended }}</option>
|
||||
<option value="blocked">{{ $ts.blocked }}</option>
|
||||
<option value="notResponding">{{ $ts.notResponding }}</option>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="sort">
|
||||
<template #label>{{ $ts.sort }}</template>
|
||||
<option value="+pubSub">{{ $ts.pubSub }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-pubSub">{{ $ts.pubSub }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+notes">{{ $ts.notes }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-notes">{{ $ts.notes }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+users">{{ $ts.users }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-users">{{ $ts.users }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+following">{{ $ts.following }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-following">{{ $ts.following }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+followers">{{ $ts.followers }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-followers">{{ $ts.followers }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+caughtAt">{{ $ts.registeredAt }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-caughtAt">{{ $ts.registeredAt }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.ascendingOrder }})</option>
|
||||
</MkSelect>
|
||||
</FormSplit>
|
||||
</div>
|
||||
|
||||
<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination">
|
||||
<div class="dqokceoi">
|
||||
<MkA v-for="instance in items" :key="instance.id" class="instance" :to="`/instance-info/${instance.host}`">
|
||||
<div class="host"><img :src="instance.faviconUrl">{{ instance.host }}</div>
|
||||
<div class="table">
|
||||
<div class="cell">
|
||||
<div class="key">{{ $ts.registeredAt }}</div>
|
||||
<div class="value"><MkTime :time="instance.caughtAt"/></div>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<div class="key">{{ $ts.software }}</div>
|
||||
<div class="value">{{ instance.softwareName || `(${$ts.unknown})` }}</div>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<div class="key">{{ $ts.version }}</div>
|
||||
<div class="value">{{ instance.softwareVersion || `(${$ts.unknown})` }}</div>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<div class="key">{{ $ts.users }}</div>
|
||||
<div class="value">{{ instance.usersCount }}</div>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<div class="key">{{ $ts.notes }}</div>
|
||||
<div class="value">{{ instance.notesCount }}</div>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<div class="key">{{ $ts.sent }}</div>
|
||||
<div class="value"><MkTime v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></div>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<div class="key">{{ $ts.received }}</div>
|
||||
<div class="value"><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<span class="status" :class="getStatus(instance)">{{ getStatus(instance) }}</span>
|
||||
<span class="pubSub">
|
||||
<span v-if="instance.followersCount > 0" class="sub"><i class="fas fa-caret-down icon"></i>Sub</span>
|
||||
<span v-else class="sub"><i class="fas fa-caret-down icon"></i>-</span>
|
||||
<span v-if="instance.followingCount > 0" class="pub"><i class="fas fa-caret-up icon"></i>Pub</span>
|
||||
<span v-else class="pub"><i class="fas fa-caret-up icon"></i>-</span>
|
||||
</span>
|
||||
<span class="right">
|
||||
<span class="latestStatus">{{ instance.latestStatus || '-' }}</span>
|
||||
<span class="lastCommunicatedAt"><MkTime :time="instance.lastCommunicatedAt"/></span>
|
||||
</span>
|
||||
</div>
|
||||
</MkA>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="1000">
|
||||
<div class="taeiyria">
|
||||
<div class="query">
|
||||
<MkInput v-model="host" :debounce="true" class="">
|
||||
<template #prefix><i class="fas fa-search"></i></template>
|
||||
<template #label>{{ $ts.host }}</template>
|
||||
</MkInput>
|
||||
<FormSplit style="margin-top: var(--margin);">
|
||||
<MkSelect v-model="state">
|
||||
<template #label>{{ $ts.state }}</template>
|
||||
<option value="all">{{ $ts.all }}</option>
|
||||
<option value="federating">{{ $ts.federating }}</option>
|
||||
<option value="subscribing">{{ $ts.subscribing }}</option>
|
||||
<option value="publishing">{{ $ts.publishing }}</option>
|
||||
<option value="suspended">{{ $ts.suspended }}</option>
|
||||
<option value="blocked">{{ $ts.blocked }}</option>
|
||||
<option value="notResponding">{{ $ts.notResponding }}</option>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="sort">
|
||||
<template #label>{{ $ts.sort }}</template>
|
||||
<option value="+pubSub">{{ $ts.pubSub }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-pubSub">{{ $ts.pubSub }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+notes">{{ $ts.notes }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-notes">{{ $ts.notes }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+users">{{ $ts.users }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-users">{{ $ts.users }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+following">{{ $ts.following }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-following">{{ $ts.following }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+followers">{{ $ts.followers }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-followers">{{ $ts.followers }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+caughtAt">{{ $ts.registeredAt }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-caughtAt">{{ $ts.registeredAt }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.ascendingOrder }})</option>
|
||||
</MkSelect>
|
||||
</FormSplit>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
|
||||
<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination">
|
||||
<div class="dqokceoi">
|
||||
<MkA v-for="instance in items" :key="instance.id" class="instance" :to="`/instance-info/${instance.host}`">
|
||||
<div class="host"><img :src="instance.faviconUrl">{{ instance.host }}</div>
|
||||
<div class="table">
|
||||
<div class="cell">
|
||||
<div class="key">{{ $ts.registeredAt }}</div>
|
||||
<div class="value"><MkTime :time="instance.caughtAt"/></div>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<div class="key">{{ $ts.software }}</div>
|
||||
<div class="value">{{ instance.softwareName || `(${$ts.unknown})` }}</div>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<div class="key">{{ $ts.version }}</div>
|
||||
<div class="value">{{ instance.softwareVersion || `(${$ts.unknown})` }}</div>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<div class="key">{{ $ts.users }}</div>
|
||||
<div class="value">{{ instance.usersCount }}</div>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<div class="key">{{ $ts.notes }}</div>
|
||||
<div class="value">{{ instance.notesCount }}</div>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<div class="key">{{ $ts.sent }}</div>
|
||||
<div class="value"><MkTime v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></div>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<div class="key">{{ $ts.received }}</div>
|
||||
<div class="value"><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<span class="status" :class="getStatus(instance)">{{ getStatus(instance) }}</span>
|
||||
<span class="pubSub">
|
||||
<span v-if="instance.followersCount > 0" class="sub"><i class="fas fa-caret-down icon"></i>Sub</span>
|
||||
<span v-else class="sub"><i class="fas fa-caret-down icon"></i>-</span>
|
||||
<span v-if="instance.followingCount > 0" class="pub"><i class="fas fa-caret-up icon"></i>Pub</span>
|
||||
<span v-else class="pub"><i class="fas fa-caret-up icon"></i>-</span>
|
||||
</span>
|
||||
<span class="right">
|
||||
<span class="latestStatus">{{ instance.latestStatus || '-' }}</span>
|
||||
<span class="lastCommunicatedAt"><MkTime :time="instance.lastCommunicatedAt"/></span>
|
||||
</span>
|
||||
</div>
|
||||
</MkA>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@ -99,8 +102,8 @@ import MkSelect from '@/components/form/select.vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
let host = $ref('');
|
||||
let state = $ref('federating');
|
||||
@ -119,8 +122,8 @@ const pagination = {
|
||||
state === 'suspended' ? { suspended: true } :
|
||||
state === 'blocked' ? { blocked: true } :
|
||||
state === 'notResponding' ? { notResponding: true } :
|
||||
{})
|
||||
}))
|
||||
{}),
|
||||
})),
|
||||
};
|
||||
|
||||
function getStatus(instance) {
|
||||
@ -129,12 +132,14 @@ function getStatus(instance) {
|
||||
return 'alive';
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.federation,
|
||||
icon: 'fas fa-globe',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.federation,
|
||||
icon: 'fas fa-globe',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
<div>{{ $ts.noFollowRequests }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot="{items}">
|
||||
<template #default="{items}">
|
||||
<div class="mk-follow-requests">
|
||||
<div v-for="req in items" :key="req.id" class="user _panel">
|
||||
<MkAvatar class="avatar" :user="req.follower" :show-indicator="true"/>
|
||||
@ -36,8 +36,8 @@ import { ref, computed } from 'vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import { userPage, acct } from '@/filters/user';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const paginationComponent = ref<InstanceType<typeof MkPagination>>();
|
||||
|
||||
@ -58,13 +58,15 @@ function reject(user) {
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: computed(() => ({
|
||||
title: i18n.ts.followRequests,
|
||||
icon: 'fas fa-user-clock',
|
||||
bg: 'var(--bg)',
|
||||
})),
|
||||
});
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
title: i18n.ts.followRequests,
|
||||
icon: 'fas fa-user-clock',
|
||||
bg: 'var(--bg)',
|
||||
})));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -5,8 +5,9 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import * as os from '@/os';
|
||||
import * as Acct from 'misskey-js/built/acct';
|
||||
import * as os from '@/os';
|
||||
import { mainRouter } from '@/router';
|
||||
|
||||
export default defineComponent({
|
||||
created() {
|
||||
@ -17,17 +18,17 @@ export default defineComponent({
|
||||
|
||||
if (acct.startsWith('https://')) {
|
||||
promise = os.api('ap/show', {
|
||||
uri: acct
|
||||
uri: acct,
|
||||
});
|
||||
promise.then(res => {
|
||||
if (res.type === 'User') {
|
||||
this.follow(res.object);
|
||||
} else if (res.type === 'Note') {
|
||||
this.$router.push(`/notes/${res.object.id}`);
|
||||
mainRouter.push(`/notes/${res.object.id}`);
|
||||
} else {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: 'Not a user'
|
||||
text: 'Not a user',
|
||||
}).then(() => {
|
||||
window.close();
|
||||
});
|
||||
@ -56,9 +57,9 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
os.apiWithDialog('following/create', {
|
||||
userId: user.id
|
||||
userId: user.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
@ -27,8 +27,8 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, watch } from 'vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import FormTextarea from '@/components/form/textarea.vue';
|
||||
@ -37,104 +37,87 @@ import FormGroup from '@/components/form/group.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import { selectFiles } from '@/scripts/select-file';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { useRouter } from '@/router';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormButton,
|
||||
FormInput,
|
||||
FormTextarea,
|
||||
FormSwitch,
|
||||
FormGroup,
|
||||
FormSuspense,
|
||||
},
|
||||
const router = useRouter();
|
||||
|
||||
props: {
|
||||
postId: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: computed(() => this.postId ? {
|
||||
title: this.$ts.edit,
|
||||
icon: 'fas fa-pencil-alt'
|
||||
} : {
|
||||
title: this.$ts.postToGallery,
|
||||
icon: 'fas fa-pencil-alt'
|
||||
}),
|
||||
init: null,
|
||||
files: [],
|
||||
description: null,
|
||||
title: null,
|
||||
isSensitive: false,
|
||||
};
|
||||
},
|
||||
const props = defineProps<{
|
||||
postId?: string;
|
||||
}>();
|
||||
|
||||
watch: {
|
||||
postId: {
|
||||
handler() {
|
||||
this.init = () => this.postId ? os.api('gallery/posts/show', {
|
||||
postId: this.postId
|
||||
}).then(post => {
|
||||
this.files = post.files;
|
||||
this.title = post.title;
|
||||
this.description = post.description;
|
||||
this.isSensitive = post.isSensitive;
|
||||
}) : Promise.resolve(null);
|
||||
},
|
||||
immediate: true,
|
||||
}
|
||||
},
|
||||
let init = $ref(null);
|
||||
let files = $ref([]);
|
||||
let description = $ref(null);
|
||||
let title = $ref(null);
|
||||
let isSensitive = $ref(false);
|
||||
|
||||
methods: {
|
||||
selectFile(evt) {
|
||||
selectFiles(evt.currentTarget ?? evt.target, null).then(files => {
|
||||
this.files = this.files.concat(files);
|
||||
});
|
||||
},
|
||||
function selectFile(evt) {
|
||||
selectFiles(evt.currentTarget ?? evt.target, null).then(selected => {
|
||||
files = files.concat(selected);
|
||||
});
|
||||
}
|
||||
|
||||
remove(file) {
|
||||
this.files = this.files.filter(f => f.id !== file.id);
|
||||
},
|
||||
function remove(file) {
|
||||
files = files.filter(f => f.id !== file.id);
|
||||
}
|
||||
|
||||
async save() {
|
||||
if (this.postId) {
|
||||
await os.apiWithDialog('gallery/posts/update', {
|
||||
postId: this.postId,
|
||||
title: this.title,
|
||||
description: this.description,
|
||||
fileIds: this.files.map(file => file.id),
|
||||
isSensitive: this.isSensitive,
|
||||
});
|
||||
this.$router.push(`/gallery/${this.postId}`);
|
||||
} else {
|
||||
const post = await os.apiWithDialog('gallery/posts/create', {
|
||||
title: this.title,
|
||||
description: this.description,
|
||||
fileIds: this.files.map(file => file.id),
|
||||
isSensitive: this.isSensitive,
|
||||
});
|
||||
this.$router.push(`/gallery/${post.id}`);
|
||||
}
|
||||
},
|
||||
|
||||
async del() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: this.$ts.deleteConfirm,
|
||||
});
|
||||
if (canceled) return;
|
||||
await os.apiWithDialog('gallery/posts/delete', {
|
||||
postId: this.postId,
|
||||
});
|
||||
this.$router.push(`/gallery`);
|
||||
}
|
||||
async function save() {
|
||||
if (props.postId) {
|
||||
await os.apiWithDialog('gallery/posts/update', {
|
||||
postId: props.postId,
|
||||
title: title,
|
||||
description: description,
|
||||
fileIds: files.map(file => file.id),
|
||||
isSensitive: isSensitive,
|
||||
});
|
||||
mainRouter.push(`/gallery/${props.postId}`);
|
||||
} else {
|
||||
const created = await os.apiWithDialog('gallery/posts/create', {
|
||||
title: title,
|
||||
description: description,
|
||||
fileIds: files.map(file => file.id),
|
||||
isSensitive: isSensitive,
|
||||
});
|
||||
router.push(`/gallery/${created.id}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function del() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.deleteConfirm,
|
||||
});
|
||||
if (canceled) return;
|
||||
await os.apiWithDialog('gallery/posts/delete', {
|
||||
postId: props.postId,
|
||||
});
|
||||
mainRouter.push('/gallery');
|
||||
}
|
||||
|
||||
watch(() => props.postId, () => {
|
||||
init = () => props.postId ? os.api('gallery/posts/show', {
|
||||
postId: props.postId,
|
||||
}).then(post => {
|
||||
files = post.files;
|
||||
title = post.title;
|
||||
description = post.description;
|
||||
isSensitive = post.isSensitive;
|
||||
}) : Promise.resolve(null);
|
||||
}, { immediate: true });
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => props.postId ? {
|
||||
title: i18n.ts.edit,
|
||||
icon: 'fas fa-pencil-alt',
|
||||
} : {
|
||||
title: i18n.ts.postToGallery,
|
||||
icon: 'fas fa-pencil-alt',
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -1,49 +1,54 @@
|
||||
<template>
|
||||
<div class="xprsixdl _root">
|
||||
<MkTab v-if="$i" v-model="tab">
|
||||
<option value="explore"><i class="fas fa-icons"></i> {{ $ts.gallery }}</option>
|
||||
<option value="liked"><i class="fas fa-heart"></i> {{ $ts._gallery.liked }}</option>
|
||||
<option value="my"><i class="fas fa-edit"></i> {{ $ts._gallery.my }}</option>
|
||||
</MkTab>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="1400">
|
||||
<div class="_root">
|
||||
<MkTab v-if="$i" v-model="tab">
|
||||
<option value="explore"><i class="fas fa-icons"></i> {{ $ts.gallery }}</option>
|
||||
<option value="liked"><i class="fas fa-heart"></i> {{ $ts._gallery.liked }}</option>
|
||||
<option value="my"><i class="fas fa-edit"></i> {{ $ts._gallery.my }}</option>
|
||||
</MkTab>
|
||||
|
||||
<div v-if="tab === 'explore'">
|
||||
<MkFolder class="_gap">
|
||||
<template #header><i class="fas fa-clock"></i>{{ $ts.recentPosts }}</template>
|
||||
<MkPagination v-slot="{items}" :pagination="recentPostsPagination" :disable-auto-load="true">
|
||||
<div class="vfpdbgtk">
|
||||
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</MkFolder>
|
||||
<MkFolder class="_gap">
|
||||
<template #header><i class="fas fa-fire-alt"></i>{{ $ts.popularPosts }}</template>
|
||||
<MkPagination v-slot="{items}" :pagination="popularPostsPagination" :disable-auto-load="true">
|
||||
<div class="vfpdbgtk">
|
||||
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</MkFolder>
|
||||
</div>
|
||||
<div v-else-if="tab === 'liked'">
|
||||
<MkPagination v-slot="{items}" :pagination="likedPostsPagination">
|
||||
<div class="vfpdbgtk">
|
||||
<MkGalleryPostPreview v-for="like in items" :key="like.id" :post="like.post" class="post"/>
|
||||
<div v-if="tab === 'explore'">
|
||||
<MkFolder class="_gap">
|
||||
<template #header><i class="fas fa-clock"></i>{{ $ts.recentPosts }}</template>
|
||||
<MkPagination v-slot="{items}" :pagination="recentPostsPagination" :disable-auto-load="true">
|
||||
<div class="vfpdbgtk">
|
||||
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</MkFolder>
|
||||
<MkFolder class="_gap">
|
||||
<template #header><i class="fas fa-fire-alt"></i>{{ $ts.popularPosts }}</template>
|
||||
<MkPagination v-slot="{items}" :pagination="popularPostsPagination" :disable-auto-load="true">
|
||||
<div class="vfpdbgtk">
|
||||
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</MkFolder>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div v-else-if="tab === 'my'">
|
||||
<MkA to="/gallery/new" class="_link" style="margin: 16px;"><i class="fas fa-plus"></i> {{ $ts.postToGallery }}</MkA>
|
||||
<MkPagination v-slot="{items}" :pagination="myPostsPagination">
|
||||
<div class="vfpdbgtk">
|
||||
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
|
||||
<div v-else-if="tab === 'liked'">
|
||||
<MkPagination v-slot="{items}" :pagination="likedPostsPagination">
|
||||
<div class="vfpdbgtk">
|
||||
<MkGalleryPostPreview v-for="like in items" :key="like.id" :post="like.post" class="post"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="tab === 'my'">
|
||||
<MkA to="/gallery/new" class="_link" style="margin: 16px;"><i class="fas fa-plus"></i> {{ $ts.postToGallery }}</MkA>
|
||||
<MkPagination v-slot="{items}" :pagination="myPostsPagination">
|
||||
<div class="vfpdbgtk">
|
||||
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineComponent, watch } from 'vue';
|
||||
import XUserList from '@/components/user-list.vue';
|
||||
import MkFolder from '@/components/ui/folder.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
@ -53,92 +58,60 @@ import MkPagination from '@/components/ui/pagination.vue';
|
||||
import MkGalleryPostPreview from '@/components/gallery-post-preview.vue';
|
||||
import number from '@/filters/number';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XUserList,
|
||||
MkFolder,
|
||||
MkInput,
|
||||
MkButton,
|
||||
MkTab,
|
||||
MkPagination,
|
||||
MkGalleryPostPreview,
|
||||
const props = defineProps<{
|
||||
tag?: string;
|
||||
}>();
|
||||
|
||||
let tab = $ref('explore');
|
||||
let tags = $ref([]);
|
||||
let tagsRef = $ref();
|
||||
|
||||
const recentPostsPagination = {
|
||||
endpoint: 'gallery/posts' as const,
|
||||
limit: 6,
|
||||
};
|
||||
const popularPostsPagination = {
|
||||
endpoint: 'gallery/featured' as const,
|
||||
limit: 5,
|
||||
};
|
||||
const myPostsPagination = {
|
||||
endpoint: 'i/gallery/posts' as const,
|
||||
limit: 5,
|
||||
};
|
||||
const likedPostsPagination = {
|
||||
endpoint: 'i/gallery/likes' as const,
|
||||
limit: 5,
|
||||
};
|
||||
|
||||
const tagUsersPagination = $computed(() => ({
|
||||
endpoint: 'hashtags/users' as const,
|
||||
limit: 30,
|
||||
params: {
|
||||
tag: this.tag,
|
||||
origin: 'combined',
|
||||
sort: '+follower',
|
||||
},
|
||||
}));
|
||||
|
||||
props: {
|
||||
tag: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
watch(() => props.tag, () => {
|
||||
if (tagsRef) tagsRef.tags.toggleContent(props.tag == null);
|
||||
});
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.gallery,
|
||||
icon: 'fas fa-icons'
|
||||
},
|
||||
tab: 'explore',
|
||||
recentPostsPagination: {
|
||||
endpoint: 'gallery/posts' as const,
|
||||
limit: 6,
|
||||
},
|
||||
popularPostsPagination: {
|
||||
endpoint: 'gallery/featured' as const,
|
||||
limit: 5,
|
||||
},
|
||||
myPostsPagination: {
|
||||
endpoint: 'i/gallery/posts' as const,
|
||||
limit: 5,
|
||||
},
|
||||
likedPostsPagination: {
|
||||
endpoint: 'i/gallery/likes' as const,
|
||||
limit: 5,
|
||||
},
|
||||
tags: [],
|
||||
};
|
||||
},
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
computed: {
|
||||
meta() {
|
||||
return this.$instance;
|
||||
},
|
||||
tagUsers(): any {
|
||||
return {
|
||||
endpoint: 'hashtags/users' as const,
|
||||
limit: 30,
|
||||
params: {
|
||||
tag: this.tag,
|
||||
origin: 'combined',
|
||||
sort: '+follower',
|
||||
}
|
||||
};
|
||||
},
|
||||
},
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
watch: {
|
||||
tag() {
|
||||
if (this.$refs.tags) this.$refs.tags.toggleContent(this.tag == null);
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
}
|
||||
definePageMetadata({
|
||||
title: i18n.ts.gallery,
|
||||
icon: 'fas fa-icons',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.xprsixdl {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.vfpdbgtk {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
|
@ -49,123 +49,108 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineComponent, inject, watch } from 'vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import MkContainer from '@/components/ui/container.vue';
|
||||
import ImgWithBlurhash from '@/components/img-with-blurhash.vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import MkGalleryPostPreview from '@/components/gallery-post-preview.vue';
|
||||
import MkFollowButton from '@/components/follow-button.vue';
|
||||
import { url } from '@/config';
|
||||
import { useRouter } from '@/router';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkContainer,
|
||||
ImgWithBlurhash,
|
||||
MkPagination,
|
||||
MkGalleryPostPreview,
|
||||
MkButton,
|
||||
MkFollowButton,
|
||||
const router = useRouter();
|
||||
|
||||
const props = defineProps<{
|
||||
postId: string;
|
||||
}>();
|
||||
|
||||
const post = $ref(null);
|
||||
const error = $ref(null);
|
||||
const otherPostsPagination = {
|
||||
endpoint: 'users/gallery/posts' as const,
|
||||
limit: 6,
|
||||
params: computed(() => ({
|
||||
userId: post.user.id,
|
||||
})),
|
||||
};
|
||||
|
||||
function fetchPost() {
|
||||
post = null;
|
||||
os.api('gallery/posts/show', {
|
||||
postId: props.postId,
|
||||
}).then(_post => {
|
||||
post = _post;
|
||||
}).catch(_error => {
|
||||
error = _error;
|
||||
});
|
||||
}
|
||||
|
||||
function share() {
|
||||
navigator.share({
|
||||
title: post.title,
|
||||
text: post.description,
|
||||
url: `${url}/gallery/${post.id}`,
|
||||
});
|
||||
}
|
||||
|
||||
function shareWithNote() {
|
||||
os.post({
|
||||
initialText: `${post.title} ${url}/gallery/${post.id}`,
|
||||
});
|
||||
}
|
||||
|
||||
function like() {
|
||||
os.apiWithDialog('gallery/posts/like', {
|
||||
postId: props.postId,
|
||||
}).then(() => {
|
||||
post.isLiked = true;
|
||||
post.likedCount++;
|
||||
});
|
||||
}
|
||||
|
||||
async function unlike() {
|
||||
const confirm = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.unlikeConfirm,
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
os.apiWithDialog('gallery/posts/unlike', {
|
||||
postId: props.postId,
|
||||
}).then(() => {
|
||||
post.isLiked = false;
|
||||
post.likedCount--;
|
||||
});
|
||||
}
|
||||
|
||||
function edit() {
|
||||
router.push(`/gallery/${post.id}/edit`);
|
||||
}
|
||||
|
||||
watch(() => props.postId, fetchPost, { immediate: true });
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => post ? {
|
||||
title: post.title,
|
||||
avatar: post.user,
|
||||
path: `/gallery/${post.id}`,
|
||||
share: {
|
||||
title: post.title,
|
||||
text: post.description,
|
||||
},
|
||||
props: {
|
||||
postId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: computed(() => this.post ? {
|
||||
title: this.post.title,
|
||||
avatar: this.post.user,
|
||||
path: `/gallery/${this.post.id}`,
|
||||
share: {
|
||||
title: this.post.title,
|
||||
text: this.post.description,
|
||||
},
|
||||
actions: [{
|
||||
icon: 'fas fa-pencil-alt',
|
||||
text: this.$ts.edit,
|
||||
handler: this.edit
|
||||
}]
|
||||
} : null),
|
||||
otherPostsPagination: {
|
||||
endpoint: 'users/gallery/posts' as const,
|
||||
limit: 6,
|
||||
params: computed(() => ({
|
||||
userId: this.post.user.id
|
||||
})),
|
||||
},
|
||||
post: null,
|
||||
error: null,
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
postId: 'fetch'
|
||||
},
|
||||
|
||||
created() {
|
||||
this.fetch();
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetch() {
|
||||
this.post = null;
|
||||
os.api('gallery/posts/show', {
|
||||
postId: this.postId
|
||||
}).then(post => {
|
||||
this.post = post;
|
||||
}).catch(err => {
|
||||
this.error = err;
|
||||
});
|
||||
},
|
||||
|
||||
share() {
|
||||
navigator.share({
|
||||
title: this.post.title,
|
||||
text: this.post.description,
|
||||
url: `${url}/gallery/${this.post.id}`
|
||||
});
|
||||
},
|
||||
|
||||
shareWithNote() {
|
||||
os.post({
|
||||
initialText: `${this.post.title} ${url}/gallery/${this.post.id}`
|
||||
});
|
||||
},
|
||||
|
||||
like() {
|
||||
os.apiWithDialog('gallery/posts/like', {
|
||||
postId: this.postId,
|
||||
}).then(() => {
|
||||
this.post.isLiked = true;
|
||||
this.post.likedCount++;
|
||||
});
|
||||
},
|
||||
|
||||
async unlike() {
|
||||
const confirm = await os.confirm({
|
||||
type: 'warning',
|
||||
text: this.$ts.unlikeConfirm,
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
os.apiWithDialog('gallery/posts/unlike', {
|
||||
postId: this.postId,
|
||||
}).then(() => {
|
||||
this.post.isLiked = false;
|
||||
this.post.likedCount--;
|
||||
});
|
||||
},
|
||||
|
||||
edit() {
|
||||
this.$router.push(`/gallery/${this.post.id}/edit`);
|
||||
}
|
||||
}
|
||||
});
|
||||
actions: [{
|
||||
icon: 'fas fa-pencil-alt',
|
||||
text: i18n.ts.edit,
|
||||
handler: edit,
|
||||
}],
|
||||
} : null));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="600" :margin-min="16" :margin-max="32">
|
||||
<template><MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="600" :margin-min="16" :margin-max="32">
|
||||
<div v-if="instance" class="_formRoot">
|
||||
<div class="fnfelxur">
|
||||
<img :src="instance.iconUrl || instance.faviconUrl" alt="" class="icon"/>
|
||||
@ -102,7 +103,7 @@
|
||||
<FormLink :to="`https://${host}/manifest.json`" external style="margin-bottom: 8px;">manifest.json</FormLink>
|
||||
</FormSection>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkSpacer></MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@ -120,8 +121,8 @@ import FormSwitch from '@/components/form/switch.vue';
|
||||
import * as os from '@/os';
|
||||
import number from '@/filters/number';
|
||||
import bytes from '@/filters/bytes';
|
||||
import * as symbols from '@/symbols';
|
||||
import { iAmModerator } from '@/account';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const props = defineProps<{
|
||||
host: string;
|
||||
@ -146,7 +147,7 @@ async function fetch() {
|
||||
async function toggleBlock(ev) {
|
||||
if (meta == null) return;
|
||||
await os.api('admin/update-meta', {
|
||||
blockedHosts: isBlocked ? meta.blockedHosts.concat([instance.host]) : meta.blockedHosts.filter(x => x !== instance.host)
|
||||
blockedHosts: isBlocked ? meta.blockedHosts.concat([instance.host]) : meta.blockedHosts.filter(x => x !== instance.host),
|
||||
});
|
||||
}
|
||||
|
||||
@ -168,19 +169,21 @@ function refreshMetadata() {
|
||||
|
||||
fetch();
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: props.host,
|
||||
icon: 'fas fa-info-circle',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
text: `https://${props.host}`,
|
||||
icon: 'fas fa-external-link-alt',
|
||||
handler: () => {
|
||||
window.open(`https://${props.host}`, '_blank');
|
||||
}
|
||||
}],
|
||||
},
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: props.host,
|
||||
icon: 'fas fa-info-circle',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
text: `https://${props.host}`,
|
||||
icon: 'fas fa-external-link-alt',
|
||||
handler: () => {
|
||||
window.open(`https://${props.host}`, '_blank');
|
||||
},
|
||||
}],
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -1,24 +1,27 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="800">
|
||||
<template><MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="800">
|
||||
<XNotes :pagination="pagination"/>
|
||||
</MkSpacer>
|
||||
</MkSpacer></MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import XNotes from '@/components/notes.vue';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'notes/mentions' as const,
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.mentions,
|
||||
icon: 'fas fa-at',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.mentions,
|
||||
icon: 'fas fa-at',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
@ -1,27 +1,30 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="800">
|
||||
<template><MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="800">
|
||||
<XNotes :pagination="pagination"/>
|
||||
</MkSpacer>
|
||||
</MkSpacer></MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import XNotes from '@/components/notes.vue';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'notes/mentions' as const,
|
||||
limit: 10,
|
||||
params: {
|
||||
visibility: 'specified'
|
||||
visibility: 'specified',
|
||||
},
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.directNotes,
|
||||
icon: 'fas fa-envelope',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.directNotes,
|
||||
icon: 'fas fa-envelope',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
@ -1,165 +1,165 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="800">
|
||||
<div v-size="{ max: [400] }" class="yweeujhr">
|
||||
<MkButton primary class="start" @click="start"><i class="fas fa-plus"></i> {{ $ts.startMessaging }}</MkButton>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="800">
|
||||
<div v-size="{ max: [400] }" class="yweeujhr">
|
||||
<MkButton primary class="start" @click="start"><i class="fas fa-plus"></i> {{ $ts.startMessaging }}</MkButton>
|
||||
|
||||
<div v-if="messages.length > 0" class="history">
|
||||
<MkA v-for="(message, i) in messages"
|
||||
:key="message.id"
|
||||
v-anim="i"
|
||||
class="message _block"
|
||||
:class="{ isMe: isMe(message), isRead: message.groupId ? message.reads.includes($i.id) : message.isRead }"
|
||||
:to="message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`"
|
||||
:data-index="i"
|
||||
>
|
||||
<div>
|
||||
<MkAvatar class="avatar" :user="message.groupId ? message.user : isMe(message) ? message.recipient : message.user" :show-indicator="true"/>
|
||||
<header v-if="message.groupId">
|
||||
<span class="name">{{ message.group.name }}</span>
|
||||
<MkTime :time="message.createdAt" class="time"/>
|
||||
</header>
|
||||
<header v-else>
|
||||
<span class="name"><MkUserName :user="isMe(message) ? message.recipient : message.user"/></span>
|
||||
<span class="username">@{{ acct(isMe(message) ? message.recipient : message.user) }}</span>
|
||||
<MkTime :time="message.createdAt" class="time"/>
|
||||
</header>
|
||||
<div class="body">
|
||||
<p class="text"><span v-if="isMe(message)" class="me">{{ $ts.you }}:</span>{{ message.text }}</p>
|
||||
<div v-if="messages.length > 0" class="history">
|
||||
<MkA
|
||||
v-for="(message, i) in messages"
|
||||
:key="message.id"
|
||||
v-anim="i"
|
||||
class="message _block"
|
||||
:class="{ isMe: isMe(message), isRead: message.groupId ? message.reads.includes($i.id) : message.isRead }"
|
||||
:to="message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`"
|
||||
:data-index="i"
|
||||
>
|
||||
<div>
|
||||
<MkAvatar class="avatar" :user="message.groupId ? message.user : isMe(message) ? message.recipient : message.user" :show-indicator="true"/>
|
||||
<header v-if="message.groupId">
|
||||
<span class="name">{{ message.group.name }}</span>
|
||||
<MkTime :time="message.createdAt" class="time"/>
|
||||
</header>
|
||||
<header v-else>
|
||||
<span class="name"><MkUserName :user="isMe(message) ? message.recipient : message.user"/></span>
|
||||
<span class="username">@{{ acct(isMe(message) ? message.recipient : message.user) }}</span>
|
||||
<MkTime :time="message.createdAt" class="time"/>
|
||||
</header>
|
||||
<div class="body">
|
||||
<p class="text"><span v-if="isMe(message)" class="me">{{ $ts.you }}:</span>{{ message.text }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MkA>
|
||||
</MkA>
|
||||
</div>
|
||||
<div v-if="!fetching && messages.length == 0" class="_fullinfo">
|
||||
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
||||
<div>{{ $ts.noHistory }}</div>
|
||||
</div>
|
||||
<MkLoading v-if="fetching"/>
|
||||
</div>
|
||||
<div v-if="!fetching && messages.length == 0" class="_fullinfo">
|
||||
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
||||
<div>{{ $ts.noHistory }}</div>
|
||||
</div>
|
||||
<MkLoading v-if="fetching"/>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent, defineComponent, markRaw } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, defineComponent, inject, markRaw, onMounted, onUnmounted } from 'vue';
|
||||
import * as Acct from 'misskey-js/built/acct';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import { acct } from '@/filters/user';
|
||||
import * as os from '@/os';
|
||||
import { stream } from '@/stream';
|
||||
import * as symbols from '@/symbols';
|
||||
import { useRouter } from '@/router';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { $i } from '@/account';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton
|
||||
},
|
||||
const router = useRouter();
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.messaging,
|
||||
icon: 'fas fa-comments',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
fetching: true,
|
||||
moreFetching: false,
|
||||
messages: [],
|
||||
connection: null,
|
||||
};
|
||||
},
|
||||
let fetching = $ref(true);
|
||||
let moreFetching = $ref(false);
|
||||
let messages = $ref([]);
|
||||
let connection = $ref(null);
|
||||
|
||||
mounted() {
|
||||
this.connection = markRaw(stream.useChannel('messagingIndex'));
|
||||
const getAcct = Acct.toString;
|
||||
|
||||
this.connection.on('message', this.onMessage);
|
||||
this.connection.on('read', this.onRead);
|
||||
function isMe(message) {
|
||||
return message.userId === $i.id;
|
||||
}
|
||||
|
||||
os.api('messaging/history', { group: false }).then(userMessages => {
|
||||
os.api('messaging/history', { group: true }).then(groupMessages => {
|
||||
const messages = userMessages.concat(groupMessages);
|
||||
messages.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||
this.messages = messages;
|
||||
this.fetching = false;
|
||||
});
|
||||
});
|
||||
},
|
||||
function onMessage(message) {
|
||||
if (message.recipientId) {
|
||||
messages = messages.filter(m => !(
|
||||
(m.recipientId === message.recipientId && m.userId === message.userId) ||
|
||||
(m.recipientId === message.userId && m.userId === message.recipientId)));
|
||||
|
||||
beforeUnmount() {
|
||||
this.connection.dispose();
|
||||
},
|
||||
|
||||
methods: {
|
||||
getAcct: Acct.toString,
|
||||
|
||||
isMe(message) {
|
||||
return message.userId === this.$i.id;
|
||||
},
|
||||
|
||||
onMessage(message) {
|
||||
if (message.recipientId) {
|
||||
this.messages = this.messages.filter(m => !(
|
||||
(m.recipientId === message.recipientId && m.userId === message.userId) ||
|
||||
(m.recipientId === message.userId && m.userId === message.recipientId)));
|
||||
|
||||
this.messages.unshift(message);
|
||||
} else if (message.groupId) {
|
||||
this.messages = this.messages.filter(m => m.groupId !== message.groupId);
|
||||
this.messages.unshift(message);
|
||||
}
|
||||
},
|
||||
|
||||
onRead(ids) {
|
||||
for (const id of ids) {
|
||||
const found = this.messages.find(m => m.id === id);
|
||||
if (found) {
|
||||
if (found.recipientId) {
|
||||
found.isRead = true;
|
||||
} else if (found.groupId) {
|
||||
found.reads.push(this.$i.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
start(ev) {
|
||||
os.popupMenu([{
|
||||
text: this.$ts.messagingWithUser,
|
||||
icon: 'fas fa-user',
|
||||
action: () => { this.startUser(); }
|
||||
}, {
|
||||
text: this.$ts.messagingWithGroup,
|
||||
icon: 'fas fa-users',
|
||||
action: () => { this.startGroup(); }
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
},
|
||||
|
||||
async startUser() {
|
||||
os.selectUser().then(user => {
|
||||
this.$router.push(`/my/messaging/${Acct.toString(user)}`);
|
||||
});
|
||||
},
|
||||
|
||||
async startGroup() {
|
||||
const groups1 = await os.api('users/groups/owned');
|
||||
const groups2 = await os.api('users/groups/joined');
|
||||
if (groups1.length === 0 && groups2.length === 0) {
|
||||
os.alert({
|
||||
type: 'warning',
|
||||
title: this.$ts.youHaveNoGroups,
|
||||
text: this.$ts.joinOrCreateGroup,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const { canceled, result: group } = await os.select({
|
||||
title: this.$ts.group,
|
||||
items: groups1.concat(groups2).map(group => ({
|
||||
value: group, text: group.name
|
||||
}))
|
||||
});
|
||||
if (canceled) return;
|
||||
this.$router.push(`/my/messaging/group/${group.id}`);
|
||||
},
|
||||
|
||||
acct
|
||||
messages.unshift(message);
|
||||
} else if (message.groupId) {
|
||||
messages = messages.filter(m => m.groupId !== message.groupId);
|
||||
messages.unshift(message);
|
||||
}
|
||||
}
|
||||
|
||||
function onRead(ids) {
|
||||
for (const id of ids) {
|
||||
const found = messages.find(m => m.id === id);
|
||||
if (found) {
|
||||
if (found.recipientId) {
|
||||
found.isRead = true;
|
||||
} else if (found.groupId) {
|
||||
found.reads.push($i.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function start(ev) {
|
||||
os.popupMenu([{
|
||||
text: i18n.ts.messagingWithUser,
|
||||
icon: 'fas fa-user',
|
||||
action: () => { startUser(); },
|
||||
}, {
|
||||
text: i18n.ts.messagingWithGroup,
|
||||
icon: 'fas fa-users',
|
||||
action: () => { startGroup(); },
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
async function startUser() {
|
||||
os.selectUser().then(user => {
|
||||
router.push(`/my/messaging/${Acct.toString(user)}`);
|
||||
});
|
||||
}
|
||||
|
||||
async function startGroup() {
|
||||
const groups1 = await os.api('users/groups/owned');
|
||||
const groups2 = await os.api('users/groups/joined');
|
||||
if (groups1.length === 0 && groups2.length === 0) {
|
||||
os.alert({
|
||||
type: 'warning',
|
||||
title: i18n.ts.youHaveNoGroups,
|
||||
text: i18n.ts.joinOrCreateGroup,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const { canceled, result: group } = await os.select({
|
||||
title: i18n.ts.group,
|
||||
items: groups1.concat(groups2).map(group => ({
|
||||
value: group, text: group.name,
|
||||
})),
|
||||
});
|
||||
if (canceled) return;
|
||||
router.push(`/my/messaging/group/${group.id}`);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
connection = markRaw(stream.useChannel('messagingIndex'));
|
||||
|
||||
connection.on('message', onMessage);
|
||||
connection.on('read', onRead);
|
||||
|
||||
os.api('messaging/history', { group: false }).then(userMessages => {
|
||||
os.api('messaging/history', { group: true }).then(groupMessages => {
|
||||
const _messages = userMessages.concat(groupMessages);
|
||||
_messages.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||
messages = _messages;
|
||||
fetching = false;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (connection) connection.dispose();
|
||||
});
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.messaging,
|
||||
icon: 'fas fa-comments',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -61,10 +61,10 @@ import { isBottomVisible, onScrollBottom, scrollToBottom } from '@/scripts/scrol
|
||||
import * as os from '@/os';
|
||||
import { stream } from '@/stream';
|
||||
import * as sound from '@/scripts/sound';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
import { $i } from '@/account';
|
||||
import { defaultStore } from '@/store';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const props = defineProps<{
|
||||
userAcct?: string;
|
||||
@ -280,15 +280,13 @@ onBeforeUnmount(() => {
|
||||
if (scrollRemove) scrollRemove();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: computed(() => !fetching ? user ? {
|
||||
userName: user,
|
||||
avatar: user,
|
||||
} : {
|
||||
title: group?.name,
|
||||
icon: 'fas fa-users',
|
||||
} : null),
|
||||
});
|
||||
definePageMetadata(computed(() => !fetching ? user ? {
|
||||
userName: user,
|
||||
avatar: user,
|
||||
} : {
|
||||
title: group?.name,
|
||||
icon: 'fas fa-users',
|
||||
} : null));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -1,127 +1,129 @@
|
||||
<template>
|
||||
<div class="mwysmxbg">
|
||||
<div class="_isolated">{{ $ts._mfm.intro }}</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.mention }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.mentionDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_mention"/>
|
||||
<MkTextarea v-model="preview_mention"><template #label>MFM</template></MkTextarea>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader/></template>
|
||||
<div class="mwysmxbg">
|
||||
<div class="_isolated">{{ $ts._mfm.intro }}</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.mention }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.mentionDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_mention"/>
|
||||
<MkTextarea v-model="preview_mention"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.hashtag }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.hashtagDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_hashtag"/>
|
||||
<MkTextarea v-model="preview_hashtag"><template #label>MFM</template></MkTextarea>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.hashtag }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.hashtagDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_hashtag"/>
|
||||
<MkTextarea v-model="preview_hashtag"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.url }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.urlDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_url"/>
|
||||
<MkTextarea v-model="preview_url"><template #label>MFM</template></MkTextarea>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.url }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.urlDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_url"/>
|
||||
<MkTextarea v-model="preview_url"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.link }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.linkDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_link"/>
|
||||
<MkTextarea v-model="preview_link"><template #label>MFM</template></MkTextarea>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.link }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.linkDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_link"/>
|
||||
<MkTextarea v-model="preview_link"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.emoji }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.emojiDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_emoji"/>
|
||||
<MkTextarea v-model="preview_emoji"><template #label>MFM</template></MkTextarea>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.emoji }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.emojiDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_emoji"/>
|
||||
<MkTextarea v-model="preview_emoji"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.bold }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.boldDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_bold"/>
|
||||
<MkTextarea v-model="preview_bold"><template #label>MFM</template></MkTextarea>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.bold }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.boldDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_bold"/>
|
||||
<MkTextarea v-model="preview_bold"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.small }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.smallDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_small"/>
|
||||
<MkTextarea v-model="preview_small"><template #label>MFM</template></MkTextarea>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.small }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.smallDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_small"/>
|
||||
<MkTextarea v-model="preview_small"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.quote }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.quoteDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_quote"/>
|
||||
<MkTextarea v-model="preview_quote"><template #label>MFM</template></MkTextarea>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.quote }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.quoteDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_quote"/>
|
||||
<MkTextarea v-model="preview_quote"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.center }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.centerDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_center"/>
|
||||
<MkTextarea v-model="preview_center"><template #label>MFM</template></MkTextarea>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.center }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.centerDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_center"/>
|
||||
<MkTextarea v-model="preview_center"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.inlineCode }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.inlineCodeDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_inlineCode"/>
|
||||
<MkTextarea v-model="preview_inlineCode"><template #label>MFM</template></MkTextarea>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.inlineCode }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.inlineCodeDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_inlineCode"/>
|
||||
<MkTextarea v-model="preview_inlineCode"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.blockCode }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.blockCodeDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_blockCode"/>
|
||||
<MkTextarea v-model="preview_blockCode"><template #label>MFM</template></MkTextarea>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.blockCode }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.blockCodeDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_blockCode"/>
|
||||
<MkTextarea v-model="preview_blockCode"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.inlineMath }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.inlineMathDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_inlineMath"/>
|
||||
<MkTextarea v-model="preview_inlineMath"><template #label>MFM</template></MkTextarea>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.inlineMath }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.inlineMathDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_inlineMath"/>
|
||||
<MkTextarea v-model="preview_inlineMath"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- deprecated
|
||||
<!-- deprecated
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.search }}</div>
|
||||
<div class="content">
|
||||
@ -133,216 +135,210 @@
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.flip }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.flipDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_flip"/>
|
||||
<MkTextarea v-model="preview_flip"><template #label>MFM</template></MkTextarea>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.flip }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.flipDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_flip"/>
|
||||
<MkTextarea v-model="preview_flip"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.font }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.fontDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_font"/>
|
||||
<MkTextarea v-model="preview_font"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.x2 }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.x2Description }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_x2"/>
|
||||
<MkTextarea v-model="preview_x2"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.x3 }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.x3Description }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_x3"/>
|
||||
<MkTextarea v-model="preview_x3"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.x4 }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.x4Description }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_x4"/>
|
||||
<MkTextarea v-model="preview_x4"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.blur }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.blurDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_blur"/>
|
||||
<MkTextarea v-model="preview_blur"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.jelly }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.jellyDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_jelly"/>
|
||||
<MkTextarea v-model="preview_jelly"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.tada }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.tadaDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_tada"/>
|
||||
<MkTextarea v-model="preview_tada"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.jump }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.jumpDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_jump"/>
|
||||
<MkTextarea v-model="preview_jump"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.bounce }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.bounceDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_bounce"/>
|
||||
<MkTextarea v-model="preview_bounce"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.spin }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.spinDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_spin"/>
|
||||
<MkTextarea v-model="preview_spin"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.shake }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.shakeDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_shake"/>
|
||||
<MkTextarea v-model="preview_shake"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.twitch }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.twitchDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_twitch"/>
|
||||
<MkTextarea v-model="preview_twitch"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.rainbow }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.rainbowDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_rainbow"/>
|
||||
<MkTextarea v-model="preview_rainbow"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.sparkle }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.sparkleDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_sparkle"/>
|
||||
<MkTextarea v-model="preview_sparkle"><span>MFM</span></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.rotate }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.rotateDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_rotate"/>
|
||||
<MkTextarea v-model="preview_rotate"><span>MFM</span></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.font }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.fontDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_font"/>
|
||||
<MkTextarea v-model="preview_font"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.x2 }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.x2Description }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_x2"/>
|
||||
<MkTextarea v-model="preview_x2"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.x3 }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.x3Description }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_x3"/>
|
||||
<MkTextarea v-model="preview_x3"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.x4 }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.x4Description }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_x4"/>
|
||||
<MkTextarea v-model="preview_x4"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.blur }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.blurDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_blur"/>
|
||||
<MkTextarea v-model="preview_blur"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.jelly }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.jellyDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_jelly"/>
|
||||
<MkTextarea v-model="preview_jelly"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.tada }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.tadaDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_tada"/>
|
||||
<MkTextarea v-model="preview_tada"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.jump }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.jumpDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_jump"/>
|
||||
<MkTextarea v-model="preview_jump"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.bounce }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.bounceDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_bounce"/>
|
||||
<MkTextarea v-model="preview_bounce"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.spin }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.spinDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_spin"/>
|
||||
<MkTextarea v-model="preview_spin"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.shake }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.shakeDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_shake"/>
|
||||
<MkTextarea v-model="preview_shake"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.twitch }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.twitchDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_twitch"/>
|
||||
<MkTextarea v-model="preview_twitch"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.rainbow }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.rainbowDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_rainbow"/>
|
||||
<MkTextarea v-model="preview_rainbow"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.sparkle }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.sparkleDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_sparkle"/>
|
||||
<MkTextarea v-model="preview_sparkle"><span>MFM</span></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.rotate }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.rotateDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_rotate"/>
|
||||
<MkTextarea v-model="preview_rotate"><span>MFM</span></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { defineComponent } from 'vue';
|
||||
import MkTextarea from '@/components/form/textarea.vue';
|
||||
import * as symbols from '@/symbols';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { i18n } from '@/i18n';
|
||||
import { instance } from '@/instance';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkTextarea
|
||||
},
|
||||
const preview_mention = '@example';
|
||||
const preview_hashtag = '#test';
|
||||
const preview_url = 'https://example.com';
|
||||
const preview_link = `[${i18n.ts._mfm.dummy}](https://example.com)`;
|
||||
const preview_emoji = instance.emojis.length ? `:${instance.emojis[0].name}:` : ':emojiname:';
|
||||
const preview_bold = `**${i18n.ts._mfm.dummy}**`;
|
||||
const preview_small = `<small>${i18n.ts._mfm.dummy}</small>`;
|
||||
const preview_center = `<center>${i18n.ts._mfm.dummy}</center>`;
|
||||
const preview_inlineCode = '`<: "Hello, world!"`';
|
||||
const preview_blockCode = '```\n~ (#i, 100) {\n\t<: ? ((i % 15) = 0) "FizzBuzz"\n\t\t.? ((i % 3) = 0) "Fizz"\n\t\t.? ((i % 5) = 0) "Buzz"\n\t\t. i\n}\n```';
|
||||
const preview_inlineMath = '\\(x= \\frac{-b\' \\pm \\sqrt{(b\')^2-ac}}{a}\\)';
|
||||
const preview_quote = `> ${i18n.ts._mfm.dummy}`;
|
||||
const preview_search = `${i18n.ts._mfm.dummy} 検索`;
|
||||
const preview_jelly = '$[jelly 🍮] $[jelly.speed=5s 🍮]';
|
||||
const preview_tada = '$[tada 🍮] $[tada.speed=5s 🍮]';
|
||||
const preview_jump = '$[jump 🍮] $[jump.speed=5s 🍮]';
|
||||
const preview_bounce = '$[bounce 🍮] $[bounce.speed=5s 🍮]';
|
||||
const preview_shake = '$[shake 🍮] $[shake.speed=5s 🍮]';
|
||||
const preview_twitch = '$[twitch 🍮] $[twitch.speed=5s 🍮]';
|
||||
const preview_spin = '$[spin 🍮] $[spin.left 🍮] $[spin.alternate 🍮]\n$[spin.x 🍮] $[spin.x,left 🍮] $[spin.x,alternate 🍮]\n$[spin.y 🍮] $[spin.y,left 🍮] $[spin.y,alternate 🍮]\n\n$[spin.speed=5s 🍮]';
|
||||
const preview_flip = `$[flip ${i18n.ts._mfm.dummy}]\n$[flip.v ${i18n.ts._mfm.dummy}]\n$[flip.h,v ${i18n.ts._mfm.dummy}]`;
|
||||
const preview_font = `$[font.serif ${i18n.ts._mfm.dummy}]\n$[font.monospace ${i18n.ts._mfm.dummy}]\n$[font.cursive ${i18n.ts._mfm.dummy}]\n$[font.fantasy ${i18n.ts._mfm.dummy}]`;
|
||||
const preview_x2 = '$[x2 🍮]';
|
||||
const preview_x3 = '$[x3 🍮]';
|
||||
const preview_x4 = '$[x4 🍮]';
|
||||
const preview_blur = `$[blur ${i18n.ts._mfm.dummy}]`;
|
||||
const preview_rainbow = '$[rainbow 🍮] $[rainbow.speed=5s 🍮]';
|
||||
const preview_sparkle = '$[sparkle 🍮]';
|
||||
const preview_rotate = '$[rotate 🍮]';
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts._mfm.cheatSheet,
|
||||
icon: 'fas fa-question-circle',
|
||||
},
|
||||
preview_mention: '@example',
|
||||
preview_hashtag: '#test',
|
||||
preview_url: `https://example.com`,
|
||||
preview_link: `[${this.$ts._mfm.dummy}](https://example.com)`,
|
||||
preview_emoji: this.$instance.emojis.length ? `:${this.$instance.emojis[0].name}:` : `:emojiname:`,
|
||||
preview_bold: `**${this.$ts._mfm.dummy}**`,
|
||||
preview_small: `<small>${this.$ts._mfm.dummy}</small>`,
|
||||
preview_center: `<center>${this.$ts._mfm.dummy}</center>`,
|
||||
preview_inlineCode: '`<: "Hello, world!"`',
|
||||
preview_blockCode: '```\n~ (#i, 100) {\n\t<: ? ((i % 15) = 0) "FizzBuzz"\n\t\t.? ((i % 3) = 0) "Fizz"\n\t\t.? ((i % 5) = 0) "Buzz"\n\t\t. i\n}\n```',
|
||||
preview_inlineMath: '\\(x= \\frac{-b\' \\pm \\sqrt{(b\')^2-ac}}{a}\\)',
|
||||
preview_quote: `> ${this.$ts._mfm.dummy}`,
|
||||
preview_search: `${this.$ts._mfm.dummy} 検索`,
|
||||
preview_jelly: `$[jelly 🍮] $[jelly.speed=5s 🍮]`,
|
||||
preview_tada: `$[tada 🍮] $[tada.speed=5s 🍮]`,
|
||||
preview_jump: `$[jump 🍮] $[jump.speed=5s 🍮]`,
|
||||
preview_bounce: `$[bounce 🍮] $[bounce.speed=5s 🍮]`,
|
||||
preview_shake: `$[shake 🍮] $[shake.speed=5s 🍮]`,
|
||||
preview_twitch: `$[twitch 🍮] $[twitch.speed=5s 🍮]`,
|
||||
preview_spin: `$[spin 🍮] $[spin.left 🍮] $[spin.alternate 🍮]\n$[spin.x 🍮] $[spin.x,left 🍮] $[spin.x,alternate 🍮]\n$[spin.y 🍮] $[spin.y,left 🍮] $[spin.y,alternate 🍮]\n\n$[spin.speed=5s 🍮]`,
|
||||
preview_flip: `$[flip ${this.$ts._mfm.dummy}]\n$[flip.v ${this.$ts._mfm.dummy}]\n$[flip.h,v ${this.$ts._mfm.dummy}]`,
|
||||
preview_font: `$[font.serif ${this.$ts._mfm.dummy}]\n$[font.monospace ${this.$ts._mfm.dummy}]\n$[font.cursive ${this.$ts._mfm.dummy}]\n$[font.fantasy ${this.$ts._mfm.dummy}]`,
|
||||
preview_x2: `$[x2 🍮]`,
|
||||
preview_x3: `$[x3 🍮]`,
|
||||
preview_x4: `$[x4 🍮]`,
|
||||
preview_blur: `$[blur ${this.$ts._mfm.dummy}]`,
|
||||
preview_rainbow: `$[rainbow 🍮] $[rainbow.speed=5s 🍮]`,
|
||||
preview_sparkle: `$[sparkle 🍮]`,
|
||||
preview_rotate: `$[rotate 🍮]`,
|
||||
};
|
||||
},
|
||||
definePageMetadata({
|
||||
title: i18n.ts._mfm.cheatSheet,
|
||||
icon: 'fas fa-question-circle',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -49,28 +49,12 @@ export default defineComponent({
|
||||
MkSignin,
|
||||
MkButton,
|
||||
},
|
||||
props: ['session', 'callback', 'name', 'icon', 'permission'],
|
||||
data() {
|
||||
return {
|
||||
state: null
|
||||
state: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
session(): string {
|
||||
return this.$route.params.session;
|
||||
},
|
||||
callback(): string {
|
||||
return this.$route.query.callback;
|
||||
},
|
||||
name(): string {
|
||||
return this.$route.query.name;
|
||||
},
|
||||
icon(): string {
|
||||
return this.$route.query.icon;
|
||||
},
|
||||
permission(): string[] {
|
||||
return this.$route.query.permission ? this.$route.query.permission.split(',') : [];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async accept() {
|
||||
this.state = 'waiting';
|
||||
@ -84,7 +68,7 @@ export default defineComponent({
|
||||
this.state = 'accepted';
|
||||
if (this.callback) {
|
||||
location.href = appendQuery(this.callback, query({
|
||||
session: this.session
|
||||
session: this.session,
|
||||
}));
|
||||
}
|
||||
},
|
||||
@ -93,8 +77,8 @@ export default defineComponent({
|
||||
},
|
||||
onLogin(res) {
|
||||
login(res.i);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -5,11 +5,13 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import { inject } from 'vue';
|
||||
import XAntenna from './editor.vue';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
import { router } from '@/router';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { useRouter } from '@/router';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
let draft = $ref({
|
||||
name: '',
|
||||
@ -22,19 +24,21 @@ let draft = $ref({
|
||||
withReplies: false,
|
||||
caseSensitive: false,
|
||||
withFile: false,
|
||||
notify: false
|
||||
notify: false,
|
||||
});
|
||||
|
||||
function onAntennaCreated() {
|
||||
router.push('/my/antennas');
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.manageAntennas,
|
||||
icon: 'fas fa-satellite',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.manageAntennas,
|
||||
icon: 'fas fa-satellite',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -5,14 +5,14 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch } from 'vue';
|
||||
import { inject, watch } from 'vue';
|
||||
import XAntenna from './editor.vue';
|
||||
import * as symbols from '@/symbols';
|
||||
import * as os from '@/os';
|
||||
import { MisskeyNavigator } from '@/scripts/navigate';
|
||||
import { i18n } from '@/i18n';
|
||||
import { useRouter } from '@/router';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const nav = new MisskeyNavigator();
|
||||
const router = useRouter();
|
||||
|
||||
let antenna: any = $ref(null);
|
||||
|
||||
@ -21,18 +21,20 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
function onAntennaUpdated() {
|
||||
nav.push('/my/antennas');
|
||||
router.push('/my/antennas');
|
||||
}
|
||||
|
||||
os.api('antennas/show', { antennaId: props.antennaId }).then((antennaResponse) => {
|
||||
antenna = antennaResponse;
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.manageAntennas,
|
||||
icon: 'fas fa-satellite',
|
||||
}
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.manageAntennas,
|
||||
icon: 'fas fa-satellite',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="700">
|
||||
<template><MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700">
|
||||
<div class="ieepwinx">
|
||||
<MkButton :link="true" to="/my/antennas/create" primary class="add"><i class="fas fa-plus"></i> {{ i18n.ts.add }}</MkButton>
|
||||
|
||||
@ -11,27 +12,29 @@
|
||||
</MkPagination>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkSpacer></MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'antennas/list' as const,
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.manageAntennas,
|
||||
icon: 'fas fa-satellite',
|
||||
bg: 'var(--bg)'
|
||||
}
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.manageAntennas,
|
||||
icon: 'fas fa-satellite',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="700">
|
||||
<template><MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700">
|
||||
<div class="qtcaoidl">
|
||||
<MkButton primary class="add" @click="create"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
|
||||
|
||||
@ -10,7 +11,7 @@
|
||||
</MkA>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkSpacer></MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@ -18,8 +19,8 @@ import { } from 'vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'clips/list' as const,
|
||||
@ -61,15 +62,17 @@ function onClipDeleted() {
|
||||
pagingComponent.reload();
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.clip,
|
||||
icon: 'fas fa-paperclip',
|
||||
bg: 'var(--bg)',
|
||||
action: {
|
||||
icon: 'fas fa-plus',
|
||||
handler: create
|
||||
},
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.clip,
|
||||
icon: 'fas fa-paperclip',
|
||||
bg: 'var(--bg)',
|
||||
action: {
|
||||
icon: 'fas fa-plus',
|
||||
handler: create,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
@ -1,178 +0,0 @@
|
||||
<template>
|
||||
<div class="mk-group-page">
|
||||
<transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
|
||||
<div v-if="group" class="_section">
|
||||
<div class="_content" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
|
||||
<MkButton inline @click="invite()">{{ $ts.invite }}</MkButton>
|
||||
<MkButton inline @click="renameGroup()">{{ $ts.rename }}</MkButton>
|
||||
<MkButton inline @click="transfer()">{{ $ts.transfer }}</MkButton>
|
||||
<MkButton inline @click="deleteGroup()">{{ $ts.delete }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
|
||||
<div v-if="group" class="_section members _gap">
|
||||
<div class="_title">{{ $ts.members }}</div>
|
||||
<div class="_content">
|
||||
<div class="users">
|
||||
<div v-for="user in users" :key="user.id" class="user _panel">
|
||||
<MkAvatar :user="user" class="avatar" :show-indicator="true"/>
|
||||
<div class="body">
|
||||
<MkUserName :user="user" class="name"/>
|
||||
<MkAcct :user="user" class="acct"/>
|
||||
</div>
|
||||
<div class="action">
|
||||
<button class="_button" @click="removeUser(user)"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton
|
||||
},
|
||||
|
||||
props: {
|
||||
groupId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: computed(() => this.group ? {
|
||||
title: this.group.name,
|
||||
icon: 'fas fa-users',
|
||||
} : null),
|
||||
group: null,
|
||||
users: [],
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
groupId: 'fetch',
|
||||
},
|
||||
|
||||
created() {
|
||||
this.fetch();
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetch() {
|
||||
os.api('users/groups/show', {
|
||||
groupId: this.groupId
|
||||
}).then(group => {
|
||||
this.group = group;
|
||||
os.api('users/show', {
|
||||
userIds: this.group.userIds
|
||||
}).then(users => {
|
||||
this.users = users;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
invite() {
|
||||
os.selectUser().then(user => {
|
||||
os.apiWithDialog('users/groups/invite', {
|
||||
groupId: this.group.id,
|
||||
userId: user.id
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
removeUser(user) {
|
||||
os.api('users/groups/pull', {
|
||||
groupId: this.group.id,
|
||||
userId: user.id
|
||||
}).then(() => {
|
||||
this.users = this.users.filter(x => x.id !== user.id);
|
||||
});
|
||||
},
|
||||
|
||||
async renameGroup() {
|
||||
const { canceled, result: name } = await os.inputText({
|
||||
title: this.$ts.groupName,
|
||||
default: this.group.name
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
await os.api('users/groups/update', {
|
||||
groupId: this.group.id,
|
||||
name: name
|
||||
});
|
||||
|
||||
this.group.name = name;
|
||||
},
|
||||
|
||||
transfer() {
|
||||
os.selectUser().then(user => {
|
||||
os.apiWithDialog('users/groups/transfer', {
|
||||
groupId: this.group.id,
|
||||
userId: user.id
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
async deleteGroup() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: this.$t('removeAreYouSure', { x: this.group.name }),
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
await os.apiWithDialog('users/groups/delete', {
|
||||
groupId: this.group.id
|
||||
});
|
||||
this.$router.push('/my/groups');
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mk-group-page {
|
||||
> .members {
|
||||
> ._content {
|
||||
> .users {
|
||||
> .user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
|
||||
> .avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
> .body {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
|
||||
> .name {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
> .acct {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,147 +0,0 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="700">
|
||||
<div v-if="tab === 'owned'" class="_content">
|
||||
<MkButton primary style="margin: 0 auto var(--margin) auto;" @click="create"><i class="fas fa-plus"></i> {{ $ts.createGroup }}</MkButton>
|
||||
|
||||
<MkPagination v-slot="{items}" ref="owned" :pagination="ownedPagination">
|
||||
<div v-for="group in items" :key="group.id" class="_card">
|
||||
<div class="_title"><MkA :to="`/my/groups/${ group.id }`" class="_link">{{ group.name }}</MkA></div>
|
||||
<div class="_content"><MkAvatars :user-ids="group.userIds"/></div>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tab === 'joined'" class="_content">
|
||||
<MkPagination v-slot="{items}" ref="joined" :pagination="joinedPagination">
|
||||
<div v-for="group in items" :key="group.id" class="_card">
|
||||
<div class="_title">{{ group.name }}</div>
|
||||
<div class="_content"><MkAvatars :user-ids="group.userIds"/></div>
|
||||
<div class="_footer">
|
||||
<MkButton danger @click="leave(group)">{{ $ts.leaveGroup }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tab === 'invites'" class="_content">
|
||||
<MkPagination v-slot="{items}" ref="invitations" :pagination="invitationPagination">
|
||||
<div v-for="invitation in items" :key="invitation.id" class="_card">
|
||||
<div class="_title">{{ invitation.group.name }}</div>
|
||||
<div class="_content"><MkAvatars :user-ids="invitation.group.userIds"/></div>
|
||||
<div class="_footer">
|
||||
<MkButton primary inline @click="acceptInvite(invitation)"><i class="fas fa-check"></i> {{ $ts.accept }}</MkButton>
|
||||
<MkButton primary inline @click="rejectInvite(invitation)"><i class="fas fa-ban"></i> {{ $ts.reject }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from 'vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkContainer from '@/components/ui/container.vue';
|
||||
import MkAvatars from '@/components/avatars.vue';
|
||||
import MkTab from '@/components/tab.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkPagination,
|
||||
MkButton,
|
||||
MkContainer,
|
||||
MkTab,
|
||||
MkAvatars,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: computed(() => ({
|
||||
title: this.$ts.groups,
|
||||
icon: 'fas fa-users',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
icon: 'fas fa-plus',
|
||||
text: this.$ts.createGroup,
|
||||
handler: this.create,
|
||||
}],
|
||||
tabs: [{
|
||||
active: this.tab === 'owned',
|
||||
title: this.$ts.ownedGroups,
|
||||
icon: 'fas fa-user-tie',
|
||||
onClick: () => { this.tab = 'owned'; },
|
||||
}, {
|
||||
active: this.tab === 'joined',
|
||||
title: this.$ts.joinedGroups,
|
||||
icon: 'fas fa-id-badge',
|
||||
onClick: () => { this.tab = 'joined'; },
|
||||
}, {
|
||||
active: this.tab === 'invites',
|
||||
title: this.$ts.invites,
|
||||
icon: 'fas fa-envelope-open-text',
|
||||
onClick: () => { this.tab = 'invites'; },
|
||||
},]
|
||||
})),
|
||||
tab: 'owned',
|
||||
ownedPagination: {
|
||||
endpoint: 'users/groups/owned' as const,
|
||||
limit: 10,
|
||||
},
|
||||
joinedPagination: {
|
||||
endpoint: 'users/groups/joined' as const,
|
||||
limit: 10,
|
||||
},
|
||||
invitationPagination: {
|
||||
endpoint: 'i/user-group-invites' as const,
|
||||
limit: 10,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
async create() {
|
||||
const { canceled, result: name } = await os.inputText({
|
||||
title: this.$ts.groupName,
|
||||
});
|
||||
if (canceled) return;
|
||||
await os.api('users/groups/create', { name: name });
|
||||
this.$refs.owned.reload();
|
||||
os.success();
|
||||
},
|
||||
acceptInvite(invitation) {
|
||||
os.api('users/groups/invitations/accept', {
|
||||
invitationId: invitation.id
|
||||
}).then(() => {
|
||||
os.success();
|
||||
this.$refs.invitations.reload();
|
||||
this.$refs.joined.reload();
|
||||
});
|
||||
},
|
||||
rejectInvite(invitation) {
|
||||
os.api('users/groups/invitations/reject', {
|
||||
invitationId: invitation.id
|
||||
}).then(() => {
|
||||
this.$refs.invitations.reload();
|
||||
});
|
||||
},
|
||||
async leave(group) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: this.$t('leaveGroupConfirm', { name: group.name }),
|
||||
});
|
||||
if (canceled) return;
|
||||
os.apiWithDialog('users/groups/leave', {
|
||||
groupId: group.id,
|
||||
}).then(() => {
|
||||
this.$refs.joined.reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="700">
|
||||
<template><MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700">
|
||||
<div class="qkcjvfiv">
|
||||
<MkButton primary class="add" @click="create"><i class="fas fa-plus"></i> {{ $ts.createList }}</MkButton>
|
||||
|
||||
@ -10,7 +11,7 @@
|
||||
</MkA>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkSpacer></MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@ -19,8 +20,8 @@ import MkPagination from '@/components/ui/pagination.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkAvatars from '@/components/avatars.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const pagingComponent = $ref<InstanceType<typeof MkPagination>>();
|
||||
|
||||
@ -38,15 +39,17 @@ async function create() {
|
||||
pagingComponent.reload();
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.manageLists,
|
||||
icon: 'fas fa-list-ul',
|
||||
bg: 'var(--bg)',
|
||||
action: {
|
||||
icon: 'fas fa-plus',
|
||||
handler: create,
|
||||
},
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.manageLists,
|
||||
icon: 'fas fa-list-ul',
|
||||
bg: 'var(--bg)',
|
||||
action: {
|
||||
icon: 'fas fa-plus',
|
||||
handler: create,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="700">
|
||||
<template><MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700">
|
||||
<div class="mk-list-page">
|
||||
<transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
|
||||
<div v-if="list" class="_section">
|
||||
@ -31,104 +32,96 @@
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkSpacer></MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineComponent, watch } from 'vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { mainRouter } from '@/router';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton
|
||||
},
|
||||
const props = defineProps<{
|
||||
listId: string;
|
||||
}>();
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: computed(() => this.list ? {
|
||||
title: this.list.name,
|
||||
icon: 'fas fa-list-ul',
|
||||
bg: 'var(--bg)',
|
||||
} : null),
|
||||
list: null,
|
||||
users: [],
|
||||
};
|
||||
},
|
||||
let list = $ref(null);
|
||||
let users = $ref([]);
|
||||
|
||||
watch: {
|
||||
$route: 'fetch'
|
||||
},
|
||||
function fetchList() {
|
||||
os.api('users/lists/show', {
|
||||
listId: props.listId,
|
||||
}).then(_list => {
|
||||
list = _list;
|
||||
os.api('users/show', {
|
||||
userIds: list.userIds,
|
||||
}).then(_users => {
|
||||
users = _users;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
created() {
|
||||
this.fetch();
|
||||
},
|
||||
function addUser() {
|
||||
os.selectUser().then(user => {
|
||||
os.apiWithDialog('users/lists/push', {
|
||||
listId: list.id,
|
||||
userId: user.id,
|
||||
}).then(() => {
|
||||
users.push(user);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
methods: {
|
||||
fetch() {
|
||||
os.api('users/lists/show', {
|
||||
listId: this.$route.params.list
|
||||
}).then(list => {
|
||||
this.list = list;
|
||||
os.api('users/show', {
|
||||
userIds: this.list.userIds
|
||||
}).then(users => {
|
||||
this.users = users;
|
||||
});
|
||||
});
|
||||
},
|
||||
function removeUser(user) {
|
||||
os.api('users/lists/pull', {
|
||||
listId: list.id,
|
||||
userId: user.id,
|
||||
}).then(() => {
|
||||
users = users.filter(x => x.id !== user.id);
|
||||
});
|
||||
}
|
||||
|
||||
addUser() {
|
||||
os.selectUser().then(user => {
|
||||
os.apiWithDialog('users/lists/push', {
|
||||
listId: this.list.id,
|
||||
userId: user.id
|
||||
}).then(() => {
|
||||
this.users.push(user);
|
||||
});
|
||||
});
|
||||
},
|
||||
async function renameList() {
|
||||
const { canceled, result: name } = await os.inputText({
|
||||
title: i18n.ts.enterListName,
|
||||
default: list.name,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
removeUser(user) {
|
||||
os.api('users/lists/pull', {
|
||||
listId: this.list.id,
|
||||
userId: user.id
|
||||
}).then(() => {
|
||||
this.users = this.users.filter(x => x.id !== user.id);
|
||||
});
|
||||
},
|
||||
await os.api('users/lists/update', {
|
||||
listId: list.id,
|
||||
name: name,
|
||||
});
|
||||
|
||||
async renameList() {
|
||||
const { canceled, result: name } = await os.inputText({
|
||||
title: this.$ts.enterListName,
|
||||
default: this.list.name
|
||||
});
|
||||
if (canceled) return;
|
||||
list.name = name;
|
||||
}
|
||||
|
||||
await os.api('users/lists/update', {
|
||||
listId: this.list.id,
|
||||
name: name
|
||||
});
|
||||
async function deleteList() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('removeAreYouSure', { x: list.name }),
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
this.list.name = name;
|
||||
},
|
||||
await os.api('users/lists/delete', {
|
||||
listId: list.id,
|
||||
});
|
||||
os.success();
|
||||
mainRouter.push('/my/lists');
|
||||
}
|
||||
|
||||
async deleteList() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: this.$t('removeAreYouSure', { x: this.list.name }),
|
||||
});
|
||||
if (canceled) return;
|
||||
watch(() => props.listId, fetchList, { immediate: true });
|
||||
|
||||
await os.api('users/lists/delete', {
|
||||
listId: this.list.id
|
||||
});
|
||||
os.success();
|
||||
this.$router.push('/my/lists');
|
||||
}
|
||||
}
|
||||
});
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => list ? {
|
||||
title: list.name,
|
||||
icon: 'fas fa-list-ul',
|
||||
bg: 'var(--bg)',
|
||||
} : null));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -8,14 +8,16 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.notFound,
|
||||
icon: 'fas fa-exclamation-triangle',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.notFound,
|
||||
icon: 'fas fa-exclamation-triangle',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
@ -1,10 +1,11 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="800">
|
||||
<template><MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="800">
|
||||
<div class="fcuexfpr">
|
||||
<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
|
||||
<div v-if="note" class="note">
|
||||
<div v-if="showNext" class="_gap">
|
||||
<XNotes class="_content" :pagination="next" :no-gap="true"/>
|
||||
<XNotes class="_content" :pagination="nextPagination" :no-gap="true"/>
|
||||
</div>
|
||||
|
||||
<div class="main _gap">
|
||||
@ -27,121 +28,112 @@
|
||||
</div>
|
||||
|
||||
<div v-if="showPrev" class="_gap">
|
||||
<XNotes class="_content" :pagination="prev" :no-gap="true"/>
|
||||
<XNotes class="_content" :pagination="prevPagination" :no-gap="true"/>
|
||||
</div>
|
||||
</div>
|
||||
<MkError v-else-if="error" @retry="fetch()"/>
|
||||
<MkLoading v-else/>
|
||||
</transition>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkSpacer></MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineComponent, watch } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import XNote from '@/components/note.vue';
|
||||
import XNoteDetailed from '@/components/note-detailed.vue';
|
||||
import XNotes from '@/components/notes.vue';
|
||||
import MkRemoteCaution from '@/components/remote-caution.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XNote,
|
||||
XNoteDetailed,
|
||||
XNotes,
|
||||
MkRemoteCaution,
|
||||
MkButton,
|
||||
},
|
||||
props: {
|
||||
noteId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: computed(() => this.note ? {
|
||||
title: this.$ts.note,
|
||||
subtitle: new Date(this.note.createdAt).toLocaleString(),
|
||||
avatar: this.note.user,
|
||||
path: `/notes/${this.note.id}`,
|
||||
share: {
|
||||
title: this.$t('noteOf', { user: this.note.user.name }),
|
||||
text: this.note.text,
|
||||
},
|
||||
bg: 'var(--bg)',
|
||||
} : null),
|
||||
note: null,
|
||||
clips: null,
|
||||
hasPrev: false,
|
||||
hasNext: false,
|
||||
showPrev: false,
|
||||
showNext: false,
|
||||
error: null,
|
||||
prev: {
|
||||
endpoint: 'users/notes' as const,
|
||||
limit: 10,
|
||||
params: computed(() => ({
|
||||
userId: this.note.userId,
|
||||
untilId: this.note.id,
|
||||
})),
|
||||
},
|
||||
next: {
|
||||
reversed: true,
|
||||
endpoint: 'users/notes' as const,
|
||||
limit: 10,
|
||||
params: computed(() => ({
|
||||
userId: this.note.userId,
|
||||
sinceId: this.note.id,
|
||||
})),
|
||||
},
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
noteId: 'fetch'
|
||||
},
|
||||
created() {
|
||||
this.fetch();
|
||||
},
|
||||
methods: {
|
||||
fetch() {
|
||||
this.hasPrev = false;
|
||||
this.hasNext = false;
|
||||
this.showPrev = false;
|
||||
this.showNext = false;
|
||||
this.note = null;
|
||||
os.api('notes/show', {
|
||||
noteId: this.noteId
|
||||
}).then(note => {
|
||||
this.note = note;
|
||||
Promise.all([
|
||||
os.api('notes/clips', {
|
||||
noteId: note.id,
|
||||
}),
|
||||
os.api('users/notes', {
|
||||
userId: note.userId,
|
||||
untilId: note.id,
|
||||
limit: 1,
|
||||
}),
|
||||
os.api('users/notes', {
|
||||
userId: note.userId,
|
||||
sinceId: note.id,
|
||||
limit: 1,
|
||||
}),
|
||||
]).then(([clips, prev, next]) => {
|
||||
this.clips = clips;
|
||||
this.hasPrev = prev.length !== 0;
|
||||
this.hasNext = next.length !== 0;
|
||||
});
|
||||
}).catch(err => {
|
||||
this.error = err;
|
||||
});
|
||||
}
|
||||
}
|
||||
const props = defineProps<{
|
||||
noteId: string;
|
||||
}>();
|
||||
|
||||
let note = $ref<null | misskey.entities.Note>();
|
||||
let clips = $ref();
|
||||
let hasPrev = $ref(false);
|
||||
let hasNext = $ref(false);
|
||||
let showPrev = $ref(false);
|
||||
let showNext = $ref(false);
|
||||
let error = $ref();
|
||||
|
||||
const prevPagination = {
|
||||
endpoint: 'users/notes' as const,
|
||||
limit: 10,
|
||||
params: computed(() => note ? ({
|
||||
userId: note.userId,
|
||||
untilId: note.id,
|
||||
}) : null),
|
||||
};
|
||||
|
||||
const nextPagination = {
|
||||
reversed: true,
|
||||
endpoint: 'users/notes' as const,
|
||||
limit: 10,
|
||||
params: computed(() => note ? ({
|
||||
userId: note.userId,
|
||||
sinceId: note.id,
|
||||
}) : null),
|
||||
};
|
||||
|
||||
function fetchNote() {
|
||||
hasPrev = false;
|
||||
hasNext = false;
|
||||
showPrev = false;
|
||||
showNext = false;
|
||||
note = null;
|
||||
os.api('notes/show', {
|
||||
noteId: props.noteId,
|
||||
}).then(res => {
|
||||
note = res;
|
||||
Promise.all([
|
||||
os.api('notes/clips', {
|
||||
noteId: note.id,
|
||||
}),
|
||||
os.api('users/notes', {
|
||||
userId: note.userId,
|
||||
untilId: note.id,
|
||||
limit: 1,
|
||||
}),
|
||||
os.api('users/notes', {
|
||||
userId: note.userId,
|
||||
sinceId: note.id,
|
||||
limit: 1,
|
||||
}),
|
||||
]).then(([_clips, prev, next]) => {
|
||||
clips = _clips;
|
||||
hasPrev = prev.length !== 0;
|
||||
hasNext = next.length !== 0;
|
||||
});
|
||||
}).catch(err => {
|
||||
error = err;
|
||||
});
|
||||
}
|
||||
|
||||
watch(() => props.noteId, fetchNote, {
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => note ? {
|
||||
title: i18n.ts.note,
|
||||
subtitle: new Date(note.createdAt).toLocaleString(),
|
||||
avatar: note.user,
|
||||
path: `/notes/${note.id}`,
|
||||
share: {
|
||||
title: i18n.t('noteOf', { user: note.user.name }),
|
||||
text: note.text,
|
||||
},
|
||||
bg: 'var(--bg)',
|
||||
} : null));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -1,18 +1,21 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="800">
|
||||
<div class="clupoqwt">
|
||||
<XNotifications class="notifications" :include-types="includeTypes" :unread-only="tab === 'unread'"/>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="800">
|
||||
<div class="clupoqwt">
|
||||
<XNotifications class="notifications" :include-types="includeTypes" :unread-only="tab === 'unread'"/>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { notificationTypes } from 'misskey-js';
|
||||
import XNotifications from '@/components/notifications.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { notificationTypes } from 'misskey-js';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
let tab = $ref('all');
|
||||
let includeTypes = $ref<string[] | null>(null);
|
||||
@ -23,46 +26,46 @@ function setFilter(ev) {
|
||||
active: includeTypes && includeTypes.includes(t),
|
||||
action: () => {
|
||||
includeTypes = [t];
|
||||
}
|
||||
},
|
||||
}));
|
||||
const items = includeTypes != null ? [{
|
||||
icon: 'fas fa-times',
|
||||
text: i18n.ts.clear,
|
||||
action: () => {
|
||||
includeTypes = null;
|
||||
}
|
||||
},
|
||||
}, null, ...typeItems] : typeItems;
|
||||
os.popupMenu(items, ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: computed(() => ({
|
||||
title: i18n.ts.notifications,
|
||||
icon: 'fas fa-bell',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
text: i18n.ts.filter,
|
||||
icon: 'fas fa-filter',
|
||||
highlighted: includeTypes != null,
|
||||
handler: setFilter,
|
||||
}, {
|
||||
text: i18n.ts.markAllAsRead,
|
||||
icon: 'fas fa-check',
|
||||
handler: () => {
|
||||
os.apiWithDialog('notifications/mark-all-as-read');
|
||||
},
|
||||
}],
|
||||
tabs: [{
|
||||
active: tab === 'all',
|
||||
title: i18n.ts.all,
|
||||
onClick: () => { tab = 'all'; },
|
||||
}, {
|
||||
active: tab === 'unread',
|
||||
title: i18n.ts.unread,
|
||||
onClick: () => { tab = 'unread'; },
|
||||
},]
|
||||
})),
|
||||
});
|
||||
const headerActions = $computed(() => [{
|
||||
text: i18n.ts.filter,
|
||||
icon: 'fas fa-filter',
|
||||
highlighted: includeTypes != null,
|
||||
handler: setFilter,
|
||||
}, {
|
||||
text: i18n.ts.markAllAsRead,
|
||||
icon: 'fas fa-check',
|
||||
handler: () => {
|
||||
os.apiWithDialog('notifications/mark-all-as-read');
|
||||
},
|
||||
}]);
|
||||
|
||||
const headerTabs = $computed(() => [{
|
||||
active: tab === 'all',
|
||||
title: i18n.ts.all,
|
||||
onClick: () => { tab = 'all'; },
|
||||
}, {
|
||||
active: tab === 'unread',
|
||||
title: i18n.ts.unread,
|
||||
onClick: () => { tab = 'unread'; },
|
||||
}]);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
title: i18n.ts.notifications,
|
||||
icon: 'fas fa-bell',
|
||||
bg: 'var(--bg)',
|
||||
})));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="700">
|
||||
<template><MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700">
|
||||
<div class="jqqmcavi">
|
||||
<MkButton v-if="pageId" class="button" inline link :to="`/@${ author.username }/pages/${ currentName }`"><i class="fas fa-external-link-square-alt"></i> {{ $ts._pages.viewPage }}</MkButton>
|
||||
<MkButton v-if="!readonly" inline primary class="button" @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
|
||||
@ -55,7 +56,7 @@
|
||||
<XDraggable v-show="variables.length > 0" v-model="variables" tag="div" class="variables" item-key="name" handle=".drag-handle" :group="{ name: 'variables' }" animation="150" swap-threshold="0.5">
|
||||
<template #item="{element}">
|
||||
<XVariable
|
||||
:modelValue="element"
|
||||
:model-value="element"
|
||||
:removable="true"
|
||||
:hpml="hpml"
|
||||
:name="element.name"
|
||||
@ -75,11 +76,11 @@
|
||||
<MkTextarea v-model="script" class="_code"/>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkSpacer></MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, defineAsyncComponent, computed } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { defineComponent, defineAsyncComponent, computed, provide, watch } from 'vue';
|
||||
import 'prismjs';
|
||||
import { highlight, languages } from 'prismjs/components/prism-core';
|
||||
import 'prismjs/components/prism-clike';
|
||||
@ -101,367 +102,349 @@ import { url } from '@/config';
|
||||
import { collectPageVars } from '@/scripts/collect-page-vars';
|
||||
import * as os from '@/os';
|
||||
import { selectFile } from '@/scripts/select-file';
|
||||
import * as symbols from '@/symbols';
|
||||
import { mainRouter } from '@/router';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { $i } from '@/account';
|
||||
const XDraggable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XDraggable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)),
|
||||
XVariable, XBlocks, MkTextarea, MkContainer, MkButton, MkSelect, MkSwitch, MkInput,
|
||||
},
|
||||
const props = defineProps<{
|
||||
initPageId?: string;
|
||||
initPageName?: string;
|
||||
initUser?: string;
|
||||
}>();
|
||||
|
||||
provide() {
|
||||
return {
|
||||
readonly: this.readonly,
|
||||
getScriptBlockList: this.getScriptBlockList,
|
||||
getPageBlockList: this.getPageBlockList
|
||||
};
|
||||
},
|
||||
let tab = $ref('settings');
|
||||
let author = $ref($i);
|
||||
let readonly = $ref(false);
|
||||
let page = $ref(null);
|
||||
let pageId = $ref(null);
|
||||
let currentName = $ref(null);
|
||||
let title = $ref('');
|
||||
let summary = $ref(null);
|
||||
let name = $ref(Date.now().toString());
|
||||
let eyeCatchingImage = $ref(null);
|
||||
let eyeCatchingImageId = $ref(null);
|
||||
let font = $ref('sans-serif');
|
||||
let content = $ref([]);
|
||||
let alignCenter = $ref(false);
|
||||
let hideTitleWhenPinned = $ref(false);
|
||||
let variables = $ref([]);
|
||||
let hpml = $ref(null);
|
||||
let script = $ref('');
|
||||
|
||||
props: {
|
||||
initPageId: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
initPageName: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
initUser: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
},
|
||||
provide('readonly', readonly);
|
||||
provide('getScriptBlockList', getScriptBlockList);
|
||||
provide('getPageBlockList', getPageBlockList);
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: computed(() => {
|
||||
let title = this.$ts._pages.newPage;
|
||||
if (this.initPageId) {
|
||||
title = this.$ts._pages.editPage;
|
||||
}
|
||||
else if (this.initPageName && this.initUser) {
|
||||
title = this.$ts._pages.readPage;
|
||||
}
|
||||
return {
|
||||
title: title,
|
||||
icon: 'fas fa-pencil-alt',
|
||||
bg: 'var(--bg)',
|
||||
tabs: [{
|
||||
active: this.tab === 'settings',
|
||||
title: this.$ts._pages.pageSetting,
|
||||
icon: 'fas fa-cog',
|
||||
onClick: () => { this.tab = 'settings'; },
|
||||
}, {
|
||||
active: this.tab === 'contents',
|
||||
title: this.$ts._pages.contents,
|
||||
icon: 'fas fa-sticky-note',
|
||||
onClick: () => { this.tab = 'contents'; },
|
||||
}, {
|
||||
active: this.tab === 'variables',
|
||||
title: this.$ts._pages.variables,
|
||||
icon: 'fas fa-magic',
|
||||
onClick: () => { this.tab = 'variables'; },
|
||||
}, {
|
||||
active: this.tab === 'script',
|
||||
title: this.$ts.script,
|
||||
icon: 'fas fa-code',
|
||||
onClick: () => { this.tab = 'script'; },
|
||||
}],
|
||||
};
|
||||
}),
|
||||
tab: 'settings',
|
||||
author: this.$i,
|
||||
readonly: false,
|
||||
page: null,
|
||||
pageId: null,
|
||||
currentName: null,
|
||||
title: '',
|
||||
summary: null,
|
||||
name: Date.now().toString(),
|
||||
eyeCatchingImage: null,
|
||||
eyeCatchingImageId: null,
|
||||
font: 'sans-serif',
|
||||
content: [],
|
||||
alignCenter: false,
|
||||
hideTitleWhenPinned: false,
|
||||
variables: [],
|
||||
hpml: null,
|
||||
script: '',
|
||||
url,
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
async eyeCatchingImageId() {
|
||||
if (this.eyeCatchingImageId == null) {
|
||||
this.eyeCatchingImage = null;
|
||||
} else {
|
||||
this.eyeCatchingImage = await os.api('drive/files/show', {
|
||||
fileId: this.eyeCatchingImageId,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
async created() {
|
||||
this.hpml = new HpmlTypeChecker();
|
||||
|
||||
this.$watch('variables', () => {
|
||||
this.hpml.variables = this.variables;
|
||||
}, { deep: true });
|
||||
|
||||
this.$watch('content', () => {
|
||||
this.hpml.pageVars = collectPageVars(this.content);
|
||||
}, { deep: true });
|
||||
|
||||
if (this.initPageId) {
|
||||
this.page = await os.api('pages/show', {
|
||||
pageId: this.initPageId,
|
||||
});
|
||||
} else if (this.initPageName && this.initUser) {
|
||||
this.page = await os.api('pages/show', {
|
||||
name: this.initPageName,
|
||||
username: this.initUser,
|
||||
});
|
||||
this.readonly = true;
|
||||
}
|
||||
|
||||
if (this.page) {
|
||||
this.author = this.page.user;
|
||||
this.pageId = this.page.id;
|
||||
this.title = this.page.title;
|
||||
this.name = this.page.name;
|
||||
this.currentName = this.page.name;
|
||||
this.summary = this.page.summary;
|
||||
this.font = this.page.font;
|
||||
this.script = this.page.script;
|
||||
this.hideTitleWhenPinned = this.page.hideTitleWhenPinned;
|
||||
this.alignCenter = this.page.alignCenter;
|
||||
this.content = this.page.content;
|
||||
this.variables = this.page.variables;
|
||||
this.eyeCatchingImageId = this.page.eyeCatchingImageId;
|
||||
} else {
|
||||
const id = uuid();
|
||||
this.content = [{
|
||||
id,
|
||||
type: 'text',
|
||||
text: 'Hello World!'
|
||||
}];
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
getSaveOptions() {
|
||||
return {
|
||||
title: this.title.trim(),
|
||||
name: this.name.trim(),
|
||||
summary: this.summary,
|
||||
font: this.font,
|
||||
script: this.script,
|
||||
hideTitleWhenPinned: this.hideTitleWhenPinned,
|
||||
alignCenter: this.alignCenter,
|
||||
content: this.content,
|
||||
variables: this.variables,
|
||||
eyeCatchingImageId: this.eyeCatchingImageId,
|
||||
};
|
||||
},
|
||||
|
||||
save() {
|
||||
const options = this.getSaveOptions();
|
||||
|
||||
const onError = err => {
|
||||
if (err.id == '3d81ceae-475f-4600-b2a8-2bc116157532') {
|
||||
if (err.info.param == 'name') {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title: this.$ts._pages.invalidNameTitle,
|
||||
text: this.$ts._pages.invalidNameText
|
||||
});
|
||||
}
|
||||
} else if (err.code == 'NAME_ALREADY_EXISTS') {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: this.$ts._pages.nameAlreadyExists
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (this.pageId) {
|
||||
options.pageId = this.pageId;
|
||||
os.api('pages/update', options)
|
||||
.then(page => {
|
||||
this.currentName = this.name.trim();
|
||||
os.alert({
|
||||
type: 'success',
|
||||
text: this.$ts._pages.updated
|
||||
});
|
||||
}).catch(onError);
|
||||
} else {
|
||||
os.api('pages/create', options)
|
||||
.then(page => {
|
||||
this.pageId = page.id;
|
||||
this.currentName = this.name.trim();
|
||||
os.alert({
|
||||
type: 'success',
|
||||
text: this.$ts._pages.created
|
||||
});
|
||||
this.$router.push(`/pages/edit/${this.pageId}`);
|
||||
}).catch(onError);
|
||||
}
|
||||
},
|
||||
|
||||
del() {
|
||||
os.confirm({
|
||||
type: 'warning',
|
||||
text: this.$t('removeAreYouSure', { x: this.title.trim() }),
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
os.api('pages/delete', {
|
||||
pageId: this.pageId,
|
||||
}).then(() => {
|
||||
os.alert({
|
||||
type: 'success',
|
||||
text: this.$ts._pages.deleted
|
||||
});
|
||||
this.$router.push(`/pages`);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
duplicate() {
|
||||
this.title = this.title + ' - copy';
|
||||
this.name = this.name + '-copy';
|
||||
os.api('pages/create', this.getSaveOptions()).then(page => {
|
||||
this.pageId = page.id;
|
||||
this.currentName = this.name.trim();
|
||||
os.alert({
|
||||
type: 'success',
|
||||
text: this.$ts._pages.created
|
||||
});
|
||||
this.$router.push(`/pages/edit/${this.pageId}`);
|
||||
});
|
||||
},
|
||||
|
||||
async add() {
|
||||
const { canceled, result: type } = await os.select({
|
||||
type: null,
|
||||
title: this.$ts._pages.chooseBlock,
|
||||
groupedItems: this.getPageBlockList()
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
const id = uuid();
|
||||
this.content.push({ id, type });
|
||||
},
|
||||
|
||||
async addVariable() {
|
||||
let { canceled, result: name } = await os.inputText({
|
||||
title: this.$ts._pages.enterVariableName,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
name = name.trim();
|
||||
|
||||
if (this.hpml.isUsedName(name)) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: this.$ts._pages.variableNameIsAlreadyUsed
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const id = uuid();
|
||||
this.variables.push({ id, name, type: null });
|
||||
},
|
||||
|
||||
removeVariable(v) {
|
||||
this.variables = this.variables.filter(x => x.name !== v.name);
|
||||
},
|
||||
|
||||
getPageBlockList() {
|
||||
return [{
|
||||
label: this.$ts._pages.contentBlocks,
|
||||
items: [
|
||||
{ value: 'section', text: this.$ts._pages.blocks.section },
|
||||
{ value: 'text', text: this.$ts._pages.blocks.text },
|
||||
{ value: 'image', text: this.$ts._pages.blocks.image },
|
||||
{ value: 'textarea', text: this.$ts._pages.blocks.textarea },
|
||||
{ value: 'note', text: this.$ts._pages.blocks.note },
|
||||
{ value: 'canvas', text: this.$ts._pages.blocks.canvas },
|
||||
]
|
||||
}, {
|
||||
label: this.$ts._pages.inputBlocks,
|
||||
items: [
|
||||
{ value: 'button', text: this.$ts._pages.blocks.button },
|
||||
{ value: 'radioButton', text: this.$ts._pages.blocks.radioButton },
|
||||
{ value: 'textInput', text: this.$ts._pages.blocks.textInput },
|
||||
{ value: 'textareaInput', text: this.$ts._pages.blocks.textareaInput },
|
||||
{ value: 'numberInput', text: this.$ts._pages.blocks.numberInput },
|
||||
{ value: 'switch', text: this.$ts._pages.blocks.switch },
|
||||
{ value: 'counter', text: this.$ts._pages.blocks.counter }
|
||||
]
|
||||
}, {
|
||||
label: this.$ts._pages.specialBlocks,
|
||||
items: [
|
||||
{ value: 'if', text: this.$ts._pages.blocks.if },
|
||||
{ value: 'post', text: this.$ts._pages.blocks.post }
|
||||
]
|
||||
}];
|
||||
},
|
||||
|
||||
getScriptBlockList(type: string = null) {
|
||||
const list = [];
|
||||
|
||||
const blocks = blockDefs.filter(block => type === null || block.out === null || block.out === type || typeof block.out === 'number');
|
||||
|
||||
for (const block of blocks) {
|
||||
const category = list.find(x => x.category === block.category);
|
||||
if (category) {
|
||||
category.items.push({
|
||||
value: block.type,
|
||||
text: this.$t(`_pages.script.blocks.${block.type}`)
|
||||
});
|
||||
} else {
|
||||
list.push({
|
||||
category: block.category,
|
||||
label: this.$t(`_pages.script.categories.${block.category}`),
|
||||
items: [{
|
||||
value: block.type,
|
||||
text: this.$t(`_pages.script.blocks.${block.type}`)
|
||||
}]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const userFns = this.variables.filter(x => x.type === 'fn');
|
||||
if (userFns.length > 0) {
|
||||
list.unshift({
|
||||
label: this.$t(`_pages.script.categories.fn`),
|
||||
items: userFns.map(v => ({
|
||||
value: 'fn:' + v.name,
|
||||
text: v.name
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
return list;
|
||||
},
|
||||
|
||||
setEyeCatchingImage(e) {
|
||||
selectFile(e.currentTarget ?? e.target, null).then(file => {
|
||||
this.eyeCatchingImageId = file.id;
|
||||
});
|
||||
},
|
||||
|
||||
removeEyeCatchingImage() {
|
||||
this.eyeCatchingImageId = null;
|
||||
},
|
||||
|
||||
highlighter(code) {
|
||||
return highlight(code, languages.js, 'javascript');
|
||||
},
|
||||
watch($$(eyeCatchingImageId), async () => {
|
||||
if (eyeCatchingImageId == null) {
|
||||
eyeCatchingImage = null;
|
||||
} else {
|
||||
eyeCatchingImage = await os.api('drive/files/show', {
|
||||
fileId: eyeCatchingImageId,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function getSaveOptions() {
|
||||
return {
|
||||
title: tatitle.trim(),
|
||||
name: taname.trim(),
|
||||
summary: tasummary,
|
||||
font: tafont,
|
||||
script: tascript,
|
||||
hideTitleWhenPinned: tahideTitleWhenPinned,
|
||||
alignCenter: taalignCenter,
|
||||
content: tacontent,
|
||||
variables: tavariables,
|
||||
eyeCatchingImageId: taeyeCatchingImageId,
|
||||
};
|
||||
}
|
||||
|
||||
function save() {
|
||||
const options = tagetSaveOptions();
|
||||
|
||||
const onError = err => {
|
||||
if (err.id == '3d81ceae-475f-4600-b2a8-2bc116157532') {
|
||||
if (err.info.param == 'name') {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title: i18n.ts._pages.invalidNameTitle,
|
||||
text: i18n.ts._pages.invalidNameText,
|
||||
});
|
||||
}
|
||||
} else if (err.code == 'NAME_ALREADY_EXISTS') {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts._pages.nameAlreadyExists,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (tapageId) {
|
||||
options.pageId = tapageId;
|
||||
os.api('pages/update', options)
|
||||
.then(page => {
|
||||
tacurrentName = taname.trim();
|
||||
os.alert({
|
||||
type: 'success',
|
||||
text: i18n.ts._pages.updated,
|
||||
});
|
||||
}).catch(onError);
|
||||
} else {
|
||||
os.api('pages/create', options)
|
||||
.then(created => {
|
||||
tapageId = created.id;
|
||||
tacurrentName = name.trim();
|
||||
os.alert({
|
||||
type: 'success',
|
||||
text: i18n.ts._pages.created,
|
||||
});
|
||||
mainRouter.push(`/pages/edit/${pageId}`);
|
||||
}).catch(onError);
|
||||
}
|
||||
}
|
||||
|
||||
function del() {
|
||||
os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('removeAreYouSure', { x: title.trim() }),
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
os.api('pages/delete', {
|
||||
pageId: pageId,
|
||||
}).then(() => {
|
||||
os.alert({
|
||||
type: 'success',
|
||||
text: i18n.ts._pages.deleted,
|
||||
});
|
||||
mainRouter.push('/pages');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function duplicate() {
|
||||
tatitle = tatitle + ' - copy';
|
||||
taname = taname + '-copy';
|
||||
os.api('pages/create', tagetSaveOptions()).then(created => {
|
||||
tapageId = created.id;
|
||||
tacurrentName = taname.trim();
|
||||
os.alert({
|
||||
type: 'success',
|
||||
text: i18n.ts._pages.created,
|
||||
});
|
||||
mainRouter.push(`/pages/edit/${pageId}`);
|
||||
});
|
||||
}
|
||||
|
||||
async function add() {
|
||||
const { canceled, result: type } = await os.select({
|
||||
type: null,
|
||||
title: i18n.ts._pages.chooseBlock,
|
||||
groupedItems: tagetPageBlockList(),
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
const id = uuid();
|
||||
tacontent.push({ id, type });
|
||||
}
|
||||
|
||||
async function addVariable() {
|
||||
let { canceled, result: name } = await os.inputText({
|
||||
title: i18n.ts._pages.enterVariableName,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
name = name.trim();
|
||||
|
||||
if (tahpml.isUsedName(name)) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts._pages.variableNameIsAlreadyUsed,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const id = uuid();
|
||||
tavariables.push({ id, name, type: null });
|
||||
}
|
||||
|
||||
function removeVariable(v) {
|
||||
tavariables = tavariables.filter(x => x.name !== v.name);
|
||||
}
|
||||
|
||||
function getPageBlockList() {
|
||||
return [{
|
||||
label: i18n.ts._pages.contentBlocks,
|
||||
items: [
|
||||
{ value: 'section', text: i18n.ts._pages.blocks.section },
|
||||
{ value: 'text', text: i18n.ts._pages.blocks.text },
|
||||
{ value: 'image', text: i18n.ts._pages.blocks.image },
|
||||
{ value: 'textarea', text: i18n.ts._pages.blocks.textarea },
|
||||
{ value: 'note', text: i18n.ts._pages.blocks.note },
|
||||
{ value: 'canvas', text: i18n.ts._pages.blocks.canvas },
|
||||
],
|
||||
}, {
|
||||
label: i18n.ts._pages.inputBlocks,
|
||||
items: [
|
||||
{ value: 'button', text: i18n.ts._pages.blocks.button },
|
||||
{ value: 'radioButton', text: i18n.ts._pages.blocks.radioButton },
|
||||
{ value: 'textInput', text: i18n.ts._pages.blocks.textInput },
|
||||
{ value: 'textareaInput', text: i18n.ts._pages.blocks.textareaInput },
|
||||
{ value: 'numberInput', text: i18n.ts._pages.blocks.numberInput },
|
||||
{ value: 'switch', text: i18n.ts._pages.blocks.switch },
|
||||
{ value: 'counter', text: i18n.ts._pages.blocks.counter },
|
||||
],
|
||||
}, {
|
||||
label: i18n.ts._pages.specialBlocks,
|
||||
items: [
|
||||
{ value: 'if', text: i18n.ts._pages.blocks.if },
|
||||
{ value: 'post', text: i18n.ts._pages.blocks.post },
|
||||
],
|
||||
}];
|
||||
}
|
||||
|
||||
function getScriptBlockList(type: string = null) {
|
||||
const list = [];
|
||||
|
||||
const blocks = blockDefs.filter(block => type === null || block.out === null || block.out === type || typeof block.out === 'number');
|
||||
|
||||
for (const block of blocks) {
|
||||
const category = list.find(x => x.category === block.category);
|
||||
if (category) {
|
||||
category.items.push({
|
||||
value: block.type,
|
||||
text: i18n.t(`_pages.script.blocks.${block.type}`),
|
||||
});
|
||||
} else {
|
||||
list.push({
|
||||
category: block.category,
|
||||
label: i18n.t(`_pages.script.categories.${block.category}`),
|
||||
items: [{
|
||||
value: block.type,
|
||||
text: i18n.t(`_pages.script.blocks.${block.type}`),
|
||||
}],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const userFns = variables.filter(x => x.type === 'fn');
|
||||
if (userFns.length > 0) {
|
||||
list.unshift({
|
||||
label: i18n.t('_pages.script.categories.fn'),
|
||||
items: userFns.map(v => ({
|
||||
value: 'fn:' + v.name,
|
||||
text: v.name,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
function setEyeCatchingImage(e) {
|
||||
selectFile(e.currentTarget ?? e.target, null).then(file => {
|
||||
eyeCatchingImageId = file.id;
|
||||
});
|
||||
}
|
||||
|
||||
function removeEyeCatchingImage() {
|
||||
taeyeCatchingImageId = null;
|
||||
}
|
||||
|
||||
function highlighter(code) {
|
||||
return highlight(code, languages.js, 'javascript');
|
||||
}
|
||||
|
||||
async function init() {
|
||||
hpml = new HpmlTypeChecker();
|
||||
|
||||
watch($$(variables), () => {
|
||||
hpml.variables = variables;
|
||||
}, { deep: true });
|
||||
|
||||
watch($$(content), () => {
|
||||
hpml.pageVars = collectPageVars(content);
|
||||
}, { deep: true });
|
||||
|
||||
if (props.initPageId) {
|
||||
page = await os.api('pages/show', {
|
||||
pageId: props.initPageId,
|
||||
});
|
||||
} else if (props.initPageName && props.initUser) {
|
||||
page = await os.api('pages/show', {
|
||||
name: props.initPageName,
|
||||
username: props.initUser,
|
||||
});
|
||||
readonly = true;
|
||||
}
|
||||
|
||||
if (page) {
|
||||
author = page.user;
|
||||
pageId = page.id;
|
||||
title = page.title;
|
||||
name = page.name;
|
||||
currentName = page.name;
|
||||
summary = page.summary;
|
||||
font = page.font;
|
||||
script = page.script;
|
||||
hideTitleWhenPinned = page.hideTitleWhenPinned;
|
||||
alignCenter = page.alignCenter;
|
||||
content = page.content;
|
||||
variables = page.variables;
|
||||
eyeCatchingImageId = page.eyeCatchingImageId;
|
||||
} else {
|
||||
const id = uuid();
|
||||
content = [{
|
||||
id,
|
||||
type: 'text',
|
||||
text: 'Hello World!',
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => {
|
||||
let title = i18n.ts._pages.newPage;
|
||||
if (props.initPageId) {
|
||||
title = i18n.ts._pages.editPage;
|
||||
}
|
||||
else if (props.initPageName && props.initUser) {
|
||||
title = i18n.ts._pages.readPage;
|
||||
}
|
||||
return {
|
||||
title: title,
|
||||
icon: 'fas fa-pencil-alt',
|
||||
bg: 'var(--bg)',
|
||||
tabs: [{
|
||||
active: tab === 'settings',
|
||||
title: i18n.ts._pages.pageSetting,
|
||||
icon: 'fas fa-cog',
|
||||
onClick: () => { tab = 'settings'; },
|
||||
}, {
|
||||
active: tab === 'contents',
|
||||
title: i18n.ts._pages.contents,
|
||||
icon: 'fas fa-sticky-note',
|
||||
onClick: () => { tab = 'contents'; },
|
||||
}, {
|
||||
active: tab === 'variables',
|
||||
title: i18n.ts._pages.variables,
|
||||
icon: 'fas fa-magic',
|
||||
onClick: () => { tab = 'variables'; },
|
||||
}, {
|
||||
active: tab === 'script',
|
||||
title: i18n.ts.script,
|
||||
icon: 'fas fa-code',
|
||||
onClick: () => { tab = 'script'; },
|
||||
}],
|
||||
};
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="700">
|
||||
<template><MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700">
|
||||
<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
|
||||
<div v-if="page" :key="page.id" v-size="{ max: [450] }" class="xcukqgmh">
|
||||
<div class="_block main">
|
||||
@ -56,138 +57,108 @@
|
||||
<MkError v-else-if="error" @retry="fetch()"/>
|
||||
<MkLoading v-else/>
|
||||
</transition>
|
||||
</MkSpacer>
|
||||
</MkSpacer></MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch } from 'vue';
|
||||
import XPage from '@/components/page/page.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { url } from '@/config';
|
||||
import MkFollowButton from '@/components/follow-button.vue';
|
||||
import MkContainer from '@/components/ui/container.vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import MkPagePreview from '@/components/page-preview.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XPage,
|
||||
MkButton,
|
||||
MkFollowButton,
|
||||
MkContainer,
|
||||
MkPagination,
|
||||
MkPagePreview,
|
||||
const props = defineProps<{
|
||||
pageName: string;
|
||||
username: string;
|
||||
}>();
|
||||
|
||||
let page = $ref(null);
|
||||
let error = $ref(null);
|
||||
const otherPostsPagination = {
|
||||
endpoint: 'users/pages' as const,
|
||||
limit: 6,
|
||||
params: computed(() => ({
|
||||
userId: page.user.id,
|
||||
})),
|
||||
};
|
||||
const path = $computed(() => props.username + '/' + props.pageName);
|
||||
|
||||
function fetchPage() {
|
||||
page = null;
|
||||
os.api('pages/show', {
|
||||
name: props.pageName,
|
||||
username: props.username,
|
||||
}).then(_page => {
|
||||
page = _page;
|
||||
}).catch(err => {
|
||||
error = err;
|
||||
});
|
||||
}
|
||||
|
||||
function share() {
|
||||
navigator.share({
|
||||
title: page.title ?? page.name,
|
||||
text: page.summary,
|
||||
url: `${url}/@${page.user.username}/pages/${page.name}`,
|
||||
});
|
||||
}
|
||||
|
||||
function shareWithNote() {
|
||||
os.post({
|
||||
initialText: `${page.title || page.name} ${url}/@${page.user.username}/pages/${page.name}`,
|
||||
});
|
||||
}
|
||||
|
||||
function like() {
|
||||
os.apiWithDialog('pages/like', {
|
||||
pageId: page.id,
|
||||
}).then(() => {
|
||||
page.isLiked = true;
|
||||
page.likedCount++;
|
||||
});
|
||||
}
|
||||
|
||||
async function unlike() {
|
||||
const confirm = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.unlikeConfirm,
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
os.apiWithDialog('pages/unlike', {
|
||||
pageId: page.id,
|
||||
}).then(() => {
|
||||
page.isLiked = false;
|
||||
page.likedCount--;
|
||||
});
|
||||
}
|
||||
|
||||
function pin(pin) {
|
||||
os.apiWithDialog('i/update', {
|
||||
pinnedPageId: pin ? page.id : null,
|
||||
});
|
||||
}
|
||||
|
||||
watch(() => path, fetchPage, { immediate: true });
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => page ? {
|
||||
title: computed(() => page.title || page.name),
|
||||
avatar: page.user,
|
||||
path: `/@${page.user.username}/pages/${page.name}`,
|
||||
share: {
|
||||
title: page.title || page.name,
|
||||
text: page.summary,
|
||||
},
|
||||
|
||||
props: {
|
||||
pageName: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
username: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: computed(() => this.page ? {
|
||||
title: computed(() => this.page.title || this.page.name),
|
||||
avatar: this.page.user,
|
||||
path: `/@${this.page.user.username}/pages/${this.page.name}`,
|
||||
share: {
|
||||
title: this.page.title || this.page.name,
|
||||
text: this.page.summary,
|
||||
},
|
||||
} : null),
|
||||
page: null,
|
||||
error: null,
|
||||
otherPostsPagination: {
|
||||
endpoint: 'users/pages' as const,
|
||||
limit: 6,
|
||||
params: computed(() => ({
|
||||
userId: this.page.user.id
|
||||
})),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
path(): string {
|
||||
return this.username + '/' + this.pageName;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
path() {
|
||||
this.fetch();
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.fetch();
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetch() {
|
||||
this.page = null;
|
||||
os.api('pages/show', {
|
||||
name: this.pageName,
|
||||
username: this.username,
|
||||
}).then(page => {
|
||||
this.page = page;
|
||||
}).catch(err => {
|
||||
this.error = err;
|
||||
});
|
||||
},
|
||||
|
||||
share() {
|
||||
navigator.share({
|
||||
title: this.page.title || this.page.name,
|
||||
text: this.page.summary,
|
||||
url: `${url}/@${this.page.user.username}/pages/${this.page.name}`
|
||||
});
|
||||
},
|
||||
|
||||
shareWithNote() {
|
||||
os.post({
|
||||
initialText: `${this.page.title || this.page.name} ${url}/@${this.page.user.username}/pages/${this.page.name}`
|
||||
});
|
||||
},
|
||||
|
||||
like() {
|
||||
os.apiWithDialog('pages/like', {
|
||||
pageId: this.page.id,
|
||||
}).then(() => {
|
||||
this.page.isLiked = true;
|
||||
this.page.likedCount++;
|
||||
});
|
||||
},
|
||||
|
||||
async unlike() {
|
||||
const confirm = await os.confirm({
|
||||
type: 'warning',
|
||||
text: this.$ts.unlikeConfirm,
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
os.apiWithDialog('pages/unlike', {
|
||||
pageId: this.page.id,
|
||||
}).then(() => {
|
||||
this.page.isLiked = false;
|
||||
this.page.likedCount--;
|
||||
});
|
||||
},
|
||||
|
||||
pin(pin) {
|
||||
os.apiWithDialog('i/update', {
|
||||
pinnedPageId: pin ? this.page.id : null,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
} : null));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -1,86 +1,87 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="700">
|
||||
<div v-if="tab === 'featured'" class="rknalgpo">
|
||||
<MkPagination v-slot="{items}" :pagination="featuredPagesPagination">
|
||||
<MkPagePreview v-for="page in items" :key="page.id" class="ckltabjg" :page="page"/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700">
|
||||
<div v-if="tab === 'featured'" class="rknalgpo">
|
||||
<MkPagination v-slot="{items}" :pagination="featuredPagesPagination">
|
||||
<MkPagePreview v-for="page in items" :key="page.id" class="ckltabjg" :page="page"/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tab === 'my'" class="rknalgpo my">
|
||||
<MkButton class="new" @click="create()"><i class="fas fa-plus"></i></MkButton>
|
||||
<MkPagination v-slot="{items}" :pagination="myPagesPagination">
|
||||
<MkPagePreview v-for="page in items" :key="page.id" class="ckltabjg" :page="page"/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div v-else-if="tab === 'my'" class="rknalgpo my">
|
||||
<MkButton class="new" @click="create()"><i class="fas fa-plus"></i></MkButton>
|
||||
<MkPagination v-slot="{items}" :pagination="myPagesPagination">
|
||||
<MkPagePreview v-for="page in items" :key="page.id" class="ckltabjg" :page="page"/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tab === 'liked'" class="rknalgpo">
|
||||
<MkPagination v-slot="{items}" :pagination="likedPagesPagination">
|
||||
<MkPagePreview v-for="like in items" :key="like.page.id" class="ckltabjg" :page="like.page"/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
<div v-else-if="tab === 'liked'" class="rknalgpo">
|
||||
<MkPagination v-slot="{items}" :pagination="likedPagesPagination">
|
||||
<MkPagePreview v-for="like in items" :key="like.page.id" class="ckltabjg" :page="like.page"/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject } from 'vue';
|
||||
import MkPagePreview from '@/components/page-preview.vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import * as symbols from '@/symbols';
|
||||
import { useRouter } from '@/router';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkPagePreview, MkPagination, MkButton
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: computed(() => ({
|
||||
title: this.$ts.pages,
|
||||
icon: 'fas fa-sticky-note',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
icon: 'fas fa-plus',
|
||||
text: this.$ts.create,
|
||||
handler: this.create,
|
||||
}],
|
||||
tabs: [{
|
||||
active: this.tab === 'featured',
|
||||
title: this.$ts._pages.featured,
|
||||
icon: 'fas fa-fire-alt',
|
||||
onClick: () => { this.tab = 'featured'; },
|
||||
}, {
|
||||
active: this.tab === 'my',
|
||||
title: this.$ts._pages.my,
|
||||
icon: 'fas fa-edit',
|
||||
onClick: () => { this.tab = 'my'; },
|
||||
}, {
|
||||
active: this.tab === 'liked',
|
||||
title: this.$ts._pages.liked,
|
||||
icon: 'fas fa-heart',
|
||||
onClick: () => { this.tab = 'liked'; },
|
||||
},]
|
||||
})),
|
||||
tab: 'featured',
|
||||
featuredPagesPagination: {
|
||||
endpoint: 'pages/featured' as const,
|
||||
noPaging: true,
|
||||
},
|
||||
myPagesPagination: {
|
||||
endpoint: 'i/pages' as const,
|
||||
limit: 5,
|
||||
},
|
||||
likedPagesPagination: {
|
||||
endpoint: 'i/page-likes' as const,
|
||||
limit: 5,
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
create() {
|
||||
this.$router.push(`/pages/new`);
|
||||
}
|
||||
}
|
||||
});
|
||||
const router = useRouter();
|
||||
|
||||
let tab = $ref('featured');
|
||||
|
||||
const featuredPagesPagination = {
|
||||
endpoint: 'pages/featured' as const,
|
||||
noPaging: true,
|
||||
};
|
||||
const myPagesPagination = {
|
||||
endpoint: 'i/pages' as const,
|
||||
limit: 5,
|
||||
};
|
||||
const likedPagesPagination = {
|
||||
endpoint: 'i/page-likes' as const,
|
||||
limit: 5,
|
||||
};
|
||||
|
||||
function create() {
|
||||
router.push('/pages/new');
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => [{
|
||||
icon: 'fas fa-plus',
|
||||
text: i18n.ts.create,
|
||||
handler: create,
|
||||
}]);
|
||||
|
||||
const headerTabs = $computed(() => [{
|
||||
active: tab === 'featured',
|
||||
title: i18n.ts._pages.featured,
|
||||
icon: 'fas fa-fire-alt',
|
||||
onClick: () => { tab = 'featured'; },
|
||||
}, {
|
||||
active: tab === 'my',
|
||||
title: i18n.ts._pages.my,
|
||||
icon: 'fas fa-edit',
|
||||
onClick: () => { tab = 'my'; },
|
||||
}, {
|
||||
active: tab === 'liked',
|
||||
title: i18n.ts._pages.liked,
|
||||
icon: 'fas fa-heart',
|
||||
onClick: () => { tab = 'liked'; },
|
||||
}]);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
title: i18n.ts.pages,
|
||||
icon: 'fas fa-sticky-note',
|
||||
bg: 'var(--bg)',
|
||||
})));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -7,16 +7,18 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import MkSample from '@/components/sample.vue';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: computed(() => ({
|
||||
title: i18n.ts.preview,
|
||||
icon: 'fas fa-eye',
|
||||
bg: 'var(--bg)',
|
||||
})),
|
||||
});
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
title: i18n.ts.preview,
|
||||
icon: 'fas fa-eye',
|
||||
bg: 'var(--bg)',
|
||||
})));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -1,14 +1,17 @@
|
||||
<template>
|
||||
<MkSpacer v-if="token" :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<div class="_formRoot">
|
||||
<FormInput v-model="password" type="password" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-lock"></i></template>
|
||||
<template #label>{{ i18n.ts.newPassword }}</template>
|
||||
</FormInput>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer v-if="token" :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<div class="_formRoot">
|
||||
<FormInput v-model="password" type="password" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-lock"></i></template>
|
||||
<template #label>{{ i18n.ts.newPassword }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormButton primary class="_formBlock" @click="save">{{ i18n.ts.save }}</FormButton>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
<FormButton primary class="_formBlock" @click="save">{{ i18n.ts.save }}</FormButton>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@ -16,9 +19,9 @@ import { defineAsyncComponent, onMounted } from 'vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
import { router } from '@/router';
|
||||
import { mainRouter } from '@/router';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const props = defineProps<{
|
||||
token?: string;
|
||||
@ -31,22 +34,24 @@ async function save() {
|
||||
token: props.token,
|
||||
password: password,
|
||||
});
|
||||
router.push('/');
|
||||
mainRouter.push('/');
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.token == null) {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/forgot-password.vue')), {}, {}, 'closed');
|
||||
router.push('/');
|
||||
mainRouter.push('/');
|
||||
}
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.resetPassword,
|
||||
icon: 'fas fa-lock',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.resetPassword,
|
||||
icon: 'fas fa-lock',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -19,7 +19,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineExpose, ref, watch } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import 'prismjs';
|
||||
import { highlight, languages } from 'prismjs/components/prism-core';
|
||||
import 'prismjs/components/prism-clike';
|
||||
@ -32,9 +32,9 @@ import MkContainer from '@/components/ui/container.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import { createAiScriptEnv } from '@/scripts/aiscript/api';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { $i } from '@/account';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const code = ref('');
|
||||
const logs = ref<any[]>([]);
|
||||
@ -67,7 +67,7 @@ async function run() {
|
||||
logs.value.push({
|
||||
id: Math.random(),
|
||||
text: value.type === 'str' ? value.value : utils.valToString(value),
|
||||
print: true
|
||||
print: true,
|
||||
});
|
||||
},
|
||||
log: (type, params) => {
|
||||
@ -75,11 +75,11 @@ async function run() {
|
||||
case 'end': logs.value.push({
|
||||
id: Math.random(),
|
||||
text: utils.valToString(params.val, true),
|
||||
print: false
|
||||
print: false,
|
||||
}); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
let ast;
|
||||
@ -88,7 +88,7 @@ async function run() {
|
||||
} catch (error) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: 'Syntax error :('
|
||||
text: 'Syntax error :(',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -97,7 +97,7 @@ async function run() {
|
||||
} catch (error: any) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: error.message
|
||||
text: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -106,11 +106,13 @@ function highlighter(code) {
|
||||
return highlight(code, languages.js, 'javascript');
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.scratchpad,
|
||||
icon: 'fas fa-terminal',
|
||||
},
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.scratchpad,
|
||||
icon: 'fas fa-terminal',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -1,16 +1,17 @@
|
||||
<template>
|
||||
<div class="_section">
|
||||
<div class="_content">
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="800">
|
||||
<XNotes ref="notes" :pagination="pagination"/>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import XNotes from '@/components/notes.vue';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const props = defineProps<{
|
||||
query: string;
|
||||
@ -23,14 +24,16 @@ const pagination = {
|
||||
params: computed(() => ({
|
||||
query: props.query,
|
||||
channelId: props.channel,
|
||||
}))
|
||||
})),
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: computed(() => ({
|
||||
title: i18n.t('searchWith', { q: props.query }),
|
||||
icon: 'fas fa-search',
|
||||
bg: 'var(--bg)',
|
||||
})),
|
||||
});
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
title: i18n.t('searchWith', { q: props.query }),
|
||||
icon: 'fas fa-search',
|
||||
bg: 'var(--bg)',
|
||||
})));
|
||||
</script>
|
||||
|
@ -127,30 +127,32 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineExpose, onMounted, ref } from 'vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import MkKeyValue from '@/components/key-value.vue';
|
||||
import * as os from '@/os';
|
||||
import number from '@/filters/number';
|
||||
import bytes from '@/filters/bytes';
|
||||
import * as symbols from '@/symbols';
|
||||
import { $i } from '@/account';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const stats = ref<any>({});
|
||||
|
||||
onMounted(() => {
|
||||
os.api('users/stats', {
|
||||
userId: $i!.id
|
||||
userId: $i!.id,
|
||||
}).then(response => {
|
||||
stats.value = response;
|
||||
});
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.accountInfo,
|
||||
icon: 'fas fa-info-circle'
|
||||
}
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.accountInfo,
|
||||
icon: 'fas fa-info-circle',
|
||||
});
|
||||
</script>
|
||||
|
@ -21,13 +21,13 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, defineExpose, ref } from 'vue';
|
||||
import { defineAsyncComponent, ref } from 'vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { getAccounts, addAccount as addAccounts, login, $i } from '@/account';
|
||||
import { getAccounts, addAccount as addAccounts, removeAccount as _removeAccount, login, $i } from '@/account';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const storedAccounts = ref<any>(null);
|
||||
const accounts = ref<any>(null);
|
||||
@ -39,7 +39,7 @@ const init = async () => {
|
||||
console.log(storedAccounts.value);
|
||||
|
||||
return os.api('users/show', {
|
||||
userIds: storedAccounts.value.map(x => x.id)
|
||||
userIds: storedAccounts.value.map(x => x.id),
|
||||
});
|
||||
}).then(response => {
|
||||
accounts.value = response;
|
||||
@ -70,6 +70,10 @@ function addAccount(ev) {
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
function removeAccount(account) {
|
||||
_removeAccount(account.id);
|
||||
}
|
||||
|
||||
function addExistingAccount() {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/signin-dialog.vue')), {}, {
|
||||
done: res => {
|
||||
@ -98,12 +102,14 @@ function switchAccountWithToken(token: string) {
|
||||
login(token);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.accounts,
|
||||
icon: 'fas fa-users',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.accounts,
|
||||
icon: 'fas fa-users',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -7,12 +7,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, defineExpose, ref } from 'vue';
|
||||
import { defineAsyncComponent, ref } from 'vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const isDesktop = ref(window.innerWidth >= 1100);
|
||||
|
||||
@ -29,17 +29,19 @@ function generateToken() {
|
||||
os.alert({
|
||||
type: 'success',
|
||||
title: i18n.ts.token,
|
||||
text: token
|
||||
text: token,
|
||||
});
|
||||
},
|
||||
}, 'closed');
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: 'API',
|
||||
icon: 'fas fa-key',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: 'API',
|
||||
icon: 'fas fa-key',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
@ -7,7 +7,7 @@
|
||||
<div>{{ i18n.ts.nothing }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot="{items}">
|
||||
<template #default="{items}">
|
||||
<div v-for="token in items" :key="token.id" class="_panel bfomjevm">
|
||||
<img v-if="token.iconUrl" class="icon" :src="token.iconUrl" alt=""/>
|
||||
<div class="body">
|
||||
@ -38,11 +38,11 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineExpose, ref } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import FormPagination from '@/components/ui/pagination.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const list = ref<any>(null);
|
||||
|
||||
@ -50,8 +50,8 @@ const pagination = {
|
||||
endpoint: 'i/apps' as const,
|
||||
limit: 100,
|
||||
params: {
|
||||
sort: '+lastUsedAt'
|
||||
}
|
||||
sort: '+lastUsedAt',
|
||||
},
|
||||
};
|
||||
|
||||
function revoke(token) {
|
||||
@ -60,12 +60,14 @@ function revoke(token) {
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.installedApps,
|
||||
icon: 'fas fa-plug',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.installedApps,
|
||||
icon: 'fas fa-plug',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -9,13 +9,13 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineExpose, ref, watch } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import FormTextarea from '@/components/form/textarea.vue';
|
||||
import FormInfo from '@/components/ui/info.vue';
|
||||
import * as os from '@/os';
|
||||
import { unisonReload } from '@/scripts/unison-reload';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const localCustomCss = ref(localStorage.getItem('customCss') ?? '');
|
||||
|
||||
@ -35,11 +35,13 @@ watch(localCustomCss, async () => {
|
||||
await apply();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.customCss,
|
||||
icon: 'fas fa-code',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.customCss,
|
||||
icon: 'fas fa-code',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
@ -30,7 +30,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineExpose, watch } from 'vue';
|
||||
import { computed, watch } from 'vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormRadios from '@/components/form/radios.vue';
|
||||
@ -39,8 +39,8 @@ import FormGroup from '@/components/form/group.vue';
|
||||
import { deckStore } from '@/ui/deck/deck-store';
|
||||
import * as os from '@/os';
|
||||
import { unisonReload } from '@/scripts/unison-reload';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const navWindow = computed(deckStore.makeGetterSetter('navWindow'));
|
||||
const alwaysShowMainColumn = computed(deckStore.makeGetterSetter('alwaysShowMainColumn'));
|
||||
@ -62,7 +62,7 @@ watch(navWindow, async () => {
|
||||
async function setProfile() {
|
||||
const { canceled, result: name } = await os.inputText({
|
||||
title: i18n.ts._deck.profile,
|
||||
allowEmpty: false
|
||||
allowEmpty: false,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
@ -70,11 +70,13 @@ async function setProfile() {
|
||||
unisonReload();
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.deck,
|
||||
icon: 'fas fa-columns',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.deck,
|
||||
icon: 'fas fa-columns',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
@ -8,13 +8,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineExpose } from 'vue';
|
||||
import FormInfo from '@/components/ui/info.vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
import * as os from '@/os';
|
||||
import { signout } from '@/account';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
async function deleteAccount() {
|
||||
{
|
||||
@ -27,12 +26,12 @@ async function deleteAccount() {
|
||||
|
||||
const { canceled, result: password } = await os.inputText({
|
||||
title: i18n.ts.password,
|
||||
type: 'password'
|
||||
type: 'password',
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
await os.apiWithDialog('i/delete-account', {
|
||||
password: password
|
||||
password: password,
|
||||
});
|
||||
|
||||
await os.alert({
|
||||
@ -42,11 +41,13 @@ async function deleteAccount() {
|
||||
await signout();
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts._accountDelete.accountDelete,
|
||||
icon: 'fas fa-exclamation-triangle',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts._accountDelete.accountDelete,
|
||||
icon: 'fas fa-exclamation-triangle',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
@ -34,7 +34,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineExpose, ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
@ -43,10 +43,10 @@ import MkKeyValue from '@/components/key-value.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import * as os from '@/os';
|
||||
import bytes from '@/filters/bytes';
|
||||
import * as symbols from '@/symbols';
|
||||
import { defaultStore } from '@/store';
|
||||
import MkChart from '@/components/chart.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const fetching = ref(true);
|
||||
const usage = ref<any>(null);
|
||||
@ -59,8 +59,8 @@ const meterStyle = computed(() => {
|
||||
background: tinycolor({
|
||||
h: 180 - (usage.value / capacity.value * 180),
|
||||
s: 0.7,
|
||||
l: 0.5
|
||||
})
|
||||
l: 0.5,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
@ -74,7 +74,7 @@ os.api('drive').then(info => {
|
||||
|
||||
if (defaultStore.state.uploadFolder) {
|
||||
os.api('drive/folders/show', {
|
||||
folderId: defaultStore.state.uploadFolder
|
||||
folderId: defaultStore.state.uploadFolder,
|
||||
}).then(response => {
|
||||
uploadFolder.value = response;
|
||||
});
|
||||
@ -86,7 +86,7 @@ function chooseUploadFolder() {
|
||||
os.success();
|
||||
if (defaultStore.state.uploadFolder) {
|
||||
uploadFolder.value = await os.api('drive/folders/show', {
|
||||
folderId: defaultStore.state.uploadFolder
|
||||
folderId: defaultStore.state.uploadFolder,
|
||||
});
|
||||
} else {
|
||||
uploadFolder.value = null;
|
||||
@ -94,12 +94,14 @@ function chooseUploadFolder() {
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.drive,
|
||||
icon: 'fas fa-cloud',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.drive,
|
||||
icon: 'fas fa-cloud',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -40,27 +40,27 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineExpose, onMounted, ref, watch } from 'vue';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { $i } from '@/account';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const emailAddress = ref($i!.email);
|
||||
|
||||
const onChangeReceiveAnnouncementEmail = (v) => {
|
||||
os.api('i/update', {
|
||||
receiveAnnouncementEmail: v
|
||||
receiveAnnouncementEmail: v,
|
||||
});
|
||||
};
|
||||
|
||||
const saveEmailAddress = () => {
|
||||
os.inputText({
|
||||
title: i18n.ts.password,
|
||||
type: 'password'
|
||||
type: 'password',
|
||||
}).then(({ canceled, result: password }) => {
|
||||
if (canceled) return;
|
||||
os.apiWithDialog('i/update-email', {
|
||||
@ -86,7 +86,7 @@ const saveNotificationSettings = () => {
|
||||
...[emailNotification_follow.value ? 'follow' : null],
|
||||
...[emailNotification_receiveFollowRequest.value ? 'receiveFollowRequest' : null],
|
||||
...[emailNotification_groupInvited.value ? 'groupInvited' : null],
|
||||
].filter(x => x != null)
|
||||
].filter(x => x != null),
|
||||
});
|
||||
};
|
||||
|
||||
@ -100,11 +100,13 @@ onMounted(() => {
|
||||
});
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.email,
|
||||
icon: 'fas fa-envelope',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.email,
|
||||
icon: 'fas fa-envelope',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
@ -48,7 +48,8 @@
|
||||
<FormSwitch v-model="disableShowingAnimatedImages" class="_formBlock">{{ i18n.ts.disableShowingAnimatedImages }}</FormSwitch>
|
||||
<FormSwitch v-model="squareAvatars" class="_formBlock">{{ i18n.ts.squareAvatars }}</FormSwitch>
|
||||
<FormSwitch v-model="useSystemFont" class="_formBlock">{{ i18n.ts.useSystemFont }}</FormSwitch>
|
||||
<FormSwitch v-model="useOsNativeEmojis" class="_formBlock">{{ i18n.ts.useOsNativeEmojis }}
|
||||
<FormSwitch v-model="useOsNativeEmojis" class="_formBlock">
|
||||
{{ i18n.ts.useOsNativeEmojis }}
|
||||
<div><Mfm :key="useOsNativeEmojis" text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></div>
|
||||
</FormSwitch>
|
||||
<FormSwitch v-model="disableDrawer" class="_formBlock">{{ i18n.ts.disableDrawer }}</FormSwitch>
|
||||
@ -92,7 +93,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineExpose, ref, watch } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormSelect from '@/components/form/select.vue';
|
||||
import FormRadios from '@/components/form/radios.vue';
|
||||
@ -104,8 +105,8 @@ import { langs } from '@/config';
|
||||
import { defaultStore } from '@/store';
|
||||
import * as os from '@/os';
|
||||
import { unisonReload } from '@/scripts/unison-reload';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const lang = ref(localStorage.getItem('lang'));
|
||||
const fontSize = ref(localStorage.getItem('fontSize'));
|
||||
@ -173,16 +174,18 @@ watch([
|
||||
aiChanMode,
|
||||
showGapBetweenNotesInTimeline,
|
||||
instanceTicker,
|
||||
overridedDeviceKind
|
||||
overridedDeviceKind,
|
||||
], async () => {
|
||||
await reloadAsk();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.general,
|
||||
icon: 'fas fa-cogs',
|
||||
bg: 'var(--bg)'
|
||||
}
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.general,
|
||||
icon: 'fas fa-cogs',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
@ -38,15 +38,15 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineExpose, ref } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import FormGroup from '@/components/form/group.vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import * as os from '@/os';
|
||||
import { selectFile } from '@/scripts/select-file';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const excludeMutingUsers = ref(false);
|
||||
const excludeInactiveUsers = ref(false);
|
||||
@ -116,12 +116,14 @@ const importBlocking = async (ev) => {
|
||||
os.api('i/import-blocking', { fileId: file.id }).then(onImportSuccess).catch(onError);
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.importAndExport,
|
||||
icon: 'fas fa-boxes',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.importAndExport,
|
||||
icon: 'fas fa-boxes',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -1,46 +1,42 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="900" :margin-min="20" :margin-max="32">
|
||||
<div ref="el" class="vvcocwet" :class="{ wide: !narrow }">
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
<MkA v-if="narrow" to="/settings">{{ $ts.settings }}</MkA>
|
||||
<template v-else>{{ $ts.settings }}</template>
|
||||
</div>
|
||||
<div v-if="childInfo" class="subtitle">{{ childInfo.title }}</div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div v-if="!narrow || initialPage == null" class="nav">
|
||||
<div class="baaadecd">
|
||||
<MkInfo v-if="emailNotConfigured" warn class="info">{{ $ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ $ts.configure }}</MkA></MkInfo>
|
||||
<MkSuperMenu :def="menuDef" :grid="initialPage == null"></MkSuperMenu>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="900" :margin-min="20" :margin-max="32">
|
||||
<div ref="el" class="vvcocwet" :class="{ wide: !narrow }">
|
||||
<div class="body">
|
||||
<div v-if="!narrow || initialPage == null" class="nav">
|
||||
<div class="baaadecd">
|
||||
<MkInfo v-if="emailNotConfigured" warn class="info">{{ $ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ $ts.configure }}</MkA></MkInfo>
|
||||
<MkSuperMenu :def="menuDef" :grid="initialPage == null"></MkSuperMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!(narrow && initialPage == null)" class="main">
|
||||
<div class="bkzroven">
|
||||
<component :is="component" :ref="el => pageChanged(el)" :key="initialPage" v-bind="pageProps"/>
|
||||
<div v-if="!(narrow && initialPage == null)" class="main">
|
||||
<div class="bkzroven">
|
||||
<component :is="component" :key="initialPage" v-bind="pageProps"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkSpacer>
|
||||
</mkstickycontainer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, defineAsyncComponent, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { computed, defineAsyncComponent, inject, nextTick, onMounted, onUnmounted, provide, ref, watch } from 'vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import MkInfo from '@/components/ui/info.vue';
|
||||
import MkSuperMenu from '@/components/ui/super-menu.vue';
|
||||
import { scroll } from '@/scripts/scroll';
|
||||
import { signout } from '@/account';
|
||||
import { signout , $i } from '@/account';
|
||||
import { unisonReload } from '@/scripts/unison-reload';
|
||||
import * as symbols from '@/symbols';
|
||||
import { instance } from '@/instance';
|
||||
import { $i } from '@/account';
|
||||
import { MisskeyNavigator } from '@/scripts/navigate';
|
||||
import { useRouter } from '@/router';
|
||||
import { definePageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const props = defineProps<{
|
||||
initialPage?: string
|
||||
}>();
|
||||
const props = withDefaults(defineProps<{
|
||||
initialPage?: string;
|
||||
}>(), {
|
||||
});
|
||||
|
||||
const indexInfo = {
|
||||
title: i18n.ts.settings,
|
||||
@ -52,7 +48,7 @@ const INFO = ref(indexInfo);
|
||||
const el = ref<HTMLElement | null>(null);
|
||||
const childInfo = ref(null);
|
||||
|
||||
const nav = new MisskeyNavigator();
|
||||
const router = useRouter();
|
||||
|
||||
const narrow = ref(false);
|
||||
const NARROW_THRESHOLD = 600;
|
||||
@ -189,7 +185,7 @@ const menuDef = computed(() => [{
|
||||
signout();
|
||||
},
|
||||
danger: true,
|
||||
},],
|
||||
}],
|
||||
}]);
|
||||
|
||||
const pageProps = ref({});
|
||||
@ -242,7 +238,7 @@ watch(component, () => {
|
||||
|
||||
watch(() => props.initialPage, () => {
|
||||
if (props.initialPage == null && !narrow.value) {
|
||||
nav.push('/settings/profile');
|
||||
router.push('/settings/profile');
|
||||
} else {
|
||||
if (props.initialPage == null) {
|
||||
INFO.value = indexInfo;
|
||||
@ -252,7 +248,7 @@ watch(() => props.initialPage, () => {
|
||||
|
||||
watch(narrow, () => {
|
||||
if (props.initialPage == null && !narrow.value) {
|
||||
nav.push('/settings/profile');
|
||||
router.push('/settings/profile');
|
||||
}
|
||||
});
|
||||
|
||||
@ -261,7 +257,7 @@ onMounted(() => {
|
||||
|
||||
narrow.value = el.value.offsetWidth < NARROW_THRESHOLD;
|
||||
if (props.initialPage == null && !narrow.value) {
|
||||
nav.push('/settings/profile');
|
||||
router.push('/settings/profile');
|
||||
}
|
||||
});
|
||||
|
||||
@ -271,38 +267,23 @@ onUnmounted(() => {
|
||||
|
||||
const emailNotConfigured = computed(() => instance.enableEmail && ($i.email == null || !$i.emailVerified));
|
||||
|
||||
const pageChanged = (page) => {
|
||||
if (page == null) {
|
||||
provideMetadataReceiver((info) => {
|
||||
if (info == null) {
|
||||
childInfo.value = null;
|
||||
} else {
|
||||
childInfo.value = page[symbols.PAGE_INFO];
|
||||
childInfo.value = info;
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: INFO,
|
||||
});
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata(INFO);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vvcocwet {
|
||||
> .header {
|
||||
display: flex;
|
||||
margin-bottom: 24px;
|
||||
font-size: 1.3em;
|
||||
font-weight: bold;
|
||||
|
||||
> .title {
|
||||
display: block;
|
||||
width: 34%;
|
||||
}
|
||||
|
||||
> .subtitle {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
> .body {
|
||||
> .nav {
|
||||
.baaadecd {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user