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() { let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
return { let rootEl = $ref();
width: 860, let modal = $ref<InstanceType<typeof MkModal>>();
height: 660, let path = $ref(props.initialPath);
pageInfo: null, let width = $ref(860);
path: this.initialPath, let height = $ref(660);
component: this.initialComponent, const history = [];
props: this.initialProps,
history: [],
};
},
computed: { provide('router', router);
url(): string { provideMetadataReceiver((info) => {
return url + this.path; pageMetadata = info;
}, });
provide('shouldOmitHeaderTitle', true);
provide('shouldHeaderThin', true);
contextmenu() { const pageUrl = $computed(() => url + path);
const contextmenu = $computed(() => {
return [{ return [{
type: 'label', type: 'label',
text: this.path, text: path,
}, { }, {
icon: 'fas fa-expand-alt', icon: 'fas fa-expand-alt',
text: this.$ts.showInPage, text: i18n.ts.showInPage,
action: this.expand, action: 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', icon: 'fas fa-external-link-alt',
text: this.$ts.popout, text: i18n.ts.popout,
action: this.popout, action: popout,
}, null, { }, null, {
icon: 'fas fa-external-link-alt', icon: 'fas fa-external-link-alt',
text: this.$ts.openInNewTab, text: i18n.ts.openInNewTab,
action: () => { action: () => {
window.open(this.url, '_blank'); window.open(pageUrl, '_blank');
this.$refs.window.close(); modal.close();
}, },
}, { }, {
icon: 'fas fa-link', icon: 'fas fa-link',
text: this.$ts.copyLink, text: i18n.ts.copyLink,
action: () => { action: () => {
copyToClipboard(this.url); copyToClipboard(pageUrl);
}, },
}]; }];
},
},
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);
},
},
}); });
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, }>();
},
inject: { defineEmits<{
sideViewHook: { (ev: 'closed'): void;
default: null }>();
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,
});
} }
},
provide() { return buttons;
return { });
navHook: (path) => { const buttonsRight = $computed(() => {
this.navigate(path); const buttons = [{
},
shouldHeaderThin: true,
};
},
props: {
initialPath: {
type: String,
required: true,
},
initialComponent: {
type: Object,
required: true,
},
initialProps: {
type: Object,
required: false,
default: () => {},
},
},
emits: ['closed'],
data() {
return {
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', icon: 'fas fa-expand-alt',
text: this.$ts.showInPage, title: i18n.ts.showInPage,
action: this.expand onClick: 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: { return buttons;
changePage(page) { });
if (page == null) return;
if (page[symbols.PAGE_INFO]) {
this.pageInfo = page[symbols.PAGE_INFO];
}
},
navigate(path, record = true) { router.addListener('push', ctx => {
if (record) this.history.push(this.path); history.push(router.getCurrentPath());
this.path = path; });
const { component, props } = resolve(path);
this.component = component;
this.props = props;
},
menu(ev) { provide('router', router);
os.popupMenu([{ 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', icon: 'fas fa-external-link-alt',
text: this.$ts.openInNewTab, text: i18n.ts.popout,
action: popout,
}, {
icon: 'fas fa-external-link-alt',
text: i18n.ts.openInNewTab,
action: () => { action: () => {
window.open(this.url, '_blank'); window.open(url + router.getCurrentPath(), '_blank');
this.$refs.window.close(); windowEl.close();
} },
}, { }, {
icon: 'fas fa-link', icon: 'fas fa-link',
text: this.$ts.copyLink, text: i18n.ts.copyLink,
action: () => { action: () => {
copyToClipboard(this.url); copyToClipboard(url + router.getCurrentPath());
}
}], ev.currentTarget ?? ev.target);
}, },
}]));
back() { function menu(ev) {
this.navigate(this.history.pop(), false); os.popupMenu(contextmenu, ev.currentTarget ?? ev.target);
}, }
close() { function back() {
this.$refs.window.close(); history.pop();
}, router.change(history[history.length - 1]);
}
expand() { function close() {
this.$router.push(this.path); windowEl.close();
this.$refs.window.close(); }
},
popout() { function expand() {
popout(this.path, this.$el); mainRouter.push(router.getCurrentPath());
this.$refs.window.close(); windowEl.close();
}, }
},
function popout() {
_popout(router.getCurrentPath(), windowEl.$el);
windowEl.close();
}
defineExpose({
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">
@ -63,24 +63,24 @@ function dragClear(fn) {
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();
@ -360,8 +373,8 @@ export default defineComponent({
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';
@ -172,9 +171,8 @@ 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]: {
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.error, title: i18n.ts.error,
icon: 'fas fa-exclamation-triangle', icon: 'fas fa-exclamation-triangle',
},
}); });
</script> </script>

View File

@ -1,5 +1,7 @@
<template> <template>
<div style="overflow: clip;"> <MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<div style="overflow: clip;">
<MkSpacer :content-max="600" :margin-min="20"> <MkSpacer :content-max="600" :margin-min="20">
<div class="_formRoot znqjceqz"> <div class="_formRoot znqjceqz">
<div id="debug"></div> <div id="debug"></div>
@ -56,7 +58,8 @@
</FormSection> </FormSection>
</div> </div>
</MkSpacer> </MkSpacer>
</div> </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]: {
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.aboutMisskey, title: i18n.ts.aboutMisskey,
icon: null, icon: null,
bg: 'var(--bg)', bg: 'var(--bg)',
},
}); });
</script> </script>

View File

@ -1,5 +1,7 @@
<template> <template>
<MkSpacer v-if="tab === 'overview'" :content-max="600" :margin-min="20"> <MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer v-if="tab === 'overview'" :content-max="600" :margin-min="20">
<div class="_formRoot"> <div class="_formRoot">
<div class="_formBlock fwhjspax" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }"> <div class="_formBlock fwhjspax" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }">
<div class="content"> <div class="content">
@ -64,15 +66,16 @@
</div> </div>
</FormSection> </FormSection>
</div> </div>
</MkSpacer> </MkSpacer>
<MkSpacer v-else-if="tab === 'charts'" :content-max="1200" :margin-min="20"> <MkSpacer v-else-if="tab === 'charts'" :content-max="1200" :margin-min="20">
<MkInstanceStats :chart-limit="500" :detailed="true"/> <MkInstanceStats :chart-limit="500" :detailed="true"/>
</MkSpacer> </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',
bg: 'var(--bg)',
tabs: [{
active: tab === 'overview', active: tab === 'overview',
title: i18n.ts.overview, title: i18n.ts.overview,
onClick: () => { tab = 'overview'; }, onClick: () => { tab = 'overview'; },
}, { }, {
active: tab === 'charts', active: tab === 'charts',
title: i18n.ts.charts, title: i18n.ts.charts,
icon: 'fas fa-chart-bar', icon: 'fas fa-chart-bar',
onClick: () => { tab = 'charts'; }, 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,5 +1,7 @@
<template> <template>
<MkSpacer :content-max="500" :margin-min="16" :margin-max="32"> <MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="500" :margin-min="16" :margin-max="32">
<div v-if="file" class="cxqhhsmd _formRoot"> <div v-if="file" class="cxqhhsmd _formRoot">
<div class="_formBlock"> <div class="_formBlock">
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/> <MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
@ -24,7 +26,8 @@
</details> </details>
</div> </div>
</div> </div>
</MkSpacer> </MkSpacer>
</MkStickyContainer>
</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(() => ({
const headerTabs = $computed(() => []);
definePageMetadata(computed(() => ({
title: file ? i18n.ts.file + ': ' + file.name : i18n.ts.file, title: file ? i18n.ts.file + ': ' + file.name : i18n.ts.file,
icon: 'fas fa-file', icon: 'fas fa-file',
bg: 'var(--bg)', 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,5 +1,8 @@
<template> <template>
<div class="lcixvhis"> <MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="900">
<div class="lcixvhis">
<div class="_section reports"> <div class="_section reports">
<div class="_content"> <div class="_content">
<div class="inputs" style="display: flex;"> <div class="inputs" style="display: flex;">
@ -38,19 +41,22 @@
</MkPagination> </MkPagination>
</div> </div>
</div> </div>
</div> </div>
</MkSpacer>
</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]: {
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.abuseReports, title: i18n.ts.abuseReports,
icon: 'fas fa-exclamation-circle', icon: 'fas fa-exclamation-circle',
bg: 'var(--bg)', bg: 'var(--bg)',
}
}); });
</script> </script>

View File

@ -1,5 +1,7 @@
<template> <template>
<MkSpacer :content-max="900"> <MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="900">
<div class="uqshojas"> <div class="uqshojas">
<div v-for="ad in ads" class="_panel _formRoot ad"> <div v-for="ad in ads" class="_panel _formRoot ad">
<MkAd v-if="ad.url" :specify="ad"/> <MkAd v-if="ad.url" :specify="ad"/>
@ -40,19 +42,21 @@
</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]: {
title: i18n.ts.ads,
icon: 'fas fa-audio-description',
bg: 'var(--bg)',
actions: [{
asFullButton: true, asFullButton: true,
icon: 'fas fa-plus', icon: 'fas fa-plus',
text: i18n.ts.add, text: i18n.ts.add,
handler: add, handler: add,
}], }]);
}
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.ads,
icon: 'fas fa-audio-description',
bg: 'var(--bg)',
}); });
</script> </script>

View File

@ -1,5 +1,8 @@
<template> <template>
<div class="ztgjmzrw"> <MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="900">
<div class="ztgjmzrw">
<section v-for="announcement in announcements" class="_card _gap announcements"> <section v-for="announcement in announcements" class="_card _gap announcements">
<div class="_content announcement"> <div class="_content announcement">
<MkInput v-model="announcement.title"> <MkInput v-model="announcement.title">
@ -18,17 +21,20 @@
</div> </div>
</div> </div>
</section> </section>
</div> </div>
</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 * 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]: {
title: i18n.ts.announcements,
icon: 'fas fa-broadcast-tower',
bg: 'var(--bg)',
actions: [{
asFullButton: true, asFullButton: true,
icon: 'fas fa-plus', icon: 'fas fa-plus',
text: i18n.ts.add, text: i18n.ts.add,
handler: add, handler: add,
}], }]);
}
const headerTabs = $computed(() => []);
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]: {
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.database, title: i18n.ts.database,
icon: 'fas fa-database', icon: 'fas fa-database',
bg: 'var(--bg)', bg: 'var(--bg)',
}
}); });
</script> </script>

