mirror of
https://github.com/paricafe/misskey.git
synced 2024-11-25 02:46:43 -06:00
自前ルーティング (#6759)
* wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip
This commit is contained in:
parent
d4da5a1eea
commit
254cfaea28
59 changed files with 625 additions and 220 deletions
|
@ -593,6 +593,9 @@ fillAbuseReportDescription: "通報理由の詳細を記入してください。
|
|||
abuseReported: "内容が送信されました。ご報告ありがとうございました。"
|
||||
send: "送信"
|
||||
abuseMarkAsResolved: "対応済みにする"
|
||||
openInNewTab: "新しいタブで開く"
|
||||
openInSideView: "サイドビューで開く"
|
||||
defaultNavigationBehaviour: "デフォルトのナビゲーション"
|
||||
|
||||
_serverDisconnectedBehavior:
|
||||
reload: "自動でリロード"
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
<span class="eiwwqkts" :class="{ cat }" :title="acct(user)" v-if="disableLink" v-user-preview="disablePreview ? undefined : user.id" @click="onClick">
|
||||
<img class="inner" :src="url"/>
|
||||
</span>
|
||||
<router-link class="eiwwqkts" :class="{ cat }" :to="userPage(user)" :title="acct(user)" :target="target" v-else v-user-preview="disablePreview ? undefined : user.id">
|
||||
<MkA class="eiwwqkts" :class="{ cat }" :to="userPage(user)" :title="acct(user)" :target="target" v-else v-user-preview="disablePreview ? undefined : user.id">
|
||||
<img class="inner" :src="url"/>
|
||||
</router-link>
|
||||
</MkA>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<router-link :to="`/channels/${channel.id}`" class="eftoefju _panel" tabindex="-1">
|
||||
<MkA :to="`/channels/${channel.id}`" class="eftoefju _panel" tabindex="-1">
|
||||
<div class="banner" v-if="channel.bannerUrl" :style="`background-image: url('${channel.bannerUrl}')`">
|
||||
<div class="fade"></div>
|
||||
<div class="name"><Fa :icon="faSatelliteDish"/> {{ channel.name }}</div>
|
||||
|
@ -30,7 +30,7 @@
|
|||
{{ $t('updatedAt') }}: <MkTime :time="channel.lastNotedAt"/>
|
||||
</span>
|
||||
</footer>
|
||||
</router-link>
|
||||
</MkA>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { App } from 'vue';
|
||||
|
||||
import mfm from './misskey-flavored-markdown.vue';
|
||||
import a from './ui/a.vue';
|
||||
import acct from './acct.vue';
|
||||
import avatar from './avatar.vue';
|
||||
import emoji from './emoji.vue';
|
||||
|
@ -10,10 +11,10 @@ import time from './time.vue';
|
|||
import url from './url.vue';
|
||||
import loading from './loading.vue';
|
||||
import error from './error.vue';
|
||||
import streamIndicator from './stream-indicator.vue';
|
||||
|
||||
export default function(app: App) {
|
||||
app.component('Mfm', mfm);
|
||||
app.component('MkA', a);
|
||||
app.component('MkAcct', acct);
|
||||
app.component('MkAvatar', avatar);
|
||||
app.component('MkEmoji', emoji);
|
||||
|
@ -23,5 +24,4 @@ export default function(app: App) {
|
|||
app.component('MkUrl', url);
|
||||
app.component('MkLoading', loading);
|
||||
app.component('MkError', error);
|
||||
app.component('StreamIndicator', streamIndicator);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<component :is="self ? 'router-link' : 'a'" class="xlcxczvw _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target"
|
||||
<component :is="self ? 'MkA' : 'a'" class="xlcxczvw _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target"
|
||||
@mouseover="onMouseover"
|
||||
@mouseleave="onMouseleave"
|
||||
:title="url"
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<router-link class="ldlomzub" :class="{ isMe }" :to="url" v-user-preview="canonical" v-if="url.startsWith('/')">
|
||||
<MkA class="ldlomzub" :class="{ isMe }" :to="url" v-user-preview="canonical" v-if="url.startsWith('/')">
|
||||
<span class="me" v-if="isMe">{{ $t('you') }}</span>
|
||||
<span class="main">
|
||||
<span class="username">@{{ username }}</span>
|
||||
<span class="host" v-if="(host != localHost) || $store.state.settings.showFullAcct">@{{ toUnicode(host) }}</span>
|
||||
</span>
|
||||
</router-link>
|
||||
</MkA>
|
||||
<a class="ldlomzub" :href="url" target="_blank" rel="noopener" v-else>
|
||||
<span class="main">
|
||||
<span class="username">@{{ username }}</span>
|
||||
|
|
|
@ -9,8 +9,8 @@ import { concat } from '../../prelude/array';
|
|||
import MkFormula from './formula.vue';
|
||||
import MkCode from './code.vue';
|
||||
import MkGoogle from './google.vue';
|
||||
import MkA from './ui/a.vue';
|
||||
import { host } from '@/config';
|
||||
import { RouterLink } from 'vue-router';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
|
@ -150,7 +150,7 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
case 'hashtag': {
|
||||
return [h(RouterLink, {
|
||||
return [h(MkA, {
|
||||
key: Math.random(),
|
||||
to: this.isNote ? `/tags/${encodeURIComponent(token.node.props.hashtag)}` : `/explore/tags/${encodeURIComponent(token.node.props.hashtag)}`,
|
||||
style: 'color:var(--hashtag);'
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
<template>
|
||||
<header class="kkwtjztg">
|
||||
<router-link class="name" :to="userPage(note.user)" v-user-preview="note.user.id">
|
||||
<MkA class="name" :to="userPage(note.user)" v-user-preview="note.user.id">
|
||||
<MkUserName :user="note.user"/>
|
||||
</router-link>
|
||||
</MkA>
|
||||
<span class="is-bot" v-if="note.user.isBot">bot</span>
|
||||
<span class="username"><MkAcct :user="note.user"/></span>
|
||||
<span class="admin" v-if="note.user.isAdmin"><Fa :icon="faBookmark"/></span>
|
||||
<span class="moderator" v-if="!note.user.isAdmin && note.user.isModerator"><Fa :icon="farBookmark"/></span>
|
||||
<div class="info">
|
||||
<span class="mobile" v-if="note.viaMobile"><Fa :icon="faMobileAlt"/></span>
|
||||
<router-link class="created-at" :to="notePage(note)">
|
||||
<MkA class="created-at" :to="notePage(note)">
|
||||
<MkTime :time="note.createdAt"/>
|
||||
</router-link>
|
||||
</MkA>
|
||||
<span class="visibility" v-if="note.visibility !== 'public'">
|
||||
<Fa v-if="note.visibility === 'home'" :icon="faHome"/>
|
||||
<Fa v-if="note.visibility === 'followers'" :icon="faUnlock"/>
|
||||
|
|
|
@ -18,9 +18,9 @@
|
|||
<Fa :icon="faRetweet"/>
|
||||
<i18n-t keypath="renotedBy" tag="span">
|
||||
<template #user>
|
||||
<router-link class="name" :to="userPage(note.user)" v-user-preview="note.userId">
|
||||
<MkA class="name" :to="userPage(note.user)" v-user-preview="note.userId">
|
||||
<MkUserName :user="note.user"/>
|
||||
</router-link>
|
||||
</MkA>
|
||||
</template>
|
||||
</i18n-t>
|
||||
<div class="info">
|
||||
|
@ -48,7 +48,7 @@
|
|||
<div class="content" v-show="appearNote.cw == null || showContent">
|
||||
<div class="text">
|
||||
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $t('private') }})</span>
|
||||
<router-link class="reply" v-if="appearNote.replyId" :to="`/notes/${appearNote.replyId}`"><Fa :icon="faReply"/></router-link>
|
||||
<MkA class="reply" v-if="appearNote.replyId" :to="`/notes/${appearNote.replyId}`"><Fa :icon="faReply"/></MkA>
|
||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/>
|
||||
<a class="rp" v-if="appearNote.renote != null">RN:</a>
|
||||
</div>
|
||||
|
@ -59,7 +59,7 @@
|
|||
<MkUrlPreview v-for="url in urls" :url="url" :key="url" :compact="true" :detail="detail" class="url-preview"/>
|
||||
<div class="renote" v-if="appearNote.renote"><XNotePreview :note="appearNote.renote"/></div>
|
||||
</div>
|
||||
<router-link v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><Fa :icon="faSatelliteDish"/> {{ appearNote.channel.name }}</router-link>
|
||||
<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><Fa :icon="faSatelliteDish"/> {{ appearNote.channel.name }}</MkA>
|
||||
</div>
|
||||
<footer class="footer">
|
||||
<XReactionsViewer :note="appearNote" ref="reactionsViewer"/>
|
||||
|
@ -91,9 +91,9 @@
|
|||
<div v-else class="_panel muted" @click="muted = false">
|
||||
<i18n-t keypath="userSaysSomething" tag="small">
|
||||
<template #name>
|
||||
<router-link class="name" :to="userPage(appearNote.user)" v-user-preview="appearNote.userId">
|
||||
<MkA class="name" :to="userPage(appearNote.user)" v-user-preview="appearNote.userId">
|
||||
<MkUserName :user="appearNote.user"/>
|
||||
</router-link>
|
||||
</MkA>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
|
@ -144,7 +144,7 @@ export default defineComponent({
|
|||
inject: {
|
||||
inChannel: {
|
||||
default: null
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
props: {
|
||||
|
@ -581,11 +581,6 @@ export default defineComponent({
|
|||
});
|
||||
|
||||
menu = [{
|
||||
type: 'link',
|
||||
icon: faInfoCircle,
|
||||
text: this.$t('details'),
|
||||
to: '/notes/' + this.appearNote.id
|
||||
}, null, {
|
||||
icon: faCopy,
|
||||
text: this.$t('copyContent'),
|
||||
action: this.copyContent
|
||||
|
|
|
@ -18,34 +18,34 @@
|
|||
</div>
|
||||
<div class="tail">
|
||||
<header>
|
||||
<router-link v-if="notification.user" class="name" :to="userPage(notification.user)" v-user-preview="notification.user.id"><MkUserName :user="notification.user"/></router-link>
|
||||
<MkA v-if="notification.user" class="name" :to="userPage(notification.user)" v-user-preview="notification.user.id"><MkUserName :user="notification.user"/></MkA>
|
||||
<span v-else>{{ notification.header }}</span>
|
||||
<MkTime :time="notification.createdAt" v-if="withTime"/>
|
||||
</header>
|
||||
<router-link v-if="notification.type === 'reaction'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
<MkA v-if="notification.type === 'reaction'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
<Fa :icon="faQuoteLeft"/>
|
||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
|
||||
<Fa :icon="faQuoteRight"/>
|
||||
</router-link>
|
||||
<router-link v-if="notification.type === 'renote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)">
|
||||
</MkA>
|
||||
<MkA v-if="notification.type === 'renote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)">
|
||||
<Fa :icon="faQuoteLeft"/>
|
||||
<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.renote.emojis"/>
|
||||
<Fa :icon="faQuoteRight"/>
|
||||
</router-link>
|
||||
<router-link v-if="notification.type === 'reply'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
</MkA>
|
||||
<MkA v-if="notification.type === 'reply'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
|
||||
</router-link>
|
||||
<router-link v-if="notification.type === 'mention'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
</MkA>
|
||||
<MkA v-if="notification.type === 'mention'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
|
||||
</router-link>
|
||||
<router-link v-if="notification.type === 'quote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
</MkA>
|
||||
<MkA v-if="notification.type === 'quote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
|
||||
</router-link>
|
||||
<router-link v-if="notification.type === 'pollVote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
</MkA>
|
||||
<MkA v-if="notification.type === 'pollVote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
<Fa :icon="faQuoteLeft"/>
|
||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
|
||||
<Fa :icon="faQuoteRight"/>
|
||||
</router-link>
|
||||
</MkA>
|
||||
<span v-if="notification.type === 'follow'" class="text" style="opacity: 0.6;">{{ $t('youGotNewFollower') }}<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div></span>
|
||||
<span v-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ $t('followRequestAccepted') }}</span>
|
||||
<span v-if="notification.type === 'receiveFollowRequest'" class="text" style="opacity: 0.6;">{{ $t('receiveFollowRequest') }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ $t('accept') }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ $t('reject') }}</button></div></span>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<router-link :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj" tabindex="-1">
|
||||
<MkA :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj" tabindex="-1">
|
||||
<div class="thumbnail" v-if="page.eyeCatchingImage" :style="`background-image: url('${page.eyeCatchingImage.thumbnailUrl}')`"></div>
|
||||
<article>
|
||||
<header>
|
||||
|
@ -11,7 +11,7 @@
|
|||
<p>{{ userName(page.user) }}</p>
|
||||
</footer>
|
||||
</article>
|
||||
</router-link>
|
||||
</MkA>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
<template>
|
||||
<XWindow ref="window" :initial-width="400" :initial-height="500" :can-resize="true" @closed="$emit('closed')">
|
||||
<XWindow ref="window"
|
||||
:initial-width="400"
|
||||
:initial-height="500"
|
||||
:can-resize="true"
|
||||
:close-right="true"
|
||||
:contextmenu="contextmenu"
|
||||
@closed="$emit('closed')"
|
||||
>
|
||||
<template #header>
|
||||
<XHeader :info="pageInfo" :with-back="false"/>
|
||||
</template>
|
||||
<template #buttons>
|
||||
<button class="_button" @click="expand" v-tooltip="$t('showInPage')"><Fa :icon="faExpandAlt"/></button>
|
||||
<button class="_button" @click="popout" v-tooltip="$t('popout')"><Fa :icon="faExternalLinkAlt"/></button>
|
||||
<button class="_button" @click="back()" v-if="history.length > 0"><Fa :icon="faChevronLeft"/></button>
|
||||
<button class="_button" style="pointer-events: none;" v-else><!-- マージンのバランスを取るためのダミー --></button>
|
||||
</template>
|
||||
<div class="yrolvcoq" style="min-height: 100%; background: var(--bg);">
|
||||
<component :is="component" v-bind="props" :ref="changePage"/>
|
||||
|
@ -14,11 +21,13 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, markRaw } from 'vue';
|
||||
import { faExternalLinkAlt, faExpandAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { defineComponent } from 'vue';
|
||||
import { faExternalLinkAlt, faExpandAlt, faLink, faChevronLeft } from '@fortawesome/free-solid-svg-icons';
|
||||
import XWindow from '@/components/ui/window.vue';
|
||||
import XHeader from '@/ui/_common_/header.vue';
|
||||
import { popout } from '@/scripts/popout';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
||||
import { resolve } from '@/router';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
|
@ -26,6 +35,14 @@ export default defineComponent({
|
|||
XHeader,
|
||||
},
|
||||
|
||||
provide() {
|
||||
return {
|
||||
navHook: (url) => {
|
||||
this.navigate(url);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
props: {
|
||||
initialUrl: {
|
||||
type: String,
|
||||
|
@ -38,7 +55,7 @@ export default defineComponent({
|
|||
initialProps: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: {},
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -50,18 +67,39 @@ export default defineComponent({
|
|||
url: this.initialUrl,
|
||||
component: this.initialComponent,
|
||||
props: this.initialProps,
|
||||
faExternalLinkAlt, faExpandAlt,
|
||||
history: [],
|
||||
faChevronLeft,
|
||||
};
|
||||
},
|
||||
|
||||
provide() {
|
||||
return {
|
||||
navHook: (url, component, props) => {
|
||||
this.url = url;
|
||||
this.component = markRaw(component);
|
||||
this.props = props;
|
||||
}
|
||||
};
|
||||
computed: {
|
||||
contextmenu() {
|
||||
return [{
|
||||
type: 'label',
|
||||
text: this.url,
|
||||
}, {
|
||||
icon: faExpandAlt,
|
||||
text: this.$t('showInPage'),
|
||||
action: this.expand
|
||||
}, {
|
||||
icon: faExternalLinkAlt,
|
||||
text: this.$t('popout'),
|
||||
action: this.popout
|
||||
}, null, {
|
||||
icon: faExternalLinkAlt,
|
||||
text: this.$t('openInNewTab'),
|
||||
action: () => {
|
||||
window.open(this.url, '_blank');
|
||||
this.$refs.window.close();
|
||||
}
|
||||
}, {
|
||||
icon: faLink,
|
||||
text: this.$t('copyLink'),
|
||||
action: () => {
|
||||
copyToClipboard(this.url);
|
||||
}
|
||||
}];
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -72,6 +110,18 @@ export default defineComponent({
|
|||
}
|
||||
},
|
||||
|
||||
navigate(url, record = true) {
|
||||
if (record) this.history.push(this.url);
|
||||
this.url = url;
|
||||
const { component, props } = resolve(url);
|
||||
this.component = component;
|
||||
this.props = props;
|
||||
},
|
||||
|
||||
back() {
|
||||
this.navigate(this.history.pop(), false);
|
||||
},
|
||||
|
||||
expand() {
|
||||
this.$router.push(this.url);
|
||||
this.$refs.window.close();
|
||||
|
|
|
@ -17,12 +17,12 @@
|
|||
<button class="item _button index active" @click="top()" v-if="$route.name === 'index'">
|
||||
<Fa :icon="faHome" fixed-width/><span class="text">{{ $store.getters.isSignedIn ? $t('timeline') : $t('home') }}</span>
|
||||
</button>
|
||||
<router-link class="item index" active-class="active" to="/" exact v-else>
|
||||
<MkA class="item index" active-class="active" to="/" exact v-else>
|
||||
<Fa :icon="faHome" fixed-width/><span class="text">{{ $store.getters.isSignedIn ? $t('timeline') : $t('home') }}</span>
|
||||
</router-link>
|
||||
</MkA>
|
||||
<template v-for="item in menu">
|
||||
<div v-if="item === '-'" class="divider"></div>
|
||||
<component v-else-if="menuDef[item] && (menuDef[item].show !== false)" :is="menuDef[item].to ? 'router-link' : 'button'" class="item _button" :class="item" active-class="active" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}" :to="menuDef[item].to">
|
||||
<component v-else-if="menuDef[item] && (menuDef[item].show !== false)" :is="menuDef[item].to ? 'MkA' : 'button'" class="item _button" :class="item" active-class="active" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}" :to="menuDef[item].to">
|
||||
<Fa :icon="menuDef[item].icon" fixed-width/><span class="text">{{ $t(menuDef[item].title) }}</span>
|
||||
<i v-if="menuDef[item].indicated"><Fa :icon="faCircle"/></i>
|
||||
</component>
|
||||
|
@ -35,9 +35,9 @@
|
|||
<Fa :icon="faEllipsisH" fixed-width/><span class="text">{{ $t('more') }}</span>
|
||||
<i v-if="otherNavItemIndicated"><Fa :icon="faCircle"/></i>
|
||||
</button>
|
||||
<router-link class="item" active-class="active" to="/settings">
|
||||
<MkA class="item" active-class="active" to="/settings">
|
||||
<Fa :icon="faCog" fixed-width/><span class="text">{{ $t('settings') }}</span>
|
||||
</router-link>
|
||||
</MkA>
|
||||
</div>
|
||||
</nav>
|
||||
</transition>
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
<div class="body">
|
||||
<span v-if="note.isHidden" style="opacity: 0.5">({{ $t('private') }})</span>
|
||||
<span v-if="note.deletedAt" style="opacity: 0.5">({{ $t('deleted') }})</span>
|
||||
<router-link class="reply" v-if="note.replyId" :to="`/notes/${note.replyId}`"><Fa :icon="faReply"/></router-link>
|
||||
<MkA class="reply" v-if="note.replyId" :to="`/notes/${note.replyId}`"><Fa :icon="faReply"/></MkA>
|
||||
<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis"/>
|
||||
<router-link class="rp" v-if="note.renoteId" :to="`/notes/${note.renoteId}`">RN: ...</router-link>
|
||||
<MkA class="rp" v-if="note.renoteId" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
|
||||
</div>
|
||||
<details v-if="note.files.length > 0">
|
||||
<summary>({{ $t('withNFiles', { n: note.files.length }) }})</summary>
|
||||
|
|
104
src/client/components/ui/a.vue
Normal file
104
src/client/components/ui/a.vue
Normal file
|
@ -0,0 +1,104 @@
|
|||
<template>
|
||||
<a :href="to" :class="active ? activeClass : null" @click.prevent="nav" @contextmenu.prevent.stop="onContextmenu">
|
||||
<slot></slot>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { faExpandAlt, faColumns, faExternalLinkAlt, faLink, faWindowMaximize } from '@fortawesome/free-solid-svg-icons';
|
||||
import * as os from '@/os';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
||||
import { router } from '@/router';
|
||||
import { deckmode } from '@/config';
|
||||
|
||||
export default defineComponent({
|
||||
inject: {
|
||||
navHook: {
|
||||
default: null
|
||||
},
|
||||
sideViewHook: {
|
||||
default: null
|
||||
}
|
||||
},
|
||||
|
||||
props: {
|
||||
to: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
activeClass: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
active() {
|
||||
if (this.activeClass == null) return false;
|
||||
const resolved = router.resolve(this.to);
|
||||
if (resolved.path == this.$route.path) return true;
|
||||
if (resolved.name == null) return false;
|
||||
if (this.$route.name == null) return false;
|
||||
return resolved.name == this.$route.name;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onContextmenu(e) {
|
||||
if (window.getSelection().toString() !== '') return;
|
||||
os.contextMenu([{
|
||||
type: 'label',
|
||||
text: this.to,
|
||||
}, {
|
||||
icon: faWindowMaximize,
|
||||
text: this.$t('openInWindow'),
|
||||
action: () => {
|
||||
os.pageWindow(this.to);
|
||||
}
|
||||
}, !this.navHook && this.sideViewHook ? {
|
||||
icon: faColumns,
|
||||
text: this.$t('openInSideView'),
|
||||
action: () => {
|
||||
this.sideViewHook(this.to);
|
||||
}
|
||||
} : undefined, {
|
||||
icon: faExpandAlt,
|
||||
text: this.$t('showInPage'),
|
||||
action: () => {
|
||||
this.$router.push(this.to);
|
||||
}
|
||||
}, null, {
|
||||
icon: faExternalLinkAlt,
|
||||
text: this.$t('openInNewTab'),
|
||||
action: () => {
|
||||
window.open(this.to, '_blank');
|
||||
}
|
||||
}, {
|
||||
icon: faLink,
|
||||
text: this.$t('copyLink'),
|
||||
action: () => {
|
||||
copyToClipboard(this.to);
|
||||
}
|
||||
}], e);
|
||||
},
|
||||
|
||||
nav() {
|
||||
if (this.navHook) {
|
||||
this.navHook(this.to);
|
||||
} else {
|
||||
if (this.$store.state.device.defaultSideView && this.sideViewHook && this.to !== '/') {
|
||||
this.sideViewHook(this.to);
|
||||
return;
|
||||
}
|
||||
if (this.$store.state.device.deckNavWindow && deckmode && this.to !== '/') {
|
||||
os.pageWindow(this.to);
|
||||
return;
|
||||
}
|
||||
|
||||
this.$router.push(this.to);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="nvlagfpb">
|
||||
<div class="nvlagfpb" @contextmenu.prevent.stop="() => {}">
|
||||
<MkMenu :items="items" @close="$emit('closed')" class="_popup _shadow" :align="'left'"/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -12,12 +12,12 @@
|
|||
<span v-else-if="item.type === 'pending'" :tabindex="i" class="pending item">
|
||||
<span><MkEllipsis/></span>
|
||||
</span>
|
||||
<router-link v-else-if="item.type === 'link'" :to="item.to" @click.passive="close()" :tabindex="i" class="_button item">
|
||||
<MkA v-else-if="item.type === 'link'" :to="item.to" @click.passive="close()" :tabindex="i" class="_button item">
|
||||
<Fa v-if="item.icon" :icon="item.icon" fixed-width/>
|
||||
<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
|
||||
<span>{{ item.text }}</span>
|
||||
<i v-if="item.indicate"><Fa :icon="faCircle"/></i>
|
||||
</router-link>
|
||||
</MkA>
|
||||
<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" @click="close()" :tabindex="i" class="_button item">
|
||||
<Fa v-if="item.icon" :icon="item.icon" fixed-width/>
|
||||
<span>{{ item.text }}</span>
|
||||
|
|
|
@ -51,7 +51,7 @@ export default defineComponent({
|
|||
.novjtctn {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin: 0 32px 0 0;
|
||||
margin: 16px 32px 0 0;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
|
|
|
@ -2,14 +2,16 @@
|
|||
<transition :name="$store.state.device.animation ? 'window' : ''" appear @after-leave="$emit('closed')">
|
||||
<div class="ebkgocck" v-if="showing">
|
||||
<div class="body _popup _shadow _narrow_" @mousedown="onBodyMousedown" @keydown="onKeydown">
|
||||
<div class="header">
|
||||
<button class="_button" @click="close()"><Fa :icon="faTimes"/></button>
|
||||
<div class="header" @contextmenu.prevent.stop="onContextmenu">
|
||||
<slot v-if="closeRight" name="buttons"><button class="_button" style="pointer-events: none;"></button></slot>
|
||||
<button v-else class="_button" @click="close()"><Fa :icon="faTimes"/></button>
|
||||
|
||||
<span class="title" @mousedown.prevent="onHeaderMousedown" @touchstart.prevent="onHeaderMousedown">
|
||||
<slot name="header"></slot>
|
||||
</span>
|
||||
<slot name="buttons">
|
||||
<button class="_button" style="pointer-events: none;"></button>
|
||||
</slot>
|
||||
|
||||
<button v-if="closeRight" class="_button" @click="close()"><Fa :icon="faTimes"/></button>
|
||||
<slot v-else name="buttons"><button class="_button" style="pointer-events: none;"></button></slot>
|
||||
</div>
|
||||
<div class="body" v-if="padding">
|
||||
<div class="_section">
|
||||
|
@ -85,6 +87,15 @@ export default defineComponent({
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
closeRight: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
contextmenu: {
|
||||
type: Array,
|
||||
required: false,
|
||||
}
|
||||
},
|
||||
|
||||
emits: ['closed'],
|
||||
|
@ -129,6 +140,12 @@ export default defineComponent({
|
|||
}
|
||||
},
|
||||
|
||||
onContextmenu(e) {
|
||||
if (this.contextmenu) {
|
||||
os.contextMenu(this.contextmenu, e);
|
||||
}
|
||||
},
|
||||
|
||||
// 最前面へ移動
|
||||
top() {
|
||||
let z = 0;
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
</div>
|
||||
<div v-else class="mk-url-preview" v-size="{ max: [400, 350] }">
|
||||
<transition name="zoom" mode="out-in">
|
||||
<component :is="self ? 'router-link' : 'a'" :class="{ compact }" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url" v-if="!fetching">
|
||||
<component :is="self ? 'MkA' : 'a'" :class="{ compact }" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url" v-if="!fetching">
|
||||
<div class="thumbnail" v-if="thumbnail" :style="`background-image: url('${thumbnail}')`">
|
||||
<button class="_button" v-if="!playerEnabled && player.url" @click.prevent="playerEnabled = true" :title="$t('enablePlayer')"><Fa :icon="faPlayCircle"/></button>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<component :is="self ? 'router-link' : 'a'" class="ieqqeuvs _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target"
|
||||
<component :is="self ? 'MkA' : 'a'" class="ieqqeuvs _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target"
|
||||
@mouseover="onMouseover"
|
||||
@mouseleave="onMouseleave"
|
||||
>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"></div>
|
||||
<MkAvatar class="avatar" :user="user" :disable-preview="true"/>
|
||||
<div class="title">
|
||||
<router-link class="name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></router-link>
|
||||
<MkA class="name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA>
|
||||
<p class="username"><MkAcct :user="user"/></p>
|
||||
</div>
|
||||
<div class="description">
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"></div>
|
||||
<MkAvatar class="avatar" :user="user" :disable-preview="true"/>
|
||||
<div class="title">
|
||||
<router-link class="name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></router-link>
|
||||
<MkA class="name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA>
|
||||
<p class="username"><MkAcct :user="user"/></p>
|
||||
</div>
|
||||
<div class="description">
|
||||
|
|
|
@ -6,13 +6,13 @@
|
|||
</div>
|
||||
|
||||
<div class="users">
|
||||
<router-link v-for="item in items" class="user" :key="item.id" :to="userPage(extract ? extract(item) : item)">
|
||||
<MkA v-for="item in items" class="user" :key="item.id" :to="userPage(extract ? extract(item) : item)">
|
||||
<MkAvatar :user="extract ? extract(item) : item" class="avatar" :disable-link="true"/>
|
||||
<div class="body">
|
||||
<MkUserName :user="extract ? extract(item) : item" class="name"/>
|
||||
<MkAcct :user="extract ? extract(item) : item" class="acct"/>
|
||||
</div>
|
||||
</router-link>
|
||||
</MkA>
|
||||
</div>
|
||||
<button class="more _button" v-appear="$store.state.device.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" v-show="more" :disabled="moreFetching">
|
||||
<template v-if="!moreFetching">{{ $t('loadMore') }}</template>
|
||||
|
|
|
@ -4,14 +4,13 @@
|
|||
|
||||
import '@/style.scss';
|
||||
|
||||
import { createApp } from 'vue';
|
||||
import { createApp, defineAsyncComponent } from 'vue';
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||
|
||||
import Root from './root.vue';
|
||||
import widgets from './widgets';
|
||||
import directives from './directives';
|
||||
import components from '@/components';
|
||||
import { version, apiUrl } from '@/config';
|
||||
import { version, apiUrl, deckmode } from '@/config';
|
||||
import { store } from './store';
|
||||
import { router } from './router';
|
||||
import { applyTheme } from '@/scripts/theme';
|
||||
|
@ -152,7 +151,12 @@ store.dispatch('instance/fetch').then(() => {
|
|||
|
||||
stream.init(store.state.i);
|
||||
|
||||
const app = createApp(Root);
|
||||
const app = createApp(await (
|
||||
window.location.search === '?zen' ? import('@/ui/zen.vue') :
|
||||
!store.getters.isSignedIn ? import('@/ui/visitor.vue') :
|
||||
deckmode ? import('@/ui/deck.vue') :
|
||||
import('@/ui/default.vue')
|
||||
).then(x => x.default));
|
||||
|
||||
if (_DEV_) {
|
||||
app.config.performance = true;
|
||||
|
|
|
@ -5,6 +5,7 @@ import { store } from '@/store';
|
|||
import { apiUrl } from '@/config';
|
||||
import MkPostFormDialog from '@/components/post-form-dialog.vue';
|
||||
import MkWaitingDialog from '@/components/waiting-dialog.vue';
|
||||
import { resolve } from '@/router';
|
||||
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
export const isMobile = /mobile|iphone|ipad|android/.test(ua);
|
||||
|
@ -162,7 +163,8 @@ export function popup(component: Component | typeof import('*.vue'), props: Reco
|
|||
};
|
||||
}
|
||||
|
||||
export function pageWindow(url: string, component: Component | typeof import('*.vue'), props: Record<string, any>) {
|
||||
export function pageWindow(url: string) {
|
||||
const { component, props } = resolve(url);
|
||||
popup(defineAsyncComponent(() => import('@/components/page-window.vue')), {
|
||||
initialUrl: url,
|
||||
initialComponent: markRaw(component),
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div class="_content">
|
||||
<ul>
|
||||
<li v-for="doc in docs" :key="doc.path">
|
||||
<router-link :to="`/docs/${doc.path}`">{{ doc.title }}</router-link>
|
||||
<MkA :to="`/docs/${doc.path}`">{{ doc.title }}</MkA>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -38,8 +38,8 @@
|
|||
<template #header><Fa :icon="faHashtag" fixed-width style="margin-right: 0.5em;"/>{{ $t('popularTags') }}</template>
|
||||
|
||||
<div class="vxjfqztj">
|
||||
<router-link v-for="tag in tagsLocal" :to="`/explore/tags/${tag.tag}`" :key="'local:' + tag.tag" class="local">{{ tag.tag }}</router-link>
|
||||
<router-link v-for="tag in tagsRemote" :to="`/explore/tags/${tag.tag}`" :key="'remote:' + tag.tag">{{ tag.tag }}</router-link>
|
||||
<MkA v-for="tag in tagsLocal" :to="`/explore/tags/${tag.tag}`" :key="'local:' + tag.tag" class="local">{{ tag.tag }}</MkA>
|
||||
<MkA v-for="tag in tagsRemote" :to="`/explore/tags/${tag.tag}`" :key="'remote:' + tag.tag">{{ tag.tag }}</MkA>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<MkAvatar class="avatar" :user="req.follower"/>
|
||||
<div class="body">
|
||||
<div class="name">
|
||||
<router-link class="name" :to="userPage(req.follower)" v-user-preview="req.follower.id"><MkUserName :user="req.follower"/></router-link>
|
||||
<MkA class="name" :to="userPage(req.follower)" v-user-preview="req.follower.id"><MkUserName :user="req.follower"/></MkA>
|
||||
<p class="acct">@{{ acct(req.follower) }}</p>
|
||||
</div>
|
||||
<div class="description" v-if="req.follower.description" :title="req.follower.description">
|
||||
|
|
|
@ -4,13 +4,12 @@
|
|||
<MkButton @click="start" primary class="start"><Fa :icon="faPlus"/> {{ $t('startMessaging') }}</MkButton>
|
||||
|
||||
<div class="history" v-if="messages.length > 0">
|
||||
<router-link v-for="(message, i) in messages"
|
||||
<MkA v-for="(message, i) in messages"
|
||||
class="message _panel"
|
||||
:class="{ isMe: isMe(message), isRead: message.groupId ? message.reads.includes($store.state.i.id) : message.isRead }"
|
||||
:to="message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`"
|
||||
:data-index="i"
|
||||
:key="message.id"
|
||||
@click.prevent="go(message)"
|
||||
>
|
||||
<div>
|
||||
<MkAvatar class="avatar" :user="message.groupId ? message.user : isMe(message) ? message.recipient : message.user"/>
|
||||
|
@ -27,7 +26,7 @@
|
|||
<p class="text"><span class="me" v-if="isMe(message)">{{ $t('you') }}:</span>{{ message.text }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</MkA>
|
||||
</div>
|
||||
<div class="_fullinfo" v-if="!fetching && messages.length == 0">
|
||||
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
||||
|
@ -90,18 +89,6 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
methods: {
|
||||
go(message) {
|
||||
const url = message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(this.isMe(message) ? message.recipient : message.user)}`;
|
||||
if (this.navHook) {
|
||||
this.navHook(url, defineAsyncComponent(() => import('@/pages/messaging/messaging-room.vue')), {
|
||||
userAcct: message.groupId ? null : getAcct(this.isMe(message) ? message.recipient : message.user),
|
||||
groupId: message.groupId
|
||||
});
|
||||
} else {
|
||||
this.$router.push(url);
|
||||
}
|
||||
},
|
||||
|
||||
getAcct,
|
||||
|
||||
isMe(message) {
|
||||
|
|
|
@ -317,7 +317,7 @@ const Component = defineComponent({
|
|||
text: this.$t('openInWindow'),
|
||||
icon: faWindowMaximize,
|
||||
action: () => {
|
||||
os.pageWindow(url, Component, this.$props);
|
||||
os.pageWindow(url);
|
||||
this.$router.back();
|
||||
},
|
||||
}, this.inWindow ? undefined : {
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
<MkPagination :pagination="ownedPagination" #default="{items}" ref="owned">
|
||||
<div class="_card" v-for="group in items" :key="group.id">
|
||||
<div class="_title"><router-link :to="`/my/groups/${ group.id }`" class="_link">{{ group.name }}</router-link></div>
|
||||
<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>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
<MkPagination :pagination="pagination" #default="{items}" class="lists _content" ref="list">
|
||||
<div class="list _panel" v-for="(list, i) in items" :key="list.id">
|
||||
<router-link :to="`/my/lists/${ list.id }`">{{ list.name }}</router-link>
|
||||
<MkA :to="`/my/lists/${ list.id }`">{{ list.name }}</MkA>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</div>
|
||||
|
|
|
@ -42,6 +42,12 @@ export default defineComponent({
|
|||
MkRemoteCaution,
|
||||
MkButton,
|
||||
},
|
||||
props: {
|
||||
noteId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
INFO: computed(() => this.note ? {
|
||||
|
@ -77,7 +83,7 @@ export default defineComponent({
|
|||
};
|
||||
},
|
||||
watch: {
|
||||
$route: 'fetch'
|
||||
noteId: 'fetch'
|
||||
},
|
||||
created() {
|
||||
this.fetch();
|
||||
|
@ -86,7 +92,7 @@ export default defineComponent({
|
|||
fetch() {
|
||||
Progress.start();
|
||||
os.api('notes/show', {
|
||||
noteId: this.$route.params.note
|
||||
noteId: this.noteId
|
||||
}).then(note => {
|
||||
Promise.all([
|
||||
os.api('users/notes', {
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
</header>
|
||||
|
||||
<section>
|
||||
<router-link class="view" v-if="pageId" :to="`/@${ author.username }/pages/${ currentName }`"><Fa :icon="faExternalLinkSquareAlt"/> {{ $t('_pages.viewPage') }}</router-link>
|
||||
<MkA class="view" v-if="pageId" :to="`/@${ author.username }/pages/${ currentName }`"><Fa :icon="faExternalLinkSquareAlt"/> {{ $t('_pages.viewPage') }}</MkA>
|
||||
|
||||
<MkInput v-model:value="title">
|
||||
<span>{{ $t('_pages.title') }}</span>
|
||||
|
|
|
@ -20,9 +20,9 @@
|
|||
</div>
|
||||
<div class="_section links">
|
||||
<div class="_content">
|
||||
<router-link :to="`./${page.name}/view-source`" class="link">{{ $t('_pages.viewSource') }}</router-link>
|
||||
<MkA :to="`./${page.name}/view-source`" class="link">{{ $t('_pages.viewSource') }}</MkA>
|
||||
<template v-if="$store.getters.isSignedIn && $store.state.i.id === page.userId">
|
||||
<router-link :to="`/my/pages/edit/${page.id}`" class="link">{{ $t('_pages.editThisPage') }}</router-link>
|
||||
<MkA :to="`/my/pages/edit/${page.id}`" class="link">{{ $t('_pages.editThisPage') }}</MkA>
|
||||
<button v-if="$store.state.i.pinnedPageId === page.id" @click="pin(false)" class="link _textButton">{{ $t('unpin') }}</button>
|
||||
<button v-else @click="pin(true)" class="link _textButton">{{ $t('pin') }}</button>
|
||||
</template>
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
<div class="_section">
|
||||
<section class="_card _vMargin">
|
||||
<div class="_title"><Fa :icon="faCog"/> {{ $t('general') }}</div>
|
||||
<div class="_content">
|
||||
<div>{{ $t('defaultNavigationBehaviour') }}</div>
|
||||
<MkSwitch v-model:value="defaultSideView">{{ $t('openInSideView') }}</MkSwitch>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<div>{{ $t('whenServerDisconnected') }}</div>
|
||||
<MkRadio v-model="serverDisconnectedBehavior" value="reload">{{ $t('_serverDisconnectedBehavior.reload') }}</MkRadio>
|
||||
|
@ -51,6 +55,10 @@
|
|||
|
||||
<section class="_card _vMargin">
|
||||
<div class="_title"><Fa :icon="faColumns"/> {{ $t('deck') }}</div>
|
||||
<div class="_content">
|
||||
<div>{{ $t('defaultNavigationBehaviour') }}</div>
|
||||
<MkSwitch v-model:value="deckNavWindow">{{ $t('openInWindow') }}</MkSwitch>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<MkSwitch v-model:value="deckAlwaysShowMainColumn">
|
||||
{{ $t('_deck.alwaysShowMainColumn') }}
|
||||
|
@ -146,6 +154,16 @@ export default defineComponent({
|
|||
set(value) { this.$store.commit('device/set', { key: 'showFixedPostForm', value }); }
|
||||
},
|
||||
|
||||
defaultSideView: {
|
||||
get() { return this.$store.state.device.defaultSideView; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'defaultSideView', value }); }
|
||||
},
|
||||
|
||||
deckNavWindow: {
|
||||
get() { return this.$store.state.device.deckNavWindow; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'deckNavWindow', value }); }
|
||||
},
|
||||
|
||||
chatOpenBehavior: {
|
||||
get() { return this.$store.state.device.chatOpenBehavior; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'chatOpenBehavior', value }); }
|
||||
|
|
|
@ -1,52 +1,57 @@
|
|||
<template>
|
||||
<div class="vvcocwet" :class="{ wide: !narrow }" ref="el">
|
||||
<div class="nav" v-if="!narrow || $route.name === 'settings'">
|
||||
<div class="nav" v-if="!narrow || page == null">
|
||||
<div class="menu">
|
||||
<div class="label">{{ $t('basicSettings') }}</div>
|
||||
<router-link class="item" replace to="/settings/profile"><Fa :icon="faUser" fixed-width class="icon"/>{{ $t('profile') }}</router-link>
|
||||
<router-link class="item" replace to="/settings/privacy"><Fa :icon="faLockOpen" fixed-width class="icon"/>{{ $t('privacy') }}</router-link>
|
||||
<router-link class="item" replace to="/settings/reaction"><Fa :icon="faLaugh" fixed-width class="icon"/>{{ $t('reaction') }}</router-link>
|
||||
<router-link class="item" replace to="/settings/notifications"><Fa :icon="faBell" fixed-width class="icon"/>{{ $t('notifications') }}</router-link>
|
||||
<router-link class="item" replace to="/settings/integration"><Fa :icon="faShareAlt" fixed-width class="icon"/>{{ $t('integration') }}</router-link>
|
||||
<router-link class="item" replace to="/settings/security"><Fa :icon="faLock" fixed-width class="icon"/>{{ $t('security') }}</router-link>
|
||||
<MkA class="item" :class="{ active: page === 'profile' }" replace to="/settings/profile"><Fa :icon="faUser" fixed-width class="icon"/>{{ $t('profile') }}</MkA>
|
||||
<MkA class="item" :class="{ active: page === 'privacy' }" replace to="/settings/privacy"><Fa :icon="faLockOpen" fixed-width class="icon"/>{{ $t('privacy') }}</MkA>
|
||||
<MkA class="item" :class="{ active: page === 'reaction' }" replace to="/settings/reaction"><Fa :icon="faLaugh" fixed-width class="icon"/>{{ $t('reaction') }}</MkA>
|
||||
<MkA class="item" :class="{ active: page === 'notifications' }" replace to="/settings/notifications"><Fa :icon="faBell" fixed-width class="icon"/>{{ $t('notifications') }}</MkA>
|
||||
<MkA class="item" :class="{ active: page === 'integration' }" replace to="/settings/integration"><Fa :icon="faShareAlt" fixed-width class="icon"/>{{ $t('integration') }}</MkA>
|
||||
<MkA class="item" :class="{ active: page === 'security' }" replace to="/settings/security"><Fa :icon="faLock" fixed-width class="icon"/>{{ $t('security') }}</MkA>
|
||||
</div>
|
||||
<div class="menu">
|
||||
<div class="label">{{ $t('clientSettings') }}</div>
|
||||
<router-link class="item" replace to="/settings/general"><Fa :icon="faCogs" fixed-width class="icon"/>{{ $t('general') }}</router-link>
|
||||
<router-link class="item" replace to="/settings/theme"><Fa :icon="faPalette" fixed-width class="icon"/>{{ $t('theme') }}</router-link>
|
||||
<router-link class="item" replace to="/settings/sidebar"><Fa :icon="faListUl" fixed-width class="icon"/>{{ $t('sidebar') }}</router-link>
|
||||
<router-link class="item" replace to="/settings/sounds"><Fa :icon="faMusic" fixed-width class="icon"/>{{ $t('sounds') }}</router-link>
|
||||
<router-link class="item" replace to="/settings/plugins"><Fa :icon="faPlug" fixed-width class="icon"/>{{ $t('plugins') }}</router-link>
|
||||
<MkA class="item" :class="{ active: page === 'general' }" replace to="/settings/general"><Fa :icon="faCogs" fixed-width class="icon"/>{{ $t('general') }}</MkA>
|
||||
<MkA class="item" :class="{ active: page === 'theme' }" replace to="/settings/theme"><Fa :icon="faPalette" fixed-width class="icon"/>{{ $t('theme') }}</MkA>
|
||||
<MkA class="item" :class="{ active: page === 'sidebar' }" replace to="/settings/sidebar"><Fa :icon="faListUl" fixed-width class="icon"/>{{ $t('sidebar') }}</MkA>
|
||||
<MkA class="item" :class="{ active: page === 'sounds' }" replace to="/settings/sounds"><Fa :icon="faMusic" fixed-width class="icon"/>{{ $t('sounds') }}</MkA>
|
||||
<MkA class="item" :class="{ active: page === 'plugins' }" replace to="/settings/plugins"><Fa :icon="faPlug" fixed-width class="icon"/>{{ $t('plugins') }}</MkA>
|
||||
</div>
|
||||
<div class="menu">
|
||||
<div class="label">{{ $t('otherSettings') }}</div>
|
||||
<router-link class="item" replace to="/settings/mute-block"><Fa :icon="faBan" fixed-width class="icon"/>{{ $t('muteAndBlock') }}</router-link>
|
||||
<router-link class="item" replace to="/settings/word-mute"><Fa :icon="faCommentSlash" fixed-width class="icon"/>{{ $t('wordMute') }}</router-link>
|
||||
<router-link class="item" replace to="/settings/api"><Fa :icon="faKey" fixed-width class="icon"/>API</router-link>
|
||||
<router-link class="item" replace to="/settings/other"><Fa :icon="faEllipsisH" fixed-width class="icon"/>{{ $t('other') }}</router-link>
|
||||
<MkA class="item" :class="{ active: page === 'mute-block' }" replace to="/settings/mute-block"><Fa :icon="faBan" fixed-width class="icon"/>{{ $t('muteAndBlock') }}</MkA>
|
||||
<MkA class="item" :class="{ active: page === 'word-mute' }" replace to="/settings/word-mute"><Fa :icon="faCommentSlash" fixed-width class="icon"/>{{ $t('wordMute') }}</MkA>
|
||||
<MkA class="item" :class="{ active: page === 'api' }" replace to="/settings/api"><Fa :icon="faKey" fixed-width class="icon"/>API</MkA>
|
||||
<MkA class="item" :class="{ active: page === 'other' }" replace to="/settings/other"><Fa :icon="faEllipsisH" fixed-width class="icon"/>{{ $t('other') }}</MkA>
|
||||
</div>
|
||||
<div class="menu">
|
||||
<button class="_button item" @click="logout">{{ $t('logout') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition :name="($store.state.device.animation && !narrow) ? 'view-slide' : ''" appear mode="out-in">
|
||||
<component :is="Component" @info="onInfo"/>
|
||||
</transition>
|
||||
</router-view>
|
||||
<transition :name="($store.state.device.animation && !narrow) ? 'view-slide' : ''" appear mode="out-in">
|
||||
<component :is="component" @info="onInfo"/>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, ref } from 'vue';
|
||||
import { computed, defineAsyncComponent, defineComponent, onMounted, ref } from 'vue';
|
||||
import { faCog, faPalette, faPlug, faUser, faListUl, faLock, faCommentSlash, faMusic, faCogs, faEllipsisH, faBan, faShareAlt, faLockOpen, faKey } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faLaugh, faBell } from '@fortawesome/free-regular-svg-icons';
|
||||
import { store } from '@/store';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
page: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
|
||||
setup(props, context) {
|
||||
const INFO = ref({
|
||||
header: [{
|
||||
|
@ -60,6 +65,27 @@ export default defineComponent({
|
|||
const onInfo = (viewInfo) => {
|
||||
INFO.value = viewInfo;
|
||||
};
|
||||
const component = computed(() => {
|
||||
switch (props.page) {
|
||||
case 'profile': return defineAsyncComponent(() => import('./profile.vue'));
|
||||
case 'privacy': return defineAsyncComponent(() => import('./privacy.vue'));
|
||||
case 'reaction': return defineAsyncComponent(() => import('./reaction.vue'));
|
||||
case 'notifications': return defineAsyncComponent(() => import('./notifications.vue'));
|
||||
case 'mute-block': return defineAsyncComponent(() => import('./mute-block.vue'));
|
||||
case 'word-mute': return defineAsyncComponent(() => import('./word-mute.vue'));
|
||||
case 'integration': return defineAsyncComponent(() => import('./integration.vue'));
|
||||
case 'security': return defineAsyncComponent(() => import('./security.vue'));
|
||||
case 'api': return defineAsyncComponent(() => import('./api.vue'));
|
||||
case 'other': return defineAsyncComponent(() => import('./other.vue'));
|
||||
case 'general': return defineAsyncComponent(() => import('./general.vue'));
|
||||
case 'theme': return defineAsyncComponent(() => import('./theme.vue'));
|
||||
case 'sidebar': return defineAsyncComponent(() => import('./sidebar.vue'));
|
||||
case 'sounds': return defineAsyncComponent(() => import('./sounds.vue'));
|
||||
case 'plugins': return defineAsyncComponent(() => import('./plugins.vue'));
|
||||
case 'import-export': return defineAsyncComponent(() => import('./import-export.vue'));
|
||||
default: return null;
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
narrow.value = el.value.offsetWidth < 650;
|
||||
|
@ -71,6 +97,7 @@ export default defineComponent({
|
|||
view,
|
||||
el,
|
||||
onInfo,
|
||||
component,
|
||||
logout: () => {
|
||||
store.dispatch('logout');
|
||||
location.href = '/';
|
||||
|
@ -121,7 +148,7 @@ export default defineComponent({
|
|||
//border-top: solid 1px var(--divider);
|
||||
}
|
||||
|
||||
&.router-link-active {
|
||||
&.active {
|
||||
color: var(--accent);
|
||||
padding-left: 42px;
|
||||
}
|
||||
|
|
|
@ -6,9 +6,9 @@
|
|||
<template #empty><MkInfo>{{ $t('noUsers') }}</MkInfo></template>
|
||||
<template #default="{items}">
|
||||
<div class="user" v-for="mute in items" :key="mute.id">
|
||||
<router-link class="name" :to="userPage(mute.mutee)">
|
||||
<MkA class="name" :to="userPage(mute.mutee)">
|
||||
<MkAcct :user="mute.mutee"/>
|
||||
</router-link>
|
||||
</MkA>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
|
@ -18,9 +18,9 @@
|
|||
<template #empty><MkInfo>{{ $t('noUsers') }}</MkInfo></template>
|
||||
<template #default="{items}">
|
||||
<div class="user" v-for="block in items" :key="block.id">
|
||||
<router-link class="name" :to="userPage(block.blockee)">
|
||||
<MkA class="name" :to="userPage(block.blockee)">
|
||||
<MkAcct :user="block.blockee"/>
|
||||
</router-link>
|
||||
</MkA>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
|
||||
</optgroup>
|
||||
</MkSelect>
|
||||
<a href="https://assets.msky.cafe/theme/list" rel="noopener" target="_blank" class="_link">{{ $t('_theme.explore') }}</a>・<router-link to="/theme-editor" class="_link">{{ $t('_theme.make') }}</router-link>
|
||||
<a href="https://assets.msky.cafe/theme/list" rel="noopener" target="_blank" class="_link">{{ $t('_theme.explore') }}</a>・<MkA to="/theme-editor" class="_link">{{ $t('_theme.make') }}</MkA>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<MkButton primary v-if="wallpaper == null" @click="setWallpaper">{{ $t('setWallpaper') }}</MkButton>
|
||||
|
|
|
@ -15,11 +15,18 @@ export default defineComponent({
|
|||
XNotes
|
||||
},
|
||||
|
||||
props: {
|
||||
tag: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
INFO: {
|
||||
header: [{
|
||||
title: this.$route.params.tag,
|
||||
title: this.tag,
|
||||
icon: faHashtag
|
||||
}],
|
||||
},
|
||||
|
@ -27,7 +34,7 @@ export default defineComponent({
|
|||
endpoint: 'notes/search-by-tag',
|
||||
limit: 10,
|
||||
params: () => ({
|
||||
tag: this.$route.params.tag,
|
||||
tag: this.tag,
|
||||
})
|
||||
},
|
||||
faHashtag
|
||||
|
@ -35,7 +42,7 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
watch: {
|
||||
$route() {
|
||||
tag() {
|
||||
(this.$refs.notes as any).reload();
|
||||
}
|
||||
},
|
||||
|
|
|
@ -229,7 +229,7 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
messagingWindowOpen() {
|
||||
os.pageWindow('/my/messaging', defineAsyncComponent(() => import('@/pages/messaging/index.vue')));
|
||||
os.pageWindow('/my/messaging');
|
||||
},
|
||||
|
||||
openWaitingDialog(text?) {
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<div class="_content" v-else-if="tutorial === 1">
|
||||
<div>{{ $t('_tutorial.step2_1') }}</div>
|
||||
<div>{{ $t('_tutorial.step2_2') }}</div>
|
||||
<router-link class="_link" to="/settings/profile">{{ $t('editProfile') }}</router-link>
|
||||
<MkA class="_link" to="/settings/profile">{{ $t('editProfile') }}</MkA>
|
||||
</div>
|
||||
<div class="_content" v-else-if="tutorial === 2">
|
||||
<div>{{ $t('_tutorial.step3_1') }}</div>
|
||||
|
@ -25,10 +25,10 @@
|
|||
<div>{{ $t('_tutorial.step5_1') }}</div>
|
||||
<i18n-t keypath="_tutorial.step5_2" tag="div">
|
||||
<template #featured>
|
||||
<router-link class="_link" to="/featured">{{ $t('featured') }}</router-link>
|
||||
<MkA class="_link" to="/featured">{{ $t('featured') }}</MkA>
|
||||
</template>
|
||||
<template #explore>
|
||||
<router-link class="_link" to="/explore">{{ $t('explore') }}</router-link>
|
||||
<MkA class="_link" to="/explore">{{ $t('explore') }}</MkA>
|
||||
</template>
|
||||
</i18n-t>
|
||||
<div>{{ $t('_tutorial.step5_3') }}</div>
|
||||
|
@ -43,7 +43,7 @@
|
|||
<div>{{ $t('_tutorial.step7_1') }}</div>
|
||||
<i18n-t keypath="_tutorial.step7_2" tag="div">
|
||||
<template #help>
|
||||
<router-link class="_link" to="/docs">{{ $t('help') }}</router-link>
|
||||
<MkA class="_link" to="/docs">{{ $t('help') }}</MkA>
|
||||
</template>
|
||||
</i18n-t>
|
||||
<div>{{ $t('_tutorial.step7_3') }}</div>
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import parseAcct from '../../../misc/acct/parse';
|
||||
import MkUserInfo from '@/components/user-info.vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import { userPage, acct } from '../../filters/user';
|
||||
|
@ -22,10 +21,14 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
props: {
|
||||
user: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
|
@ -34,7 +37,7 @@ export default defineComponent({
|
|||
endpoint: () => this.type === 'following' ? 'users/following' : 'users/followers',
|
||||
limit: 20,
|
||||
params: {
|
||||
...parseAcct(this.$route.params.user),
|
||||
userId: this.user.id,
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -45,7 +48,7 @@ export default defineComponent({
|
|||
this.$refs.list.reload();
|
||||
},
|
||||
|
||||
'$route'() {
|
||||
user() {
|
||||
this.$refs.list.reload();
|
||||
}
|
||||
},
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
<div class="ujigsodd">
|
||||
<MkLoading v-if="fetching"/>
|
||||
<div class="stream" v-if="!fetching && images.length > 0">
|
||||
<router-link v-for="(image, i) in images" :key="i"
|
||||
<MkA v-for="image in images"
|
||||
class="img"
|
||||
:style="`background-image: url(${thumbnail(image.file)})`"
|
||||
:to="notePage(image.note)"
|
||||
></router-link>
|
||||
></MkA>
|
||||
</div>
|
||||
<p class="empty" v-if="!fetching && images.length == 0">{{ $t('nothing') }}</p>
|
||||
</div>
|
||||
|
|
|
@ -67,24 +67,23 @@
|
|||
</dl>
|
||||
</div>
|
||||
<div class="status">
|
||||
<router-link :to="userPage(user)" :class="{ active: $route.name === 'user' }">
|
||||
<MkA :to="userPage(user)" :class="{ active: page === 'index' }">
|
||||
<b>{{ number(user.notesCount) }}</b>
|
||||
<span>{{ $t('notes') }}</span>
|
||||
</router-link>
|
||||
<router-link :to="userPage(user, 'following')" :class="{ active: $route.name === 'userFollowing' }">
|
||||
</MkA>
|
||||
<MkA :to="userPage(user, 'following')" :class="{ active: page === 'following' }">
|
||||
<b>{{ number(user.followingCount) }}</b>
|
||||
<span>{{ $t('following') }}</span>
|
||||
</router-link>
|
||||
<router-link :to="userPage(user, 'followers')" :class="{ active: $route.name === 'userFollowers' }">
|
||||
</MkA>
|
||||
<MkA :to="userPage(user, 'followers')" :class="{ active: page === 'followers' }">
|
||||
<b>{{ number(user.followersCount) }}</b>
|
||||
<span>{{ $t('followers') }}</span>
|
||||
</router-link>
|
||||
</MkA>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<router-view :user="user"></router-view>
|
||||
<template v-if="$route.name == 'user'">
|
||||
<template v-if="page === 'index'">
|
||||
<div class="_section">
|
||||
<div class="_content _vMargin" v-if="user.pinnedNotes.length > 0">
|
||||
<XNote v-for="note in user.pinnedNotes" class="note _vMargin" :note="note" @update:note="pinnedNoteUpdated(note, $event)" :key="note.id" :detail="true" :pinned="true"/>
|
||||
|
@ -106,6 +105,8 @@
|
|||
<XUserTimeline :user="user" class="_content"/>
|
||||
</div>
|
||||
</template>
|
||||
<XFollowList v-else-if="page === 'following'" type="following" :user="user"/>
|
||||
<XFollowList v-else-if="page === 'followers'" type="followers" :user="user"/>
|
||||
</div>
|
||||
<div v-else-if="error">
|
||||
<MkError @retry="fetch()"/>
|
||||
|
@ -128,7 +129,7 @@ import parseAcct from '../../../misc/acct/parse';
|
|||
import { getScrollPosition } from '@/scripts/scroll';
|
||||
import { getUserMenu } from '@/scripts/get-user-menu';
|
||||
import number from '../../filters/number';
|
||||
import { userPage, acct } from '../../filters/user';
|
||||
import { userPage, acct as getAcct } from '../../filters/user';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default defineComponent({
|
||||
|
@ -139,10 +140,23 @@ export default defineComponent({
|
|||
MkContainer,
|
||||
MkRemoteCaution,
|
||||
MkFolder,
|
||||
XFollowList: defineAsyncComponent(() => import('./follow-list.vue')),
|
||||
XPhotos: defineAsyncComponent(() => import('./index.photos.vue')),
|
||||
XActivity: defineAsyncComponent(() => import('./index.activity.vue')),
|
||||
},
|
||||
|
||||
props: {
|
||||
acct: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
page: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'index'
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
INFO: computed(() => this.user ? {
|
||||
|
@ -176,7 +190,7 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
watch: {
|
||||
$route: 'fetch'
|
||||
acct: 'fetch'
|
||||
},
|
||||
|
||||
created() {
|
||||
|
@ -192,10 +206,12 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
methods: {
|
||||
getAcct,
|
||||
|
||||
fetch() {
|
||||
if (this.$route.params.user == null) return;
|
||||
if (this.acct == null) return;
|
||||
Progress.start();
|
||||
os.api('users/show', parseAcct(this.$route.params.user)).then(user => {
|
||||
os.api('users/show', parseAcct(this.acct)).then(user => {
|
||||
this.user = user;
|
||||
}).catch(e => {
|
||||
this.error = e;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { defineAsyncComponent } from 'vue';
|
||||
import { defineAsyncComponent, markRaw } from 'vue';
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import MkLoading from '@/pages/_loading_.vue';
|
||||
import MkError from '@/pages/_error_.vue';
|
||||
|
@ -18,30 +18,11 @@ export const router = createRouter({
|
|||
routes: [
|
||||
// NOTE: MkTimelineをdynamic importするとAsyncComponentWrapperが間に入るせいでkeep-aliveのコンポーネント指定が効かなくなる
|
||||
{ path: '/', name: 'index', component: store.getters.isSignedIn ? MkTimeline : page('welcome') },
|
||||
{ path: '/@:user', name: 'user', component: page('user/index'), children: [
|
||||
{ path: 'following', name: 'userFollowing', component: page('user/follow-list'), props: { type: 'following' } },
|
||||
{ path: 'followers', name: 'userFollowers', component: page('user/follow-list'), props: { type: 'followers' } },
|
||||
]},
|
||||
{ path: '/@:acct/:page?', name: 'user', component: page('user/index'), props: route => ({ acct: route.params.acct, page: route.params.page || 'index' }) },
|
||||
{ path: '/@:user/pages/:page', component: page('page'), props: route => ({ pageName: route.params.page, username: route.params.user }) },
|
||||
{ path: '/@:user/pages/:pageName/view-source', component: page('page-editor/page-editor'), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) },
|
||||
{ path: '/@:acct/room', props: true, component: page('room/room') },
|
||||
{ path: '/settings', name: 'settings', component: page('settings/index'), children: [
|
||||
{ path: 'profile', component: page('settings/profile') },
|
||||
{ path: 'privacy', component: page('settings/privacy') },
|
||||
{ path: 'reaction', component: page('settings/reaction') },
|
||||
{ path: 'notifications', component: page('settings/notifications') },
|
||||
{ path: 'mute-block', component: page('settings/mute-block') },
|
||||
{ path: 'word-mute', component: page('settings/word-mute') },
|
||||
{ path: 'integration', component: page('settings/integration') },
|
||||
{ path: 'security', component: page('settings/security') },
|
||||
{ path: 'api', component: page('settings/api') },
|
||||
{ path: 'other', component: page('settings/other') },
|
||||
{ path: 'general', component: page('settings/general') },
|
||||
{ path: 'theme', component: page('settings/theme') },
|
||||
{ path: 'sidebar', component: page('settings/sidebar') },
|
||||
{ path: 'sounds', component: page('settings/sounds') },
|
||||
{ path: 'plugins', component: page('settings/plugins') },
|
||||
]},
|
||||
{ path: '/settings/:page?', name: 'settings', component: page('settings/index'), props: route => ({ page: route.params.page || null }) },
|
||||
{ path: '/announcements', component: page('announcements') },
|
||||
{ path: '/about', component: page('about') },
|
||||
{ path: '/about-misskey', component: page('about-misskey') },
|
||||
|
@ -87,8 +68,8 @@ export const router = createRouter({
|
|||
{ path: '/instance/relays', component: page('instance/relays') },
|
||||
{ path: '/instance/announcements', component: page('instance/announcements') },
|
||||
{ path: '/instance/abuses', component: page('instance/abuses') },
|
||||
{ path: '/notes/:note', name: 'note', component: page('note') },
|
||||
{ path: '/tags/:tag', component: page('tag') },
|
||||
{ path: '/notes/:note', name: 'note', component: page('note'), props: route => ({ noteId: route.params.note }) },
|
||||
{ path: '/tags/:tag', component: page('tag'), props: route => ({ tag: route.params.tag }) },
|
||||
{ path: '/auth/:token', component: page('auth') },
|
||||
{ path: '/miauth/:session', component: page('miauth') },
|
||||
{ path: '/authorize-follow', component: page('follow') },
|
||||
|
@ -120,3 +101,13 @@ router.afterEach((to, from) => {
|
|||
indexScrollPos = window.scrollY;
|
||||
}
|
||||
});
|
||||
|
||||
export function resolve(path: string) {
|
||||
const resolved = router.resolve(path);
|
||||
const route = resolved.matched[0];
|
||||
return {
|
||||
component: markRaw(route.components.default),
|
||||
// TODO: route.propsには関数以外も入る可能性があるのでよしなにハンドリングする
|
||||
props: route.props?.default ? route.props.default(resolved) : resolved.params
|
||||
};
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import getAcct from '../../misc/acct/render';
|
|||
import * as os from '@/os';
|
||||
import { store, userActions } from '@/store';
|
||||
import { router } from '@/router';
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import { popout } from './popout';
|
||||
|
||||
export function getUserMenu(user) {
|
||||
|
@ -137,7 +136,7 @@ export function getUserMenu(user) {
|
|||
action: () => {
|
||||
const acct = getAcct(user);
|
||||
switch (store.state.device.chatOpenBehavior) {
|
||||
case 'window': { os.pageWindow('/my/messaging/' + acct, defineAsyncComponent(() => import('@/pages/messaging/messaging-room.vue')), { userAcct: acct }); break; }
|
||||
case 'window': { os.pageWindow('/my/messaging/' + acct); break; }
|
||||
case 'popout': { popout('/my/messaging'); break; }
|
||||
default: { router.push('/my/messaging'); break; }
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { faBell, faComments, faEnvelope } from '@fortawesome/free-regular-svg-icons';
|
||||
import { faAt, faBroadcastTower, faCloud, faColumns, faDoorClosed, faFileAlt, faFireAlt, faGamepad, faHashtag, faListUl, faSatellite, faSatelliteDish, faSearch, faStar, faTerminal, faUserClock, faUsers } from '@fortawesome/free-solid-svg-icons';
|
||||
import { computed, defineAsyncComponent } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import { store } from '@/store';
|
||||
import { deckmode } from '@/config';
|
||||
import { search } from '@/scripts/search';
|
||||
|
@ -23,7 +23,7 @@ export const sidebarDef = {
|
|||
indicated: computed(() => store.getters.isSignedIn && store.state.i.hasUnreadMessagingMessage),
|
||||
action: () => {
|
||||
switch (store.state.device.chatOpenBehavior) {
|
||||
case 'window': { os.pageWindow('/my/messaging', defineAsyncComponent(() => import('@/pages/messaging/index.vue'))); break; }
|
||||
case 'window': { os.pageWindow('/my/messaging'); break; }
|
||||
case 'popout': { popout('/my/messaging'); break; }
|
||||
default: { router.push('/my/messaging'); break; }
|
||||
}
|
||||
|
|
|
@ -70,6 +70,8 @@ export const defaultDeviceSettings = {
|
|||
animatedMfm: true,
|
||||
imageNewTab: false,
|
||||
chatOpenBehavior: 'page',
|
||||
defaultSideView: false,
|
||||
deckNavWindow: true,
|
||||
showFixedPostForm: false,
|
||||
disablePagesScript: false,
|
||||
enableInfiniteScroll: true,
|
||||
|
|
|
@ -1,9 +1,4 @@
|
|||
<template>
|
||||
<ZenUI v-if="zen"/>
|
||||
<VisitorUI v-else-if="!$store.getters.isSignedIn"/>
|
||||
<DeckUI v-else-if="deckmode"/>
|
||||
<DefaultUI v-else/>
|
||||
|
||||
<component v-for="popup in popups"
|
||||
:key="popup.id"
|
||||
:is="popup.component"
|
||||
|
@ -13,27 +8,23 @@
|
|||
|
||||
<XUpload v-if="uploads.length > 0"/>
|
||||
|
||||
<XStreamIndicator/>
|
||||
|
||||
<div id="wait" v-if="pendingApiRequestsCount > 0"></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||
import { deckmode } from '@/config';
|
||||
import { popups, uploads, pendingApiRequestsCount } from '@/os';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
DefaultUI: defineAsyncComponent(() => import('@/ui/default.vue')),
|
||||
DeckUI: defineAsyncComponent(() => import('@/ui/deck.vue')),
|
||||
ZenUI: defineAsyncComponent(() => import('@/ui/zen.vue')),
|
||||
VisitorUI: defineAsyncComponent(() => import('@/ui/visitor.vue')),
|
||||
XUpload: defineAsyncComponent(() => import('@/components/upload.vue')),
|
||||
XStreamIndicator: defineAsyncComponent(() => import('./stream-indicator.vue')),
|
||||
XUpload: defineAsyncComponent(() => import('./upload.vue')),
|
||||
},
|
||||
|
||||
setup() {
|
||||
return {
|
||||
zen: window.location.search === '?zen',
|
||||
deckmode,
|
||||
uploads,
|
||||
popups,
|
||||
pendingApiRequestsCount,
|
|
@ -29,7 +29,7 @@
|
|||
<button v-if="$store.getters.isSignedIn" class="nav _button" @click="showNav()"><Fa :icon="faBars"/><i v-if="navIndicated"><Fa :icon="faCircle"/></i></button>
|
||||
<button v-if="$store.getters.isSignedIn" class="post _buttonPrimary" @click="post()"><Fa :icon="faPencilAlt"/></button>
|
||||
|
||||
<StreamIndicator v-if="$store.getters.isSignedIn"/>
|
||||
<XCommon/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -47,9 +47,11 @@ import XHeader from './_common_/header.vue';
|
|||
import { getScrollContainer } from '@/scripts/scroll';
|
||||
import * as os from '@/os';
|
||||
import { sidebarDef } from '@/sidebar';
|
||||
import XCommon from './_common_/common.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XCommon,
|
||||
XSidebar,
|
||||
XHeader,
|
||||
DeckColumn,
|
||||
|
|
157
src/client/ui/default.side.vue
Normal file
157
src/client/ui/default.side.vue
Normal file
|
@ -0,0 +1,157 @@
|
|||
<template>
|
||||
<div class="qvzfzxam _narrow_" v-if="component">
|
||||
<div class="container">
|
||||
<header class="header" @contextmenu.prevent.stop="onContextmenu">
|
||||
<button class="_button" @click="back()" v-if="history.length > 0"><Fa :icon="faChevronLeft"/></button>
|
||||
<button class="_button" style="pointer-events: none;" v-else><!-- マージンのバランスを取るためのダミー --></button>
|
||||
<XHeader class="title" :info="pageInfo" :with-back="false"/>
|
||||
<button class="_button" @click="close()"><Fa :icon="faTimes"/></button>
|
||||
</header>
|
||||
<component :is="component" v-bind="props" :ref="changePage"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { faTimes, faChevronLeft, faExpandAlt, faWindowMaximize, faExternalLinkAlt, faLink } from '@fortawesome/free-solid-svg-icons';
|
||||
import XHeader from './_common_/header.vue';
|
||||
import * as os from '@/os';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
||||
import { resolve } from '@/router';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XHeader
|
||||
},
|
||||
|
||||
provide() {
|
||||
return {
|
||||
navHook: (url) => {
|
||||
this.navigate(url);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
url: null,
|
||||
component: null,
|
||||
props: {},
|
||||
pageInfo: null,
|
||||
history: [],
|
||||
faTimes, faChevronLeft,
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
changePage(page) {
|
||||
if (page == null) return;
|
||||
if (page.INFO) {
|
||||
this.pageInfo = page.INFO;
|
||||
}
|
||||
},
|
||||
|
||||
navigate(url, record = true) {
|
||||
if (record && this.url) this.history.push(this.url);
|
||||
this.url = url;
|
||||
const { component, props } = resolve(url);
|
||||
this.component = component;
|
||||
this.props = props;
|
||||
},
|
||||
|
||||
back() {
|
||||
this.navigate(this.history.pop(), false);
|
||||
},
|
||||
|
||||
close() {
|
||||
this.url = null;
|
||||
this.component = null;
|
||||
this.props = {};
|
||||
},
|
||||
|
||||
onContextmenu(e) {
|
||||
os.contextMenu([{
|
||||
type: 'label',
|
||||
text: this.url,
|
||||
}, {
|
||||
icon: faExpandAlt,
|
||||
text: this.$t('showInPage'),
|
||||
action: () => {
|
||||
this.$router.push(this.url);
|
||||
this.close();
|
||||
}
|
||||
}, {
|
||||
icon: faWindowMaximize,
|
||||
text: this.$t('openInWindow'),
|
||||
action: () => {
|
||||
os.pageWindow(this.url);
|
||||
this.close();
|
||||
}
|
||||
}, null, {
|
||||
icon: faExternalLinkAlt,
|
||||
text: this.$t('openInNewTab'),
|
||||
action: () => {
|
||||
window.open(this.url, '_blank');
|
||||
this.close();
|
||||
}
|
||||
}, {
|
||||
icon: faLink,
|
||||
text: this.$t('copyLink'),
|
||||
action: () => {
|
||||
copyToClipboard(this.url);
|
||||
}
|
||||
}], e);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.qvzfzxam {
|
||||
$header-height: 58px; // TODO: どこかに集約したい
|
||||
|
||||
--section-padding: 16px;
|
||||
--margin: var(--marginHalf);
|
||||
|
||||
> .container {
|
||||
position: fixed;
|
||||
width: 370px;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
box-sizing: border-box;
|
||||
|
||||
> .header {
|
||||
display: flex;
|
||||
position: sticky;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
height: $header-height;
|
||||
width: 100%;
|
||||
line-height: $header-height;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
//background-color: var(--panel);
|
||||
-webkit-backdrop-filter: blur(32px);
|
||||
backdrop-filter: blur(32px);
|
||||
background-color: var(--header);
|
||||
border-bottom: solid 1px var(--divider);
|
||||
|
||||
> ._button {
|
||||
height: $header-height;
|
||||
width: $header-height;
|
||||
|
||||
&:hover {
|
||||
color: var(--fgHighlighted);
|
||||
}
|
||||
}
|
||||
|
||||
> .title {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -20,6 +20,8 @@
|
|||
</main>
|
||||
</div>
|
||||
|
||||
<XSide v-if="isDesktop" class="side" ref="side"/>
|
||||
|
||||
<div v-if="isDesktop" class="widgets">
|
||||
<div ref="widgetsSpacer"></div>
|
||||
<XWidgets @mounted="attachSticky"/>
|
||||
|
@ -47,19 +49,21 @@
|
|||
<XWidgets v-if="widgetsShowing" class="tray"/>
|
||||
</transition>
|
||||
|
||||
<StreamIndicator/>
|
||||
<XCommon/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, defineAsyncComponent } from 'vue';
|
||||
import { defineComponent, defineAsyncComponent, markRaw } from 'vue';
|
||||
import { faLayerGroup, faBars, faHome, faCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faBell } from '@fortawesome/free-regular-svg-icons';
|
||||
import { host } from '@/config';
|
||||
import { search } from '@/scripts/search';
|
||||
import { StickySidebar } from '@/scripts/sticky-sidebar';
|
||||
import XSidebar from '@/components/sidebar.vue';
|
||||
import XCommon from './_common_/common.vue';
|
||||
import XHeader from './_common_/header.vue';
|
||||
import XSide from './default.side.vue';
|
||||
import * as os from '@/os';
|
||||
import { sidebarDef } from '@/sidebar';
|
||||
|
||||
|
@ -67,9 +71,19 @@ const DESKTOP_THRESHOLD = 1100;
|
|||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XCommon,
|
||||
XSidebar,
|
||||
XHeader,
|
||||
XWidgets: defineAsyncComponent(() => import('./default.widgets.vue')),
|
||||
XSide, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる
|
||||
},
|
||||
|
||||
provide() {
|
||||
return {
|
||||
sideViewHook: (url) => {
|
||||
this.$refs.side.navigate(url);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
data() {
|
||||
|
@ -245,7 +259,7 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
.mk-app {
|
||||
$header-height: 60px;
|
||||
$header-height: 58px; // TODO: どこかに集約したい
|
||||
$ui-font-size: 1em; // TODO: どこかに集約したい
|
||||
$widgets-hide-threshold: 1090px;
|
||||
|
||||
|
@ -301,6 +315,12 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
|
||||
> .side {
|
||||
min-width: 370px;
|
||||
max-width: 370px;
|
||||
border-left: solid 1px var(--divider);
|
||||
}
|
||||
|
||||
> .widgets {
|
||||
padding: 0 var(--margin);
|
||||
border-left: solid 1px var(--divider);
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<div class="mk-app">
|
||||
<header>
|
||||
<router-link class="link" to="/">{{ $t('home') }}</router-link>
|
||||
<router-link class="link" to="/announcements">{{ $t('announcements') }}</router-link>
|
||||
<router-link class="link" to="/channels">{{ $t('channel') }}</router-link>
|
||||
<router-link class="link" to="/about">{{ $t('aboutX', { x: instanceName || host }) }}</router-link>
|
||||
<MkA class="link" to="/">{{ $t('home') }}</MkA>
|
||||
<MkA class="link" to="/announcements">{{ $t('announcements') }}</MkA>
|
||||
<MkA class="link" to="/channels">{{ $t('channel') }}</MkA>
|
||||
<MkA class="link" to="/about">{{ $t('aboutX', { x: instanceName || host }) }}</MkA>
|
||||
</header>
|
||||
|
||||
<div class="banner" :style="{ backgroundImage: `url(${ $store.state.instance.meta.bannerUrl })` }">
|
||||
|
@ -23,12 +23,12 @@
|
|||
</router-view>
|
||||
</main>
|
||||
<div class="powered-by">
|
||||
<b><router-link to="/">{{ host }}</router-link></b>
|
||||
<b><MkA to="/">{{ host }}</MkA></b>
|
||||
<small>Powered by <a href="https://github.com/syuilo/misskey" target="_blank">Misskey</a></small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<StreamIndicator v-if="$store.getters.isSignedIn"/>
|
||||
<XCommon/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -39,12 +39,14 @@ import { host, instanceName } from '@/config';
|
|||
import { search } from '@/scripts/search';
|
||||
import * as os from '@/os';
|
||||
import XHeader from './_common_/header.vue';
|
||||
import XCommon from './_common_/common.vue';
|
||||
|
||||
const DESKTOP_THRESHOLD = 1100;
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XHeader
|
||||
XCommon,
|
||||
XHeader,
|
||||
},
|
||||
|
||||
data() {
|
||||
|
@ -130,7 +132,7 @@ export default defineComponent({
|
|||
line-height: 60px;
|
||||
padding: 0 0.7em;
|
||||
|
||||
&.router-link-active {
|
||||
&.MkA-active {
|
||||
box-shadow: 0 -2px 0 0 var(--accent) inset;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
</main>
|
||||
</div>
|
||||
|
||||
<StreamIndicator/>
|
||||
<XCommon/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -28,10 +28,12 @@ import { faBell } from '@fortawesome/free-regular-svg-icons';
|
|||
import { host } from '@/config';
|
||||
import { search } from '@/scripts/search';
|
||||
import XHeader from './_common_/header.vue';
|
||||
import XCommon from './_common_/common.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XCommon,
|
||||
XHeader,
|
||||
},
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<transition-group tag="div" name="chart" class="tags" v-else>
|
||||
<div v-for="stat in stats" :key="stat.tag">
|
||||
<div class="tag">
|
||||
<router-link class="a" :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</router-link>
|
||||
<MkA class="a" :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</MkA>
|
||||
<p>{{ $t('nUsersMentioned', { n: stat.usersCount }) }}</p>
|
||||
</div>
|
||||
<MkMiniChart class="chart" :src="stat.chart"/>
|
||||
|
|
Loading…
Reference in a new issue