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
149 changed files with 6312 additions and 6670 deletions

View File

@ -13,7 +13,7 @@
id-denylist violation when setting it. This is causing about 60+ lint issues.
As this is part of Chart.js's API it makes sense to disable the check here.
*/
import { defineProps, onMounted, ref, watch, PropType, onUnmounted } from 'vue';
import { onMounted, ref, watch, PropType, onUnmounted } from 'vue';
import {
Chart,
ArcElement,
@ -53,7 +53,7 @@ const props = defineProps({
limit: {
type: Number,
required: false,
default: 90
default: 90,
},
span: {
type: String as PropType<'hour' | 'day'>,
@ -62,22 +62,22 @@ const props = defineProps({
detailed: {
type: Boolean,
required: false,
default: false
default: false,
},
stacked: {
type: Boolean,
required: false,
default: false
default: false,
},
bar: {
type: Boolean,
required: false,
default: false
default: false,
},
aspectRatio: {
type: Number,
required: false,
default: null
default: null,
},
});
@ -156,7 +156,7 @@ const getDate = (ago: number) => {
const format = (arr) => {
return arr.map((v, i) => ({
x: getDate(i).getTime(),
y: v
y: v,
}));
};
@ -343,7 +343,7 @@ const render = () => {
min: 'original',
max: 'original',
},
}
},
} : undefined,
//gradient,
},
@ -367,8 +367,8 @@ const render = () => {
ctx.stroke();
ctx.restore();
}
}
}]
},
}],
});
};
@ -433,18 +433,18 @@ const fetchApRequestChart = async (): Promise<typeof chartData> => {
name: 'In',
type: 'area',
color: '#008FFB',
data: format(raw.inboxReceived)
data: format(raw.inboxReceived),
}, {
name: 'Out (succ)',
type: 'area',
color: '#00E396',
data: format(raw.deliverSucceeded)
data: format(raw.deliverSucceeded),
}, {
name: 'Out (fail)',
type: 'area',
color: '#FEB019',
data: format(raw.deliverFailed)
}]
data: format(raw.deliverFailed),
}],
};
};
@ -456,7 +456,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => {
type: 'line',
data: format(type === 'combined'
? sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec))
: sum(raw[type].inc, negate(raw[type].dec))
: sum(raw[type].inc, negate(raw[type].dec)),
),
color: '#888888',
}, {
@ -464,7 +464,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => {
type: 'area',
data: format(type === 'combined'
? sum(raw.local.diffs.renote, raw.remote.diffs.renote)
: raw[type].diffs.renote
: raw[type].diffs.renote,
),
color: colors.green,
}, {
@ -472,7 +472,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => {
type: 'area',
data: format(type === 'combined'
? sum(raw.local.diffs.reply, raw.remote.diffs.reply)
: raw[type].diffs.reply
: raw[type].diffs.reply,
),
color: colors.yellow,
}, {
@ -480,7 +480,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => {
type: 'area',
data: format(type === 'combined'
? sum(raw.local.diffs.normal, raw.remote.diffs.normal)
: raw[type].diffs.normal
: raw[type].diffs.normal,
),
color: colors.blue,
}, {
@ -488,7 +488,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => {
type: 'area',
data: format(type === 'combined'
? sum(raw.local.diffs.withFile, raw.remote.diffs.withFile)
: raw[type].diffs.withFile
: raw[type].diffs.withFile,
),
color: colors.purple,
}],
@ -522,21 +522,21 @@ const fetchUsersChart = async (total: boolean): Promise<typeof chartData> => {
type: 'line',
data: format(total
? sum(raw.local.total, raw.remote.total)
: sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec))
: sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec)),
),
}, {
name: 'Local',
type: 'area',
data: format(total
? raw.local.total
: sum(raw.local.inc, negate(raw.local.dec))
: sum(raw.local.inc, negate(raw.local.dec)),
),
}, {
name: 'Remote',
type: 'area',
data: format(total
? raw.remote.total
: sum(raw.remote.inc, negate(raw.remote.dec))
: sum(raw.remote.inc, negate(raw.remote.dec)),
),
}],
};
@ -607,8 +607,8 @@ const fetchDriveChart = async (): Promise<typeof chartData> => {
raw.local.incSize,
negate(raw.local.decSize),
raw.remote.incSize,
negate(raw.remote.decSize)
)
negate(raw.remote.decSize),
),
),
}, {
name: 'Local +',
@ -642,8 +642,8 @@ const fetchDriveFilesChart = async (): Promise<typeof chartData> => {
raw.local.incCount,
negate(raw.local.decCount),
raw.remote.incCount,
negate(raw.remote.decCount)
)
negate(raw.remote.decCount),
),
),
}, {
name: 'Local +',
@ -672,18 +672,18 @@ const fetchInstanceRequestsChart = async (): Promise<typeof chartData> => {
name: 'In',
type: 'area',
color: '#008FFB',
data: format(raw.requests.received)
data: format(raw.requests.received),
}, {
name: 'Out (succ)',
type: 'area',
color: '#00E396',
data: format(raw.requests.succeeded)
data: format(raw.requests.succeeded),
}, {
name: 'Out (fail)',
type: 'area',
color: '#FEB019',
data: format(raw.requests.failed)
}]
data: format(raw.requests.failed),
}],
};
};
@ -696,9 +696,9 @@ const fetchInstanceUsersChart = async (total: boolean): Promise<typeof chartData
color: '#008FFB',
data: format(total
? raw.users.total
: sum(raw.users.inc, negate(raw.users.dec))
)
}]
: sum(raw.users.inc, negate(raw.users.dec)),
),
}],
};
};
@ -711,9 +711,9 @@ const fetchInstanceNotesChart = async (total: boolean): Promise<typeof chartData
color: '#008FFB',
data: format(total
? raw.notes.total
: sum(raw.notes.inc, negate(raw.notes.dec))
)
}]
: sum(raw.notes.inc, negate(raw.notes.dec)),
),
}],
};
};
@ -726,17 +726,17 @@ const fetchInstanceFfChart = async (total: boolean): Promise<typeof chartData> =
color: '#008FFB',
data: format(total
? raw.following.total
: sum(raw.following.inc, negate(raw.following.dec))
)
: sum(raw.following.inc, negate(raw.following.dec)),
),
}, {
name: 'Followers',
type: 'area',
color: '#00E396',
data: format(total
? raw.followers.total
: sum(raw.followers.inc, negate(raw.followers.dec))
)
}]
: sum(raw.followers.inc, negate(raw.followers.dec)),
),
}],
};
};
@ -750,9 +750,9 @@ const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof char
color: '#008FFB',
data: format(total
? raw.drive.totalUsage
: sum(raw.drive.incUsage, negate(raw.drive.decUsage))
)
}]
: sum(raw.drive.incUsage, negate(raw.drive.decUsage)),
),
}],
};
};
@ -765,9 +765,9 @@ const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof char
color: '#008FFB',
data: format(total
? raw.drive.totalFiles
: sum(raw.drive.incFiles, negate(raw.drive.decFiles))
)
}]
: sum(raw.drive.incFiles, negate(raw.drive.decFiles)),
),
}],
};
};