View File

@ -1,5 +1,7 @@
<template> <template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> <MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init"> <FormSuspense :p="init">
<div class="_formRoot"> <div class="_formRoot">
<FormSwitch v-model="enableEmail" class="_formBlock"> <FormSwitch v-model="enableEmail" class="_formBlock">
@ -39,11 +41,13 @@
</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]: {
title: i18n.ts.emailServer,
icon: 'fas fa-envelope',
bg: 'var(--bg)',
actions: [{
asFullButton: true, asFullButton: true,
text: i18n.ts.testEmail, text: i18n.ts.testEmail,
handler: testEmail, handler: testEmail,
}, { }, {
asFullButton: true, asFullButton: true,
icon: 'fas fa-check', icon: 'fas fa-check',
text: i18n.ts.save, text: i18n.ts.save,
handler: save, handler: save,
}], }]);
}
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.emailServer,
icon: 'fas fa-envelope',
bg: 'var(--bg)',
}); });
</script> </script>

View File

@ -1,5 +1,8 @@
<template> <template>
<MkSpacer :content-max="900"> <div>
<MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="900">
<div class="ogwlenmc"> <div class="ogwlenmc">
<div v-if="tab === 'local'" class="local"> <div v-if="tab === 'local'" class="local">
<MkInput v-model="query" :debounce="true" type="search"> <MkInput v-model="query" :debounce="true" type="search">
@ -19,7 +22,7 @@
</div> </div>
<MkPagination ref="emojisPaginationComponent" :pagination="pagination"> <MkPagination ref="emojisPaginationComponent" :pagination="pagination">
<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">
<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)"> <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"/> <img :src="emoji.url" class="img" :alt="emoji.name"/>
@ -45,7 +48,7 @@
</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"/>
@ -59,11 +62,14 @@
</MkPagination> </MkPagination>
</div> </div>
</div> </div>
</MkSpacer> </MkSpacer>
</MkStickyContainer>
</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(() => ({
title: i18n.ts.customEmojis,
icon: 'fas fa-laugh',
bg: 'var(--bg)',
actions: [{
asFullButton: true, asFullButton: true,
icon: 'fas fa-plus', icon: 'fas fa-plus',
text: i18n.ts.addEmoji, text: i18n.ts.addEmoji,
handler: add, handler: add,
}, { }, {
icon: 'fas fa-ellipsis-h', icon: 'fas fa-ellipsis-h',
handler: menu, handler: menu,
}], }]);
tabs: [{
const headerTabs = $computed(() => [{
active: tab.value === 'local', active: tab.value === 'local',
title: i18n.ts.local, title: i18n.ts.local,
onClick: () => { tab.value = 'local'; }, onClick: () => { tab.value = 'local'; },
}, { }, {
active: tab.value === 'remote', active: tab.value === 'remote',
title: i18n.ts.remote, title: i18n.ts.remote,
onClick: () => { tab.value = 'remote'; }, onClick: () => { tab.value = 'remote'; },
},] }]);
})),
}); definePageMetadata(computed(() => ({
title: i18n.ts.customEmojis,
icon: 'fas fa-laugh',
bg: 'var(--bg)',
})));
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -1,5 +1,9 @@
<template> <template>
<div class="xrmjdkdw"> <div>
<MkStickyContainer>
<template #header><XHeader :actions="headerActions"/></template>
<MkSpacer :content-max="900">
<div class="xrmjdkdw">
<div> <div>
<div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap;"> <div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
<MkSelect v-model="origin" style="margin: 0; flex: 1;"> <MkSelect v-model="origin" style="margin: 0; flex: 1;">
@ -18,7 +22,7 @@
</MkInput> </MkInput>
</div> </div>
<MkPagination v-slot="{items}" :pagination="pagination" class="urempief" :class="{ grid: viewMode === 'grid' }"> <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)"> <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"/> <MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
<div v-if="viewMode === 'list'" class="body"> <div v-if="viewMode === 'list'" class="body">
<div> <div>
@ -39,12 +43,16 @@
</button> </button>
</MkPagination> </MkPagination>
</div> </div>
</div>
</MkSpacer>
</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(() => ({
title: i18n.ts.files,
icon: 'fas fa-cloud',
bg: 'var(--bg)',
actions: [{
text: i18n.ts.lookup, text: i18n.ts.lookup,
icon: 'fas fa-search', icon: 'fas fa-search',
handler: find, handler: find,
}, { }, {
text: i18n.ts.clearCachedFiles, text: i18n.ts.clearCachedFiles,
icon: 'fas fa-trash-alt', icon: 'fas fa-trash-alt',
handler: clear, handler: clear,
}], }]);
})),
}); const headerTabs = $computed(() => []);
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,5 +1,7 @@
<template> <template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> <MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init"> <FormSuspense :p="init">
<FormTextarea v-model="blockedHosts" class="_formBlock"> <FormTextarea v-model="blockedHosts" class="_formBlock">
<span>{{ i18n.ts.blockedInstances }}</span> <span>{{ i18n.ts.blockedInstances }}</span>
@ -8,18 +10,20 @@
<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]: {
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.instanceBlocking, title: i18n.ts.instanceBlocking,
icon: 'fas fa-ban', icon: 'fas fa-ban',
bg: 'var(--bg)', 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]: {
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.integration, title: i18n.ts.integration,
icon: 'fas fa-share-alt', icon: 'fas fa-share-alt',
bg: 'var(--bg)', bg: 'var(--bg)',
}
}); });
</script> </script>

View File

@ -1,5 +1,7 @@
<template> <template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> <MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init"> <FormSuspense :p="init">
<div class="_formRoot"> <div class="_formRoot">
<FormSwitch v-model="useObjectStorage" class="_formBlock">{{ i18n.ts.useObjectStorage }}</FormSwitch> <FormSwitch v-model="useObjectStorage" class="_formBlock">{{ i18n.ts.useObjectStorage }}</FormSwitch>
@ -62,11 +64,13 @@
</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 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]: {
title: i18n.ts.objectStorage,
icon: 'fas fa-cloud',
bg: 'var(--bg)',
actions: [{
asFullButton: true, asFullButton: true,
icon: 'fas fa-check', icon: 'fas fa-check',
text: i18n.ts.save, text: i18n.ts.save,
handler: save, handler: save,
}], }]);
}
const headerTabs = $computed(() => []);
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>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init"> <FormSuspense :p="init">
none none
</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 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]: {
title: i18n.ts.other,
icon: 'fas fa-cogs',
bg: 'var(--bg)',
actions: [{
asFullButton: true, asFullButton: true,
icon: 'fas fa-check', icon: 'fas fa-check',
text: i18n.ts.save, text: i18n.ts.save,
handler: save, handler: save,
}], }]);
}
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.other,
icon: 'fas fa-cogs',
bg: 'var(--bg)',
}); });
</script> </script>

View File

@ -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]: {
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.dashboard, title: i18n.ts.dashboard,
icon: 'fas fa-tachometer-alt', icon: 'fas fa-tachometer-alt',
bg: 'var(--bg)', 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]: {
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.proxyAccount, title: i18n.ts.proxyAccount,
icon: 'fas fa-ghost', icon: 'fas fa-ghost',
bg: 'var(--bg)', bg: 'var(--bg)',
}
}); });
</script> </script>

View File

@ -1,5 +1,7 @@
<template> <template>
<MkSpacer :content-max="800"> <MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="800">
<XQueue :connection="connection" domain="inbox"> <XQueue :connection="connection" domain="inbox">
<template #title>In</template> <template #title>In</template>
</XQueue> </XQueue>
@ -7,18 +9,20 @@
<template #title>Out</template> <template #title>Out</template>
</XQueue> </XQueue>
<MkButton danger @click="clear()"><i class="fas fa-trash-alt"></i> {{ i18n.ts.clearQueue }}</MkButton> <MkButton danger @click="clear()"><i class="fas fa-trash-alt"></i> {{ i18n.ts.clearQueue }}</MkButton>
</MkSpacer> </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]: {
title: i18n.ts.jobQueue,
icon: 'fas fa-clipboard-list',
bg: 'var(--bg)',
actions: [{
asFullButton: true, asFullButton: true,
icon: 'fas fa-up-right-from-square', icon: 'fas fa-up-right-from-square',
text: i18n.ts.dashboard, text: i18n.ts.dashboard,
handler: () => { handler: () => {
window.open(config.url + '/queue', '_blank'); window.open(config.url + '/queue', '_blank');
}, },
}], }]);
}
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.jobQueue,
icon: 'fas fa-clipboard-list',
bg: 'var(--bg)',
}); });
</script> </script>

View File

@ -1,5 +1,7 @@
<template> <template>
<MkSpacer :content-max="800"> <MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="800">
<div v-for="relay in relays" :key="relay.inbox" class="relaycxt _panel _block" style="padding: 16px;"> <div v-for="relay in relays" :key="relay.inbox" class="relaycxt _panel _block" style="padding: 16px;">
<div>{{ relay.inbox }}</div> <div>{{ relay.inbox }}</div>
<div class="status"> <div class="status">
@ -10,15 +12,17 @@
</div> </div>
<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton> <MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton>
</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 * 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]: {
title: i18n.ts.relays,
icon: 'fas fa-globe',
bg: 'var(--bg)',
actions: [{
asFullButton: true, asFullButton: true,
icon: 'fas fa-plus', icon: 'fas fa-plus',
text: i18n.ts.addRelay, text: i18n.ts.addRelay,
handler: addRelay, handler: addRelay,
}], }]);
}
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.relays,
icon: 'fas fa-globe',
bg: 'var(--bg)',
}); });
</script> </script>

View File

