diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2019-02-25 19:45:00 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2019-02-25 19:45:00 +0900 |
| commit | c0a60260c25c8f7e0c4975b6a1a4342f2b430210 (patch) | |
| tree | 5bab5271e74eb52bd13fc79d359ef30f829d6dc3 /src/client/app/mobile | |
| parent | Fix error (diff) | |
| download | misskey-c0a60260c25c8f7e0c4975b6a1a4342f2b430210.tar.gz misskey-c0a60260c25c8f7e0c4975b6a1a4342f2b430210.tar.bz2 misskey-c0a60260c25c8f7e0c4975b6a1a4342f2b430210.zip | |
モバイル版でもデッキを使えるように (#4366)
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* Fix bug
* wip
* Update notifications.vue
* Update user-menu.vue
* deck settings
* indicate
Diffstat (limited to 'src/client/app/mobile')
| -rw-r--r-- | src/client/app/mobile/script.ts | 24 | ||||
| -rw-r--r-- | src/client/app/mobile/views/components/note-preview.vue | 42 | ||||
| -rw-r--r-- | src/client/app/mobile/views/components/note.sub.vue | 44 | ||||
| -rw-r--r-- | src/client/app/mobile/views/components/note.vue | 81 | ||||
| -rw-r--r-- | src/client/app/mobile/views/components/notes.vue | 2 | ||||
| -rw-r--r-- | src/client/app/mobile/views/components/notification.vue | 16 | ||||
| -rw-r--r-- | src/client/app/mobile/views/components/notifications.vue | 14 | ||||
| -rw-r--r-- | src/client/app/mobile/views/components/ui-container.vue | 97 | ||||
| -rw-r--r-- | src/client/app/mobile/views/components/ui.header.vue | 37 | ||||
| -rw-r--r-- | src/client/app/mobile/views/components/ui.nav.vue | 288 | ||||
| -rw-r--r-- | src/client/app/mobile/views/components/ui.vue | 71 | ||||
| -rw-r--r-- | src/client/app/mobile/views/pages/notifications.vue | 41 | ||||
| -rw-r--r-- | src/client/app/mobile/views/pages/settings.vue | 25 |
13 files changed, 437 insertions, 345 deletions
diff --git a/src/client/app/mobile/script.ts b/src/client/app/mobile/script.ts index fbe117c4ac..688beac9ee 100644 --- a/src/client/app/mobile/script.ts +++ b/src/client/app/mobile/script.ts @@ -11,10 +11,10 @@ import './style.styl'; import init from '../init'; import MkIndex from './views/pages/index.vue'; +import MkDeck from '../common/views/deck/deck.vue'; import MkSignup from './views/pages/signup.vue'; import MkSelectDrive from './views/pages/selectdrive.vue'; import MkDrive from './views/pages/drive.vue'; -import MkNotifications from './views/pages/notifications.vue'; import MkWidgets from './views/pages/widgets.vue'; import MkMessaging from './views/pages/messaging.vue'; import MkMessagingRoom from './views/pages/messaging-room.vue'; @@ -37,7 +37,7 @@ import FolderChooser from './views/components/drive-folder-chooser.vue'; /** * init */ -init((launch) => { +init((launch, os) => { Vue.mixin({ data() { return { @@ -114,10 +114,26 @@ init((launch) => { const router = new VueRouter({ mode: 'history', routes: [ - { path: '/', name: 'index', component: MkIndex }, + ...(os.store.state.device.inDeckMode + ? [{ path: '/', name: 'index', component: MkDeck, children: [ + { path: '/@:user', component: () => import('../common/views/deck/deck.user-column.vue').then(m => m.default), children: [ + { path: '', name: 'user', component: () => import('../common/views/deck/deck.user-column.home.vue').then(m => m.default) }, + { path: 'following', component: () => import('../common/views/pages/following.vue').then(m => m.default) }, + { path: 'followers', component: () => import('../common/views/pages/followers.vue').then(m => m.default) }, + ]}, + { path: '/notes/:note', name: 'note', component: () => import('../common/views/deck/deck.note-column.vue').then(m => m.default) }, + { path: '/search', component: () => import('../common/views/deck/deck.search-column.vue').then(m => m.default) }, + { path: '/tags/:tag', name: 'tag', component: () => import('../common/views/deck/deck.hashtag-column.vue').then(m => m.default) }, + { path: '/featured', name: 'featured', component: () => import('../common/views/deck/deck.featured-column.vue').then(m => m.default) }, + { path: '/explore', name: 'explore', component: () => import('../common/views/deck/deck.explore-column.vue').then(m => m.default) }, + { path: '/explore/tags/:tag', name: 'explore-tag', props: true, component: () => import('../common/views/deck/deck.explore-column.vue').then(m => m.default) }, + { path: '/i/favorites', component: () => import('../common/views/deck/deck.favorites-column.vue').then(m => m.default) } + ]}] + : [ + { path: '/', name: 'index', component: MkIndex }, + ]), { path: '/signup', name: 'signup', component: MkSignup }, { path: '/i/settings', name: 'settings', component: () => import('./views/pages/settings.vue').then(m => m.default) }, - { path: '/i/notifications', name: 'notifications', component: MkNotifications }, { path: '/i/favorites', name: 'favorites', component: MkFavorites }, { path: '/i/lists', name: 'user-lists', component: MkUserLists }, { path: '/i/lists/:list', name: 'user-list', component: MkUserList }, diff --git a/src/client/app/mobile/views/components/note-preview.vue b/src/client/app/mobile/views/components/note-preview.vue index 565a38de32..1dbbddaa62 100644 --- a/src/client/app/mobile/views/components/note-preview.vue +++ b/src/client/app/mobile/views/components/note-preview.vue @@ -1,6 +1,6 @@ <template> -<div class="yohlumlkhizgfkvvscwfcrcggkotpvry" :class="{ smart: $store.state.device.postStyle == 'smart' }"> - <mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle != 'smart'"/> +<div class="yohlumlkhizgfkvvscwfcrcggkotpvry" :class="{ smart: $store.state.device.postStyle == 'smart', mini: narrow }"> + <mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle != 'smart' && !narrow"/> <div class="main"> <mk-note-header class="header" :note="note" :mini="true"/> <div class="body"> @@ -27,6 +27,12 @@ export default Vue.extend({ } }, + inject: { + narrow: { + default: false + } + }, + data() { return { showContent: false @@ -43,11 +49,25 @@ export default Vue.extend({ overflow hidden font-size 10px - @media (min-width 350px) - font-size 12px + &:not(.mini) + + @media (min-width 350px) + font-size 12px + + @media (min-width 500px) + font-size 14px - @media (min-width 500px) - font-size 14px + > .avatar + + @media (min-width 350px) + margin 0 10px 0 0 + width 44px + height 44px + + @media (min-width 500px) + margin 0 12px 0 0 + width 48px + height 48px &.smart > .main @@ -64,16 +84,6 @@ export default Vue.extend({ height 40px border-radius 8px - @media (min-width 350px) - margin 0 10px 0 0 - width 44px - height 44px - - @media (min-width 500px) - margin 0 12px 0 0 - width 48px - height 48px - > .main flex 1 min-width 0 diff --git a/src/client/app/mobile/views/components/note.sub.vue b/src/client/app/mobile/views/components/note.sub.vue index 2d4bdbf420..0f7363b2aa 100644 --- a/src/client/app/mobile/views/components/note.sub.vue +++ b/src/client/app/mobile/views/components/note.sub.vue @@ -1,5 +1,5 @@ <template> -<div class="zlrxdaqttccpwhpaagdmkawtzklsccam" :class="{ smart: $store.state.device.postStyle == 'smart' }"> +<div class="zlrxdaqttccpwhpaagdmkawtzklsccam" :class="{ smart: $store.state.device.postStyle == 'smart', mini: narrow }"> <mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle != 'smart'"/> <div class="main"> <mk-note-header class="header" :note="note" :mini="true"/> @@ -32,6 +32,12 @@ export default Vue.extend({ } }, + inject: { + narrow: { + default: false + } + }, + data() { return { showContent: false @@ -47,14 +53,28 @@ export default Vue.extend({ font-size 10px background var(--subNoteBg) - @media (min-width 350px) - font-size 12px + &:not(.mini) + + @media (min-width 350px) + font-size 12px + + @media (min-width 500px) + font-size 14px - @media (min-width 500px) - font-size 14px + @media (min-width 600px) + padding 24px 32px - @media (min-width 600px) - padding 24px 32px + > .avatar + + @media (min-width 350px) + margin-right 10px + width 42px + height 42px + + @media (min-width 500px) + margin-right 14px + width 50px + height 50px &.smart > .main @@ -71,16 +91,6 @@ export default Vue.extend({ height 38px border-radius 8px - @media (min-width 350px) - margin-right 10px - width 42px - height 42px - - @media (min-width 500px) - margin-right 14px - width 50px - height 50px - > .main flex 1 min-width 0 diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue index 564069dade..16ee2677b4 100644 --- a/src/client/app/mobile/views/components/note.vue +++ b/src/client/app/mobile/views/components/note.vue @@ -3,14 +3,14 @@ class="note" v-show="appearNote.deletedAt == null && !hideThisNote" :tabindex="appearNote.deletedAt == null ? '-1' : null" - :class="{ renote: isRenote, smart: $store.state.device.postStyle == 'smart' }" + :class="{ renote: isRenote, smart: $store.state.device.postStyle == 'smart', mini: narrow }" v-hotkey="keymap" > <div class="reply-to" v-if="appearNote.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)"> <x-sub :note="appearNote.reply"/> </div> - <mk-renote class="renote" v-if="isRenote" :note="note" mini/> - <article> + <mk-renote class="renote" v-if="isRenote" :note="note"/> + <article class="article"> <mk-avatar class="avatar" :user="appearNote.user" v-if="$store.state.device.postStyle != 'smart'"/> <div class="main"> <mk-note-header class="header" :note="appearNote" :mini="true"/> @@ -30,7 +30,7 @@ <mk-media-list :media-list="appearNote.files"/> </div> <mk-poll v-if="appearNote.poll" :note="appearNote" ref="pollViewer"/> - <mk-url-preview v-for="url in urls" :url="url" :key="url" :compact="compact"/> + <mk-url-preview v-for="url in urls" :url="url" :key="url" :compact="true"/> <a class="location" v-if="appearNote.geo" :href="`https://maps.google.com/maps?q=${appearNote.geo.coordinates[1]},${appearNote.geo.coordinates[0]}`" target="_blank"><fa icon="map-marker-alt"/> {{ $t('location') }}</a> <div class="renote" v-if="appearNote.renote"><mk-note-preview :note="appearNote.renote"/></div> </div> @@ -91,19 +91,20 @@ export default Vue.extend({ type: Object, required: true }, - compact: { - type: Boolean, - required: false, + }, + + inject: { + narrow: { default: false } - } + }, }); </script> <style lang="stylus" scoped> .note overflow hidden - font-size 12px + font-size 13px border-bottom solid var(--lineWidth) var(--faceDivider) &:focus @@ -123,29 +124,53 @@ export default Vue.extend({ &:last-of-type border-bottom none - @media (min-width 350px) - font-size 14px + &:not(.mini) + + @media (min-width 350px) + font-size 14px + + @media (min-width 500px) + font-size 16px - @media (min-width 500px) - font-size 16px + > .article + @media (min-width 600px) + padding 32px 32px 22px + + > .avatar + @media (min-width 350px) + width 48px + height 48px + border-radius 6px + + @media (min-width 500px) + margin-right 16px + width 58px + height 58px + border-radius 8px + + > .main + > .header + @media (min-width 500px) + margin-bottom 2px + + > .body + @media (min-width 700px) + font-size 1.1em &.smart - > article + > .article > .main > header align-items center margin-bottom 4px - > .renote + article + > .renote + .article padding-top 8px - > article + > .article display flex padding 16px 16px 9px - @media (min-width 600px) - padding 32px 32px 22px - > .avatar flex-shrink 0 display block @@ -157,29 +182,11 @@ export default Vue.extend({ //position sticky //top 62px - @media (min-width 350px) - width 48px - height 48px - border-radius 6px - - @media (min-width 500px) - margin-right 16px - width 58px - height 58px - border-radius 8px - > .main flex 1 min-width 0 - > .header - @media (min-width 500px) - margin-bottom 2px - > .body - @media (min-width 700px) - font-size 1.1em - > .cw cursor default display block diff --git a/src/client/app/mobile/views/components/notes.vue b/src/client/app/mobile/views/components/notes.vue index 0d85e321d5..a65482329f 100644 --- a/src/client/app/mobile/views/components/notes.vue +++ b/src/client/app/mobile/views/components/notes.vue @@ -13,7 +13,7 @@ <!-- トランジションを有効にするとなぜかメモリリークする --> <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="transition" tag="div"> <template v-for="(note, i) in _notes"> - <mk-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)" :compact="true"/> + <mk-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/> <p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date"> <span><fa icon="angle-up"/>{{ note._datetext }}</span> <span><fa icon="angle-down"/>{{ _notes[i + 1]._datetext }}</span> diff --git a/src/client/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue index 3f9cefd00c..5308d96533 100644 --- a/src/client/app/mobile/views/components/notification.vue +++ b/src/client/app/mobile/views/components/notification.vue @@ -116,15 +116,6 @@ export default Vue.extend({ font-size 12px overflow-wrap break-word - @media (min-width 350px) - font-size 14px - - @media (min-width 500px) - font-size 16px - - @media (min-width 600px) - padding 24px 32px - &:after content "" display block @@ -137,18 +128,11 @@ export default Vue.extend({ height 36px border-radius 6px - @media (min-width 500px) - width 42px - height 42px - > div float right width calc(100% - 36px) padding-left 8px - @media (min-width 500px) - width calc(100% - 42px) - > header display flex align-items baseline diff --git a/src/client/app/mobile/views/components/notifications.vue b/src/client/app/mobile/views/components/notifications.vue index 37f953edeb..cb8783fa3b 100644 --- a/src/client/app/mobile/views/components/notifications.vue +++ b/src/client/app/mobile/views/components/notifications.vue @@ -131,15 +131,6 @@ export default Vue.extend({ <style lang="stylus" scoped> .mk-notifications - margin 0 auto - background var(--face) - border-radius 8px - box-shadow 0 0 2px rgba(#000, 0.1) - overflow hidden - - @media (min-width 500px) - box-shadow 0 8px 32px rgba(#000, 0.1) - .transition .mk-notifications-enter .mk-notifications-leave-to @@ -187,10 +178,7 @@ export default Vue.extend({ color var(--text) > .placeholder - padding 16px + padding 32px opacity 0.3 - @media (min-width 500px) - padding 32px - </style> diff --git a/src/client/app/mobile/views/components/ui-container.vue b/src/client/app/mobile/views/components/ui-container.vue index 90b29d0c23..806dcc9a1d 100644 --- a/src/client/app/mobile/views/components/ui-container.vue +++ b/src/client/app/mobile/views/components/ui-container.vue @@ -1,5 +1,5 @@ <template> -<div class="ukygtjoj" :class="{ naked, hideHeader: !showHeader }"> +<div class="ukygtjoj" :class="{ naked, inDeck, hideHeader: !showHeader }"> <header v-if="showHeader"> <div class="title"><slot name="header"></slot></div> <slot name="func"></slot> @@ -35,6 +35,11 @@ export default Vue.extend({ default: true }, }, + inject: { + inDeck: { + default: false + } + }, data() { return { showBody: this.expanded @@ -50,49 +55,69 @@ export default Vue.extend({ <style lang="stylus" scoped> .ukygtjoj - background var(--face) - border-radius 8px - box-shadow 0 4px 16px rgba(#000, 0.1) overflow hidden - & + .ukygtjoj - margin-top 16px + &:not(.inDeck) + background var(--face) + border-radius 8px + box-shadow 0 4px 16px rgba(#000, 0.1) - @media (max-width 500px) - margin-top 8px + & + .ukygtjoj + margin-top 16px - &.naked - background transparent !important - box-shadow none !important + @media (max-width 500px) + margin-top 8px - > header - > .title - margin 0 - padding 8px 10px - font-size 15px - font-weight normal - color var(--faceHeaderText) - background var(--faceHeader) - border-radius 8px 8px 0 0 + &.naked + background transparent !important + box-shadow none !important + + > header + > .title + margin 0 + padding 8px 10px + font-size 15px + font-weight normal + color var(--faceHeaderText) + background var(--faceHeader) + border-radius 8px 8px 0 0 - > [data-icon] - margin-right 6px + > [data-icon] + margin-right 6px - &:empty - display none + &:empty + display none - > button - position absolute - z-index 2 - top 0 - right 0 - padding 0 - width 42px - height 100% - font-size 15px - color var(--faceTextButton) + > button + position absolute + z-index 2 + top 0 + right 0 + padding 0 + width 42px + height 100% + font-size 15px + color var(--faceTextButton) + + > div + color var(--text) + + &.inDeck + background var(--face) + + > header + margin 0 + padding 8px 16px + font-size 12px + color var(--text) + background var(--deckColumnBg) - > div - color var(--text) + > button + position absolute + top 0 + right 8px + padding 8px 6px + font-size 14px + color var(--text) </style> diff --git a/src/client/app/mobile/views/components/ui.header.vue b/src/client/app/mobile/views/components/ui.header.vue index d9994f236a..532720ceb4 100644 --- a/src/client/app/mobile/views/components/ui.header.vue +++ b/src/client/app/mobile/views/components/ui.header.vue @@ -5,7 +5,7 @@ <div class="backdrop"></div> <div class="content" ref="mainContainer"> <button class="nav" @click="$parent.isDrawerOpening = true"><fa icon="bars"/></button> - <i v-if="hasUnreadNotification || hasUnreadMessagingMessage || hasGameInvitation" class="circle"><fa icon="circle"/></i> + <i v-if="$parent.indicate" class="circle"><fa icon="circle"/></i> <h1> <slot>{{ $root.instanceName }}</slot> </h1> @@ -27,48 +27,13 @@ export default Vue.extend({ data() { return { - hasGameInvitation: false, - connection: null, env: env }; }, - computed: { - hasUnreadNotification(): boolean { - return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadNotification; - }, - - hasUnreadMessagingMessage(): boolean { - return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadMessagingMessage; - } - }, - mounted() { this.$store.commit('setUiHeaderHeight', 48); - - if (this.$store.getters.isSignedIn) { - this.connection = this.$root.stream.useSharedConnection('main'); - - this.connection.on('reversiInvited', this.onReversiInvited); - this.connection.on('reversiNoInvites', this.onReversiNoInvites); - } }, - - beforeDestroy() { - if (this.$store.getters.isSignedIn) { - this.connection.dispose(); - } - }, - - methods: { - onReversiInvited() { - this.hasGameInvitation = true; - }, - - onReversiNoInvites() { - this.hasGameInvitation = false; - } - } }); </script> diff --git a/src/client/app/mobile/views/components/ui.nav.vue b/src/client/app/mobile/views/components/ui.nav.vue index 627a322e6c..4c33de8842 100644 --- a/src/client/app/mobile/views/components/ui.nav.vue +++ b/src/client/app/mobile/views/components/ui.nav.vue @@ -1,5 +1,5 @@ <template> -<div class="nav"> +<div class="fquwcbxs"> <transition name="back"> <div class="backdrop" v-if="isOpen" @@ -8,41 +8,52 @@ ></div> </transition> <transition name="nav"> - <div class="body" v-if="isOpen"> - <router-link class="me" v-if="$store.getters.isSignedIn" :to="`/@${$store.state.i.username}`"> - <img class="avatar" :src="$store.state.i.avatarUrl" alt="avatar"/> - <p class="name"><mk-user-name :user="$store.state.i"/></p> - </router-link> - <div class="links"> - <ul> - <li><router-link to="/" :data-active="$route.name == 'index'"><i><fa icon="home" fixed-width/></i>{{ $t('timeline') }}<i><fa icon="angle-right"/></i></router-link></li> - <li><router-link to="/i/notifications" :data-active="$route.name == 'notifications'"><i><fa :icon="['far', 'bell']" fixed-width/></i>{{ $t('notifications') }}<i v-if="hasUnreadNotification" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li> - <li><router-link to="/i/messaging" :data-active="$route.name == 'messaging'"><i><fa :icon="['far', 'comments']" fixed-width/></i>{{ $t('@.messaging') }}<i v-if="hasUnreadMessagingMessage" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li> - <li v-if="$store.getters.isSignedIn && ($store.state.i.isLocked || $store.state.i.carefulBot)"><router-link to="/i/received-follow-requests" :data-active="$route.name == 'received-follow-requests'"><i><fa :icon="['far', 'envelope']" fixed-width/></i>{{ $t('follow-requests') }}<i v-if="$store.getters.isSignedIn && $store.state.i.pendingReceivedFollowRequestsCount" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li> - <li><router-link to="/featured" :data-active="$route.name == 'featured'"><i><fa :icon="faNewspaper" fixed-width/></i>{{ $t('@.featured-notes') }}<i><fa icon="angle-right"/></i></router-link></li> - <li><router-link to="/explore" :data-active="$route.name == 'explore' || $route.name == 'explore-tag'"><i><fa :icon="faHashtag" fixed-width/></i>{{ $t('@.explore') }}<i><fa icon="angle-right"/></i></router-link></li> - <li><router-link to="/games/reversi" :data-active="$route.name == 'reversi'"><i><fa icon="gamepad" fixed-width/></i>{{ $t('game') }}<i v-if="hasGameInvitation" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li> - </ul> - <ul> - <li><router-link to="/i/widgets" :data-active="$route.name == 'widgets'"><i><fa :icon="['far', 'calendar-alt']" fixed-width/></i>{{ $t('widgets') }}<i><fa icon="angle-right"/></i></router-link></li> - <li><router-link to="/i/favorites" :data-active="$route.name == 'favorites'"><i><fa icon="star" fixed-width/></i>{{ $t('favorites') }}<i><fa icon="angle-right"/></i></router-link></li> - <li><router-link to="/i/lists" :data-active="$route.name == 'user-lists'"><i><fa icon="list" fixed-width/></i>{{ $t('user-lists') }}<i><fa icon="angle-right"/></i></router-link></li> - <li><router-link to="/i/drive" :data-active="$route.name == 'drive'"><i><fa icon="cloud" fixed-width/></i>{{ $t('@.drive') }}<i><fa icon="angle-right"/></i></router-link></li> - </ul> - <ul> - <li><a @click="search"><i><fa icon="search" fixed-width/></i>{{ $t('search') }}<i><fa icon="angle-right"/></i></a></li> - <li><router-link to="/i/settings" :data-active="$route.name == 'settings'"><i><fa icon="cog" fixed-width/></i>{{ $t('settings') }}<i><fa icon="angle-right"/></i></router-link></li> - <li v-if="$store.getters.isSignedIn && ($store.state.i.isAdmin || $store.state.i.isModerator)"><a href="/admin"><i><fa icon="terminal" fixed-width/></i><span>{{ $t('admin') }}</span><i><fa icon="angle-right"/></i></a></li> - <li @click="dark"><p><template><i><fa :icon="$store.state.device.darkmode ? faSun : faMoon" fixed-width/></i></template><span>{{ $store.state.device.darkmode ? $t('@.turn-off-darkmode') : $t('@.turn-on-darkmode') }}</span></p></li> - </ul> + <div class="body" :class="{ notifications: showNotifications }" v-if="isOpen"> + <div class="nav" v-show="!showNotifications"> + <router-link class="me" v-if="$store.getters.isSignedIn" :to="`/@${$store.state.i.username}`"> + <img class="avatar" :src="$store.state.i.avatarUrl" alt="avatar"/> + <p class="name"><mk-user-name :user="$store.state.i"/></p> + </router-link> + <div class="links"> + <ul> + <li><router-link to="/" :data-active="$route.name == 'index'"><i><fa icon="home" fixed-width/></i>{{ $t('timeline') }}<i><fa icon="angle-right"/></i></router-link></li> + <li><p @click="showNotifications = true"><i><fa :icon="['far', 'bell']" fixed-width/></i>{{ $t('notifications') }}<i v-if="hasUnreadNotification" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></p></li> + <li><router-link to="/i/messaging" :data-active="$route.name == 'messaging'"><i><fa :icon="['far', 'comments']" fixed-width/></i>{{ $t('@.messaging') }}<i v-if="hasUnreadMessagingMessage" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li> + <li v-if="$store.getters.isSignedIn && ($store.state.i.isLocked || $store.state.i.carefulBot)"><router-link to="/i/received-follow-requests" :data-active="$route.name == 'received-follow-requests'"><i><fa :icon="['far', 'envelope']" fixed-width/></i>{{ $t('follow-requests') }}<i v-if="$store.getters.isSignedIn && $store.state.i.pendingReceivedFollowRequestsCount" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li> + <li><router-link to="/featured" :data-active="$route.name == 'featured'"><i><fa :icon="faNewspaper" fixed-width/></i>{{ $t('@.featured-notes') }}<i><fa icon="angle-right"/></i></router-link></li> + <li><router-link to="/explore" :data-active="$route.name == 'explore' || $route.name == 'explore-tag'"><i><fa :icon="faHashtag" fixed-width/></i>{{ $t('@.explore') }}<i><fa icon="angle-right"/></i></router-link></li> + <li><router-link to="/games/reversi" :data-active="$route.name == 'reversi'"><i><fa icon="gamepad" fixed-width/></i>{{ $t('game') }}<i v-if="hasGameInvitation" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li> + </ul> + <ul> + <li><router-link to="/i/widgets" :data-active="$route.name == 'widgets'"><i><fa :icon="['far', 'calendar-alt']" fixed-width/></i>{{ $t('widgets') }}<i><fa icon="angle-right"/></i></router-link></li> + <li><router-link to="/i/favorites" :data-active="$route.name == 'favorites'"><i><fa icon="star" fixed-width/></i>{{ $t('favorites') }}<i><fa icon="angle-right"/></i></router-link></li> + <li><router-link to="/i/lists" :data-active="$route.name == 'user-lists'"><i><fa icon="list" fixed-width/></i>{{ $t('user-lists') }}<i><fa icon="angle-right"/></i></router-link></li> + <li><router-link to="/i/drive" :data-active="$route.name == 'drive'"><i><fa icon="cloud" fixed-width/></i>{{ $t('@.drive') }}<i><fa icon="angle-right"/></i></router-link></li> + </ul> + <ul> + <li><a @click="search"><i><fa icon="search" fixed-width/></i>{{ $t('search') }}<i><fa icon="angle-right"/></i></a></li> + <li><router-link to="/i/settings" :data-active="$route.name == 'settings'"><i><fa icon="cog" fixed-width/></i>{{ $t('settings') }}<i><fa icon="angle-right"/></i></router-link></li> + <li v-if="$store.getters.isSignedIn && ($store.state.i.isAdmin || $store.state.i.isModerator)"><a href="/admin"><i><fa icon="terminal" fixed-width/></i><span>{{ $t('admin') }}</span><i><fa icon="angle-right"/></i></a></li> + </ul> + <ul> + <li @click="toggleDeckMode"><p><i><fa :icon="$store.state.device.inDeckMode ? faHome : faColumns"/></i><span>{{ $store.state.device.inDeckMode ? $t('@.home') : $t('@.deck') }}</span></p></li> + <li @click="dark"><p><i><fa :icon="$store.state.device.darkmode ? faSun : faMoon" fixed-width/></i><span>{{ $store.state.device.darkmode ? $t('@.turn-off-darkmode') : $t('@.turn-on-darkmode') }}</span></p></li> + </ul> + </div> + <div class="announcements" v-if="announcements && announcements.length > 0"> + <article v-for="announcement in announcements"> + <span v-html="announcement.title" class="title"></span> + <div v-html="announcement.text"></div> + </article> + </div> + <a :href="aboutUrl"><p class="about">{{ $t('about') }}</p></a> </div> - <div class="announcements" v-if="announcements && announcements.length > 0"> - <article v-for="announcement in announcements"> - <span v-html="announcement.title" class="title"></span> - <div v-html="announcement.text"></div> - </article> + <div class="notifications" v-if="showNotifications"> + <header> + <button @click="$parent.isDrawerOpening = false"><fa icon="times"/></button> + </header> + <mk-notifications/> </div> - <a :href="aboutUrl"><p class="about">{{ $t('about') }}</p></a> </div> </transition> </div> @@ -52,13 +63,18 @@ import Vue from 'vue'; import i18n from '../../../i18n'; import { lang } from '../../../config'; -import { faNewspaper, faHashtag } from '@fortawesome/free-solid-svg-icons'; +import { faNewspaper, faHashtag, faHome, faColumns } from '@fortawesome/free-solid-svg-icons'; import { faMoon, faSun } from '@fortawesome/free-regular-svg-icons'; export default Vue.extend({ i18n: i18n('mobile/views/components/ui.nav.vue'), + props: ['isOpen'], + provide: { + narrow: true + }, + data() { return { hasGameInvitation: false, @@ -66,7 +82,8 @@ export default Vue.extend({ aboutUrl: `/docs/${lang}/about`, announcements: [], searching: false, - faNewspaper, faHashtag, faMoon, faSun + showNotifications: false, + faNewspaper, faHashtag, faMoon, faSun, faHome, faColumns }; }, @@ -80,6 +97,12 @@ export default Vue.extend({ } }, + watch: { + isOpen() { + this.showNotifications = false; + } + }, + mounted() { this.$root.getMeta().then(meta => { this.announcements = meta.announcements; @@ -148,13 +171,18 @@ export default Vue.extend({ key: 'darkmode', value: !this.$store.state.device.darkmode }); - } + }, + + toggleDeckMode() { + this.$store.commit('device/set', { key: 'deckMode', value: !this.$store.state.device.inDeckMode }); + location.replace('/'); + }, } }); </script> <style lang="stylus" scoped> -.nav +.fquwcbxs $color = var(--text) .backdrop @@ -178,102 +206,126 @@ export default Vue.extend({ background var(--secondary) font-size 15px - .me - display block - margin 0 - padding 16px + &.notifications + width 340px - .avatar - display inline - max-width 64px - border-radius 32px - vertical-align middle + > .notifications + padding-top 42px - .name - display block - margin 0 16px - position absolute - top 0 - left 80px - padding 0 - width calc(100% - 112px) - color $color - line-height 96px - overflow hidden - text-overflow ellipsis - white-space nowrap + > header + position fixed + top 0 + left 0 + z-index 1000 + width 340px + line-height 42px + background var(--secondary) - ul - display block - margin 16px 0 - padding 0 - list-style none + > button + display block + padding 0 14px + font-size 20px + line-height 42px + color var(--text) - &:first-child - margin-top 0 + > .nav + + > .me + display block + margin 0 + padding 16px - &:last-child - margin-bottom 0 + .avatar + display inline + max-width 64px + border-radius 32px + vertical-align middle - > li - display block - font-size 1em - line-height 1em + .name + display block + margin 0 16px + position absolute + top 0 + left 80px + padding 0 + width calc(100% - 112px) + color $color + line-height 96px + overflow hidden + text-overflow ellipsis + white-space nowrap - a, p + ul display block - margin 0 - padding 0 20px - line-height 3rem - line-height calc(1rem + 30px) - color $color - text-decoration none + margin 16px 0 + padding 0 + list-style none - &[data-active] - color var(--primaryForeground) - background var(--primary) + &:first-child + margin-top 0 - > i:last-child - color var(--primaryForeground) + &:last-child + margin-bottom 0 - > i:first-child - margin-right 0.5em - width 20px - text-align center + > li + display block + font-size 1em + line-height 1em - > i.circle - margin-left 6px - font-size 10px - color var(--notificationIndicator) + a, p + display block + margin 0 + padding 0 20px + line-height 3rem + line-height calc(1rem + 30px) + color $color + text-decoration none - > i:last-child - position absolute - top 0 - right 0 - padding 0 20px - font-size 1.2em - line-height calc(1rem + 30px) - color $color - opacity 0.5 + &[data-active] + color var(--primaryForeground) + background var(--primary) + + > i:last-child + color var(--primaryForeground) + + > i:first-child + margin-right 0.5em + width 20px + text-align center - .announcements - > article - background var(--mobileAnnouncement) - color var(--mobileAnnouncementFg) - padding 16px - margin 8px 0 - font-size 12px + > i.circle + margin-left 6px + font-size 10px + color var(--notificationIndicator) - > .title - font-weight bold + > i:last-child + position absolute + top 0 + right 0 + padding 0 20px + font-size 1.2em + line-height calc(1rem + 30px) + color $color + opacity 0.5 - .about - margin 0 0 8px 0 - padding 1em 0 - text-align center - font-size 0.8em - color $color - opacity 0.5 + .announcements + > article + background var(--mobileAnnouncement) + color var(--mobileAnnouncementFg) + padding 16px + margin 8px 0 + font-size 12px + + > .title + font-weight bold + + .about + margin 0 0 8px 0 + padding 1em 0 + text-align center + font-size 0.8em + color $color + opacity 0.5 .nav-enter-active, .nav-leave-active { diff --git a/src/client/app/mobile/views/components/ui.vue b/src/client/app/mobile/views/components/ui.vue index 6cd42b0207..7ae7dd5f78 100644 --- a/src/client/app/mobile/views/components/ui.vue +++ b/src/client/app/mobile/views/components/ui.vue @@ -1,6 +1,6 @@ <template> -<div class="mk-ui"> - <x-header> +<div class="mk-ui" :class="{ deck: $store.state.device.inDeckMode }"> + <x-header v-if="!$store.state.device.inDeckMode"> <template #func><slot name="func"></slot></template> <slot name="header"></slot> </x-header> @@ -9,6 +9,8 @@ <slot></slot> </div> <mk-stream-indicator v-if="$store.getters.isSignedIn"/> + <button class="nav button" v-if="$store.state.device.inDeckMode" @click="isDrawerOpening = !isDrawerOpening"><fa icon="bars"/><i v-if="indicate"><fa icon="circle"/></i></button> + <button class="post button" v-if="$store.state.device.inDeckMode" @click="$post()"><fa icon="pencil-alt"/></button> </div> </template> @@ -28,11 +30,26 @@ export default Vue.extend({ data() { return { + hasGameInvitation: false, isDrawerOpening: false, connection: null }; }, + computed: { + hasUnreadNotification(): boolean { + return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadNotification; + }, + + hasUnreadMessagingMessage(): boolean { + return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadMessagingMessage; + }, + + indicate(): boolean { + return this.hasUnreadNotification || this.hasUnreadMessagingMessage || this.hasGameInvitation; + } + }, + watch: { '$store.state.uiHeaderHeight'() { this.$el.style.paddingTop = this.$store.state.uiHeaderHeight + 'px'; @@ -46,6 +63,8 @@ export default Vue.extend({ this.connection = this.$root.stream.useSharedConnection('main'); this.connection.on('notification', this.onNotification); + this.connection.on('reversiInvited', this.onReversiInvited); + this.connection.on('reversiNoInvites', this.onReversiNoInvites); } }, @@ -65,6 +84,14 @@ export default Vue.extend({ this.$root.new(MkNotify, { notification }); + }, + + onReversiInvited() { + this.hasGameInvitation = true; + }, + + onReversiNoInvites() { + this.hasGameInvitation = false; } } }); @@ -72,13 +99,37 @@ export default Vue.extend({ <style lang="stylus" scoped> .mk-ui - display flex - flex 1 - flex-direction column - padding-top 48px + &:not(.deck) + padding-top 48px + + > .button + position fixed + z-index 1000 + bottom 28px + padding 0 + width 64px + height 64px + border-radius 100% + box-shadow 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12) + + > * + font-size 24px + + &.nav + left 28px + background var(--secondary) + color var(--text) + + > i + position absolute + top 0 + left 0 + color var(--primary) + font-size 16px + + &.post + right 28px + background var(--primary) + color var(--primaryForeground) - > .content - display flex - flex 1 - flex-direction column </style> diff --git a/src/client/app/mobile/views/pages/notifications.vue b/src/client/app/mobile/views/pages/notifications.vue deleted file mode 100644 index 8472a623c1..0000000000 --- a/src/client/app/mobile/views/pages/notifications.vue +++ /dev/null @@ -1,41 +0,0 @@ -<template> -<mk-ui> - <template #header><span style="margin-right:4px;"><fa :icon="['far', 'bell']"/></span>{{ $t('notifications') }}</template> - <template #func><button @click="fn"><fa icon="check"/></button></template> - - <main> - <mk-notifications @fetched="onFetched"/> - </main> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import Progress from '../../../common/scripts/loading'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/notifications.vue'), - mounted() { - document.title = this.$t('notifications'); - - Progress.start(); - }, - methods: { - fn() { - this.$root.dialog({ - type: 'warning', - text: this.$t('read-all'), - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) return; - - this.$root.api('notifications/mark_all_as_read'); - }); - }, - onFetched() { - Progress.done(); - } - } -}); -</script> diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue index 4a1e18540c..17f9f65881 100644 --- a/src/client/app/mobile/views/pages/settings.vue +++ b/src/client/app/mobile/views/pages/settings.vue @@ -60,6 +60,21 @@ <ui-radio v-model="mobileNotificationPosition" value="bottom">{{ $t('notification-position-bottom') }}</ui-radio> <ui-radio v-model="mobileNotificationPosition" value="top">{{ $t('notification-position-top') }}</ui-radio> </section> + + <section> + <header>{{ $t('@.deck-column-align') }}</header> + <ui-radio v-model="deckColumnAlign" value="center">{{ $t('@.deck-column-align-center') }}</ui-radio> + <ui-radio v-model="deckColumnAlign" value="left">{{ $t('@.deck-column-align-left') }}</ui-radio> + <ui-radio v-model="deckColumnAlign" value="flexible">{{ $t('@.deck-column-align-flexible') }}</ui-radio> + </section> + <section> + <header>{{ $t('@.deck-column-width') }}</header> + <ui-radio v-model="deckColumnWidth" value="narrow">{{ $t('@.deck-column-width-narrow') }}</ui-radio> + <ui-radio v-model="deckColumnWidth" value="narrower">{{ $t('@.deck-column-width-narrower') }}</ui-radio> + <ui-radio v-model="deckColumnWidth" value="normal">{{ $t('@.deck-column-width-normal') }}</ui-radio> + <ui-radio v-model="deckColumnWidth" value="wider">{{ $t('@.deck-column-width-wider') }}</ui-radio> + <ui-radio v-model="deckColumnWidth" value="wide">{{ $t('@.deck-column-width-wide') }}</ui-radio> + </section> </ui-card> <ui-card> @@ -244,6 +259,16 @@ export default Vue.extend({ set(value) { this.$store.commit('device/set', { key: 'enableSounds', value }); } }, + deckColumnAlign: { + get() { return this.$store.state.device.deckColumnAlign; }, + set(value) { this.$store.commit('device/set', { key: 'deckColumnAlign', value }); } + }, + + deckColumnWidth: { + get() { return this.$store.state.device.deckColumnWidth; }, + set(value) { this.$store.commit('device/set', { key: 'deckColumnWidth', value }); } + }, + fetchOnScroll: { get() { return this.$store.state.settings.fetchOnScroll; }, set(value) { this.$store.dispatch('settings/set', { key: 'fetchOnScroll', value }); } |