refactor(client): Refine routing (#8846)

This commit is contained in:
syuilo 2022-06-20 17:38:49 +09:00 committed by GitHub
parent 30a39a296d
commit 699f24f3dc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
149 changed files with 6312 additions and 6670 deletions

View file

@ -77,7 +77,6 @@
"vite": "2.9.10", "vite": "2.9.10",
"vue": "3.2.37", "vue": "3.2.37",
"vue-prism-editor": "2.0.0-alpha.2", "vue-prism-editor": "2.0.0-alpha.2",
"vue-router": "4.0.16",
"vuedraggable": "4.0.1", "vuedraggable": "4.0.1",
"websocket": "1.0.34", "websocket": "1.0.34",
"ws": "8.8.0" "ws": "8.8.0"

View file

@ -1,11 +1,11 @@
import { del, get, set } from '@/scripts/idb-proxy';
import { defineAsyncComponent, reactive } from 'vue'; import { defineAsyncComponent, reactive } from 'vue';
import * as misskey from 'misskey-js'; 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 { apiUrl } from '@/config';
import { waiting, api, popup, popupMenu, success, alert } from '@/os'; import { waiting, api, popup, popupMenu, success, alert } from '@/os';
import { unisonReload, reloadChannel } from '@/scripts/unison-reload'; import { unisonReload, reloadChannel } from '@/scripts/unison-reload';
import { showSuspendedDialog } from './scripts/show-suspended-dialog';
import { i18n } from './i18n';
// TODO: 他のタブと永続化されたstateを同期 // TODO: 他のタブと永続化されたstateを同期
@ -22,13 +22,7 @@ export async function signout() {
waiting(); waiting();
localStorage.removeItem('account'); localStorage.removeItem('account');
//#region Remove account await removeAccount($i.id);
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
//#region Remove service worker registration //#region Remove service worker registration
try { try {
@ -55,7 +49,7 @@ export async function signout() {
} catch (err) {} } catch (err) {}
//#endregion //#endregion
document.cookie = `igi=; path=/`; document.cookie = 'igi=; path=/';
if (accounts.length > 0) login(accounts[0].token); if (accounts.length > 0) login(accounts[0].token);
else unisonReload('/'); 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> { function fetchAccount(token: string): Promise<Account> {
return new Promise((done, fail) => { return new Promise((done, fail) => {
// Fetch user // Fetch user
fetch(`${apiUrl}/i`, { fetch(`${apiUrl}/i`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
i: token i: token,
}) }),
}) })
.then(res => res.json()) .then(res => res.json())
.then(res => { .then(res => {
@ -216,13 +218,13 @@ export async function openAccountMenu(opts: {
type: 'link', type: 'link',
icon: 'fas fa-users', icon: 'fas fa-users',
text: i18n.ts.manageAccounts, text: i18n.ts.manageAccounts,
to: `/settings/accounts`, to: '/settings/accounts',
}]], ev.currentTarget ?? ev.target, { }]], ev.currentTarget ?? ev.target, {
align: 'left' align: 'left',
}); });
} else { } else {
popupMenu([...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises], ev.currentTarget ?? ev.target, { popupMenu([...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises], ev.currentTarget ?? ev.target, {
align: 'left' align: 'left',
}); });
} }
} }

View file

@ -13,7 +13,7 @@
id-denylist violation when setting it. This is causing about 60+ lint issues. 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. 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 { import {
Chart, Chart,
ArcElement, ArcElement,
@ -53,7 +53,7 @@ const props = defineProps({
limit: { limit: {
type: Number, type: Number,
required: false, required: false,
default: 90 default: 90,
}, },
span: { span: {
type: String as PropType<'hour' | 'day'>, type: String as PropType<'hour' | 'day'>,
@ -62,22 +62,22 @@ const props = defineProps({
detailed: { detailed: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false default: false,
}, },
stacked: { stacked: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false default: false,
}, },
bar: { bar: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false default: false,
}, },
aspectRatio: { aspectRatio: {
type: Number, type: Number,
required: false, required: false,
default: null default: null,
}, },
}); });
@ -156,7 +156,7 @@ const getDate = (ago: number) => {
const format = (arr) => { const format = (arr) => {
return arr.map((v, i) => ({ return arr.map((v, i) => ({
x: getDate(i).getTime(), x: getDate(i).getTime(),
y: v y: v,
})); }));
}; };
@ -343,7 +343,7 @@ const render = () => {
min: 'original', min: 'original',
max: 'original', max: 'original',
}, },
} },
} : undefined, } : undefined,
//gradient, //gradient,
}, },
@ -367,8 +367,8 @@ const render = () => {
ctx.stroke(); ctx.stroke();
ctx.restore(); ctx.restore();
} }
} },
}] }],
}); });
}; };
@ -433,18 +433,18 @@ const fetchApRequestChart = async (): Promise<typeof chartData> => {
name: 'In', name: 'In',
type: 'area', type: 'area',
color: '#008FFB', color: '#008FFB',
data: format(raw.inboxReceived) data: format(raw.inboxReceived),
}, { }, {
name: 'Out (succ)', name: 'Out (succ)',
type: 'area', type: 'area',
color: '#00E396', color: '#00E396',
data: format(raw.deliverSucceeded) data: format(raw.deliverSucceeded),
}, { }, {
name: 'Out (fail)', name: 'Out (fail)',
type: 'area', type: 'area',
color: '#FEB019', color: '#FEB019',
data: format(raw.deliverFailed) data: format(raw.deliverFailed),
}] }],
}; };
}; };
@ -456,7 +456,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => {
type: 'line', type: 'line',
data: format(type === 'combined' data: format(type === 'combined'
? 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))
: sum(raw[type].inc, negate(raw[type].dec)) : sum(raw[type].inc, negate(raw[type].dec)),
), ),
color: '#888888', color: '#888888',
}, { }, {
@ -464,7 +464,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => {
type: 'area', type: 'area',
data: format(type === 'combined' data: format(type === 'combined'
? sum(raw.local.diffs.renote, raw.remote.diffs.renote) ? sum(raw.local.diffs.renote, raw.remote.diffs.renote)
: raw[type].diffs.renote : raw[type].diffs.renote,
), ),
color: colors.green, color: colors.green,
}, { }, {
@ -472,7 +472,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => {
type: 'area', type: 'area',
data: format(type === 'combined' data: format(type === 'combined'
? sum(raw.local.diffs.reply, raw.remote.diffs.reply) ? sum(raw.local.diffs.reply, raw.remote.diffs.reply)
: raw[type].diffs.reply : raw[type].diffs.reply,
), ),
color: colors.yellow, color: colors.yellow,
}, { }, {
@ -480,7 +480,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => {
type: 'area', type: 'area',
data: format(type === 'combined' data: format(type === 'combined'
? sum(raw.local.diffs.normal, raw.remote.diffs.normal) ? sum(raw.local.diffs.normal, raw.remote.diffs.normal)
: raw[type].diffs.normal : raw[type].diffs.normal,
), ),
color: colors.blue, color: colors.blue,
}, { }, {
@ -488,7 +488,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => {
type: 'area', type: 'area',
data: format(type === 'combined' data: format(type === 'combined'
? sum(raw.local.diffs.withFile, raw.remote.diffs.withFile) ? sum(raw.local.diffs.withFile, raw.remote.diffs.withFile)
: raw[type].diffs.withFile : raw[type].diffs.withFile,
), ),
color: colors.purple, color: colors.purple,
}], }],
@ -522,21 +522,21 @@ const fetchUsersChart = async (total: boolean): Promise<typeof chartData> => {
type: 'line', type: 'line',
data: format(total data: format(total
? sum(raw.local.total, raw.remote.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', name: 'Local',
type: 'area', type: 'area',
data: format(total data: format(total
? raw.local.total ? raw.local.total
: sum(raw.local.inc, negate(raw.local.dec)) : sum(raw.local.inc, negate(raw.local.dec)),
), ),
}, { }, {
name: 'Remote', name: 'Remote',
type: 'area', type: 'area',
data: format(total data: format(total
? raw.remote.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, raw.local.incSize,
negate(raw.local.decSize), negate(raw.local.decSize),
raw.remote.incSize, raw.remote.incSize,
negate(raw.remote.decSize) negate(raw.remote.decSize),
) ),
), ),
}, { }, {
name: 'Local +', name: 'Local +',
@ -642,8 +642,8 @@ const fetchDriveFilesChart = async (): Promise<typeof chartData> => {
raw.local.incCount, raw.local.incCount,
negate(raw.local.decCount), negate(raw.local.decCount),
raw.remote.incCount, raw.remote.incCount,
negate(raw.remote.decCount) negate(raw.remote.decCount),
) ),
), ),
}, { }, {
name: 'Local +', name: 'Local +',
@ -672,18 +672,18 @@ const fetchInstanceRequestsChart = async (): Promise<typeof chartData> => {
name: 'In', name: 'In',
type: 'area', type: 'area',
color: '#008FFB', color: '#008FFB',
data: format(raw.requests.received) data: format(raw.requests.received),
}, { }, {
name: 'Out (succ)', name: 'Out (succ)',
type: 'area', type: 'area',
color: '#00E396', color: '#00E396',
data: format(raw.requests.succeeded) data: format(raw.requests.succeeded),
}, { }, {
name: 'Out (fail)', name: 'Out (fail)',
type: 'area', type: 'area',
color: '#FEB019', 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', color: '#008FFB',
data: format(total data: format(total
? raw.users.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', color: '#008FFB',
data: format(total data: format(total
? raw.notes.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', color: '#008FFB',
data: format(total data: format(total
? raw.following.total ? raw.following.total
: sum(raw.following.inc, negate(raw.following.dec)) : sum(raw.following.inc, negate(raw.following.dec)),
) ),
}, { }, {
name: 'Followers', name: 'Followers',
type: 'area', type: 'area',
color: '#00E396', color: '#00E396',
data: format(total data: format(total
? raw.followers.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', color: '#008FFB',
data: format(total data: format(total
? raw.drive.totalUsage ? 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', color: '#008FFB',
data: format(total data: format(total
? raw.drive.totalFiles ? raw.drive.totalFiles
: sum(raw.drive.incFiles, negate(raw.drive.decFiles)) : sum(raw.drive.incFiles, negate(raw.drive.decFiles)),
) ),
}] }],
}; };
}; };

View file

@ -57,7 +57,7 @@ const isThumbnailAvailable = computed(() => {
.zdjebgpv { .zdjebgpv {
position: relative; position: relative;
display: flex; display: flex;
background: #e1e1e1; background: var(--panel);
border-radius: 8px; border-radius: 8px;
overflow: clip; overflow: clip;

View file

@ -9,13 +9,13 @@
<i v-else class="fas fa-angle-down icon"></i> <i v-else class="fas fa-angle-down icon"></i>
</span> </span>
</div> </div>
<keep-alive> <KeepAlive>
<div v-if="openedAtLeastOnce" v-show="opened" class="body"> <div v-if="openedAtLeastOnce" v-show="opened" class="body">
<MkSpacer :margin-min="14" :margin-max="22"> <MkSpacer :margin-min="14" :margin-max="22">
<slot></slot> <slot></slot>
</MkSpacer> </MkSpacer>
</div> </div>
</keep-alive> </KeepAlive>
</div> </div>
</template> </template>

View file

@ -5,13 +5,13 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { inject } from 'vue';
import * as os from '@/os'; import * as os from '@/os';
import copyToClipboard from '@/scripts/copy-to-clipboard'; import copyToClipboard from '@/scripts/copy-to-clipboard';
import { router } from '@/router';
import { url } from '@/config'; import { url } from '@/config';
import { popout as popout_ } from '@/scripts/popout'; import { popout as popout_ } from '@/scripts/popout';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { MisskeyNavigator } from '@/scripts/navigate'; import { useRouter } from '@/router';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
to: string; to: string;
@ -22,15 +22,16 @@ const props = withDefaults(defineProps<{
behavior: null, behavior: null,
}); });
const mkNav = new MisskeyNavigator(); const router = useRouter();
const active = $computed(() => { const active = $computed(() => {
if (props.activeClass == null) return false; if (props.activeClass == null) return false;
const resolved = router.resolve(props.to); const resolved = router.resolve(props.to);
if (resolved.path === router.currentRoute.value.path) return true; if (resolved == null) return false;
if (resolved.name == 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; 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) { function onContextmenu(ev) {
@ -44,31 +45,25 @@ function onContextmenu(ev) {
text: i18n.ts.openInWindow, text: i18n.ts.openInWindow,
action: () => { action: () => {
os.pageWindow(props.to); 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', icon: 'fas fa-expand-alt',
text: i18n.ts.showInPage, text: i18n.ts.showInPage,
action: () => { action: () => {
router.push(props.to); router.push(props.to);
} },
}, null, { }, null, {
icon: 'fas fa-external-link-alt', icon: 'fas fa-external-link-alt',
text: i18n.ts.openInNewTab, text: i18n.ts.openInNewTab,
action: () => { action: () => {
window.open(props.to, '_blank'); window.open(props.to, '_blank');
} },
}, { }, {
icon: 'fas fa-link', icon: 'fas fa-link',
text: i18n.ts.copyLink, text: i18n.ts.copyLink,
action: () => { action: () => {
copyToClipboard(`${url}${props.to}`); copyToClipboard(`${url}${props.to}`);
} },
}], ev); }], ev);
} }
@ -98,6 +93,6 @@ function nav() {
} }
} }
mkNav.push(props.to); router.push(props.to);
} }
</script> </script>

View file

@ -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>

View 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>

View 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>

View file

