diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2020-07-11 10:13:11 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-07-11 10:13:11 +0900 |
| commit | cf3fc97202588e835ade5d6ab1a3c087e46958ad (patch) | |
| tree | b3fe472b455bf913a47df4d41b1363c7122bf1d9 /src/client/components/deck | |
| parent | タイムライン上でTwitterウィジットを展開できるようにな... (diff) | |
| download | misskey-cf3fc97202588e835ade5d6ab1a3c087e46958ad.tar.gz misskey-cf3fc97202588e835ade5d6ab1a3c087e46958ad.tar.bz2 misskey-cf3fc97202588e835ade5d6ab1a3c087e46958ad.zip | |
Deck (#6504)
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
Diffstat (limited to 'src/client/components/deck')
| -rw-r--r-- | src/client/components/deck/antenna-column.vue | 80 | ||||
| -rw-r--r-- | src/client/components/deck/column-core.vue | 50 | ||||
| -rw-r--r-- | src/client/components/deck/column.vue | 426 | ||||
| -rw-r--r-- | src/client/components/deck/direct-column.vue | 39 | ||||
| -rw-r--r-- | src/client/components/deck/list-column.vue | 87 | ||||
| -rw-r--r-- | src/client/components/deck/mentions-column.vue | 39 | ||||
| -rw-r--r-- | src/client/components/deck/notifications-column.vue | 69 | ||||
| -rw-r--r-- | src/client/components/deck/tl-column.vue | 141 | ||||
| -rw-r--r-- | src/client/components/deck/widgets-column.vue | 151 |
9 files changed, 1082 insertions, 0 deletions
diff --git a/src/client/components/deck/antenna-column.vue b/src/client/components/deck/antenna-column.vue new file mode 100644 index 0000000000..83fe14f2cc --- /dev/null +++ b/src/client/components/deck/antenna-column.vue @@ -0,0 +1,80 @@ +<template> +<x-column :menu="menu" :column="column" :is-stacked="isStacked"> + <template #header> + <fa :icon="faSatellite"/><span style="margin-left: 8px;">{{ column.name }}</span> + </template> + + <x-timeline ref="timeline" src="antenna" :antenna="column.antennaId" @after="() => $emit('loaded')"/> +</x-column> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faSatellite, faCog } from '@fortawesome/free-solid-svg-icons'; +import XColumn from './column.vue'; +import XTimeline from '../timeline.vue'; + +export default Vue.extend({ + components: { + XColumn, + XTimeline, + }, + + props: { + column: { + type: Object, + required: true + }, + isStacked: { + type: Boolean, + required: true + } + }, + + data() { + return { + menu: null, + faSatellite + }; + }, + + watch: { + mediaOnly() { + (this.$refs.timeline as any).reload(); + } + }, + + created() { + this.menu = [{ + icon: faCog, + text: this.$t('antenna'), + action: async () => { + const antennas = await this.$root.api('antennas/list'); + this.$root.dialog({ + title: this.$t('antenna'), + type: null, + select: { + items: antennas.map(x => ({ + value: x, text: x.name + })) + }, + showCancelButton: true + }).then(({ canceled, result: antenna }) => { + if (canceled) return; + this.column.antennaId = antenna.id; + this.$store.commit('deviceUser/updateDeckColumn', this.column); + }); + } + }]; + }, + + methods: { + focus() { + (this.$refs.timeline as any).focus(); + } + } +}); +</script> + +<style lang="scss" scoped> +</style> diff --git a/src/client/components/deck/column-core.vue b/src/client/components/deck/column-core.vue new file mode 100644 index 0000000000..44f19e7eda --- /dev/null +++ b/src/client/components/deck/column-core.vue @@ -0,0 +1,50 @@ +<template> +<!-- TODO: リファクタの余地がありそう --> +<x-widgets-column v-if="column.type === 'widgets'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> +<x-notifications-column v-else-if="column.type === 'notifications'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> +<x-tl-column v-else-if="column.type === 'tl'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> +<x-list-column v-else-if="column.type === 'list'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> +<x-antenna-column v-else-if="column.type === 'antenna'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> +<!-- TODO: <x-tl-column v-else-if="column.type === 'hashtag'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> --> +<x-mentions-column v-else-if="column.type === 'mentions'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> +<x-direct-column v-else-if="column.type === 'direct'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XTlColumn from './tl-column.vue'; +import XAntennaColumn from './antenna-column.vue'; +import XListColumn from './list-column.vue'; +import XNotificationsColumn from './notifications-column.vue'; +import XWidgetsColumn from './widgets-column.vue'; +import XMentionsColumn from './mentions-column.vue'; +import XDirectColumn from './direct-column.vue'; + +export default Vue.extend({ + components: { + XTlColumn, + XAntennaColumn, + XListColumn, + XNotificationsColumn, + XWidgetsColumn, + XMentionsColumn, + XDirectColumn + }, + props: { + column: { + type: Object, + required: true + }, + isStacked: { + type: Boolean, + required: false, + default: false + } + }, + methods: { + focus() { + this.$children[0].focus(); + } + } +}); +</script> diff --git a/src/client/components/deck/column.vue b/src/client/components/deck/column.vue new file mode 100644 index 0000000000..f7620e5749 --- /dev/null +++ b/src/client/components/deck/column.vue @@ -0,0 +1,426 @@ +<template> +<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため --> +<section class="dnpfarvg _panel _narrow_" :class="{ naked, paged: isMainColumn, _close_: !isMainColumn, active, isStacked, draghover, dragging, dropready }" + @dragover.prevent.stop="onDragover" + @dragleave="onDragleave" + @drop.prevent.stop="onDrop" + v-hotkey="keymap" + :style="{ width: `${width}px` }" +> + <header :class="{ indicated }" + draggable="true" + @click="goTop" + @dragstart="onDragstart" + @dragend="onDragend" + @contextmenu.prevent.stop="onContextmenu" + > + <button class="toggleActive _button" @click="toggleActive" v-if="isStacked"> + <template v-if="active"><fa :icon="faAngleUp"/></template> + <template v-else><fa :icon="faAngleDown"/></template> + </button> + <div class="action"> + <slot name="action"></slot> + </div> + <span class="header"><slot name="header"></slot></span> + <button v-if="!isMainColumn" class="menu _button" ref="menu" @click.stop="showMenu"><fa :icon="faCaretDown"/></button> + <button v-else-if="$route.name !== 'index'" class="close _button" @click.stop="close"><fa :icon="faTimes"/></button> + </header> + <div ref="body" v-show="active"> + <slot></slot> + </div> +</section> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faArrowUp, faArrowDown, faAngleUp, faAngleDown, faCaretDown, faTimes, faArrowRight, faArrowLeft, faPencilAlt } from '@fortawesome/free-solid-svg-icons'; +import { faWindowMaximize, faTrashAlt, faWindowRestore } from '@fortawesome/free-regular-svg-icons'; + +export default Vue.extend({ + props: { + column: { + type: Object, + required: false, + default: null + }, + isStacked: { + type: Boolean, + required: false, + default: false + }, + menu: { + type: Array, + required: false, + default: null + }, + naked: { + type: Boolean, + required: false, + default: false + }, + indicated: { + type: Boolean, + required: false, + default: false + }, + }, + + data() { + return { + active: true, + dragging: false, + draghover: false, + dropready: false, + faArrowUp, faArrowDown, faAngleUp, faAngleDown, faCaretDown, faTimes, + }; + }, + + computed: { + isMainColumn(): boolean { + return this.column == null; + }, + + width(): number { + return this.isMainColumn ? 350 : this.column.width; + }, + + keymap(): any { + return { + 'shift+up': () => this.$parent.$emit('parentFocus', 'up'), + 'shift+down': () => this.$parent.$emit('parentFocus', 'down'), + 'shift+left': () => this.$parent.$emit('parentFocus', 'left'), + 'shift+right': () => this.$parent.$emit('parentFocus', 'right'), + }; + } + }, + + watch: { + active(v) { + this.$emit('change-active-state', v); + }, + + dragging(v) { + this.$root.$emit(v ? 'deck.column.dragStart' : 'deck.column.dragEnd'); + } + }, + + mounted() { + if (!this.isMainColumn) { + this.$root.$on('deck.column.dragStart', this.onOtherDragStart); + this.$root.$on('deck.column.dragEnd', this.onOtherDragEnd); + } + }, + + beforeDestroy() { + if (!this.isMainColumn) { + this.$root.$off('deck.column.dragStart', this.onOtherDragStart); + this.$root.$off('deck.column.dragEnd', this.onOtherDragEnd); + } + }, + + methods: { + onOtherDragStart() { + this.dropready = true; + }, + + onOtherDragEnd() { + this.dropready = false; + }, + + toggleActive() { + if (!this.isStacked) return; + this.active = !this.active; + }, + + getMenu() { + const items = [{ + icon: faPencilAlt, + text: this.$t('rename'), + action: () => { + this.$root.dialog({ + title: this.$t('rename'), + input: { + default: this.column.name, + allowEmpty: false + } + }).then(({ canceled, result: name }) => { + if (canceled) return; + this.$store.commit('deviceUser/renameDeckColumn', { id: this.column.id, name }); + }); + } + }, null, { + icon: faArrowLeft, + text: this.$t('swap-left'), + action: () => { + this.$store.commit('deviceUser/swapLeftDeckColumn', this.column.id); + } + }, { + icon: faArrowRight, + text: this.$t('swap-right'), + action: () => { + this.$store.commit('deviceUser/swapRightDeckColumn', this.column.id); + } + }, this.isStacked ? { + icon: faArrowUp, + text: this.$t('swap-up'), + action: () => { + this.$store.commit('deviceUser/swapUpDeckColumn', this.column.id); + } + } : undefined, this.isStacked ? { + icon: faArrowDown, + text: this.$t('swap-down'), + action: () => { + this.$store.commit('deviceUser/swapDownDeckColumn', this.column.id); + } + } : undefined, null, { + icon: faWindowRestore, + text: this.$t('stack-left'), + action: () => { + this.$store.commit('deviceUser/stackLeftDeckColumn', this.column.id); + } + }, this.isStacked ? { + icon: faWindowMaximize, + text: this.$t('pop-right'), + action: () => { + this.$store.commit('deviceUser/popRightDeckColumn', this.column.id); + } + } : undefined, null, { + icon: faTrashAlt, + text: this.$t('remove'), + action: () => { + this.$store.commit('deviceUser/removeDeckColumn', this.column.id); + } + }]; + + if (this.menu) { + for (const i of this.menu.reverse()) { + items.unshift(i); + } + } + + return items; + }, + + onContextmenu(e) { + if (this.isMainColumn) return; + this.showMenu(); + }, + + showMenu() { + this.$root.menu({ + items: this.getMenu(), + source: this.$refs.menu, + }); + }, + + close() { + this.$router.push('/'); + }, + + goTop() { + this.$refs.body.scrollTo({ + top: 0, + behavior: 'smooth' + }); + }, + + onDragstart(e) { + // メインカラムはドラッグさせない + if (this.isMainColumn) { + e.preventDefault(); + return; + } + + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('mk-deck-column', this.column.id); + this.dragging = true; + }, + + onDragend(e) { + this.dragging = false; + }, + + onDragover(e) { + // メインカラムにはドロップさせない + if (this.isMainColumn) { + e.dataTransfer.dropEffect = 'none'; + return; + } + + // 自分自身がドラッグされている場合 + if (this.dragging) { + // 自分自身にはドロップさせない + e.dataTransfer.dropEffect = 'none'; + return; + } + + const isDeckColumn = e.dataTransfer.types[0] == 'mk-deck-column'; + + e.dataTransfer.dropEffect = isDeckColumn ? 'move' : 'none'; + + if (!this.dragging && isDeckColumn) this.draghover = true; + }, + + onDragleave() { + this.draghover = false; + }, + + onDrop(e) { + this.draghover = false; + this.$root.$emit('deck.column.dragEnd'); + + const id = e.dataTransfer.getData('mk-deck-column'); + if (id != null && id != '') { + this.$store.commit('deviceUser/swapDeckColumn', { + a: this.column.id, + b: id + }); + } + } + } +}); +</script> + +<style lang="scss" scoped> +.dnpfarvg { + $header-height: 42px; + + height: 100%; + overflow: hidden; + box-shadow: 0 0 0 1px var(--deckColumnBorder); + + &.draghover { + box-shadow: 0 0 0 2px var(--focus); + + &:after { + content: ""; + display: block; + position: absolute; + z-index: 1000; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: var(--focus); + } + } + + &.dragging { + box-shadow: 0 0 0 2px var(--focus); + } + + &.dropready { + * { + pointer-events: none; + } + } + + &:not(.active) { + flex-basis: $header-height; + min-height: $header-height; + + > header.indicated { + box-shadow: 4px 0px var(--accent) inset; + } + } + + &.naked { + //background: var(--deckAcrylicColumnBg); + background: transparent !important; + + > header { + background: transparent; + box-shadow: none; + + > button { + color: var(--fg); + } + } + } + + &.paged { + > div { + background: var(--bg); + padding: var(--margin); + } + } + + > header { + position: relative; + display: flex; + z-index: 2; + line-height: $header-height; + padding: 0 16px; + font-size: 0.9em; + color: var(--panelHeaderFg); + background: var(--panelHeaderBg); + box-shadow: 0 1px 0 0 var(--panelHeaderDivider); + cursor: pointer; + + &, * { + user-select: none; + } + + &.indicated { + box-shadow: 0 3px 0 0 var(--accent); + } + + > .header { + display: inline-block; + align-items: center; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + > span:only-of-type { + width: 100%; + } + + > .toggleActive, + > .action > *, + > .menu, + > .close { + z-index: 1; + width: $header-height; + line-height: $header-height; + font-size: 16px; + color: var(--faceTextButton); + + &:hover { + color: var(--faceTextButtonHover); + } + + &:active { + color: var(--faceTextButtonActive); + } + } + + > .toggleActive, > .action { + margin-left: -16px; + } + + > .action { + z-index: 1; + } + + > .action:empty { + display: none; + } + + > .menu, + > .close { + margin-left: auto; + margin-right: -16px; + } + } + + > div { + height: calc(100% - #{$header-height}); + overflow: auto; + overflow-x: hidden; + -webkit-overflow-scrolling: touch; + box-sizing: border-box; + } +} +</style> diff --git a/src/client/components/deck/direct-column.vue b/src/client/components/deck/direct-column.vue new file mode 100644 index 0000000000..f340048d6a --- /dev/null +++ b/src/client/components/deck/direct-column.vue @@ -0,0 +1,39 @@ +<template> +<x-column :name="name" :column="column" :is-stacked="isStacked" :menu="menu"> + <template #header><fa :icon="faEnvelope" style="margin-right: 8px;"/>{{ column.name }}</template> + + <x-direct/> +</x-column> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faEnvelope } from '@fortawesome/free-solid-svg-icons'; +import XColumn from './column.vue'; +import XDirect from '../../pages/messages.vue'; + +export default Vue.extend({ + components: { + XColumn, + XDirect + }, + + props: { + column: { + type: Object, + required: true + }, + isStacked: { + type: Boolean, + required: true + } + }, + + data() { + return { + menu: null, + faEnvelope + } + }, +}); +</script> diff --git a/src/client/components/deck/list-column.vue b/src/client/components/deck/list-column.vue new file mode 100644 index 0000000000..a3576e8d67 --- /dev/null +++ b/src/client/components/deck/list-column.vue @@ -0,0 +1,87 @@ +<template> +<x-column :menu="menu" :column="column" :is-stacked="isStacked"> + <template #header> + <fa :icon="faListUl"/><span style="margin-left: 8px;">{{ column.name }}</span> + </template> + + <x-timeline v-if="column.listId" ref="timeline" src="list" :list="column.listId" @after="() => $emit('loaded')"/> +</x-column> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faListUl, faCog } from '@fortawesome/free-solid-svg-icons'; +import XColumn from './column.vue'; +import XTimeline from '../timeline.vue'; + +export default Vue.extend({ + components: { + XColumn, + XTimeline, + }, + + props: { + column: { + type: Object, + required: true + }, + isStacked: { + type: Boolean, + required: true + } + }, + + data() { + return { + faListUl + }; + }, + + watch: { + mediaOnly() { + (this.$refs.timeline as any).reload(); + } + }, + + created() { + this.menu = [{ + icon: faCog, + text: this.$t('list'), + action: this.setList + }]; + }, + + mounted() { + if (this.column.listId == null) { + this.setList(); + } + }, + + methods: { + async setList() { + const lists = await this.$root.api('users/lists/list'); + const { canceled, result: list } = await this.$root.dialog({ + title: this.$t('list'), + type: null, + select: { + items: lists.map(x => ({ + value: x, text: x.name + })), + default: this.column.listId + }, + showCancelButton: true + }); + if (canceled) return; + Vue.set(this.column, 'listId', list.id); + this.$store.commit('deviceUser/updateDeckColumn', this.column); + }, + + focus() { + (this.$refs.timeline as any).focus(); + } + } +}); +</script> + +<style lang="scss" scoped> +</style> diff --git a/src/client/components/deck/mentions-column.vue b/src/client/components/deck/mentions-column.vue new file mode 100644 index 0000000000..19e49d2a89 --- /dev/null +++ b/src/client/components/deck/mentions-column.vue @@ -0,0 +1,39 @@ +<template> +<x-column :column="column" :is-stacked="isStacked" :menu="menu"> + <template #header><fa :icon="faAt" style="margin-right: 8px;"/>{{ column.name }}</template> + + <x-mentions/> +</x-column> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faAt } from '@fortawesome/free-solid-svg-icons'; +import XColumn from './column.vue'; +import XMentions from '../../pages/mentions.vue'; + +export default Vue.extend({ + components: { + XColumn, + XMentions + }, + + props: { + column: { + type: Object, + required: true + }, + isStacked: { + type: Boolean, + required: true + } + }, + + data() { + return { + menu: null, + faAt + } + }, +}); +</script> diff --git a/src/client/components/deck/notifications-column.vue b/src/client/components/deck/notifications-column.vue new file mode 100644 index 0000000000..58873aa130 --- /dev/null +++ b/src/client/components/deck/notifications-column.vue @@ -0,0 +1,69 @@ +<template> +<x-column :column="column" :is-stacked="isStacked" :menu="menu"> + <template #header><fa :icon="faBell" style="margin-right: 8px;"/>{{ column.name }}</template> + + <x-notifications/> +</x-column> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faCog } from '@fortawesome/free-solid-svg-icons'; +import { faBell } from '@fortawesome/free-regular-svg-icons'; +import XColumn from './column.vue'; +import XNotifications from '../notifications.vue'; + +export default Vue.extend({ + components: { + XColumn, + XNotifications + }, + + props: { + column: { + type: Object, + required: true + }, + isStacked: { + type: Boolean, + required: true + } + }, + + data() { + return { + menu: null, + faBell + } + }, + + created() { + if (this.column.notificationType == null) { + this.column.notificationType = 'all'; + this.$store.commit('deviceUser/updateDeckColumn', this.column); + } + + this.menu = [{ + icon: faCog, + text: this.$t('@.notification-type'), + action: () => { + this.$root.dialog({ + title: this.$t('@.notification-type'), + type: null, + select: { + items: ['all', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'].map(x => ({ + value: x, text: this.$t('@.notification-types.' + x) + })) + default: this.column.notificationType, + }, + showCancelButton: true + }).then(({ canceled, result: type }) => { + if (canceled) return; + this.column.notificationType = type; + this.$store.commit('deviceUser/updateDeckColumn', this.column); + }); + } + }]; + }, +}); +</script> diff --git a/src/client/components/deck/tl-column.vue b/src/client/components/deck/tl-column.vue new file mode 100644 index 0000000000..c3ee67af3a --- /dev/null +++ b/src/client/components/deck/tl-column.vue @@ -0,0 +1,141 @@ +<template> +<x-column :menu="menu" :column="column" :is-stacked="isStacked" :indicated="indicated" @change-active-state="onChangeActiveState"> + <template #header> + <fa v-if="column.tl === 'home'" :icon="faHome"/> + <fa v-else-if="column.tl === 'local'" :icon="faComments"/> + <fa v-else-if="column.tl === 'social'" :icon="faShareAlt"/> + <fa v-else-if="column.tl === 'global'" :icon="faGlobe"/> + <span style="margin-left: 8px;">{{ column.name }}</span> + </template> + + <div class="iwaalbte" v-if="disabled"> + <p> + <fa :icon="faMinusCircle"/> + {{ $t('disabled-timeline.title') }} + </p> + <p class="desc">{{ $t('disabled-timeline.description') }}</p> + </div> + <x-timeline v-else-if="column.tl" ref="timeline" :src="column.tl" @after="() => $emit('loaded')" @queue="queueUpdated" @note="onNote" :key="column.tl"/> +</x-column> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faMinusCircle, faHome, faComments, faShareAlt, faGlobe, faCog } from '@fortawesome/free-solid-svg-icons'; +import XColumn from './column.vue'; +import XTimeline from '../timeline.vue'; + +export default Vue.extend({ + components: { + XColumn, + XTimeline, + }, + + props: { + column: { + type: Object, + required: true + }, + isStacked: { + type: Boolean, + required: true + } + }, + + data() { + return { + menu: null, + disabled: false, + indicated: false, + columnActive: true, + faMinusCircle, faHome, faComments, faShareAlt, faGlobe, + }; + }, + + watch: { + mediaOnly() { + (this.$refs.timeline as any).reload(); + } + }, + + created() { + this.menu = [{ + icon: faCog, + text: this.$t('timeline'), + action: this.setType + }]; + }, + + mounted() { + if (this.column.tl == null) { + this.setType(); + } else { + this.disabled = !this.$store.state.i.isModerator && !this.$store.state.i.isAdmin && ( + this.$store.state.instance.meta.disableLocalTimeline && ['local', 'social'].includes(this.column.tl) || + this.$store.state.instance.meta.disableGlobalTimeline && ['global'].includes(this.column.tl)); + } + }, + + methods: { + async setType() { + const { canceled, result: src } = await this.$root.dialog({ + title: this.$t('timeline'), + type: null, + select: { + items: [{ + value: 'home', text: this.$t('_timelines.home') + }, { + value: 'local', text: this.$t('_timelines.local') + }, { + value: 'social', text: this.$t('_timelines.social') + }, { + value: 'global', text: this.$t('_timelines.global') + }] + }, + showCancelButton: true + }); + if (canceled) return; + Vue.set(this.column, 'tl', src); + this.$store.commit('deviceUser/updateDeckColumn', this.column); + }, + + queueUpdated(q) { + if (this.columnActive) { + this.indicated = q !== 0; + } + }, + + onNote() { + if (!this.columnActive) { + this.indicated = true; + } + }, + + onChangeActiveState(state) { + this.columnActive = state; + + if (this.columnActive) { + this.indicated = false; + } + }, + + focus() { + (this.$refs.timeline as any).focus(); + } + } +}); +</script> + +<style lang="scss" scoped> +.iwaalbte { + text-align: center; + + > p { + margin: 16px; + + &.desc { + font-size: 14px; + } + } +} +</style> diff --git a/src/client/components/deck/widgets-column.vue b/src/client/components/deck/widgets-column.vue new file mode 100644 index 0000000000..37b17451ec --- /dev/null +++ b/src/client/components/deck/widgets-column.vue @@ -0,0 +1,151 @@ +<template> +<x-column :menu="menu" :naked="true" :column="column" :is-stacked="isStacked"> + <template #header><fa :icon="faWindowMaximize" style="margin-right: 8px;"/>{{ column.name }}</template> + + <div class="wtdtxvec"> + <template v-if="edit"> + <header> + <select v-model="widgetAdderSelected" @change="addWidget"> + <option v-for="widget in widgets" :value="widget" :key="widget">{{ widget }}</option> + </select> + </header> + <x-draggable + :list="column.widgets" + animation="150" + @sort="onWidgetSort" + > + <div v-for="widget in column.widgets" class="customize-container" :key="widget.id" @click="widgetFunc(widget.id)"> + <button class="remove _button" @click="removeWidget(widget)"><fa :icon="faTimes"/></button> + <component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" :column="column"/> + </div> + </x-draggable> + </template> + <component v-else class="widget" v-for="widget in column.widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" :column="column"/> + </div> +</x-column> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as XDraggable from 'vuedraggable'; +import { v4 as uuid } from 'uuid'; +import { faWindowMaximize, faTimes } from '@fortawesome/free-solid-svg-icons'; +import XColumn from './column.vue'; +import { widgets } from '../../widgets'; + +export default Vue.extend({ + components: { + XColumn, + XDraggable, + }, + + props: { + column: { + type: Object, + required: true, + }, + isStacked: { + type: Boolean, + required: true, + }, + }, + + data() { + return { + edit: false, + menu: null, + widgetAdderSelected: null, + widgets, + faWindowMaximize, faTimes + }; + }, + + created() { + this.menu = [{ + icon: 'cog', + text: this.$t('edit'), + action: () => { + this.edit = !this.edit; + } + }]; + }, + + methods: { + widgetFunc(id) { + this.$refs[id][0].setting(); + }, + + onWidgetSort() { + this.saveWidgets(); + }, + + addWidget() { + this.$store.commit('deviceUser/addDeckWidget', { + id: this.column.id, + widget: { + name: this.widgetAdderSelected, + id: uuid(), + data: {} + } + }); + + this.widgetAdderSelected = null; + }, + + removeWidget(widget) { + this.$store.commit('deviceUser/removeDeckWidget', { + id: this.column.id, + widget + }); + }, + + saveWidgets() { + this.$store.commit('deviceUser/updateDeckColumn', this.column); + } + } +}); +</script> + +<style lang="scss" scoped> +.wtdtxvec { + padding-top: 1px; // ウィジェットのbox-shadowを利用した1px borderを隠さないようにするため + + > header { + padding: 16px; + + > * { + width: 100%; + padding: 4px; + } + } + + > .widget, .customize-container { + margin: 8px; + + &:first-of-type { + margin-top: 0; + } + } + + .customize-container { + position: relative; + cursor: move; + + > *:not(.remove) { + pointer-events: none; + } + + > .remove { + position: absolute; + z-index: 2; + top: 8px; + right: 8px; + width: 32px; + height: 32px; + color: #fff; + background: rgba(#000, 0.7); + border-radius: 4px; + } + } +} +</style> |