View File

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

View File

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

View File

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

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

View File

@ -1,163 +1,118 @@
<template>
<MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')">
<div class="hrmcaedk _window _narrow_" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }">
<div ref="rootEl" class="hrmcaedk _window _narrow_" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }">
<div class="header" @contextmenu="onContextmenu">
<button v-if="history.length > 0" v-tooltip="$ts.goBack" class="_button" @click="back()"><i class="fas fa-arrow-left"></i></button>
<span v-else style="display: inline-block; width: 20px"></span>
<span v-if="pageInfo" class="title">
<i v-if="pageInfo.icon" class="icon" :class="pageInfo.icon"></i>
<span>{{ pageInfo.title }}</span>
<span v-if="pageMetadata?.value" class="title">
<i v-if="pageMetadata?.value.icon" class="icon" :class="pageMetadata?.value.icon"></i>
<span>{{ pageMetadata?.value.title }}</span>
</span>
<button class="_button" @click="$refs.modal.close()"><i class="fas fa-times"></i></button>
</div>
<div class="body">
<MkStickyContainer>
<template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template>
<keep-alive>
<component :is="component" v-bind="props" :ref="changePage"/>
</keep-alive>
<template #header><MkPageHeader v-if="pageMetadata?.value && !pageMetadata?.value.hideHeader" :info="pageMetadata?.value"/></template>
<RouterView :router="router"/>
</MkStickyContainer>
</div>
</div>
</MkModal>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { ComputedRef, provide } from 'vue';
import MkModal from '@/components/ui/modal.vue';
import { popout } from '@/scripts/popout';
import { popout as _popout } from '@/scripts/popout';
import copyToClipboard from '@/scripts/copy-to-clipboard';
import { resolve } from '@/router';
import { url } from '@/config';
import * as symbols from '@/symbols';
import * as os from '@/os';
import { mainRouter, routes } from '@/router';
import { i18n } from '@/i18n';
import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
import { Router } from '@/nirax';
export default defineComponent({
components: {
MkModal,
},
const props = defineProps<{
initialPath: string;
}>();
inject: {
sideViewHook: {
default: null,
},
},
defineEmits<{
(ev: 'closed'): void;
(ev: 'click'): void;
}>();
provide() {
return {
navHook: (path) => {
this.navigate(path);
},
shouldHeaderThin: true,
};
},
const router = new Router(routes, props.initialPath);
props: {
initialPath: {
type: String,
required: true,
},
initialComponent: {
type: Object,
required: true,
},
initialProps: {
type: Object,
required: false,
default: () => {},
},
},
emits: ['closed'],
data() {
return {
width: 860,
height: 660,
pageInfo: null,
path: this.initialPath,
component: this.initialComponent,
props: this.initialProps,
history: [],
};
},
computed: {
url(): string {
return url + this.path;
},
contextmenu() {
return [{
type: 'label',
text: this.path,
}, {
icon: 'fas fa-expand-alt',
text: this.$ts.showInPage,
action: this.expand,
}, this.sideViewHook ? {
icon: 'fas fa-columns',
text: this.$ts.openInSideView,
action: () => {
this.sideViewHook(this.path);
this.$refs.window.close();
},
} : undefined, {
icon: 'fas fa-external-link-alt',
text: this.$ts.popout,
action: this.popout,
}, null, {
icon: 'fas fa-external-link-alt',
text: this.$ts.openInNewTab,
action: () => {
window.open(this.url, '_blank');
this.$refs.window.close();
},
}, {
icon: 'fas fa-link',
text: this.$ts.copyLink,
action: () => {
copyToClipboard(this.url);
},
}];
},
},
methods: {
changePage(page) {
if (page == null) return;
if (page[symbols.PAGE_INFO]) {
this.pageInfo = page[symbols.PAGE_INFO];
}
},
navigate(path, record = true) {
if (record) this.history.push(this.path);
this.path = path;
const { component, props } = resolve(path);
this.component = component;
this.props = props;
},
back() {
this.navigate(this.history.pop(), false);
},
expand() {
this.$router.push(this.path);
this.$refs.window.close();
},
popout() {
popout(this.path, this.$el);
this.$refs.window.close();
},
onContextmenu(ev: MouseEvent) {
os.contextMenu(this.contextmenu, ev);
},
},
router.addListener('push', ctx => {
});
let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
let rootEl = $ref();
let modal = $ref<InstanceType<typeof MkModal>>();
let path = $ref(props.initialPath);
let width = $ref(860);
let height = $ref(660);
const history = [];
provide('router', router);
provideMetadataReceiver((info) => {
pageMetadata = info;
});
provide('shouldOmitHeaderTitle', true);
provide('shouldHeaderThin', true);
const pageUrl = $computed(() => url + path);
const contextmenu = $computed(() => {
return [{
type: 'label',
text: path,
}, {
icon: 'fas fa-expand-alt',
text: i18n.ts.showInPage,
action: expand,
}, {
icon: 'fas fa-external-link-alt',
text: i18n.ts.popout,
action: popout,
}, null, {
icon: 'fas fa-external-link-alt',
text: i18n.ts.openInNewTab,
action: () => {
window.open(pageUrl, '_blank');
modal.close();
},
}, {
icon: 'fas fa-link',
text: i18n.ts.copyLink,
action: () => {
copyToClipboard(pageUrl);
},
}];
});
function navigate(path, record = true) {
if (record) history.push(router.getCurrentPath());
router.push(path);
}
function back() {
navigate(history.pop(), false);
}
function expand() {
mainRouter.push(path);
modal.close();
}
function popout() {
_popout(path, rootEl);
modal.close();
}
function onContextmenu(ev: MouseEvent) {
os.contextMenu(contextmenu, ev);
}
</script>
<style lang="scss" scoped>

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 {
const isLink = (el: HTMLElement) => {

View File

@ -1,186 +1,135 @@
<template>
<XWindow ref="window"
<XWindow
ref="windowEl"
:initial-width="500"
:initial-height="500"
:can-resize="true"
:close-button="true"
:buttons-left="buttonsLeft"
:buttons-right="buttonsRight"
:contextmenu="contextmenu"
@closed="$emit('closed')"
>
<template #header>
<template v-if="pageInfo">
<i v-if="pageInfo.icon" class="icon" :class="pageInfo.icon" style="margin-right: 0.5em;"></i>
<span>{{ pageInfo.title }}</span>
<template v-if="pageMetadata?.value">
<i v-if="pageMetadata.value.icon" class="icon" :class="pageMetadata.value.icon" style="margin-right: 0.5em;"></i>
<span>{{ pageMetadata.value.title }}</span>
</template>
</template>
<template #headerLeft>
<button v-if="history.length > 0" v-tooltip="$ts.goBack" class="_button" @click="back()"><i class="fas fa-arrow-left"></i></button>
</template>
<template #headerRight>
<button v-tooltip="$ts.showInPage" class="_button" @click="expand()"><i class="fas fa-expand-alt"></i></button>
<button v-tooltip="$ts.popout" class="_button" @click="popout()"><i class="fas fa-external-link-alt"></i></button>
<button class="_button" @click="menu"><i class="fas fa-ellipsis-h"></i></button>
</template>
<div class="yrolvcoq" :style="{ background: pageInfo?.bg }">
<MkStickyContainer>
<template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template>
<component :is="component" v-bind="props" :ref="changePage"/>
</MkStickyContainer>
<div class="yrolvcoq" :style="{ background: pageMetadata?.value?.bg }">
<RouterView :router="router"/>
</div>
</XWindow>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { ComputedRef, inject, provide } from 'vue';
import RouterView from './global/router-view.vue';
import XWindow from '@/components/ui/window.vue';
import { popout } from '@/scripts/popout';
import { popout as _popout } from '@/scripts/popout';
import copyToClipboard from '@/scripts/copy-to-clipboard';
import { resolve } from '@/router';
import { url } from '@/config';
import * as symbols from '@/symbols';
import * as os from '@/os';
import { mainRouter, routes } from '@/router';
import { Router } from '@/nirax';
import { i18n } from '@/i18n';
import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
export default defineComponent({
components: {
XWindow,
const props = defineProps<{
initialPath: string;
}>();
defineEmits<{
(ev: 'closed'): void;
}>();
const router = new Router(routes, props.initialPath);
let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
let windowEl = $ref<InstanceType<typeof XWindow>>();
const history = $ref<string[]>([props.initialPath]);
const buttonsLeft = $computed(() => {
const buttons = [];
if (history.length > 1) {
buttons.push({
icon: 'fas fa-arrow-left',
onClick: back,
});
}
return buttons;
});
const buttonsRight = $computed(() => {
const buttons = [{
icon: 'fas fa-expand-alt',
title: i18n.ts.showInPage,
onClick: expand,
}];
return buttons;
});
router.addListener('push', ctx => {
history.push(router.getCurrentPath());
});
provide('router', router);
provideMetadataReceiver((info) => {
pageMetadata = info;
});
provide('shouldOmitHeaderTitle', true);
provide('shouldHeaderThin', true);
const contextmenu = $computed(() => ([{
icon: 'fas fa-expand-alt',
text: i18n.ts.showInPage,
action: expand,
}, {
icon: 'fas fa-external-link-alt',
text: i18n.ts.popout,
action: popout,
}, {
icon: 'fas fa-external-link-alt',
text: i18n.ts.openInNewTab,
action: () => {
window.open(url + router.getCurrentPath(), '_blank');
windowEl.close();
},
inject: {
sideViewHook: {
default: null
}
}, {
icon: 'fas fa-link',
text: i18n.ts.copyLink,
action: () => {
copyToClipboard(url + router.getCurrentPath());
},
}]));
provide() {
return {
navHook: (path) => {
this.navigate(path);
},
shouldHeaderThin: true,
};
},
function menu(ev) {
os.popupMenu(contextmenu, ev.currentTarget ?? ev.target);
}
props: {
initialPath: {
type: String,
required: true,
},
initialComponent: {
type: Object,
required: true,
},
initialProps: {
type: Object,
required: false,
default: () => {},
},
},
function back() {
history.pop();
router.change(history[history.length - 1]);
}
emits: ['closed'],
function close() {
windowEl.close();
}
data() {
return {
pageInfo: null,
path: this.initialPath,
component: this.initialComponent,
props: this.initialProps,
history: [],
};
},
function expand() {
mainRouter.push(router.getCurrentPath());
windowEl.close();
}
computed: {
url(): string {
return url + this.path;
},
function popout() {
_popout(router.getCurrentPath(), windowEl.$el);
windowEl.close();
}
contextmenu() {
return [{
type: 'label',
text: this.path,
}, {
icon: 'fas fa-expand-alt',
text: this.$ts.showInPage,
action: this.expand
}, this.sideViewHook ? {
icon: 'fas fa-columns',
text: this.$ts.openInSideView,
action: () => {
this.sideViewHook(this.path);
this.$refs.window.close();
}
} : undefined, {
icon: 'fas fa-external-link-alt',
text: this.$ts.popout,
action: this.popout
}, null, {
icon: 'fas fa-external-link-alt',
text: this.$ts.openInNewTab,
action: () => {
window.open(this.url, '_blank');
this.$refs.window.close();
}
}, {
icon: 'fas fa-link',
text: this.$ts.copyLink,
action: () => {
copyToClipboard(this.url);
}
}];
},
},
methods: {
changePage(page) {
if (page == null) return;
if (page[symbols.PAGE_INFO]) {
this.pageInfo = page[symbols.PAGE_INFO];
}
},
navigate(path, record = true) {
if (record) this.history.push(this.path);
this.path = path;
const { component, props } = resolve(path);
this.component = component;
this.props = props;
},
menu(ev) {
os.popupMenu([{
icon: 'fas fa-external-link-alt',
text: this.$ts.openInNewTab,
action: () => {
window.open(this.url, '_blank');
this.$refs.window.close();
}
}, {
icon: 'fas fa-link',
text: this.$ts.copyLink,
action: () => {
copyToClipboard(this.url);
}
}], ev.currentTarget ?? ev.target);
},
back() {
this.navigate(this.history.pop(), false);
},
close() {
this.$refs.window.close();
},
expand() {
this.$router.push(this.path);
this.$refs.window.close();
},
popout() {
popout(this.path, this.$el);
this.$refs.window.close();
},
},
defineExpose({
close,
});
</script>

View File

@ -4,14 +4,14 @@
<div class="body _window _shadow _narrow_" @mousedown="onBodyMousedown" @keydown="onKeydown">
<div class="header" :class="{ mini }" @contextmenu.prevent.stop="onContextmenu">
<span class="left">
<slot name="headerLeft"></slot>
<button v-for="button in buttonsLeft" v-tooltip="button.title" class="button _button" :class="{ highlighted: button.highlighted }" @click="button.onClick"><i :class="button.icon"></i></button>
</span>
<span class="title" @mousedown.prevent="onHeaderMousedown" @touchstart.prevent="onHeaderMousedown">
<slot name="header"></slot>
</span>
<span class="right">
<slot name="headerRight"></slot>
<button v-if="closeButton" class="_button" @click="close()"><i class="fas fa-times"></i></button>
<button v-for="button in buttonsRight" v-tooltip="button.title" class="button _button" :class="{ highlighted: button.highlighted }" @click="button.onClick"><i :class="button.icon"></i></button>
<button v-if="closeButton" class="button _button" @click="close()"><i class="fas fa-times"></i></button>
</span>
</div>
<div v-if="padding" class="body">
@ -46,41 +46,41 @@ const minHeight = 50;
const minWidth = 250;
function dragListen(fn) {
window.addEventListener('mousemove', fn);
window.addEventListener('touchmove', fn);
window.addEventListener('mousemove', fn);
window.addEventListener('touchmove', fn);
window.addEventListener('mouseleave', dragClear.bind(null, fn));
window.addEventListener('mouseup', dragClear.bind(null, fn));
window.addEventListener('touchend', dragClear.bind(null, fn));
window.addEventListener('mouseup', dragClear.bind(null, fn));
window.addEventListener('touchend', dragClear.bind(null, fn));
}
function dragClear(fn) {
window.removeEventListener('mousemove', fn);
window.removeEventListener('touchmove', fn);
window.removeEventListener('mousemove', fn);
window.removeEventListener('touchmove', fn);
window.removeEventListener('mouseleave', dragClear);
window.removeEventListener('mouseup', dragClear);
window.removeEventListener('touchend', dragClear);
window.removeEventListener('mouseup', dragClear);
window.removeEventListener('touchend', dragClear);
}
export default defineComponent({
provide: {
inWindow: true
inWindow: true,
},
props: {
padding: {
type: Boolean,
required: false,
default: false
default: false,
},
initialWidth: {
type: Number,
required: false,
default: 400
default: 400,
},
initialHeight: {
type: Number,
required: false,
default: null
default: null,
},
canResize: {
type: Boolean,
@ -105,7 +105,17 @@ export default defineComponent({
contextmenu: {
type: Array,
required: false,
}
},
buttonsLeft: {
type: Array,
required: false,
default: [],
},
buttonsRight: {
type: Array,
required: false,
default: [],
},
},
emits: ['closed'],
@ -162,7 +172,10 @@ export default defineComponent({
this.top();
},
onHeaderMousedown(evt) {
onHeaderMousedown(evt: MouseEvent) {
// 右クリックはコンテキストメニューを開こうとした可能性が高いため無視
if (evt.button === 2) return;
const main = this.$el as any;
if (!contains(main, document.activeElement)) main.focus();
@ -356,12 +369,12 @@ export default defineComponent({
const browserHeight = window.innerHeight;
const windowWidth = main.offsetWidth;
const windowHeight = main.offsetHeight;
if (position.left < 0) main.style.left = 0; // 左はみ出し
if (position.top + windowHeight > browserHeight) main.style.top = browserHeight - windowHeight + 'px'; // 下はみ出し
if (position.left + windowWidth > browserWidth) main.style.left = browserWidth - windowWidth + 'px'; // 右はみ出し
if (position.top < 0) main.style.top = 0; // 上はみ出し
}
}
if (position.left < 0) main.style.left = 0; // 左はみ出し
if (position.top + windowHeight > browserHeight) main.style.top = browserHeight - windowHeight + 'px'; // 下はみ出し
if (position.left + windowWidth > browserWidth) main.style.left = browserWidth - windowWidth + 'px'; // 右はみ出し
if (position.top < 0) main.style.top = 0; // 上はみ出し
},
},
});
</script>
@ -404,17 +417,25 @@ export default defineComponent({
border-bottom: solid 1px var(--divider);
> .left, > .right {
> ::v-deep(button) {
> .button {
height: var(--height);
width: var(--height);
&:hover {
color: var(--fgHighlighted);
}
&.highlighted {
color: var(--accent);
}
}
}
> .left {
margin-right: 16px;
}
> .right {
min-width: 16px;
}