@ -10,15 +10,17 @@ import MkEllipsis from './global/ellipsis.vue';
import MkTime from './global/time.vue'; import MkTime from './global/time.vue';
import MkUrl from './global/url.vue'; import MkUrl from './global/url.vue';
import I18n from './global/i18n'; import I18n from './global/i18n';
import RouterView from './global/router-view.vue';
import MkLoading from './global/loading.vue'; import MkLoading from './global/loading.vue';
import MkError from './global/error.vue'; import MkError from './global/error.vue';
import MkAd from './global/ad.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 MkSpacer from './global/spacer.vue';
import MkStickyContainer from './global/sticky-container.vue'; import MkStickyContainer from './global/sticky-container.vue';
export default function(app: App) { export default function(app: App) {
app.component('I18n', I18n); app.component('I18n', I18n);
app.component('RouterView', RouterView);
app.component('Mfm', Mfm); app.component('Mfm', Mfm);
app.component('MkA', MkA); app.component('MkA', MkA);
app.component('MkAcct', MkAcct); app.component('MkAcct', MkAcct);
@ -31,7 +33,7 @@ export default function(app: App) {
app.component('MkLoading', MkLoading); app.component('MkLoading', MkLoading);
app.component('MkError', MkError); app.component('MkError', MkError);
app.component('MkAd', MkAd); app.component('MkAd', MkAd);
app.component('MkHeader', MkHeader); app.component('MkPageHeader', MkPageHeader);
app.component('MkSpacer', MkSpacer); app.component('MkSpacer', MkSpacer);
app.component('MkStickyContainer', MkStickyContainer); app.component('MkStickyContainer', MkStickyContainer);
} }
@ -39,6 +41,7 @@ export default function(app: App) {
declare module '@vue/runtime-core' { declare module '@vue/runtime-core' {
export interface GlobalComponents { export interface GlobalComponents {
I18n: typeof I18n; I18n: typeof I18n;
RouterView: typeof RouterView;
Mfm: typeof Mfm; Mfm: typeof Mfm;
MkA: typeof MkA; MkA: typeof MkA;
MkAcct: typeof MkAcct; MkAcct: typeof MkAcct;
@ -51,7 +54,7 @@ declare module '@vue/runtime-core' {
MkLoading: typeof MkLoading; MkLoading: typeof MkLoading;
MkError: typeof MkError; MkError: typeof MkError;
MkAd: typeof MkAd; MkAd: typeof MkAd;
MkHeader: typeof MkHeader; MkPageHeader: typeof MkPageHeader;
MkSpacer: typeof MkSpacer; MkSpacer: typeof MkSpacer;
MkStickyContainer: typeof MkStickyContainer; MkStickyContainer: typeof MkStickyContainer;
} }

View file

@ -1,163 +1,118 @@
<template> <template>
<MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')"> <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"> <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> <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-else style="display: inline-block; width: 20px"></span>
<span v-if="pageInfo" class="title"> <span v-if="pageMetadata?.value" class="title">
<i v-if="pageInfo.icon" class="icon" :class="pageInfo.icon"></i> <i v-if="pageMetadata?.value.icon" class="icon" :class="pageMetadata?.value.icon"></i>
<span>{{ pageInfo.title }}</span> <span>{{ pageMetadata?.value.title }}</span>
</span> </span>
<button class="_button" @click="$refs.modal.close()"><i class="fas fa-times"></i></button> <button class="_button" @click="$refs.modal.close()"><i class="fas fa-times"></i></button>
</div> </div>
<div class="body"> <div class="body">
<MkStickyContainer> <MkStickyContainer>
<template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template> <template #header><MkPageHeader v-if="pageMetadata?.value && !pageMetadata?.value.hideHeader" :info="pageMetadata?.value"/></template>
<keep-alive> <RouterView :router="router"/>
<component :is="component" v-bind="props" :ref="changePage"/>
</keep-alive>
</MkStickyContainer> </MkStickyContainer>
</div> </div>
</div> </div>
</MkModal> </MkModal>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from 'vue'; import { ComputedRef, provide } from 'vue';
import MkModal from '@/components/ui/modal.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 copyToClipboard from '@/scripts/copy-to-clipboard';
import { resolve } from '@/router';
import { url } from '@/config'; import { url } from '@/config';
import * as symbols from '@/symbols';
import * as os from '@/os'; 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({ const props = defineProps<{
components: { initialPath: string;
MkModal, }>();
},
inject: { defineEmits<{
sideViewHook: { (ev: 'closed'): void;
default: null, (ev: 'click'): void;
}, }>();
},
provide() { const router = new Router(routes, props.initialPath);
return {
navHook: (path) => {
this.navigate(path);
},
shouldHeaderThin: true,
};
},
props: { router.addListener('push', ctx => {
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);
},
},
}); });
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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -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 { function onContextmenu(ev: MouseEvent): void {
const isLink = (el: HTMLElement) => { const isLink = (el: HTMLElement) => {

View file

@ -1,186 +1,135 @@
<template> <template>
<XWindow ref="window" <XWindow
ref="windowEl"
:initial-width="500" :initial-width="500"
:initial-height="500" :initial-height="500"
:can-resize="true" :can-resize="true"
:close-button="true" :close-button="true"
:buttons-left="buttonsLeft"
:buttons-right="buttonsRight"
:contextmenu="contextmenu" :contextmenu="contextmenu"
@closed="$emit('closed')" @closed="$emit('closed')"
> >
<template #header> <template #header>
<template v-if="pageInfo"> <template v-if="pageMetadata?.value">
<i v-if="pageInfo.icon" class="icon" :class="pageInfo.icon" style="margin-right: 0.5em;"></i> <i v-if="pageMetadata.value.icon" class="icon" :class="pageMetadata.value.icon" style="margin-right: 0.5em;"></i>
<span>{{ pageInfo.title }}</span> <span>{{ pageMetadata.value.title }}</span>
</template> </template>
</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 }"> <div class="yrolvcoq" :style="{ background: pageMetadata?.value?.bg }">
<MkStickyContainer> <RouterView :router="router"/>
<template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template>
<component :is="component" v-bind="props" :ref="changePage"/>
</MkStickyContainer>
</div> </div>
</XWindow> </XWindow>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from 'vue'; import { ComputedRef, inject, provide } from 'vue';
import RouterView from './global/router-view.vue';
import XWindow from '@/components/ui/window.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 copyToClipboard from '@/scripts/copy-to-clipboard';
import { resolve } from '@/router';
import { url } from '@/config'; import { url } from '@/config';
import * as symbols from '@/symbols';
import * as os from '@/os'; 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({ const props = defineProps<{
components: { initialPath: string;
XWindow, }>();
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: { icon: 'fas fa-link',
sideViewHook: { text: i18n.ts.copyLink,
default: null action: () => {
} copyToClipboard(url + router.getCurrentPath());
}, },
}]));
provide() { function menu(ev) {
return { os.popupMenu(contextmenu, ev.currentTarget ?? ev.target);
navHook: (path) => { }
this.navigate(path);
},
shouldHeaderThin: true,
};
},
props: { function back() {
initialPath: { history.pop();
type: String, router.change(history[history.length - 1]);
required: true, }
},
initialComponent: {
type: Object,
required: true,
},
initialProps: {
type: Object,
required: false,
default: () => {},
},
},
emits: ['closed'], function close() {
windowEl.close();
}
data() { function expand() {
return { mainRouter.push(router.getCurrentPath());
pageInfo: null, windowEl.close();
path: this.initialPath, }
component: this.initialComponent,
props: this.initialProps,
history: [],
};
},
computed: { function popout() {
url(): string { _popout(router.getCurrentPath(), windowEl.$el);
return url + this.path; windowEl.close();
}, }
contextmenu() { defineExpose({
return [{ close,
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();
},
},
}); });
</script> </script>

View file

@ -4,14 +4,14 @@
<div class="body _window _shadow _narrow_" @mousedown="onBodyMousedown" @keydown="onKeydown"> <div class="body _window _shadow _narrow_" @mousedown="onBodyMousedown" @keydown="onKeydown">
<div class="header" :class="{ mini }" @contextmenu.prevent.stop="onContextmenu"> <div class="header" :class="{ mini }" @contextmenu.prevent.stop="onContextmenu">
<span class="left"> <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>
<span class="title" @mousedown.prevent="onHeaderMousedown" @touchstart.prevent="onHeaderMousedown"> <span class="title" @mousedown.prevent="onHeaderMousedown" @touchstart.prevent="onHeaderMousedown">
<slot name="header"></slot> <slot name="header"></slot>
</span> </span>
<span class="right"> <span class="right">
<slot name="headerRight"></slot> <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" @click="close()"><i class="fas fa-times"></i></button> <button v-if="closeButton" class="button _button" @click="close()"><i class="fas fa-times"></i></button>
</span> </span>
</div> </div>
<div v-if="padding" class="body"> <div v-if="padding" class="body">
@ -46,41 +46,41 @@ const minHeight = 50;
const minWidth = 250; const minWidth = 250;
function dragListen(fn) { function dragListen(fn) {
window.addEventListener('mousemove', fn); window.addEventListener('mousemove', fn);
window.addEventListener('touchmove', fn); window.addEventListener('touchmove', fn);
window.addEventListener('mouseleave', dragClear.bind(null, fn)); window.addEventListener('mouseleave', dragClear.bind(null, fn));
window.addEventListener('mouseup', dragClear.bind(null, fn)); window.addEventListener('mouseup', dragClear.bind(null, fn));
window.addEventListener('touchend', dragClear.bind(null, fn)); window.addEventListener('touchend', dragClear.bind(null, fn));
} }
function dragClear(fn) { function dragClear(fn) {
window.removeEventListener('mousemove', fn); window.removeEventListener('mousemove', fn);
window.removeEventListener('touchmove', fn); window.removeEventListener('touchmove', fn);
window.removeEventListener('mouseleave', dragClear); window.removeEventListener('mouseleave', dragClear);
window.removeEventListener('mouseup', dragClear); window.removeEventListener('mouseup', dragClear);
window.removeEventListener('touchend', dragClear); window.removeEventListener('touchend', dragClear);
} }
export default defineComponent({ export default defineComponent({
provide: { provide: {
inWindow: true inWindow: true,
}, },
props: { props: {
padding: { padding: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false default: false,
}, },
initialWidth: { initialWidth: {
type: Number, type: Number,
required: false, required: false,
default: 400 default: 400,
}, },
initialHeight: { initialHeight: {
type: Number, type: Number,
required: false, required: false,
default: null default: null,
}, },
canResize: { canResize: {
type: Boolean, type: Boolean,
@ -105,7 +105,17 @@ export default defineComponent({
contextmenu: { contextmenu: {
type: Array, type: Array,
required: false, required: false,
} },
buttonsLeft: {
type: Array,
required: false,
default: [],
},
buttonsRight: {
type: Array,
required: false,
default: [],
},
}, },
emits: ['closed'], emits: ['closed'],
@ -162,7 +172,10 @@ export default defineComponent({
this.top(); this.top();
}, },
onHeaderMousedown(evt) { onHeaderMousedown(evt: MouseEvent) {
//
if (evt.button === 2) return;
const main = this.$el as any; const main = this.$el as any;
if (!contains(main, document.activeElement)) main.focus(); if (!contains(main, document.activeElement)) main.focus();
@ -356,12 +369,12 @@ export default defineComponent({
const browserHeight = window.innerHeight; const browserHeight = window.innerHeight;
const windowWidth = main.offsetWidth; const windowWidth = main.offsetWidth;
const windowHeight = main.offsetHeight; const windowHeight = main.offsetHeight;
if (position.left < 0) main.style.left = 0; // if (position.left < 0) main.style.left = 0; //
if (position.top + windowHeight > browserHeight) main.style.top = browserHeight - windowHeight + 'px'; // if (position.top + windowHeight > browserHeight) main.style.top = browserHeight - windowHeight + 'px'; //
if (position.left + windowWidth > browserWidth) main.style.left = browserWidth - windowWidth + 'px'; // if (position.left + windowWidth > browserWidth) main.style.left = browserWidth - windowWidth + 'px'; //
if (position.top < 0) main.style.top = 0; // if (position.top < 0) main.style.top = 0; //
} },
} },
}); });
</script> </script>
@ -404,17 +417,25 @@ export default defineComponent({
border-bottom: solid 1px var(--divider); border-bottom: solid 1px var(--divider);
> .left, > .right { > .left, > .right {
> ::v-deep(button) { > .button {
height: var(--height); height: var(--height);
width: var(--height); width: var(--height);
&:hover { &:hover {
color: var(--fgHighlighted); color: var(--fgHighlighted);
} }
&.highlighted {
color: var(--accent);
}
} }
} }
> .left { > .left {
margin-right: 16px;
}
> .right {
min-width: 16px; min-width: 16px;
} }

View file

@ -21,7 +21,6 @@ import widgets from '@/widgets';
import directives from '@/directives'; import directives from '@/directives';
import components from '@/components'; import components from '@/components';
import { version, ui, lang, host } from '@/config'; import { version, ui, lang, host } from '@/config';
import { router } from '@/router';
import { applyTheme } from '@/scripts/theme'; import { applyTheme } from '@/scripts/theme';
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode'; import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
@ -170,11 +169,10 @@ fetchInstanceMetaPromise.then(() => {
const app = createApp( const app = createApp(
window.location.search === '?zen' ? defineAsyncComponent(() => import('@/ui/zen.vue')) : window.location.search === '?zen' ? defineAsyncComponent(() => import('@/ui/zen.vue')) :
!$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) : !$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) :
ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) : ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) :
ui === 'desktop' ? defineAsyncComponent(() => import('@/ui/desktop.vue')) : ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) :
ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) : defineAsyncComponent(() => import('@/ui/universal.vue')),
defineAsyncComponent(() => import('@/ui/universal.vue'))
); );
if (_DEV_) { if (_DEV_) {
@ -189,14 +187,10 @@ app.config.globalProperties = {
$ts: i18n.ts, $ts: i18n.ts,
}; };
app.use(router);
widgets(app); widgets(app);
directives(app); directives(app);
components(app); components(app);
await router.isReady();
const splash = document.getElementById('splash'); const splash = document.getElementById('splash');
// 念のためnullチェック(HTMLが古い場合があるため(そのうち消す)) // 念のためnullチェック(HTMLが古い場合があるため(そのうち消す))
if (splash) splash.addEventListener('transitionend', () => { if (splash) splash.addEventListener('transitionend', () => {

View file

@ -1,11 +1,11 @@
import { computed, ref, reactive } from 'vue'; import { computed, ref, reactive } from 'vue';
import { $i } from './account';
import { mainRouter } from '@/router';
import { search } from '@/scripts/search'; import { search } from '@/scripts/search';
import * as os from '@/os'; import * as os from '@/os';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { ui } from '@/config'; import { ui } from '@/config';
import { $i } from './account';
import { unisonReload } from '@/scripts/unison-reload'; import { unisonReload } from '@/scripts/unison-reload';
import { router } from './router';
export const menuDef = reactive({ export const menuDef = reactive({
notifications: { notifications: {
@ -60,16 +60,16 @@ export const menuDef = reactive({
title: 'lists', title: 'lists',
icon: 'fas fa-list-ul', icon: 'fas fa-list-ul',
show: computed(() => $i != null), 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) => { action: (ev) => {
const items = ref([{ const items = ref([{
type: 'pending' type: 'pending',
}]); }]);
os.api('users/lists/list').then(lists => { os.api('users/lists/list').then(lists => {
const _items = [...lists.map(list => ({ const _items = [...lists.map(list => ({
type: 'link', type: 'link',
text: list.name, text: list.name,
to: `/timeline/list/${list.id}` to: `/timeline/list/${list.id}`,
})), null, { })), null, {
type: 'link', type: 'link',
to: '/my/lists', to: '/my/lists',
@ -91,16 +91,16 @@ export const menuDef = reactive({
title: 'antennas', title: 'antennas',
icon: 'fas fa-satellite', icon: 'fas fa-satellite',
show: computed(() => $i != null), 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) => { action: (ev) => {
const items = ref([{ const items = ref([{
type: 'pending' type: 'pending',
}]); }]);
os.api('antennas/list').then(antennas => { os.api('antennas/list').then(antennas => {
const _items = [...antennas.map(antenna => ({ const _items = [...antennas.map(antenna => ({
type: 'link', type: 'link',
text: antenna.name, text: antenna.name,
to: `/timeline/antenna/${antenna.id}` to: `/timeline/antenna/${antenna.id}`,
})), null, { })), null, {
type: 'link', type: 'link',
to: '/my/antennas', to: '/my/antennas',
@ -178,29 +178,22 @@ export const menuDef = reactive({
action: () => { action: () => {
localStorage.setItem('ui', 'default'); localStorage.setItem('ui', 'default');
unisonReload(); unisonReload();
} },
}, { }, {
text: i18n.ts.deck, text: i18n.ts.deck,
active: ui === 'deck', active: ui === 'deck',
action: () => { action: () => {
localStorage.setItem('ui', 'deck'); localStorage.setItem('ui', 'deck');
unisonReload(); unisonReload();
} },
}, { }, {
text: i18n.ts.classic, text: i18n.ts.classic,
active: ui === 'classic', active: ui === 'classic',
action: () => { action: () => {
localStorage.setItem('ui', 'classic'); localStorage.setItem('ui', 'classic');
unisonReload(); unisonReload();
} },
}, /*{ }], ev.currentTarget ?? ev.target);
text: i18n.ts.desktop + ' (β)',
active: ui === 'desktop',
action: () => {
localStorage.setItem('ui', 'desktop');
unisonReload();
}
}*/], ev.currentTarget ?? ev.target);
}, },
}, },
}); });

View 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);
}
}

View file

@ -8,7 +8,6 @@ import { apiUrl, url } from '@/config';
import MkPostFormDialog from '@/components/post-form-dialog.vue'; import MkPostFormDialog from '@/components/post-form-dialog.vue';
import MkWaitingDialog from '@/components/waiting-dialog.vue'; import MkWaitingDialog from '@/components/waiting-dialog.vue';
import { MenuItem } from '@/types/menu'; import { MenuItem } from '@/types/menu';
import { resolve } from '@/router';
import { $i } from '@/account'; import { $i } from '@/account';
export const pendingApiRequestsCount = ref(0); 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) { export function pageWindow(path: string) {
const { component, props } = resolve(path);
popup(defineAsyncComponent(() => import('@/components/page-window.vue')), { popup(defineAsyncComponent(() => import('@/components/page-window.vue')), {
initialPath: path, initialPath: path,
initialComponent: markRaw(component),
initialProps: props,
}, {}, 'closed'); }, {}, 'closed');
} }
export function modalPageWindow(path: string) { export function modalPageWindow(path: string) {
const { component, props } = resolve(path);
popup(defineAsyncComponent(() => import('@/components/modal-page-window.vue')), { popup(defineAsyncComponent(() => import('@/components/modal-page-window.vue')), {
initialPath: path, initialPath: path,
initialComponent: markRaw(component),
initialProps: props,
}, {}, 'closed'); }, {}, 'closed');
} }

View file

@ -21,11 +21,11 @@
import { } from 'vue'; import { } from 'vue';
import * as misskey from 'misskey-js'; import * as misskey from 'misskey-js';
import MkButton from '@/components/ui/button.vue'; import MkButton from '@/components/ui/button.vue';
import * as symbols from '@/symbols';
import { version } from '@/config'; import { version } from '@/config';
import * as os from '@/os'; import * as os from '@/os';
import { unisonReload } from '@/scripts/unison-reload'; import { unisonReload } from '@/scripts/unison-reload';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
error?: Error; error?: Error;
@ -52,11 +52,13 @@ function reload() {
unisonReload(); unisonReload();
} }
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: {
title: i18n.ts.error, const headerTabs = $computed(() => []);
icon: 'fas fa-exclamation-triangle',
}, definePageMetadata({
title: i18n.ts.error,
icon: 'fas fa-exclamation-triangle',
}); });
</script> </script>

View file

@ -1,62 +1,65 @@
<template> <template>
<div style="overflow: clip;"> <MkStickyContainer>
<MkSpacer :content-max="600" :margin-min="20"> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<div class="_formRoot znqjceqz"> <div style="overflow: clip;">
<div id="debug"></div> <MkSpacer :content-max="600" :margin-min="20">
<div ref="containerEl" v-panel class="_formBlock about" :class="{ playing: easterEggEngine != null }"> <div class="_formRoot znqjceqz">
<img src="/client-assets/about-icon.png" alt="" class="icon" draggable="false" @load="iconLoaded" @click="gravity"/> <div id="debug"></div>
<div class="misskey">Misskey</div> <div ref="containerEl" v-panel class="_formBlock about" :class="{ playing: easterEggEngine != null }">
<div class="version">v{{ version }}</div> <img src="/client-assets/about-icon.png" alt="" class="icon" draggable="false" @load="iconLoaded" @click="gravity"/>
<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 class="misskey">Misskey</div>
</div> <div class="version">v{{ version }}</div>
<div class="_formBlock" style="text-align: center;"> <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>
{{ 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>
</div> </div>
</FormSection> <div class="_formBlock" style="text-align: center;">
<FormSection> {{ i18n.ts._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ i18n.ts.learnMore }}</a>
<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> </div>
<template #caption><MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ i18n.ts._aboutMisskey.allContributors }}</MkLink></template> <div class="_formBlock" style="text-align: center;">
</FormSection> <MkButton primary rounded inline @click="iLoveMisskey">I <Mfm text="$[jelly ❤]"/> #Misskey</MkButton>
<FormSection> </div>
<template #label><Mfm text="$[jelly ❤]"/> {{ i18n.ts._aboutMisskey.patrons }}</template> <FormSection>
<div v-for="patron in patrons" :key="patron">{{ patron }}</div> <div class="_formLinks">
<template #caption>{{ i18n.ts._aboutMisskey.morePatrons }}</template> <FormLink to="https://github.com/misskey-dev/misskey" external>
</FormSection> <template #icon><i class="fas fa-code"></i></template>
</div> {{ i18n.ts._aboutMisskey.source }}
</MkSpacer> <template #suffix>GitHub</template>
</div> </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> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -67,10 +70,10 @@ import FormSection from '@/components/form/section.vue';
import MkButton from '@/components/ui/button.vue'; import MkButton from '@/components/ui/button.vue';
import MkLink from '@/components/link.vue'; import MkLink from '@/components/link.vue';
import { physics } from '@/scripts/physics'; import { physics } from '@/scripts/physics';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
import * as os from '@/os'; import * as os from '@/os';
import { definePageMetadata } from '@/scripts/page-metadata';
const patrons = [ const patrons = [
'まっちゃとーにゅ', 'まっちゃとーにゅ',
@ -194,12 +197,14 @@ onBeforeUnmount(() => {
} }
}); });
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: {
title: i18n.ts.aboutMisskey, const headerTabs = $computed(() => []);
icon: null,
bg: 'var(--bg)', definePageMetadata({
}, title: i18n.ts.aboutMisskey,
icon: null,
bg: 'var(--bg)',
}); });
</script> </script>

View file

@ -1,78 +1,81 @@
<template> <template>
<MkSpacer v-if="tab === 'overview'" :content-max="600" :margin-min="20"> <MkStickyContainer>
<div class="_formRoot"> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<div class="_formBlock fwhjspax" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }"> <MkSpacer v-if="tab === 'overview'" :content-max="600" :margin-min="20">
<div class="content"> <div class="_formRoot">
<img :src="$instance.iconUrl || $instance.faviconUrl || '/favicon.ico'" alt="" class="icon"/> <div class="_formBlock fwhjspax" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }">
<div class="name"> <div class="content">
<b>{{ $instance.name || host }}</b> <img :src="$instance.iconUrl || $instance.faviconUrl || '/favicon.ico'" alt="" class="icon"/>
<div class="name">
<b>{{ $instance.name || host }}</b>
</div>
</div> </div>
</div> </div>
</div>
<MkKeyValue class="_formBlock"> <MkKeyValue class="_formBlock">
<template #key>{{ $ts.description }}</template> <template #key>{{ $ts.description }}</template>
<template #value>{{ $instance.description }}</template> <template #value>{{ $instance.description }}</template>
</MkKeyValue>
<FormSection>
<MkKeyValue class="_formBlock" :copy="version">
<template #key>Misskey</template>
<template #value>{{ version }}</template>
</MkKeyValue> </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> <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> <FormSplit>
<MkKeyValue class="_formBlock"> <MkKeyValue class="_formBlock">
<template #key>{{ $ts.users }}</template> <template #key>{{ $ts.administrator }}</template>
<template #value>{{ number(stats.originalUsersCount) }}</template> <template #value>{{ $instance.maintainerName }}</template>
</MkKeyValue> </MkKeyValue>
<MkKeyValue class="_formBlock"> <MkKeyValue class="_formBlock">
<template #key>{{ $ts.notes }}</template> <template #key>{{ $ts.contact }}</template>
<template #value>{{ number(stats.originalNotesCount) }}</template> <template #value>{{ $instance.maintainerEmail }}</template>
</MkKeyValue> </MkKeyValue>
</FormSplit> </FormSplit>
<FormLink v-if="$instance.tosUrl" :to="$instance.tosUrl" class="_formBlock" external>{{ $ts.tos }}</FormLink>
</FormSection> </FormSection>
</FormSuspense>
<FormSection> <FormSuspense :p="initStats">
<template #label>Well-known resources</template> <FormSection>
<div class="_formLinks"> <template #label>{{ $ts.statistics }}</template>
<FormLink :to="`/.well-known/host-meta`" external>host-meta</FormLink> <FormSplit>
<FormLink :to="`/.well-known/host-meta.json`" external>host-meta.json</FormLink> <MkKeyValue class="_formBlock">
<FormLink :to="`/.well-known/nodeinfo`" external>nodeinfo</FormLink> <template #key>{{ $ts.users }}</template>
<FormLink :to="`/robots.txt`" external>robots.txt</FormLink> <template #value>{{ number(stats.originalUsersCount) }}</template>
<FormLink :to="`/manifest.json`" external>manifest.json</FormLink> </MkKeyValue>
</div> <MkKeyValue class="_formBlock">
</FormSection> <template #key>{{ $ts.notes }}</template>
</div> <template #value>{{ number(stats.originalNotesCount) }}</template>
</MkSpacer> </MkKeyValue>
<MkSpacer v-else-if="tab === 'charts'" :content-max="1200" :margin-min="20"> </FormSplit>
<MkInstanceStats :chart-limit="500" :detailed="true"/> </FormSection>
</MkSpacer> </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> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { version, instanceName } from '@/config'; import { version, instanceName , host } from '@/config';
import FormLink from '@/components/form/link.vue'; import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue'; import FormSection from '@/components/form/section.vue';
import FormSuspense from '@/components/form/suspense.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 MkInstanceStats from '@/components/instance-stats.vue';
import * as os from '@/os'; import * as os from '@/os';
import number from '@/filters/number'; import number from '@/filters/number';
import * as symbols from '@/symbols';
import { host } from '@/config';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let stats = $ref(null); let stats = $ref(null);
let tab = $ref('overview'); let tab = $ref('overview');
@ -93,23 +95,24 @@ const initStats = () => os.api('stats', {
stats = res; stats = res;
}); });
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: computed(() => ({
title: i18n.ts.instanceInfo, const headerTabs = $computed(() => [{
icon: 'fas fa-info-circle', active: tab === 'overview',
bg: 'var(--bg)', title: i18n.ts.overview,
tabs: [{ onClick: () => { tab = 'overview'; },
active: tab === 'overview', }, {
title: i18n.ts.overview, active: tab === 'charts',
onClick: () => { tab = 'overview'; }, title: i18n.ts.charts,
}, { icon: 'fas fa-chart-bar',
active: tab === 'charts', onClick: () => { 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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -1,30 +1,33 @@
<template> <template>
<MkSpacer :content-max="500" :margin-min="16" :margin-max="32"> <MkStickyContainer>
<div v-if="file" class="cxqhhsmd _formRoot"> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<div class="_formBlock"> <MkSpacer :content-max="500" :margin-min="16" :margin-max="32">
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/> <div v-if="file" class="cxqhhsmd _formRoot">
<div class="info"> <div class="_formBlock">
<span style="margin-right: 1em;">{{ file.type }}</span> <MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
<span>{{ bytes(file.size) }}</span> <div class="info">
<MkTime :time="file.createdAt" mode="detail" style="display: block;"/> <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> </div>
<div class="_formBlock"> </MkSpacer>
<MkSwitch v-model="isSensitive" @update:modelValue="toggleIsSensitive">NSFW</MkSwitch> </MkStickyContainer>
</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>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -35,7 +38,7 @@ import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue';
import bytes from '@/filters/bytes'; import bytes from '@/filters/bytes';
import * as os from '@/os'; import * as os from '@/os';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import * as symbols from '@/symbols'; import { definePageMetadata } from '@/scripts/page-metadata';
let file: any = $ref(null); let file: any = $ref(null);
let info: any = $ref(null); let info: any = $ref(null);
@ -74,13 +77,15 @@ async function toggleIsSensitive(v) {
isSensitive = v; isSensitive = v;
} }
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: computed(() => ({
title: file ? i18n.ts.file + ': ' + file.name : i18n.ts.file, const headerTabs = $computed(() => []);
icon: 'fas fa-file',
bg: 'var(--bg)', definePageMetadata(computed(() => ({
})), title: file ? i18n.ts.file + ': ' + file.name : i18n.ts.file,
}); icon: 'fas fa-file',
bg: 'var(--bg)',
})));
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View 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>

View file

@ -1,28 +1,31 @@
<template> <template>
<div class="lcixvhis"> <MkStickyContainer>
<div class="_section reports"> <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<div class="_content"> <MkSpacer :content-max="900">
<div class="inputs" style="display: flex;"> <div class="lcixvhis">
<MkSelect v-model="state" style="margin: 0; flex: 1;"> <div class="_section reports">
<template #label>{{ $ts.state }}</template> <div class="_content">
<option value="all">{{ $ts.all }}</option> <div class="inputs" style="display: flex;">
<option value="unresolved">{{ $ts.unresolved }}</option> <MkSelect v-model="state" style="margin: 0; flex: 1;">
<option value="resolved">{{ $ts.resolved }}</option> <template #label>{{ $ts.state }}</template>
</MkSelect> <option value="all">{{ $ts.all }}</option>
<MkSelect v-model="targetUserOrigin" style="margin: 0; flex: 1;"> <option value="unresolved">{{ $ts.unresolved }}</option>
<template #label>{{ $ts.reporteeOrigin }}</template> <option value="resolved">{{ $ts.resolved }}</option>
<option value="combined">{{ $ts.all }}</option> </MkSelect>
<option value="local">{{ $ts.local }}</option> <MkSelect v-model="targetUserOrigin" style="margin: 0; flex: 1;">
<option value="remote">{{ $ts.remote }}</option> <template #label>{{ $ts.reporteeOrigin }}</template>
</MkSelect> <option value="combined">{{ $ts.all }}</option>
<MkSelect v-model="reporterOrigin" style="margin: 0; flex: 1;"> <option value="local">{{ $ts.local }}</option>
<template #label>{{ $ts.reporterOrigin }}</template> <option value="remote">{{ $ts.remote }}</option>
<option value="combined">{{ $ts.all }}</option> </MkSelect>
<option value="local">{{ $ts.local }}</option> <MkSelect v-model="reporterOrigin" style="margin: 0; flex: 1;">
<option value="remote">{{ $ts.remote }}</option> <template #label>{{ $ts.reporterOrigin }}</template>
</MkSelect> <option value="combined">{{ $ts.all }}</option>
</div> <option value="local">{{ $ts.local }}</option>
<!-- TODO <option value="remote">{{ $ts.remote }}</option>
</MkSelect>
</div>
<!-- TODO
<div class="inputs" style="display: flex; padding-top: 1.2em;"> <div class="inputs" style="display: flex; padding-top: 1.2em;">
<MkInput v-model="searchUsername" style="margin: 0; flex: 1;" type="text" spellcheck="false"> <MkInput v-model="searchUsername" style="margin: 0; flex: 1;" type="text" spellcheck="false">
<span>{{ $ts.username }}</span> <span>{{ $ts.username }}</span>
@ -33,24 +36,27 @@
</div> </div>
--> -->
<MkPagination v-slot="{items}" ref="reports" :pagination="pagination" style="margin-top: var(--margin);"> <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"/> <XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/>
</MkPagination> </MkPagination>
</div>
</div>
</div> </div>
</div> </MkSpacer>
</div> </MkStickyContainer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'; import { computed } from 'vue';
import XHeader from './_header_.vue';
import MkInput from '@/components/form/input.vue'; import MkInput from '@/components/form/input.vue';
import MkSelect from '@/components/form/select.vue'; import MkSelect from '@/components/form/select.vue';
import MkPagination from '@/components/ui/pagination.vue'; import MkPagination from '@/components/ui/pagination.vue';
import XAbuseReport from '@/components/abuse-report.vue'; import XAbuseReport from '@/components/abuse-report.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let reports = $ref<InstanceType<typeof MkPagination>>(); let reports = $ref<InstanceType<typeof MkPagination>>();
@ -74,12 +80,14 @@ function resolved(reportId) {
reports.removeItem(item => item.id === reportId); reports.removeItem(item => item.id === reportId);
} }
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: {
title: i18n.ts.abuseReports, const headerTabs = $computed(() => []);
icon: 'fas fa-exclamation-circle',
bg: 'var(--bg)', definePageMetadata({
} title: i18n.ts.abuseReports,
icon: 'fas fa-exclamation-circle',
bg: 'var(--bg)',
}); });
</script> </script>

View file

@ -1,21 +1,23 @@
<template> <template>
<MkSpacer :content-max="900"> <MkStickyContainer>
<div class="uqshojas"> <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<div v-for="ad in ads" class="_panel _formRoot ad"> <MkSpacer :content-max="900">
<MkAd v-if="ad.url" :specify="ad"/> <div class="uqshojas">
<MkInput v-model="ad.url" type="url" class="_formBlock"> <div v-for="ad in ads" class="_panel _formRoot ad">
<template #label>URL</template> <MkAd v-if="ad.url" :specify="ad"/>
</MkInput> <MkInput v-model="ad.url" type="url" class="_formBlock">
<MkInput v-model="ad.imageUrl" class="_formBlock"> <template #label>URL</template>
<template #label>{{ i18n.ts.imageUrl }}</template> </MkInput>
</MkInput> <MkInput v-model="ad.imageUrl" class="_formBlock">
<FormRadios v-model="ad.place" class="_formBlock"> <template #label>{{ i18n.ts.imageUrl }}</template>
<template #label>Form</template> </MkInput>
<option value="square">square</option> <FormRadios v-model="ad.place" class="_formBlock">
<option value="horizontal">horizontal</option> <template #label>Form</template>
<option value="horizontal-big">horizontal-big</option> <option value="square">square</option>
</FormRadios> <option value="horizontal">horizontal</option>
<!-- <option value="horizontal-big">horizontal-big</option>
</FormRadios>
<!--
<div style="margin: 32px 0;"> <div style="margin: 32px 0;">
{{ i18n.ts.priority }} {{ i18n.ts.priority }}
<MkRadio v-model="ad.priority" value="high">{{ i18n.ts.high }}</MkRadio> <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> <MkRadio v-model="ad.priority" value="low">{{ i18n.ts.low }}</MkRadio>
</div> </div>
--> -->
<FormSplit> <FormSplit>
<MkInput v-model="ad.ratio" type="number"> <MkInput v-model="ad.ratio" type="number">
<template #label>{{ i18n.ts.ratio }}</template> <template #label>{{ i18n.ts.ratio }}</template>
</MkInput> </MkInput>
<MkInput v-model="ad.expiresAt" type="date"> <MkInput v-model="ad.expiresAt" type="date">
<template #label>{{ i18n.ts.expiration }}</template> <template #label>{{ i18n.ts.expiration }}</template>
</MkInput> </MkInput>
</FormSplit> </FormSplit>
<MkTextarea v-model="ad.memo" class="_formBlock"> <MkTextarea v-model="ad.memo" class="_formBlock">
<template #label>{{ i18n.ts.memo }}</template> <template #label>{{ i18n.ts.memo }}</template>
</MkTextarea> </MkTextarea>
<div class="buttons _formBlock"> <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 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> <MkButton class="button" inline danger @click="remove(ad)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton>
</div>
</div> </div>
</div> </div>
</div> </MkSpacer>
</MkSpacer> </MkStickyContainer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { } from 'vue';
import XHeader from './_header_.vue';
import MkButton from '@/components/ui/button.vue'; import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue'; import MkInput from '@/components/form/input.vue';
import MkTextarea from '@/components/form/textarea.vue'; import MkTextarea from '@/components/form/textarea.vue';
import FormRadios from '@/components/form/radios.vue'; import FormRadios from '@/components/form/radios.vue';
import FormSplit from '@/components/form/split.vue'; import FormSplit from '@/components/form/split.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let ads: any[] = $ref([]); let ads: any[] = $ref([]);
@ -81,7 +85,7 @@ function remove(ad) {
if (canceled) return; if (canceled) return;
ads = ads.filter(x => x !== ad); ads = ads.filter(x => x !== ad);
os.apiWithDialog('admin/ad/delete', { os.apiWithDialog('admin/ad/delete', {
id: ad.id id: ad.id,
}); });
}); });
} }
@ -90,28 +94,29 @@ function save(ad) {
if (ad.id == null) { if (ad.id == null) {
os.apiWithDialog('admin/ad/create', { os.apiWithDialog('admin/ad/create', {
...ad, ...ad,
expiresAt: new Date(ad.expiresAt).getTime() expiresAt: new Date(ad.expiresAt).getTime(),
}); });
} else { } else {
os.apiWithDialog('admin/ad/update', { os.apiWithDialog('admin/ad/update', {
...ad, ...ad,
expiresAt: new Date(ad.expiresAt).getTime() expiresAt: new Date(ad.expiresAt).getTime(),
}); });
} }
} }
defineExpose({ const headerActions = $computed(() => [{
[symbols.PAGE_INFO]: { asFullButton: true,
title: i18n.ts.ads, icon: 'fas fa-plus',
icon: 'fas fa-audio-description', text: i18n.ts.add,
bg: 'var(--bg)', handler: add,
actions: [{ }]);
asFullButton: true,
icon: 'fas fa-plus', const headerTabs = $computed(() => []);
text: i18n.ts.add,
handler: add, definePageMetadata({
}], title: i18n.ts.ads,
} icon: 'fas fa-audio-description',
bg: 'var(--bg)',
}); });
</script> </script>

View file

@ -1,34 +1,40 @@
<template> <template>
<div class="ztgjmzrw"> <MkStickyContainer>
<section v-for="announcement in announcements" class="_card _gap announcements"> <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<div class="_content announcement"> <MkSpacer :content-max="900">
<MkInput v-model="announcement.title"> <div class="ztgjmzrw">
<template #label>{{ i18n.ts.title }}</template> <section v-for="announcement in announcements" class="_card _gap announcements">
</MkInput> <div class="_content announcement">
<MkTextarea v-model="announcement.text"> <MkInput v-model="announcement.title">
<template #label>{{ i18n.ts.text }}</template> <template #label>{{ i18n.ts.title }}</template>
</MkTextarea> </MkInput>
<MkInput v-model="announcement.imageUrl"> <MkTextarea v-model="announcement.text">
<template #label>{{ i18n.ts.imageUrl }}</template> <template #label>{{ i18n.ts.text }}</template>
</MkInput> </MkTextarea>
<p v-if="announcement.reads">{{ i18n.t('nUsersRead', { n: announcement.reads }) }}</p> <MkInput v-model="announcement.imageUrl">
<div class="buttons"> <template #label>{{ i18n.ts.imageUrl }}</template>
<MkButton class="button" inline primary @click="save(announcement)"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton> </MkInput>
<MkButton class="button" inline @click="remove(announcement)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton> <p v-if="announcement.reads">{{ i18n.t('nUsersRead', { n: announcement.reads }) }}</p>
</div> <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> </div>
</section> </MkSpacer>
</div> </MkStickyContainer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { } from 'vue';
import XHeader from './_header_.vue';
import MkButton from '@/components/ui/button.vue'; import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue'; import MkInput from '@/components/form/input.vue';
import MkTextarea from '@/components/form/textarea.vue'; import MkTextarea from '@/components/form/textarea.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let announcements: any[] = $ref([]); let announcements: any[] = $ref([]);
@ -41,7 +47,7 @@ function add() {
id: null, id: null,
title: '', title: '',
text: '', text: '',
imageUrl: null imageUrl: null,
}); });
} }
@ -61,41 +67,42 @@ function save(announcement) {
os.api('admin/announcements/create', announcement).then(() => { os.api('admin/announcements/create', announcement).then(() => {
os.alert({ os.alert({
type: 'success', type: 'success',
text: i18n.ts.saved text: i18n.ts.saved,
}); });
}).catch(err => { }).catch(err => {
os.alert({ os.alert({
type: 'error', type: 'error',
text: err text: err,
}); });
}); });
} else { } else {
os.api('admin/announcements/update', announcement).then(() => { os.api('admin/announcements/update', announcement).then(() => {
os.alert({ os.alert({
type: 'success', type: 'success',
text: i18n.ts.saved text: i18n.ts.saved,
}); });
}).catch(err => { }).catch(err => {
os.alert({ os.alert({
type: 'error', type: 'error',
text: err text: err,
}); });
}); });
} }
} }
defineExpose({ const headerActions = $computed(() => [{
[symbols.PAGE_INFO]: { asFullButton: true,
title: i18n.ts.announcements, icon: 'fas fa-plus',
icon: 'fas fa-broadcast-tower', text: i18n.ts.add,
bg: 'var(--bg)', handler: add,
actions: [{ }]);
asFullButton: true,
icon: 'fas fa-plus', const headerTabs = $computed(() => []);
text: i18n.ts.add,
handler: add, definePageMetadata({
}], title: i18n.ts.announcements,
} icon: 'fas fa-broadcast-tower',
bg: 'var(--bg)',
}); });
</script> </script>

View file

@ -51,7 +51,6 @@ import FormButton from '@/components/ui/button.vue';
import FormSuspense from '@/components/form/suspense.vue'; import FormSuspense from '@/components/form/suspense.vue';
import FormSlot from '@/components/form/slot.vue'; import FormSlot from '@/components/form/slot.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance'; import { fetchInstance } from '@/instance';
const MkCaptcha = defineAsyncComponent(() => import('@/components/captcha.vue')); const MkCaptcha = defineAsyncComponent(() => import('@/components/captcha.vue'));

View file

@ -1,12 +1,13 @@
<template> <template><MkStickyContainer>
<MkSpacer :content-max="800" :margin-min="16" :margin-max="32"> <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"> <FormSuspense v-slot="{ result: database }" :p="databasePromiseFactory">
<MkKeyValue v-for="table in database" :key="table[0]" oneline style="margin: 1em 0;"> <MkKeyValue v-for="table in database" :key="table[0]" oneline style="margin: 1em 0;">
<template #key>{{ table[0] }}</template> <template #key>{{ table[0] }}</template>
<template #value>{{ bytes(table[1].size) }} ({{ number(table[1].count) }} recs)</template> <template #value>{{ bytes(table[1].size) }} ({{ number(table[1].count) }} recs)</template>
</MkKeyValue> </MkKeyValue>
</FormSuspense> </FormSuspense>
</MkSpacer> </MkSpacer></MkStickyContainer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -14,18 +15,20 @@ import { } from 'vue';
import FormSuspense from '@/components/form/suspense.vue'; import FormSuspense from '@/components/form/suspense.vue';
import MkKeyValue from '@/components/key-value.vue'; import MkKeyValue from '@/components/key-value.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols';
import bytes from '@/filters/bytes'; import bytes from '@/filters/bytes';
import number from '@/filters/number'; import number from '@/filters/number';
import { i18n } from '@/i18n'; 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)); const databasePromiseFactory = () => os.api('admin/get-table-stats').then(res => Object.entries(res).sort((a, b) => b[1].size - a[1].size));
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: {
title: i18n.ts.database, const headerTabs = $computed(() => []);
icon: 'fas fa-database',
bg: 'var(--bg)', definePageMetadata({
} title: i18n.ts.database,
icon: 'fas fa-database',
bg: 'var(--bg)',
}); });
</script> </script>

View file

@ -1,49 +1,53 @@
<template> <template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> <MkStickyContainer>
<FormSuspense :p="init"> <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<div class="_formRoot"> <MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSwitch v-model="enableEmail" class="_formBlock"> <FormSuspense :p="init">
<template #label>{{ i18n.ts.enableEmail }}</template> <div class="_formRoot">
<template #caption>{{ i18n.ts.emailConfigInfo }}</template> <FormSwitch v-model="enableEmail" class="_formBlock">
</FormSwitch> <template #label>{{ i18n.ts.enableEmail }}</template>
<template #caption>{{ i18n.ts.emailConfigInfo }}</template>
</FormSwitch>
<template v-if="enableEmail"> <template v-if="enableEmail">
<FormInput v-model="email" type="email" class="_formBlock"> <FormInput v-model="email" type="email" class="_formBlock">
<template #label>{{ i18n.ts.emailAddress }}</template> <template #label>{{ i18n.ts.emailAddress }}</template>
</FormInput> </FormInput>
<FormSection> <FormSection>
<template #label>{{ i18n.ts.smtpConfig }}</template> <template #label>{{ i18n.ts.smtpConfig }}</template>
<FormSplit :min-width="280"> <FormSplit :min-width="280">
<FormInput v-model="smtpHost" class="_formBlock"> <FormInput v-model="smtpHost" class="_formBlock">
<template #label>{{ i18n.ts.smtpHost }}</template> <template #label>{{ i18n.ts.smtpHost }}</template>
</FormInput> </FormInput>
<FormInput v-model="smtpPort" type="number" class="_formBlock"> <FormInput v-model="smtpPort" type="number" class="_formBlock">
<template #label>{{ i18n.ts.smtpPort }}</template> <template #label>{{ i18n.ts.smtpPort }}</template>
</FormInput> </FormInput>
</FormSplit> </FormSplit>
<FormSplit :min-width="280"> <FormSplit :min-width="280">
<FormInput v-model="smtpUser" class="_formBlock"> <FormInput v-model="smtpUser" class="_formBlock">
<template #label>{{ i18n.ts.smtpUser }}</template> <template #label>{{ i18n.ts.smtpUser }}</template>
</FormInput> </FormInput>
<FormInput v-model="smtpPass" type="password" class="_formBlock"> <FormInput v-model="smtpPass" type="password" class="_formBlock">
<template #label>{{ i18n.ts.smtpPass }}</template> <template #label>{{ i18n.ts.smtpPass }}</template>
</FormInput> </FormInput>
</FormSplit> </FormSplit>
<FormInfo class="_formBlock">{{ i18n.ts.emptyToDisableSmtpAuth }}</FormInfo> <FormInfo class="_formBlock">{{ i18n.ts.emptyToDisableSmtpAuth }}</FormInfo>
<FormSwitch v-model="smtpSecure" class="_formBlock"> <FormSwitch v-model="smtpSecure" class="_formBlock">
<template #label>{{ i18n.ts.smtpSecure }}</template> <template #label>{{ i18n.ts.smtpSecure }}</template>
<template #caption>{{ i18n.ts.smtpSecureInfo }}</template> <template #caption>{{ i18n.ts.smtpSecureInfo }}</template>
</FormSwitch> </FormSwitch>
</FormSection> </FormSection>
</template> </template>
</div> </div>
</FormSuspense> </FormSuspense>
</MkSpacer> </MkSpacer>
</MkStickyContainer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { } from 'vue';
import XHeader from './_header_.vue';
import FormSwitch from '@/components/form/switch.vue'; import FormSwitch from '@/components/form/switch.vue';
import FormInput from '@/components/form/input.vue'; import FormInput from '@/components/form/input.vue';
import FormInfo from '@/components/ui/info.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 FormSplit from '@/components/form/split.vue';
import FormSection from '@/components/form/section.vue'; import FormSection from '@/components/form/section.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance, instance } from '@/instance'; import { fetchInstance, instance } from '@/instance';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let enableEmail: boolean = $ref(false); let enableEmail: boolean = $ref(false);
let email: any = $ref(null); let email: any = $ref(null);
@ -78,13 +82,13 @@ async function testEmail() {
const { canceled, result: destination } = await os.inputText({ const { canceled, result: destination } = await os.inputText({
title: i18n.ts.destination, title: i18n.ts.destination,
type: 'email', type: 'email',
placeholder: instance.maintainerEmail placeholder: instance.maintainerEmail,
}); });
if (canceled) return; if (canceled) return;
os.apiWithDialog('admin/send-email', { os.apiWithDialog('admin/send-email', {
to: destination, to: destination,
subject: 'Test email', subject: 'Test email',
text: 'Yo' text: 'Yo',
}); });
} }
@ -102,21 +106,22 @@ function save() {
}); });
} }
defineExpose({ const headerActions = $computed(() => [{
[symbols.PAGE_INFO]: { asFullButton: true,
title: i18n.ts.emailServer, text: i18n.ts.testEmail,
icon: 'fas fa-envelope', handler: testEmail,
bg: 'var(--bg)', }, {
actions: [{ asFullButton: true,
asFullButton: true, icon: 'fas fa-check',
text: i18n.ts.testEmail, text: i18n.ts.save,
handler: testEmail, handler: save,
}, { }]);
asFullButton: true,
icon: 'fas fa-check', const headerTabs = $computed(() => []);
text: i18n.ts.save,
handler: save, definePageMetadata({
}], title: i18n.ts.emailServer,
} icon: 'fas fa-envelope',
bg: 'var(--bg)',
}); });
</script> </script>

