diff --git a/src/web/app/desktop/-tags/donation.tag b/src/web/app/desktop/-tags/donation.tag deleted file mode 100644 index fe446f2e61..0000000000 --- a/src/web/app/desktop/-tags/donation.tag +++ /dev/null @@ -1,66 +0,0 @@ -<mk-donation> - <button class="close" @click="close">閉じる x</button> - <div class="message"> - <p>利用者の皆さま、</p> - <p> - 今日は、日本の皆さまにお知らせがあります。 - Misskeyの援助をお願いいたします。 - 私は独立性を守るため、一切の広告を掲載いたしません。 - 平均で約¥1,500の寄付をいただき、運営しております。 - 援助をしてくださる利用者はほんの少数です。 - お願いいたします。 - 今日、利用者の皆さまが¥300ご援助くだされば、募金活動を一時間で終了することができます。 - コーヒー1杯ほどの金額です。 - Misskeyを活用しておられるのでしたら、広告を掲載せずにもう1年活動できるよう、どうか1分だけお時間をください。 - 私は小さな非営利個人ですが、サーバー、プログラム、人件費など、世界でトップクラスのウェブサイト同等のコストがかかります。 - 利用者は何億人といますが、他の大きなサイトに比べてほんの少額の費用で運営しているのです。 - 人間の可能性、自由、そして機会。知識こそ、これらの基盤を成すものです。 - 私は、誰もが無料かつ制限なく知識に触れられるべきだと信じています。 - 募金活動を終了し、Misskeyの改善に戻れるようご援助ください。 - よろしくお願いいたします。 - </p> - </div> - <style lang="stylus" scoped> - :scope - display block - color #fff - background #03072C - - > .close - position absolute - top 16px - right 16px - z-index 1 - - > .message - padding 32px - font-size 1.4em - font-family serif - - > p - display block - margin 0 auto - max-width 1200px - - > p:first-child - margin-bottom 16px - - </style> - <script lang="typescript"> - this.mixin('i'); - this.mixin('api'); - - this.close = e => { - e.preventDefault(); - e.stopPropagation(); - - this.I.client_settings.show_donation = false; - this.I.update(); - this.api('i/update', { - show_donation: false - }); - - this.$destroy(); - }; - </script> -</mk-donation> diff --git a/src/web/app/desktop/-tags/ui.tag b/src/web/app/desktop/-tags/ui.tag deleted file mode 100644 index f8b7b3f4f0..0000000000 --- a/src/web/app/desktop/-tags/ui.tag +++ /dev/null @@ -1,859 +0,0 @@ - -<mk-ui-header> - <mk-donation v-if="SIGNIN && I.client_settings.show_donation"/> - <mk-special-message/> - <div class="main"> - <div class="backdrop"></div> - <div class="main"> - <div class="container"> - <div class="left"> - <mk-ui-header-nav page={ opts.page }/> - </div> - <div class="right"> - <mk-ui-header-search/> - <mk-ui-header-account v-if="SIGNIN"/> - <mk-ui-header-notifications v-if="SIGNIN"/> - <mk-ui-header-post-button v-if="SIGNIN"/> - <mk-ui-header-clock/> - </div> - </div> - </div> - </div> - <style lang="stylus" scoped> - :scope - display block - position -webkit-sticky - position sticky - top 0 - z-index 1024 - width 100% - box-shadow 0 1px 1px rgba(0, 0, 0, 0.075) - - > .main - - > .backdrop - position absolute - top 0 - z-index 1023 - width 100% - height 48px - backdrop-filter blur(12px) - background #f7f7f7 - - &:after - content "" - display block - width 100% - height 48px - background-image url(/assets/desktop/header-logo.svg) - background-size 46px - background-position center - background-repeat no-repeat - opacity 0.3 - - > .main - z-index 1024 - margin 0 - padding 0 - background-clip content-box - font-size 0.9rem - user-select none - - > .container - width 100% - max-width 1300px - margin 0 auto - - &:after - content "" - display block - clear both - - > .left - float left - height 3rem - - > .right - float right - height 48px - - @media (max-width 1100px) - > mk-ui-header-search - display none - - </style> - <script lang="typescript">this.mixin('i');</script> -</mk-ui-header> - -<mk-ui-header-search> - <form class="search" onsubmit={ onsubmit }> - %fa:search% - <input ref="q" type="search" placeholder="%i18n:desktop.tags.mk-ui-header-search.placeholder%"/> - <div class="result"></div> - </form> - <style lang="stylus" scoped> - :scope - - > form - display block - float left - - > [data-fa] - display block - position absolute - top 0 - left 0 - width 48px - text-align center - line-height 48px - color #9eaba8 - pointer-events none - - > * - vertical-align middle - - > input - user-select text - cursor auto - margin 8px 0 0 0 - padding 6px 18px 6px 36px - width 14em - height 32px - font-size 1em - background rgba(0, 0, 0, 0.05) - outline none - //border solid 1px #ddd - border none - border-radius 16px - transition color 0.5s ease, border 0.5s ease - font-family FontAwesome, sans-serif - - &::placeholder - color #9eaba8 - - &:hover - background rgba(0, 0, 0, 0.08) - - &:focus - box-shadow 0 0 0 2px rgba($theme-color, 0.5) !important - - </style> - <script lang="typescript"> - this.mixin('page'); - - this.onsubmit = e => { - e.preventDefault(); - this.page('/search?q=' + encodeURIComponent(this.$refs.q.value)); - }; - </script> -</mk-ui-header-search> - -<mk-ui-header-post-button> - <button @click="post" title="%i18n:desktop.tags.mk-ui-header-post-button.post%">%fa:pencil-alt%</button> - <style lang="stylus" scoped> - :scope - display inline-block - padding 8px - height 100% - vertical-align top - - > button - display inline-block - margin 0 - padding 0 10px - height 100% - font-size 1.2em - font-weight normal - text-decoration none - color $theme-color-foreground - background $theme-color !important - outline none - border none - border-radius 4px - transition background 0.1s ease - cursor pointer - - * - pointer-events none - - &:hover - background lighten($theme-color, 10%) !important - - &:active - background darken($theme-color, 10%) !important - transition background 0s ease - - </style> - <script lang="typescript"> - this.post = e => { - this.parent.parent.openPostForm(); - }; - </script> -</mk-ui-header-post-button> - -<mk-ui-header-notifications> - <button data-active={ isOpen } @click="toggle" title="%i18n:desktop.tags.mk-ui-header-notifications.title%"> - %fa:R bell%<template v-if="hasUnreadNotifications">%fa:circle%</template> - </button> - <div class="notifications" v-if="isOpen"> - <mk-notifications/> - </div> - <style lang="stylus" scoped> - :scope - display block - float left - - > button - display block - margin 0 - padding 0 - width 32px - color #9eaba8 - border none - background transparent - cursor pointer - - * - pointer-events none - - &:hover - &[data-active='true'] - color darken(#9eaba8, 20%) - - &:active - color darken(#9eaba8, 30%) - - > [data-fa].bell - font-size 1.2em - line-height 48px - - > [data-fa].circle - margin-left -5px - vertical-align super - font-size 10px - color $theme-color - - > .notifications - display block - position absolute - top 56px - right -72px - width 300px - background #fff - border-radius 4px - box-shadow 0 1px 4px rgba(0, 0, 0, 0.25) - - &:before - content "" - pointer-events none - display block - position absolute - top -28px - right 74px - border-top solid 14px transparent - border-right solid 14px transparent - border-bottom solid 14px rgba(0, 0, 0, 0.1) - border-left solid 14px transparent - - &:after - content "" - pointer-events none - display block - position absolute - top -27px - right 74px - border-top solid 14px transparent - border-right solid 14px transparent - border-bottom solid 14px #fff - border-left solid 14px transparent - - > mk-notifications - max-height 350px - font-size 1rem - overflow auto - - </style> - <script lang="typescript"> - import contains from '../../common/scripts/contains'; - - this.mixin('i'); - this.mixin('api'); - - if (this.SIGNIN) { - this.mixin('stream'); - this.connection = this.stream.getConnection(); - this.connectionId = this.stream.use(); - } - - this.isOpen = false; - - this.on('mount', () => { - if (this.SIGNIN) { - this.connection.on('read_all_notifications', this.onReadAllNotifications); - this.connection.on('unread_notification', this.onUnreadNotification); - - // Fetch count of unread notifications - this.api('notifications/get_unread_count').then(res => { - if (res.count > 0) { - this.update({ - hasUnreadNotifications: true - }); - } - }); - } - }); - - this.on('unmount', () => { - if (this.SIGNIN) { - this.connection.off('read_all_notifications', this.onReadAllNotifications); - this.connection.off('unread_notification', this.onUnreadNotification); - this.stream.dispose(this.connectionId); - } - }); - - this.onReadAllNotifications = () => { - this.update({ - hasUnreadNotifications: false - }); - }; - - this.onUnreadNotification = () => { - this.update({ - hasUnreadNotifications: true - }); - }; - - this.toggle = () => { - this.isOpen ? this.close() : this.open(); - }; - - this.open = () => { - this.update({ - isOpen: true - }); - document.querySelectorAll('body *').forEach(el => { - el.addEventListener('mousedown', this.mousedown); - }); - }; - - this.close = () => { - this.update({ - isOpen: false - }); - document.querySelectorAll('body *').forEach(el => { - el.removeEventListener('mousedown', this.mousedown); - }); - }; - - this.mousedown = e => { - e.preventDefault(); - if (!contains(this.root, e.target) && this.root != e.target) this.close(); - return false; - }; - </script> -</mk-ui-header-notifications> - -<mk-ui-header-nav> - <ul> - <template v-if="SIGNIN"> - <li class="home { active: page == 'home' }"> - <a href={ _URL_ }> - %fa:home% - <p>%i18n:desktop.tags.mk-ui-header-nav.home%</p> - </a> - </li> - <li class="messaging"> - <a @click="messaging"> - %fa:comments% - <p>%i18n:desktop.tags.mk-ui-header-nav.messaging%</p> - <template v-if="hasUnreadMessagingMessages">%fa:circle%</template> - </a> - </li> - </template> - <li class="ch"> - <a href={ _CH_URL_ } target="_blank"> - %fa:tv% - <p>%i18n:desktop.tags.mk-ui-header-nav.ch%</p> - </a> - </li> - <li class="info"> - <a href="https://twitter.com/misskey_xyz" target="_blank"> - %fa:info% - <p>%i18n:desktop.tags.mk-ui-header-nav.info%</p> - </a> - </li> - </ul> - <style lang="stylus" scoped> - :scope - display inline-block - margin 0 - padding 0 - line-height 3rem - vertical-align top - - > ul - display inline-block - margin 0 - padding 0 - vertical-align top - line-height 3rem - list-style none - - > li - display inline-block - vertical-align top - height 48px - line-height 48px - - &.active - > a - border-bottom solid 3px $theme-color - - > a - display inline-block - z-index 1 - height 100% - padding 0 24px - font-size 13px - font-variant small-caps - color #9eaba8 - text-decoration none - transition none - cursor pointer - - * - pointer-events none - - &:hover - color darken(#9eaba8, 20%) - text-decoration none - - > [data-fa]:first-child - margin-right 8px - - > [data-fa]:last-child - margin-left 5px - font-size 10px - color $theme-color - - @media (max-width 1100px) - margin-left -5px - - > p - display inline - margin 0 - - @media (max-width 1100px) - display none - - @media (max-width 700px) - padding 0 12px - - </style> - <script lang="typescript"> - this.mixin('i'); - this.mixin('api'); - - if (this.SIGNIN) { - this.mixin('stream'); - this.connection = this.stream.getConnection(); - this.connectionId = this.stream.use(); - } - - this.page = this.opts.page; - - this.on('mount', () => { - if (this.SIGNIN) { - this.connection.on('read_all_messaging_messages', this.onReadAllMessagingMessages); - this.connection.on('unread_messaging_message', this.onUnreadMessagingMessage); - - // Fetch count of unread messaging messages - this.api('messaging/unread').then(res => { - if (res.count > 0) { - this.update({ - hasUnreadMessagingMessages: true - }); - } - }); - } - }); - - this.on('unmount', () => { - if (this.SIGNIN) { - this.connection.off('read_all_messaging_messages', this.onReadAllMessagingMessages); - this.connection.off('unread_messaging_message', this.onUnreadMessagingMessage); - this.stream.dispose(this.connectionId); - } - }); - - this.onReadAllMessagingMessages = () => { - this.update({ - hasUnreadMessagingMessages: false - }); - }; - - this.onUnreadMessagingMessage = () => { - this.update({ - hasUnreadMessagingMessages: true - }); - }; - - this.messaging = () => { - riot.mount(document.body.appendChild(document.createElement('mk-messaging-window'))); - }; - </script> -</mk-ui-header-nav> - -<mk-ui-header-clock> - <div class="header"> - <time ref="time"> - <span class="yyyymmdd">{ yyyy }/{ mm }/{ dd }</span> - <br> - <span class="hhnn">{ hh }<span style="visibility:{ now.getSeconds() % 2 == 0 ? 'visible' : 'hidden' }">:</span>{ nn }</span> - </time> - </div> - <div class="content"> - <mk-analog-clock/> - </div> - <style lang="stylus" scoped> - :scope - display inline-block - overflow visible - - > .header - padding 0 12px - text-align center - font-size 10px - - &, * - cursor: default - - &:hover - background #899492 - - & + .content - visibility visible - - > time - color #fff !important - - * - color #fff !important - - &:after - content "" - display block - clear both - - > time - display table-cell - vertical-align middle - height 48px - color #9eaba8 - - > .yyyymmdd - opacity 0.7 - - > .content - visibility hidden - display block - position absolute - top auto - right 0 - z-index 3 - margin 0 - padding 0 - width 256px - background #899492 - - </style> - <script lang="typescript"> - this.now = new Date(); - - this.draw = () => { - const now = this.now = new Date(); - this.yyyy = now.getFullYear(); - this.mm = ('0' + (now.getMonth() + 1)).slice(-2); - this.dd = ('0' + now.getDate()).slice(-2); - this.hh = ('0' + now.getHours()).slice(-2); - this.nn = ('0' + now.getMinutes()).slice(-2); - this.update(); - }; - - this.on('mount', () => { - this.draw(); - this.clock = setInterval(this.draw, 1000); - }); - - this.on('unmount', () => { - clearInterval(this.clock); - }); - </script> -</mk-ui-header-clock> - -<mk-ui-header-account> - <button class="header" data-active={ isOpen.toString() } @click="toggle"> - <span class="username">{ I.username }<template v-if="!isOpen">%fa:angle-down%</template><template v-if="isOpen">%fa:angle-up%</template></span> - <img class="avatar" src={ I.avatar_url + '?thumbnail&size=64' } alt="avatar"/> - </button> - <div class="menu" v-if="isOpen"> - <ul> - <li> - <a href={ '/' + I.username }>%fa:user%%i18n:desktop.tags.mk-ui-header-account.profile%%fa:angle-right%</a> - </li> - <li @click="drive"> - <p>%fa:cloud%%i18n:desktop.tags.mk-ui-header-account.drive%%fa:angle-right%</p> - </li> - <li> - <a href="/i/mentions">%fa:at%%i18n:desktop.tags.mk-ui-header-account.mentions%%fa:angle-right%</a> - </li> - </ul> - <ul> - <li @click="settings"> - <p>%fa:cog%%i18n:desktop.tags.mk-ui-header-account.settings%%fa:angle-right%</p> - </li> - </ul> - <ul> - <li @click="signout"> - <p>%fa:power-off%%i18n:desktop.tags.mk-ui-header-account.signout%%fa:angle-right%</p> - </li> - </ul> - </div> - <style lang="stylus" scoped> - :scope - display block - float left - - > .header - display block - margin 0 - padding 0 - color #9eaba8 - border none - background transparent - cursor pointer - - * - pointer-events none - - &:hover - &[data-active='true'] - color darken(#9eaba8, 20%) - - > .avatar - filter saturate(150%) - - &:active - color darken(#9eaba8, 30%) - - > .username - display block - float left - margin 0 12px 0 16px - max-width 16em - line-height 48px - font-weight bold - font-family Meiryo, sans-serif - text-decoration none - - [data-fa] - margin-left 8px - - > .avatar - display block - float left - min-width 32px - max-width 32px - min-height 32px - max-height 32px - margin 8px 8px 8px 0 - border-radius 4px - transition filter 100ms ease - - > .menu - display block - position absolute - top 56px - right -2px - width 230px - font-size 0.8em - background #fff - border-radius 4px - box-shadow 0 1px 4px rgba(0, 0, 0, 0.25) - - &:before - content "" - pointer-events none - display block - position absolute - top -28px - right 12px - border-top solid 14px transparent - border-right solid 14px transparent - border-bottom solid 14px rgba(0, 0, 0, 0.1) - border-left solid 14px transparent - - &:after - content "" - pointer-events none - display block - position absolute - top -27px - right 12px - border-top solid 14px transparent - border-right solid 14px transparent - border-bottom solid 14px #fff - border-left solid 14px transparent - - ul - display block - margin 10px 0 - padding 0 - list-style none - - & + ul - padding-top 10px - border-top solid 1px #eee - - > li - display block - margin 0 - padding 0 - - > a - > p - display block - z-index 1 - padding 0 28px - margin 0 - line-height 40px - color #868C8C - cursor pointer - - * - pointer-events none - - > [data-fa]:first-of-type - margin-right 6px - - > [data-fa]:last-of-type - display block - position absolute - top 0 - right 8px - z-index 1 - padding 0 20px - font-size 1.2em - line-height 40px - - &:hover, &:active - text-decoration none - background $theme-color - color $theme-color-foreground - - </style> - <script lang="typescript"> - import contains from '../../common/scripts/contains'; - import signout from '../../common/scripts/signout'; - this.signout = signout; - - this.mixin('i'); - - this.isOpen = false; - - this.on('before-unmount', () => { - this.close(); - }); - - this.toggle = () => { - this.isOpen ? this.close() : this.open(); - }; - - this.open = () => { - this.update({ - isOpen: true - }); - document.querySelectorAll('body *').forEach(el => { - el.addEventListener('mousedown', this.mousedown); - }); - }; - - this.close = () => { - this.update({ - isOpen: false - }); - document.querySelectorAll('body *').forEach(el => { - el.removeEventListener('mousedown', this.mousedown); - }); - }; - - this.mousedown = e => { - e.preventDefault(); - if (!contains(this.root, e.target) && this.root != e.target) this.close(); - return false; - }; - - this.drive = () => { - this.close(); - riot.mount(document.body.appendChild(document.createElement('mk-drive-browser-window'))); - }; - - this.settings = () => { - this.close(); - riot.mount(document.body.appendChild(document.createElement('mk-settings-window'))); - }; - - </script> -</mk-ui-header-account> - -<mk-ui-notification> - <p>{ opts.message }</p> - <style lang="stylus" scoped> - :scope - display block - position fixed - z-index 10000 - top -128px - left 0 - right 0 - margin 0 auto - padding 128px 0 0 0 - width 500px - color rgba(#000, 0.6) - background rgba(#fff, 0.9) - border-radius 0 0 8px 8px - box-shadow 0 2px 4px rgba(#000, 0.2) - transform translateY(-64px) - opacity 0 - - > p - margin 0 - line-height 64px - text-align center - - </style> - <script lang="typescript"> - import anime from 'animejs'; - - this.on('mount', () => { - anime({ - targets: this.root, - opacity: 1, - translateY: [-64, 0], - easing: 'easeOutElastic', - duration: 500 - }); - - setTimeout(() => { - anime({ - targets: this.root, - opacity: 0, - translateY: -64, - duration: 500, - easing: 'easeInElastic', - complete: () => this.$destroy() - }); - }, 6000); - }); - </script> -</mk-ui-notification> diff --git a/src/web/app/desktop/views/components/ui-header-account.vue b/src/web/app/desktop/views/components/ui-header-account.vue new file mode 100644 index 0000000000..435a0dcaf2 --- /dev/null +++ b/src/web/app/desktop/views/components/ui-header-account.vue @@ -0,0 +1,210 @@ +<template> +<div class="mk-ui-header-account"> + <button class="header" :data-active="isOpen" @click="toggle"> + <span class="username">{{ $root.$data.os.i.username }}<template v-if="!isOpen">%fa:angle-down%</template><template v-if="isOpen">%fa:angle-up%</template></span> + <img class="avatar" :src="`${ $root.$data.os.i.avatar_url }?thumbnail&size=64`" alt="avatar"/> + </button> + <div class="menu" v-if="isOpen"> + <ul> + <li> + <a :href="`/${ $root.$data.os.i.username }`">%fa:user%%i18n:desktop.tags.mk-ui-header-account.profile%%fa:angle-right%</a> + </li> + <li @click="drive"> + <p>%fa:cloud%%i18n:desktop.tags.mk-ui-header-account.drive%%fa:angle-right%</p> + </li> + <li> + <a href="/i/mentions">%fa:at%%i18n:desktop.tags.mk-ui-header-account.mentions%%fa:angle-right%</a> + </li> + </ul> + <ul> + <li @click="settings"> + <p>%fa:cog%%i18n:desktop.tags.mk-ui-header-account.settings%%fa:angle-right%</p> + </li> + </ul> + <ul> + <li @click="signout"> + <p>%fa:power-off%%i18n:desktop.tags.mk-ui-header-account.signout%%fa:angle-right%</p> + </li> + </ul> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import contains from '../../../common/scripts/contains'; +import signout from '../../../common/scripts/signout'; + +export default Vue.extend({ + data() { + return { + isOpen: false, + signout + }; + }, + beforeDestroy() { + this.close(); + }, + methods: { + toggle() { + this.isOpen ? this.close() : this.open(); + }, + open() { + this.isOpen = true; + Array.from(document.querySelectorAll('body *')).forEach(el => { + el.addEventListener('mousedown', this.onMousedown); + }); + }, + close() { + this.isOpen = false; + Array.from(document.querySelectorAll('body *')).forEach(el => { + el.removeEventListener('mousedown', this.onMousedown); + }); + }, + onMousedown(e) { + e.preventDefault(); + if (!contains(this.$el, e.target) && this.$el != e.target) this.close(); + return false; + }, + drive() { + this.close(); + document.body.appendChild(new MkDriveWindow().$mount().$el); + }, + settings() { + this.close(); + document.body.appendChild(new MkSettingsWindow().$mount().$el); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-ui-header-account + > .header + display block + margin 0 + padding 0 + color #9eaba8 + border none + background transparent + cursor pointer + + * + pointer-events none + + &:hover + &[data-active='true'] + color darken(#9eaba8, 20%) + + > .avatar + filter saturate(150%) + + &:active + color darken(#9eaba8, 30%) + + > .username + display block + float left + margin 0 12px 0 16px + max-width 16em + line-height 48px + font-weight bold + font-family Meiryo, sans-serif + text-decoration none + + [data-fa] + margin-left 8px + + > .avatar + display block + float left + min-width 32px + max-width 32px + min-height 32px + max-height 32px + margin 8px 8px 8px 0 + border-radius 4px + transition filter 100ms ease + + > .menu + display block + position absolute + top 56px + right -2px + width 230px + font-size 0.8em + background #fff + border-radius 4px + box-shadow 0 1px 4px rgba(0, 0, 0, 0.25) + + &:before + content "" + pointer-events none + display block + position absolute + top -28px + right 12px + border-top solid 14px transparent + border-right solid 14px transparent + border-bottom solid 14px rgba(0, 0, 0, 0.1) + border-left solid 14px transparent + + &:after + content "" + pointer-events none + display block + position absolute + top -27px + right 12px + border-top solid 14px transparent + border-right solid 14px transparent + border-bottom solid 14px #fff + border-left solid 14px transparent + + ul + display block + margin 10px 0 + padding 0 + list-style none + + & + ul + padding-top 10px + border-top solid 1px #eee + + > li + display block + margin 0 + padding 0 + + > a + > p + display block + z-index 1 + padding 0 28px + margin 0 + line-height 40px + color #868C8C + cursor pointer + + * + pointer-events none + + > [data-fa]:first-of-type + margin-right 6px + + > [data-fa]:last-of-type + display block + position absolute + top 0 + right 8px + z-index 1 + padding 0 20px + font-size 1.2em + line-height 40px + + &:hover, &:active + text-decoration none + background $theme-color + color $theme-color-foreground + +</style> diff --git a/src/web/app/desktop/views/components/ui-header-clock.vue b/src/web/app/desktop/views/components/ui-header-clock.vue new file mode 100644 index 0000000000..cfed1e84a6 --- /dev/null +++ b/src/web/app/desktop/views/components/ui-header-clock.vue @@ -0,0 +1,109 @@ +<template> +<div class="mk-ui-header-clock"> + <div class="header"> + <time ref="time"> + <span class="yyyymmdd">{{ yyyy }}/{{ mm }}/{{ dd }}</span> + <br> + <span class="hhnn">{{ hh }}<span :style="{ visibility: now.getSeconds() % 2 == 0 ? 'visible' : 'hidden' }">:</span>{{ nn }}</span> + </time> + </div> + <div class="content"> + <mk-analog-clock/> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + data() { + return { + now: new Date(), + clock: null + }; + }, + computed: { + yyyy(): number { + return this.now.getFullYear(); + }, + mm(): string { + return ('0' + (this.now.getMonth() + 1)).slice(-2); + }, + dd(): string { + return ('0' + this.now.getDate()).slice(-2); + }, + hh(): string { + return ('0' + this.now.getHours()).slice(-2); + }, + nn(): string { + return ('0' + this.now.getMinutes()).slice(-2); + } + }, + mounted() { + this.tick(); + this.clock = setInterval(this.tick, 1000); + }, + beforeDestroy() { + clearInterval(this.clock); + }, + methods: { + tick() { + this.now = new Date(); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-ui-header-clock + display inline-block + overflow visible + + > .header + padding 0 12px + text-align center + font-size 10px + + &, * + cursor: default + + &:hover + background #899492 + + & + .content + visibility visible + + > time + color #fff !important + + * + color #fff !important + + &:after + content "" + display block + clear both + + > time + display table-cell + vertical-align middle + height 48px + color #9eaba8 + + > .yyyymmdd + opacity 0.7 + + > .content + visibility hidden + display block + position absolute + top auto + right 0 + z-index 3 + margin 0 + padding 0 + width 256px + background #899492 + +</style> diff --git a/src/web/app/desktop/views/components/ui-header-nav.vue b/src/web/app/desktop/views/components/ui-header-nav.vue new file mode 100644 index 0000000000..5295787b9f --- /dev/null +++ b/src/web/app/desktop/views/components/ui-header-nav.vue @@ -0,0 +1,151 @@ +<template> +<div class="mk-ui-header-nav"> + <ul> + <template v-if="$root.$data.os.isSignedIn"> + <li class="home" :class="{ active: page == 'home' }"> + <a href="/"> + %fa:home% + <p>%i18n:desktop.tags.mk-ui-header-nav.home%</p> + </a> + </li> + <li class="messaging"> + <a @click="messaging"> + %fa:comments% + <p>%i18n:desktop.tags.mk-ui-header-nav.messaging%</p> + <template v-if="hasUnreadMessagingMessages">%fa:circle%</template> + </a> + </li> + </template> + <li class="ch"> + <a :href="_CH_URL_" target="_blank"> + %fa:tv% + <p>%i18n:desktop.tags.mk-ui-header-nav.ch%</p> + </a> + </li> + <li class="info"> + <a href="https://twitter.com/misskey_xyz" target="_blank"> + %fa:info% + <p>%i18n:desktop.tags.mk-ui-header-nav.info%</p> + </a> + </li> + </ul> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + data() { + return { + hasUnreadMessagingMessages: false, + connection: null, + connectionId: null + }; + }, + mounted() { + if (this.$root.$data.os.isSignedIn) { + this.connection = this.$root.$data.os.stream.getConnection(); + this.connectionId = this.$root.$data.os.stream.use(); + + this.connection.on('read_all_messaging_messages', this.onReadAllMessagingMessages); + this.connection.on('unread_messaging_message', this.onUnreadMessagingMessage); + + // Fetch count of unread messaging messages + this.$root.$data.os.api('messaging/unread').then(res => { + if (res.count > 0) { + this.hasUnreadMessagingMessages = true; + } + }); + } + }, + beforeDestroy() { + if (this.$root.$data.os.isSignedIn) { + this.connection.off('read_all_messaging_messages', this.onReadAllMessagingMessages); + this.connection.off('unread_messaging_message', this.onUnreadMessagingMessage); + this.$root.$data.os.stream.dispose(this.connectionId); + } + }, + methods: { + onReadAllMessagingMessages() { + this.hasUnreadMessagingMessages = false; + }, + + onUnreadMessagingMessage() { + this.hasUnreadMessagingMessages = true; + }, + + messaging() { + document.body.appendChild(new MkMessagingWindow().$mount().$el); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-ui-header-nav + display inline-block + margin 0 + padding 0 + line-height 3rem + vertical-align top + + > ul + display inline-block + margin 0 + padding 0 + vertical-align top + line-height 3rem + list-style none + + > li + display inline-block + vertical-align top + height 48px + line-height 48px + + &.active + > a + border-bottom solid 3px $theme-color + + > a + display inline-block + z-index 1 + height 100% + padding 0 24px + font-size 13px + font-variant small-caps + color #9eaba8 + text-decoration none + transition none + cursor pointer + + * + pointer-events none + + &:hover + color darken(#9eaba8, 20%) + text-decoration none + + > [data-fa]:first-child + margin-right 8px + + > [data-fa]:last-child + margin-left 5px + font-size 10px + color $theme-color + + @media (max-width 1100px) + margin-left -5px + + > p + display inline + margin 0 + + @media (max-width 1100px) + display none + + @media (max-width 700px) + padding 0 12px + +</style> diff --git a/src/web/app/desktop/views/components/ui-header-notifications.vue b/src/web/app/desktop/views/components/ui-header-notifications.vue new file mode 100644 index 0000000000..779ee48864 --- /dev/null +++ b/src/web/app/desktop/views/components/ui-header-notifications.vue @@ -0,0 +1,156 @@ +<template> +<div class="mk-ui-header-notifications"> + <button :data-active="isOpen" @click="toggle" title="%i18n:desktop.tags.mk-ui-header-notifications.title%"> + %fa:R bell%<template v-if="hasUnreadNotifications">%fa:circle%</template> + </button> + <div class="notifications" v-if="isOpen"> + <mk-notifications/> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import contains from '../../../common/scripts/contains'; + +export default Vue.extend({ + data() { + return { + isOpen: false, + hasUnreadNotifications: false, + connection: null, + connectionId: null + }; + }, + mounted() { + if (this.$root.$data.os.isSignedIn) { + this.connection = this.$root.$data.os.stream.getConnection(); + this.connectionId = this.$root.$data.os.stream.use(); + + this.connection.on('read_all_notifications', this.onReadAllNotifications); + this.connection.on('unread_notification', this.onUnreadNotification); + + // Fetch count of unread notifications + this.$root.$data.os.api('notifications/get_unread_count').then(res => { + if (res.count > 0) { + this.hasUnreadNotifications = true; + } + }); + } + }, + beforeDestroy() { + if (this.$root.$data.os.isSignedIn) { + this.connection.off('read_all_notifications', this.onReadAllNotifications); + this.connection.off('unread_notification', this.onUnreadNotification); + this.$root.$data.os.stream.dispose(this.connectionId); + } + }, + methods: { + onReadAllNotifications() { + this.hasUnreadNotifications = false; + }, + + onUnreadNotification() { + this.hasUnreadNotifications = true; + }, + + toggle() { + this.isOpen ? this.close() : this.open(); + }, + + open() { + this.isOpen = true; + Array.from(document.querySelectorAll('body *')).forEach(el => { + el.addEventListener('mousedown', this.onMousedown); + }); + }, + + close() { + this.isOpen = false; + Array.from(document.querySelectorAll('body *')).forEach(el => { + el.removeEventListener('mousedown', this.onMousedown); + }); + }, + + onMousedown(e) { + e.preventDefault(); + if (!contains(this.$el, e.target) && this.$el != e.target) this.close(); + return false; + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-ui-header-notifications + + > button + display block + margin 0 + padding 0 + width 32px + color #9eaba8 + border none + background transparent + cursor pointer + + * + pointer-events none + + &:hover + &[data-active='true'] + color darken(#9eaba8, 20%) + + &:active + color darken(#9eaba8, 30%) + + > [data-fa].bell + font-size 1.2em + line-height 48px + + > [data-fa].circle + margin-left -5px + vertical-align super + font-size 10px + color $theme-color + + > .notifications + display block + position absolute + top 56px + right -72px + width 300px + background #fff + border-radius 4px + box-shadow 0 1px 4px rgba(0, 0, 0, 0.25) + + &:before + content "" + pointer-events none + display block + position absolute + top -28px + right 74px + border-top solid 14px transparent + border-right solid 14px transparent + border-bottom solid 14px rgba(0, 0, 0, 0.1) + border-left solid 14px transparent + + &:after + content "" + pointer-events none + display block + position absolute + top -27px + right 74px + border-top solid 14px transparent + border-right solid 14px transparent + border-bottom solid 14px #fff + border-left solid 14px transparent + + > mk-notifications + max-height 350px + font-size 1rem + overflow auto + +</style> diff --git a/src/web/app/desktop/views/components/ui-header-post-button.vue b/src/web/app/desktop/views/components/ui-header-post-button.vue new file mode 100644 index 0000000000..754e05b23f --- /dev/null +++ b/src/web/app/desktop/views/components/ui-header-post-button.vue @@ -0,0 +1,52 @@ +<template> +<div class="mk-ui-header-post-button"> + <button @click="post" title="%i18n:desktop.tags.mk-ui-header-post-button.post%">%fa:pencil-alt%</button> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + methods: { + post() { + (this.$parent.$parent as any).openPostForm(); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-ui-header-post-button + display inline-block + padding 8px + height 100% + vertical-align top + + > button + display inline-block + margin 0 + padding 0 10px + height 100% + font-size 1.2em + font-weight normal + text-decoration none + color $theme-color-foreground + background $theme-color !important + outline none + border none + border-radius 4px + transition background 0.1s ease + cursor pointer + + * + pointer-events none + + &:hover + background lighten($theme-color, 10%) !important + + &:active + background darken($theme-color, 10%) !important + transition background 0s ease + +</style> diff --git a/src/web/app/desktop/views/components/ui-header-search.vue b/src/web/app/desktop/views/components/ui-header-search.vue new file mode 100644 index 0000000000..a9cddd8aed --- /dev/null +++ b/src/web/app/desktop/views/components/ui-header-search.vue @@ -0,0 +1,68 @@ +<template> +<form class="ui-header-search" @submit.prevent="onSubmit"> + %fa:search% + <input v-model="q" type="search" placeholder="%i18n:desktop.tags.mk-ui-header-search.placeholder%"/> + <div class="result"></div> +</form> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + data() { + return { + q: '' + }; + }, + methods: { + onSubmit() { + location.href = `/search?q=${encodeURIComponent(this.q)}`; + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-ui-header-search + + > [data-fa] + display block + position absolute + top 0 + left 0 + width 48px + text-align center + line-height 48px + color #9eaba8 + pointer-events none + + > * + vertical-align middle + + > input + user-select text + cursor auto + margin 8px 0 0 0 + padding 6px 18px 6px 36px + width 14em + height 32px + font-size 1em + background rgba(0, 0, 0, 0.05) + outline none + //border solid 1px #ddd + border none + border-radius 16px + transition color 0.5s ease, border 0.5s ease + font-family FontAwesome, sans-serif + + &::placeholder + color #9eaba8 + + &:hover + background rgba(0, 0, 0, 0.08) + + &:focus + box-shadow 0 0 0 2px rgba($theme-color, 0.5) !important + +</style> diff --git a/src/web/app/desktop/views/components/ui-header.vue b/src/web/app/desktop/views/components/ui-header.vue new file mode 100644 index 0000000000..19e4fe697f --- /dev/null +++ b/src/web/app/desktop/views/components/ui-header.vue @@ -0,0 +1,86 @@ +<template> +<div class="mk-ui-header"> + <mk-special-message/> + <div class="main"> + <div class="backdrop"></div> + <div class="main"> + <div class="container"> + <div class="left"> + <mk-ui-header-nav/> + </div> + <div class="right"> + <mk-ui-header-search/> + <mk-ui-header-account v-if="$root.$data.os.isSignedIn"/> + <mk-ui-header-notifications v-if="$root.$data.os.isSignedIn"/> + <mk-ui-header-post-button v-if="$root.$data.os.isSignedIn"/> + <mk-ui-header-clock/> + </div> + </div> + </div> + </div> +</div> +</template> + +<style lang="stylus" scoped> +.mk-ui-header + display block + position -webkit-sticky + position sticky + top 0 + z-index 1024 + width 100% + box-shadow 0 1px 1px rgba(0, 0, 0, 0.075) + + > .main + + > .backdrop + position absolute + top 0 + z-index 1023 + width 100% + height 48px + backdrop-filter blur(12px) + background #f7f7f7 + + &:after + content "" + display block + width 100% + height 48px + background-image url(/assets/desktop/header-logo.svg) + background-size 46px + background-position center + background-repeat no-repeat + opacity 0.3 + + > .main + z-index 1024 + margin 0 + padding 0 + background-clip content-box + font-size 0.9rem + user-select none + + > .container + width 100% + max-width 1300px + margin 0 auto + + &:after + content "" + display block + clear both + + > .left + float left + height 3rem + + > .right + float right + height 48px + + @media (max-width 1100px) + > mk-ui-header-search + display none + +</style> diff --git a/src/web/app/desktop/views/components/ui-notification.vue b/src/web/app/desktop/views/components/ui-notification.vue new file mode 100644 index 0000000000..f240037d0b --- /dev/null +++ b/src/web/app/desktop/views/components/ui-notification.vue @@ -0,0 +1,59 @@ +<template> +<div class="mk-ui-notification"> + <p>{{ message }}</p> +<div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import anime from 'animejs'; + +export default Vue.extend({ + props: ['message'], + mounted() { + anime({ + targets: this.$el, + opacity: 1, + translateY: [-64, 0], + easing: 'easeOutElastic', + duration: 500 + }); + + setTimeout(() => { + anime({ + targets: this.$el, + opacity: 0, + translateY: -64, + duration: 500, + easing: 'easeInElastic', + complete: () => this.$destroy() + }); + }, 6000); + } +}); +</script> + +<style lang="stylus" scoped> +.mk-ui-notification + display block + position fixed + z-index 10000 + top -128px + left 0 + right 0 + margin 0 auto + padding 128px 0 0 0 + width 500px + color rgba(#000, 0.6) + background rgba(#fff, 0.9) + border-radius 0 0 8px 8px + box-shadow 0 2px 4px rgba(#000, 0.2) + transform translateY(-64px) + opacity 0 + + > p + margin 0 + line-height 64px + text-align center + +</style>