@ -1,5 +1,7 @@
<template> <template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> <MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init"> <FormSuspense :p="init">
<div class="_formRoot"> <div class="_formRoot">
<FormFolder class="_formBlock"> <FormFolder class="_formBlock">
@ -26,11 +28,14 @@
</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]: {
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.security, title: i18n.ts.security,
icon: 'fas fa-lock', icon: 'fas fa-lock',
bg: 'var(--bg)', bg: 'var(--bg)',
}
}); });
</script> </script>

View File

@ -1,5 +1,8 @@
<template> <template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> <div>
<MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init"> <FormSuspense :p="init">
<div class="_formRoot"> <div class="_formRoot">
<FormInput v-model="name" class="_formBlock"> <FormInput v-model="name" class="_formBlock">
@ -139,11 +142,14 @@
</FormSection> </FormSection>
</div> </div>
</FormSuspense> </FormSuspense>
</MkSpacer> </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]: {
title: i18n.ts.general,
icon: 'fas fa-cog',
bg: 'var(--bg)',
actions: [{
asFullButton: true, asFullButton: true,
icon: 'fas fa-check', icon: 'fas fa-check',
text: i18n.ts.save, text: i18n.ts.save,
handler: save, handler: save,
}], }]);
}
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.general,
icon: 'fas fa-cog',
bg: 'var(--bg)',
}); });
</script> </script>

View File