View file

@ -1,69 +1,75 @@
<template> <template>
<MkSpacer :content-max="900"> <div>
<div class="ogwlenmc"> <MkStickyContainer>
<div v-if="tab === 'local'" class="local"> <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkInput v-model="query" :debounce="true" type="search"> <MkSpacer :content-max="900">
<template #prefix><i class="fas fa-search"></i></template> <div class="ogwlenmc">
<template #label>{{ $ts.search }}</template> <div v-if="tab === 'local'" class="local">
</MkInput> <MkInput v-model="query" :debounce="true" type="search">
<MkSwitch v-model="selectMode" style="margin: 8px 0;"> <template #prefix><i class="fas fa-search"></i></template>
<template #label>Select mode</template> <template #label>{{ $ts.search }}</template>
</MkSwitch> </MkInput>
<div v-if="selectMode" style="display: flex; gap: var(--margin); flex-wrap: wrap;"> <MkSwitch v-model="selectMode" style="margin: 8px 0;">
<MkButton inline @click="selectAll">Select all</MkButton> <template #label>Select mode</template>
<MkButton inline @click="setCategoryBulk">Set category</MkButton> </MkSwitch>
<MkButton inline @click="addTagBulk">Add tag</MkButton> <div v-if="selectMode" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
<MkButton inline @click="removeTagBulk">Remove tag</MkButton> <MkButton inline @click="selectAll">Select all</MkButton>
<MkButton inline @click="setTagBulk">Set tag</MkButton> <MkButton inline @click="setCategoryBulk">Set category</MkButton>
<MkButton inline danger @click="delBulk">Delete</MkButton> <MkButton inline @click="addTagBulk">Add tag</MkButton>
</div> <MkButton inline @click="removeTagBulk">Remove tag</MkButton>
<MkPagination ref="emojisPaginationComponent" :pagination="pagination"> <MkButton inline @click="setTagBulk">Set tag</MkButton>
<template #empty><span>{{ $ts.noCustomEmojis }}</span></template> <MkButton inline danger @click="delBulk">Delete</MkButton>
<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> </div>
</template> <MkPagination ref="emojisPaginationComponent" :pagination="pagination">
</MkPagination> <template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
</div> <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"> <div v-else-if="tab === 'remote'" class="remote">
<FormSplit> <FormSplit>
<MkInput v-model="queryRemote" :debounce="true" type="search"> <MkInput v-model="queryRemote" :debounce="true" type="search">
<template #prefix><i class="fas fa-search"></i></template> <template #prefix><i class="fas fa-search"></i></template>
<template #label>{{ $ts.search }}</template> <template #label>{{ $ts.search }}</template>
</MkInput> </MkInput>
<MkInput v-model="host" :debounce="true"> <MkInput v-model="host" :debounce="true">
<template #label>{{ $ts.host }}</template> <template #label>{{ $ts.host }}</template>
</MkInput> </MkInput>
</FormSplit> </FormSplit>
<MkPagination :pagination="remotePagination"> <MkPagination :pagination="remotePagination">
<template #empty><span>{{ $ts.noCustomEmojis }}</span></template> <template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
<template v-slot="{items}"> <template #default="{items}">
<div class="ldhfsamy"> <div class="ldhfsamy">
<div v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="remoteMenu(emoji, $event)"> <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"/> <img :src="emoji.url" class="img" :alt="emoji.name"/>
<div class="body"> <div class="body">
<div class="name _monospace">{{ emoji.name }}</div> <div class="name _monospace">{{ emoji.name }}</div>
<div class="info">{{ emoji.host }}</div> <div class="info">{{ emoji.host }}</div>
</div>
</div>
</div> </div>
</div> </template>
</div> </MkPagination>
</template> </div>
</MkPagination> </div>
</div> </MkSpacer>
</div> </MkStickyContainer>
</MkSpacer> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, defineAsyncComponent, defineComponent, ref, toRef } from 'vue'; import { computed, defineAsyncComponent, defineComponent, ref, toRef } from 'vue';
import XHeader from './_header_.vue';
import MkButton from '@/components/ui/button.vue'; import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue'; import MkInput from '@/components/form/input.vue';
import MkPagination from '@/components/ui/pagination.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 FormSplit from '@/components/form/split.vue';
import { selectFile, selectFiles } from '@/scripts/select-file'; import { selectFile, selectFiles } from '@/scripts/select-file';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const emojisPaginationComponent = ref<InstanceType<typeof MkPagination>>(); const emojisPaginationComponent = ref<InstanceType<typeof MkPagination>>();
@ -131,13 +137,13 @@ const add = async (ev: MouseEvent) => {
const edit = (emoji) => { const edit = (emoji) => {
os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), { os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), {
emoji: emoji emoji: emoji,
}, { }, {
done: result => { done: result => {
if (result.updated) { if (result.updated) {
emojisPaginationComponent.value.updateItem(result.updated.id, (oldEmoji: any) => ({ emojisPaginationComponent.value.updateItem(result.updated.id, (oldEmoji: any) => ({
...oldEmoji, ...oldEmoji,
...result.updated ...result.updated,
})); }));
} else if (result.deleted) { } else if (result.deleted) {
emojisPaginationComponent.value.removeItem((item) => item.id === emoji.id); emojisPaginationComponent.value.removeItem((item) => item.id === emoji.id);
@ -159,7 +165,7 @@ const remoteMenu = (emoji, ev: MouseEvent) => {
}, { }, {
text: i18n.ts.import, text: i18n.ts.import,
icon: 'fas fa-plus', icon: 'fas fa-plus',
action: () => { im(emoji); } action: () => { im(emoji); },
}], ev.currentTarget ?? ev.target); }], ev.currentTarget ?? ev.target);
}; };
@ -181,7 +187,7 @@ const menu = (ev: MouseEvent) => {
text: err.message, text: err.message,
}); });
}); });
} },
}, { }, {
icon: 'fas fa-upload', icon: 'fas fa-upload',
text: i18n.ts.import, text: i18n.ts.import,
@ -201,7 +207,7 @@ const menu = (ev: MouseEvent) => {
text: err.message, text: err.message,
}); });
}); });
} },
}], ev.currentTarget ?? ev.target); }], ev.currentTarget ?? ev.target);
}; };
@ -265,31 +271,31 @@ const delBulk = async () => {
emojisPaginationComponent.value.reload(); emojisPaginationComponent.value.reload();
}; };
defineExpose({ const headerActions = $computed(() => [{
[symbols.PAGE_INFO]: computed(() => ({ asFullButton: true,
title: i18n.ts.customEmojis, icon: 'fas fa-plus',
icon: 'fas fa-laugh', text: i18n.ts.addEmoji,
bg: 'var(--bg)', handler: add,
actions: [{ }, {
asFullButton: true, icon: 'fas fa-ellipsis-h',
icon: 'fas fa-plus', handler: menu,
text: i18n.ts.addEmoji, }]);
handler: add,
}, { const headerTabs = $computed(() => [{
icon: 'fas fa-ellipsis-h', active: tab.value === 'local',
handler: menu, title: i18n.ts.local,
}], onClick: () => { tab.value = 'local'; },
tabs: [{ }, {
active: tab.value === 'local', active: tab.value === 'remote',
title: i18n.ts.local, title: i18n.ts.remote,
onClick: () => { tab.value = 'local'; }, onClick: () => { tab.value = 'remote'; },
}, { }]);
active: tab.value === 'remote',
title: i18n.ts.remote, definePageMetadata(computed(() => ({
onClick: () => { tab.value = 'remote'; }, title: i18n.ts.customEmojis,
},] icon: 'fas fa-laugh',
})), bg: 'var(--bg)',
}); })));
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -1,50 +1,58 @@
<template> <template>
<div class="xrmjdkdw"> <div>
<div> <MkStickyContainer>
<div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap;"> <template #header><XHeader :actions="headerActions"/></template>
<MkSelect v-model="origin" style="margin: 0; flex: 1;"> <MkSpacer :content-max="900">
<template #label>{{ $ts.instance }}</template> <div class="xrmjdkdw">
<option value="combined">{{ $ts.all }}</option> <div>
<option value="local">{{ $ts.local }}</option> <div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
<option value="remote">{{ $ts.remote }}</option> <MkSelect v-model="origin" style="margin: 0; flex: 1;">
</MkSelect> <template #label>{{ $ts.instance }}</template>
<MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="pagination.params.origin === 'local'"> <option value="combined">{{ $ts.all }}</option>
<template #label>{{ $ts.host }}</template> <option value="local">{{ $ts.local }}</option>
</MkInput> <option value="remote">{{ $ts.remote }}</option>
</div> </MkSelect>
<div class="inputs" style="display: flex; padding-top: 1.2em;"> <MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="pagination.params.origin === 'local'">
<MkInput v-model="type" :debounce="true" type="search" style="margin: 0; flex: 1;"> <template #label>{{ $ts.host }}</template>
<template #label>MIME type</template> </MkInput>
</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> </div>
<div> <div class="inputs" style="display: flex; padding-top: 1.2em;">
<MkAcct v-if="file.user" :user="file.user"/> <MkInput v-model="type" :debounce="true" type="search" style="margin: 0; flex: 1;">
<div v-else>{{ $ts.system }}</div> <template #label>MIME type</template>
</div> </MkInput>
<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>
<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> </div>
</button> </div>
</MkPagination> </MkSpacer>
</div> </MkStickyContainer>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, defineAsyncComponent } from 'vue'; import { computed, defineAsyncComponent } from 'vue';
import * as Acct from 'misskey-js/built/acct'; import * as Acct from 'misskey-js/built/acct';
import XHeader from './_header_.vue';
import MkButton from '@/components/ui/button.vue'; import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue'; import MkInput from '@/components/form/input.vue';
import MkSelect from '@/components/form/select.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 MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue';
import bytes from '@/filters/bytes'; import bytes from '@/filters/bytes';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let origin = $ref('local'); let origin = $ref('local');
let type = $ref(null); let type = $ref(null);
@ -82,7 +90,7 @@ function clear() {
} }
function show(file) { function show(file) {
os.pageWindow(`/admin-file/${file.id}`); os.pageWindow(`/admin/file/${file.id}`);
} }
async function find() { async function find() {
@ -104,22 +112,23 @@ async function find() {
}); });
} }
defineExpose({ const headerActions = $computed(() => [{
[symbols.PAGE_INFO]: computed(() => ({ text: i18n.ts.lookup,
title: i18n.ts.files, icon: 'fas fa-search',
icon: 'fas fa-cloud', handler: find,
bg: 'var(--bg)', }, {
actions: [{ text: i18n.ts.clearCachedFiles,
text: i18n.ts.lookup, icon: 'fas fa-trash-alt',
icon: 'fas fa-search', handler: clear,
handler: find, }]);
}, {
text: i18n.ts.clearCachedFiles, const headerTabs = $computed(() => []);
icon: 'fas fa-trash-alt',
handler: clear, definePageMetadata(computed(() => ({
}], title: i18n.ts.files,
})), icon: 'fas fa-cloud',
}); bg: 'var(--bg)',
})));
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -1,8 +1,6 @@
<template> <template>
<div ref="el" class="hiyeyicy" :class="{ wide: !narrow }"> <div ref="el" class="hiyeyicy" :class="{ wide: !narrow }">
<div v-if="!narrow || initialPage == null" class="nav"> <div v-if="!narrow || initialPage == null" class="nav">
<MkHeader :info="header"></MkHeader>
<MkSpacer :content-max="700" :margin-min="16"> <MkSpacer :content-max="700" :margin-min="16">
<div class="lxpfedzu"> <div class="lxpfedzu">
<div class="banner"> <div class="banner">
@ -17,29 +15,26 @@
</MkSpacer> </MkSpacer>
</div> </div>
<div v-if="!(narrow && initialPage == null)" class="main"> <div v-if="!(narrow && initialPage == null)" class="main">
<MkStickyContainer> <component :is="component" :key="initialPage" v-bind="pageProps"/>
<template #header><MkHeader v-if="childInfo && !childInfo.hideHeader" :info="childInfo"/></template>
<component :is="component" :ref="el => pageChanged(el)" :key="initialPage" v-bind="pageProps"/>
</MkStickyContainer>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <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 { i18n } from '@/i18n';
import MkSuperMenu from '@/components/ui/super-menu.vue'; import MkSuperMenu from '@/components/ui/super-menu.vue';
import MkInfo from '@/components/ui/info.vue'; import MkInfo from '@/components/ui/info.vue';
import { scroll } from '@/scripts/scroll'; import { scroll } from '@/scripts/scroll';
import { instance } from '@/instance'; import { instance } from '@/instance';
import * as symbols from '@/symbols';
import * as os from '@/os'; import * as os from '@/os';
import { lookupUser } from '@/scripts/lookup-user'; 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 isEmpty = (x: string | null) => x == null || x === '';
const nav = new MisskeyNavigator(); const router = useRouter();
const indexInfo = { const indexInfo = {
title: i18n.ts.controlPanel, title: i18n.ts.controlPanel,
@ -224,7 +219,7 @@ watch(component, () => {
watch(() => props.initialPage, () => { watch(() => props.initialPage, () => {
if (props.initialPage == null && !narrow) { if (props.initialPage == null && !narrow) {
nav.push('/admin/overview'); router.push('/admin/overview');
} else { } else {
if (props.initialPage == null) { if (props.initialPage == null) {
INFO = indexInfo; INFO = indexInfo;
@ -234,7 +229,7 @@ watch(() => props.initialPage, () => {
watch(narrow, () => { watch(narrow, () => {
if (props.initialPage == null && !narrow) { if (props.initialPage == null && !narrow) {
nav.push('/admin/overview'); router.push('/admin/overview');
} }
}); });
@ -243,7 +238,7 @@ onMounted(() => {
narrow = el.offsetWidth < NARROW_THRESHOLD; narrow = el.offsetWidth < NARROW_THRESHOLD;
if (props.initialPage == null && !narrow) { if (props.initialPage == null && !narrow) {
nav.push('/admin/overview'); router.push('/admin/overview');
} }
}); });
@ -251,19 +246,19 @@ onUnmounted(() => {
ro.disconnect(); ro.disconnect();
}); });
const pageChanged = (page) => { provideMetadataReceiver((info) => {
if (page == null) { if (info == null) {
childInfo = null; childInfo = null;
} else { } else {
childInfo = page[symbols.PAGE_INFO]; childInfo = info;
} }
}; });
const invite = () => { const invite = () => {
os.api('admin/invite').then(x => { os.api('admin/invite').then(x => {
os.alert({ os.alert({
type: 'info', type: 'info',
text: x.code text: x.code,
}); });
}).catch(err => { }).catch(err => {
os.alert({ os.alert({
@ -279,33 +274,38 @@ const lookup = (ev) => {
icon: 'fas fa-user', icon: 'fas fa-user',
action: () => { action: () => {
lookupUser(); lookupUser();
} },
}, { }, {
text: i18n.ts.note, text: i18n.ts.note,
icon: 'fas fa-pencil-alt', icon: 'fas fa-pencil-alt',
action: () => { action: () => {
alert('TODO'); alert('TODO');
} },
}, { }, {
text: i18n.ts.file, text: i18n.ts.file,
icon: 'fas fa-cloud', icon: 'fas fa-cloud',
action: () => { action: () => {
alert('TODO'); alert('TODO');
} },
}, { }, {
text: i18n.ts.instance, text: i18n.ts.instance,
icon: 'fas fa-globe', icon: 'fas fa-globe',
action: () => { action: () => {
alert('TODO'); alert('TODO');
} },
}], ev.currentTarget ?? ev.target); }], ev.currentTarget ?? ev.target);
}; };
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata(INFO);
defineExpose({ defineExpose({
[symbols.PAGE_INFO]: INFO,
header: { header: {
title: i18n.ts.controlPanel, title: i18n.ts.controlPanel,
} },
}); });
</script> </script>

View file

@ -1,25 +1,29 @@
<template> <template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> <MkStickyContainer>
<FormSuspense :p="init"> <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<FormTextarea v-model="blockedHosts" class="_formBlock"> <MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<span>{{ i18n.ts.blockedInstances }}</span> <FormSuspense :p="init">
<template #caption>{{ i18n.ts.blockedInstancesDescription }}</template> <FormTextarea v-model="blockedHosts" class="_formBlock">
</FormTextarea> <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> <FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton>
</FormSuspense> </FormSuspense>
</MkSpacer> </MkSpacer>
</MkStickyContainer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { } from 'vue';
import XHeader from './_header_.vue';
import FormButton from '@/components/ui/button.vue'; import FormButton from '@/components/ui/button.vue';
import FormTextarea from '@/components/form/textarea.vue'; import FormTextarea from '@/components/form/textarea.vue';
import FormSuspense from '@/components/form/suspense.vue'; import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance'; import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let blockedHosts: string = $ref(''); let blockedHosts: string = $ref('');
@ -36,11 +40,13 @@ function save() {
}); });
} }
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: {
title: i18n.ts.instanceBlocking, const headerTabs = $computed(() => []);
icon: 'fas fa-ban',
bg: 'var(--bg)', definePageMetadata({
} title: i18n.ts.instanceBlocking,
icon: 'fas fa-ban',
bg: 'var(--bg)',
}); });
</script> </script>

View file

@ -1,5 +1,6 @@
<template> <template><MkStickyContainer>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init"> <FormSuspense :p="init">
<FormFolder class="_formBlock"> <FormFolder class="_formBlock">
<template #icon><i class="fab fa-twitter"></i></template> <template #icon><i class="fab fa-twitter"></i></template>
@ -20,19 +21,19 @@
<XDiscord/> <XDiscord/>
</FormFolder> </FormFolder>
</FormSuspense> </FormSuspense>
</MkSpacer> </MkSpacer></MkStickyContainer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { } from 'vue';
import FormFolder from '@/components/form/folder.vue';
import FormSuspense from '@/components/form/suspense.vue';
import XTwitter from './integrations.twitter.vue'; import XTwitter from './integrations.twitter.vue';
import XGithub from './integrations.github.vue'; import XGithub from './integrations.github.vue';
import XDiscord from './integrations.discord.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 os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let enableTwitterIntegration: boolean = $ref(false); let enableTwitterIntegration: boolean = $ref(false);
let enableGithubIntegration: boolean = $ref(false); let enableGithubIntegration: boolean = $ref(false);
@ -45,11 +46,13 @@ async function init() {
enableDiscordIntegration = meta.enableDiscordIntegration; enableDiscordIntegration = meta.enableDiscordIntegration;
} }
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: {
title: i18n.ts.integration, const headerTabs = $computed(() => []);
icon: 'fas fa-share-alt',
bg: 'var(--bg)', definePageMetadata({
} title: i18n.ts.integration,
icon: 'fas fa-share-alt',
bg: 'var(--bg)',
}); });
</script> </script>

View file

@ -1,72 +1,76 @@
<template> <template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> <MkStickyContainer>
<FormSuspense :p="init"> <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<div class="_formRoot"> <MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSwitch v-model="useObjectStorage" class="_formBlock">{{ i18n.ts.useObjectStorage }}</FormSwitch> <FormSuspense :p="init">
<div class="_formRoot">
<FormSwitch v-model="useObjectStorage" class="_formBlock">{{ i18n.ts.useObjectStorage }}</FormSwitch>
<template v-if="useObjectStorage"> <template v-if="useObjectStorage">
<FormInput v-model="objectStorageBaseUrl" class="_formBlock"> <FormInput v-model="objectStorageBaseUrl" class="_formBlock">
<template #label>{{ i18n.ts.objectStorageBaseUrl }}</template> <template #label>{{ i18n.ts.objectStorageBaseUrl }}</template>
<template #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</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>
</FormInput> </FormInput>
<FormInput v-model="objectStorageSecretKey" class="_formBlock"> <FormInput v-model="objectStorageBucket" class="_formBlock">
<template #prefix><i class="fas fa-key"></i></template> <template #label>{{ i18n.ts.objectStorageBucket }}</template>
<template #label>Secret key</template> <template #caption>{{ i18n.ts.objectStorageBucketDesc }}</template>
</FormInput> </FormInput>
</FormSplit>
<FormSwitch v-model="objectStorageUseSSL" class="_formBlock"> <FormInput v-model="objectStoragePrefix" class="_formBlock">
<template #label>{{ i18n.ts.objectStorageUseSSL }}</template> <template #label>{{ i18n.ts.objectStoragePrefix }}</template>
<template #caption>{{ i18n.ts.objectStorageUseSSLDesc }}</template> <template #caption>{{ i18n.ts.objectStoragePrefixDesc }}</template>
</FormSwitch> </FormInput>
<FormSwitch v-model="objectStorageUseProxy" class="_formBlock"> <FormInput v-model="objectStorageEndpoint" class="_formBlock">
<template #label>{{ i18n.ts.objectStorageUseProxy }}</template> <template #label>{{ i18n.ts.objectStorageEndpoint }}</template>
<template #caption>{{ i18n.ts.objectStorageUseProxyDesc }}</template> <template #caption>{{ i18n.ts.objectStorageEndpointDesc }}</template>
</FormSwitch> </FormInput>
<FormSwitch v-model="objectStorageSetPublicRead" class="_formBlock"> <FormInput v-model="objectStorageRegion" class="_formBlock">
<template #label>{{ i18n.ts.objectStorageSetPublicRead }}</template> <template #label>{{ i18n.ts.objectStorageRegion }}</template>
</FormSwitch> <template #caption>{{ i18n.ts.objectStorageRegionDesc }}</template>
</FormInput>
<FormSwitch v-model="objectStorageS3ForcePathStyle" class="_formBlock"> <FormSplit :min-width="280">
<template #label>s3ForcePathStyle</template> <FormInput v-model="objectStorageAccessKey" class="_formBlock">
</FormSwitch> <template #prefix><i class="fas fa-key"></i></template>
</template> <template #label>Access key</template>
</div> </FormInput>
</FormSuspense>
</MkSpacer> <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> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { } from 'vue';
import XHeader from './_header_.vue';
import FormSwitch from '@/components/form/switch.vue'; import FormSwitch from '@/components/form/switch.vue';
import FormInput from '@/components/form/input.vue'; import FormInput from '@/components/form/input.vue';
import FormGroup from '@/components/form/group.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 FormSplit from '@/components/form/split.vue';
import FormSection from '@/components/form/section.vue'; import FormSection from '@/components/form/section.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance'; import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let useObjectStorage: boolean = $ref(false); let useObjectStorage: boolean = $ref(false);
let objectStorageBaseUrl: string | null = $ref(null); let objectStorageBaseUrl: string | null = $ref(null);
@ -129,17 +133,18 @@ function save() {
}); });
} }
defineExpose({ const headerActions = $computed(() => [{
[symbols.PAGE_INFO]: { asFullButton: true,
title: i18n.ts.objectStorage, icon: 'fas fa-check',
icon: 'fas fa-cloud', text: i18n.ts.save,
bg: 'var(--bg)', handler: save,
actions: [{ }]);
asFullButton: true,
icon: 'fas fa-check', const headerTabs = $computed(() => []);
text: i18n.ts.save,
handler: save, definePageMetadata({
}], title: i18n.ts.objectStorage,
} icon: 'fas fa-cloud',
bg: 'var(--bg)',
}); });
</script> </script>

View file

