diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2018-10-19 06:18:33 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2018-10-19 06:18:33 +0900 |
| commit | fef4f7fce81d5aca76a8c45713c76c0c482884e7 (patch) | |
| tree | 87f460732361921860ac40b2130430014fce6e58 /src | |
| parent | Merge pull request #2929 from syuilo/l10n_develop (diff) | |
| download | sharkey-fef4f7fce81d5aca76a8c45713c76c0c482884e7.tar.gz sharkey-fef4f7fce81d5aca76a8c45713c76c0c482884e7.tar.bz2 sharkey-fef4f7fce81d5aca76a8c45713c76c0c482884e7.zip | |
#2930 (#2933)
* wip
* wip
* Clean up
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* :art:
* wip
* wip
Diffstat (limited to 'src')
28 files changed, 785 insertions, 774 deletions
diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts index e2b9089d35..0dd013807c 100644 --- a/src/client/app/common/views/components/index.ts +++ b/src/client/app/common/views/components/index.ts @@ -1,5 +1,6 @@ import Vue from 'vue'; +import profileEditor from './profile-editor.vue'; import noteSkeleton from './note-skeleton.vue'; import theme from './theme.vue'; import instance from './instance.vue'; @@ -45,6 +46,7 @@ import uiSelect from './ui/select.vue'; import formButton from './ui/form/button.vue'; import formRadio from './ui/form/radio.vue'; +Vue.component('mk-profile-editor', profileEditor); Vue.component('mk-note-skeleton', noteSkeleton); Vue.component('mk-theme', theme); Vue.component('mk-instance', instance); diff --git a/src/client/app/mobile/views/pages/settings/settings.profile.vue b/src/client/app/common/views/components/profile-editor.vue index 160a05a3e0..10bdc0b579 100644 --- a/src/client/app/mobile/views/pages/settings/settings.profile.vue +++ b/src/client/app/common/views/components/profile-editor.vue @@ -49,6 +49,7 @@ <div> <ui-switch v-model="isCat" @change="save(false)">%i18n:@is-cat%</ui-switch> + <ui-switch v-model="isBot" @change="save(false)">%i18n:@is-bot%</ui-switch> <ui-switch v-model="alwaysMarkNsfw">%i18n:common.always-mark-nsfw%</ui-switch> </div> </section> @@ -66,7 +67,7 @@ <script lang="ts"> import Vue from 'vue'; -import { apiUrl, host } from '../../../../config'; +import { apiUrl, host } from '../../../config'; export default Vue.extend({ data() { @@ -80,6 +81,7 @@ export default Vue.extend({ avatarId: null, bannerId: null, isCat: false, + isBot: false, isLocked: false, carefulBot: false, saving: false, @@ -104,6 +106,7 @@ export default Vue.extend({ this.avatarId = this.$store.state.i.avatarId; this.bannerId = this.$store.state.i.bannerId; this.isCat = this.$store.state.i.isCat; + this.isBot = this.$store.state.i.isBot; this.isLocked = this.$store.state.i.isLocked; this.carefulBot = this.$store.state.i.carefulBot; }, @@ -164,6 +167,7 @@ export default Vue.extend({ avatarId: this.avatarId, bannerId: this.bannerId, isCat: this.isCat, + isBot: this.isBot, isLocked: this.isLocked, carefulBot: this.carefulBot }).then(i => { diff --git a/src/client/app/common/views/components/ui/input.vue b/src/client/app/common/views/components/ui/input.vue index abbd5a2feb..7e1a16bb3f 100644 --- a/src/client/app/common/views/components/ui/input.vue +++ b/src/client/app/common/views/components/ui/input.vue @@ -122,17 +122,19 @@ export default Vue.extend({ } }, mounted() { - if (this.$refs.prefix) { - this.$refs.label.style.left = (this.$refs.prefix.offsetLeft + this.$refs.prefix.offsetWidth) + 'px'; - if (this.$refs.prefix.offsetWidth) { - this.$refs.input.style.paddingLeft = this.$refs.prefix.offsetWidth + 'px'; + this.$nextTick(() => { + if (this.$refs.prefix) { + this.$refs.label.style.left = (this.$refs.prefix.offsetLeft + this.$refs.prefix.offsetWidth) + 'px'; + if (this.$refs.prefix.offsetWidth) { + this.$refs.input.style.paddingLeft = this.$refs.prefix.offsetWidth + 'px'; + } } - } - if (this.$refs.suffix) { - if (this.$refs.suffix.offsetWidth) { - this.$refs.input.style.paddingRight = this.$refs.suffix.offsetWidth + 'px'; + if (this.$refs.suffix) { + if (this.$refs.suffix.offsetWidth) { + this.$refs.input.style.paddingRight = this.$refs.suffix.offsetWidth + 'px'; + } } - } + }); }, methods: { focus() { diff --git a/src/client/app/common/views/widgets/photo-stream.vue b/src/client/app/common/views/widgets/photo-stream.vue index 047b01df4f..02cb9204b5 100644 --- a/src/client/app/common/views/widgets/photo-stream.vue +++ b/src/client/app/common/views/widgets/photo-stream.vue @@ -73,9 +73,6 @@ export default define({ border-radius 8px .stream - display -webkit-flex - display -moz-flex - display -ms-flex display flex justify-content center flex-wrap wrap diff --git a/src/client/app/desktop/script.ts b/src/client/app/desktop/script.ts index 85c81d73a2..3c9003cbe7 100644 --- a/src/client/app/desktop/script.ts +++ b/src/client/app/desktop/script.ts @@ -67,8 +67,8 @@ init(async (launch) => { { path: '/tags/:tag', component: MkTag }, { path: '/share', component: MkShare }, { path: '/reversi/:game?', component: MkReversi }, - { path: '/@:user', component: MkUser }, - { path: '/notes/:note', component: MkNote }, + { path: '/@:user', name: 'user', component: MkUser }, + { path: '/notes/:note', name: 'note', component: MkNote }, { path: '/authorize-follow', component: MkFollow } ] }); diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue index b119f23d7a..dce5b12615 100644 --- a/src/client/app/desktop/views/components/note-detail.vue +++ b/src/client/app/desktop/views/components/note-detail.vue @@ -91,7 +91,7 @@ import MkPostFormWindow from './post-form-window.vue'; import MkRenoteFormWindow from './renote-form-window.vue'; import MkNoteMenu from '../../../common/views/components/note-menu.vue'; import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; -import XSub from './notes.note.sub.vue'; +import XSub from './note.sub.vue'; import { sum } from '../../../../../prelude/array'; import noteSubscriber from '../../../common/scripts/note-subscriber'; diff --git a/src/client/app/desktop/views/components/notes.note.sub.vue b/src/client/app/desktop/views/components/note.sub.vue index ee52670f8f..5ba22fc76f 100644 --- a/src/client/app/desktop/views/components/notes.note.sub.vue +++ b/src/client/app/desktop/views/components/note.sub.vue @@ -1,5 +1,5 @@ <template> -<div class="tkfdzaxtkdeianobciwadajxzbddorql" :title="title"> +<div class="tkfdzaxtkdeianobciwadajxzbddorql" :class="{ mini }" :title="title"> <mk-avatar class="avatar" :user="note.user"/> <div class="main"> <mk-note-header class="header" :note="note"/> @@ -24,6 +24,11 @@ export default Vue.extend({ note: { type: Object, required: true + }, + mini: { + type: Boolean, + required: false, + default: false } }, @@ -44,11 +49,19 @@ export default Vue.extend({ <style lang="stylus" scoped> .tkfdzaxtkdeianobciwadajxzbddorql display flex - margin 0 padding 16px 32px font-size 0.9em background var(--subNoteBg) + &.mini + padding 16px + font-size 10px + + > .avatar + margin 0 8px 0 0 + width 38px + height 38px + > .avatar flex-shrink 0 display block diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/note.vue index 7714557ae7..7fa423b19b 100644 --- a/src/client/app/desktop/views/components/notes.note.vue +++ b/src/client/app/desktop/views/components/note.vue @@ -1,7 +1,17 @@ <template> -<div class="note" v-show="appearNote.deletedAt == null" :tabindex="appearNote.deletedAt == null ? '-1' : null" v-hotkey="keymap" :title="title"> +<div + class="note" + :class="{ mini }" + v-show="appearNote.deletedAt == null" + :tabindex="appearNote.deletedAt == null ? '-1' : null" + v-hotkey="keymap" + :title="title" +> + <div class="conversation" v-if="detail && conversation.length > 0"> + <x-sub v-for="note in conversation" :key="note.id" :note="note" :mini="mini"/> + </div> <div class="reply-to" v-if="appearNote.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)"> - <x-sub :note="appearNote.reply"/> + <x-sub :note="appearNote.reply" :mini="mini"/> </div> <div class="renote" v-if="isRenote"> <mk-avatar class="avatar" :user="note.user"/> @@ -32,8 +42,8 @@ </div> <mk-poll v-if="appearNote.poll" :note="appearNote" ref="pollViewer"/> <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:map-marker-alt% 位置情報</a> - <div class="renote" v-if="appearNote.renote"><mk-note-preview :note="appearNote.renote"/></div> - <mk-url-preview v-for="url in urls" :url="url" :key="url"/> + <div class="renote" v-if="appearNote.renote"><mk-note-preview :note="appearNote.renote" :mini="mini"/></div> + <mk-url-preview v-for="url in urls" :url="url" :key="url" :mini="mini"/> </div> </div> <footer> @@ -55,15 +65,16 @@ </footer> </div> </article> + <div class="replies" v-if="detail && replies.length > 0"> + <x-sub v-for="note in replies" :key="note.id" :note="note" :mini="mini"/> + </div> </div> </template> <script lang="ts"> import Vue from 'vue'; -import MkPostFormWindow from './post-form-window.vue'; -import MkRenoteFormWindow from './renote-form-window.vue'; -import XSub from './notes.note.sub.vue'; +import XSub from './note.sub.vue'; import noteMixin from '../../../common/scripts/note-mixin'; import noteSubscriber from '../../../common/scripts/note-subscriber'; @@ -81,6 +92,40 @@ export default Vue.extend({ note: { type: Object, required: true + }, + detail: { + type: Boolean, + required: false, + default: false + }, + mini: { + type: Boolean, + required: false, + default: false + } + }, + + data() { + return { + conversation: [], + replies: [] + }; + }, + + created() { + if (this.detail) { + (this as any).api('notes/replies', { + noteId: this.appearNote.id, + limit: 8 + }).then(replies => { + this.replies = replies; + }); + + (this as any).api('notes/conversation', { + noteId: this.appearNote.replyId + }).then(conversation => { + this.conversation = conversation.reverse(); + }); } } }); @@ -93,14 +138,23 @@ export default Vue.extend({ background var(--face) border-bottom solid 1px var(--faceDivider) - &[data-round] - &:first-child - border-top-left-radius 6px - border-top-right-radius 6px + &.mini + font-size 13px + + > .renote + padding 8px 16px 0 16px + + .avatar + width 20px + height 20px - > .renote - border-top-left-radius 6px - border-top-right-radius 6px + > article + padding 16px 16px 4px + + > .avatar + margin 0 10px 8px 0 + width 42px + height 42px &:last-of-type border-bottom none @@ -129,6 +183,7 @@ export default Vue.extend({ background linear-gradient(to bottom, var(--renoteGradient) 0%, var(--face) 100%) .avatar + flex-shrink 0 display inline-block width 28px height 28px @@ -273,6 +328,9 @@ export default Vue.extend({ border none cursor pointer + &:last-child + margin-right 0 + &:hover color var(--noteActionsHover) diff --git a/src/client/app/desktop/views/components/notes.vue b/src/client/app/desktop/views/components/notes.vue index 219fd5db60..768dd4f9d1 100644 --- a/src/client/app/desktop/views/components/notes.vue +++ b/src/client/app/desktop/views/components/notes.vue @@ -40,7 +40,7 @@ import Vue from 'vue'; import * as config from '../../../config'; import getNoteSummary from '../../../../../misc/get-note-summary'; -import XNote from './notes.note.vue'; +import XNote from './note.vue'; const displayLimit = 30; diff --git a/src/client/app/desktop/views/components/settings.2fa.vue b/src/client/app/desktop/views/components/settings.2fa.vue index 3e8c860eba..98e73a95b3 100644 --- a/src/client/app/desktop/views/components/settings.2fa.vue +++ b/src/client/app/desktop/views/components/settings.2fa.vue @@ -2,10 +2,10 @@ <div class="2fa"> <p>%i18n:@intro%<a href="%i18n:@url%" target="_blank">%i18n:@detail%</a></p> <div class="ui info warn"><p>%fa:exclamation-triangle%%i18n:@caution%</p></div> - <p v-if="!data && !$store.state.i.twoFactorEnabled"><button @click="register" class="ui primary">%i18n:@register%</button></p> + <p v-if="!data && !$store.state.i.twoFactorEnabled"><ui-button @click="register">%i18n:@register%</ui-button></p> <template v-if="$store.state.i.twoFactorEnabled"> <p>%i18n:@already-registered%</p> - <button @click="unregister" class="ui">%i18n:@unregister%</button> + <ui-button @click="unregister">%i18n:@unregister%</ui-button> </template> <div v-if="data"> <ol> @@ -13,7 +13,7 @@ <li>%i18n:@scan%<br><img :src="data.qr"></li> <li>%i18n:@done%<br> <input type="number" v-model="token" class="ui"> - <button @click="submit" class="ui primary">%i18n:@submit%</button> + <ui-button primary @click="submit">%i18n:@submit%</ui-button> </li> </ol> <div class="ui info"><p>%fa:info-circle%%i18n:@info%</p></div> diff --git a/src/client/app/desktop/views/components/settings.api.vue b/src/client/app/desktop/views/components/settings.api.vue index 113764c3e1..f0103b5d43 100644 --- a/src/client/app/desktop/views/components/settings.api.vue +++ b/src/client/app/desktop/views/components/settings.api.vue @@ -1,10 +1,12 @@ <template> <div class="root api"> - <p>%i18n:@token% <code>{{ $store.state.i.token }}</code></p> + <ui-input :value="$store.state.i.token" readonly> + <span>%i18n:@token%</span> + </ui-input> <p>%i18n:@intro%</p> <div class="ui info warn"><p>%fa:exclamation-triangle%%i18n:@caution%</p></div> <p>%i18n:@regeneration-of-token%</p> - <button class="ui" @click="regenerateToken">%i18n:@regenerate-token%</button> + <ui-button @click="regenerateToken">%i18n:@regenerate-token%</ui-button> </div> </template> diff --git a/src/client/app/desktop/views/components/settings.password.vue b/src/client/app/desktop/views/components/settings.password.vue index 39896daf67..82b163f1fa 100644 --- a/src/client/app/desktop/views/components/settings.password.vue +++ b/src/client/app/desktop/views/components/settings.password.vue @@ -1,6 +1,6 @@ <template> <div> - <button @click="reset" class="ui primary">%i18n:@reset%</button> + <ui-button @click="reset">%i18n:@reset%</ui-button> </div> </template> diff --git a/src/client/app/desktop/views/components/settings.profile.vue b/src/client/app/desktop/views/components/settings.profile.vue deleted file mode 100644 index 15569d3694..0000000000 --- a/src/client/app/desktop/views/components/settings.profile.vue +++ /dev/null @@ -1,106 +0,0 @@ -<template> -<div class="profile"> - <label class="avatar ui from group"> - <p>%i18n:@avatar%</p> - <img class="avatar" :src="$store.state.i.avatarUrl" alt="avatar"/> - <button class="ui" @click="updateAvatar">%i18n:@choice-avatar%</button> - </label> - <label class="ui from group"> - <ui-input v-model="name" type="text">%i18n:@name%</ui-input> - </label> - <label class="ui from group"> - <ui-input v-model="location" type="text">%i18n:@location%</ui-input> - </label> - <label class="ui from group"> - <ui-textarea v-model="description">%i18n:@description%</ui-textarea> - </label> - <label class="ui from group"> - <p>%i18n:@birthday%</p> - <input type="date" v-model="birthday"/> - </label> - <ui-button primary @click="save">%i18n:@save%</ui-button> - <section> - <h2>%i18n:@locked-account%</h2> - <ui-switch v-model="isLocked" @change="save(false)">%i18n:@is-locked%</ui-switch> - <ui-switch v-model="carefulBot" @change="save(false)">%i18n:@careful-bot%</ui-switch> - </section> - <section> - <h2>%i18n:@other%</h2> - <ui-switch v-model="isBot" @change="save(false)">%i18n:@is-bot%</ui-switch> - <ui-switch v-model="isCat" @change="save(false)">%i18n:@is-cat%</ui-switch> - <ui-switch v-model="alwaysMarkNsfw">%i18n:common.always-mark-nsfw%</ui-switch> - </section> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - data() { - return { - name: null, - location: null, - description: null, - birthday: null, - isBot: false, - isCat: false, - isLocked: false, - carefulBot: false, - }; - }, - computed: { - alwaysMarkNsfw: { - get() { return this.$store.state.i.settings.alwaysMarkNsfw; }, - set(value) { (this as any).api('i/update', { alwaysMarkNsfw: value }); } - }, - }, - created() { - this.name = this.$store.state.i.name || ''; - this.location = this.$store.state.i.profile.location; - this.description = this.$store.state.i.description; - this.birthday = this.$store.state.i.profile.birthday; - this.isCat = this.$store.state.i.isCat; - this.isBot = this.$store.state.i.isBot; - this.isLocked = this.$store.state.i.isLocked; - this.carefulBot = this.$store.state.i.carefulBot; - }, - methods: { - updateAvatar() { - (this as any).apis.updateAvatar(); - }, - save(notify) { - (this as any).api('i/update', { - name: this.name || null, - location: this.location || null, - description: this.description || null, - birthday: this.birthday || null, - isCat: this.isCat, - isBot: this.isBot, - isLocked: this.isLocked, - carefulBot: this.carefulBot - }).then(() => { - if (notify) { - (this as any).apis.notify('%i18n:@profile-updated%'); - } - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.profile - > .avatar - > img - display inline-block - vertical-align top - width 64px - height 64px - border-radius 4px - - > button - margin-left 8px - -</style> - diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue index 778302a7a5..1a8cb6b0b9 100644 --- a/src/client/app/desktop/views/components/settings.vue +++ b/src/client/app/desktop/views/components/settings.vue @@ -2,38 +2,59 @@ <div class="mk-settings"> <div class="nav"> <p :class="{ active: page == 'profile' }" @mousedown="page = 'profile'">%fa:user .fw%%i18n:@profile%</p> + <p :class="{ active: page == 'theme' }" @mousedown="page = 'theme'">%fa:palette .fw%%i18n:@theme%</p> <p :class="{ active: page == 'web' }" @mousedown="page = 'web'">%fa:desktop .fw%Web</p> <p :class="{ active: page == 'notification' }" @mousedown="page = 'notification'">%fa:R bell .fw%%i18n:@notification%</p> <p :class="{ active: page == 'drive' }" @mousedown="page = 'drive'">%fa:cloud .fw%%i18n:@drive%</p> <p :class="{ active: page == 'hashtags' }" @mousedown="page = 'hashtags'">%fa:hashtag .fw%%i18n:@tags%</p> <p :class="{ active: page == 'mute' }" @mousedown="page = 'mute'">%fa:ban .fw%%i18n:@mute%</p> <p :class="{ active: page == 'apps' }" @mousedown="page = 'apps'">%fa:puzzle-piece .fw%%i18n:@apps%</p> - <p :class="{ active: page == 'twitter' }" @mousedown="page = 'twitter'">%fa:B twitter .fw%Twitter</p> <p :class="{ active: page == 'security' }" @mousedown="page = 'security'">%fa:unlock-alt .fw%%i18n:@security%</p> <p :class="{ active: page == 'api' }" @mousedown="page = 'api'">%fa:key .fw%API</p> <p :class="{ active: page == 'other' }" @mousedown="page = 'other'">%fa:cogs .fw%%i18n:@other%</p> </div> <div class="pages"> - <section class="profile" v-show="page == 'profile'"> - <h1>%i18n:@profile%</h1> - <x-profile/> - </section> + <div class="profile" v-show="page == 'profile'"> + <mk-profile-editor/> - <section class="web" v-show="page == 'web'"> - <h1>%i18n:@theme%</h1> - <mk-theme/> - </section> + <ui-card> + <div slot="title">%fa:B twitter% %i18n:@twitter%</div> + <section> + <mk-twitter-setting/> + </section> + </ui-card> + </div> + + <ui-card class="theme" v-show="page == 'theme'"> + <div slot="title">%fa:palette% %i18n:@theme%</div> + + <section> + <mk-theme/> + </section> + </ui-card> + + <ui-card class="web" v-show="page == 'web'"> + <div slot="title">%fa:sliders-h% %i18n:@behaviour%</div> + + <section> + <ui-switch v-model="fetchOnScroll"> + %i18n:@fetch-on-scroll% + <span slot="desc">%i18n:@fetch-on-scroll-desc%</span> + </ui-switch> + <ui-switch v-model="autoPopout"> + %i18n:@auto-popout% + <span slot="desc">%i18n:@auto-popout-desc%</span> + </ui-switch> + <ui-switch v-model="deckNav">%i18n:@deck-nav%<span slot="desc">%i18n:@deck-nav-desc%</span></ui-switch> - <section class="web" v-show="page == 'web'"> - <h1>%i18n:@behaviour%</h1> - <ui-switch v-model="fetchOnScroll"> - %i18n:@fetch-on-scroll% - <span slot="desc">%i18n:@fetch-on-scroll-desc%</span> - </ui-switch> - <ui-switch v-model="autoPopout"> - %i18n:@auto-popout% - <span slot="desc">%i18n:@auto-popout-desc%</span> - </ui-switch> + <details> + <summary>%i18n:@advanced%</summary> + <ui-switch v-model="apiViaStream"> + %i18n:@api-via-stream% + <span slot="desc">%i18n:@api-via-stream-desc%</span> + </ui-switch> + </details> + </section> <section> <header>%i18n:@note-visibility%</header> @@ -49,24 +70,26 @@ </ui-select> </section> </section> + </ui-card> - <details> - <summary>%i18n:@advanced%</summary> - <ui-switch v-model="apiViaStream"> - %i18n:@api-via-stream% - <span slot="desc">%i18n:@api-via-stream-desc%</span> - </ui-switch> - </details> - </section> + <ui-card class="web" v-show="page == 'web'"> + <div slot="title">%fa:desktop% %i18n:@display%</div> - <section class="web" v-show="page == 'web'"> - <h1>%i18n:@display%</h1> - <div class="div"> - <button class="ui button" @click="customizeHome" style="margin-bottom: 16px">%i18n:@customize%</button> - </div> - <div class="div"> - <button class="ui" @click="updateWallpaper">%i18n:@choose-wallpaper%</button> - <button class="ui" @click="deleteWallpaper">%i18n:@delete-wallpaper%</button> + <section> + <ui-button @click="customizeHome">%i18n:@customize%</ui-button> + </section> + <section> + <header>%i18n:@wallpaper%</header> + <ui-button @click="updateWallpaper">%i18n:@choose-wallpaper%</ui-button> + <ui-button @click="deleteWallpaper">%i18n:@delete-wallpaper%</ui-button> + </section> + <section> + <header>%i18n:@navbar-position%</header> + <ui-radio v-model="navbar" value="top">%i18n:@navbar-position-top%</ui-radio> + <ui-radio v-model="navbar" value="left">%i18n:@navbar-position-left%</ui-radio> + <ui-radio v-model="navbar" value="right">%i18n:@navbar-position-right%</ui-radio> + </section> + <section> <ui-switch v-model="darkmode">%i18n:@dark-mode%</ui-switch> <ui-switch v-model="useShadow">%i18n:@use-shadow%</ui-switch> <ui-switch v-model="roundedCorners">%i18n:@rounded-corners%</ui-switch> @@ -75,171 +98,186 @@ <ui-switch v-model="contrastedAcct">%i18n:@contrasted-acct%</ui-switch> <ui-switch v-model="showFullAcct">%i18n:common.show-full-acct%</ui-switch> <ui-switch v-model="iLikeSushi">%i18n:common.i-like-sushi%</ui-switch> - </div> - <ui-switch v-model="showPostFormOnTopOfTl">%i18n:@post-form-on-timeline%</ui-switch> - <ui-switch v-model="suggestRecentHashtags">%i18n:@suggest-recent-hashtags%</ui-switch> - <ui-switch v-model="showClockOnHeader">%i18n:@show-clock-on-header%</ui-switch> - <ui-switch v-model="alwaysShowNsfw">%i18n:common.always-show-nsfw%</ui-switch> - <ui-switch v-model="showReplyTarget">%i18n:@show-reply-target%</ui-switch> - <ui-switch v-model="showMyRenotes">%i18n:@show-my-renotes%</ui-switch> - <ui-switch v-model="showRenotedMyNotes">%i18n:@show-renoted-my-notes%</ui-switch> - <ui-switch v-model="showLocalRenotes">%i18n:@show-local-renotes%</ui-switch> - <ui-switch v-model="showMaps">%i18n:@show-maps%</ui-switch> - <ui-switch v-model="disableAnimatedMfm">%i18n:common.disable-animated-mfm%</ui-switch> - <ui-switch v-model="games_reversi_showBoardLabels">%i18n:common.show-reversi-board-labels%</ui-switch> - <ui-switch v-model="games_reversi_useContrastStones">%i18n:common.use-contrast-reversi-stones%</ui-switch> - + </section> <section> - <header>%i18n:@navbar-position%</header> - <ui-radio v-model="navbar" value="top">%i18n:@navbar-position-top%</ui-radio> - <ui-radio v-model="navbar" value="left">%i18n:@navbar-position-left%</ui-radio> - <ui-radio v-model="navbar" value="right">%i18n:@navbar-position-right%</ui-radio> + <ui-switch v-model="showPostFormOnTopOfTl">%i18n:@post-form-on-timeline%</ui-switch> + <ui-switch v-model="suggestRecentHashtags">%i18n:@suggest-recent-hashtags%</ui-switch> + <ui-switch v-model="showClockOnHeader">%i18n:@show-clock-on-header%</ui-switch> + <ui-switch v-model="alwaysShowNsfw">%i18n:common.always-show-nsfw%</ui-switch> + <ui-switch v-model="showReplyTarget">%i18n:@show-reply-target%</ui-switch> + <ui-switch v-model="showMyRenotes">%i18n:@show-my-renotes%</ui-switch> + <ui-switch v-model="showRenotedMyNotes">%i18n:@show-renoted-my-notes%</ui-switch> + <ui-switch v-model="showLocalRenotes">%i18n:@show-local-renotes%</ui-switch> + <ui-switch v-model="showMaps">%i18n:@show-maps%</ui-switch> + <ui-switch v-model="disableAnimatedMfm">%i18n:common.disable-animated-mfm%</ui-switch> + <ui-switch v-model="games_reversi_showBoardLabels">%i18n:common.show-reversi-board-labels%</ui-switch> + <ui-switch v-model="games_reversi_useContrastStones">%i18n:common.use-contrast-reversi-stones%</ui-switch> </section> - </section> + </ui-card> - <section class="web" v-show="page == 'web'"> - <h1>%i18n:@sound%</h1> - <ui-switch v-model="enableSounds"> - %i18n:@enable-sounds% - <span slot="desc">%i18n:@enable-sounds-desc%</span> - </ui-switch> - <label>%i18n:@volume%</label> - <input type="range" - v-model="soundVolume" - :disabled="!enableSounds" - max="1" - step="0.1" - /> - <button class="ui button" @click="soundTest">%fa:volume-up% %i18n:@test%</button> - </section> + <ui-card class="web" v-show="page == 'web'"> + <div slot="title">%fa:volume-up% %i18n:@sound%</div> - <section class="web" v-show="page == 'web'"> - <h1>%i18n:@mobile%</h1> - <ui-switch v-model="disableViaMobile">%i18n:@disable-via-mobile%</ui-switch> - </section> + <section> + <ui-switch v-model="enableSounds"> + %i18n:@enable-sounds% + <span slot="desc">%i18n:@enable-sounds-desc%</span> + </ui-switch> + <label>%i18n:@volume%</label> + <input type="range" + v-model="soundVolume" + :disabled="!enableSounds" + max="1" + step="0.1" + /> + <ui-button @click="soundTest">%fa:volume-up% %i18n:@test%</ui-button> + </section> + </ui-card> - <section class="web" v-show="page == 'web'"> - <h1>%i18n:@language%</h1> - <select v-model="lang" placeholder="%i18n:@pick-language%"> - <optgroup label="%i18n:@recommended%"> - <option value="">%i18n:@auto%</option> - </optgroup> + <ui-card class="web" v-show="page == 'web'"> + <div slot="title">%fa:language% %i18n:@language%</div> + <section class="fit-top"> + <ui-select v-model="lang" placeholder="%i18n:@pick-language%"> + <optgroup label="%i18n:@recommended%"> + <option value="">%i18n:@auto%</option> + </optgroup> - <optgroup label="%i18n:@specify-language%"> - <option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option> - </optgroup> - </select> - <div class="none ui info"> - <p>%fa:info-circle%%i18n:@language-desc%</p> - </div> - </section> + <optgroup label="%i18n:@specify-language%"> + <option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option> + </optgroup> + </ui-select> + <div class="none ui info"> + <p>%fa:info-circle%%i18n:@language-desc%</p> + </div> + </section> + </ui-card> - <section class="web" v-show="page == 'web'"> - <h1>%i18n:@cache%</h1> - <button class="ui button" @click="clean">%i18n:@clean-cache%</button> - <div class="none ui info warn"> - <p>%fa:exclamation-triangle%%i18n:@cache-warn%</p> - </div> - </section> + <ui-card class="web" v-show="page == 'web'"> + <div slot="title">%fa:trash-alt R% %i18n:@cache%</div> + <section> + <ui-button @click="clean">%i18n:@clean-cache%</ui-button> + <div class="none ui info warn"> + <p>%fa:exclamation-triangle%%i18n:@cache-warn%</p> + </div> + </section> + </ui-card> - <section class="notification" v-show="page == 'notification'"> - <h1>%i18n:@notification%</h1> - <ui-switch v-model="$store.state.i.settings.autoWatch" @change="onChangeAutoWatch"> - %i18n:@auto-watch% - <span slot="desc">%i18n:@auto-watch-desc%</span> - </ui-switch> - </section> + <ui-card class="notification" v-show="page == 'notification'"> + <div slot="title">%fa:bell R% %i18n:@notification%</div> + <section> + <ui-switch v-model="$store.state.i.settings.autoWatch" @change="onChangeAutoWatch"> + %i18n:@auto-watch% + <span slot="desc">%i18n:@auto-watch-desc%</span> + </ui-switch> + </section> + </ui-card> - <section class="drive" v-show="page == 'drive'"> - <h1>%i18n:@drive%</h1> - <x-drive/> - </section> + <ui-card class="drive" v-show="page == 'drive'"> + <div slot="title">%fa:cloud% %i18n:@drive%</div> + <section> + <x-drive/> + </section> + </ui-card> - <section class="hashtags" v-show="page == 'hashtags'"> - <h1>%i18n:@tags%</h1> - <x-tags/> - </section> + <ui-card class="hashtags" v-show="page == 'hashtags'"> + <div slot="title">%fa:hashtag% %i18n:@tags%</div> + <section> + <x-tags/> + </section> + </ui-card> - <section class="mute" v-show="page == 'mute'"> - <h1>%i18n:@mute%</h1> - <x-mute/> - </section> + <ui-card class="mute" v-show="page == 'mute'"> + <div slot="title">%fa:ban% %i18n:@mute%</div> + <section> + <x-mute/> + </section> + </ui-card> - <section class="apps" v-show="page == 'apps'"> - <h1>%i18n:@apps%</h1> - <x-apps/> - </section> + <ui-card class="apps" v-show="page == 'apps'"> + <div slot="title">%fa:puzzle-piece% %i18n:@apps%</div> + <section> + <x-apps/> + </section> + </ui-card> - <section class="twitter" v-show="page == 'twitter'"> - <h1>Twitter</h1> - <mk-twitter-setting/> - </section> + <ui-card class="password" v-show="page == 'security'"> + <div slot="title">%fa:unlock-alt% %i18n:@password%</div> + <section> + <x-password/> + </section> + </ui-card> - <section class="password" v-show="page == 'security'"> - <h1>%i18n:@password%</h1> - <x-password/> - </section> + <ui-card class="2fa" v-show="page == 'security'"> + <div slot="title">%fa:mobile-alt% %i18n:@2fa%</div> + <section> + <x-2fa/> + </section> + </ui-card> - <section class="2fa" v-show="page == 'security'"> - <h1>%i18n:@2fa%</h1> - <x-2fa/> - </section> + <ui-card class="signin" v-show="page == 'security'"> + <div slot="title">%fa:sign-in-alt% %i18n:@signin%</div> + <section> + <x-signins/> + </section> + </ui-card> - <section class="signin" v-show="page == 'security'"> - <h1>%i18n:@signin%</h1> - <x-signins/> - </section> + <ui-card class="api" v-show="page == 'api'"> + <div slot="title">%fa:key% API</div> + <section class="fit-top"> + <x-api/> + </section> + </ui-card> - <section class="api" v-show="page == 'api'"> - <h1>API</h1> - <x-api/> - </section> + <ui-card class="other" v-show="page == 'other'"> + <div slot="title">%fa:info-circle% %i18n:@about%</div> + <section> + <p v-if="meta">%i18n:@operator%: <i><a :href="meta.maintainer.url" target="_blank">{{ meta.maintainer.name }}</a></i></p> + </section> + </ui-card> - <section class="other" v-show="page == 'other'"> - <h1>%i18n:@about%</h1> - <p v-if="meta">%i18n:@operator%: <i><a :href="meta.maintainer.url" target="_blank">{{ meta.maintainer.name }}</a></i></p> - </section> + <ui-card class="other" v-show="page == 'other'"> + <div slot="title">%fa:sync-alt% %i18n:@update%</div> + <section> + <p> + <span>%i18n:@version% <i>{{ version }}</i></span> + <template v-if="latestVersion !== undefined"> + <br> + <span>%i18n:@latest-version% <i>{{ latestVersion ? latestVersion : version }}</i></span> + </template> + </p> + <button class="ui button block" @click="checkForUpdate" :disabled="checkingForUpdate"> + <template v-if="checkingForUpdate">%i18n:@update-checking%<mk-ellipsis/></template> + <template v-else>%i18n:@do-update%</template> + </button> + <details> + <summary>%i18n:@update-settings%</summary> + <ui-switch v-model="preventUpdate"> + %i18n:@prevent-update% + <span slot="desc">%i18n:@prevent-update-desc%</span> + </ui-switch> + </details> + </section> + </ui-card> - <section class="other" v-show="page == 'other'"> - <h1>%i18n:@update%</h1> - <p> - <span>%i18n:@version% <i>{{ version }}</i></span> - <template v-if="latestVersion !== undefined"> - <br> - <span>%i18n:@latest-version% <i>{{ latestVersion ? latestVersion : version }}</i></span> - </template> - </p> - <button class="ui button block" @click="checkForUpdate" :disabled="checkingForUpdate"> - <template v-if="checkingForUpdate">%i18n:@update-checking%<mk-ellipsis/></template> - <template v-else>%i18n:@do-update%</template> - </button> - <details> - <summary>%i18n:@update-settings%</summary> - <ui-switch v-model="preventUpdate"> - %i18n:@prevent-update% - <span slot="desc">%i18n:@prevent-update-desc%</span> + <ui-card class="other" v-show="page == 'other'"> + <div slot="title">%fa:cogs% %i18n:@advanced-settings%</div> + <section> + <ui-switch v-model="debug"> + %i18n:@debug-mode% + <span slot="desc">%i18n:@debug-mode-desc%</span> </ui-switch> - </details> - </section> - - <section class="other" v-show="page == 'other'"> - <h1>%i18n:@advanced-settings%</h1> - <ui-switch v-model="debug"> - %i18n:@debug-mode% - <span slot="desc">%i18n:@debug-mode-desc%</span> - </ui-switch> - <ui-switch v-model="enableExperimentalFeatures"> - %i18n:@experimental% - <span slot="desc">%i18n:@experimental-desc%</span> - </ui-switch> - </section> + <ui-switch v-model="enableExperimentalFeatures"> + %i18n:@experimental% + <span slot="desc">%i18n:@experimental-desc%</span> + </ui-switch> + </section> + </ui-card> </div> </div> </template> <script lang="ts"> import Vue from 'vue'; -import XProfile from './settings.profile.vue'; import XMute from './settings.mute.vue'; import XPassword from './settings.password.vue'; import X2fa from './settings.2fa.vue'; @@ -253,7 +291,6 @@ import checkForUpdate from '../../../common/scripts/check-for-update'; export default Vue.extend({ components: { - XProfile, XMute, XPassword, X2fa, @@ -295,6 +332,11 @@ export default Vue.extend({ set(value) { this.$store.commit('device/set', { key: 'autoPopout', value }); } }, + deckNav: { + get() { return this.$store.state.settings.deckNav; }, + set(value) { this.$store.commit('settings/set', { key: 'deckNav', value }); } + }, + darkmode: { get() { return this.$store.state.device.darkmode; }, set(value) { this.$store.commit('device/set', { key: 'darkmode', value }); } @@ -438,11 +480,6 @@ export default Vue.extend({ disableAnimatedMfm: { get() { return this.$store.state.settings.disableAnimatedMfm; }, set(value) { this.$store.dispatch('settings/set', { key: 'disableAnimatedMfm', value }); } - }, - - disableViaMobile: { - get() { return this.$store.state.settings.disableViaMobile; }, - set(value) { this.$store.dispatch('settings/set', { key: 'disableViaMobile', value }); } } }, created() { @@ -546,34 +583,10 @@ export default Vue.extend({ height 100% flex auto overflow auto + background var(--bg) > section margin 32px color var(--text) - > h1 - margin 0 0 1em 0 - padding 0 0 8px 0 - font-size 1em - border-bottom solid 1px var(--faceDivider) - - &, >>> * - .ui.button.block - margin 16px 0 - - > section - margin 32px 0 - - > h2 - margin 0 0 1em 0 - padding 0 0 8px 0 - font-size 1em - color var(--text) - border-bottom solid 1px var(--faceDivider) - - > .web - > .div - border-bottom solid 1px var(--faceDivider) - margin 16px 0 - </style> diff --git a/src/client/app/desktop/views/pages/deck/deck.column.vue b/src/client/app/desktop/views/pages/deck/deck.column.vue index e2c7e83199..aced5848e4 100644 --- a/src/client/app/desktop/views/pages/deck/deck.column.vue +++ b/src/client/app/desktop/views/pages/deck/deck.column.vue @@ -1,7 +1,6 @@ <template> <div class="dnpfarvgbnfmyzbdquhhzyxcmstpdqzs" :class="{ naked, narrow, active, isStacked, draghover, dragging, dropready }" @dragover.prevent.stop="onDragover" - @dragenter.prevent="onDragenter" @dragleave="onDragleave" @drop.prevent.stop="onDrop"> <header :class="{ indicate: count > 0 }" @@ -16,7 +15,8 @@ </button> <slot name="header"></slot> <span class="count" v-if="count > 0">({{ count }})</span> - <button class="menu" ref="menu" @click.stop="showMenu">%fa:caret-down%</button> + <button v-if="!isTemporaryColumn" class="menu" ref="menu" @click.stop="showMenu">%fa:caret-down%</button> + <button v-else class="close" @click.stop="close">%fa:times%</button> </header> <div ref="body" v-show="active"> <slot></slot> @@ -34,11 +34,13 @@ export default Vue.extend({ props: { column: { type: Object, - required: true + required: false, + default: null }, isStacked: { type: Boolean, - required: true + required: false, + default: false }, name: { type: String, @@ -61,6 +63,12 @@ export default Vue.extend({ } }, + computed: { + isTemporaryColumn(): boolean { + return this.column == null; + } + }, + inject: { getColumnVm: { from: 'getColumnVm' } }, @@ -96,14 +104,20 @@ export default Vue.extend({ mounted() { this.$refs.body.addEventListener('scroll', this.onScroll, { passive: true }); - this.$root.$on('deck.column.dragStart', this.onOtherDragStart); - this.$root.$on('deck.column.dragEnd', this.onOtherDragEnd); + + if (!this.isTemporaryColumn) { + this.$root.$on('deck.column.dragStart', this.onOtherDragStart); + this.$root.$on('deck.column.dragEnd', this.onOtherDragEnd); + } }, beforeDestroy() { this.$refs.body.removeEventListener('scroll', this.onScroll); - this.$root.$off('deck.column.dragStart', this.onOtherDragStart); - this.$root.$off('deck.column.dragEnd', this.onOtherDragEnd); + + if (!this.isTemporaryColumn) { + this.$root.$off('deck.column.dragStart', this.onOtherDragStart); + this.$root.$off('deck.column.dragEnd', this.onOtherDragEnd); + } }, methods: { @@ -203,6 +217,7 @@ export default Vue.extend({ }, onContextmenu(e) { + if (this.isTemporaryColumn) return; contextmenu((this as any).os)(e, this.getMenu()); }, @@ -214,6 +229,13 @@ export default Vue.extend({ }); }, + close() { + this.$store.commit('device/set', { + key: 'deckTemporaryColumn', + value: null + }); + }, + goTop() { this.$refs.body.scrollTo({ top: 0, @@ -222,6 +244,12 @@ export default Vue.extend({ }, onDragstart(e) { + // テンポラリカラムはドラッグさせない + if (this.isTemporaryColumn) { + e.preventDefault(); + return; + } + e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('mk-deck-column', this.column.id); this.dragging = true; @@ -232,6 +260,12 @@ export default Vue.extend({ }, onDragover(e) { + // テンポラリカラムにはドロップさせない + if (this.isTemporaryColumn) { + e.dataTransfer.dropEffect = 'none'; + return; + } + // 自分自身がドラッグされている場合 if (this.dragging) { // 自分自身にはドロップさせない @@ -242,9 +276,7 @@ export default Vue.extend({ const isDeckColumn = e.dataTransfer.types[0] == 'mk-deck-column'; e.dataTransfer.dropEffect = isDeckColumn ? 'move' : 'none'; - }, - onDragenter() { if (!this.dragging) this.draghover = true; }, @@ -349,6 +381,7 @@ export default Vue.extend({ > .toggleActive > .menu + > .close padding 0 width $header-height line-height $header-height @@ -365,6 +398,7 @@ export default Vue.extend({ margin-left -16px > .menu + > .close margin-left auto margin-right -16px diff --git a/src/client/app/desktop/views/pages/deck/deck.note-column.vue b/src/client/app/desktop/views/pages/deck/deck.note-column.vue new file mode 100644 index 0000000000..b8386e5b3d --- /dev/null +++ b/src/client/app/desktop/views/pages/deck/deck.note-column.vue @@ -0,0 +1,69 @@ +<template> +<x-column> + <span slot="header"> + %fa:comment-alt R%<span>{{ title }}</span> + </span> + + <div class="rvtscbadixhhbsczoorqoaygovdeecsx" v-if="note"> + <div class="is-remote" v-if="note.user.host != null">%fa:exclamation-triangle% %i18n:@is-remote%<a :href="note.url || note.uri" target="_blank">%i18n:@view-remote%</a></div> + <x-note :note="note" :detail="true" :mini="true"/> + </div> +</x-column> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XColumn from './deck.column.vue'; +import XNotes from './deck.notes.vue'; +import XNote from '../../components/note.vue'; + +export default Vue.extend({ + components: { + XColumn, + XNotes, + XNote + }, + + props: { + noteId: { + type: String, + required: true + } + }, + + data() { + return { + note: null, + fetching: true + }; + }, + + computed: { + title(): string { + return this.note ? Vue.filter('userName')(this.note.user) : ''; + } + }, + + created() { + (this as any).api('notes/show', { noteId: this.noteId }).then(note => { + this.note = note; + this.fetching = false; + }); + } +}); +</script> + +<style lang="stylus" scoped> +.rvtscbadixhhbsczoorqoaygovdeecsx + > .is-remote + padding 8px 16px + font-size 12px + + &.is-remote + color var(--remoteInfoFg) + background var(--remoteInfoBg) + + > a + font-weight bold + +</style> diff --git a/src/client/app/desktop/views/pages/deck/deck.note.sub.vue b/src/client/app/desktop/views/pages/deck/deck.note.sub.vue deleted file mode 100644 index 445bf7e365..0000000000 --- a/src/client/app/desktop/views/pages/deck/deck.note.sub.vue +++ /dev/null @@ -1,71 +0,0 @@ -<template> -<div class="fnlfosztlhtptnongximhlbykxblytcq"> - <mk-avatar class="avatar" :user="note.user"/> - <div class="main"> - <mk-note-header class="header" :note="note" :mini="true"/> - <div class="body"> - <mk-sub-note-content class="text" :note="note"/> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - note: { - type: Object, - required: true - }, - // TODO - truncate: { - type: Boolean, - default: true - } - } -}); -</script> - -<style lang="stylus" scoped> -.fnlfosztlhtptnongximhlbykxblytcq - display flex - padding 16px - font-size 10px - background var(--subNoteBg) - - &.smart - > .main - width 100% - - > header - align-items center - - > .avatar - flex-shrink 0 - display block - margin 0 8px 0 0 - width 38px - height 38px - border-radius 8px - - > .main - flex 1 - min-width 0 - - > .header - margin-bottom 2px - - > .body - - > .text - margin 0 - padding 0 - color var(--subNoteText) - - pre - max-height 120px - font-size 80% - -</style> diff --git a/src/client/app/desktop/views/pages/deck/deck.note.vue b/src/client/app/desktop/views/pages/deck/deck.note.vue deleted file mode 100644 index 03cf3e2911..0000000000 --- a/src/client/app/desktop/views/pages/deck/deck.note.vue +++ /dev/null @@ -1,323 +0,0 @@ -<template> -<div - v-if="!mediaView" - v-show="appearNote.deletedAt == null" - :tabindex="appearNote.deletedAt == null ? '-1' : null" - class="zyjjkidcqjnlegkqebitfviomuqmseqk" - :class="{ renote: isRenote }" - v-hotkey="keymap" - :title="title" -> - <div class="reply-to" v-if="appearNote.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)"> - <x-sub :note="appearNote.reply"/> - </div> - <div class="renote" v-if="isRenote"> - <mk-avatar class="avatar" :user="note.user"/> - %fa:retweet% - <span>{{ '%i18n:@reposted-by%'.substr(0, '%i18n:@reposted-by%'.indexOf('{')) }}</span> - <router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link> - <span>{{ '%i18n:@reposted-by%'.substr('%i18n:@reposted-by%'.indexOf('}') + 1) }}</span> - <mk-time :time="note.createdAt"/> - </div> - <article> - <mk-avatar class="avatar" :user="appearNote.user"/> - <div class="main"> - <mk-note-header class="header" :note="appearNote" :mini="true"/> - <div class="body"> - <p v-if="appearNote.cw != null" class="cw"> - <span class="text" v-if="appearNote.cw != ''">{{ appearNote.cw }}</span> - <mk-cw-button v-model="showContent"/> - </p> - <div class="content" v-show="appearNote.cw == null || showContent"> - <div class="text"> - <span v-if="appearNote.isHidden" style="opacity: 0.5">(%i18n:@private%)</span> - <a class="reply" v-if="appearNote.reply">%fa:reply%</a> - <misskey-flavored-markdown v-if="appearNote.text" :text="appearNote.text" :i="$store.state.i"/> - <a class="rp" v-if="appearNote.renote != null">RP:</a> - </div> - <div class="files" v-if="appearNote.files.length > 0"> - <mk-media-list :media-list="appearNote.files"/> - </div> - <mk-poll v-if="appearNote.poll" :note="appearNote" ref="pollViewer"/> - <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:map-marker-alt% %i18n:@location%</a> - <div class="renote" v-if="appearNote.renote"> - <mk-note-preview :note="appearNote.renote" :mini="true"/> - </div> - <mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="false" :mini="true"/> - </div> - <span class="app" v-if="appearNote.app">via <b>{{ appearNote.app.name }}</b></span> - </div> - <footer> - <mk-reactions-viewer :note="appearNote" ref="reactionsViewer"/> - <button @click="reply()"> - <template v-if="appearNote.reply">%fa:reply-all%</template> - <template v-else>%fa:reply%</template> - </button> - <button @click="renote()" title="Renote">%fa:retweet%</button> - <button :class="{ reacted: appearNote.myReaction != null }" @click="react()" ref="reactButton">%fa:plus%</button> - <button class="menu" @click="menu()" ref="menuButton">%fa:ellipsis-h%</button> - </footer> - </div> - </article> -</div> -<div v-else class="srwrkujossgfuhrbnvqkybtzxpblgchi"> - <div v-if="note.files.length > 0"> - <mk-media-list :media-list="note.files"/> - </div> - <div v-if="note.renote && note.renote.files.length > 0"> - <mk-media-list :media-list="note.renote.files"/> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import MkPostFormWindow from '../../components/post-form-window.vue'; -import MkRenoteFormWindow from '../../components/renote-form-window.vue'; -import XSub from './deck.note.sub.vue'; -import noteMixin from '../../../../common/scripts/note-mixin'; -import noteSubscriber from '../../../../common/scripts/note-subscriber'; - -export default Vue.extend({ - components: { - XSub - }, - - mixins: [ - noteMixin(), - noteSubscriber('note') - ], - - props: { - note: { - type: Object, - required: true - }, - mediaView: { - type: Boolean, - required: false, - default: false - } - } -}); -</script> - -<style lang="stylus" scoped> -.srwrkujossgfuhrbnvqkybtzxpblgchi - font-size 13px - margin 4px 12px - - &:first-child - margin-top 12px - - &:last-child - margin-bottom 12px - -.zyjjkidcqjnlegkqebitfviomuqmseqk - font-size 13px - border-bottom solid 1px var(--faceDivider) - - &:focus - z-index 1 - - &:after - content "" - pointer-events none - position absolute - top 2px - right 2px - bottom 2px - left 2px - border 2px solid var(--primaryAlpha03) - border-radius 4px - - &:last-of-type - border-bottom none - - &.smart - > article - > .main - > header - align-items center - margin-bottom 4px - - > .renote - display flex - align-items center - padding 8px 16px 0 16px - line-height 28px - white-space pre - color var(--renoteText) - background linear-gradient(to bottom, var(--renoteGradient) 0%, var(--face) 100%) - - .avatar - flex-shrink 0 - display inline-block - width 20px - height 20px - margin 0 8px 0 0 - border-radius 6px - - [data-fa] - margin-right 4px - - > span - flex-shrink 0 - - &:last-of-type - margin-right 8px - - .name - overflow hidden - flex-shrink 1 - text-overflow ellipsis - white-space nowrap - font-weight bold - - > .mk-time - display block - margin-left auto - flex-shrink 0 - font-size 0.9em - - & + article - padding-top 8px - - > article - display flex - padding 16px 16px 4px - - > .avatar - flex-shrink 0 - display block - margin 0 10px 8px 0 - width 42px - height 42px - border-radius 6px - //position -webkit-sticky - //position sticky - //top 62px - - > .main - flex 1 - min-width 0 - - > .body - - > .cw - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - color var(--noteText) - - > .text - margin-right 8px - - > .content - - > .text - display block - margin 0 - padding 0 - overflow-wrap break-word - color var(--noteText) - - >>> .title - display block - margin-bottom 4px - padding 4px - font-size 90% - text-align center - background var(--mfmTitleBg) - border-radius 4px - - >>> .code - margin 8px 0 - - >>> .quote - margin 8px - padding 6px 12px - color var(--mfmQuote) - border-left solid 3px var(--mfmQuoteLine) - - > .reply - margin-right 8px - color var(--noteText) - - > .rp - margin-left 4px - font-style oblique - color var(--renoteText) - - [data-is-me]:after - content "you" - padding 0 4px - margin-left 4px - font-size 80% - color var(--primaryForeground) - background var(--primary) - border-radius 4px - - .mk-url-preview - margin-top 8px - - > .files - > img - display block - max-width 100% - - > .location - margin 4px 0 - font-size 12px - color #ccc - - > .map - width 100% - height 200px - - &:empty - display none - - > .mk-poll - font-size 80% - - > .renote - margin 8px 0 - - > * - padding 16px - border dashed 1px var(--quoteBorder) - border-radius 8px - - > .app - font-size 12px - color #ccc - - > footer - > button - margin 0 - padding 4px 8px 8px 8px - background transparent - border none - box-shadow none - font-size 1em - color var(--noteActions) - cursor pointer - - &:not(:last-child) - margin-right 28px - - &:hover - color var(--noteActionsHover) - - > .count - display inline - margin 0 0 0 8px - color #999 - - &.reacted - color var(--primary) - -</style> diff --git a/src/client/app/desktop/views/pages/deck/deck.notes.vue b/src/client/app/desktop/views/pages/deck/deck.notes.vue index 5898f5b05e..c656fe9179 100644 --- a/src/client/app/desktop/views/pages/deck/deck.notes.vue +++ b/src/client/app/desktop/views/pages/deck/deck.notes.vue @@ -17,7 +17,7 @@ <!--<transition-group name="mk-notes" class="transition">--> <div class="notes"> <template v-for="(note, i) in _notes"> - <x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)" :media-view="mediaView"/> + <x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)" :media-view="mediaView" :mini="true"/> <p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date"> <span>%fa:angle-up%{{ note._datetext }}</span> <span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span> @@ -38,7 +38,7 @@ <script lang="ts"> import Vue from 'vue'; -import XNote from './deck.note.vue'; +import XNote from '../../components/note.vue'; const displayLimit = 20; @@ -220,7 +220,7 @@ export default Vue.extend({ display block margin 0 line-height 32px - font-size 14px + font-size 12px text-align center color var(--dateDividerFg) background var(--dateDividerBg) diff --git a/src/client/app/desktop/views/pages/deck/deck.notification.vue b/src/client/app/desktop/views/pages/deck/deck.notification.vue index 149bd10293..fa8f99a2ba 100644 --- a/src/client/app/desktop/views/pages/deck/deck.notification.vue +++ b/src/client/app/desktop/views/pages/deck/deck.notification.vue @@ -66,15 +66,15 @@ </div> <template v-if="notification.type == 'quote'"> - <x-note :note="notification.note" @update:note="onNoteUpdated"/> + <x-note :note="notification.note" @update:note="onNoteUpdated" :mini="true"/> </template> <template v-if="notification.type == 'reply'"> - <x-note :note="notification.note" @update:note="onNoteUpdated"/> + <x-note :note="notification.note" @update:note="onNoteUpdated" :mini="true"/> </template> <template v-if="notification.type == 'mention'"> - <x-note :note="notification.note" @update:note="onNoteUpdated"/> + <x-note :note="notification.note" @update:note="onNoteUpdated" :mini="true"/> </template> </div> </template> @@ -82,7 +82,7 @@ <script lang="ts"> import Vue from 'vue'; import getNoteSummary from '../../../../../../misc/get-note-summary'; -import XNote from './deck.note.vue'; +import XNote from '../../components/note.vue'; export default Vue.extend({ components: { diff --git a/src/client/app/desktop/views/pages/deck/deck.user-column.vue b/src/client/app/desktop/views/pages/deck/deck.user-column.vue new file mode 100644 index 0000000000..217008e0ec --- /dev/null +++ b/src/client/app/desktop/views/pages/deck/deck.user-column.vue @@ -0,0 +1,261 @@ +<template> +<x-column> + <span slot="header"> + %fa:user%<span>{{ title }}</span> + </span> + + <div class="zubukjlciycdsyynicqrnlsmdwmymzqu" v-if="user"> + <div class="is-remote" v-if="user.host != null">%fa:exclamation-triangle% %i18n:@is-remote%<a :href="user.url || user.uri" target="_blank">%i18n:@view-remote%</a></div> + <header :style="bannerStyle"> + <div> + <mk-follow-button v-if="$store.getters.isSignedIn && user.id != $store.state.i.id" :user="user" class="follow"/> + <mk-avatar class="avatar" :user="user" :disable-preview="true"/> + <span class="name">{{ user | userName }}</span> + <span class="acct">@{{ user | acct }}</span> + </div> + </header> + <div class="info"> + <div class="description"> + <misskey-flavored-markdown v-if="user.description" :text="user.description" :i="$store.state.i"/> + </div> + </div> + <div class="pinned" v-if="user.pinnedNotes && user.pinnedNotes.length > 0"> + <p>%fa:thumbtack% %i18n:@pinned-notes%</p> + <div class="notes"> + <x-note v-for="n in user.pinnedNotes" :key="n.id" :note="n" :mini="true"/> + </div> + </div> + <div class="images" v-if="images.length > 0"> + <router-link v-for="image in images" :style="`background-image: url(${image.thumbnailUrl})`" :key="`${image.id}:${image._note.id}`" :to="image._note | notePage"></router-link> + </div> + <div class="tl"> + <x-notes ref="timeline" :more="existMore ? fetchMoreNotes : null"/> + </div> + </div> +</x-column> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import parseAcct from '../../../../../../misc/acct/parse'; +import XColumn from './deck.column.vue'; +import XNotes from './deck.notes.vue'; +import XNote from '../../components/note.vue'; + +const fetchLimit = 10; + +export default Vue.extend({ + components: { + XColumn, + XNotes, + XNote + }, + + props: { + acct: { + type: String, + required: true + } + }, + + data() { + return { + user: null, + fetching: true, + existMore: false, + moreFetching: false, + withFiles: false, + images: [] + }; + }, + + computed: { + title(): string { + return this.user ? Vue.filter('userName')(this.user) : ''; + }, + + bannerStyle(): any { + if (this.user == null) return {}; + if (this.user.bannerUrl == null) return {}; + return { + backgroundColor: this.user.bannerColor && this.user.bannerColor.length == 3 ? `rgb(${ this.user.bannerColor.join(',') })` : null, + backgroundImage: `url(${ this.user.bannerUrl })` + }; + }, + }, + + created() { + (this as any).api('users/show', parseAcct(this.acct)).then(user => { + this.user = user; + this.fetching = false; + + this.$nextTick(() => { + (this.$refs.timeline as any).init(() => this.initTl()); + }); + + (this as any).api('users/notes', { + userId: this.user.id, + withFiles: true, + limit: 9 + }).then(notes => { + notes.forEach(note => { + note.files.forEach(file => { + file._note = note; + if (this.images.length < 9) this.images.push(file); + }); + }); + }); + }); + }, + + methods: { + initTl() { + return new Promise((res, rej) => { + (this as any).api('users/notes', { + userId: this.user.id, + limit: fetchLimit + 1, + withFiles: this.withFiles, + includeMyRenotes: this.$store.state.settings.showMyRenotes, + includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, + includeLocalRenotes: this.$store.state.settings.showLocalRenotes + }).then(notes => { + if (notes.length == fetchLimit + 1) { + notes.pop(); + this.existMore = true; + } + res(notes); + }, rej); + }); + }, + + fetchMoreNotes() { + this.moreFetching = true; + + const promise = (this as any).api('users/notes', { + userId: this.user.id, + limit: fetchLimit + 1, + untilId: (this.$refs.timeline as any).tail().id, + withFiles: this.withFiles, + includeMyRenotes: this.$store.state.settings.showMyRenotes, + includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, + includeLocalRenotes: this.$store.state.settings.showLocalRenotes + }); + + promise.then(notes => { + if (notes.length == fetchLimit + 1) { + notes.pop(); + } else { + this.existMore = false; + } + notes.forEach(n => (this.$refs.timeline as any).append(n)); + this.moreFetching = false; + }); + + return promise; + }, + } +}); +</script> + +<style lang="stylus" scoped> +.zubukjlciycdsyynicqrnlsmdwmymzqu + background var(--deckUserColumnBg) + + > .is-remote + padding 8px 16px + font-size 12px + + &.is-remote + color var(--remoteInfoFg) + background var(--remoteInfoBg) + + > a + font-weight bold + + > header + overflow hidden + background-size cover + background-position center + + > div + padding 32px + background rgba(#000, 0.5) + color #fff + text-align center + + > .follow + position absolute + top 16px + right 16px + + > .avatar + display block + width 64px + height 64px + margin 0 auto + + > .name + display block + margin-top 8px + font-weight bold + text-shadow 0 0 8px #000 + + > .acct + font-size 14px + opacity 0.7 + text-shadow 0 0 8px #000 + + > .info + padding 16px + font-size 14px + color var(--text) + text-align center + background var(--face) + border-bottom solid 1px var(--faceDivider) + + &:before + content "" + display blcok + position absolute + top -32px + left 0 + right 0 + width 0px + margin 0 auto + border-top solid 16px transparent + border-left solid 16px transparent + border-right solid 16px transparent + border-bottom solid 16px var(--face) + + > .pinned + padding-bottom 16px + background var(--deckUserColumnBg) + + > p + margin 0 + padding 8px 16px + font-size 14px + color var(--text) + + > .notes + background var(--face) + + > .images + display grid + grid-template-rows 1fr 1fr 1fr + grid-template-columns 1fr 1fr 1fr + gap 4px + height 250px + padding 16px + margin-bottom 16px + background var(--face) + + > * + background-position center center + background-size cover + background-clip content-box + + > .tl + background var(--face) + +</style> diff --git a/src/client/app/desktop/views/pages/deck/deck.vue b/src/client/app/desktop/views/pages/deck/deck.vue index 22b4c50bb4..2ff3d68a91 100644 --- a/src/client/app/desktop/views/pages/deck/deck.vue +++ b/src/client/app/desktop/views/pages/deck/deck.vue @@ -9,6 +9,10 @@ </div> <x-column-core v-else :ref="ids[0]" :key="ids[0]" :column="columns.find(c => c.id == ids[0])"/> </template> + <template v-if="temporaryColumn"> + <x-user-column v-if="temporaryColumn.type == 'user'" :acct="temporaryColumn.acct" :key="temporaryColumn.acct"/> + <x-note-column v-else-if="temporaryColumn.type == 'note'" :note-id="temporaryColumn.noteId" :key="temporaryColumn.noteId"/> + </template> <button ref="add" @click="add" title="%i18n:common.deck.add-column%">%fa:plus%</button> </div> </mk-ui> @@ -19,11 +23,16 @@ import Vue from 'vue'; import XColumnCore from './deck.column-core.vue'; import Menu from '../../../../common/views/components/menu.vue'; import MkUserListsWindow from '../../components/user-lists-window.vue'; +import XUserColumn from './deck.user-column.vue'; +import XNoteColumn from './deck.note-column.vue'; + import * as uuid from 'uuid'; export default Vue.extend({ components: { - XColumnCore + XColumnCore, + XUserColumn, + XNoteColumn }, computed: { @@ -31,15 +40,21 @@ export default Vue.extend({ if (this.$store.state.settings.deck == null) return []; return this.$store.state.settings.deck.columns; }, + layout(): any[] { if (this.$store.state.settings.deck == null) return []; if (this.$store.state.settings.deck.layout == null) return this.$store.state.settings.deck.columns.map(c => [c.id]); return this.$store.state.settings.deck.layout; }, + style(): any { return { height: `calc(100vh - ${this.$store.state.uiHeaderHeight}px)` }; + }, + + temporaryColumn(): any { + return this.$store.state.device.deckTemporaryColumn; } }, @@ -50,6 +65,8 @@ export default Vue.extend({ }, created() { + this.$store.commit('navHook', this.onNav); + if (this.$store.state.settings.deck == null) { const deck = { columns: [/*{ @@ -95,6 +112,8 @@ export default Vue.extend({ }, beforeDestroy() { + this.$store.commit('navHook', null); + document.documentElement.style.overflow = 'auto'; }, @@ -103,6 +122,30 @@ export default Vue.extend({ return this.$refs[id][0]; }, + onNav(to) { + if (!this.$store.state.settings.deckNav) return false; + + if (to.name == 'user') { + this.$store.commit('device/set', { + key: 'deckTemporaryColumn', + value: { + type: 'user', + acct: to.params.user + } + }); + return true; + } else if (to.name == 'note') { + this.$store.commit('device/set', { + key: 'deckTemporaryColumn', + value: { + type: 'note', + noteId: to.params.note + } + }); + return true; + } + }, + add() { this.os.new(Menu, { source: this.$refs.add, diff --git a/src/client/app/desktop/views/pages/user/user.photos.vue b/src/client/app/desktop/views/pages/user/user.photos.vue index 628d5b6d95..2f525b003d 100644 --- a/src/client/app/desktop/views/pages/user/user.photos.vue +++ b/src/client/app/desktop/views/pages/user/user.photos.vue @@ -60,9 +60,6 @@ export default Vue.extend({ margin-right 4px > .stream - display -webkit-flex - display -moz-flex - display -ms-flex display flex justify-content center flex-wrap wrap diff --git a/src/client/app/init.ts b/src/client/app/init.ts index 2cbceb8e78..7277e70d83 100644 --- a/src/client/app/init.ts +++ b/src/client/app/init.ts @@ -148,6 +148,19 @@ export default (callback: (launch: (router: VueRouter, api?: (os: MiOS) => API) }); //#endregion + // Navigation hook + router.beforeEach((to, from, next) => { + if (os.store.state.navHook) { + if (os.store.state.navHook(to)) { + next(false); + } else { + next(); + } + } else { + next(); + } + }); + Vue.mixin({ data() { return { diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue index c0159a0484..6ac8c8bd08 100644 --- a/src/client/app/mobile/views/pages/settings.vue +++ b/src/client/app/mobile/views/pages/settings.vue @@ -5,7 +5,7 @@ <div class="signin-as" v-html="'%i18n:@signed-in-as%'.replace('{}', `<b>${name}</b>`)"></div> <div> - <x-profile/> + <mk-profile-editor/> <ui-card> <div slot="title">%fa:palette% %i18n:@theme%</div> @@ -148,13 +148,7 @@ import Vue from 'vue'; import { apiUrl, version, codename, langs } from '../../../config'; import checkForUpdate from '../../../common/scripts/check-for-update'; -import XProfile from './settings/settings.profile.vue'; - export default Vue.extend({ - components: { - XProfile - }, - data() { return { apiUrl, diff --git a/src/client/app/store.ts b/src/client/app/store.ts index 63365f7fba..0a38decfa4 100644 --- a/src/client/app/store.ts +++ b/src/client/app/store.ts @@ -10,6 +10,7 @@ const defaultSettings = { home: null, mobileHome: [], deck: null, + deckNav: true, tagTimelines: [], fetchOnScroll: true, showMaps: true, @@ -57,7 +58,8 @@ const defaultDeviceSettings = { alwaysShowNsfw: false, postStyle: 'standard', navbar: 'top', - mobileNotificationPosition: 'bottom' + mobileNotificationPosition: 'bottom', + deckTemporaryColumn: null }; export default (os: MiOS) => new Vuex.Store({ @@ -68,7 +70,8 @@ export default (os: MiOS) => new Vuex.Store({ state: { i: null, indicate: false, - uiHeaderHeight: 0 + uiHeaderHeight: 0, + navHook: null }, getters: { @@ -90,6 +93,10 @@ export default (os: MiOS) => new Vuex.Store({ setUiHeaderHeight(state, height) { state.uiHeaderHeight = height; + }, + + navHook(state, callback) { + state.navHook = callback; } }, diff --git a/src/client/theme/dark.json5 b/src/client/theme/dark.json5 index 59cff0ae44..deb4c4ceee 100644 --- a/src/client/theme/dark.json5 +++ b/src/client/theme/dark.json5 @@ -174,6 +174,7 @@ desktopSettingsNavItemHover: ':lighten<10<$text', deckAcrylicColumnBg: 'rgba(0, 0, 0, 0.25)', + deckUserColumnBg: ':darken<3<@face', mobileHeaderBg: ':lighten<5<$secondary', mobileHeaderFg: '$text', diff --git a/src/client/theme/light.json5 b/src/client/theme/light.json5 index a1b000530a..c5b3148834 100644 --- a/src/client/theme/light.json5 +++ b/src/client/theme/light.json5 @@ -174,6 +174,7 @@ desktopSettingsNavItemHover: ':darken<10<$text', deckAcrylicColumnBg: 'rgba(0, 0, 0, 0.1)', + deckUserColumnBg: ':darken<4<@face', mobileHeaderBg: ':lighten<5<$secondary', mobileHeaderFg: '$text', |