@ -1,5 +1,9 @@
<template> <template>
<div class="lknzcolw"> <div>
<MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="900">
<div class="lknzcolw">
<div class="users"> <div class="users">
<div class="inputs"> <div class="inputs">
<MkSelect v-model="sort" style="flex: 1;"> <MkSelect v-model="sort" style="flex: 1;">
@ -58,19 +62,23 @@
</button> </button>
</MkPagination> </MkPagination>
</div> </div>
</div>
</MkSpacer>
</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(() => ({
title: i18n.ts.users,
icon: 'fas fa-users',
bg: 'var(--bg)',
actions: [{
icon: 'fas fa-search', icon: 'fas fa-search',
text: i18n.ts.search, text: i18n.ts.search,
handler: searchUser handler: searchUser,
}, { }, {
asFullButton: true, asFullButton: true,
icon: 'fas fa-plus', icon: 'fas fa-plus',
text: i18n.ts.addUser, text: i18n.ts.addUser,
handler: addUser handler: addUser,
}, { }, {
asFullButton: true, asFullButton: true,
icon: 'fas fa-search', icon: 'fas fa-search',
text: i18n.ts.lookup, text: i18n.ts.lookup,
handler: lookupUser handler: lookupUser,
}], }]);
})),
}); const headerTabs = $computed(() => []);
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,5 +1,7 @@
<template> <template>
<MkSpacer :content-max="800"> <MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="800">
<MkPagination v-slot="{items}" :pagination="pagination" class="ruryvtyk _content"> <MkPagination v-slot="{items}" :pagination="pagination" class="ruryvtyk _content">
<section v-for="(announcement, i) in items" :key="announcement.id" class="_card announcement"> <section v-for="(announcement, i) in items" :key="announcement.id" class="_card announcement">
<div class="_title"><span v-if="$i && !announcement.isRead">🆕 </span>{{ announcement.title }}</div> <div class="_title"><span v-if="$i && !announcement.isRead">🆕 </span>{{ announcement.title }}</div>
@ -12,46 +14,40 @@
</div> </div>
</section> </section>
</MkPagination> </MkPagination>
</MkSpacer> </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: {
MkPagination,
MkButton
},
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.announcements,
icon: 'fas fa-broadcast-tower',
bg: 'var(--bg)',
},
pagination: {
endpoint: 'announcements' as const, endpoint: 'announcements' as const,
limit: 10, limit: 10,
}, };
};
},
methods: { // TODO:
// TODO: function read(items, announcement, i) {
read(items, announcement, i) {
items[i] = { items[i] = {
...announcement, ...announcement,
isRead: true, isRead: true,
}; };
os.api('i/read-announcement', { announcementId: announcement.id }); os.api('i/read-announcement', { announcementId: announcement.id });
}, }
}
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.announcements,
icon: 'fas fa-broadcast-tower',
bg: 'var(--bg)',
}); });
</script> </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,
}));
function queueUpdated(q) {
queue = q;
}
function top() {
scroll(rootEl, { top: 0 });
}
async function timetravel() {
const { canceled, result: date } = await os.inputDate({
title: i18n.ts.date,
});
if (canceled) return;
tlEl.timetravel(date);
}
function settings() {
router.push(`/my/antennas/${props.antennaId}`);
}
function focus() {
tlEl.focus();
}
watch(() => props.antennaId, async () => {
antenna = await os.api('antennas/show', {
antennaId: props.antennaId,
});
}, { immediate: true });
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata(computed(() => antenna ? {
title: antenna.name,
icon: 'fas fa-satellite', icon: 'fas fa-satellite',
bg: 'var(--bg)', bg: 'var(--bg)',
actions: [{ actions: [{
icon: 'fas fa-calendar-alt', icon: 'fas fa-calendar-alt',
text: this.$ts.jumpToSpecifiedDate, text: i18n.ts.jumpToSpecifiedDate,
handler: this.timetravel handler: timetravel,
}, { }, {
icon: 'fas fa-cog', icon: 'fas fa-cog',
text: this.$ts.settings, text: i18n.ts.settings,
handler: this.settings handler: settings,
}], }],
} : null), } : null));
};
},
computed: {
keymap(): any {
return {
't': this.focus
};
},
},
watch: {
antennaId: {
async handler() {
this.antenna = await os.api('antennas/show', {
antennaId: this.antennaId
});
},
immediate: true
}
},
methods: {
queueUpdated(q) {
this.queue = q;
},
top() {
scroll(this.$el, { top: 0 });
},
async timetravel() {
const { canceled, result: date } = await os.inputDate({
title: this.$ts.date,
});
if (canceled) return;
this.$refs.tl.timetravel(date);
},
settings() {
this.$router.push(`/my/antennas/${this.antennaId}`);
},
focus() {
(this.$refs.tl as any).focus();
}
}
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -1,5 +1,7 @@
<template> <template>
<MkSpacer :content-max="700"> <MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700">
<div class="_formRoot"> <div class="_formRoot">
<div class="_formBlock"> <div class="_formBlock">
<MkInput v-model="endpoint" :datalist="endpoints" class="_formBlock" @update:modelValue="onEndpointChange()"> <MkInput v-model="endpoint" :datalist="endpoints" class="_formBlock" @update:modelValue="onEndpointChange()">
@ -22,19 +24,20 @@
</MkTextarea> </MkTextarea>
</div> </div>
</div> </div>
</MkSpacer> </MkSpacer>
</MkStickyContainer>
</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]: {
const headerTabs = $computed(() => []);
definePageMetadata({
title: 'API console', title: 'API console',
icon: 'fas fa-terminal' 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,5 +1,7 @@
<template> <template>
<MkSpacer :content-max="700"> <MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700">
<div class="_formRoot"> <div class="_formRoot">
<MkInput v-model="name" class="_formBlock"> <MkInput v-model="name" class="_formBlock">
<template #label>{{ $ts.name }}</template> <template #label>{{ $ts.name }}</template>
@ -20,108 +22,101 @@
<MkButton primary @click="save()"><i class="fas fa-save"></i> {{ channelId ? $ts.save : $ts.create }}</MkButton> <MkButton primary @click="save()"><i class="fas fa-save"></i> {{ channelId ? $ts.save : $ts.create }}</MkButton>
</div> </div>
</div> </div>
</MkSpacer> </MkSpacer>
</MkStickyContainer>
</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 {
this.bannerUrl = (await os.api('drive/files/show', { bannerUrl = (await os.api('drive/files/show', {
fileId: this.bannerId, fileId: bannerId,
})).url; })).url;
} }
}, });
},
async created() { async function fetchChannel() {
if (this.channelId) { if (props.channelId == null) return;
this.channel = await os.api('channels/show', {
channelId: this.channelId, channel = await os.api('channels/show', {
channelId: props.channelId,
}); });
this.name = this.channel.name; name = channel.name;
this.description = this.channel.description; description = channel.description;
this.bannerId = this.channel.bannerId; bannerId = channel.bannerId;
this.bannerUrl = this.channel.bannerUrl; bannerUrl = channel.bannerUrl;
} }
},
methods: { fetchChannel();
save() {
function save() {
const params = { const params = {
name: this.name, name: name,
description: this.description, description: description,
bannerId: this.bannerId, bannerId: bannerId,
}; };
if (this.channelId) { if (props.channelId) {
params.channelId = this.channelId; params.channelId = props.channelId;
os.api('channels/update', params) os.api('channels/update', params).then(() => {
.then(channel => {
os.success(); os.success();
}); });
} else { } else {
os.api('channels/create', params) os.api('channels/create', params).then(created => {
.then(channel => {
os.success(); os.success();
this.$router.push(`/channels/${channel.id}`); router.push(`/channels/${created.id}`);
}); });
} }
}, }
setBannerImage(evt) { function setBannerImage(evt) {
selectFile(evt.currentTarget ?? evt.target, null).then(file => { selectFile(evt.currentTarget ?? evt.target, null).then(file => {
this.bannerId = file.id; bannerId = file.id;
}); });
}, }
removeBannerImage() { function removeBannerImage() {
this.bannerId = null; 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,5 +1,7 @@
<template> <template>
<MkSpacer :content-max="700"> <MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700">
<div v-if="channel"> <div v-if="channel">
<div class="wpgynlbz _panel _gap" :class="{ hide: !showBanner }"> <div class="wpgynlbz _panel _gap" :class="{ hide: !showBanner }">
<XChannelFollowButton :channel="channel" :full="true" class="subscribe"/> <XChannelFollowButton :channel="channel" :full="true" class="subscribe"/>
@ -25,74 +27,61 @@
<XTimeline :key="channelId" class="_gap" src="channel" :channel="channelId" @before="before" @after="after"/> <XTimeline :key="channelId" class="_gap" src="channel" :channel="channelId" @before="before" @after="after"/>
</div> </div>
</MkSpacer> </MkSpacer>
</MkStickyContainer>
</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,
icon: 'fas fa-satellite-dish',
bg: 'var(--bg)',
actions: [...(this.$i && this.$i.id === this.channel.userId ? [{
icon: 'fas fa-cog',
text: this.$ts.edit,
handler: this.edit,
}] : [])],
} : null),
channel: null,
showBanner: true,
pagination: {
endpoint: 'channels/timeline' as const, endpoint: 'channels/timeline' as const,
limit: 10, limit: 10,
params: computed(() => ({ params: computed(() => ({
channelId: this.channelId, channelId: props.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,5 +1,7 @@
<template> <template>
<MkSpacer :content-max="700"> <MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700">
<div v-if="tab === 'featured'" class="_content grwlizim featured"> <div v-if="tab === 'featured'" class="_content grwlizim featured">
<MkPagination v-slot="{items}" :pagination="featuredPagination"> <MkPagination v-slot="{items}" :pagination="featuredPagination">
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/> <MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/>
@ -16,67 +18,66 @@
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/> <MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/>
</MkPagination> </MkPagination>
</div> </div>
</MkSpacer> </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 {
[symbols.PAGE_INFO]: computed(() => ({
title: this.$ts.channel,
icon: 'fas fa-satellite-dish',
bg: 'var(--bg)',
actions: [{
icon: 'fas fa-plus',
text: this.$ts.create,
handler: this.create,
}],
tabs: [{
active: this.tab === 'featured',
title: this.$ts._channel.featured,
icon: 'fas fa-fire-alt',
onClick: () => { this.tab = 'featured'; },
}, {
active: this.tab === 'following',
title: this.$ts._channel.following,
icon: 'fas fa-heart',
onClick: () => { this.tab = 'following'; },
}, {
active: this.tab === 'owned',
title: this.$ts._channel.owned,
icon: 'fas fa-edit',
onClick: () => { this.tab = 'owned'; },
},]
})),
tab: 'featured',
featuredPagination: {
endpoint: 'channels/featured' as const, endpoint: 'channels/featured' as const,
noPaging: true, noPaging: true,
}, };
followingPagination: { const followingPagination = {
endpoint: 'channels/followed' as const, endpoint: 'channels/followed' as const,
limit: 5, limit: 5,
}, };
ownedPagination: { const ownedPagination = {
endpoint: 'channels/owned' as const, endpoint: 'channels/owned' as const,
limit: 5, limit: 5,
}, };
};
}, function create() {
methods: { router.push('/channels/new');
create() { }
this.$router.push(`/channels/new`);
} const headerActions = $computed(() => [{
} icon: 'fas fa-plus',
}); text: i18n.ts.create,
handler: create,
}]);
const headerTabs = $computed(() => [{
active: tab === 'featured',
title: i18n.ts._channel.featured,
icon: 'fas fa-fire-alt',
onClick: () => { tab = 'featured'; },
}, {
active: tab === 'following',
title: i18n.ts._channel.following,
icon: 'fas fa-heart',
onClick: () => { tab = 'following'; },
}, {
active: tab === 'owned',
title: i18n.ts._channel.owned,
icon: 'fas fa-edit',
onClick: () => { tab = 'owned'; },
}]);
definePageMetadata(computed(() => ({
title: i18n.ts.channel,
icon: 'fas fa-satellite-dish',
bg: 'var(--bg)',
})));
</script> </script>

View File

@ -1,5 +1,7 @@
<template> <template>
<MkSpacer :content-max="800"> <MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions"/></template>
<MkSpacer :content-max="800">
<div v-if="clip"> <div v-if="clip">
<div class="okzinsic _panel"> <div class="okzinsic _panel">
<div v-if="clip.description" class="description"> <div v-if="clip.description" class="description">
@ -12,7 +14,8 @@
<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,12 +52,7 @@ watch(() => props.clipId, async () => {
provide('currentClipPage', $$(clip)); provide('currentClipPage', $$(clip));
defineExpose({ const headerActions = $computed(() => clip && isOwned ? [{
[symbols.PAGE_INFO]: computed(() => clip ? {
title: clip.name,
icon: 'fas fa-paperclip',
bg: 'var(--bg)',
actions: isOwned ? [{
icon: 'fas fa-pencil-alt', icon: 'fas fa-pencil-alt',
text: i18n.ts.edit, text: i18n.ts.edit,
handler: async (): Promise<void> => { handler: async (): Promise<void> => {
@ -84,7 +82,7 @@ defineExpose({
...result, ...result,
}); });
}, },
}, { }, {
icon: 'fas fa-trash-alt', icon: 'fas fa-trash-alt',
text: i18n.ts.delete, text: i18n.ts.delete,
danger: true, danger: true,
@ -99,9 +97,13 @@ defineExpose({
clipId: clip.id, clipId: clip.id,
}); });
}, },
}] : [], }] : null);
} : 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(() => ({
const headerTabs = $computed(() => []);
definePageMetadata(computed(() => ({
title: folder ? folder.name : i18n.ts.drive, title: folder ? folder.name : i18n.ts.drive,
icon: 'fas fa-cloud', icon: 'fas fa-cloud',
bg: 'var(--bg)', bg: 'var(--bg)',
hideHeader: true, 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>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<div :class="$style.root">
<XCategory v-if="tab === 'category'"/> <XCategory v-if="tab === 'category'"/>
</div> </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',
handler: menu,
}]);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.customEmojis, title: i18n.ts.customEmojis,
icon: 'fas fa-laugh', icon: 'fas fa-laugh',
bg: 'var(--bg)', bg: 'var(--bg)',
actions: [{
icon: 'fas fa-ellipsis-h',
handler: menu,
}],
},
}); });
</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: { const tagUsers = $computed(() => ({
meta() {
return this.$instance;
},
tagUsers(): any {
return {
endpoint: 'hashtags/users' as const, endpoint: 'hashtags/users' as const,
limit: 30, limit: 30,
params: { params: {
tag: this.tag, tag: props.tag,
origin: 'combined', origin: 'combined',
sort: '+follower', sort: '+follower',
}
};
},
}, },
}));
watch: { const pinnedUsers = { endpoint: 'pinned-users' };
tag() { const popularUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
if (this.$refs.tags) this.$refs.tags.toggleContent(this.tag == null); 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),
};
created() { os.api('hashtags/list', {
os.api('hashtags/list', {
sort: '+attachedLocalUsers', sort: '+attachedLocalUsers',
attachedToLocalUserOnly: true, attachedToLocalUserOnly: true,
limit: 30 limit: 30,
}).then(tags => { }).then(tags => {
this.tagsLocal = tags; tagsLocal = tags;
}); });
os.api('hashtags/list', { os.api('hashtags/list', {
sort: '+attachedRemoteUsers', sort: '+attachedRemoteUsers',
attachedToRemoteUserOnly: true, attachedToRemoteUserOnly: true,
limit: 30 limit: 30,
}).then(tags => { }).then(tags => {
this.tagsRemote = tags; tagsRemote = tags;
});
os.api('stats').then(stats => {
this.stats = stats;
});
},
}); });
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,5 +1,7 @@
<template> <template>
<MkSpacer :content-max="800"> <MkStickyContainer>
<template #header><MkPageHeader/></template>
<MkSpacer :content-max="800">
<MkPagination ref="pagingComponent" :pagination="pagination"> <MkPagination ref="pagingComponent" :pagination="pagination">
<template #empty> <template #empty>
<div class="_fullinfo"> <div class="_fullinfo">
@ -14,7 +16,8 @@
</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>
<template #header><MkPageHeader/></template>
<MkSpacer :content-max="800">
<XNotes ref="notes" :pagination="pagination"/> <XNotes ref="notes" :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/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,5 +1,7 @@
<template> <template>
<MkSpacer :content-max="1000"> <MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="1000">
<div class="taeiyria"> <div class="taeiyria">
<div class="query"> <div class="query">
<MkInput v-model="host" :debounce="true" class=""> <MkInput v-model="host" :debounce="true" class="">
@ -88,7 +90,8 @@
</div> </div>
</MkPagination> </MkPagination>
</div> </div>
</MkSpacer> </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]: {
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.federation, title: i18n.ts.federation,
icon: 'fas fa-globe', icon: 'fas fa-globe',
bg: 'var(--bg)', 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(() => ({
const headerTabs = $computed(() => []);
definePageMetadata(computed(() => ({
title: i18n.ts.followRequests, title: i18n.ts.followRequests,
icon: 'fas fa-user-clock', icon: 'fas fa-user-clock',
bg: 'var(--bg)', 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() { let init = $ref(null);
return { let files = $ref([]);
[symbols.PAGE_INFO]: computed(() => this.postId ? { let description = $ref(null);
title: this.$ts.edit, let title = $ref(null);
icon: 'fas fa-pencil-alt' let isSensitive = $ref(false);
} : {
title: this.$ts.postToGallery,
icon: 'fas fa-pencil-alt'
}),
init: null,
files: [],
description: null,
title: null,
isSensitive: false,
};
},
watch: { function selectFile(evt) {
postId: { selectFiles(evt.currentTarget ?? evt.target, null).then(selected => {
handler() { files = files.concat(selected);
this.init = () => this.postId ? os.api('gallery/posts/show', {
postId: this.postId
}).then(post => {
this.files = post.files;
this.title = post.title;
this.description = post.description;
this.isSensitive = post.isSensitive;
}) : Promise.resolve(null);
},
immediate: true,
}
},
methods: {
selectFile(evt) {
selectFiles(evt.currentTarget ?? evt.target, null).then(files => {
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() { async function del() {
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: 'warning', type: 'warning',
text: this.$ts.deleteConfirm, text: i18n.ts.deleteConfirm,
}); });
if (canceled) return; if (canceled) return;
await os.apiWithDialog('gallery/posts/delete', { await os.apiWithDialog('gallery/posts/delete', {
postId: this.postId, postId: props.postId,
}); });
this.$router.push(`/gallery`); 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,5 +1,8 @@
<template> <template>
<div class="xprsixdl _root"> <MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="1400">
<div class="_root">
<MkTab v-if="$i" v-model="tab"> <MkTab v-if="$i" v-model="tab">
<option value="explore"><i class="fas fa-icons"></i> {{ $ts.gallery }}</option> <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="liked"><i class="fas fa-heart"></i> {{ $ts._gallery.liked }}</option>
@ -39,11 +42,13 @@
</div> </div>
</MkPagination> </MkPagination>
</div> </div>
</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,
MkButton,
MkTab,
MkPagination,
MkGalleryPostPreview,
},
props: { let tab = $ref('explore');
tag: { let tags = $ref([]);
type: String, let tagsRef = $ref();
required: false
}
},
data() { const recentPostsPagination = {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.gallery,
icon: 'fas fa-icons'
},
tab: 'explore',
recentPostsPagination: {
endpoint: 'gallery/posts' as const, endpoint: 'gallery/posts' as const,
limit: 6, limit: 6,
}, };
popularPostsPagination: { const popularPostsPagination = {
endpoint: 'gallery/featured' as const, endpoint: 'gallery/featured' as const,
limit: 5, limit: 5,
}, };
myPostsPagination: { const myPostsPagination = {
endpoint: 'i/gallery/posts' as const, endpoint: 'i/gallery/posts' as const,
limit: 5, limit: 5,
}, };
likedPostsPagination: { const likedPostsPagination = {
endpoint: 'i/gallery/likes' as const, endpoint: 'i/gallery/likes' as const,
limit: 5, limit: 5,
}, };
tags: [],
};
},
computed: { const tagUsersPagination = $computed(() => ({
meta() {
return this.$instance;
},
tagUsers(): any {
return {
endpoint: 'hashtags/users' as const, endpoint: 'hashtags/users' as const,
limit: 30, limit: 30,
params: { params: {
tag: this.tag, tag: this.tag,
origin: 'combined', origin: 'combined',
sort: '+follower', sort: '+follower',
}
};
},
}, },
}));
watch: { watch(() => props.tag, () => {
tag() { if (tagsRef) tagsRef.tags.toggleContent(props.tag == null);
if (this.$refs.tags) this.$refs.tags.toggleContent(this.tag == null); });
},
},
created() { const headerActions = $computed(() => []);
}, const headerTabs = $computed(() => []);
methods: { definePageMetadata({
title: i18n.ts.gallery,
} icon: 'fas fa-icons',
bg: 'var(--bg)',
}); });
</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 = {
props: {
postId: {
type: String,
required: true
}
},
data() {
return {
[symbols.PAGE_INFO]: computed(() => this.post ? {
title: this.post.title,
avatar: this.post.user,
path: `/gallery/${this.post.id}`,
share: {
title: this.post.title,
text: this.post.description,
},
actions: [{
icon: 'fas fa-pencil-alt',
text: this.$ts.edit,
handler: this.edit
}]
} : null),
otherPostsPagination: {
endpoint: 'users/gallery/posts' as const, endpoint: 'users/gallery/posts' as const,
limit: 6, limit: 6,
params: computed(() => ({ params: computed(() => ({
userId: this.post.user.id userId: post.user.id,
})), })),
}, };
post: null,
error: null,
};
},
watch: { function fetchPost() {
postId: 'fetch' post = null;
},
created() {
this.fetch();
},
methods: {
fetch() {
this.post = null;
os.api('gallery/posts/show', { os.api('gallery/posts/show', {
postId: this.postId postId: props.postId,
}).then(post => { }).then(_post => {
this.post = post; post = _post;
}).catch(err => { }).catch(_error => {
this.error = err; error = _error;
}); });
}, }
share() { function share() {
navigator.share({ navigator.share({
title: this.post.title, title: post.title,
text: this.post.description, text: post.description,
url: `${url}/gallery/${this.post.id}` url: `${url}/gallery/${post.id}`,
}); });
}, }
shareWithNote() { function shareWithNote() {
os.post({ os.post({
initialText: `${this.post.title} ${url}/gallery/${this.post.id}` initialText: `${post.title} ${url}/gallery/${post.id}`,
}); });
}, }
like() { function like() {
os.apiWithDialog('gallery/posts/like', { os.apiWithDialog('gallery/posts/like', {
postId: this.postId, postId: props.postId,
}).then(() => { }).then(() => {
this.post.isLiked = true; post.isLiked = true;
this.post.likedCount++; post.likedCount++;
}); });
}, }
async unlike() { async function unlike() {
const confirm = await os.confirm({ const confirm = await os.confirm({
type: 'warning', type: 'warning',
text: this.$ts.unlikeConfirm, text: i18n.ts.unlikeConfirm,
}); });
if (confirm.canceled) return; if (confirm.canceled) return;
os.apiWithDialog('gallery/posts/unlike', { os.apiWithDialog('gallery/posts/unlike', {
postId: this.postId, postId: props.postId,
}).then(() => { }).then(() => {
this.post.isLiked = false; post.isLiked = false;
this.post.likedCount--; post.likedCount--;
}); });
}, }
edit() { function edit() {
this.$router.push(`/gallery/${this.post.id}/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,
},
actions: [{
icon: 'fas fa-pencil-alt',
text: i18n.ts.edit,
handler: edit,
}],
} : null));
</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,8 +169,11 @@ function refreshMetadata() {
fetch(); fetch();
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: {
const headerTabs = $computed(() => []);
definePageMetadata({
title: props.host, title: props.host,
icon: 'fas fa-info-circle', icon: 'fas fa-info-circle',
bg: 'var(--bg)', bg: 'var(--bg)',
@ -178,9 +182,8 @@ defineExpose({
icon: 'fas fa-external-link-alt', icon: 'fas fa-external-link-alt',
handler: () => { handler: () => {
window.open(`https://${props.host}`, '_blank'); 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]: {
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.mentions, title: i18n.ts.mentions,
icon: 'fas fa-at', icon: 'fas fa-at',
bg: 'var(--bg)', 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]: {
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.directNotes, title: i18n.ts.directNotes,
icon: 'fas fa-envelope', icon: 'fas fa-envelope',
bg: 'var(--bg)', bg: 'var(--bg)',
},
}); });
</script> </script>

View File

@ -1,10 +1,13 @@
<template> <template>
<MkSpacer :content-max="800"> <MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="800">
<div v-size="{ max: [400] }" class="yweeujhr"> <div v-size="{ max: [400] }" class="yweeujhr">
<MkButton primary class="start" @click="start"><i class="fas fa-plus"></i> {{ $ts.startMessaging }}</MkButton> <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
v-for="(message, i) in messages"
:key="message.id" :key="message.id"
v-anim="i" v-anim="i"
class="message _block" class="message _block"
@ -35,131 +38,128 @@
</div> </div>
<MkLoading v-if="fetching"/> <MkLoading v-if="fetching"/>
</div> </div>
</MkSpacer> </MkSpacer>
</MkStickyContainer>
</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 => {
const messages = userMessages.concat(groupMessages);
messages.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
this.messages = messages;
this.fetching = false;
});
});
},
beforeUnmount() {
this.connection.dispose();
},
methods: {
getAcct: Acct.toString,
isMe(message) {
return message.userId === this.$i.id;
},
onMessage(message) {
if (message.recipientId) { if (message.recipientId) {
this.messages = this.messages.filter(m => !( messages = messages.filter(m => !(
(m.recipientId === message.recipientId && m.userId === message.userId) || (m.recipientId === message.recipientId && m.userId === message.userId) ||
(m.recipientId === message.userId && m.userId === message.recipientId))); (m.recipientId === message.userId && m.userId === message.recipientId)));
this.messages.unshift(message); messages.unshift(message);
} else if (message.groupId) { } else if (message.groupId) {
this.messages = this.messages.filter(m => m.groupId !== message.groupId); messages = messages.filter(m => m.groupId !== message.groupId);
this.messages.unshift(message); messages.unshift(message);
} }
}, }
onRead(ids) { function onRead(ids) {
for (const id of ids) { for (const id of ids) {
const found = this.messages.find(m => m.id === id); const found = messages.find(m => m.id === id);
if (found) { if (found) {
if (found.recipientId) { if (found.recipientId) {
found.isRead = true; found.isRead = true;
} else if (found.groupId) { } else if (found.groupId) {
found.reads.push(this.$i.id); found.reads.push($i.id);
} }
} }
} }
}, }
start(ev) { function start(ev) {
os.popupMenu([{ os.popupMenu([{
text: this.$ts.messagingWithUser, text: i18n.ts.messagingWithUser,
icon: 'fas fa-user', icon: 'fas fa-user',
action: () => { this.startUser(); } action: () => { startUser(); },
}, { }, {
text: this.$ts.messagingWithGroup, text: i18n.ts.messagingWithGroup,
icon: 'fas fa-users', icon: 'fas fa-users',
action: () => { this.startGroup(); } action: () => { startGroup(); },
}], ev.currentTarget ?? ev.target); }], ev.currentTarget ?? ev.target);
}, }
async startUser() { async function startUser() {
os.selectUser().then(user => { os.selectUser().then(user => {
this.$router.push(`/my/messaging/${Acct.toString(user)}`); router.push(`/my/messaging/${Acct.toString(user)}`);
}); });
}, }
async startGroup() { async function startGroup() {
const groups1 = await os.api('users/groups/owned'); const groups1 = await os.api('users/groups/owned');
const groups2 = await os.api('users/groups/joined'); const groups2 = await os.api('users/groups/joined');
if (groups1.length === 0 && groups2.length === 0) { if (groups1.length === 0 && groups2.length === 0) {
os.alert({ os.alert({
type: 'warning', type: 'warning',
title: this.$ts.youHaveNoGroups, title: i18n.ts.youHaveNoGroups,
text: this.$ts.joinOrCreateGroup, text: i18n.ts.joinOrCreateGroup,
}); });
return; return;
} }
const { canceled, result: group } = await os.select({ const { canceled, result: group } = await os.select({
title: this.$ts.group, title: i18n.ts.group,
items: groups1.concat(groups2).map(group => ({ items: groups1.concat(groups2).map(group => ({
value: group, text: group.name value: group, text: group.name,
})) })),
}); });
if (canceled) return; if (canceled) return;
this.$router.push(`/my/messaging/group/${group.id}`); router.push(`/my/messaging/group/${group.id}`);
}, }
acct 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,5 +1,7 @@
<template> <template>
<div class="mwysmxbg"> <MkStickyContainer>
<template #header><MkPageHeader/></template>
<div class="mwysmxbg">
<div class="_isolated">{{ $ts._mfm.intro }}</div> <div class="_isolated">{{ $ts._mfm.intro }}</div>
<div class="section _block"> <div class="section _block">
<div class="title">{{ $ts._mfm.mention }}</div> <div class="title">{{ $ts._mfm.mention }}</div>
@ -293,56 +295,50 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</MkStickyContainer>
</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]: {
title: this.$ts._mfm.cheatSheet,
icon: 'fas fa-question-circle', 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]: {
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.manageAntennas, title: i18n.ts.manageAntennas,
icon: 'fas fa-satellite', icon: 'fas fa-satellite',
bg: 'var(--bg)', 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]: {
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.manageAntennas, title: i18n.ts.manageAntennas,
icon: 'fas fa-satellite', 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]: {
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.manageAntennas, title: i18n.ts.manageAntennas,
icon: 'fas fa-satellite', icon: 'fas fa-satellite',
bg: 'var(--bg)' 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]: {
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.clip, title: i18n.ts.clip,
icon: 'fas fa-paperclip', icon: 'fas fa-paperclip',
bg: 'var(--bg)', bg: 'var(--bg)',
action: { action: {
icon: 'fas fa-plus', icon: 'fas fa-plus',
handler: create 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,8 +39,11 @@ async function create() {
pagingComponent.reload(); pagingComponent.reload();
} }
defineExpose({ const headerActions = $computed(() => []);
[symbols.PAGE_INFO]: {
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.manageLists, title: i18n.ts.manageLists,
icon: 'fas fa-list-ul', icon: 'fas fa-list-ul',
bg: 'var(--bg)', bg: 'var(--bg)',
@ -47,7 +51,6 @@ defineExpose({
icon: 'fas fa-plus', icon: 'fas fa-plus',
handler: create, 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'
},
created() {
this.fetch();
},
methods: {
fetch() {
os.api('users/lists/show', { os.api('users/lists/show', {
listId: this.$route.params.list listId: props.listId,
}).then(list => { }).then(_list => {
this.list = list; list = _list;
os.api('users/show', { os.api('users/show', {
userIds: this.list.userIds userIds: list.userIds,
}).then(users => { }).then(_users => {
this.users = users; users = _users;
}); });
}); });
}, }
addUser() { function addUser() {
os.selectUser().then(user => { os.selectUser().then(user => {
os.apiWithDialog('users/lists/push', { os.apiWithDialog('users/lists/push', {
listId: this.list.id, listId: list.id,
userId: user.id userId: user.id,
}).then(() => { }).then(() => {
this.users.push(user); users.push(user);
}); });
}); });
}, }
removeUser(user) { function removeUser(user) {
os.api('users/lists/pull', { os.api('users/lists/pull', {
listId: this.list.id, listId: list.id,
userId: user.id userId: user.id,
}).then(() => { }).then(() => {
this.users = this.users.filter(x => x.id !== user.id); users = users.filter(x => x.id !== user.id);
}); });
}, }
async renameList() { async function renameList() {
const { canceled, result: name } = await os.inputText({ const { canceled, result: name } = await os.inputText({
title: this.$ts.enterListName, title: i18n.ts.enterListName,
default: this.list.name default: list.name,
}); });
if (canceled) return; if (canceled) return;
await os.api('users/lists/update', { await os.api('users/lists/update', {
listId: this.list.id, listId: list.id,
name: name name: name,
}); });
this.list.name = name; list.name = name;
}, }
async deleteList() { async function deleteList() {
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: 'warning', type: 'warning',
text: this.$t('removeAreYouSure', { x: this.list.name }), text: i18n.t('removeAreYouSure', { x: list.name }),
}); });
if (canceled) return; if (canceled) return;
await os.api('users/lists/delete', { await os.api('users/lists/delete', {
listId: this.list.id listId: list.id,
}); });
os.success(); os.success();
this.$router.push('/my/lists'); mainRouter.push('/my/lists');
} }
}
}); watch(() => props.listId, fetchList, { immediate: true });
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
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]: {
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.notFound, title: i18n.ts.notFound,
icon: 'fas fa-exclamation-triangle', icon: 'fas fa-exclamation-triangle',
bg: 'var(--bg)', 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,96 +28,69 @@
</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 = {
},
data() {
return {
[symbols.PAGE_INFO]: computed(() => this.note ? {
title: this.$ts.note,
subtitle: new Date(this.note.createdAt).toLocaleString(),
avatar: this.note.user,
path: `/notes/${this.note.id}`,
share: {
title: this.$t('noteOf', { user: this.note.user.name }),
text: this.note.text,
},
bg: 'var(--bg)',
} : null),
note: null,
clips: null,
hasPrev: false,
hasNext: false,
showPrev: false,
showNext: false,
error: null,
prev: {
endpoint: 'users/notes' as const, endpoint: 'users/notes' as const,
limit: 10, limit: 10,
params: computed(() => ({ params: computed(() => note ? ({
userId: this.note.userId, userId: note.userId,
untilId: this.note.id, untilId: note.id,
})), }) : null),
}, };
next: {
const nextPagination = {
reversed: true, reversed: true,
endpoint: 'users/notes' as const, endpoint: 'users/notes' as const,
limit: 10, limit: 10,
params: computed(() => ({ params: computed(() => note ? ({
userId: this.note.userId, userId: note.userId,
sinceId: this.note.id, sinceId: note.id,
})), }) : null),
}, };
};
}, function fetchNote() {
watch: { hasPrev = false;
noteId: 'fetch' hasNext = false;
}, showPrev = false;
created() { showNext = false;
this.fetch(); note = null;
},
methods: {
fetch() {
this.hasPrev = false;
this.hasNext = false;
this.showPrev = false;
this.showNext = false;
this.note = null;
os.api('notes/show', { os.api('notes/show', {
noteId: this.noteId noteId: props.noteId,
}).then(note => { }).then(res => {
this.note = note; note = res;
Promise.all([ Promise.all([
os.api('notes/clips', { os.api('notes/clips', {
noteId: note.id, noteId: note.id,
@ -131,17 +105,35 @@ export default defineComponent({
sinceId: note.id, sinceId: note.id,
limit: 1, limit: 1,
}), }),
]).then(([clips, prev, next]) => { ]).then(([_clips, prev, next]) => {
this.clips = clips; clips = _clips;
this.hasPrev = prev.length !== 0; hasPrev = prev.length !== 0;
this.hasNext = next.length !== 0; hasNext = next.length !== 0;
}); });
}).catch(err => { }).catch(err => {
this.error = err; error = err;
}); });
} }
}
watch(() => props.noteId, fetchNote, {
immediate: true,
}); });
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata(computed(() => note ? {
title: i18n.ts.note,
subtitle: new Date(note.createdAt).toLocaleString(),
avatar: note.user,
path: `/notes/${note.id}`,
share: {
title: i18n.t('noteOf', { user: note.user.name }),
text: note.text,
},
bg: 'var(--bg)',
} : null));
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -1,18 +1,21 @@
<template> <template>
<MkSpacer :content-max="800"> <MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="800">
<div class="clupoqwt"> <div class="clupoqwt">
<XNotifications class="notifications" :include-types="includeTypes" :unread-only="tab === 'unread'"/> <XNotifications class="notifications" :include-types="includeTypes" :unread-only="tab === 'unread'"/>
</div> </div>
</MkSpacer> </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(() => ({
title: i18n.ts.notifications,
icon: 'fas fa-bell',
bg: 'var(--bg)',
actions: [{
text: i18n.ts.filter, text: i18n.ts.filter,
icon: 'fas fa-filter', icon: 'fas fa-filter',
highlighted: includeTypes != null, highlighted: includeTypes != null,
handler: setFilter, handler: setFilter,
}, { }, {
text: i18n.ts.markAllAsRead, text: i18n.ts.markAllAsRead,
icon: 'fas fa-check', icon: 'fas fa-check',
handler: () => { handler: () => {
os.apiWithDialog('notifications/mark-all-as-read'); os.apiWithDialog('notifications/mark-all-as-read');
}, },
}], }]);
tabs: [{
const headerTabs = $computed(() => [{
active: tab === 'all', active: tab === 'all',
title: i18n.ts.all, title: i18n.ts.all,
onClick: () => { tab = 'all'; }, onClick: () => { tab = 'all'; },
}, { }, {
active: tab === 'unread', active: tab === 'unread',
title: i18n.ts.unread, title: i18n.ts.unread,
onClick: () => { tab = 'unread'; }, onClick: () => { tab = 'unread'; },
},] }]);
})),
}); definePageMetadata(computed(() => ({
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,315 +102,210 @@ 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;
if (this.initPageId) {
title = this.$ts._pages.editPage;
}
else if (this.initPageName && this.initUser) {
title = this.$ts._pages.readPage;
}
return {
title: title,
icon: 'fas fa-pencil-alt',
bg: 'var(--bg)',
tabs: [{
active: this.tab === 'settings',
title: this.$ts._pages.pageSetting,
icon: 'fas fa-cog',
onClick: () => { this.tab = 'settings'; },
}, {
active: this.tab === 'contents',
title: this.$ts._pages.contents,
icon: 'fas fa-sticky-note',
onClick: () => { this.tab = 'contents'; },
}, {
active: this.tab === 'variables',
title: this.$ts._pages.variables,
icon: 'fas fa-magic',
onClick: () => { this.tab = 'variables'; },
}, {
active: this.tab === 'script',
title: this.$ts.script,
icon: 'fas fa-code',
onClick: () => { this.tab = 'script'; },
}],
};
}),
tab: 'settings',
author: this.$i,
readonly: false,
page: null,
pageId: null,
currentName: null,
title: '',
summary: null,
name: Date.now().toString(),
eyeCatchingImage: null,
eyeCatchingImageId: null,
font: 'sans-serif',
content: [],
alignCenter: false,
hideTitleWhenPinned: false,
variables: [],
hpml: null,
script: '',
url,
};
},
watch: {
async eyeCatchingImageId() {
if (this.eyeCatchingImageId == null) {
this.eyeCatchingImage = null;
} else { } else {
this.eyeCatchingImage = await os.api('drive/files/show', { eyeCatchingImage = await os.api('drive/files/show', {
fileId: this.eyeCatchingImageId, fileId: eyeCatchingImageId,
}); });
} }
}, });
},
async created() { function getSaveOptions() {
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 { return {
title: this.title.trim(), title: tatitle.trim(),
name: this.name.trim(), name: taname.trim(),
summary: this.summary, summary: tasummary,
font: this.font, font: tafont,
script: this.script, script: tascript,
hideTitleWhenPinned: this.hideTitleWhenPinned, hideTitleWhenPinned: tahideTitleWhenPinned,
alignCenter: this.alignCenter, alignCenter: taalignCenter,
content: this.content, content: tacontent,
variables: this.variables, variables: tavariables,
eyeCatchingImageId: this.eyeCatchingImageId, eyeCatchingImageId: taeyeCatchingImageId,
}; };
}, }
save() { function save() {
const options = this.getSaveOptions(); const options = tagetSaveOptions();
const onError = err => { const onError = err => {
if (err.id == '3d81ceae-475f-4600-b2a8-2bc116157532') { if (err.id == '3d81ceae-475f-4600-b2a8-2bc116157532') {
if (err.info.param == 'name') { if (err.info.param == 'name') {
os.alert({ os.alert({
type: 'error', type: 'error',
title: this.$ts._pages.invalidNameTitle, title: i18n.ts._pages.invalidNameTitle,
text: this.$ts._pages.invalidNameText text: i18n.ts._pages.invalidNameText,
}); });
} }
} else if (err.code == 'NAME_ALREADY_EXISTS') { } else if (err.code == 'NAME_ALREADY_EXISTS') {
os.alert({ os.alert({
type: 'error', type: 'error',
text: this.$ts._pages.nameAlreadyExists text: i18n.ts._pages.nameAlreadyExists,
}); });
} }
}; };
if (this.pageId) { if (tapageId) {
options.pageId = this.pageId; options.pageId = tapageId;
os.api('pages/update', options) os.api('pages/update', options)
.then(page => { .then(page => {
this.currentName = this.name.trim(); tacurrentName = taname.trim();
os.alert({ os.alert({
type: 'success', type: 'success',
text: this.$ts._pages.updated text: i18n.ts._pages.updated,
}); });
}).catch(onError); }).catch(onError);
} else { } else {
os.api('pages/create', options) os.api('pages/create', options)
.then(page => { .then(created => {
this.pageId = page.id; tapageId = created.id;
this.currentName = this.name.trim(); tacurrentName = name.trim();
os.alert({ os.alert({
type: 'success', type: 'success',
text: this.$ts._pages.created text: i18n.ts._pages.created,
}); });
this.$router.push(`/pages/edit/${this.pageId}`); mainRouter.push(`/pages/edit/${pageId}`);
}).catch(onError); }).catch(onError);
} }
}, }
del() { function del() {
os.confirm({ os.confirm({
type: 'warning', type: 'warning',
text: this.$t('removeAreYouSure', { x: this.title.trim() }), text: i18n.t('removeAreYouSure', { x: title.trim() }),
}).then(({ canceled }) => { }).then(({ canceled }) => {
if (canceled) return; if (canceled) return;
os.api('pages/delete', { os.api('pages/delete', {
pageId: this.pageId, pageId: pageId,
}).then(() => { }).then(() => {
os.alert({ os.alert({
type: 'success', type: 'success',
text: this.$ts._pages.deleted text: i18n.ts._pages.deleted,
}); });
this.$router.push(`/pages`); mainRouter.push('/pages');
}); });
}); });
}, }
duplicate() { function duplicate() {
this.title = this.title + ' - copy'; tatitle = tatitle + ' - copy';
this.name = this.name + '-copy'; taname = taname + '-copy';
os.api('pages/create', this.getSaveOptions()).then(page => { os.api('pages/create', tagetSaveOptions()).then(created => {
this.pageId = page.id; tapageId = created.id;
this.currentName = this.name.trim(); tacurrentName = taname.trim();
os.alert({ os.alert({
type: 'success', type: 'success',
text: this.$ts._pages.created text: i18n.ts._pages.created,
}); });
this.$router.push(`/pages/edit/${this.pageId}`); mainRouter.push(`/pages/edit/${pageId}`);
}); });
}, }
async add() { async function add() {
const { canceled, result: type } = await os.select({ const { canceled, result: type } = await os.select({
type: null, type: null,
title: this.$ts._pages.chooseBlock, title: i18n.ts._pages.chooseBlock,
groupedItems: this.getPageBlockList() groupedItems: tagetPageBlockList(),
}); });
if (canceled) return; if (canceled) return;
const id = uuid(); const id = uuid();
this.content.push({ id, type }); tacontent.push({ id, type });
}, }
async addVariable() { async function addVariable() {
let { canceled, result: name } = await os.inputText({ let { canceled, result: name } = await os.inputText({
title: this.$ts._pages.enterVariableName, title: i18n.ts._pages.enterVariableName,
}); });
if (canceled) return; if (canceled) return;
name = name.trim(); name = name.trim();
if (this.hpml.isUsedName(name)) { if (tahpml.isUsedName(name)) {
os.alert({ os.alert({
type: 'error', type: 'error',
text: this.$ts._pages.variableNameIsAlreadyUsed text: i18n.ts._pages.variableNameIsAlreadyUsed,
}); });
return; return;
} }
const id = uuid(); const id = uuid();
this.variables.push({ id, name, type: null }); tavariables.push({ id, name, type: null });
}, }
removeVariable(v) { function removeVariable(v) {
this.variables = this.variables.filter(x => x.name !== v.name); tavariables = tavariables.filter(x => x.name !== v.name);
}, }
getPageBlockList() { function getPageBlockList() {
return [{ return [{
label: this.$ts._pages.contentBlocks, label: i18n.ts._pages.contentBlocks,
items: [ items: [
{ value: 'section', text: this.$ts._pages.blocks.section }, { value: 'section', text: i18n.ts._pages.blocks.section },
{ value: 'text', text: this.$ts._pages.blocks.text }, { value: 'text', text: i18n.ts._pages.blocks.text },
{ value: 'image', text: this.$ts._pages.blocks.image }, { value: 'image', text: i18n.ts._pages.blocks.image },
{ value: 'textarea', text: this.$ts._pages.blocks.textarea }, { value: 'textarea', text: i18n.ts._pages.blocks.textarea },
{ value: 'note', text: this.$ts._pages.blocks.note }, { value: 'note', text: i18n.ts._pages.blocks.note },
{ value: 'canvas', text: this.$ts._pages.blocks.canvas }, { value: 'canvas', text: i18n.ts._pages.blocks.canvas },
] ],
}, { }, {
label: this.$ts._pages.inputBlocks, label: i18n.ts._pages.inputBlocks,
items: [ items: [
{ value: 'button', text: this.$ts._pages.blocks.button }, { value: 'button', text: i18n.ts._pages.blocks.button },
{ value: 'radioButton', text: this.$ts._pages.blocks.radioButton }, { value: 'radioButton', text: i18n.ts._pages.blocks.radioButton },
{ value: 'textInput', text: this.$ts._pages.blocks.textInput }, { value: 'textInput', text: i18n.ts._pages.blocks.textInput },
{ value: 'textareaInput', text: this.$ts._pages.blocks.textareaInput }, { value: 'textareaInput', text: i18n.ts._pages.blocks.textareaInput },
{ value: 'numberInput', text: this.$ts._pages.blocks.numberInput }, { value: 'numberInput', text: i18n.ts._pages.blocks.numberInput },
{ value: 'switch', text: this.$ts._pages.blocks.switch }, { value: 'switch', text: i18n.ts._pages.blocks.switch },
{ value: 'counter', text: this.$ts._pages.blocks.counter } { value: 'counter', text: i18n.ts._pages.blocks.counter },
] ],
}, { }, {
label: this.$ts._pages.specialBlocks, label: i18n.ts._pages.specialBlocks,
items: [ items: [
{ value: 'if', text: this.$ts._pages.blocks.if }, { value: 'if', text: i18n.ts._pages.blocks.if },
{ value: 'post', text: this.$ts._pages.blocks.post } { value: 'post', text: i18n.ts._pages.blocks.post },
] ],
}]; }];
}, }
getScriptBlockList(type: string = null) { function getScriptBlockList(type: string = null) {
const list = []; const list = [];
const blocks = blockDefs.filter(block => type === null || block.out === null || block.out === type || typeof block.out === 'number'); const blocks = blockDefs.filter(block => type === null || block.out === null || block.out === type || typeof block.out === 'number');
@ -419,49 +315,136 @@ export default defineComponent({
if (category) { if (category) {
category.items.push({ category.items.push({
value: block.type, value: block.type,
text: this.$t(`_pages.script.blocks.${block.type}`) text: i18n.t(`_pages.script.blocks.${block.type}`),
}); });
} else { } else {
list.push({ list.push({
category: block.category, category: block.category,
label: this.$t(`_pages.script.categories.${block.category}`), label: i18n.t(`_pages.script.categories.${block.category}`),
items: [{ items: [{
value: block.type, value: block.type,
text: this.$t(`_pages.script.blocks.${block.type}`) text: i18n.t(`_pages.script.blocks.${block.type}`),
}] }],
}); });
} }
} }
const userFns = this.variables.filter(x => x.type === 'fn'); const userFns = variables.filter(x => x.type === 'fn');
if (userFns.length > 0) { if (userFns.length > 0) {
list.unshift({ list.unshift({
label: this.$t(`_pages.script.categories.fn`), label: i18n.t('_pages.script.categories.fn'),
items: userFns.map(v => ({ items: userFns.map(v => ({
value: 'fn:' + v.name, value: 'fn:' + v.name,
text: v.name text: v.name,
})) })),
}); });
} }
return list; return list;
}, }
setEyeCatchingImage(e) { function setEyeCatchingImage(e) {
selectFile(e.currentTarget ?? e.target, null).then(file => { selectFile(e.currentTarget ?? e.target, null).then(file => {
this.eyeCatchingImageId = file.id; eyeCatchingImageId = file.id;
}); });
}, }
removeEyeCatchingImage() { function removeEyeCatchingImage() {
this.eyeCatchingImageId = null; taeyeCatchingImageId = null;
}, }
highlighter(code) { function highlighter(code) {
return highlight(code, languages.js, 'javascript'); 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,
MkPagination,
MkPagePreview,
},
props: { let page = $ref(null);
pageName: { let error = $ref(null);
type: String, const otherPostsPagination = {
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, endpoint: 'users/pages' as const,
limit: 6, limit: 6,
params: computed(() => ({ params: computed(() => ({
userId: this.page.user.id userId: page.user.id,
})), })),
}, };
}; const path = $computed(() => props.username + '/' + props.pageName);
},
computed: { function fetchPage() {
path(): string { page = null;
return this.username + '/' + this.pageName;
}
},
watch: {
path() {
this.fetch();
}
},
created() {
this.fetch();
},
methods: {
fetch() {
this.page = null;
os.api('pages/show', { os.api('pages/show', {
name: this.pageName, name: props.pageName,
username: this.username, username: props.username,
}).then(page => { }).then(_page => {
this.page = page; page = _page;
}).catch(err => { }).catch(err => {
this.error = err; error = err;
}); });
}, }
share() { function share() {
navigator.share({ navigator.share({
title: this.page.title || this.page.name, title: page.title ?? page.name,
text: this.page.summary, text: page.summary,
url: `${url}/@${this.page.user.username}/pages/${this.page.name}` url: `${url}/@${page.user.username}/pages/${page.name}`,
}); });
}, }
shareWithNote() { function shareWithNote() {
os.post({ os.post({
initialText: `${this.page.title || this.page.name} ${url}/@${this.page.user.username}/pages/${this.page.name}` initialText: `${page.title || page.name} ${url}/@${page.user.username}/pages/${page.name}`,
}); });
}, }
like() { function like() {
os.apiWithDialog('pages/like', { os.apiWithDialog('pages/like', {
pageId: this.page.id, pageId: page.id,
}).then(() => { }).then(() => {
this.page.isLiked = true; page.isLiked = true;
this.page.likedCount++; page.likedCount++;
}); });
}, }
async unlike() { async function unlike() {
const confirm = await os.confirm({ const confirm = await os.confirm({
type: 'warning', type: 'warning',
text: this.$ts.unlikeConfirm, text: i18n.ts.unlikeConfirm,
}); });
if (confirm.canceled) return; if (confirm.canceled) return;
os.apiWithDialog('pages/unlike', { os.apiWithDialog('pages/unlike', {
pageId: this.page.id, pageId: page.id,
}).then(() => { }).then(() => {
this.page.isLiked = false; page.isLiked = false;
this.page.likedCount--; page.likedCount--;
}); });
}, }
pin(pin) { function pin(pin) {
os.apiWithDialog('i/update', { os.apiWithDialog('i/update', {
pinnedPageId: pin ? this.page.id : null, 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));
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -1,5 +1,7 @@
<template> <template>
<MkSpacer :content-max="700"> <MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700">
<div v-if="tab === 'featured'" class="rknalgpo"> <div v-if="tab === 'featured'" class="rknalgpo">
<MkPagination v-slot="{items}" :pagination="featuredPagesPagination"> <MkPagination v-slot="{items}" :pagination="featuredPagesPagination">
<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"/>
@ -18,69 +20,68 @@
<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 {
[symbols.PAGE_INFO]: computed(() => ({
title: this.$ts.pages,
icon: 'fas fa-sticky-note',
bg: 'var(--bg)',
actions: [{
icon: 'fas fa-plus',
text: this.$ts.create,
handler: this.create,
}],
tabs: [{
active: this.tab === 'featured',
title: this.$ts._pages.featured,
icon: 'fas fa-fire-alt',
onClick: () => { this.tab = 'featured'; },
}, {
active: this.tab === 'my',
title: this.$ts._pages.my,
icon: 'fas fa-edit',
onClick: () => { this.tab = 'my'; },
}, {
active: this.tab === 'liked',
title: this.$ts._pages.liked,
icon: 'fas fa-heart',
onClick: () => { this.tab = 'liked'; },
},]
})),
tab: 'featured',
featuredPagesPagination: {
endpoint: 'pages/featured' as const, endpoint: 'pages/featured' as const,
noPaging: true, noPaging: true,
}, };
myPagesPagination: { const myPagesPagination = {
endpoint: 'i/pages' as const, endpoint: 'i/pages' as const,
limit: 5, limit: 5,
}, };
likedPagesPagination: { const likedPagesPagination = {
endpoint: 'i/page-likes' as const, endpoint: 'i/page-likes' as const,
limit: 5, limit: 5,
}, };
};
}, function create() {
methods: { router.push('/pages/new');
create() { }
this.$router.push(`/pages/new`);
} const headerActions = $computed(() => [{
} icon: 'fas fa-plus',
}); text: i18n.ts.create,
handler: create,
}]);
const headerTabs = $computed(() => [{
active: tab === 'featured',
title: i18n.ts._pages.featured,
icon: 'fas fa-fire-alt',
onClick: () => { tab = 'featured'; },
}, {
active: tab === 'my',
title: i18n.ts._pages.my,
icon: 'fas fa-edit',
onClick: () => { tab = 'my'; },
}, {
active: tab === 'liked',
title: i18n.ts._pages.liked,
icon: 'fas fa-heart',
onClick: () => { tab = 'liked'; },
}]);
definePageMetadata(computed(() => ({
title: i18n.ts.pages,
icon: 'fas fa-sticky-note',
bg: 'var(--bg)',
})));
</script> </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(() => ({
const headerTabs = $computed(() => []);
definePageMetadata(computed(() => ({
title: i18n.ts.preview, title: i18n.ts.preview,
icon: 'fas fa-eye', icon: 'fas fa-eye',
bg: 'var(--bg)', bg: 'var(--bg)',
})), })));
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -1,5 +1,7 @@
<template> <template>
<MkSpacer v-if="token" :content-max="700" :margin-min="16" :margin-max="32"> <MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer v-if="token" :content-max="700" :margin-min="16" :margin-max="32">
<div class="_formRoot"> <div class="_formRoot">
<FormInput v-model="password" type="password" class="_formBlock"> <FormInput v-model="password" type="password" class="_formBlock">
<template #prefix><i class="fas fa-lock"></i></template> <template #prefix><i class="fas fa-lock"></i></template>
@ -8,7 +10,8 @@
<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]: {
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.resetPassword, title: i18n.ts.resetPassword,
icon: 'fas fa-lock', icon: 'fas fa-lock',
bg: 'var(--bg)', 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]: {
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.scratchpad, title: i18n.ts.scratchpad,
icon: 'fas fa-terminal', 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(() => ({
const headerTabs = $computed(() => []);
definePageMetadata(computed(() => ({
title: i18n.t('searchWith', { q: props.query }), title: i18n.t('searchWith', { q: props.query }),
icon: 'fas fa-search', icon: 'fas fa-search',
bg: 'var(--bg)', 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]: {
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.accountInfo, title: i18n.ts.accountInfo,
icon: 'fas fa-info-circle' 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]: {
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.accounts, title: i18n.ts.accounts,
icon: 'fas fa-users', icon: 'fas fa-users',
bg: 'var(--bg)', 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]: {
const headerTabs = $computed(() => []);
definePageMetadata({
title: 'API', title: 'API',
icon: 'fas fa-key', icon: 'fas fa-key',
bg: 'var(--bg)', 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]: {
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.installedApps, title: i18n.ts.installedApps,
icon: 'fas fa-plug', icon: 'fas fa-plug',
bg: 'var(--bg)', 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]: {
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.customCss, title: i18n.ts.customCss,
icon: 'fas fa-code', icon: 'fas fa-code',
bg: 'var(--bg)', 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]: {
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.deck, title: i18n.ts.deck,
icon: 'fas fa-columns', icon: 'fas fa-columns',
bg: 'var(--bg)', 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]: {
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts._accountDelete.accountDelete, title: i18n.ts._accountDelete.accountDelete,
icon: 'fas fa-exclamation-triangle', icon: 'fas fa-exclamation-triangle',
bg: 'var(--bg)', 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]: {
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.drive, title: i18n.ts.drive,
icon: 'fas fa-cloud', icon: 'fas fa-cloud',
bg: 'var(--bg)', 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]: {
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.email, title: i18n.ts.email,
icon: 'fas fa-envelope', icon: 'fas fa-envelope',
bg: 'var(--bg)', 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]: {
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.general, title: i18n.ts.general,
icon: 'fas fa-cogs', icon: 'fas fa-cogs',
bg: 'var(--bg)' 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]: {
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.importAndExport, title: i18n.ts.importAndExport,
icon: 'fas fa-boxes', icon: 'fas fa-boxes',
bg: 'var(--bg)', bg: 'var(--bg)',
}
}); });
</script> </script>

View File

@ -1,13 +1,8 @@
<template> <template>
<MkSpacer :content-max="900" :margin-min="20" :margin-max="32"> <MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="900" :margin-min="20" :margin-max="32">
<div ref="el" class="vvcocwet" :class="{ wide: !narrow }"> <div ref="el" class="vvcocwet" :class="{ wide: !narrow }">
<div class="header">
<div class="title">
<MkA v-if="narrow" to="/settings">{{ $ts.settings }}</MkA>
<template v-else>{{ $ts.settings }}</template>
</div>
<div v-if="childInfo" class="subtitle">{{ childInfo.title }}</div>
</div>
<div class="body"> <div class="body">
<div v-if="!narrow || initialPage == null" class="nav"> <div v-if="!narrow || initialPage == null" class="nav">
<div class="baaadecd"> <div class="baaadecd">
@ -17,30 +12,31 @@
</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" :ref="el => pageChanged(el)" :key="initialPage" v-bind="pageProps"/> <component :is="component" :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