@ -1,18 +1,22 @@
<template> <template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> <MkStickyContainer>
<FormSuspense :p="init"> <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
none <MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
</FormSuspense> <FormSuspense :p="init">
</MkSpacer> none
</FormSuspense>
</MkSpacer>
</MkStickyContainer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { } from 'vue';
import XHeader from './_header_.vue';
import FormSuspense from '@/components/form/suspense.vue'; import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance'; import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
async function init() { async function init() {
await os.api('admin/meta'); await os.api('admin/meta');
@ -24,17 +28,18 @@ function save() {
}); });
} }
defineExpose({ const headerActions = $computed(() => [{
[symbols.PAGE_INFO]: { asFullButton: true,
title: i18n.ts.other, icon: 'fas fa-check',
icon: 'fas fa-cogs', text: i18n.ts.save,
bg: 'var(--bg)', handler: save,
actions: [{ }]);
asFullButton: true,
icon: 'fas fa-check', const headerTabs = $computed(() => []);
text: i18n.ts.save,
handler: save, definePageMetadata({
}], title: i18n.ts.other,
} icon: 'fas fa-cogs',
bg: 'var(--bg)',
}); });
</script> </script>

View file

@ -35,7 +35,7 @@
</MkContainer> </MkContainer>
</div> </div>
<!--<XMetrics/>--> <!--<XMetrics/>-->
<MkFolder style="margin: var(--margin)"> <MkFolder style="margin: var(--margin)">
<template #header><i class="fas fa-info-circle"></i> {{ i18n.ts.info }}</template> <template #header><i class="fas fa-info-circle"></i> {{ i18n.ts.info }}</template>
@ -67,6 +67,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue'; import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue';
import XMetrics from './metrics.vue';
import MkInstanceStats from '@/components/instance-stats.vue'; import MkInstanceStats from '@/components/instance-stats.vue';
import MkNumberDiff from '@/components/number-diff.vue'; import MkNumberDiff from '@/components/number-diff.vue';
import MkContainer from '@/components/ui/container.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 MkQueueChart from '@/components/queue-chart.vue';
import { version, url } from '@/config'; import { version, url } from '@/config';
import number from '@/filters/number'; import number from '@/filters/number';
import XMetrics from './metrics.vue';
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream'; import { stream } from '@/stream';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let stats: any = $ref(null); let stats: any = $ref(null);
let serverInfo: any = $ref(null); let serverInfo: any = $ref(null);
@ -106,7 +106,7 @@ onMounted(async () => {
nextTick(() => { nextTick(() => {
queueStatsConnection.send('requestLog', { queueStatsConnection.send('requestLog', {
id: Math.random().toString().substr(2, 8), id: Math.random().toString().substr(2, 8),
length: 200 length: 200,
}); });
}); });
}); });
@ -115,12 +115,14 @@ onBeforeUnmount(() => {
queueStatsConnection.dispose(); queueStatsConnection.dispose();
}); });
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: {
title: i18n.ts.dashboard, const headerTabs = $computed(() => []);
icon: 'fas fa-tachometer-alt',
bg: 'var(--bg)', definePageMetadata({
} title: i18n.ts.dashboard,
icon: 'fas fa-tachometer-alt',
bg: 'var(--bg)',
}); });
</script> </script>

View file

@ -1,5 +1,6 @@
<template> <template><MkStickyContainer>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init"> <FormSuspense :p="init">
<MkInfo class="_formBlock">{{ i18n.ts.proxyAccountDescription }}</MkInfo> <MkInfo class="_formBlock">{{ i18n.ts.proxyAccountDescription }}</MkInfo>
<MkKeyValue class="_formBlock"> <MkKeyValue class="_formBlock">
@ -9,7 +10,7 @@
<FormButton primary class="_formBlock" @click="chooseProxyAccount">{{ i18n.ts.selectAccount }}</FormButton> <FormButton primary class="_formBlock" @click="chooseProxyAccount">{{ i18n.ts.selectAccount }}</FormButton>
</FormSuspense> </FormSuspense>
</MkSpacer> </MkSpacer></MkStickyContainer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -19,9 +20,9 @@ import FormButton from '@/components/ui/button.vue';
import MkInfo from '@/components/ui/info.vue'; import MkInfo from '@/components/ui/info.vue';
import FormSuspense from '@/components/form/suspense.vue'; import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance'; import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let proxyAccount: any = $ref(null); let proxyAccount: any = $ref(null);
let proxyAccountId: any = $ref(null); let proxyAccountId: any = $ref(null);
@ -50,11 +51,13 @@ function save() {
}); });
} }
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: {
title: i18n.ts.proxyAccount, const headerTabs = $computed(() => []);
icon: 'fas fa-ghost',
bg: 'var(--bg)', definePageMetadata({
} title: i18n.ts.proxyAccount,
icon: 'fas fa-ghost',
bg: 'var(--bg)',
}); });
</script> </script>

View file

@ -1,24 +1,28 @@
<template> <template>
<MkSpacer :content-max="800"> <MkStickyContainer>
<XQueue :connection="connection" domain="inbox"> <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<template #title>In</template> <MkSpacer :content-max="800">
</XQueue> <XQueue :connection="connection" domain="inbox">
<XQueue :connection="connection" domain="deliver"> <template #title>In</template>
<template #title>Out</template> </XQueue>
</XQueue> <XQueue :connection="connection" domain="deliver">
<MkButton danger @click="clear()"><i class="fas fa-trash-alt"></i> {{ i18n.ts.clearQueue }}</MkButton> <template #title>Out</template>
</MkSpacer> </XQueue>
<MkButton danger @click="clear()"><i class="fas fa-trash-alt"></i> {{ i18n.ts.clearQueue }}</MkButton>
</MkSpacer>
</MkStickyContainer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { markRaw, onMounted, onBeforeUnmount, nextTick } from 'vue'; import { markRaw, onMounted, onBeforeUnmount, nextTick } from 'vue';
import MkButton from '@/components/ui/button.vue';
import XQueue from './queue.chart.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 * as os from '@/os';
import { stream } from '@/stream'; import { stream } from '@/stream';
import * as symbols from '@/symbols';
import * as config from '@/config'; import * as config from '@/config';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const connection = markRaw(stream.useChannel('queueStats')); const connection = markRaw(stream.useChannel('queueStats'));
@ -38,7 +42,7 @@ onMounted(() => {
nextTick(() => { nextTick(() => {
connection.send('requestLog', { connection.send('requestLog', {
id: Math.random().toString().substr(2, 8), id: Math.random().toString().substr(2, 8),
length: 200 length: 200,
}); });
}); });
}); });
@ -47,19 +51,20 @@ onBeforeUnmount(() => {
connection.dispose(); connection.dispose();
}); });
defineExpose({ const headerActions = $computed(() => [{
[symbols.PAGE_INFO]: { asFullButton: true,
title: i18n.ts.jobQueue, icon: 'fas fa-up-right-from-square',
icon: 'fas fa-clipboard-list', text: i18n.ts.dashboard,
bg: 'var(--bg)', handler: () => {
actions: [{ window.open(config.url + '/queue', '_blank');
asFullButton: true, },
icon: 'fas fa-up-right-from-square', }]);
text: i18n.ts.dashboard,
handler: () => { const headerTabs = $computed(() => []);
window.open(config.url + '/queue', '_blank');
}, definePageMetadata({
}], title: i18n.ts.jobQueue,
} icon: 'fas fa-clipboard-list',
bg: 'var(--bg)',
}); });
</script> </script>

View file

@ -1,24 +1,28 @@
<template> <template>
<MkSpacer :content-max="800"> <MkStickyContainer>
<div v-for="relay in relays" :key="relay.inbox" class="relaycxt _panel _block" style="padding: 16px;"> <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<div>{{ relay.inbox }}</div> <MkSpacer :content-max="800">
<div class="status"> <div v-for="relay in relays" :key="relay.inbox" class="relaycxt _panel _block" style="padding: 16px;">
<i v-if="relay.status === 'accepted'" class="fas fa-check icon accepted"></i> <div>{{ relay.inbox }}</div>
<i v-else-if="relay.status === 'rejected'" class="fas fa-ban icon rejected"></i> <div class="status">
<i v-else class="fas fa-clock icon requesting"></i> <i v-if="relay.status === 'accepted'" class="fas fa-check icon accepted"></i>
<span>{{ $t(`_relayStatus.${relay.status}`) }}</span> <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> </div>
<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton> </MkSpacer>
</div> </MkStickyContainer>
</MkSpacer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { } from 'vue';
import XHeader from './_header_.vue';
import MkButton from '@/components/ui/button.vue'; import MkButton from '@/components/ui/button.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let relays: any[] = $ref([]); let relays: any[] = $ref([]);
@ -26,30 +30,30 @@ async function addRelay() {
const { canceled, result: inbox } = await os.inputText({ const { canceled, result: inbox } = await os.inputText({
title: i18n.ts.addRelay, title: i18n.ts.addRelay,
type: 'url', type: 'url',
placeholder: i18n.ts.inboxUrl placeholder: i18n.ts.inboxUrl,
}); });
if (canceled) return; if (canceled) return;
os.api('admin/relays/add', { os.api('admin/relays/add', {
inbox inbox,
}).then((relay: any) => { }).then((relay: any) => {
refresh(); refresh();
}).catch((err: any) => { }).catch((err: any) => {
os.alert({ os.alert({
type: 'error', type: 'error',
text: err.message || err text: err.message || err,
}); });
}); });
} }
function remove(inbox: string) { function remove(inbox: string) {
os.api('admin/relays/remove', { os.api('admin/relays/remove', {
inbox inbox,
}).then(() => { }).then(() => {
refresh(); refresh();
}).catch((err: any) => { }).catch((err: any) => {
os.alert({ os.alert({
type: 'error', type: 'error',
text: err.message || err text: err.message || err,
}); });
}); });
} }
@ -62,18 +66,19 @@ function refresh() {
refresh(); refresh();
defineExpose({ const headerActions = $computed(() => [{
[symbols.PAGE_INFO]: { asFullButton: true,
title: i18n.ts.relays, icon: 'fas fa-plus',
icon: 'fas fa-globe', text: i18n.ts.addRelay,
bg: 'var(--bg)', handler: addRelay,
actions: [{ }]);
asFullButton: true,
icon: 'fas fa-plus', const headerTabs = $computed(() => []);
text: i18n.ts.addRelay,
handler: addRelay, definePageMetadata({
}], title: i18n.ts.relays,
} icon: 'fas fa-globe',
bg: 'var(--bg)',
}); });
</script> </script>

View file

@ -1,36 +1,41 @@
<template> <template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> <MkStickyContainer>
<FormSuspense :p="init"> <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<div class="_formRoot"> <MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormFolder class="_formBlock"> <FormSuspense :p="init">
<template #icon><i class="fas fa-shield-alt"></i></template> <div class="_formRoot">
<template #label>{{ i18n.ts.botProtection }}</template> <FormFolder class="_formBlock">
<template v-if="enableHcaptcha" #suffix>hCaptcha</template> <template #icon><i class="fas fa-shield-alt"></i></template>
<template v-else-if="enableRecaptcha" #suffix>reCAPTCHA</template> <template #label>{{ i18n.ts.botProtection }}</template>
<template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</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/> <XBotProtection/>
</FormFolder> </FormFolder>
<FormFolder class="_formBlock"> <FormFolder class="_formBlock">
<template #label>Summaly Proxy</template> <template #label>Summaly Proxy</template>
<div class="_formRoot"> <div class="_formRoot">
<FormInput v-model="summalyProxy" class="_formBlock"> <FormInput v-model="summalyProxy" class="_formBlock">
<template #prefix><i class="fas fa-link"></i></template> <template #prefix><i class="fas fa-link"></i></template>
<template #label>Summaly Proxy URL</template> <template #label>Summaly Proxy URL</template>
</FormInput> </FormInput>
<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton> <FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton>
</div> </div>
</FormFolder> </FormFolder>
</div> </div>
</FormSuspense> </FormSuspense>
</MkSpacer> </MkSpacer>
</MkStickyContainer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { } from 'vue';
import XBotProtection from './bot-protection.vue';
import XHeader from './_header_.vue';
import FormFolder from '@/components/form/folder.vue'; import FormFolder from '@/components/form/folder.vue';
import FormSwitch from '@/components/form/switch.vue'; import FormSwitch from '@/components/form/switch.vue';
import FormInfo from '@/components/ui/info.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 FormSection from '@/components/form/section.vue';
import FormInput from '@/components/form/input.vue'; import FormInput from '@/components/form/input.vue';
import FormButton from '@/components/ui/button.vue'; import FormButton from '@/components/ui/button.vue';
import XBotProtection from './bot-protection.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance'; import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let summalyProxy: string = $ref(''); let summalyProxy: string = $ref('');
let enableHcaptcha: boolean = $ref(false); let enableHcaptcha: boolean = $ref(false);
@ -63,11 +67,13 @@ function save() {
}); });
} }
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: {
title: i18n.ts.security, const headerTabs = $computed(() => []);
icon: 'fas fa-lock',
bg: 'var(--bg)', definePageMetadata({
} title: i18n.ts.security,
icon: 'fas fa-lock',
bg: 'var(--bg)',
}); });
</script> </script>

View file

@ -1,149 +1,155 @@
<template> <template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> <div>
<FormSuspense :p="init"> <MkStickyContainer>
<div class="_formRoot"> <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<FormInput v-model="name" class="_formBlock"> <MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<template #label>{{ i18n.ts.instanceName }}</template> <FormSuspense :p="init">
</FormInput> <div class="_formRoot">
<FormInput v-model="name" class="_formBlock">
<FormTextarea v-model="description" class="_formBlock"> <template #label>{{ i18n.ts.instanceName }}</template>
<template #label>{{ i18n.ts.instanceDescription }}</template>
</FormTextarea>
<FormInput v-model="tosUrl" class="_formBlock">
<template #prefix><i class="fas fa-link"></i></template>
<template #label>{{ 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>
</FormInput> </FormInput>
<FormInput v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles" class="_formBlock"> <FormTextarea v-model="description" class="_formBlock">
<template #label>{{ i18n.ts.driveCapacityPerRemoteAccount }}</template> <template #label>{{ i18n.ts.instanceDescription }}</template>
<template #suffix>MB</template> </FormTextarea>
<template #caption>{{ i18n.ts.inMb }}</template>
</FormInput>
</FormSplit>
</FormSection>
<FormSection> <FormInput v-model="tosUrl" class="_formBlock">
<template #label>ServiceWorker</template> <template #prefix><i class="fas fa-link"></i></template>
<template #label>{{ i18n.ts.tosUrl }}</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>
<FormInput v-model="swPrivateKey" class="_formBlock"> <FormSplit :min-width="300">
<template #prefix><i class="fas fa-key"></i></template> <FormInput v-model="maintainerName" class="_formBlock">
<template #label>Private key</template> <template #label>{{ i18n.ts.maintainerName }}</template>
</FormInput> </FormInput>
</template>
</FormSection>
<FormSection> <FormInput v-model="maintainerEmail" type="email" class="_formBlock">
<template #label>DeepL Translation</template> <template #prefix><i class="fas fa-envelope"></i></template>
<template #label>{{ i18n.ts.maintainerEmail }}</template>
</FormInput>
</FormSplit>
<FormInput v-model="deeplAuthKey" class="_formBlock"> <FormTextarea v-model="pinnedUsers" class="_formBlock">
<template #prefix><i class="fas fa-key"></i></template> <template #label>{{ i18n.ts.pinnedUsers }}</template>
<template #label>DeepL Auth Key</template> <template #caption>{{ i18n.ts.pinnedUsersDescription }}</template>
</FormInput> </FormTextarea>
<FormSwitch v-model="deeplIsPro" class="_formBlock">
<template #label>Pro account</template> <FormSection>
</FormSwitch> <FormSwitch v-model="enableRegistration" class="_formBlock">
</FormSection> <template #label>{{ i18n.ts.enableRegistration }}</template>
</div> </FormSwitch>
</FormSuspense>
</MkSpacer> <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> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { } from 'vue';
import XHeader from './_header_.vue';
import FormSwitch from '@/components/form/switch.vue'; import FormSwitch from '@/components/form/switch.vue';
import FormInput from '@/components/form/input.vue'; import FormInput from '@/components/form/input.vue';
import FormTextarea from '@/components/form/textarea.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 FormSplit from '@/components/form/split.vue';
import FormSuspense from '@/components/form/suspense.vue'; import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance'; import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let name: string | null = $ref(null); let name: string | null = $ref(null);
let description: string | null = $ref(null); let description: string | null = $ref(null);
@ -240,17 +246,18 @@ function save() {
}); });
} }
defineExpose({ const headerActions = $computed(() => [{
[symbols.PAGE_INFO]: { asFullButton: true,
title: i18n.ts.general, icon: 'fas fa-check',
icon: 'fas fa-cog', text: i18n.ts.save,
bg: 'var(--bg)', handler: save,
actions: [{ }]);
asFullButton: true,
icon: 'fas fa-check', const headerTabs = $computed(() => []);
text: i18n.ts.save,
handler: save, definePageMetadata({
}], title: i18n.ts.general,
} icon: 'fas fa-cog',
bg: 'var(--bg)',
}); });
</script> </script>

View file

