From 254cfaea284d12f188e28f56a0cec863e3177a49 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 25 Oct 2020 01:21:41 +0900 Subject: [PATCH] =?UTF-8?q?=E8=87=AA=E5=89=8D=E3=83=AB=E3=83=BC=E3=83=86?= =?UTF-8?q?=E3=82=A3=E3=83=B3=E3=82=B0=20(#6759)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip --- locales/ja-JP.yml | 3 + src/client/components/avatar.vue | 4 +- src/client/components/channel-preview.vue | 4 +- src/client/components/index.ts | 4 +- src/client/components/link.vue | 2 +- src/client/components/mention.vue | 4 +- src/client/components/mfm.ts | 4 +- src/client/components/note-header.vue | 8 +- src/client/components/note.vue | 19 +-- src/client/components/notification.vue | 26 +-- src/client/components/page-preview.vue | 4 +- src/client/components/page-window.vue | 80 +++++++-- src/client/components/sidebar.vue | 10 +- src/client/components/sub-note-content.vue | 4 +- src/client/components/ui/a.vue | 104 ++++++++++++ src/client/components/ui/context-menu.vue | 2 +- src/client/components/ui/menu.vue | 4 +- src/client/components/ui/radio.vue | 2 +- src/client/components/ui/window.vue | 27 ++- src/client/components/url-preview.vue | 2 +- src/client/components/url.vue | 2 +- src/client/components/user-info.vue | 2 +- src/client/components/user-preview.vue | 2 +- src/client/components/users-dialog.vue | 4 +- src/client/init.ts | 12 +- src/client/os.ts | 4 +- src/client/pages/docs.vue | 2 +- src/client/pages/explore.vue | 4 +- src/client/pages/follow-requests.vue | 2 +- src/client/pages/messaging/index.vue | 17 +- src/client/pages/messaging/messaging-room.vue | 2 +- src/client/pages/my-groups/index.vue | 2 +- src/client/pages/my-lists/index.vue | 2 +- src/client/pages/note.vue | 10 +- src/client/pages/page-editor/page-editor.vue | 2 +- src/client/pages/page.vue | 4 +- src/client/pages/settings/general.vue | 18 ++ src/client/pages/settings/index.vue | 73 +++++--- src/client/pages/settings/mute-block.vue | 8 +- src/client/pages/settings/theme.vue | 2 +- src/client/pages/tag.vue | 13 +- src/client/pages/test.vue | 2 +- src/client/pages/timeline.tutorial.vue | 8 +- src/client/pages/user/follow-list.vue | 11 +- src/client/pages/user/index.photos.vue | 4 +- src/client/pages/user/index.vue | 40 +++-- src/client/router.ts | 39 ++--- src/client/scripts/get-user-menu.ts | 3 +- src/client/sidebar.ts | 4 +- src/client/store.ts | 2 + .../{root.vue => ui/_common_/common.vue} | 17 +- .../_common_}/stream-indicator.vue | 0 .../{components => ui/_common_}/upload.vue | 0 src/client/ui/deck.vue | 4 +- src/client/ui/default.side.vue | 157 ++++++++++++++++++ src/client/ui/default.vue | 26 ++- src/client/ui/visitor.vue | 18 +- src/client/ui/zen.vue | 4 +- src/client/widgets/trends.vue | 2 +- 59 files changed, 625 insertions(+), 220 deletions(-) create mode 100644 src/client/components/ui/a.vue rename src/client/{root.vue => ui/_common_/common.vue} (65%) rename src/client/{components => ui/_common_}/stream-indicator.vue (100%) rename src/client/{components => ui/_common_}/upload.vue (100%) create mode 100644 src/client/ui/default.side.vue diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index bccb82e51b..ae3110a7d0 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -593,6 +593,9 @@ fillAbuseReportDescription: "通報理由の詳細を記入してください。 abuseReported: "内容が送信されました。ご報告ありがとうございました。" send: "送信" abuseMarkAsResolved: "対応済みにする" +openInNewTab: "新しいタブで開く" +openInSideView: "サイドビューで開く" +defaultNavigationBehaviour: "デフォルトのナビゲーション" _serverDisconnectedBehavior: reload: "自動でリロード" diff --git a/src/client/components/avatar.vue b/src/client/components/avatar.vue index 627818a8e7..d90607bb8a 100644 --- a/src/client/components/avatar.vue +++ b/src/client/components/avatar.vue @@ -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"> diff --git a/src/client/components/channel-preview.vue b/src/client/components/channel-preview.vue index 705d3b09c4..e5676e5ae9 100644 --- a/src/client/components/channel-preview.vue +++ b/src/client/components/channel-preview.vue @@ -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"> diff --git a/src/client/components/index.ts b/src/client/components/index.ts index 6cc06e37c3..92a29ded15 100644 --- a/src/client/components/index.ts +++ b/src/client/components/index.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); } diff --git a/src/client/components/link.vue b/src/client/components/link.vue index e0a7f43477..bac49a62ef 100644 --- a/src/client/components/link.vue +++ b/src/client/components/link.vue @@ -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" diff --git a/src/client/components/mention.vue b/src/client/components/mention.vue index 50b43df07b..85f8436a42 100644 --- a/src/client/components/mention.vue +++ b/src/client/components/mention.vue @@ -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> diff --git a/src/client/components/mfm.ts b/src/client/components/mfm.ts index 791fd1b4e5..7a8ee8b19f 100644 --- a/src/client/components/mfm.ts +++ b/src/client/components/mfm.ts @@ -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);' diff --git a/src/client/components/note-header.vue b/src/client/components/note-header.vue index 3be0ba38fe..1f7a07bac3 100644 --- a/src/client/components/note-header.vue +++ b/src/client/components/note-header.vue @@ -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"/> diff --git a/src/client/components/note.vue b/src/client/components/note.vue index 85bdb9c6fb..8ddb01f733 100644 --- a/src/client/components/note.vue +++ b/src/client/components/note.vue @@ -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 diff --git a/src/client/components/notification.vue b/src/client/components/notification.vue index ab890bbf0f..db6d8ad167 100644 --- a/src/client/components/notification.vue +++ b/src/client/components/notification.vue @@ -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> diff --git a/src/client/components/page-preview.vue b/src/client/components/page-preview.vue index ad1069f53f..95ed8d0e38 100644 --- a/src/client/components/page-preview.vue +++ b/src/client/components/page-preview.vue @@ -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"> diff --git a/src/client/components/page-window.vue b/src/client/components/page-window.vue index 2673b3f8ec..c3ec7db867 100644 --- a/src/client/components/page-window.vue +++ b/src/client/components/page-window.vue @@ -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(); diff --git a/src/client/components/sidebar.vue b/src/client/components/sidebar.vue index 383378241b..3ceb1f9b8d 100644 --- a/src/client/components/sidebar.vue +++ b/src/client/components/sidebar.vue @@ -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> diff --git a/src/client/components/sub-note-content.vue b/src/client/components/sub-note-content.vue index 0bef072fe4..cb65a76495 100644 --- a/src/client/components/sub-note-content.vue +++ b/src/client/components/sub-note-content.vue @@ -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> diff --git a/src/client/components/ui/a.vue b/src/client/components/ui/a.vue new file mode 100644 index 0000000000..dce99ef676 --- /dev/null +++ b/src/client/components/ui/a.vue @@ -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> diff --git a/src/client/components/ui/context-menu.vue b/src/client/components/ui/context-menu.vue index 98586cf3fe..3a11589e8a 100644 --- a/src/client/components/ui/context-menu.vue +++ b/src/client/components/ui/context-menu.vue @@ -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> diff --git a/src/client/components/ui/menu.vue b/src/client/components/ui/menu.vue index 5e74828c20..9e4e319c8a 100644 --- a/src/client/components/ui/menu.vue +++ b/src/client/components/ui/menu.vue @@ -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> diff --git a/src/client/components/ui/radio.vue b/src/client/components/ui/radio.vue index 8f2b843ee6..890ff08751 100644 --- a/src/client/components/ui/radio.vue +++ b/src/client/components/ui/radio.vue @@ -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; diff --git a/src/client/components/ui/window.vue b/src/client/components/ui/window.vue index d545ac4827..4c90ab9c8d 100644 --- a/src/client/components/ui/window.vue +++ b/src/client/components/ui/window.vue @@ -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; diff --git a/src/client/components/url-preview.vue b/src/client/components/url-preview.vue index df02698b5d..55872113be 100644 --- a/src/client/components/url-preview.vue +++ b/src/client/components/url-preview.vue @@ -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> diff --git a/src/client/components/url.vue b/src/client/components/url.vue index 649ce5fa24..ceb0381f87 100644 --- a/src/client/components/url.vue +++ b/src/client/components/url.vue @@ -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" > diff --git a/src/client/components/user-info.vue b/src/client/components/user-info.vue index 893747b7c4..09736b1a2c 100644 --- a/src/client/components/user-info.vue +++ b/src/client/components/user-info.vue @@ -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"> diff --git a/src/client/components/user-preview.vue b/src/client/components/user-preview.vue index d1a11dc790..d258489860 100644 --- a/src/client/components/user-preview.vue +++ b/src/client/components/user-preview.vue @@ -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"> diff --git a/src/client/components/users-dialog.vue b/src/client/components/users-dialog.vue index c8ca93703d..f2e8ec480e 100644 --- a/src/client/components/users-dialog.vue +++ b/src/client/components/users-dialog.vue @@ -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> diff --git a/src/client/init.ts b/src/client/init.ts index 4a08f09997..86991b69e3 100644 --- a/src/client/init.ts +++ b/src/client/init.ts @@ -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; diff --git a/src/client/os.ts b/src/client/os.ts index 3241f82e5d..daff26efa2 100644 --- a/src/client/os.ts +++ b/src/client/os.ts @@ -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), diff --git a/src/client/pages/docs.vue b/src/client/pages/docs.vue index ea3e16df95..245dff6b57 100644 --- a/src/client/pages/docs.vue +++ b/src/client/pages/docs.vue @@ -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> diff --git a/src/client/pages/explore.vue b/src/client/pages/explore.vue index cf191a7481..c7378e0ddc 100644 --- a/src/client/pages/explore.vue +++ b/src/client/pages/explore.vue @@ -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> diff --git a/src/client/pages/follow-requests.vue b/src/client/pages/follow-requests.vue index 86e409ebbd..9f67a8a9e5 100644 --- a/src/client/pages/follow-requests.vue +++ b/src/client/pages/follow-requests.vue @@ -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"> diff --git a/src/client/pages/messaging/index.vue b/src/client/pages/messaging/index.vue index 07b1cbab83..f62a33b866 100644 --- a/src/client/pages/messaging/index.vue +++ b/src/client/pages/messaging/index.vue @@ -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) { diff --git a/src/client/pages/messaging/messaging-room.vue b/src/client/pages/messaging/messaging-room.vue index 4210b8cf89..4aca8bdabf 100644 --- a/src/client/pages/messaging/messaging-room.vue +++ b/src/client/pages/messaging/messaging-room.vue @@ -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 : { diff --git a/src/client/pages/my-groups/index.vue b/src/client/pages/my-groups/index.vue index f05226faaf..e384dfc363 100644 --- a/src/client/pages/my-groups/index.vue +++ b/src/client/pages/my-groups/index.vue @@ -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> diff --git a/src/client/pages/my-lists/index.vue b/src/client/pages/my-lists/index.vue index 5e29436ede..9d0e192286 100644 --- a/src/client/pages/my-lists/index.vue +++ b/src/client/pages/my-lists/index.vue @@ -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> diff --git a/src/client/pages/note.vue b/src/client/pages/note.vue index cd31ccc338..a458d6c063 100644 --- a/src/client/pages/note.vue +++ b/src/client/pages/note.vue @@ -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', { diff --git a/src/client/pages/page-editor/page-editor.vue b/src/client/pages/page-editor/page-editor.vue index 363f46c34b..cd033219f0 100644 --- a/src/client/pages/page-editor/page-editor.vue +++ b/src/client/pages/page-editor/page-editor.vue @@ -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> diff --git a/src/client/pages/page.vue b/src/client/pages/page.vue index eb470fdc19..e8a8a6bdfd 100644 --- a/src/client/pages/page.vue +++ b/src/client/pages/page.vue @@ -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> diff --git a/src/client/pages/settings/general.vue b/src/client/pages/settings/general.vue index 80152c5e6a..d61d8620e7 100644 --- a/src/client/pages/settings/general.vue +++ b/src/client/pages/settings/general.vue @@ -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 }); } diff --git a/src/client/pages/settings/index.vue b/src/client/pages/settings/index.vue index 4ca30ee686..1bffa9c0af 100644 --- a/src/client/pages/settings/index.vue +++ b/src/client/pages/settings/index.vue @@ -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; } diff --git a/src/client/pages/settings/mute-block.vue b/src/client/pages/settings/mute-block.vue index 5a08a8caae..87f5b88d3c 100644 --- a/src/client/pages/settings/mute-block.vue +++ b/src/client/pages/settings/mute-block.vue @@ -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> diff --git a/src/client/pages/settings/theme.vue b/src/client/pages/settings/theme.vue index 0571b6c5d1..866790bd26 100644 --- a/src/client/pages/settings/theme.vue +++ b/src/client/pages/settings/theme.vue @@ -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> diff --git a/src/client/pages/tag.vue b/src/client/pages/tag.vue index cea74d1e17..bbaf5b81ca 100644 --- a/src/client/pages/tag.vue +++ b/src/client/pages/tag.vue @@ -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(); } }, diff --git a/src/client/pages/test.vue b/src/client/pages/test.vue index b053b859bb..5a3929d630 100644 --- a/src/client/pages/test.vue +++ b/src/client/pages/test.vue @@ -229,7 +229,7 @@ export default defineComponent({ }, messagingWindowOpen() { - os.pageWindow('/my/messaging', defineAsyncComponent(() => import('@/pages/messaging/index.vue'))); + os.pageWindow('/my/messaging'); }, openWaitingDialog(text?) { diff --git a/src/client/pages/timeline.tutorial.vue b/src/client/pages/timeline.tutorial.vue index 506e97e1b5..837915229e 100644 --- a/src/client/pages/timeline.tutorial.vue +++ b/src/client/pages/timeline.tutorial.vue @@ -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> diff --git a/src/client/pages/user/follow-list.vue b/src/client/pages/user/follow-list.vue index 411109c890..6761210ff6 100644 --- a/src/client/pages/user/follow-list.vue +++ b/src/client/pages/user/follow-list.vue @@ -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(); } }, diff --git a/src/client/pages/user/index.photos.vue b/src/client/pages/user/index.photos.vue index dcd4d1fce8..aabcbebe8a 100644 --- a/src/client/pages/user/index.photos.vue +++ b/src/client/pages/user/index.photos.vue @@ -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> diff --git a/src/client/pages/user/index.vue b/src/client/pages/user/index.vue index 94940f6ef2..01f0deac49 100644 --- a/src/client/pages/user/index.vue +++ b/src/client/pages/user/index.vue @@ -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; diff --git a/src/client/router.ts b/src/client/router.ts index c9c7a32835..ef540f0d4b 100644 --- a/src/client/router.ts +++ b/src/client/router.ts @@ -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 + }; +} diff --git a/src/client/scripts/get-user-menu.ts b/src/client/scripts/get-user-menu.ts index cace2e1425..72ae9c1e7b 100644 --- a/src/client/scripts/get-user-menu.ts +++ b/src/client/scripts/get-user-menu.ts @@ -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; } } diff --git a/src/client/sidebar.ts b/src/client/sidebar.ts index b8a2b8a7c3..e57f85020d 100644 --- a/src/client/sidebar.ts +++ b/src/client/sidebar.ts @@ -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; } } diff --git a/src/client/store.ts b/src/client/store.ts index f1ad23e1f5..e627ed9b55 100644 --- a/src/client/store.ts +++ b/src/client/store.ts @@ -70,6 +70,8 @@ export const defaultDeviceSettings = { animatedMfm: true, imageNewTab: false, chatOpenBehavior: 'page', + defaultSideView: false, + deckNavWindow: true, showFixedPostForm: false, disablePagesScript: false, enableInfiniteScroll: true, diff --git a/src/client/root.vue b/src/client/ui/_common_/common.vue similarity index 65% rename from src/client/root.vue rename to src/client/ui/_common_/common.vue index 0bca5cbe8c..dea3e30a91 100644 --- a/src/client/root.vue +++ b/src/client/ui/_common_/common.vue @@ -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, diff --git a/src/client/components/stream-indicator.vue b/src/client/ui/_common_/stream-indicator.vue similarity index 100% rename from src/client/components/stream-indicator.vue rename to src/client/ui/_common_/stream-indicator.vue diff --git a/src/client/components/upload.vue b/src/client/ui/_common_/upload.vue similarity index 100% rename from src/client/components/upload.vue rename to src/client/ui/_common_/upload.vue diff --git a/src/client/ui/deck.vue b/src/client/ui/deck.vue index b067b948ce..2a5008dc56 100644 --- a/src/client/ui/deck.vue +++ b/src/client/ui/deck.vue @@ -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, diff --git a/src/client/ui/default.side.vue b/src/client/ui/default.side.vue new file mode 100644 index 0000000000..cff35f6ed3 --- /dev/null +++ b/src/client/ui/default.side.vue @@ -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> + diff --git a/src/client/ui/default.vue b/src/client/ui/default.vue index 754ed72c8d..b674186dbe 100644 --- a/src/client/ui/default.vue +++ b/src/client/ui/default.vue @@ -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); diff --git a/src/client/ui/visitor.vue b/src/client/ui/visitor.vue index fb21dc01d1..8b7dfd7911 100644 --- a/src/client/ui/visitor.vue +++ b/src/client/ui/visitor.vue @@ -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; } } diff --git a/src/client/ui/zen.vue b/src/client/ui/zen.vue index 0435f0f582..9c351f67e1 100644 --- a/src/client/ui/zen.vue +++ b/src/client/ui/zen.vue @@ -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, }, diff --git a/src/client/widgets/trends.vue b/src/client/widgets/trends.vue index 17262445ef..9510bf205c 100644 --- a/src/client/widgets/trends.vue +++ b/src/client/widgets/trends.vue @@ -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"/>