Compare commits
No commits in common. "ead8a4eb3b13e66252a348877e0850fd653ea8c9" and "7d8205930afd9d05ecf4367bc0ebea982692b213" have entirely different histories.
ead8a4eb3b
...
7d8205930a
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "misskey",
|
||||
"version": "12.119.0+birb4",
|
||||
"version": "12.119.0+birb3-2",
|
||||
"codename": "indigo",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 40 KiB |
Binary file not shown.
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 40 KiB |
Binary file not shown.
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 40 KiB |
|
@ -15,34 +15,23 @@ import { useRouter } from '@/router';
|
|||
|
||||
const props = withDefaults(defineProps<{
|
||||
to: string;
|
||||
exact?: boolean;
|
||||
activeClass?: null | string;
|
||||
behavior?: null | 'window' | 'browser' | 'modalWindow';
|
||||
}>(), {
|
||||
activeClass: null,
|
||||
behavior: null,
|
||||
exact: false,
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const currentPath = $ref(router.getCurrentPath());
|
||||
router.addListener('change', ({ path }) => {
|
||||
currentPath = path;
|
||||
});
|
||||
|
||||
const active = $computed(() => {
|
||||
if (props.activeClass == null) return false;
|
||||
if (props.exact) {
|
||||
return currentPath === props.to;
|
||||
} else {
|
||||
const resolved = router.resolve(props.to);
|
||||
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.route.name === router.currentRoute.value.name;
|
||||
}
|
||||
const resolved = router.resolve(props.to);
|
||||
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.route.name === router.currentRoute.value.name;
|
||||
});
|
||||
|
||||
function onContextmenu(ev) {
|
||||
|
|
|
@ -14,15 +14,12 @@ export default {
|
|||
const height = container.scrollHeight;
|
||||
isBottom = (pos + viewHeight > height - 32);
|
||||
}, { passive: true });
|
||||
console.log('mount', container.scrollHeight);
|
||||
|
||||
container.scroll({ top: container.scrollHeight, behavior: 'smooth' });
|
||||
container.scrollTop = container.scrollHeight;
|
||||
|
||||
const ro = new ResizeObserver((entries, observer) => {
|
||||
console.log('resize', container.scrollHeight);
|
||||
if (isBottom) {
|
||||
const height = container.scrollHeight;
|
||||
container.scroll({ top: height, behavior: 'instant' });
|
||||
container.scrollTop = height;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -11,7 +11,6 @@ import anim from './anim';
|
|||
import clickAnime from './click-anime';
|
||||
import panel from './panel';
|
||||
import adaptiveBorder from './adaptive-border';
|
||||
import follow from './follow-append';
|
||||
|
||||
export default function(app: App) {
|
||||
app.directive('userPreview', userPreview);
|
||||
|
@ -26,5 +25,4 @@ export default function(app: App) {
|
|||
app.directive('click-anime', clickAnime);
|
||||
app.directive('panel', panel);
|
||||
app.directive('adaptive-border', adaptiveBorder);
|
||||
app.directive('follow', follow);
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ export type RouteDef = {
|
|||
loginRequired?: boolean;
|
||||
name?: string;
|
||||
hash?: string;
|
||||
props?: Record<string, any>;
|
||||
globalCacheKey?: string;
|
||||
children?: RouteDef[];
|
||||
};
|
||||
|
@ -66,7 +65,7 @@ export class Router extends EventEmitter<{
|
|||
beforePath: string;
|
||||
path: string;
|
||||
route: RouteDef | null;
|
||||
props: Map<string, any> | null;
|
||||
props: Map<string, string> | null;
|
||||
key: string;
|
||||
}) => void;
|
||||
same: () => void;
|
||||
|
@ -107,7 +106,7 @@ export class Router extends EventEmitter<{
|
|||
forEachRouteLoop:
|
||||
for (const route of routes) {
|
||||
let parts = [ ..._parts ];
|
||||
const props = new Map<string, any>();
|
||||
const props = new Map<string, string>();
|
||||
|
||||
pathMatchLoop:
|
||||
for (const p of parsePath(route.path)) {
|
||||
|
@ -173,12 +172,6 @@ export class Router extends EventEmitter<{
|
|||
}
|
||||
}
|
||||
|
||||
if (route.props) {
|
||||
Object.entries(route.props).forEach(([key, value]) => {
|
||||
props.set(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
route,
|
||||
props,
|
||||
|
|
|
@ -17,34 +17,34 @@
|
|||
<div class="container">
|
||||
<div class="header">{{ $ts.timeline }}</div>
|
||||
<div class="body">
|
||||
<MkA to="/#home" class="item" active-class="active" exact><i class="fas fa-home icon"></i>{{ $ts._timelines.home }}</MkA>
|
||||
<MkA to="/#local" v-if="isLocalTimelineAvailable" class="item" active-class="active" exact><i class="fas fa-comments icon"></i>{{ $ts._timelines.local }}</MkA>
|
||||
<MkA to="/#social" v-if="isLocalTimelineAvailable" class="item" active-class="active" exact><i class="fas fa-share-alt icon"></i>{{ $ts._timelines.social }}</MkA>
|
||||
<MkA to="/#global" v-if="isGlobalTimelineAvailable" class="item" active-class="active" exact><i class="fas fa-globe icon"></i>{{ $ts._timelines.global }}</MkA>
|
||||
<MkA to="/#home" class="item" active-class="active"><i class="fas fa-home icon"></i>{{ $ts._timelines.home }}</MkA>
|
||||
<MkA to="/#local" class="item" :class="{ active: tl === 'local' }"><i class="fas fa-comments icon"></i>{{ $ts._timelines.local }}</MkA>
|
||||
<MkA to="/#social" class="item" :class="{ active: tl === 'social' }"><i class="fas fa-share-alt icon"></i>{{ $ts._timelines.social }}</MkA>
|
||||
<MkA to="/#global" class="item" :class="{ active: tl === 'global' }"><i class="fas fa-globe icon"></i>{{ $ts._timelines.global }}</MkA>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="followedChannels" class="container">
|
||||
<div class="header">{{ $ts.channel }} ({{ $ts.following }})<button class="_button add" @click="addChannel"><i class="fas fa-plus"></i></button></div>
|
||||
<div class="body">
|
||||
<MkA v-for="channel in followedChannels" :key="channel.id" :to="`/channels/${ channel.id }`" class="item" :class="{ read: !channel.hasUnreadNote }" active-class="active" exact><i class="fas fa-satellite-dish icon"></i>{{ channel.name }}</MkA>
|
||||
<MkA v-for="channel in followedChannels" :key="channel.id" :to="`/channels/${ channel.id }`" class="item" :class="{ active: tl === `channel:${ channel.id }`, read: !channel.hasUnreadNote }"><i class="fas fa-satellite-dish icon"></i>{{ channel.name }}</MkA>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="featuredChannels" class="container">
|
||||
<div class="header">{{ $ts.channel }}<button class="_button add" @click="addChannel"><i class="fas fa-plus"></i></button></div>
|
||||
<div class="body">
|
||||
<MkA v-for="channel in featuredChannels" :key="channel.id" :to="`/channels/${ channel.id }`" class="item" active-class="active" exact><i class="fas fa-satellite-dish icon"></i>{{ channel.name }}</MkA>
|
||||
<MkA v-for="channel in featuredChannels" :key="channel.id" :to="`/channels/${ channel.id }`" class="item" :class="{ active: tl === `channel:${ channel.id }` }"><i class="fas fa-satellite-dish icon"></i>{{ channel.name }}</MkA>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="lists" class="container">
|
||||
<div class="header">{{ $ts.lists }}<button class="_button add" @click="addList"><i class="fas fa-plus"></i></button></div>
|
||||
<div class="body">
|
||||
<MkA v-for="list in lists" :key="list.id" :to="`/timeline/list/${ list.id }`" class="item" active-class="active" exact><i class="fas fa-list-ul icon"></i>{{ list.name }}</MkA>
|
||||
<MkA v-for="list in lists" :key="list.id" :to="`/timeline/list/${ list.id }`" class="item" :class="{ active: tl === `list:${ list.id }` }"><i class="fas fa-list-ul icon"></i>{{ list.name }}</MkA>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="antennas" class="container">
|
||||
<div class="header">{{ $ts.antennas }}<button class="_button add" @click="addAntenna"><i class="fas fa-plus"></i></button></div>
|
||||
<div class="body">
|
||||
<MkA v-for="antenna in antennas" :key="antenna.id" :to="`/timeline/antenna/${ antenna.id }`" class="item" active-class="active" exact><i class="fas fa-satellite icon"></i>{{ antenna.name }}</MkA>
|
||||
<MkA v-for="antenna in antennas" :key="antenna.id" :to="`/timeline/antenna/${ antenna.id }`" class="item" :class="{ active: tl === `antenna:${ antenna.id }` }"><i class="fas fa-satellite icon"></i>{{ antenna.name }}</MkA>
|
||||
</div>
|
||||
</div>
|
||||
<MkAd class="a" :prefer="['square']"/>
|
||||
|
@ -68,7 +68,8 @@
|
|||
<RouterView :keepalive="{ exclude: ['timeline'] }"/>
|
||||
</main>
|
||||
|
||||
<div class="side widgets">
|
||||
<XSide ref="side" class="side" @open="sideViewOpening = true" @close="sideViewOpening = false"/>
|
||||
<div class="side widgets" :class="{ sideViewOpening }">
|
||||
<XWidgets/>
|
||||
</div>
|
||||
|
||||
|
@ -94,6 +95,7 @@ import { instanceName, url } from '@/config';
|
|||
import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
|
||||
import XWidgets from './chat/widgets.vue';
|
||||
import XCommon from './_common_/common.vue';
|
||||
import XSide from './chat/side.vue';
|
||||
import XHeaderClock from './chat/header-clock.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
|
@ -104,25 +106,20 @@ import copyToClipboard from '@/scripts/copy-to-clipboard';
|
|||
import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
|
||||
import { store } from './chat/store';
|
||||
import { openAccountMenu, $i } from '@/account';
|
||||
import { instance } from '@/instance';
|
||||
|
||||
const side = ref();
|
||||
|
||||
const routes = [
|
||||
{
|
||||
name: 'index',
|
||||
path: '/',
|
||||
hash: 'tlSrc',
|
||||
hash: 'src',
|
||||
component: $i ? page(() => import('./chat/pages/timeline.vue')) : page(() => import('../pages/welcome.vue')),
|
||||
globalCacheKey: 'index',
|
||||
}, {
|
||||
path: '/channels/:channelId',
|
||||
props: { tlSrc: 'channel' },
|
||||
component: page(() => import('./chat/pages/timeline.vue')),
|
||||
},
|
||||
...defaultRoutes,
|
||||
];
|
||||
|
||||
const isLocalTimelineAvailable = !instance.disableLocalTimeline || ($i != null && ($i.isModerator || $i.isAdmin));
|
||||
const isGlobalTimelineAvailable = !instance.disableGlobalTimeline || ($i != null && ($i.isModerator || $i.isAdmin));
|
||||
|
||||
const mainRouter = useMainRouter(routes);
|
||||
|
||||
|
@ -143,7 +140,18 @@ mainRouter.on('change', () => {
|
|||
|
||||
watch(path, () => console.log(path));
|
||||
|
||||
const sideViewRef = ref(null);
|
||||
provide('sideViewHook', (path) => {
|
||||
sideViewRef.value.navigate(path);
|
||||
});
|
||||
|
||||
const menu = computed(() => [{
|
||||
icon: 'fas fa-columns',
|
||||
text: i18n.ts.openInSideView,
|
||||
action: () => {
|
||||
sideViewRef.value.navigate(path);
|
||||
}
|
||||
}, {
|
||||
icon: 'fas fa-window-maximize',
|
||||
text: i18n.ts.openInWindow,
|
||||
action: () => {
|
||||
|
@ -231,6 +239,12 @@ function onContextmenu(e) {
|
|||
os.contextMenu([{
|
||||
type: 'label',
|
||||
text: path,
|
||||
}, {
|
||||
icon: 'fas fa-columns',
|
||||
text: i18n.ts.openInSideView,
|
||||
action: () => {
|
||||
side.navigate(path);
|
||||
}
|
||||
}, {
|
||||
icon: 'fas fa-window-maximize',
|
||||
text: i18n.ts.openInWindow,
|
||||
|
@ -457,12 +471,32 @@ onMounted(() => {
|
|||
position: relative;
|
||||
background: var(--panel);
|
||||
overflow: auto;
|
||||
|
||||
> .header {
|
||||
z-index: 1000;
|
||||
height: $header-height;
|
||||
background-color: var(--panel);
|
||||
border-bottom: solid 0.5px var(--divider);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
> .body {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
> .side {
|
||||
width: 350px;
|
||||
border-left: solid 4px var(--divider);
|
||||
background: var(--panel);
|
||||
|
||||
&.widgets.sideViewOpening {
|
||||
@media (max-width: 1400px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .menuDrawer-back {
|
||||
|
|
|
@ -30,7 +30,7 @@ export default defineComponent({
|
|||
});
|
||||
}
|
||||
|
||||
return h(TransitionGroup, {
|
||||
return h(this.reversed ? 'div' : TransitionGroup, {
|
||||
class: 'hmjzthxl',
|
||||
name: this.reversed ? 'list-reversed' : 'list',
|
||||
tag: 'div',
|
||||
|
|
|
@ -4,28 +4,20 @@
|
|||
<div v-if="date" class="info">
|
||||
<MkInfo>{{ i18n.ts.showingPastTimeline }} <button class="_textButton clear" @click="timetravel()">{{ i18n.ts.clear }}</button></MkInfo>
|
||||
</div>
|
||||
<div class="body" :class="{ reverse: props.channelId }">
|
||||
<div class="body" class="{ channel: channelId }">
|
||||
<div class="postform">
|
||||
<div v-if="typers.length > 0" class="typers">
|
||||
<I18n :src="$ts.typingUsers" text-tag="span" class="users">
|
||||
<template #users>
|
||||
<b v-for="user in typers" :key="user.id" class="user">{{ user.username }}</b>
|
||||
</template>
|
||||
</I18n>
|
||||
<MkEllipsis/>
|
||||
</div>
|
||||
<XPostForm :channel="channel"/>
|
||||
<XPostForm/>
|
||||
</div>
|
||||
<div ref="scroller" class="tl">
|
||||
<div v-if="queue > 0" class="new" :style="{ width: width + 'px', top: top + 'px' }"><button class="_buttonPrimary" @click="goTop()">{{ i18n.ts.newNoteRecived }}</button></div>
|
||||
<XNotes ref="tlElement" class="tl" :pagination="pagination" @queue="queueUpdated" v-follow="!!props.channelId"/>
|
||||
<XNotes ref="tl" class="tl" :pagination="pagination" @queue="queueUpdated"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, markRaw, onBeforeUnmount, ref, reactive, withDefaults, watchEffect, watch } from 'vue';
|
||||
import { computed, markRaw, onBeforeUnmount, ref, reactive, withDefaults, watchEffect } from 'vue';
|
||||
import XNotes from '../notes.vue';
|
||||
import * as os from '@/os';
|
||||
import { stream } from '@/stream';
|
||||
|
@ -36,113 +28,96 @@ import MkInfo from '@/components/MkInfo.vue';
|
|||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { defaultStore } from '@/store';
|
||||
import { i18n } from '@/i18n';
|
||||
import { $i } from '@/account';
|
||||
|
||||
type Props = {
|
||||
tlSrc?: string;
|
||||
tlSrc: string;
|
||||
channelId?: string;
|
||||
};
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
tlSrc: 'home',
|
||||
channelId: null,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['note']);
|
||||
|
||||
const metadata = ref({ icon: 'fas fa-home', title: i18n.ts._timelines.home });
|
||||
definePageMetadata(computed(() => metadata.value));
|
||||
|
||||
const tlElement = ref();
|
||||
|
||||
const prepend = note => {
|
||||
(tlElement.value as any).prepend(note);
|
||||
emit('note');
|
||||
sound.play(note.userId === $i.id ? 'noteMy' : 'note');
|
||||
};
|
||||
|
||||
const onChangeFollowing = () => {
|
||||
if (!tlElement.value.backed) {
|
||||
tlElement.value.reload();
|
||||
}
|
||||
};
|
||||
|
||||
let channel = ref(null);
|
||||
watch(() => props.channelId, async () => {
|
||||
if (props.channelId) {
|
||||
channel.value = await os.api('channels/show', {
|
||||
channelId: props.channelId,
|
||||
});
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
const baseQuery = {
|
||||
includeMyRenotes: defaultStore.state.showMyRenotes,
|
||||
includeRenotedMyNotes: defaultStore.state.showRenotedMyNotes,
|
||||
includeLocalRenotes: defaultStore.state.showLocalRenotes
|
||||
};
|
||||
let query = reactive({});
|
||||
let endpoint = ref(null);
|
||||
let connections = reactive([]);
|
||||
let typers = ref([]);
|
||||
let date = ref(null);
|
||||
|
||||
const query = reactive({});
|
||||
const queue = ref(0);
|
||||
const width = ref(0);
|
||||
const top = ref(0);
|
||||
const bottom = ref(0);
|
||||
const typers = ref([]);
|
||||
const date = ref(null);
|
||||
|
||||
const tl = ref();
|
||||
const scroller = ref();
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
title: i18n.ts.timeline,
|
||||
icon: 'fas fa-home',
|
||||
actions: [{
|
||||
icon: 'fas fa-calendar-alt',
|
||||
text: i18n.ts.jumpToSpecifiedDate,
|
||||
handler: timetravel
|
||||
}],
|
||||
})));
|
||||
|
||||
const prepend = note => {
|
||||
(tl.value as any).prepend(note);
|
||||
|
||||
emit('note');
|
||||
|
||||
sound.play(note.userId === this.$i.id ? 'noteMy' : 'note');
|
||||
};
|
||||
|
||||
const onChangeFollowing = () => {
|
||||
if (!tl.value.backed) {
|
||||
tl.value.reload();
|
||||
}
|
||||
};
|
||||
|
||||
const connection = ref(null);
|
||||
const connection2 = ref(null);
|
||||
const endpoint = ref(null);
|
||||
|
||||
watchEffect((onCleanup) => {
|
||||
const tlSrcMap = {
|
||||
home: {
|
||||
channel: ['homeTimeline'],
|
||||
endpoint: 'notes/timeline',
|
||||
metadata: { icon: 'fas fa-home', title: i18n.ts._timelines.home },
|
||||
},
|
||||
local: {
|
||||
channel: ['localTimeline'],
|
||||
endpoint: 'notes/local-timeline',
|
||||
metadata: { icon: 'fas fa-comments', title: i18n.ts._timelines.local },
|
||||
},
|
||||
social: {
|
||||
channel: ['hybridTimeline'],
|
||||
endpoint: 'notes/hybrid-timeline',
|
||||
metadata: { icon: 'fas fa-share-alt', title: i18n.ts._timelines.social },
|
||||
},
|
||||
global: {
|
||||
channel: ['globalTimeline'],
|
||||
endpoint: 'notes/global-timeline',
|
||||
metadata: { icon: 'fas fa-globe', title: i18n.ts._timelines.global },
|
||||
},
|
||||
channel: {
|
||||
channel: ['channel', { channelId: props.channelId }],
|
||||
endpoint: 'channels/timeline',
|
||||
metadata: { icon: 'fas fa-satellite-dish', title: channel.value?.name, subtitle: channel.value?.description },
|
||||
},
|
||||
};
|
||||
const { channel: streamChannelParams, endpoint: _endpoint, metadata: _metadata } = tlSrcMap[props.tlSrc];
|
||||
endpoint.value = _endpoint;
|
||||
metadata.value = _metadata;
|
||||
const tlStream = markRaw(stream.useChannel(...streamChannelParams));
|
||||
tlStream.on('note', prepend);
|
||||
connections.push(tlStream);
|
||||
if (props.tlSrc == 'home') {
|
||||
endpoint.value = 'notes/timeline';
|
||||
connection.value = markRaw(stream.useChannel('homeTimeline'));
|
||||
connection.value.on('note', prepend);
|
||||
|
||||
connection2.value = markRaw(stream.useChannel('main'));
|
||||
connection2.value.on('follow', onChangeFollowing);
|
||||
connection2.value.on('unfollow', onChangeFollowing);
|
||||
} else if (props.tlSrc == 'local') {
|
||||
endpoint.value = 'notes/local-timeline';
|
||||
connection.value = markRaw(stream.useChannel('localTimeline'));
|
||||
connection.value.on('note', prepend);
|
||||
} else if (props.tlSrc == 'social') {
|
||||
endpoint.value = 'notes/hybrid-timeline';
|
||||
connection.value = markRaw(stream.useChannel('hybridTimeline'));
|
||||
connection.value.on('note', prepend);
|
||||
} else if (props.tlSrc == 'global') {
|
||||
endpoint.value = 'notes/global-timeline';
|
||||
connection.value = markRaw(stream.useChannel('globalTimeline'));
|
||||
connection.value.on('note', prepend);
|
||||
} else if (props.tlSrc == 'channel') {
|
||||
|
||||
if (props.tlSrc == 'home' || props.tlSrc == 'social') {
|
||||
const mainStream = markRaw(stream.useChannel('main'));
|
||||
mainStream.on('follow', onChangeFollowing);
|
||||
mainStream.on('unfollow', onChangeFollowing);
|
||||
connections.push(mainStream);
|
||||
}
|
||||
if (props.tlSrc == 'channel') {
|
||||
tlStream.on('typers', _typers => {
|
||||
typers.value = $i ? _typers.filter(u => u.id !== $i.id) : _typers;
|
||||
});
|
||||
query.channelId = props.channelId;
|
||||
}
|
||||
|
||||
onCleanup(() => {
|
||||
connections.forEach((connection) => connection.dispose());
|
||||
connections = [];
|
||||
query = {};
|
||||
connection.value.dispose();
|
||||
if (connection2.value) connection2.value.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
const pagination = computed(() => ({
|
||||
endpoint: endpoint.value,
|
||||
reversed: props.tlSrc == 'channel',
|
||||
limit: 10,
|
||||
params: init => ({
|
||||
untilDate: date.value?.getTime(),
|
||||
|
@ -150,16 +125,6 @@ const pagination = computed(() => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
function timetravel(target?: Date) {
|
||||
date.value = target;
|
||||
tlElement.value.reload();
|
||||
};
|
||||
|
||||
const top = ref(0);
|
||||
const bottom = ref(0);
|
||||
const width = ref(0);
|
||||
const scroller = ref();
|
||||
const queue = ref(0);
|
||||
|
||||
function focus() {
|
||||
scroller.value.focus();
|
||||
|
@ -179,6 +144,11 @@ function queueUpdated(q) {
|
|||
}
|
||||
queue.value = q;
|
||||
};
|
||||
|
||||
function timetravel(target?: Date) {
|
||||
date.value = target;
|
||||
tl.value.reload();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -192,43 +162,8 @@ function queueUpdated(q) {
|
|||
padding: 16px 16px 0 16px;
|
||||
}
|
||||
|
||||
> .body {
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> .postform {
|
||||
padding: 16px 16px 0 16px;
|
||||
}
|
||||
|
||||
&.reverse {
|
||||
flex-direction: column-reverse;
|
||||
> .postform {
|
||||
padding: 0 16px 16px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
> .tl {
|
||||
position: relative;
|
||||
padding: 16px 0;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: auto;
|
||||
|
||||
> .new {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
|
||||
> button {
|
||||
display: block;
|
||||
margin: 16px auto;
|
||||
padding: 8px 16px;
|
||||
border-radius: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.postform {
|
||||
padding: 16px 16px 0 16px;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
|
@ -257,6 +192,25 @@ function queueUpdated(q) {
|
|||
}
|
||||
}
|
||||
|
||||
> .tl {
|
||||
position: relative;
|
||||
padding: 16px 0;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: auto;
|
||||
|
||||
> .new {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
|
||||
> button {
|
||||
display: block;
|
||||
margin: 16px auto;
|
||||
padding: 8px 16px;
|
||||
border-radius: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -388,7 +388,7 @@ export default defineComponent({
|
|||
return;
|
||||
}
|
||||
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkVisibilityPicker.vue')), {
|
||||
os.popup(import('@/components/MkVisibilityPicker.vue'), {
|
||||
currentVisibility: this.visibility,
|
||||
currentLocalOnly: this.localOnly,
|
||||
src: this.$refs.visibilityButton
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
<template>
|
||||
<div v-if="component" class="mrajymqm _narrow_">
|
||||
<header class="header" @contextmenu.prevent.stop="onContextmenu">
|
||||
<MkHeader class="title" :info="pageInfo" :center="false"/>
|
||||
</header>
|
||||
<component :is="component" v-bind="props" class="body"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue';
|
||||
import * as os from '@/os';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
||||
import { i18n } from '@/i18n';
|
||||
import { useRouter } from '@/router';
|
||||
import { url as baseUrl } from '@/config';
|
||||
|
||||
const router = useRouter();
|
||||
const history = reactive([]);
|
||||
const path = ref(null);
|
||||
const component = ref(null);
|
||||
const props = ref({});
|
||||
|
||||
const emit = defineEmits(['open', 'close']);
|
||||
|
||||
const url = computed(() => baseUrl + path.value)
|
||||
|
||||
const pageInfo = null;
|
||||
|
||||
function navigate(path, record = true) {
|
||||
if (record && path.value) history.push(path.value);
|
||||
path.value = path;
|
||||
const { component: newComponent, props: newProps } = router.resolve(path);
|
||||
component.value = newComponent;
|
||||
props.value = newProps;
|
||||
emit('open');
|
||||
}
|
||||
|
||||
function back() {
|
||||
navigate(history.pop(), false);
|
||||
}
|
||||
|
||||
function close() {
|
||||
path.value = null;
|
||||
component.value = null;
|
||||
props.value = {};
|
||||
emit('close');
|
||||
}
|
||||
|
||||
function onContextmenu(e) {
|
||||
os.contextMenu([{
|
||||
type: 'label',
|
||||
text: this.path,
|
||||
}, {
|
||||
icon: 'fas fa-expand-alt',
|
||||
text: i18n.ts.showInPage,
|
||||
action: () => {
|
||||
router.push(path);
|
||||
close();
|
||||
}
|
||||
}, {
|
||||
icon: 'fas fa-window-maximize',
|
||||
text: i18n.ts.openInWindow,
|
||||
action: () => {
|
||||
os.pageWindow(path);
|
||||
close();
|
||||
}
|
||||
}, null, {
|
||||
icon: 'fas fa-external-link-alt',
|
||||
text: i18n.ts.openInNewTab,
|
||||
action: () => {
|
||||
window.open(url, '_blank');
|
||||
close();
|
||||
}
|
||||
}, {
|
||||
icon: 'fas fa-link',
|
||||
text: i18n.ts.copyLink,
|
||||
action: () => {
|
||||
copyToClipboard(url);
|
||||
}
|
||||
}], e);
|
||||
}
|
||||
|
||||
defineExpose({ navHook: navigate });
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mrajymqm {
|
||||
$header-height: 55px; // TODO: どこかに集約したい
|
||||
|
||||
--root-margin: 16px;
|
||||
--margin: var(--marginHalf);
|
||||
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
box-sizing: border-box;
|
||||
|
||||
> .header {
|
||||
display: flex;
|
||||
position: sticky;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
height: $header-height;
|
||||
width: 100%;
|
||||
font-weight: bold;
|
||||
//background-color: var(--panel);
|
||||
-webkit-backdrop-filter: var(--blur, blur(32px));
|
||||
backdrop-filter: var(--blur, blur(32px));
|
||||
background-color: var(--header);
|
||||
border-bottom: solid 0.5px var(--divider);
|
||||
box-sizing: border-box;
|
||||
|
||||
> ._button {
|
||||
height: $header-height;
|
||||
width: $header-height;
|
||||
|
||||
&:hover {
|
||||
color: var(--fgHighlighted);
|
||||
}
|
||||
}
|
||||
|
||||
> .title {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
> .body {
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
Loading…
Reference in New Issue