diff options
| author | syuilo <syuilotan@yahoo.co.jp> | 2020-10-25 01:23:41 +0900 |
|---|---|---|
| committer | syuilo <syuilotan@yahoo.co.jp> | 2020-10-25 01:23:41 +0900 |
| commit | 7bd2a6ad611ffa4e4a0c96e49dec93350e0616db (patch) | |
| tree | 1a3ad6ba33e7bfd6fd884052f61805dc4d3aa365 /src/client/ui | |
| parent | regedit (diff) | |
| parent | 自前ルーティング (#6759) (diff) | |
| download | misskey-7bd2a6ad611ffa4e4a0c96e49dec93350e0616db.tar.gz misskey-7bd2a6ad611ffa4e4a0c96e49dec93350e0616db.tar.bz2 misskey-7bd2a6ad611ffa4e4a0c96e49dec93350e0616db.zip | |
Merge branch 'develop' of https://github.com/syuilo/misskey into develop
Diffstat (limited to 'src/client/ui')
| -rw-r--r-- | src/client/ui/_common_/common.vue | 66 | ||||
| -rw-r--r-- | src/client/ui/_common_/stream-indicator.vue | 70 | ||||
| -rw-r--r-- | src/client/ui/_common_/upload.vue | 136 | ||||
| -rw-r--r-- | src/client/ui/deck.vue | 4 | ||||
| -rw-r--r-- | src/client/ui/default.side.vue | 157 | ||||
| -rw-r--r-- | src/client/ui/default.vue | 26 | ||||
| -rw-r--r-- | src/client/ui/visitor.vue | 18 | ||||
| -rw-r--r-- | src/client/ui/zen.vue | 4 |
8 files changed, 468 insertions, 13 deletions
diff --git a/src/client/ui/_common_/common.vue b/src/client/ui/_common_/common.vue new file mode 100644 index 0000000000..dea3e30a91 --- /dev/null +++ b/src/client/ui/_common_/common.vue @@ -0,0 +1,66 @@ +<template> +<component v-for="popup in popups" + :key="popup.id" + :is="popup.component" + v-bind="popup.props" + v-on="popup.events" +/> + +<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 { popups, uploads, pendingApiRequestsCount } from '@/os'; + +export default defineComponent({ + components: { + XStreamIndicator: defineAsyncComponent(() => import('./stream-indicator.vue')), + XUpload: defineAsyncComponent(() => import('./upload.vue')), + }, + + setup() { + return { + uploads, + popups, + pendingApiRequestsCount, + }; + }, +}); +</script> + +<style lang="scss"> +#wait { + display: block; + position: fixed; + z-index: 10000; + top: 15px; + right: 15px; + + &:before { + content: ""; + display: block; + width: 18px; + height: 18px; + box-sizing: border-box; + border: solid 2px transparent; + border-top-color: var(--accent); + border-left-color: var(--accent); + border-radius: 50%; + animation: progress-spinner 400ms linear infinite; + } +} + +@keyframes progress-spinner { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} +</style> diff --git a/src/client/ui/_common_/stream-indicator.vue b/src/client/ui/_common_/stream-indicator.vue new file mode 100644 index 0000000000..7b020171a4 --- /dev/null +++ b/src/client/ui/_common_/stream-indicator.vue @@ -0,0 +1,70 @@ +<template> +<div class="nsbbhtug" v-if="hasDisconnected && $store.state.device.serverDisconnectedBehavior === 'quiet'" @click="resetDisconnected"> + <div>{{ $t('disconnectedFromServer') }}</div> + <div class="command"> + <button class="_textButton" @click="reload">{{ $t('reload') }}</button> + <button class="_textButton">{{ $t('doNothing') }}</button> + </div> +</div> +</template> + +<script lang="ts"> +import { defineComponent } from 'vue'; +import * as os from '@/os'; + +export default defineComponent({ + data() { + return { + hasDisconnected: false, + } + }, + computed: { + stream() { + return os.stream; + }, + }, + created() { + os.stream.on('_disconnected_', this.onDisconnected); + }, + beforeUnmount() { + os.stream.off('_disconnected_', this.onDisconnected); + }, + methods: { + onDisconnected() { + this.hasDisconnected = true; + }, + resetDisconnected() { + this.hasDisconnected = false; + }, + reload() { + location.reload(); + }, + } +}); +</script> + +<style lang="scss" scoped> +.nsbbhtug { + position: fixed; + z-index: 16385; + bottom: 8px; + right: 8px; + margin: 0; + padding: 6px 12px; + font-size: 0.9em; + color: #fff; + background: #000; + opacity: 0.8; + border-radius: 4px; + max-width: 320px; + + > .command { + display: flex; + justify-content: space-around; + + > button { + padding: 0.7em; + } + } +} +</style> diff --git a/src/client/ui/_common_/upload.vue b/src/client/ui/_common_/upload.vue new file mode 100644 index 0000000000..2ba2186f57 --- /dev/null +++ b/src/client/ui/_common_/upload.vue @@ -0,0 +1,136 @@ +<template> +<div class="mk-uploader _acrylic"> + <ol v-if="uploads.length > 0"> + <li v-for="ctx in uploads" :key="ctx.id"> + <div class="img" :style="{ backgroundImage: `url(${ ctx.img })` }"></div> + <div class="top"> + <p class="name"><Fa :icon="faSpinner" pulse/>{{ ctx.name }}</p> + <p class="status"> + <span class="initing" v-if="ctx.progressValue === undefined">{{ $t('waiting') }}<MkEllipsis/></span> + <span class="kb" v-if="ctx.progressValue !== undefined">{{ String(Math.floor(ctx.progressValue / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i> / {{ String(Math.floor(ctx.progressMax / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i></span> + <span class="percentage" v-if="ctx.progressValue !== undefined">{{ Math.floor((ctx.progressValue / ctx.progressMax) * 100) }}</span> + </p> + </div> + <progress :value="ctx.progressValue || 0" :max="ctx.progressMax || 0" :class="{ initing: ctx.progressValue === undefined, waiting: ctx.progressValue !== undefined && ctx.progressValue === ctx.progressMax }"></progress> + </li> + </ol> +</div> +</template> + +<script lang="ts"> +import { defineComponent } from 'vue'; +import { faSpinner } from '@fortawesome/free-solid-svg-icons'; +import * as os from '@/os'; + +export default defineComponent({ + data() { + return { + uploads: os.uploads, + faSpinner + }; + }, +}); +</script> + +<style lang="scss" scoped> +.mk-uploader { + position: fixed; + z-index: 10000; + right: 16px; + width: 260px; + top: 32px; + padding: 16px 20px; + pointer-events: none; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); + border-radius: 8px; +} +.mk-uploader:empty { + display: none; +} +.mk-uploader > ol { + display: block; + margin: 0; + padding: 0; + list-style: none; +} +.mk-uploader > ol > li { + display: grid; + margin: 8px 0 0 0; + padding: 0; + height: 36px; + width: 100%; + border-top: solid 8px transparent; + grid-template-columns: 36px calc(100% - 44px); + grid-template-rows: 1fr 8px; + column-gap: 8px; + box-sizing: content-box; +} +.mk-uploader > ol > li:first-child { + margin: 0; + box-shadow: none; + border-top: none; +} +.mk-uploader > ol > li > .img { + display: block; + background-size: cover; + background-position: center center; + grid-column: 1/2; + grid-row: 1/3; +} +.mk-uploader > ol > li > .top { + display: flex; + grid-column: 2/3; + grid-row: 1/2; +} +.mk-uploader > ol > li > .top > .name { + display: block; + padding: 0 8px 0 0; + margin: 0; + font-size: 0.8em; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + flex-shrink: 1; +} +.mk-uploader > ol > li > .top > .name > [data-icon] { + margin-right: 4px; +} +.mk-uploader > ol > li > .top > .status { + display: block; + margin: 0 0 0 auto; + padding: 0; + font-size: 0.8em; + flex-shrink: 0; +} +.mk-uploader > ol > li > .top > .status > .initing { +} +.mk-uploader > ol > li > .top > .status > .kb { +} +.mk-uploader > ol > li > .top > .status > .percentage { + display: inline-block; + width: 48px; + text-align: right; +} +.mk-uploader > ol > li > .top > .status > .percentage:after { + content: '%'; +} +.mk-uploader > ol > li > progress { + display: block; + background: transparent; + border: none; + border-radius: 4px; + overflow: hidden; + grid-column: 2/3; + grid-row: 2/3; + z-index: 2; + width: 100%; + height: 8px; +} +.mk-uploader > ol > li > progress::-webkit-progress-value { + background: var(--accent); +} +.mk-uploader > ol > li > progress::-webkit-progress-bar { + //background: var(--accentAlpha01); + background: transparent; +} +</style> 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, }, |