@ -1,76 +1,84 @@
<template> <template>
<div class="lknzcolw"> <div>
<div class="users"> <MkStickyContainer>
<div class="inputs"> <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSelect v-model="sort" style="flex: 1;"> <MkSpacer :content-max="900">
<template #label>{{ $ts.sort }}</template> <div class="lknzcolw">
<option value="-createdAt">{{ $ts.registeredDate }} ({{ $ts.ascendingOrder }})</option> <div class="users">
<option value="+createdAt">{{ $ts.registeredDate }} ({{ $ts.descendingOrder }})</option> <div class="inputs">
<option value="-updatedAt">{{ $ts.lastUsed }} ({{ $ts.ascendingOrder }})</option> <MkSelect v-model="sort" style="flex: 1;">
<option value="+updatedAt">{{ $ts.lastUsed }} ({{ $ts.descendingOrder }})</option> <template #label>{{ $ts.sort }}</template>
</MkSelect> <option value="-createdAt">{{ $ts.registeredDate }} ({{ $ts.ascendingOrder }})</option>
<MkSelect v-model="state" style="flex: 1;"> <option value="+createdAt">{{ $ts.registeredDate }} ({{ $ts.descendingOrder }})</option>
<template #label>{{ $ts.state }}</template> <option value="-updatedAt">{{ $ts.lastUsed }} ({{ $ts.ascendingOrder }})</option>
<option value="all">{{ $ts.all }}</option> <option value="+updatedAt">{{ $ts.lastUsed }} ({{ $ts.descendingOrder }})</option>
<option value="available">{{ $ts.normal }}</option> </MkSelect>
<option value="admin">{{ $ts.administrator }}</option> <MkSelect v-model="state" style="flex: 1;">
<option value="moderator">{{ $ts.moderator }}</option> <template #label>{{ $ts.state }}</template>
<option value="silenced">{{ $ts.silence }}</option> <option value="all">{{ $ts.all }}</option>
<option value="suspended">{{ $ts.suspend }}</option> <option value="available">{{ $ts.normal }}</option>
</MkSelect> <option value="admin">{{ $ts.administrator }}</option>
<MkSelect v-model="origin" style="flex: 1;"> <option value="moderator">{{ $ts.moderator }}</option>
<template #label>{{ $ts.instance }}</template> <option value="silenced">{{ $ts.silence }}</option>
<option value="combined">{{ $ts.all }}</option> <option value="suspended">{{ $ts.suspend }}</option>
<option value="local">{{ $ts.local }}</option> </MkSelect>
<option value="remote">{{ $ts.remote }}</option> <MkSelect v-model="origin" style="flex: 1;">
</MkSelect> <template #label>{{ $ts.instance }}</template>
</div> <option value="combined">{{ $ts.all }}</option>
<div class="inputs"> <option value="local">{{ $ts.local }}</option>
<MkInput v-model="searchUsername" style="flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.users.reload()"> <option value="remote">{{ $ts.remote }}</option>
<template #prefix>@</template> </MkSelect>
<template #label>{{ $ts.username }}</template> </div>
</MkInput> <div class="inputs">
<MkInput v-model="searchHost" style="flex: 1;" type="text" spellcheck="false" :disabled="pagination.params.origin === 'local'" @update:modelValue="$refs.users.reload()"> <MkInput v-model="searchUsername" style="flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.users.reload()">
<template #prefix>@</template> <template #prefix>@</template>
<template #label>{{ $ts.host }}</template> <template #label>{{ $ts.username }}</template>
</MkInput> </MkInput>
</div> <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"> <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)"> <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"/> <MkAvatar class="avatar" :user="user" :disable-link="true" :show-indicator="true"/>
<div class="body"> <div class="body">
<header> <header>
<MkUserName class="name" :user="user"/> <MkUserName class="name" :user="user"/>
<span class="acct">@{{ acct(user) }}</span> <span class="acct">@{{ acct(user) }}</span>
<span v-if="user.isAdmin" class="staff"><i class="fas fa-bookmark"></i></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.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.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> <span v-if="user.isSuspended" class="punished"><i class="fas fa-snowflake"></i></span>
</header> </header>
<div> <div>
<span>{{ $ts.lastUsed }}: <MkTime v-if="user.updatedAt" :time="user.updatedAt" mode="detail"/></span> <span>{{ $ts.lastUsed }}: <MkTime v-if="user.updatedAt" :time="user.updatedAt" mode="detail"/></span>
</div> </div>
<div> <div>
<span>{{ $ts.registeredDate }}: <MkTime :time="user.createdAt" mode="detail"/></span> <span>{{ $ts.registeredDate }}: <MkTime :time="user.createdAt" mode="detail"/></span>
</div> </div>
</div>
</button>
</MkPagination>
</div> </div>
</button> </div>
</MkPagination> </MkSpacer>
</div> </MkStickyContainer>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'; import { computed } from 'vue';
import XHeader from './_header_.vue';
import MkInput from '@/components/form/input.vue'; import MkInput from '@/components/form/input.vue';
import MkSelect from '@/components/form/select.vue'; import MkSelect from '@/components/form/select.vue';
import MkPagination from '@/components/ui/pagination.vue'; import MkPagination from '@/components/ui/pagination.vue';
import { acct } from '@/filters/user'; import { acct } from '@/filters/user';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols';
import { lookupUser } from '@/scripts/lookup-user'; import { lookupUser } from '@/scripts/lookup-user';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let paginationComponent = $ref<InstanceType<typeof MkPagination>>(); let paginationComponent = $ref<InstanceType<typeof MkPagination>>();
@ -89,7 +97,7 @@ const pagination = {
username: searchUsername, username: searchUsername,
hostname: searchHost, hostname: searchHost,
})), })),
offsetMode: true offsetMode: true,
}; };
function searchUser() { function searchUser() {
@ -106,7 +114,7 @@ async function addUser() {
const { canceled: canceled2, result: password } = await os.inputText({ const { canceled: canceled2, result: password } = await os.inputText({
title: i18n.ts.password, title: i18n.ts.password,
type: 'password' type: 'password',
}); });
if (canceled2) return; if (canceled2) return;
@ -122,34 +130,34 @@ function show(user) {
os.pageWindow(`/user-info/${user.id}`); os.pageWindow(`/user-info/${user.id}`);
} }
defineExpose({ const headerActions = $computed(() => [{
[symbols.PAGE_INFO]: computed(() => ({ icon: 'fas fa-search',
title: i18n.ts.users, text: i18n.ts.search,
icon: 'fas fa-users', handler: searchUser,
bg: 'var(--bg)', }, {
actions: [{ asFullButton: true,
icon: 'fas fa-search', icon: 'fas fa-plus',
text: i18n.ts.search, text: i18n.ts.addUser,
handler: searchUser handler: addUser,
}, { }, {
asFullButton: true, asFullButton: true,
icon: 'fas fa-plus', icon: 'fas fa-search',
text: i18n.ts.addUser, text: i18n.ts.lookup,
handler: addUser handler: lookupUser,
}, { }]);
asFullButton: true,
icon: 'fas fa-search', const headerTabs = $computed(() => []);
text: i18n.ts.lookup,
handler: lookupUser definePageMetadata(computed(() => ({
}], title: i18n.ts.users,
})), icon: 'fas fa-users',
}); bg: 'var(--bg)',
})));
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.lknzcolw { .lknzcolw {
> .users { > .users {
margin: var(--margin);
> .inputs { > .inputs {
display: flex; display: flex;

View file

@ -1,57 +1,53 @@
<template> <template>
<MkSpacer :content-max="800"> <MkStickyContainer>
<MkPagination v-slot="{items}" :pagination="pagination" class="ruryvtyk _content"> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<section v-for="(announcement, i) in items" :key="announcement.id" class="_card announcement"> <MkSpacer :content-max="800">
<div class="_title"><span v-if="$i && !announcement.isRead">🆕 </span>{{ announcement.title }}</div> <MkPagination v-slot="{items}" :pagination="pagination" class="ruryvtyk _content">
<div class="_content"> <section v-for="(announcement, i) in items" :key="announcement.id" class="_card announcement">
<Mfm :text="announcement.text"/> <div class="_title"><span v-if="$i && !announcement.isRead">🆕 </span>{{ announcement.title }}</div>
<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/> <div class="_content">
</div> <Mfm :text="announcement.text"/>
<div v-if="$i && !announcement.isRead" class="_footer"> <img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
<MkButton primary @click="read(items, announcement, i)"><i class="fas fa-check"></i> {{ $ts.gotIt }}</MkButton> </div>
</div> <div v-if="$i && !announcement.isRead" class="_footer">
</section> <MkButton primary @click="read(items, announcement, i)"><i class="fas fa-check"></i> {{ $ts.gotIt }}</MkButton>
</MkPagination> </div>
</MkSpacer> </section>
</MkPagination>
</MkSpacer>
</MkStickyContainer>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from 'vue'; import { } from 'vue';
import MkPagination from '@/components/ui/pagination.vue'; import MkPagination from '@/components/ui/pagination.vue';
import MkButton from '@/components/ui/button.vue'; import MkButton from '@/components/ui/button.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
export default defineComponent({ const pagination = {
components: { endpoint: 'announcements' as const,
MkPagination, limit: 10,
MkButton };
},
data() { // TODO:
return { function read(items, announcement, i) {
[symbols.PAGE_INFO]: { items[i] = {
title: this.$ts.announcements, ...announcement,
icon: 'fas fa-broadcast-tower', isRead: true,
bg: 'var(--bg)', };
}, os.api('i/read-announcement', { announcementId: announcement.id });
pagination: { }
endpoint: 'announcements' as const,
limit: 10,
},
};
},
methods: { const headerActions = $computed(() => []);
// TODO:
read(items, announcement, i) { const headerTabs = $computed(() => []);
items[i] = {
...announcement, definePageMetadata({
isRead: true, title: i18n.ts.announcements,
}; icon: 'fas fa-broadcast-tower',
os.api('i/read-announcement', { announcementId: announcement.id }); bg: 'var(--bg)',
},
}
}); });
</script> </script>

View file

@ -1,8 +1,9 @@
<template> <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 v-if="queue > 0" class="new"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div>
<div class="tl _block"> <div class="tl _block">
<XTimeline ref="tl" :key="antennaId" <XTimeline
ref="tlEl" :key="antennaId"
class="tl" class="tl"
src="antenna" src="antenna"
:antenna="antennaId" :antenna="antennaId"
@ -13,92 +14,78 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, defineAsyncComponent, computed } from 'vue'; import { computed, inject, watch } from 'vue';
import XTimeline from '@/components/timeline.vue'; import XTimeline from '@/components/timeline.vue';
import { scroll } from '@/scripts/scroll'; import { scroll } from '@/scripts/scroll';
import * as os from '@/os'; 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({ const router = useRouter();
components: {
XTimeline,
},
props: { const props = defineProps<{
antennaId: { antennaId: string;
type: String, }>();
required: true
}
},
data() { let antenna = $ref(null);
return { let queue = $ref(0);
antenna: null, let rootEl = $ref<HTMLElement>();
queue: 0, let tlEl = $ref<InstanceType<typeof XTimeline>>();
[symbols.PAGE_INFO]: computed(() => this.antenna ? { const keymap = $computed(() => ({
title: this.antenna.name, 't': focus,
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),
};
},
computed: { function queueUpdated(q) {
keymap(): any { queue = q;
return { }
't': this.focus
};
},
},
watch: { function top() {
antennaId: { scroll(rootEl, { top: 0 });
async handler() { }
this.antenna = await os.api('antennas/show', {
antennaId: this.antennaId
});
},
immediate: true
}
},
methods: { async function timetravel() {
queueUpdated(q) { const { canceled, result: date } = await os.inputDate({
this.queue = q; title: i18n.ts.date,
}, });
if (canceled) return;
top() { tlEl.timetravel(date);
scroll(this.$el, { top: 0 }); }
},
async timetravel() { function settings() {
const { canceled, result: date } = await os.inputDate({ router.push(`/my/antennas/${props.antennaId}`);
title: this.$ts.date, }
});
if (canceled) return;
this.$refs.tl.timetravel(date); function focus() {
}, tlEl.focus();
}
settings() { watch(() => props.antennaId, async () => {
this.$router.push(`/my/antennas/${this.antennaId}`); antenna = await os.api('antennas/show', {
}, antennaId: props.antennaId,
});
}, { immediate: true });
focus() { const headerActions = $computed(() => []);
(this.$refs.tl as any).focus();
} 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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -1,40 +1,43 @@
<template> <template>
<MkSpacer :content-max="700"> <MkStickyContainer>
<div class="_formRoot"> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<div class="_formBlock"> <MkSpacer :content-max="700">
<MkInput v-model="endpoint" :datalist="endpoints" class="_formBlock" @update:modelValue="onEndpointChange()"> <div class="_formRoot">
<template #label>Endpoint</template> <div class="_formBlock">
</MkInput> <MkInput v-model="endpoint" :datalist="endpoints" class="_formBlock" @update:modelValue="onEndpointChange()">
<MkTextarea v-model="body" class="_formBlock" code> <template #label>Endpoint</template>
<template #label>Params (JSON or JSON5)</template> </MkInput>
</MkTextarea> <MkTextarea v-model="body" class="_formBlock" code>
<MkSwitch v-model="withCredential" class="_formBlock"> <template #label>Params (JSON or JSON5)</template>
With credential </MkTextarea>
</MkSwitch> <MkSwitch v-model="withCredential" class="_formBlock">
<MkButton class="_formBlock" primary :disabled="sending" @click="send"> With credential
<template v-if="sending"><MkEllipsis/></template> </MkSwitch>
<template v-else><i class="fas fa-paper-plane"></i> Send</template> <MkButton class="_formBlock" primary :disabled="sending" @click="send">
</MkButton> <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>
<div v-if="res" class="_formBlock"> </MkSpacer>
<MkTextarea v-model="res" code readonly tall> </MkStickyContainer>
<template #label>Response</template>
</MkTextarea>
</div>
</div>
</MkSpacer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { ref } from 'vue';
import JSON5 from 'json5'; import JSON5 from 'json5';
import { Endpoints } from 'misskey-js';
import MkButton from '@/components/ui/button.vue'; import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue'; import MkInput from '@/components/form/input.vue';
import MkTextarea from '@/components/form/textarea.vue'; import MkTextarea from '@/components/form/textarea.vue';
import MkSwitch from '@/components/form/switch.vue'; import MkSwitch from '@/components/form/switch.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols'; import { definePageMetadata } from '@/scripts/page-metadata';
import { Endpoints } from 'misskey-js';
const body = ref('{}'); const body = ref('{}');
const endpoint = ref(''); const endpoint = ref('');
@ -75,10 +78,12 @@ function onEndpointChange() {
}); });
} }
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: {
title: 'API console', const headerTabs = $computed(() => []);
icon: 'fas fa-terminal'
}, definePageMetadata({
title: 'API console',
icon: 'fas fa-terminal',
}); });
</script> </script>

View file

@ -15,7 +15,7 @@
<h1>{{ $ts._auth.denied }}</h1> <h1>{{ $ts._auth.denied }}</h1>
</div> </div>
<div v-if="state == 'accepted'" class="accepted"> <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.callback }}<MkEllipsis/></p>
<p v-if="!session.app.callbackUrl">{{ $ts._auth.pleaseGoBack }}</p> <p v-if="!session.app.callbackUrl">{{ $ts._auth.pleaseGoBack }}</p>
</div> </div>
@ -40,24 +40,20 @@ export default defineComponent({
XForm, XForm,
MkSignin, MkSignin,
}, },
props: ['token'],
data() { data() {
return { return {
state: null, state: null,
session: null, session: null,
fetching: true fetching: true,
}; };
}, },
computed: {
token(): string {
return this.$route.params.token;
}
},
mounted() { mounted() {
if (!this.$i) return; if (!this.$i) return;
// Fetch session // Fetch session
os.api('auth/session/show', { os.api('auth/session/show', {
token: this.token token: this.token,
}).then(session => { }).then(session => {
this.session = session; this.session = session;
this.fetching = false; this.fetching = false;
@ -65,7 +61,7 @@ export default defineComponent({
// //
if (this.session.app.isAuthorized) { if (this.session.app.isAuthorized) {
os.api('auth/accept', { os.api('auth/accept', {
token: this.session.token token: this.session.token,
}).then(() => { }).then(() => {
this.accepted(); this.accepted();
}); });
@ -85,8 +81,8 @@ export default defineComponent({
} }
}, onLogin(res) { }, onLogin(res) {
login(res.i); login(res.i);
} },
} },
}); });
</script> </script>

View file

@ -1,127 +1,122 @@
<template> <template>
<MkSpacer :content-max="700"> <MkStickyContainer>
<div class="_formRoot"> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkInput v-model="name" class="_formBlock"> <MkSpacer :content-max="700">
<template #label>{{ $ts.name }}</template> <div class="_formRoot">
</MkInput> <MkInput v-model="name" class="_formBlock">
<template #label>{{ $ts.name }}</template>
</MkInput>
<MkTextarea v-model="description" class="_formBlock"> <MkTextarea v-model="description" class="_formBlock">
<template #label>{{ $ts.description }}</template> <template #label>{{ $ts.description }}</template>
</MkTextarea> </MkTextarea>
<div class="banner"> <div class="banner">
<MkButton v-if="bannerId == null" @click="setBannerImage"><i class="fas fa-plus"></i> {{ $ts._channel.setBanner }}</MkButton> <MkButton v-if="bannerId == null" @click="setBannerImage"><i class="fas fa-plus"></i> {{ $ts._channel.setBanner }}</MkButton>
<div v-else-if="bannerUrl"> <div v-else-if="bannerUrl">
<img :src="bannerUrl" style="width: 100%;"/> <img :src="bannerUrl" style="width: 100%;"/>
<MkButton @click="removeBannerImage()"><i class="fas fa-trash-alt"></i> {{ $ts._channel.removeBanner }}</MkButton> <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> </div>
<div class="_formBlock"> </MkSpacer>
<MkButton primary @click="save()"><i class="fas fa-save"></i> {{ channelId ? $ts.save : $ts.create }}</MkButton> </MkStickyContainer>
</div>
</div>
</MkSpacer>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { computed, defineComponent } from 'vue'; import { computed, inject, watch } from 'vue';
import MkTextarea from '@/components/form/textarea.vue'; import MkTextarea from '@/components/form/textarea.vue';
import MkButton from '@/components/ui/button.vue'; import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue'; import MkInput from '@/components/form/input.vue';
import { selectFile } from '@/scripts/select-file'; import { selectFile } from '@/scripts/select-file';
import * as os from '@/os'; 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({ const router = useRouter();
components: {
MkTextarea, MkButton, MkInput,
},
props: { const props = defineProps<{
channelId: { channelId?: string;
type: String, }>();
required: false
},
},
data() { let channel = $ref(null);
return { let name = $ref(null);
[symbols.PAGE_INFO]: computed(() => this.channelId ? { let description = $ref(null);
title: this.$ts._channel.edit, let bannerUrl = $ref<string | null>(null);
icon: 'fas fa-satellite-dish', let bannerId = $ref<string | null>(null);
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,
};
},
watch: { watch(() => bannerId, async () => {
async bannerId() { if (bannerId == null) {
if (this.bannerId == null) { bannerUrl = null;
this.bannerUrl = null; } else {
} else { bannerUrl = (await os.api('drive/files/show', {
this.bannerUrl = (await os.api('drive/files/show', { fileId: bannerId,
fileId: this.bannerId, })).url;
})).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;
}
} }
}); });
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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -1,98 +1,87 @@
<template> <template>
<MkSpacer :content-max="700"> <MkStickyContainer>
<div v-if="channel"> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<div class="wpgynlbz _panel _gap" :class="{ hide: !showBanner }"> <MkSpacer :content-max="700">
<XChannelFollowButton :channel="channel" :full="true" class="subscribe"/> <div v-if="channel">
<button class="_button toggle" @click="() => showBanner = !showBanner"> <div class="wpgynlbz _panel _gap" :class="{ hide: !showBanner }">
<template v-if="showBanner"><i class="fas fa-angle-up"></i></template> <XChannelFollowButton :channel="channel" :full="true" class="subscribe"/>
<template v-else><i class="fas fa-angle-down"></i></template> <button class="_button toggle" @click="() => showBanner = !showBanner">
</button> <template v-if="showBanner"><i class="fas fa-angle-up"></i></template>
<div v-if="!showBanner" class="hideOverlay"> <template v-else><i class="fas fa-angle-down"></i></template>
</div> </button>
<div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : null }" class="banner"> <div v-if="!showBanner" class="hideOverlay">
<div class="status"> </div>
<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 :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : null }" class="banner">
<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 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>
<div class="fade"></div>
</div>
<div v-if="channel.description" class="description">
<Mfm :text="channel.description" :is-note="false" :i="$i"/>
</div> </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> </div>
</MkSpacer>
<XPostForm v-if="$i" :channel="channel" class="post-form _panel _gap" fixed/> </MkStickyContainer>
<XTimeline :key="channelId" class="_gap" src="channel" :channel="channelId" @before="before" @after="after"/>
</div>
</MkSpacer>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { computed, defineComponent } from 'vue'; import { computed, inject, watch } from 'vue';
import MkContainer from '@/components/ui/container.vue'; import MkContainer from '@/components/ui/container.vue';
import XPostForm from '@/components/post-form.vue'; import XPostForm from '@/components/post-form.vue';
import XTimeline from '@/components/timeline.vue'; import XTimeline from '@/components/timeline.vue';
import XChannelFollowButton from '@/components/channel-follow-button.vue'; import XChannelFollowButton from '@/components/channel-follow-button.vue';
import * as os from '@/os'; 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({ const router = useRouter();
components: {
MkContainer,
XPostForm,
XTimeline,
XChannelFollowButton
},
props: { const props = defineProps<{
channelId: { channelId: string;
type: String, }>();
required: true
}
},
data() { let channel = $ref(null);
return { let showBanner = $ref(true);
[symbols.PAGE_INFO]: computed(() => this.channel ? { const pagination = {
title: this.channel.name, endpoint: 'channels/timeline' as const,
icon: 'fas fa-satellite-dish', limit: 10,
bg: 'var(--bg)', params: computed(() => ({
actions: [...(this.$i && this.$i.id === this.channel.userId ? [{ channelId: props.channelId,
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,
}))
},
};
},
watch: { watch(() => props.channelId, async () => {
channelId: { channel = await os.api('channels/show', {
async handler() { channelId: props.channelId,
this.channel = await os.api('channels/show', { });
channelId: this.channelId, }, { immediate: true });
});
},
immediate: true
}
},
methods: { function edit() {
edit() { router.push(`/channels/${channel.id}/edit`);
this.$router.push(`/channels/${this.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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -1,82 +1,83 @@
<template> <template>
<MkSpacer :content-max="700"> <MkStickyContainer>
<div v-if="tab === 'featured'" class="_content grwlizim featured"> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkPagination v-slot="{items}" :pagination="featuredPagination"> <MkSpacer :content-max="700">
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/> <div v-if="tab === 'featured'" class="_content grwlizim featured">
</MkPagination> <MkPagination v-slot="{items}" :pagination="featuredPagination">
</div> <MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/>
<div v-else-if="tab === 'following'" class="_content grwlizim following"> </MkPagination>
<MkPagination v-slot="{items}" :pagination="followingPagination"> </div>
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/> <div v-else-if="tab === 'following'" class="_content grwlizim following">
</MkPagination> <MkPagination v-slot="{items}" :pagination="followingPagination">
</div> <MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/>
<div v-else-if="tab === 'owned'" class="_content grwlizim owned"> </MkPagination>
<MkButton class="new" @click="create()"><i class="fas fa-plus"></i></MkButton> </div>
<MkPagination v-slot="{items}" :pagination="ownedPagination"> <div v-else-if="tab === 'owned'" class="_content grwlizim owned">
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/> <MkButton class="new" @click="create()"><i class="fas fa-plus"></i></MkButton>
</MkPagination> <MkPagination v-slot="{items}" :pagination="ownedPagination">
</div> <MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/>
</MkSpacer> </MkPagination>
</div>
</MkSpacer>
</MkStickyContainer>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { computed, defineComponent } from 'vue'; import { computed, defineComponent, inject } from 'vue';
import MkChannelPreview from '@/components/channel-preview.vue'; import MkChannelPreview from '@/components/channel-preview.vue';
import MkPagination from '@/components/ui/pagination.vue'; import MkPagination from '@/components/ui/pagination.vue';
import MkButton from '@/components/ui/button.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({ const router = useRouter();
components: {
MkChannelPreview, MkPagination, MkButton, let tab = $ref('featured');
},
data() { const featuredPagination = {
return { endpoint: 'channels/featured' as const,
[symbols.PAGE_INFO]: computed(() => ({ noPaging: true,
title: this.$ts.channel, };
icon: 'fas fa-satellite-dish', const followingPagination = {
bg: 'var(--bg)', endpoint: 'channels/followed' as const,
actions: [{ limit: 5,
icon: 'fas fa-plus', };
text: this.$ts.create, const ownedPagination = {
handler: this.create, endpoint: 'channels/owned' as const,
}], limit: 5,
tabs: [{ };
active: this.tab === 'featured',
title: this.$ts._channel.featured, function create() {
icon: 'fas fa-fire-alt', router.push('/channels/new');
onClick: () => { this.tab = 'featured'; }, }
}, {
active: this.tab === 'following', const headerActions = $computed(() => [{
title: this.$ts._channel.following, icon: 'fas fa-plus',
icon: 'fas fa-heart', text: i18n.ts.create,
onClick: () => { this.tab = 'following'; }, handler: create,
}, { }]);
active: this.tab === 'owned',
title: this.$ts._channel.owned, const headerTabs = $computed(() => [{
icon: 'fas fa-edit', active: tab === 'featured',
onClick: () => { this.tab = 'owned'; }, title: i18n.ts._channel.featured,
},] icon: 'fas fa-fire-alt',
})), onClick: () => { tab = 'featured'; },
tab: 'featured', }, {
featuredPagination: { active: tab === 'following',
endpoint: 'channels/featured' as const, title: i18n.ts._channel.following,
noPaging: true, icon: 'fas fa-heart',
}, onClick: () => { tab = 'following'; },
followingPagination: { }, {
endpoint: 'channels/followed' as const, active: tab === 'owned',
limit: 5, title: i18n.ts._channel.owned,
}, icon: 'fas fa-edit',
ownedPagination: { onClick: () => { tab = 'owned'; },
endpoint: 'channels/owned' as const, }]);
limit: 5,
}, definePageMetadata(computed(() => ({
}; title: i18n.ts.channel,
}, icon: 'fas fa-satellite-dish',
methods: { bg: 'var(--bg)',
create() { })));
this.$router.push(`/channels/new`);
}
}
});
</script> </script>

View file

@ -1,18 +1,21 @@
<template> <template>
<MkSpacer :content-max="800"> <MkStickyContainer>
<div v-if="clip"> <template #header><MkPageHeader :actions="headerActions"/></template>
<div class="okzinsic _panel"> <MkSpacer :content-max="800">
<div v-if="clip.description" class="description"> <div v-if="clip">
<Mfm :text="clip.description" :is-note="false" :i="$i"/> <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>
<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"/> <XNotes :pagination="pagination" :detail="true"/>
</div> </div>
</MkSpacer> </MkSpacer>
</MkStickyContainer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -22,7 +25,7 @@ import XNotes from '@/components/notes.vue';
import { $i } from '@/account'; import { $i } from '@/account';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols'; import { definePageMetadata } from '@/scripts/page-metadata';
const props = defineProps<{ const props = defineProps<{
clipId: string, clipId: string,
@ -49,59 +52,58 @@ watch(() => props.clipId, async () => {
provide('currentClipPage', $$(clip)); provide('currentClipPage', $$(clip));
defineExpose({ const headerActions = $computed(() => clip && isOwned ? [{
[symbols.PAGE_INFO]: computed(() => clip ? { icon: 'fas fa-pencil-alt',
title: clip.name, text: i18n.ts.edit,
icon: 'fas fa-paperclip', handler: async (): Promise<void> => {
bg: 'var(--bg)', const { canceled, result } = await os.form(clip.name, {
actions: isOwned ? [{ name: {
icon: 'fas fa-pencil-alt', type: 'string',
text: i18n.ts.edit, label: i18n.ts.name,
handler: async (): Promise<void> => { default: clip.name,
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,
});
}, },
}, { description: {
icon: 'fas fa-trash-alt', type: 'string',
text: i18n.ts.delete, required: false,
danger: true, multiline: true,
handler: async (): Promise<void> => { label: i18n.ts.description,
const { canceled } = await os.confirm({ default: clip.description,
type: 'warning',
text: i18n.t('deleteAreYouSure', { x: clip.name }),
});
if (canceled) return;
await os.apiWithDialog('clips/delete', {
clipId: clip.id,
});
}, },
}] : [], isPublic: {
} : null), 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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -8,17 +8,19 @@
import { computed } from 'vue'; import { computed } from 'vue';
import XDrive from '@/components/drive.vue'; import XDrive from '@/components/drive.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let folder = $ref(null); let folder = $ref(null);
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: computed(() => ({
title: folder ? folder.name : i18n.ts.drive, const headerTabs = $computed(() => []);
icon: 'fas fa-cloud',
bg: 'var(--bg)', definePageMetadata(computed(() => ({
hideHeader: true, title: folder ? folder.name : i18n.ts.drive,
})), icon: 'fas fa-cloud',
}); bg: 'var(--bg)',
hideHeader: true,
})));
</script> </script>

View file

@ -36,7 +36,6 @@ import MkSelect from '@/components/form/select.vue';
import MkFolder from '@/components/ui/folder.vue'; import MkFolder from '@/components/ui/folder.vue';
import MkTab from '@/components/tab.vue'; import MkTab from '@/components/tab.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols';
import { emojiCategories, emojiTags } from '@/instance'; import { emojiCategories, emojiTags } from '@/instance';
import XEmoji from './emojis.emoji.vue'; import XEmoji from './emojis.emoji.vue';

View file

@ -1,15 +1,18 @@
<template> <template>
<div :class="$style.root"> <MkStickyContainer>
<XCategory v-if="tab === 'category'"/> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
</div> <div :class="$style.root">
<XCategory v-if="tab === 'category'"/>
</div>
</MkStickyContainer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import XCategory from './emojis.category.vue'; import XCategory from './emojis.category.vue';
import * as os from '@/os';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const tab = ref('category'); const tab = ref('category');
@ -31,20 +34,21 @@ function menu(ev) {
text: err.message, text: err.message,
}); });
}); });
} },
}], ev.currentTarget ?? ev.target); }], ev.currentTarget ?? ev.target);
} }
defineExpose({ const headerActions = $computed(() => [{
[symbols.PAGE_INFO]: { icon: 'fas fa-ellipsis-h',
title: i18n.ts.customEmojis, handler: menu,
icon: 'fas fa-laugh', }]);
bg: 'var(--bg)',
actions: [{ const headerTabs = $computed(() => []);
icon: 'fas fa-ellipsis-h',
handler: menu, definePageMetadata({
}], title: i18n.ts.customEmojis,
}, icon: 'fas fa-laugh',
bg: 'var(--bg)',
}); });
</script> </script>

View file

@ -1,11 +1,12 @@
<template> <template>
<div> <MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="1200"> <MkSpacer :content-max="1200">
<div class="lznhrdub"> <div class="lznhrdub">
<div v-if="tab === 'local'"> <div v-if="tab === 'local'">
<div v-if="meta && stats && tag == null" class="localfedi7 _block _isolated" :style="{ backgroundImage: meta.bannerUrl ? `url(${meta.bannerUrl})` : null }"> <div v-if="instance && stats && tag == null" class="localfedi7 _block _isolated" :style="{ backgroundImage: instance.bannerUrl ? `url(${instance.bannerUrl})` : null }">
<header><span>{{ $t('explore', { host: meta.name || 'Misskey' }) }}</span></header> <header><span>{{ $t('explore', { host: instance.name || 'Misskey' }) }}</span></header>
<div><span>{{ $t('exploreUsersCount', { count: num(stats.originalUsersCount) }) }}</span></div> <div><span>{{ $t('exploreUsersCount', { count: number(stats.originalUsersCount) }) }}</span></div>
</div> </div>
<template v-if="tag == null"> <template v-if="tag == null">
@ -32,7 +33,7 @@
<header><span>{{ $ts.exploreFediverse }}</span></header> <header><span>{{ $ts.exploreFediverse }}</span></header>
</div> </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> <template #header><i class="fas fa-hashtag fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularTags }}</template>
<div class="vxjfqztj"> <div class="vxjfqztj">
@ -74,147 +75,127 @@
</MkRadios> </MkRadios>
</div> </div>
<XUserList v-if="searchQuery" ref="search" class="_gap" :pagination="searchPagination"/> <XUserList v-if="searchQuery" ref="searchEl" class="_gap" :pagination="searchPagination"/>
</div> </div>
</div> </div>
</MkSpacer> </MkSpacer>
</div> </MkStickyContainer>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { computed, defineComponent } from 'vue'; import { computed, defineComponent, watch } from 'vue';
import XUserList from '@/components/user-list.vue'; import XUserList from '@/components/user-list.vue';
import MkFolder from '@/components/ui/folder.vue'; import MkFolder from '@/components/ui/folder.vue';
import MkInput from '@/components/form/input.vue'; import MkInput from '@/components/form/input.vue';
import MkRadios from '@/components/form/radios.vue'; import MkRadios from '@/components/form/radios.vue';
import number from '@/filters/number'; import number from '@/filters/number';
import * as os from '@/os'; 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({ const props = defineProps<{
components: { tag?: string;
XUserList, }>();
MkFolder,
MkInput,
MkRadios,
},
props: { let tab = $ref('local');
tag: { let tagsEl = $ref<InstanceType<typeof MkFolder>>();
type: String, let tagsLocal = $ref([]);
required: false let tagsRemote = $ref([]);
} let stats = $ref(null);
}, let searchQuery = $ref(null);
let searchOrigin = $ref('combined');
data() { watch(() => props.tag, () => {
return { if (tagsEl) tagsEl.toggleContent(props.tag == null);
[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;
});
},
}); });
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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -1,20 +1,23 @@
<template> <template>
<MkSpacer :content-max="800"> <MkStickyContainer>
<MkPagination ref="pagingComponent" :pagination="pagination"> <template #header><MkPageHeader/></template>
<template #empty> <MkSpacer :content-max="800">
<div class="_fullinfo"> <MkPagination ref="pagingComponent" :pagination="pagination">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> <template #empty>
<div>{{ $ts.noNotes }}</div> <div class="_fullinfo">
</div> <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
</template> <div>{{ $ts.noNotes }}</div>
</div>
</template>
<template #default="{ items }"> <template #default="{ items }">
<XList v-slot="{ item }" :items="items" :direction="'down'" :no-gap="false" :ad="false"> <XList v-slot="{ item }" :items="items" :direction="'down'" :no-gap="false" :ad="false">
<XNote :key="item.id" :note="item.note" :class="$style.note"/> <XNote :key="item.id" :note="item.note" :class="$style.note"/>
</XList> </XList>
</template> </template>
</MkPagination> </MkPagination>
</MkSpacer> </MkSpacer>
</MkStickyContainer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -22,8 +25,8 @@ import { ref } from 'vue';
import MkPagination from '@/components/ui/pagination.vue'; import MkPagination from '@/components/ui/pagination.vue';
import XNote from '@/components/note.vue'; import XNote from '@/components/note.vue';
import XList from '@/components/date-separated-list.vue'; import XList from '@/components/date-separated-list.vue';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const pagination = { const pagination = {
endpoint: 'i/favorites' as const, endpoint: 'i/favorites' as const,
@ -32,12 +35,10 @@ const pagination = {
const pagingComponent = ref<InstanceType<typeof MkPagination>>(); const pagingComponent = ref<InstanceType<typeof MkPagination>>();
defineExpose({ definePageMetadata({
[symbols.PAGE_INFO]: { title: i18n.ts.favorites,
title: i18n.ts.favorites, icon: 'fas fa-star',
icon: 'fas fa-star', bg: 'var(--bg)',
bg: 'var(--bg)',
},
}); });
</script> </script>

View file

@ -1,13 +1,16 @@
<template> <template>
<MkSpacer :content-max="800"> <MkStickyContainer>
<XNotes ref="notes" :pagination="pagination"/> <template #header><MkPageHeader/></template>
</MkSpacer> <MkSpacer :content-max="800">
<XNotes ref="notes" :pagination="pagination"/>
</MkSpacer>
</MkStickyContainer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import XNotes from '@/components/notes.vue'; import XNotes from '@/components/notes.vue';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const pagination = { const pagination = {
endpoint: 'notes/featured' as const, endpoint: 'notes/featured' as const,
@ -15,11 +18,9 @@ const pagination = {
offsetMode: true, offsetMode: true,
}; };
defineExpose({ definePageMetadata({
[symbols.PAGE_INFO]: { title: i18n.ts.featured,
title: i18n.ts.featured, icon: 'fas fa-fire-alt',
icon: 'fas fa-fire-alt', bg: 'var(--bg)',
bg: 'var(--bg)',
},
}); });
</script> </script>

View file

@ -1,94 +1,97 @@
<template> <template>
<MkSpacer :content-max="1000"> <MkStickyContainer>
<div class="taeiyria"> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<div class="query"> <MkSpacer :content-max="1000">
<MkInput v-model="host" :debounce="true" class=""> <div class="taeiyria">
<template #prefix><i class="fas fa-search"></i></template> <div class="query">
<template #label>{{ $ts.host }}</template> <MkInput v-model="host" :debounce="true" class="">
</MkInput> <template #prefix><i class="fas fa-search"></i></template>
<FormSplit style="margin-top: var(--margin);"> <template #label>{{ $ts.host }}</template>
<MkSelect v-model="state"> </MkInput>
<template #label>{{ $ts.state }}</template> <FormSplit style="margin-top: var(--margin);">
<option value="all">{{ $ts.all }}</option> <MkSelect v-model="state">
<option value="federating">{{ $ts.federating }}</option> <template #label>{{ $ts.state }}</template>
<option value="subscribing">{{ $ts.subscribing }}</option> <option value="all">{{ $ts.all }}</option>
<option value="publishing">{{ $ts.publishing }}</option> <option value="federating">{{ $ts.federating }}</option>
<option value="suspended">{{ $ts.suspended }}</option> <option value="subscribing">{{ $ts.subscribing }}</option>
<option value="blocked">{{ $ts.blocked }}</option> <option value="publishing">{{ $ts.publishing }}</option>
<option value="notResponding">{{ $ts.notResponding }}</option> <option value="suspended">{{ $ts.suspended }}</option>
</MkSelect> <option value="blocked">{{ $ts.blocked }}</option>
<MkSelect v-model="sort"> <option value="notResponding">{{ $ts.notResponding }}</option>
<template #label>{{ $ts.sort }}</template> </MkSelect>
<option value="+pubSub">{{ $ts.pubSub }} ({{ $ts.descendingOrder }})</option> <MkSelect v-model="sort">
<option value="-pubSub">{{ $ts.pubSub }} ({{ $ts.ascendingOrder }})</option> <template #label>{{ $ts.sort }}</template>
<option value="+notes">{{ $ts.notes }} ({{ $ts.descendingOrder }})</option> <option value="+pubSub">{{ $ts.pubSub }} ({{ $ts.descendingOrder }})</option>
<option value="-notes">{{ $ts.notes }} ({{ $ts.ascendingOrder }})</option> <option value="-pubSub">{{ $ts.pubSub }} ({{ $ts.ascendingOrder }})</option>
<option value="+users">{{ $ts.users }} ({{ $ts.descendingOrder }})</option> <option value="+notes">{{ $ts.notes }} ({{ $ts.descendingOrder }})</option>
<option value="-users">{{ $ts.users }} ({{ $ts.ascendingOrder }})</option> <option value="-notes">{{ $ts.notes }} ({{ $ts.ascendingOrder }})</option>
<option value="+following">{{ $ts.following }} ({{ $ts.descendingOrder }})</option> <option value="+users">{{ $ts.users }} ({{ $ts.descendingOrder }})</option>
<option value="-following">{{ $ts.following }} ({{ $ts.ascendingOrder }})</option> <option value="-users">{{ $ts.users }} ({{ $ts.ascendingOrder }})</option>
<option value="+followers">{{ $ts.followers }} ({{ $ts.descendingOrder }})</option> <option value="+following">{{ $ts.following }} ({{ $ts.descendingOrder }})</option>
<option value="-followers">{{ $ts.followers }} ({{ $ts.ascendingOrder }})</option> <option value="-following">{{ $ts.following }} ({{ $ts.ascendingOrder }})</option>
<option value="+caughtAt">{{ $ts.registeredAt }} ({{ $ts.descendingOrder }})</option> <option value="+followers">{{ $ts.followers }} ({{ $ts.descendingOrder }})</option>
<option value="-caughtAt">{{ $ts.registeredAt }} ({{ $ts.ascendingOrder }})</option> <option value="-followers">{{ $ts.followers }} ({{ $ts.ascendingOrder }})</option>
<option value="+lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.descendingOrder }})</option> <option value="+caughtAt">{{ $ts.registeredAt }} ({{ $ts.descendingOrder }})</option>
<option value="-lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.ascendingOrder }})</option> <option value="-caughtAt">{{ $ts.registeredAt }} ({{ $ts.ascendingOrder }})</option>
</MkSelect> <option value="+lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.descendingOrder }})</option>
</FormSplit> <option value="-lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.ascendingOrder }})</option>
</div> </MkSelect>
</FormSplit>
<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> </div>
</MkPagination>
</div> <MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination">
</MkSpacer> <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> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -99,8 +102,8 @@ import MkSelect from '@/components/form/select.vue';
import MkPagination from '@/components/ui/pagination.vue'; import MkPagination from '@/components/ui/pagination.vue';
import FormSplit from '@/components/form/split.vue'; import FormSplit from '@/components/form/split.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let host = $ref(''); let host = $ref('');
let state = $ref('federating'); let state = $ref('federating');
@ -119,8 +122,8 @@ const pagination = {
state === 'suspended' ? { suspended: true } : state === 'suspended' ? { suspended: true } :
state === 'blocked' ? { blocked: true } : state === 'blocked' ? { blocked: true } :
state === 'notResponding' ? { notResponding: true } : state === 'notResponding' ? { notResponding: true } :
{}) {}),
})) })),
}; };
function getStatus(instance) { function getStatus(instance) {
@ -129,12 +132,14 @@ function getStatus(instance) {
return 'alive'; return 'alive';
} }
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: {
title: i18n.ts.federation, const headerTabs = $computed(() => []);
icon: 'fas fa-globe',
bg: 'var(--bg)', definePageMetadata({
}, title: i18n.ts.federation,
icon: 'fas fa-globe',
bg: 'var(--bg)',
}); });
</script> </script>

View file

@ -7,7 +7,7 @@
<div>{{ $ts.noFollowRequests }}</div> <div>{{ $ts.noFollowRequests }}</div>
</div> </div>
</template> </template>
<template v-slot="{items}"> <template #default="{items}">
<div class="mk-follow-requests"> <div class="mk-follow-requests">
<div v-for="req in items" :key="req.id" class="user _panel"> <div v-for="req in items" :key="req.id" class="user _panel">
<MkAvatar class="avatar" :user="req.follower" :show-indicator="true"/> <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 MkPagination from '@/components/ui/pagination.vue';
import { userPage, acct } from '@/filters/user'; import { userPage, acct } from '@/filters/user';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const paginationComponent = ref<InstanceType<typeof MkPagination>>(); const paginationComponent = ref<InstanceType<typeof MkPagination>>();
@ -58,13 +58,15 @@ function reject(user) {
}); });
} }
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: computed(() => ({
title: i18n.ts.followRequests, const headerTabs = $computed(() => []);
icon: 'fas fa-user-clock',
bg: 'var(--bg)', definePageMetadata(computed(() => ({
})), title: i18n.ts.followRequests,
}); icon: 'fas fa-user-clock',
bg: 'var(--bg)',
})));
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -5,8 +5,9 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import * as os from '@/os';
import * as Acct from 'misskey-js/built/acct'; import * as Acct from 'misskey-js/built/acct';
import * as os from '@/os';
import { mainRouter } from '@/router';
export default defineComponent({ export default defineComponent({
created() { created() {
@ -17,17 +18,17 @@ export default defineComponent({
if (acct.startsWith('https://')) { if (acct.startsWith('https://')) {
promise = os.api('ap/show', { promise = os.api('ap/show', {
uri: acct uri: acct,
}); });
promise.then(res => { promise.then(res => {
if (res.type === 'User') { if (res.type === 'User') {
this.follow(res.object); this.follow(res.object);
} else if (res.type === 'Note') { } else if (res.type === 'Note') {
this.$router.push(`/notes/${res.object.id}`); mainRouter.push(`/notes/${res.object.id}`);
} else { } else {
os.alert({ os.alert({
type: 'error', type: 'error',
text: 'Not a user' text: 'Not a user',
}).then(() => { }).then(() => {
window.close(); window.close();
}); });
@ -56,9 +57,9 @@ export default defineComponent({
} }
os.apiWithDialog('following/create', { os.apiWithDialog('following/create', {
userId: user.id userId: user.id,
}); });
} },
} },
}); });
</script> </script>

View file

@ -27,8 +27,8 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { computed, defineComponent } from 'vue'; import { computed, inject, watch } from 'vue';
import FormButton from '@/components/ui/button.vue'; import FormButton from '@/components/ui/button.vue';
import FormInput from '@/components/form/input.vue'; import FormInput from '@/components/form/input.vue';
import FormTextarea from '@/components/form/textarea.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 FormSuspense from '@/components/form/suspense.vue';
import { selectFiles } from '@/scripts/select-file'; import { selectFiles } from '@/scripts/select-file';
import * as os from '@/os'; 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({ const router = useRouter();
components: {
FormButton,
FormInput,
FormTextarea,
FormSwitch,
FormGroup,
FormSuspense,
},
props: { const props = defineProps<{
postId: { postId?: string;
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,
};
},
watch: { let init = $ref(null);
postId: { let files = $ref([]);
handler() { let description = $ref(null);
this.init = () => this.postId ? os.api('gallery/posts/show', { let title = $ref(null);
postId: this.postId let isSensitive = $ref(false);
}).then(post => {
this.files = post.files;
this.title = post.title;
this.description = post.description;
this.isSensitive = post.isSensitive;
}) : Promise.resolve(null);
},
immediate: true,
}
},
methods: { function selectFile(evt) {
selectFile(evt) { selectFiles(evt.currentTarget ?? evt.target, null).then(selected => {
selectFiles(evt.currentTarget ?? evt.target, null).then(files => { files = files.concat(selected);
this.files = this.files.concat(files); });
}); }
},
remove(file) { function remove(file) {
this.files = this.files.filter(f => f.id !== file.id); files = files.filter(f => f.id !== file.id);
}, }
async save() { async function save() {
if (this.postId) { if (props.postId) {
await os.apiWithDialog('gallery/posts/update', { await os.apiWithDialog('gallery/posts/update', {
postId: this.postId, postId: props.postId,
title: this.title, title: title,
description: this.description, description: description,
fileIds: this.files.map(file => file.id), fileIds: files.map(file => file.id),
isSensitive: this.isSensitive, isSensitive: isSensitive,
}); });
this.$router.push(`/gallery/${this.postId}`); mainRouter.push(`/gallery/${props.postId}`);
} else { } else {
const post = await os.apiWithDialog('gallery/posts/create', { const created = await os.apiWithDialog('gallery/posts/create', {
title: this.title, title: title,
description: this.description, description: description,
fileIds: this.files.map(file => file.id), fileIds: files.map(file => file.id),
isSensitive: this.isSensitive, isSensitive: isSensitive,
}); });
this.$router.push(`/gallery/${post.id}`); router.push(`/gallery/${created.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 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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -1,49 +1,54 @@
<template> <template>
<div class="xprsixdl _root"> <MkStickyContainer>
<MkTab v-if="$i" v-model="tab"> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<option value="explore"><i class="fas fa-icons"></i> {{ $ts.gallery }}</option> <MkSpacer :content-max="1400">
<option value="liked"><i class="fas fa-heart"></i> {{ $ts._gallery.liked }}</option> <div class="_root">
<option value="my"><i class="fas fa-edit"></i> {{ $ts._gallery.my }}</option> <MkTab v-if="$i" v-model="tab">
</MkTab> <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'"> <div v-if="tab === 'explore'">
<MkFolder class="_gap"> <MkFolder class="_gap">
<template #header><i class="fas fa-clock"></i>{{ $ts.recentPosts }}</template> <template #header><i class="fas fa-clock"></i>{{ $ts.recentPosts }}</template>
<MkPagination v-slot="{items}" :pagination="recentPostsPagination" :disable-auto-load="true"> <MkPagination v-slot="{items}" :pagination="recentPostsPagination" :disable-auto-load="true">
<div class="vfpdbgtk"> <div class="vfpdbgtk">
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/> <MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
</div> </div>
</MkPagination> </MkPagination>
</MkFolder> </MkFolder>
<MkFolder class="_gap"> <MkFolder class="_gap">
<template #header><i class="fas fa-fire-alt"></i>{{ $ts.popularPosts }}</template> <template #header><i class="fas fa-fire-alt"></i>{{ $ts.popularPosts }}</template>
<MkPagination v-slot="{items}" :pagination="popularPostsPagination" :disable-auto-load="true"> <MkPagination v-slot="{items}" :pagination="popularPostsPagination" :disable-auto-load="true">
<div class="vfpdbgtk"> <div class="vfpdbgtk">
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/> <MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
</div> </div>
</MkPagination> </MkPagination>
</MkFolder> </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> </div>
</MkPagination> <div v-else-if="tab === 'liked'">
</div> <MkPagination v-slot="{items}" :pagination="likedPostsPagination">
<div v-else-if="tab === 'my'"> <div class="vfpdbgtk">
<MkA to="/gallery/new" class="_link" style="margin: 16px;"><i class="fas fa-plus"></i> {{ $ts.postToGallery }}</MkA> <MkGalleryPostPreview v-for="like in items" :key="like.id" :post="like.post" class="post"/>
<MkPagination v-slot="{items}" :pagination="myPostsPagination"> </div>
<div class="vfpdbgtk"> </MkPagination>
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
</div> </div>
</MkPagination> <div v-else-if="tab === 'my'">
</div> <MkA to="/gallery/new" class="_link" style="margin: 16px;"><i class="fas fa-plus"></i> {{ $ts.postToGallery }}</MkA>
</div> <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> </template>
<script lang="ts"> <script lang="ts" setup>
import { computed, defineComponent } from 'vue'; import { computed, defineComponent, watch } from 'vue';
import XUserList from '@/components/user-list.vue'; import XUserList from '@/components/user-list.vue';
import MkFolder from '@/components/ui/folder.vue'; import MkFolder from '@/components/ui/folder.vue';
import MkInput from '@/components/form/input.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 MkGalleryPostPreview from '@/components/gallery-post-preview.vue';
import number from '@/filters/number'; import number from '@/filters/number';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols'; import { definePageMetadata } from '@/scripts/page-metadata';
import { i18n } from '@/i18n';
export default defineComponent({ const props = defineProps<{
components: { tag?: string;
XUserList, }>();
MkFolder,
MkInput, let tab = $ref('explore');
MkButton, let tags = $ref([]);
MkTab, let tagsRef = $ref();
MkPagination,
MkGalleryPostPreview, 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: { watch(() => props.tag, () => {
tag: { if (tagsRef) tagsRef.tags.toggleContent(props.tag == null);
type: String, });
required: false
}
},
data() { const headerActions = $computed(() => []);
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: [],
};
},
computed: { const headerTabs = $computed(() => []);
meta() {
return this.$instance;
},
tagUsers(): any {
return {
endpoint: 'hashtags/users' as const,
limit: 30,
params: {
tag: this.tag,
origin: 'combined',
sort: '+follower',
}
};
},
},
watch: { definePageMetadata({
tag() { title: i18n.ts.gallery,
if (this.$refs.tags) this.$refs.tags.toggleContent(this.tag == null); icon: 'fas fa-icons',
}, bg: 'var(--bg)',
},
created() {
},
methods: {
}
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.xprsixdl {
max-width: 1400px;
margin: 0 auto;
}
.vfpdbgtk { .vfpdbgtk {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));

View file

@ -49,123 +49,108 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { computed, defineComponent } from 'vue'; import { computed, defineComponent, inject, watch } from 'vue';
import MkButton from '@/components/ui/button.vue'; import MkButton from '@/components/ui/button.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols';
import MkContainer from '@/components/ui/container.vue'; import MkContainer from '@/components/ui/container.vue';
import ImgWithBlurhash from '@/components/img-with-blurhash.vue'; import ImgWithBlurhash from '@/components/img-with-blurhash.vue';
import MkPagination from '@/components/ui/pagination.vue'; import MkPagination from '@/components/ui/pagination.vue';
import MkGalleryPostPreview from '@/components/gallery-post-preview.vue'; import MkGalleryPostPreview from '@/components/gallery-post-preview.vue';
import MkFollowButton from '@/components/follow-button.vue'; import MkFollowButton from '@/components/follow-button.vue';
import { url } from '@/config'; import { url } from '@/config';
import { useRouter } from '@/router';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
export default defineComponent({ const router = useRouter();
components: {
MkContainer, const props = defineProps<{
ImgWithBlurhash, postId: string;
MkPagination, }>();
MkGalleryPostPreview,
MkButton, const post = $ref(null);
MkFollowButton, 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: { actions: [{
postId: { icon: 'fas fa-pencil-alt',
type: String, text: i18n.ts.edit,
required: true handler: edit,
} }],
}, } : null));
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`);
}
}
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -1,5 +1,6 @@
<template> <template><MkStickyContainer>
<MkSpacer :content-max="600" :margin-min="16" :margin-max="32"> <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 v-if="instance" class="_formRoot">
<div class="fnfelxur"> <div class="fnfelxur">
<img :src="instance.iconUrl || instance.faviconUrl" alt="" class="icon"/> <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> <FormLink :to="`https://${host}/manifest.json`" external style="margin-bottom: 8px;">manifest.json</FormLink>
</FormSection> </FormSection>
</div> </div>
</MkSpacer> </MkSpacer></MkStickyContainer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -120,8 +121,8 @@ import FormSwitch from '@/components/form/switch.vue';
import * as os from '@/os'; import * as os from '@/os';
import number from '@/filters/number'; import number from '@/filters/number';
import bytes from '@/filters/bytes'; import bytes from '@/filters/bytes';
import * as symbols from '@/symbols';
import { iAmModerator } from '@/account'; import { iAmModerator } from '@/account';
import { definePageMetadata } from '@/scripts/page-metadata';
const props = defineProps<{ const props = defineProps<{
host: string; host: string;
@ -146,7 +147,7 @@ async function fetch() {
async function toggleBlock(ev) { async function toggleBlock(ev) {
if (meta == null) return; if (meta == null) return;
await os.api('admin/update-meta', { 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(); fetch();
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: {
title: props.host, const headerTabs = $computed(() => []);
icon: 'fas fa-info-circle',
bg: 'var(--bg)', definePageMetadata({
actions: [{ title: props.host,
text: `https://${props.host}`, icon: 'fas fa-info-circle',
icon: 'fas fa-external-link-alt', bg: 'var(--bg)',
handler: () => { actions: [{
window.open(`https://${props.host}`, '_blank'); text: `https://${props.host}`,
} icon: 'fas fa-external-link-alt',
}], handler: () => {
}, window.open(`https://${props.host}`, '_blank');
},
}],
}); });
</script> </script>

View file

@ -1,24 +1,27 @@
<template> <template><MkStickyContainer>
<MkSpacer :content-max="800"> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="800">
<XNotes :pagination="pagination"/> <XNotes :pagination="pagination"/>
</MkSpacer> </MkSpacer></MkStickyContainer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import XNotes from '@/components/notes.vue'; import XNotes from '@/components/notes.vue';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const pagination = { const pagination = {
endpoint: 'notes/mentions' as const, endpoint: 'notes/mentions' as const,
limit: 10, limit: 10,
}; };
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: {
title: i18n.ts.mentions, const headerTabs = $computed(() => []);
icon: 'fas fa-at',
bg: 'var(--bg)', definePageMetadata({
}, title: i18n.ts.mentions,
icon: 'fas fa-at',
bg: 'var(--bg)',
}); });
</script> </script>

View file

@ -1,27 +1,30 @@
<template> <template><MkStickyContainer>
<MkSpacer :content-max="800"> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="800">
<XNotes :pagination="pagination"/> <XNotes :pagination="pagination"/>
</MkSpacer> </MkSpacer></MkStickyContainer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import XNotes from '@/components/notes.vue'; import XNotes from '@/components/notes.vue';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const pagination = { const pagination = {
endpoint: 'notes/mentions' as const, endpoint: 'notes/mentions' as const,
limit: 10, limit: 10,
params: { params: {
visibility: 'specified' visibility: 'specified',
}, },
}; };
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: {
title: i18n.ts.directNotes, const headerTabs = $computed(() => []);
icon: 'fas fa-envelope',
bg: 'var(--bg)', definePageMetadata({
}, title: i18n.ts.directNotes,
icon: 'fas fa-envelope',
bg: 'var(--bg)',
}); });
</script> </script>

View file

@ -1,165 +1,165 @@
<template> <template>
<MkSpacer :content-max="800"> <MkStickyContainer>
<div v-size="{ max: [400] }" class="yweeujhr"> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkButton primary class="start" @click="start"><i class="fas fa-plus"></i> {{ $ts.startMessaging }}</MkButton> <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"> <div v-if="messages.length > 0" class="history">
<MkA v-for="(message, i) in messages" <MkA
:key="message.id" v-for="(message, i) in messages"
v-anim="i" :key="message.id"
class="message _block" v-anim="i"
:class="{ isMe: isMe(message), isRead: message.groupId ? message.reads.includes($i.id) : message.isRead }" class="message _block"
:to="message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`" :class="{ isMe: isMe(message), isRead: message.groupId ? message.reads.includes($i.id) : message.isRead }"
:data-index="i" :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"/> <div>
<header v-if="message.groupId"> <MkAvatar class="avatar" :user="message.groupId ? message.user : isMe(message) ? message.recipient : message.user" :show-indicator="true"/>
<span class="name">{{ message.group.name }}</span> <header v-if="message.groupId">
<MkTime :time="message.createdAt" class="time"/> <span class="name">{{ message.group.name }}</span>
</header> <MkTime :time="message.createdAt" class="time"/>
<header v-else> </header>
<span class="name"><MkUserName :user="isMe(message) ? message.recipient : message.user"/></span> <header v-else>
<span class="username">@{{ acct(isMe(message) ? message.recipient : message.user) }}</span> <span class="name"><MkUserName :user="isMe(message) ? message.recipient : message.user"/></span>
<MkTime :time="message.createdAt" class="time"/> <span class="username">@{{ acct(isMe(message) ? message.recipient : message.user) }}</span>
</header> <MkTime :time="message.createdAt" class="time"/>
<div class="body"> </header>
<p class="text"><span v-if="isMe(message)" class="me">{{ $ts.you }}:</span>{{ message.text }}</p> <div class="body">
<p class="text"><span v-if="isMe(message)" class="me">{{ $ts.you }}:</span>{{ message.text }}</p>
</div>
</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>
<div v-if="!fetching && messages.length == 0" class="_fullinfo"> </MkSpacer>
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> </MkStickyContainer>
<div>{{ $ts.noHistory }}</div>
</div>
<MkLoading v-if="fetching"/>
</div>
</MkSpacer>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineAsyncComponent, defineComponent, markRaw } from 'vue'; import { defineAsyncComponent, defineComponent, inject, markRaw, onMounted, onUnmounted } from 'vue';
import * as Acct from 'misskey-js/built/acct'; import * as Acct from 'misskey-js/built/acct';
import MkButton from '@/components/ui/button.vue'; import MkButton from '@/components/ui/button.vue';
import { acct } from '@/filters/user'; import { acct } from '@/filters/user';
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream'; 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({ const router = useRouter();
components: {
MkButton
},
data() { let fetching = $ref(true);
return { let moreFetching = $ref(false);
[symbols.PAGE_INFO]: { let messages = $ref([]);
title: this.$ts.messaging, let connection = $ref(null);
icon: 'fas fa-comments',
bg: 'var(--bg)',
},
fetching: true,
moreFetching: false,
messages: [],
connection: null,
};
},
mounted() { const getAcct = Acct.toString;
this.connection = markRaw(stream.useChannel('messagingIndex'));
this.connection.on('message', this.onMessage); function isMe(message) {
this.connection.on('read', this.onRead); return message.userId === $i.id;
}
os.api('messaging/history', { group: false }).then(userMessages => { function onMessage(message) {
os.api('messaging/history', { group: true }).then(groupMessages => { if (message.recipientId) {
const messages = userMessages.concat(groupMessages); messages = messages.filter(m => !(
messages.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); (m.recipientId === message.recipientId && m.userId === message.userId) ||
this.messages = messages; (m.recipientId === message.userId && m.userId === message.recipientId)));
this.fetching = false;
});
});
},
beforeUnmount() { messages.unshift(message);
this.connection.dispose(); } else if (message.groupId) {
}, messages = messages.filter(m => m.groupId !== message.groupId);
messages.unshift(message);
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
} }
}
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> </script>

View file

@ -61,10 +61,10 @@ import { isBottomVisible, onScrollBottom, scrollToBottom } from '@/scripts/scrol
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream'; import { stream } from '@/stream';
import * as sound from '@/scripts/sound'; import * as sound from '@/scripts/sound';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { $i } from '@/account'; import { $i } from '@/account';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
import { definePageMetadata } from '@/scripts/page-metadata';
const props = defineProps<{ const props = defineProps<{
userAcct?: string; userAcct?: string;
@ -280,15 +280,13 @@ onBeforeUnmount(() => {
if (scrollRemove) scrollRemove(); if (scrollRemove) scrollRemove();
}); });
defineExpose({ definePageMetadata(computed(() => !fetching ? user ? {
[symbols.PAGE_INFO]: computed(() => !fetching ? user ? { userName: user,
userName: user, avatar: user,
avatar: user, } : {
} : { title: group?.name,
title: group?.name, icon: 'fas fa-users',
icon: 'fas fa-users', } : null));
} : null),
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -1,127 +1,129 @@
<template> <template>
<div class="mwysmxbg"> <MkStickyContainer>
<div class="_isolated">{{ $ts._mfm.intro }}</div> <template #header><MkPageHeader/></template>
<div class="section _block"> <div class="mwysmxbg">
<div class="title">{{ $ts._mfm.mention }}</div> <div class="_isolated">{{ $ts._mfm.intro }}</div>
<div class="content"> <div class="section _block">
<p>{{ $ts._mfm.mentionDescription }}</p> <div class="title">{{ $ts._mfm.mention }}</div>
<div class="preview"> <div class="content">
<Mfm :text="preview_mention"/> <p>{{ $ts._mfm.mentionDescription }}</p>
<MkTextarea v-model="preview_mention"><template #label>MFM</template></MkTextarea> <div class="preview">
<Mfm :text="preview_mention"/>
<MkTextarea v-model="preview_mention"><template #label>MFM</template></MkTextarea>
</div>
</div> </div>
</div> </div>
</div> <div class="section _block">
<div class="section _block"> <div class="title">{{ $ts._mfm.hashtag }}</div>
<div class="title">{{ $ts._mfm.hashtag }}</div> <div class="content">
<div class="content"> <p>{{ $ts._mfm.hashtagDescription }}</p>
<p>{{ $ts._mfm.hashtagDescription }}</p> <div class="preview">
<div class="preview"> <Mfm :text="preview_hashtag"/>
<Mfm :text="preview_hashtag"/> <MkTextarea v-model="preview_hashtag"><template #label>MFM</template></MkTextarea>
<MkTextarea v-model="preview_hashtag"><template #label>MFM</template></MkTextarea> </div>
</div> </div>
</div> </div>
</div> <div class="section _block">
<div class="section _block"> <div class="title">{{ $ts._mfm.url }}</div>
<div class="title">{{ $ts._mfm.url }}</div> <div class="content">
<div class="content"> <p>{{ $ts._mfm.urlDescription }}</p>
<p>{{ $ts._mfm.urlDescription }}</p> <div class="preview">
<div class="preview"> <Mfm :text="preview_url"/>
<Mfm :text="preview_url"/> <MkTextarea v-model="preview_url"><template #label>MFM</template></MkTextarea>
<MkTextarea v-model="preview_url"><template #label>MFM</template></MkTextarea> </div>
</div> </div>
</div> </div>
</div> <div class="section _block">
<div class="section _block"> <div class="title">{{ $ts._mfm.link }}</div>
<div class="title">{{ $ts._mfm.link }}</div> <div class="content">
<div class="content"> <p>{{ $ts._mfm.linkDescription }}</p>
<p>{{ $ts._mfm.linkDescription }}</p> <div class="preview">
<div class="preview"> <Mfm :text="preview_link"/>
<Mfm :text="preview_link"/> <MkTextarea v-model="preview_link"><template #label>MFM</template></MkTextarea>
<MkTextarea v-model="preview_link"><template #label>MFM</template></MkTextarea> </div>
</div> </div>
</div> </div>
</div> <div class="section _block">
<div class="section _block"> <div class="title">{{ $ts._mfm.emoji }}</div>
<div class="title">{{ $ts._mfm.emoji }}</div> <div class="content">
<div class="content"> <p>{{ $ts._mfm.emojiDescription }}</p>
<p>{{ $ts._mfm.emojiDescription }}</p> <div class="preview">
<div class="preview"> <Mfm :text="preview_emoji"/>
<Mfm :text="preview_emoji"/> <MkTextarea v-model="preview_emoji"><template #label>MFM</template></MkTextarea>
<MkTextarea v-model="preview_emoji"><template #label>MFM</template></MkTextarea> </div>
</div> </div>
</div> </div>
</div> <div class="section _block">
<div class="section _block"> <div class="title">{{ $ts._mfm.bold }}</div>
<div class="title">{{ $ts._mfm.bold }}</div> <div class="content">
<div class="content"> <p>{{ $ts._mfm.boldDescription }}</p>
<p>{{ $ts._mfm.boldDescription }}</p> <div class="preview">
<div class="preview"> <Mfm :text="preview_bold"/>
<Mfm :text="preview_bold"/> <MkTextarea v-model="preview_bold"><template #label>MFM</template></MkTextarea>
<MkTextarea v-model="preview_bold"><template #label>MFM</template></MkTextarea> </div>
</div> </div>
</div> </div>
</div> <div class="section _block">
<div class="section _block"> <div class="title">{{ $ts._mfm.small }}</div>
<div class="title">{{ $ts._mfm.small }}</div> <div class="content">
<div class="content"> <p>{{ $ts._mfm.smallDescription }}</p>
<p>{{ $ts._mfm.smallDescription }}</p> <div class="preview">
<div class="preview"> <Mfm :text="preview_small"/>
<Mfm :text="preview_small"/> <MkTextarea v-model="preview_small"><template #label>MFM</template></MkTextarea>
<MkTextarea v-model="preview_small"><template #label>MFM</template></MkTextarea> </div>
</div> </div>
</div> </div>
</div> <div class="section _block">
<div class="section _block"> <div class="title">{{ $ts._mfm.quote }}</div>
<div class="title">{{ $ts._mfm.quote }}</div> <div class="content">
<div class="content"> <p>{{ $ts._mfm.quoteDescription }}</p>
<p>{{ $ts._mfm.quoteDescription }}</p> <div class="preview">
<div class="preview"> <Mfm :text="preview_quote"/>
<Mfm :text="preview_quote"/> <MkTextarea v-model="preview_quote"><template #label>MFM</template></MkTextarea>
<MkTextarea v-model="preview_quote"><template #label>MFM</template></MkTextarea> </div>
</div> </div>
</div> </div>
</div> <div class="section _block">
<div class="section _block"> <div class="title">{{ $ts._mfm.center }}</div>
<div class="title">{{ $ts._mfm.center }}</div> <div class="content">
<div class="content"> <p>{{ $ts._mfm.centerDescription }}</p>
<p>{{ $ts._mfm.centerDescription }}</p> <div class="preview">
<div class="preview"> <Mfm :text="preview_center"/>
<Mfm :text="preview_center"/> <MkTextarea v-model="preview_center"><template #label>MFM</template></MkTextarea>
<MkTextarea v-model="preview_center"><template #label>MFM</template></MkTextarea> </div>
</div> </div>
</div> </div>
</div> <div class="section _block">
<div class="section _block"> <div class="title">{{ $ts._mfm.inlineCode }}</div>
<div class="title">{{ $ts._mfm.inlineCode }}</div> <div class="content">
<div class="content"> <p>{{ $ts._mfm.inlineCodeDescription }}</p>
<p>{{ $ts._mfm.inlineCodeDescription }}</p> <div class="preview">
<div class="preview"> <Mfm :text="preview_inlineCode"/>
<Mfm :text="preview_inlineCode"/> <MkTextarea v-model="preview_inlineCode"><template #label>MFM</template></MkTextarea>
<MkTextarea v-model="preview_inlineCode"><template #label>MFM</template></MkTextarea> </div>
</div> </div>
</div> </div>
</div> <div class="section _block">
<div class="section _block"> <div class="title">{{ $ts._mfm.blockCode }}</div>
<div class="title">{{ $ts._mfm.blockCode }}</div> <div class="content">
<div class="content"> <p>{{ $ts._mfm.blockCodeDescription }}</p>
<p>{{ $ts._mfm.blockCodeDescription }}</p> <div class="preview">
<div class="preview"> <Mfm :text="preview_blockCode"/>
<Mfm :text="preview_blockCode"/> <MkTextarea v-model="preview_blockCode"><template #label>MFM</template></MkTextarea>
<MkTextarea v-model="preview_blockCode"><template #label>MFM</template></MkTextarea> </div>
</div> </div>
</div> </div>
</div> <div class="section _block">
<div class="section _block"> <div class="title">{{ $ts._mfm.inlineMath }}</div>
<div class="title">{{ $ts._mfm.inlineMath }}</div> <div class="content">
<div class="content"> <p>{{ $ts._mfm.inlineMathDescription }}</p>
<p>{{ $ts._mfm.inlineMathDescription }}</p> <div class="preview">
<div class="preview"> <Mfm :text="preview_inlineMath"/>
<Mfm :text="preview_inlineMath"/> <MkTextarea v-model="preview_inlineMath"><template #label>MFM</template></MkTextarea>
<MkTextarea v-model="preview_inlineMath"><template #label>MFM</template></MkTextarea> </div>
</div> </div>
</div> </div>
</div> <!-- deprecated
<!-- deprecated
<div class="section _block"> <div class="section _block">
<div class="title">{{ $ts._mfm.search }}</div> <div class="title">{{ $ts._mfm.search }}</div>
<div class="content"> <div class="content">
@ -133,216 +135,210 @@
</div> </div>
</div> </div>
--> -->
<div class="section _block"> <div class="section _block">
<div class="title">{{ $ts._mfm.flip }}</div> <div class="title">{{ $ts._mfm.flip }}</div>
<div class="content"> <div class="content">
<p>{{ $ts._mfm.flipDescription }}</p> <p>{{ $ts._mfm.flipDescription }}</p>
<div class="preview"> <div class="preview">
<Mfm :text="preview_flip"/> <Mfm :text="preview_flip"/>
<MkTextarea v-model="preview_flip"><template #label>MFM</template></MkTextarea> <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>
</div> </div>
<div class="section _block"> </MkStickyContainer>
<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>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import MkTextarea from '@/components/form/textarea.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({ const preview_mention = '@example';
components: { const preview_hashtag = '#test';
MkTextarea 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() { definePageMetadata({
return { title: i18n.ts._mfm.cheatSheet,
[symbols.PAGE_INFO]: { icon: 'fas fa-question-circle',
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 🍮]`,
};
},
}); });
</script> </script>

View file

@ -49,28 +49,12 @@ export default defineComponent({
MkSignin, MkSignin,
MkButton, MkButton,
}, },
props: ['session', 'callback', 'name', 'icon', 'permission'],
data() { data() {
return { 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: { methods: {
async accept() { async accept() {
this.state = 'waiting'; this.state = 'waiting';
@ -84,7 +68,7 @@ export default defineComponent({
this.state = 'accepted'; this.state = 'accepted';
if (this.callback) { if (this.callback) {
location.href = appendQuery(this.callback, query({ location.href = appendQuery(this.callback, query({
session: this.session session: this.session,
})); }));
} }
}, },
@ -93,8 +77,8 @@ export default defineComponent({
}, },
onLogin(res) { onLogin(res) {
login(res.i); login(res.i);
} },
} },
}); });
</script> </script>

View file

@ -5,11 +5,13 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { inject } from 'vue';
import XAntenna from './editor.vue'; import XAntenna from './editor.vue';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { router } from '@/router'; import { definePageMetadata } from '@/scripts/page-metadata';
import { useRouter } from '@/router';
const router = useRouter();
let draft = $ref({ let draft = $ref({
name: '', name: '',
@ -22,19 +24,21 @@ let draft = $ref({
withReplies: false, withReplies: false,
caseSensitive: false, caseSensitive: false,
withFile: false, withFile: false,
notify: false notify: false,
}); });
function onAntennaCreated() { function onAntennaCreated() {
router.push('/my/antennas'); router.push('/my/antennas');
} }
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: {
title: i18n.ts.manageAntennas, const headerTabs = $computed(() => []);
icon: 'fas fa-satellite',
bg: 'var(--bg)', definePageMetadata({
}, title: i18n.ts.manageAntennas,
icon: 'fas fa-satellite',
bg: 'var(--bg)',
}); });
</script> </script>

View file

@ -5,14 +5,14 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { watch } from 'vue'; import { inject, watch } from 'vue';
import XAntenna from './editor.vue'; import XAntenna from './editor.vue';
import * as symbols from '@/symbols';
import * as os from '@/os'; import * as os from '@/os';
import { MisskeyNavigator } from '@/scripts/navigate';
import { i18n } from '@/i18n'; 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); let antenna: any = $ref(null);
@ -21,18 +21,20 @@ const props = defineProps<{
}>(); }>();
function onAntennaUpdated() { function onAntennaUpdated() {
nav.push('/my/antennas'); router.push('/my/antennas');
} }
os.api('antennas/show', { antennaId: props.antennaId }).then((antennaResponse) => { os.api('antennas/show', { antennaId: props.antennaId }).then((antennaResponse) => {
antenna = antennaResponse; antenna = antennaResponse;
}); });
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: {
title: i18n.ts.manageAntennas, const headerTabs = $computed(() => []);
icon: 'fas fa-satellite',
} definePageMetadata({
title: i18n.ts.manageAntennas,
icon: 'fas fa-satellite',
}); });
</script> </script>

View file

@ -1,5 +1,6 @@
<template> <template><MkStickyContainer>
<MkSpacer :content-max="700"> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700">
<div class="ieepwinx"> <div class="ieepwinx">
<MkButton :link="true" to="/my/antennas/create" primary class="add"><i class="fas fa-plus"></i> {{ i18n.ts.add }}</MkButton> <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> </MkPagination>
</div> </div>
</div> </div>
</MkSpacer> </MkSpacer></MkStickyContainer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { } from 'vue';
import MkPagination from '@/components/ui/pagination.vue'; import MkPagination from '@/components/ui/pagination.vue';
import MkButton from '@/components/ui/button.vue'; import MkButton from '@/components/ui/button.vue';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const pagination = { const pagination = {
endpoint: 'antennas/list' as const, endpoint: 'antennas/list' as const,
limit: 10, limit: 10,
}; };
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: {
title: i18n.ts.manageAntennas, const headerTabs = $computed(() => []);
icon: 'fas fa-satellite',
bg: 'var(--bg)' definePageMetadata({
} title: i18n.ts.manageAntennas,
icon: 'fas fa-satellite',
bg: 'var(--bg)',
}); });
</script> </script>

View file

@ -1,5 +1,6 @@
<template> <template><MkStickyContainer>
<MkSpacer :content-max="700"> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700">
<div class="qtcaoidl"> <div class="qtcaoidl">
<MkButton primary class="add" @click="create"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton> <MkButton primary class="add" @click="create"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
@ -10,7 +11,7 @@
</MkA> </MkA>
</MkPagination> </MkPagination>
</div> </div>
</MkSpacer> </MkSpacer></MkStickyContainer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -18,8 +19,8 @@ import { } from 'vue';
import MkPagination from '@/components/ui/pagination.vue'; import MkPagination from '@/components/ui/pagination.vue';
import MkButton from '@/components/ui/button.vue'; import MkButton from '@/components/ui/button.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const pagination = { const pagination = {
endpoint: 'clips/list' as const, endpoint: 'clips/list' as const,
@ -61,15 +62,17 @@ function onClipDeleted() {
pagingComponent.reload(); pagingComponent.reload();
} }
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: {
title: i18n.ts.clip, const headerTabs = $computed(() => []);
icon: 'fas fa-paperclip',
bg: 'var(--bg)', definePageMetadata({
action: { title: i18n.ts.clip,
icon: 'fas fa-plus', icon: 'fas fa-paperclip',
handler: create bg: 'var(--bg)',
}, action: {
icon: 'fas fa-plus',
handler: create,
}, },
}); });
</script> </script>

View file

@ -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>

View file

@ -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>

View file

@ -1,5 +1,6 @@
<template> <template><MkStickyContainer>
<MkSpacer :content-max="700"> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700">
<div class="qkcjvfiv"> <div class="qkcjvfiv">
<MkButton primary class="add" @click="create"><i class="fas fa-plus"></i> {{ $ts.createList }}</MkButton> <MkButton primary class="add" @click="create"><i class="fas fa-plus"></i> {{ $ts.createList }}</MkButton>
@ -10,7 +11,7 @@
</MkA> </MkA>
</MkPagination> </MkPagination>
</div> </div>
</MkSpacer> </MkSpacer></MkStickyContainer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -19,8 +20,8 @@ import MkPagination from '@/components/ui/pagination.vue';
import MkButton from '@/components/ui/button.vue'; import MkButton from '@/components/ui/button.vue';
import MkAvatars from '@/components/avatars.vue'; import MkAvatars from '@/components/avatars.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const pagingComponent = $ref<InstanceType<typeof MkPagination>>(); const pagingComponent = $ref<InstanceType<typeof MkPagination>>();
@ -38,15 +39,17 @@ async function create() {
pagingComponent.reload(); pagingComponent.reload();
} }
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: {
title: i18n.ts.manageLists, const headerTabs = $computed(() => []);
icon: 'fas fa-list-ul',
bg: 'var(--bg)', definePageMetadata({
action: { title: i18n.ts.manageLists,
icon: 'fas fa-plus', icon: 'fas fa-list-ul',
handler: create, bg: 'var(--bg)',
}, action: {
icon: 'fas fa-plus',
handler: create,
}, },
}); });
</script> </script>

View file

@ -1,5 +1,6 @@
<template> <template><MkStickyContainer>
<MkSpacer :content-max="700"> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700">
<div class="mk-list-page"> <div class="mk-list-page">
<transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in"> <transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
<div v-if="list" class="_section"> <div v-if="list" class="_section">
@ -31,104 +32,96 @@
</div> </div>
</transition> </transition>
</div> </div>
</MkSpacer> </MkSpacer></MkStickyContainer>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { computed, defineComponent } from 'vue'; import { computed, defineComponent, watch } from 'vue';
import MkButton from '@/components/ui/button.vue'; import MkButton from '@/components/ui/button.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols'; import { mainRouter } from '@/router';
import { definePageMetadata } from '@/scripts/page-metadata';
export default defineComponent({ const props = defineProps<{
components: { listId: string;
MkButton }>();
},
data() { let list = $ref(null);
return { let users = $ref([]);
[symbols.PAGE_INFO]: computed(() => this.list ? {
title: this.list.name,
icon: 'fas fa-list-ul',
bg: 'var(--bg)',
} : null),
list: null,
users: [],
};
},
watch: { function fetchList() {
$route: 'fetch' os.api('users/lists/show', {
}, listId: props.listId,
}).then(_list => {
list = _list;
os.api('users/show', {
userIds: list.userIds,
}).then(_users => {
users = _users;
});
});
}
created() { function addUser() {
this.fetch(); os.selectUser().then(user => {
}, os.apiWithDialog('users/lists/push', {
listId: list.id,
userId: user.id,
}).then(() => {
users.push(user);
});
});
}
methods: { function removeUser(user) {
fetch() { os.api('users/lists/pull', {
os.api('users/lists/show', { listId: list.id,
listId: this.$route.params.list userId: user.id,
}).then(list => { }).then(() => {
this.list = list; users = users.filter(x => x.id !== user.id);
os.api('users/show', { });
userIds: this.list.userIds }
}).then(users => {
this.users = users;
});
});
},
addUser() { async function renameList() {
os.selectUser().then(user => { const { canceled, result: name } = await os.inputText({
os.apiWithDialog('users/lists/push', { title: i18n.ts.enterListName,
listId: this.list.id, default: list.name,
userId: user.id });
}).then(() => { if (canceled) return;
this.users.push(user);
});
});
},
removeUser(user) { await os.api('users/lists/update', {
os.api('users/lists/pull', { listId: list.id,
listId: this.list.id, name: name,
userId: user.id });
}).then(() => {
this.users = this.users.filter(x => x.id !== user.id);
});
},
async renameList() { list.name = name;
const { canceled, result: name } = await os.inputText({ }
title: this.$ts.enterListName,
default: this.list.name
});
if (canceled) return;
await os.api('users/lists/update', { async function deleteList() {
listId: this.list.id, const { canceled } = await os.confirm({
name: name 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() { watch(() => props.listId, fetchList, { immediate: true });
const { canceled } = await os.confirm({
type: 'warning',
text: this.$t('removeAreYouSure', { x: this.list.name }),
});
if (canceled) return;
await os.api('users/lists/delete', { const headerActions = $computed(() => []);
listId: this.list.id
}); const headerTabs = $computed(() => []);
os.success();
this.$router.push('/my/lists'); definePageMetadata(computed(() => list ? {
} title: list.name,
} icon: 'fas fa-list-ul',
}); bg: 'var(--bg)',
} : null));
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -8,14 +8,16 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import * as symbols from '@/symbols';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: {
title: i18n.ts.notFound, const headerTabs = $computed(() => []);
icon: 'fas fa-exclamation-triangle',
bg: 'var(--bg)', definePageMetadata({
}, title: i18n.ts.notFound,
icon: 'fas fa-exclamation-triangle',
bg: 'var(--bg)',
}); });
</script> </script>

View file

@ -1,10 +1,11 @@
<template> <template><MkStickyContainer>
<MkSpacer :content-max="800"> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="800">
<div class="fcuexfpr"> <div class="fcuexfpr">
<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in"> <transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
<div v-if="note" class="note"> <div v-if="note" class="note">
<div v-if="showNext" class="_gap"> <div v-if="showNext" class="_gap">
<XNotes class="_content" :pagination="next" :no-gap="true"/> <XNotes class="_content" :pagination="nextPagination" :no-gap="true"/>
</div> </div>
<div class="main _gap"> <div class="main _gap">
@ -27,121 +28,112 @@
</div> </div>
<div v-if="showPrev" class="_gap"> <div v-if="showPrev" class="_gap">
<XNotes class="_content" :pagination="prev" :no-gap="true"/> <XNotes class="_content" :pagination="prevPagination" :no-gap="true"/>
</div> </div>
</div> </div>
<MkError v-else-if="error" @retry="fetch()"/> <MkError v-else-if="error" @retry="fetch()"/>
<MkLoading v-else/> <MkLoading v-else/>
</transition> </transition>
</div> </div>
</MkSpacer> </MkSpacer></MkStickyContainer>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { computed, defineComponent } from 'vue'; import { computed, defineComponent, watch } from 'vue';
import * as misskey from 'misskey-js';
import XNote from '@/components/note.vue'; import XNote from '@/components/note.vue';
import XNoteDetailed from '@/components/note-detailed.vue'; import XNoteDetailed from '@/components/note-detailed.vue';
import XNotes from '@/components/notes.vue'; import XNotes from '@/components/notes.vue';
import MkRemoteCaution from '@/components/remote-caution.vue'; import MkRemoteCaution from '@/components/remote-caution.vue';
import MkButton from '@/components/ui/button.vue'; import MkButton from '@/components/ui/button.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols'; import { definePageMetadata } from '@/scripts/page-metadata';
import { i18n } from '@/i18n';
export default defineComponent({ const props = defineProps<{
components: { noteId: string;
XNote, }>();
XNoteDetailed,
XNotes, let note = $ref<null | misskey.entities.Note>();
MkRemoteCaution, let clips = $ref();
MkButton, let hasPrev = $ref(false);
}, let hasNext = $ref(false);
props: { let showPrev = $ref(false);
noteId: { let showNext = $ref(false);
type: String, let error = $ref();
required: true
} const prevPagination = {
}, endpoint: 'users/notes' as const,
data() { limit: 10,
return { params: computed(() => note ? ({
[symbols.PAGE_INFO]: computed(() => this.note ? { userId: note.userId,
title: this.$ts.note, untilId: note.id,
subtitle: new Date(this.note.createdAt).toLocaleString(), }) : null),
avatar: this.note.user, };
path: `/notes/${this.note.id}`,
share: { const nextPagination = {
title: this.$t('noteOf', { user: this.note.user.name }), reversed: true,
text: this.note.text, endpoint: 'users/notes' as const,
}, limit: 10,
bg: 'var(--bg)', params: computed(() => note ? ({
} : null), userId: note.userId,
note: null, sinceId: note.id,
clips: null, }) : null),
hasPrev: false, };
hasNext: false,
showPrev: false, function fetchNote() {
showNext: false, hasPrev = false;
error: null, hasNext = false;
prev: { showPrev = false;
endpoint: 'users/notes' as const, showNext = false;
limit: 10, note = null;
params: computed(() => ({ os.api('notes/show', {
userId: this.note.userId, noteId: props.noteId,
untilId: this.note.id, }).then(res => {
})), note = res;
}, Promise.all([
next: { os.api('notes/clips', {
reversed: true, noteId: note.id,
endpoint: 'users/notes' as const, }),
limit: 10, os.api('users/notes', {
params: computed(() => ({ userId: note.userId,
userId: this.note.userId, untilId: note.id,
sinceId: this.note.id, limit: 1,
})), }),
}, os.api('users/notes', {
}; userId: note.userId,
}, sinceId: note.id,
watch: { limit: 1,
noteId: 'fetch' }),
}, ]).then(([_clips, prev, next]) => {
created() { clips = _clips;
this.fetch(); hasPrev = prev.length !== 0;
}, hasNext = next.length !== 0;
methods: { });
fetch() { }).catch(err => {
this.hasPrev = false; error = err;
this.hasNext = false; });
this.showPrev = false; }
this.showNext = false;
this.note = null; watch(() => props.noteId, fetchNote, {
os.api('notes/show', { immediate: true,
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 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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -1,18 +1,21 @@
<template> <template>
<MkSpacer :content-max="800"> <MkStickyContainer>
<div class="clupoqwt"> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<XNotifications class="notifications" :include-types="includeTypes" :unread-only="tab === 'unread'"/> <MkSpacer :content-max="800">
</div> <div class="clupoqwt">
</MkSpacer> <XNotifications class="notifications" :include-types="includeTypes" :unread-only="tab === 'unread'"/>
</div>
</MkSpacer>
</MkStickyContainer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'; import { computed } from 'vue';
import { notificationTypes } from 'misskey-js';
import XNotifications from '@/components/notifications.vue'; import XNotifications from '@/components/notifications.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols';
import { notificationTypes } from 'misskey-js';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let tab = $ref('all'); let tab = $ref('all');
let includeTypes = $ref<string[] | null>(null); let includeTypes = $ref<string[] | null>(null);
@ -23,46 +26,46 @@ function setFilter(ev) {
active: includeTypes && includeTypes.includes(t), active: includeTypes && includeTypes.includes(t),
action: () => { action: () => {
includeTypes = [t]; includeTypes = [t];
} },
})); }));
const items = includeTypes != null ? [{ const items = includeTypes != null ? [{
icon: 'fas fa-times', icon: 'fas fa-times',
text: i18n.ts.clear, text: i18n.ts.clear,
action: () => { action: () => {
includeTypes = null; includeTypes = null;
} },
}, null, ...typeItems] : typeItems; }, null, ...typeItems] : typeItems;
os.popupMenu(items, ev.currentTarget ?? ev.target); os.popupMenu(items, ev.currentTarget ?? ev.target);
} }
defineExpose({ const headerActions = $computed(() => [{
[symbols.PAGE_INFO]: computed(() => ({ text: i18n.ts.filter,
title: i18n.ts.notifications, icon: 'fas fa-filter',
icon: 'fas fa-bell', highlighted: includeTypes != null,
bg: 'var(--bg)', handler: setFilter,
actions: [{ }, {
text: i18n.ts.filter, text: i18n.ts.markAllAsRead,
icon: 'fas fa-filter', icon: 'fas fa-check',
highlighted: includeTypes != null, handler: () => {
handler: setFilter, os.apiWithDialog('notifications/mark-all-as-read');
}, { },
text: i18n.ts.markAllAsRead, }]);
icon: 'fas fa-check',
handler: () => { const headerTabs = $computed(() => [{
os.apiWithDialog('notifications/mark-all-as-read'); active: tab === 'all',
}, title: i18n.ts.all,
}], onClick: () => { tab = 'all'; },
tabs: [{ }, {
active: tab === 'all', active: tab === 'unread',
title: i18n.ts.all, title: i18n.ts.unread,
onClick: () => { tab = 'all'; }, onClick: () => { tab = 'unread'; },
}, { }]);
active: tab === 'unread',
title: i18n.ts.unread, definePageMetadata(computed(() => ({
onClick: () => { tab = 'unread'; }, title: i18n.ts.notifications,
},] icon: 'fas fa-bell',
})), bg: 'var(--bg)',
}); })));
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -1,5 +1,6 @@
<template> <template><MkStickyContainer>
<MkSpacer :content-max="700"> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700">
<div class="jqqmcavi"> <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="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> <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"> <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}"> <template #item="{element}">
<XVariable <XVariable
:modelValue="element" :model-value="element"
:removable="true" :removable="true"
:hpml="hpml" :hpml="hpml"
:name="element.name" :name="element.name"
@ -75,11 +76,11 @@
<MkTextarea v-model="script" class="_code"/> <MkTextarea v-model="script" class="_code"/>
</div> </div>
</div> </div>
</MkSpacer> </MkSpacer></MkStickyContainer>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, defineAsyncComponent, computed } from 'vue'; import { defineComponent, defineAsyncComponent, computed, provide, watch } from 'vue';
import 'prismjs'; import 'prismjs';
import { highlight, languages } from 'prismjs/components/prism-core'; import { highlight, languages } from 'prismjs/components/prism-core';
import 'prismjs/components/prism-clike'; import 'prismjs/components/prism-clike';
@ -101,367 +102,349 @@ import { url } from '@/config';
import { collectPageVars } from '@/scripts/collect-page-vars'; import { collectPageVars } from '@/scripts/collect-page-vars';
import * as os from '@/os'; import * as os from '@/os';
import { selectFile } from '@/scripts/select-file'; 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({ const props = defineProps<{
components: { initPageId?: string;
XDraggable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)), initPageName?: string;
XVariable, XBlocks, MkTextarea, MkContainer, MkButton, MkSelect, MkSwitch, MkInput, initUser?: string;
}, }>();
provide() { let tab = $ref('settings');
return { let author = $ref($i);
readonly: this.readonly, let readonly = $ref(false);
getScriptBlockList: this.getScriptBlockList, let page = $ref(null);
getPageBlockList: this.getPageBlockList 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: { provide('readonly', readonly);
initPageId: { provide('getScriptBlockList', getScriptBlockList);
type: String, provide('getPageBlockList', getPageBlockList);
required: false
},
initPageName: {
type: String,
required: false
},
initUser: {
type: String,
required: false
},
},
data() { watch($$(eyeCatchingImageId), async () => {
return { if (eyeCatchingImageId == null) {
[symbols.PAGE_INFO]: computed(() => { eyeCatchingImage = null;
let title = this.$ts._pages.newPage; } else {
if (this.initPageId) { eyeCatchingImage = await os.api('drive/files/show', {
title = this.$ts._pages.editPage; fileId: eyeCatchingImageId,
} });
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');
},
} }
}); });
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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -1,5 +1,6 @@
<template> <template><MkStickyContainer>
<MkSpacer :content-max="700"> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700">
<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in"> <transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
<div v-if="page" :key="page.id" v-size="{ max: [450] }" class="xcukqgmh"> <div v-if="page" :key="page.id" v-size="{ max: [450] }" class="xcukqgmh">
<div class="_block main"> <div class="_block main">
@ -56,138 +57,108 @@
<MkError v-else-if="error" @retry="fetch()"/> <MkError v-else-if="error" @retry="fetch()"/>
<MkLoading v-else/> <MkLoading v-else/>
</transition> </transition>
</MkSpacer> </MkSpacer></MkStickyContainer>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { computed, defineComponent } from 'vue'; import { computed, watch } from 'vue';
import XPage from '@/components/page/page.vue'; import XPage from '@/components/page/page.vue';
import MkButton from '@/components/ui/button.vue'; import MkButton from '@/components/ui/button.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols';
import { url } from '@/config'; import { url } from '@/config';
import MkFollowButton from '@/components/follow-button.vue'; import MkFollowButton from '@/components/follow-button.vue';
import MkContainer from '@/components/ui/container.vue'; import MkContainer from '@/components/ui/container.vue';
import MkPagination from '@/components/ui/pagination.vue'; import MkPagination from '@/components/ui/pagination.vue';
import MkPagePreview from '@/components/page-preview.vue'; import MkPagePreview from '@/components/page-preview.vue';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
export default defineComponent({ const props = defineProps<{
components: { pageName: string;
XPage, username: string;
MkButton, }>();
MkFollowButton,
MkContainer, let page = $ref(null);
MkPagination, let error = $ref(null);
MkPagePreview, 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,
}, },
} : null));
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,
});
}
}
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -1,86 +1,87 @@
<template> <template>
<MkSpacer :content-max="700"> <MkStickyContainer>
<div v-if="tab === 'featured'" class="rknalgpo"> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkPagination v-slot="{items}" :pagination="featuredPagesPagination"> <MkSpacer :content-max="700">
<MkPagePreview v-for="page in items" :key="page.id" class="ckltabjg" :page="page"/> <div v-if="tab === 'featured'" class="rknalgpo">
</MkPagination> <MkPagination v-slot="{items}" :pagination="featuredPagesPagination">
</div> <MkPagePreview v-for="page in items" :key="page.id" class="ckltabjg" :page="page"/>
</MkPagination>
</div>
<div v-else-if="tab === 'my'" class="rknalgpo my"> <div v-else-if="tab === 'my'" class="rknalgpo my">
<MkButton class="new" @click="create()"><i class="fas fa-plus"></i></MkButton> <MkButton class="new" @click="create()"><i class="fas fa-plus"></i></MkButton>
<MkPagination v-slot="{items}" :pagination="myPagesPagination"> <MkPagination v-slot="{items}" :pagination="myPagesPagination">
<MkPagePreview v-for="page in items" :key="page.id" class="ckltabjg" :page="page"/> <MkPagePreview v-for="page in items" :key="page.id" class="ckltabjg" :page="page"/>
</MkPagination> </MkPagination>
</div> </div>
<div v-else-if="tab === 'liked'" class="rknalgpo"> <div v-else-if="tab === 'liked'" class="rknalgpo">
<MkPagination v-slot="{items}" :pagination="likedPagesPagination"> <MkPagination v-slot="{items}" :pagination="likedPagesPagination">
<MkPagePreview v-for="like in items" :key="like.page.id" class="ckltabjg" :page="like.page"/> <MkPagePreview v-for="like in items" :key="like.page.id" class="ckltabjg" :page="like.page"/>
</MkPagination> </MkPagination>
</div> </div>
</MkSpacer> </MkSpacer>
</MkStickyContainer>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { computed, defineComponent } from 'vue'; import { computed, inject } from 'vue';
import MkPagePreview from '@/components/page-preview.vue'; import MkPagePreview from '@/components/page-preview.vue';
import MkPagination from '@/components/ui/pagination.vue'; import MkPagination from '@/components/ui/pagination.vue';
import MkButton from '@/components/ui/button.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({ const router = useRouter();
components: {
MkPagePreview, MkPagination, MkButton let tab = $ref('featured');
},
data() { const featuredPagesPagination = {
return { endpoint: 'pages/featured' as const,
[symbols.PAGE_INFO]: computed(() => ({ noPaging: true,
title: this.$ts.pages, };
icon: 'fas fa-sticky-note', const myPagesPagination = {
bg: 'var(--bg)', endpoint: 'i/pages' as const,
actions: [{ limit: 5,
icon: 'fas fa-plus', };
text: this.$ts.create, const likedPagesPagination = {
handler: this.create, endpoint: 'i/page-likes' as const,
}], limit: 5,
tabs: [{ };
active: this.tab === 'featured',
title: this.$ts._pages.featured, function create() {
icon: 'fas fa-fire-alt', router.push('/pages/new');
onClick: () => { this.tab = 'featured'; }, }
}, {
active: this.tab === 'my', const headerActions = $computed(() => [{
title: this.$ts._pages.my, icon: 'fas fa-plus',
icon: 'fas fa-edit', text: i18n.ts.create,
onClick: () => { this.tab = 'my'; }, handler: create,
}, { }]);
active: this.tab === 'liked',
title: this.$ts._pages.liked, const headerTabs = $computed(() => [{
icon: 'fas fa-heart', active: tab === 'featured',
onClick: () => { this.tab = 'liked'; }, title: i18n.ts._pages.featured,
},] icon: 'fas fa-fire-alt',
})), onClick: () => { tab = 'featured'; },
tab: 'featured', }, {
featuredPagesPagination: { active: tab === 'my',
endpoint: 'pages/featured' as const, title: i18n.ts._pages.my,
noPaging: true, icon: 'fas fa-edit',
}, onClick: () => { tab = 'my'; },
myPagesPagination: { }, {
endpoint: 'i/pages' as const, active: tab === 'liked',
limit: 5, title: i18n.ts._pages.liked,
}, icon: 'fas fa-heart',
likedPagesPagination: { onClick: () => { tab = 'liked'; },
endpoint: 'i/page-likes' as const, }]);
limit: 5,
}, definePageMetadata(computed(() => ({
}; title: i18n.ts.pages,
}, icon: 'fas fa-sticky-note',
methods: { bg: 'var(--bg)',
create() { })));
this.$router.push(`/pages/new`);
}
}
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -7,16 +7,18 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'; import { computed } from 'vue';
import MkSample from '@/components/sample.vue'; import MkSample from '@/components/sample.vue';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: computed(() => ({
title: i18n.ts.preview, const headerTabs = $computed(() => []);
icon: 'fas fa-eye',
bg: 'var(--bg)', definePageMetadata(computed(() => ({
})), title: i18n.ts.preview,
}); icon: 'fas fa-eye',
bg: 'var(--bg)',
})));
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -1,14 +1,17 @@
<template> <template>
<MkSpacer v-if="token" :content-max="700" :margin-min="16" :margin-max="32"> <MkStickyContainer>
<div class="_formRoot"> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<FormInput v-model="password" type="password" class="_formBlock"> <MkSpacer v-if="token" :content-max="700" :margin-min="16" :margin-max="32">
<template #prefix><i class="fas fa-lock"></i></template> <div class="_formRoot">
<template #label>{{ i18n.ts.newPassword }}</template> <FormInput v-model="password" type="password" class="_formBlock">
</FormInput> <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> <FormButton primary class="_formBlock" @click="save">{{ i18n.ts.save }}</FormButton>
</div> </div>
</MkSpacer> </MkSpacer>
</MkStickyContainer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -16,9 +19,9 @@ import { defineAsyncComponent, onMounted } from 'vue';
import FormInput from '@/components/form/input.vue'; import FormInput from '@/components/form/input.vue';
import FormButton from '@/components/ui/button.vue'; import FormButton from '@/components/ui/button.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { router } from '@/router'; import { mainRouter } from '@/router';
import { definePageMetadata } from '@/scripts/page-metadata';
const props = defineProps<{ const props = defineProps<{
token?: string; token?: string;
@ -31,22 +34,24 @@ async function save() {
token: props.token, token: props.token,
password: password, password: password,
}); });
router.push('/'); mainRouter.push('/');
} }
onMounted(() => { onMounted(() => {
if (props.token == null) { if (props.token == null) {
os.popup(defineAsyncComponent(() => import('@/components/forgot-password.vue')), {}, {}, 'closed'); os.popup(defineAsyncComponent(() => import('@/components/forgot-password.vue')), {}, {}, 'closed');
router.push('/'); mainRouter.push('/');
} }
}); });
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: {
title: i18n.ts.resetPassword, const headerTabs = $computed(() => []);
icon: 'fas fa-lock',
bg: 'var(--bg)', definePageMetadata({
}, title: i18n.ts.resetPassword,
icon: 'fas fa-lock',
bg: 'var(--bg)',
}); });
</script> </script>

View file

@ -19,7 +19,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { defineExpose, ref, watch } from 'vue'; import { ref, watch } from 'vue';
import 'prismjs'; import 'prismjs';
import { highlight, languages } from 'prismjs/components/prism-core'; import { highlight, languages } from 'prismjs/components/prism-core';
import 'prismjs/components/prism-clike'; import 'prismjs/components/prism-clike';
@ -32,9 +32,9 @@ import MkContainer from '@/components/ui/container.vue';
import MkButton from '@/components/ui/button.vue'; import MkButton from '@/components/ui/button.vue';
import { createAiScriptEnv } from '@/scripts/aiscript/api'; import { createAiScriptEnv } from '@/scripts/aiscript/api';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols';
import { $i } from '@/account'; import { $i } from '@/account';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const code = ref(''); const code = ref('');
const logs = ref<any[]>([]); const logs = ref<any[]>([]);
@ -67,7 +67,7 @@ async function run() {
logs.value.push({ logs.value.push({
id: Math.random(), id: Math.random(),
text: value.type === 'str' ? value.value : utils.valToString(value), text: value.type === 'str' ? value.value : utils.valToString(value),
print: true print: true,
}); });
}, },
log: (type, params) => { log: (type, params) => {
@ -75,11 +75,11 @@ async function run() {
case 'end': logs.value.push({ case 'end': logs.value.push({
id: Math.random(), id: Math.random(),
text: utils.valToString(params.val, true), text: utils.valToString(params.val, true),
print: false print: false,
}); break; }); break;
default: break; default: break;
} }
} },
}); });
let ast; let ast;
@ -88,7 +88,7 @@ async function run() {
} catch (error) { } catch (error) {
os.alert({ os.alert({
type: 'error', type: 'error',
text: 'Syntax error :(' text: 'Syntax error :(',
}); });
return; return;
} }
@ -97,7 +97,7 @@ async function run() {
} catch (error: any) { } catch (error: any) {
os.alert({ os.alert({
type: 'error', type: 'error',
text: error.message text: error.message,
}); });
} }
} }
@ -106,11 +106,13 @@ function highlighter(code) {
return highlight(code, languages.js, 'javascript'); return highlight(code, languages.js, 'javascript');
} }
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: {
title: i18n.ts.scratchpad, const headerTabs = $computed(() => []);
icon: 'fas fa-terminal',
}, definePageMetadata({
title: i18n.ts.scratchpad,
icon: 'fas fa-terminal',
}); });
</script> </script>

View file

@ -1,16 +1,17 @@
<template> <template>
<div class="_section"> <MkStickyContainer>
<div class="_content"> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="800">
<XNotes ref="notes" :pagination="pagination"/> <XNotes ref="notes" :pagination="pagination"/>
</div> </MkSpacer>
</div> </MkStickyContainer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'; import { computed } from 'vue';
import XNotes from '@/components/notes.vue'; import XNotes from '@/components/notes.vue';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const props = defineProps<{ const props = defineProps<{
query: string; query: string;
@ -23,14 +24,16 @@ const pagination = {
params: computed(() => ({ params: computed(() => ({
query: props.query, query: props.query,
channelId: props.channel, channelId: props.channel,
})) })),
}; };
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: computed(() => ({
title: i18n.t('searchWith', { q: props.query }), const headerTabs = $computed(() => []);
icon: 'fas fa-search',
bg: 'var(--bg)', definePageMetadata(computed(() => ({
})), title: i18n.t('searchWith', { q: props.query }),
}); icon: 'fas fa-search',
bg: 'var(--bg)',
})));
</script> </script>

View file

@ -127,30 +127,32 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { defineExpose, onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import FormSection from '@/components/form/section.vue'; import FormSection from '@/components/form/section.vue';
import MkKeyValue from '@/components/key-value.vue'; import MkKeyValue from '@/components/key-value.vue';
import * as os from '@/os'; import * as os from '@/os';
import number from '@/filters/number'; import number from '@/filters/number';
import bytes from '@/filters/bytes'; import bytes from '@/filters/bytes';
import * as symbols from '@/symbols';
import { $i } from '@/account'; import { $i } from '@/account';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const stats = ref<any>({}); const stats = ref<any>({});
onMounted(() => { onMounted(() => {
os.api('users/stats', { os.api('users/stats', {
userId: $i!.id userId: $i!.id,
}).then(response => { }).then(response => {
stats.value = response; stats.value = response;
}); });
}); });
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: {
title: i18n.ts.accountInfo, const headerTabs = $computed(() => []);
icon: 'fas fa-info-circle'
} definePageMetadata({
title: i18n.ts.accountInfo,
icon: 'fas fa-info-circle',
}); });
</script> </script>

View file

@ -21,13 +21,13 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { defineAsyncComponent, defineExpose, ref } from 'vue'; import { defineAsyncComponent, ref } from 'vue';
import FormSuspense from '@/components/form/suspense.vue'; import FormSuspense from '@/components/form/suspense.vue';
import FormButton from '@/components/ui/button.vue'; import FormButton from '@/components/ui/button.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols'; import { getAccounts, addAccount as addAccounts, removeAccount as _removeAccount, login, $i } from '@/account';
import { getAccounts, addAccount as addAccounts, login, $i } from '@/account';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const storedAccounts = ref<any>(null); const storedAccounts = ref<any>(null);
const accounts = ref<any>(null); const accounts = ref<any>(null);
@ -39,7 +39,7 @@ const init = async () => {
console.log(storedAccounts.value); console.log(storedAccounts.value);
return os.api('users/show', { return os.api('users/show', {
userIds: storedAccounts.value.map(x => x.id) userIds: storedAccounts.value.map(x => x.id),
}); });
}).then(response => { }).then(response => {
accounts.value = response; accounts.value = response;
@ -70,6 +70,10 @@ function addAccount(ev) {
}], ev.currentTarget ?? ev.target); }], ev.currentTarget ?? ev.target);
} }
function removeAccount(account) {
_removeAccount(account.id);
}
function addExistingAccount() { function addExistingAccount() {
os.popup(defineAsyncComponent(() => import('@/components/signin-dialog.vue')), {}, { os.popup(defineAsyncComponent(() => import('@/components/signin-dialog.vue')), {}, {
done: res => { done: res => {
@ -98,12 +102,14 @@ function switchAccountWithToken(token: string) {
login(token); login(token);
} }
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: {
title: i18n.ts.accounts, const headerTabs = $computed(() => []);
icon: 'fas fa-users',
bg: 'var(--bg)', definePageMetadata({
} title: i18n.ts.accounts,
icon: 'fas fa-users',
bg: 'var(--bg)',
}); });
</script> </script>

View file

@ -7,12 +7,12 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { defineAsyncComponent, defineExpose, ref } from 'vue'; import { defineAsyncComponent, ref } from 'vue';
import FormLink from '@/components/form/link.vue'; import FormLink from '@/components/form/link.vue';
import FormButton from '@/components/ui/button.vue'; import FormButton from '@/components/ui/button.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const isDesktop = ref(window.innerWidth >= 1100); const isDesktop = ref(window.innerWidth >= 1100);
@ -29,17 +29,19 @@ function generateToken() {
os.alert({ os.alert({
type: 'success', type: 'success',
title: i18n.ts.token, title: i18n.ts.token,
text: token text: token,
}); });
}, },
}, 'closed'); }, 'closed');
} }
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: {
title: 'API', const headerTabs = $computed(() => []);
icon: 'fas fa-key',
bg: 'var(--bg)', definePageMetadata({
} title: 'API',
icon: 'fas fa-key',
bg: 'var(--bg)',
}); });
</script> </script>

View file

@ -7,7 +7,7 @@
<div>{{ i18n.ts.nothing }}</div> <div>{{ i18n.ts.nothing }}</div>
</div> </div>
</template> </template>
<template v-slot="{items}"> <template #default="{items}">
<div v-for="token in items" :key="token.id" class="_panel bfomjevm"> <div v-for="token in items" :key="token.id" class="_panel bfomjevm">
<img v-if="token.iconUrl" class="icon" :src="token.iconUrl" alt=""/> <img v-if="token.iconUrl" class="icon" :src="token.iconUrl" alt=""/>
<div class="body"> <div class="body">
@ -38,11 +38,11 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { defineExpose, ref } from 'vue'; import { ref } from 'vue';
import FormPagination from '@/components/ui/pagination.vue'; import FormPagination from '@/components/ui/pagination.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const list = ref<any>(null); const list = ref<any>(null);
@ -50,8 +50,8 @@ const pagination = {
endpoint: 'i/apps' as const, endpoint: 'i/apps' as const,
limit: 100, limit: 100,
params: { params: {
sort: '+lastUsedAt' sort: '+lastUsedAt',
} },
}; };
function revoke(token) { function revoke(token) {
@ -60,12 +60,14 @@ function revoke(token) {
}); });
} }
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: {
title: i18n.ts.installedApps, const headerTabs = $computed(() => []);
icon: 'fas fa-plug',
bg: 'var(--bg)', definePageMetadata({
} title: i18n.ts.installedApps,
icon: 'fas fa-plug',
bg: 'var(--bg)',
}); });
</script> </script>

View file

@ -9,13 +9,13 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { defineExpose, ref, watch } from 'vue'; import { ref, watch } from 'vue';
import FormTextarea from '@/components/form/textarea.vue'; import FormTextarea from '@/components/form/textarea.vue';
import FormInfo from '@/components/ui/info.vue'; import FormInfo from '@/components/ui/info.vue';
import * as os from '@/os'; import * as os from '@/os';
import { unisonReload } from '@/scripts/unison-reload'; import { unisonReload } from '@/scripts/unison-reload';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const localCustomCss = ref(localStorage.getItem('customCss') ?? ''); const localCustomCss = ref(localStorage.getItem('customCss') ?? '');
@ -35,11 +35,13 @@ watch(localCustomCss, async () => {
await apply(); await apply();
}); });
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: {
title: i18n.ts.customCss, const headerTabs = $computed(() => []);
icon: 'fas fa-code',
bg: 'var(--bg)', definePageMetadata({
} title: i18n.ts.customCss,
icon: 'fas fa-code',
bg: 'var(--bg)',
}); });
</script> </script>

View file

@ -30,7 +30,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, defineExpose, watch } from 'vue'; import { computed, watch } from 'vue';
import FormSwitch from '@/components/form/switch.vue'; import FormSwitch from '@/components/form/switch.vue';
import FormLink from '@/components/form/link.vue'; import FormLink from '@/components/form/link.vue';
import FormRadios from '@/components/form/radios.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 { deckStore } from '@/ui/deck/deck-store';
import * as os from '@/os'; import * as os from '@/os';
import { unisonReload } from '@/scripts/unison-reload'; import { unisonReload } from '@/scripts/unison-reload';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const navWindow = computed(deckStore.makeGetterSetter('navWindow')); const navWindow = computed(deckStore.makeGetterSetter('navWindow'));
const alwaysShowMainColumn = computed(deckStore.makeGetterSetter('alwaysShowMainColumn')); const alwaysShowMainColumn = computed(deckStore.makeGetterSetter('alwaysShowMainColumn'));
@ -62,7 +62,7 @@ watch(navWindow, async () => {
async function setProfile() { async function setProfile() {
const { canceled, result: name } = await os.inputText({ const { canceled, result: name } = await os.inputText({
title: i18n.ts._deck.profile, title: i18n.ts._deck.profile,
allowEmpty: false allowEmpty: false,
}); });
if (canceled) return; if (canceled) return;
@ -70,11 +70,13 @@ async function setProfile() {
unisonReload(); unisonReload();
} }
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: {
title: i18n.ts.deck, const headerTabs = $computed(() => []);
icon: 'fas fa-columns',
bg: 'var(--bg)', definePageMetadata({
} title: i18n.ts.deck,
icon: 'fas fa-columns',
bg: 'var(--bg)',
}); });
</script> </script>

View file

@ -8,13 +8,12 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { defineExpose } from 'vue';
import FormInfo from '@/components/ui/info.vue'; import FormInfo from '@/components/ui/info.vue';
import FormButton from '@/components/ui/button.vue'; import FormButton from '@/components/ui/button.vue';
import * as os from '@/os'; import * as os from '@/os';
import { signout } from '@/account'; import { signout } from '@/account';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
async function deleteAccount() { async function deleteAccount() {
{ {
@ -27,12 +26,12 @@ async function deleteAccount() {
const { canceled, result: password } = await os.inputText({ const { canceled, result: password } = await os.inputText({
title: i18n.ts.password, title: i18n.ts.password,
type: 'password' type: 'password',
}); });
if (canceled) return; if (canceled) return;
await os.apiWithDialog('i/delete-account', { await os.apiWithDialog('i/delete-account', {
password: password password: password,
}); });
await os.alert({ await os.alert({
@ -42,11 +41,13 @@ async function deleteAccount() {
await signout(); await signout();
} }
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: {
title: i18n.ts._accountDelete.accountDelete, const headerTabs = $computed(() => []);
icon: 'fas fa-exclamation-triangle',
bg: 'var(--bg)', definePageMetadata({
} title: i18n.ts._accountDelete.accountDelete,
icon: 'fas fa-exclamation-triangle',
bg: 'var(--bg)',
}); });
</script> </script>

View file

@ -34,7 +34,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, defineExpose, ref } from 'vue'; import { computed, ref } from 'vue';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import FormLink from '@/components/form/link.vue'; import FormLink from '@/components/form/link.vue';
import FormSwitch from '@/components/form/switch.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 FormSplit from '@/components/form/split.vue';
import * as os from '@/os'; import * as os from '@/os';
import bytes from '@/filters/bytes'; import bytes from '@/filters/bytes';
import * as symbols from '@/symbols';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
import MkChart from '@/components/chart.vue'; import MkChart from '@/components/chart.vue';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const fetching = ref(true); const fetching = ref(true);
const usage = ref<any>(null); const usage = ref<any>(null);
@ -59,8 +59,8 @@ const meterStyle = computed(() => {
background: tinycolor({ background: tinycolor({
h: 180 - (usage.value / capacity.value * 180), h: 180 - (usage.value / capacity.value * 180),
s: 0.7, s: 0.7,
l: 0.5 l: 0.5,
}) }),
}; };
}); });
@ -74,7 +74,7 @@ os.api('drive').then(info => {
if (defaultStore.state.uploadFolder) { if (defaultStore.state.uploadFolder) {
os.api('drive/folders/show', { os.api('drive/folders/show', {
folderId: defaultStore.state.uploadFolder folderId: defaultStore.state.uploadFolder,
}).then(response => { }).then(response => {
uploadFolder.value = response; uploadFolder.value = response;
}); });
@ -86,7 +86,7 @@ function chooseUploadFolder() {
os.success(); os.success();
if (defaultStore.state.uploadFolder) { if (defaultStore.state.uploadFolder) {
uploadFolder.value = await os.api('drive/folders/show', { uploadFolder.value = await os.api('drive/folders/show', {
folderId: defaultStore.state.uploadFolder folderId: defaultStore.state.uploadFolder,
}); });
} else { } else {
uploadFolder.value = null; uploadFolder.value = null;
@ -94,12 +94,14 @@ function chooseUploadFolder() {
}); });
} }
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: {
title: i18n.ts.drive, const headerTabs = $computed(() => []);
icon: 'fas fa-cloud',
bg: 'var(--bg)', definePageMetadata({
} title: i18n.ts.drive,
icon: 'fas fa-cloud',
bg: 'var(--bg)',
}); });
</script> </script>

View file

@ -40,27 +40,27 @@
</template> </template>
<script lang="ts" setup> <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 FormSection from '@/components/form/section.vue';
import FormInput from '@/components/form/input.vue'; import FormInput from '@/components/form/input.vue';
import FormSwitch from '@/components/form/switch.vue'; import FormSwitch from '@/components/form/switch.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols';
import { $i } from '@/account'; import { $i } from '@/account';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const emailAddress = ref($i!.email); const emailAddress = ref($i!.email);
const onChangeReceiveAnnouncementEmail = (v) => { const onChangeReceiveAnnouncementEmail = (v) => {
os.api('i/update', { os.api('i/update', {
receiveAnnouncementEmail: v receiveAnnouncementEmail: v,
}); });
}; };
const saveEmailAddress = () => { const saveEmailAddress = () => {
os.inputText({ os.inputText({
title: i18n.ts.password, title: i18n.ts.password,
type: 'password' type: 'password',
}).then(({ canceled, result: password }) => { }).then(({ canceled, result: password }) => {
if (canceled) return; if (canceled) return;
os.apiWithDialog('i/update-email', { os.apiWithDialog('i/update-email', {
@ -86,7 +86,7 @@ const saveNotificationSettings = () => {
...[emailNotification_follow.value ? 'follow' : null], ...[emailNotification_follow.value ? 'follow' : null],
...[emailNotification_receiveFollowRequest.value ? 'receiveFollowRequest' : null], ...[emailNotification_receiveFollowRequest.value ? 'receiveFollowRequest' : null],
...[emailNotification_groupInvited.value ? 'groupInvited' : null], ...[emailNotification_groupInvited.value ? 'groupInvited' : null],
].filter(x => x != null) ].filter(x => x != null),
}); });
}; };
@ -100,11 +100,13 @@ onMounted(() => {
}); });
}); });
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: {
title: i18n.ts.email, const headerTabs = $computed(() => []);
icon: 'fas fa-envelope',
bg: 'var(--bg)', definePageMetadata({
} title: i18n.ts.email,
icon: 'fas fa-envelope',
bg: 'var(--bg)',
}); });
</script> </script>

View file

@ -48,7 +48,8 @@
<FormSwitch v-model="disableShowingAnimatedImages" class="_formBlock">{{ i18n.ts.disableShowingAnimatedImages }}</FormSwitch> <FormSwitch v-model="disableShowingAnimatedImages" class="_formBlock">{{ i18n.ts.disableShowingAnimatedImages }}</FormSwitch>
<FormSwitch v-model="squareAvatars" class="_formBlock">{{ i18n.ts.squareAvatars }}</FormSwitch> <FormSwitch v-model="squareAvatars" class="_formBlock">{{ i18n.ts.squareAvatars }}</FormSwitch>
<FormSwitch v-model="useSystemFont" class="_formBlock">{{ i18n.ts.useSystemFont }}</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> <div><Mfm :key="useOsNativeEmojis" text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></div>
</FormSwitch> </FormSwitch>
<FormSwitch v-model="disableDrawer" class="_formBlock">{{ i18n.ts.disableDrawer }}</FormSwitch> <FormSwitch v-model="disableDrawer" class="_formBlock">{{ i18n.ts.disableDrawer }}</FormSwitch>
@ -92,7 +93,7 @@
</template> </template>
<script lang="ts" setup> <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 FormSwitch from '@/components/form/switch.vue';
import FormSelect from '@/components/form/select.vue'; import FormSelect from '@/components/form/select.vue';
import FormRadios from '@/components/form/radios.vue'; import FormRadios from '@/components/form/radios.vue';
@ -104,8 +105,8 @@ import { langs } from '@/config';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
import * as os from '@/os'; import * as os from '@/os';
import { unisonReload } from '@/scripts/unison-reload'; import { unisonReload } from '@/scripts/unison-reload';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const lang = ref(localStorage.getItem('lang')); const lang = ref(localStorage.getItem('lang'));
const fontSize = ref(localStorage.getItem('fontSize')); const fontSize = ref(localStorage.getItem('fontSize'));
@ -173,16 +174,18 @@ watch([
aiChanMode, aiChanMode,
showGapBetweenNotesInTimeline, showGapBetweenNotesInTimeline,
instanceTicker, instanceTicker,
overridedDeviceKind overridedDeviceKind,
], async () => { ], async () => {
await reloadAsk(); await reloadAsk();
}); });
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: {
title: i18n.ts.general, const headerTabs = $computed(() => []);
icon: 'fas fa-cogs',
bg: 'var(--bg)' definePageMetadata({
} title: i18n.ts.general,
icon: 'fas fa-cogs',
bg: 'var(--bg)',
}); });
</script> </script>

View file

@ -38,15 +38,15 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { defineExpose, ref } from 'vue'; import { ref } from 'vue';
import MkButton from '@/components/ui/button.vue'; import MkButton from '@/components/ui/button.vue';
import FormSection from '@/components/form/section.vue'; import FormSection from '@/components/form/section.vue';
import FormGroup from '@/components/form/group.vue'; import FormGroup from '@/components/form/group.vue';
import FormSwitch from '@/components/form/switch.vue'; import FormSwitch from '@/components/form/switch.vue';
import * as os from '@/os'; import * as os from '@/os';
import { selectFile } from '@/scripts/select-file'; import { selectFile } from '@/scripts/select-file';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const excludeMutingUsers = ref(false); const excludeMutingUsers = ref(false);
const excludeInactiveUsers = 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); os.api('i/import-blocking', { fileId: file.id }).then(onImportSuccess).catch(onError);
}; };
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: {
title: i18n.ts.importAndExport, const headerTabs = $computed(() => []);
icon: 'fas fa-boxes',
bg: 'var(--bg)', definePageMetadata({
} title: i18n.ts.importAndExport,
icon: 'fas fa-boxes',
bg: 'var(--bg)',
}); });
</script> </script>

View file

@ -1,46 +1,42 @@
<template> <template>
<MkSpacer :content-max="900" :margin-min="20" :margin-max="32"> <MkStickyContainer>
<div ref="el" class="vvcocwet" :class="{ wide: !narrow }"> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<div class="header"> <MkSpacer :content-max="900" :margin-min="20" :margin-max="32">
<div class="title"> <div ref="el" class="vvcocwet" :class="{ wide: !narrow }">
<MkA v-if="narrow" to="/settings">{{ $ts.settings }}</MkA> <div class="body">
<template v-else>{{ $ts.settings }}</template> <div v-if="!narrow || initialPage == null" class="nav">
</div> <div class="baaadecd">
<div v-if="childInfo" class="subtitle">{{ childInfo.title }}</div> <MkInfo v-if="emailNotConfigured" warn class="info">{{ $ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ $ts.configure }}</MkA></MkInfo>
</div> <MkSuperMenu :def="menuDef" :grid="initialPage == null"></MkSuperMenu>
<div class="body"> </div>
<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 v-if="!(narrow && initialPage == null)" class="main"> <div class="bkzroven">
<div class="bkzroven"> <component :is="component" :key="initialPage" v-bind="pageProps"/>
<component :is="component" :ref="el => pageChanged(el)" :key="initialPage" v-bind="pageProps"/> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </MkSpacer>
</MkSpacer> </mkstickycontainer>
</template> </template>
<script setup lang="ts"> <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 { i18n } from '@/i18n';
import MkInfo from '@/components/ui/info.vue'; import MkInfo from '@/components/ui/info.vue';
import MkSuperMenu from '@/components/ui/super-menu.vue'; import MkSuperMenu from '@/components/ui/super-menu.vue';
import { scroll } from '@/scripts/scroll'; import { scroll } from '@/scripts/scroll';
import { signout } from '@/account'; import { signout , $i } from '@/account';
import { unisonReload } from '@/scripts/unison-reload'; import { unisonReload } from '@/scripts/unison-reload';
import * as symbols from '@/symbols';
import { instance } from '@/instance'; import { instance } from '@/instance';
import { $i } from '@/account'; import { useRouter } from '@/router';
import { MisskeyNavigator } from '@/scripts/navigate'; import { definePageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
const props = defineProps<{ const props = withDefaults(defineProps<{
initialPage?: string initialPage?: string;
}>(); }>(), {
});
const indexInfo = { const indexInfo = {
title: i18n.ts.settings, title: i18n.ts.settings,
@ -52,7 +48,7 @@ const INFO = ref(indexInfo);
const el = ref<HTMLElement | null>(null); const el = ref<HTMLElement | null>(null);
const childInfo = ref(null); const childInfo = ref(null);
const nav = new MisskeyNavigator(); const router = useRouter();
const narrow = ref(false); const narrow = ref(false);
const NARROW_THRESHOLD = 600; const NARROW_THRESHOLD = 600;
@ -189,7 +185,7 @@ const menuDef = computed(() => [{
signout(); signout();
}, },
danger: true, danger: true,
},], }],
}]); }]);
const pageProps = ref({}); const pageProps = ref({});
@ -242,7 +238,7 @@ watch(component, () => {
watch(() => props.initialPage, () => { watch(() => props.initialPage, () => {
if (props.initialPage == null && !narrow.value) { if (props.initialPage == null && !narrow.value) {
nav.push('/settings/profile'); router.push('/settings/profile');
} else { } else {
if (props.initialPage == null) { if (props.initialPage == null) {
INFO.value = indexInfo; INFO.value = indexInfo;
@ -252,7 +248,7 @@ watch(() => props.initialPage, () => {
watch(narrow, () => { watch(narrow, () => {
if (props.initialPage == null && !narrow.value) { 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; narrow.value = el.value.offsetWidth < NARROW_THRESHOLD;
if (props.initialPage == null && !narrow.value) { 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 emailNotConfigured = computed(() => instance.enableEmail && ($i.email == null || !$i.emailVerified));
const pageChanged = (page) => { provideMetadataReceiver((info) => {
if (page == null) { if (info == null) {
childInfo.value = null; childInfo.value = null;
} else { } else {
childInfo.value = page[symbols.PAGE_INFO]; childInfo.value = info;
} }
};
defineExpose({
[symbols.PAGE_INFO]: INFO,
}); });
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata(INFO);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.vvcocwet { .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 { > .body {
> .nav { > .nav {
.baaadecd { .baaadecd {

Some files were not shown because too many files have changed in this diff Show more