Merge branch 'develop' into sw-notification-action
This commit is contained in:
commit
e41aa2390c
20 changed files with 740 additions and 377 deletions
|
@ -97,6 +97,7 @@ cantRenote: "この投稿はRenoteできません。"
|
||||||
cantReRenote: "RenoteをRenoteすることはできません。"
|
cantReRenote: "RenoteをRenoteすることはできません。"
|
||||||
quote: "引用"
|
quote: "引用"
|
||||||
pinnedNote: "ピン留めされたノート"
|
pinnedNote: "ピン留めされたノート"
|
||||||
|
pinned: "ピン留め"
|
||||||
you: "あなた"
|
you: "あなた"
|
||||||
clickToShow: "クリックして表示"
|
clickToShow: "クリックして表示"
|
||||||
sensitive: "閲覧注意"
|
sensitive: "閲覧注意"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="note _panel"
|
class="tkcbzcuz _panel"
|
||||||
v-if="!muted"
|
v-if="!muted"
|
||||||
v-show="!isDeleted"
|
v-show="!isDeleted"
|
||||||
:tabindex="!isDeleted ? '-1' : null"
|
:tabindex="!isDeleted ? '-1' : null"
|
||||||
|
@ -858,7 +858,7 @@ export default defineComponent({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.note {
|
.tkcbzcuz {
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: box-shadow 0.1s ease;
|
transition: box-shadow 0.1s ease;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="mfcuwfyp">
|
<div class="mfcuwfyp _noGap_">
|
||||||
<XList class="notifications" :items="items" v-slot="{ item: notification }">
|
<XList class="notifications" :items="items" v-slot="{ item: notification }">
|
||||||
<XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :note="notification.note" @update:note="noteUpdated(notification.note, $event)" :key="notification.id"/>
|
<XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :note="notification.note" @update:note="noteUpdated(notification.note, $event)" :key="notification.id"/>
|
||||||
<XNotification v-else :notification="notification" :with-time="true" :full="true" class="_panel notification" :key="notification.id"/>
|
<XNotification v-else :notification="notification" :with-time="true" :full="true" class="_panel notification" :key="notification.id"/>
|
||||||
|
|
|
@ -98,11 +98,11 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (left + width - window.pageXOffset > window.innerWidth) {
|
if (left + width - window.pageXOffset > window.innerWidth) {
|
||||||
left = window.innerWidth - width + window.pageXOffset;
|
left = window.innerWidth - width + window.pageXOffset - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (top + height - window.pageYOffset > window.innerHeight) {
|
if (top + height - window.pageYOffset > window.innerHeight) {
|
||||||
top = window.innerHeight - height + window.pageYOffset;
|
top = window.innerHeight - height + window.pageYOffset - 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<transition name="zoom-in-top" appear @after-leave="$emit('closed')">
|
<transition name="tooltip" appear @after-leave="$emit('closed')">
|
||||||
<div class="buebdbiu _acrylic _shadow" v-if="showing">
|
<div class="buebdbiu _acrylic _shadow" v-show="showing" ref="content">
|
||||||
<slot>{{ text }}</slot>
|
<slot>{{ text }}</slot>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
|
@ -35,19 +35,43 @@ export default defineComponent({
|
||||||
|
|
||||||
const rect = this.source.getBoundingClientRect();
|
const rect = this.source.getBoundingClientRect();
|
||||||
|
|
||||||
let x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
|
const contentWidth = this.$refs.content.offsetWidth;
|
||||||
let y = rect.top + window.pageYOffset + this.source.offsetHeight;
|
const contentHeight = this.$refs.content.offsetHeight;
|
||||||
|
|
||||||
x -= (this.$el.offsetWidth / 2);
|
let left = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
|
||||||
|
let top = rect.top + window.pageYOffset + this.source.offsetHeight;
|
||||||
|
|
||||||
this.$el.style.left = x + 'px';
|
left -= (this.$el.offsetWidth / 2);
|
||||||
this.$el.style.top = y + 'px';
|
|
||||||
|
if (left + contentWidth - window.pageXOffset > window.innerWidth) {
|
||||||
|
left = window.innerWidth - contentWidth + window.pageXOffset - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (top + contentHeight - window.pageYOffset > window.innerHeight) {
|
||||||
|
top = rect.top + window.pageYOffset - contentHeight;
|
||||||
|
this.$refs.content.style.transformOrigin = 'center bottom';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$el.style.left = left + 'px';
|
||||||
|
this.$el.style.top = top + 'px';
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.tooltip-enter-active,
|
||||||
|
.tooltip-leave-active {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
transition: transform 200ms cubic-bezier(0.23, 1, 0.32, 1), opacity 200ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
|
}
|
||||||
|
.tooltip-enter-from,
|
||||||
|
.tooltip-leave-active {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.75);
|
||||||
|
}
|
||||||
|
|
||||||
.buebdbiu {
|
.buebdbiu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 11000;
|
z-index: 11000;
|
||||||
|
@ -57,6 +81,6 @@ export default defineComponent({
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transform-origin: center -16px;
|
transform-origin: center top;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
153
src/client/components/widgets.vue
Normal file
153
src/client/components/widgets.vue
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
<template>
|
||||||
|
<div class="vjoppmmu">
|
||||||
|
<template v-if="edit">
|
||||||
|
<header>
|
||||||
|
<MkSelect v-model:value="widgetAdderSelected" style="margin-bottom: var(--margin)">
|
||||||
|
<template #label>{{ $ts.selectWidget }}</template>
|
||||||
|
<option v-for="widget in widgetDefs" :value="widget" :key="widget">{{ $t(`_widgets.${widget}`) }}</option>
|
||||||
|
</MkSelect>
|
||||||
|
<MkButton inline @click="addWidget" primary><Fa :icon="faPlus"/> {{ $ts.add }}</MkButton>
|
||||||
|
<MkButton inline @click="$emit('exit')">{{ $ts.close }}</MkButton>
|
||||||
|
</header>
|
||||||
|
<XDraggable
|
||||||
|
v-model="_widgets"
|
||||||
|
item-key="id"
|
||||||
|
animation="150"
|
||||||
|
>
|
||||||
|
<template #item="{element}">
|
||||||
|
<div class="customize-container">
|
||||||
|
<button class="config _button" @click.prevent.stop="configWidget(element.id)"><Fa :icon="faCog"/></button>
|
||||||
|
<button class="remove _button" @click.prevent.stop="removeWidget(element)"><Fa :icon="faTimes"/></button>
|
||||||
|
<component :is="`mkw-${element.name}`" :widget="element" :setting-callback="setting => settings[element.id] = setting" :column="column" @updateProps="updateWidget(element.id, $event)"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</XDraggable>
|
||||||
|
</template>
|
||||||
|
<component v-else class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :widget="widget" :column="column" @updateProps="updateWidget(widget.id, $event)"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, defineAsyncComponent } from 'vue';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import { faTimes, faCog, faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import MkSelect from '@/components/ui/select.vue';
|
||||||
|
import MkButton from '@/components/ui/button.vue';
|
||||||
|
import { widgets as widgetDefs } from '@/widgets';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
XDraggable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)),
|
||||||
|
MkSelect,
|
||||||
|
MkButton,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
widgets: {
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['updateWidgets', 'addWidget', 'removeWidget', 'updateWidget', 'exit'],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
widgetAdderSelected: null,
|
||||||
|
widgetDefs,
|
||||||
|
settings: {},
|
||||||
|
faTimes, faPlus, faCog
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
_widgets: {
|
||||||
|
get() {
|
||||||
|
return this.widgets;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit('updateWidgets', value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
configWidget(id) {
|
||||||
|
this.settings[id]();
|
||||||
|
},
|
||||||
|
|
||||||
|
addWidget() {
|
||||||
|
if (this.widgetAdderSelected == null) return;
|
||||||
|
|
||||||
|
this.$emit('addWidget', {
|
||||||
|
name: this.widgetAdderSelected,
|
||||||
|
id: uuid(),
|
||||||
|
data: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.widgetAdderSelected = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
removeWidget(widget) {
|
||||||
|
this.$emit('removeWidget', widget);
|
||||||
|
},
|
||||||
|
|
||||||
|
updateWidget(id, data) {
|
||||||
|
this.$emit('updateWidget', { id, data });
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.vjoppmmu {
|
||||||
|
> header {
|
||||||
|
margin: 16px 0;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
width: 100%;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .widget, .customize-container {
|
||||||
|
margin: var(--margin) 0;
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.customize-container {
|
||||||
|
position: relative;
|
||||||
|
cursor: move;
|
||||||
|
|
||||||
|
> *:not(.remove):not(.config) {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .config,
|
||||||
|
> .remove {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10000;
|
||||||
|
top: 8px;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(#000, 0.7);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .config {
|
||||||
|
right: 8px + 8px + 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .remove {
|
||||||
|
right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -13,12 +13,10 @@ export default {
|
||||||
const viewHeight = container.clientHeight;
|
const viewHeight = container.clientHeight;
|
||||||
const height = container.scrollHeight;
|
const height = container.scrollHeight;
|
||||||
isBottom = (pos + viewHeight > height - 32);
|
isBottom = (pos + viewHeight > height - 32);
|
||||||
console.log(isBottom);
|
|
||||||
}, { passive: true });
|
}, { passive: true });
|
||||||
container.scrollTop = container.scrollHeight;
|
container.scrollTop = container.scrollHeight;
|
||||||
|
|
||||||
const ro = new ResizeObserver((entries, observer) => {
|
const ro = new ResizeObserver((entries, observer) => {
|
||||||
console.log(isBottom);
|
|
||||||
if (isBottom) {
|
if (isBottom) {
|
||||||
const height = container.scrollHeight;
|
const height = container.scrollHeight;
|
||||||
container.scrollTop = height;
|
container.scrollTop = height;
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { popup } from '@/os';
|
||||||
|
|
||||||
const start = isDeviceTouch ? 'touchstart' : 'mouseover';
|
const start = isDeviceTouch ? 'touchstart' : 'mouseover';
|
||||||
const end = isDeviceTouch ? 'touchend' : 'mouseleave';
|
const end = isDeviceTouch ? 'touchend' : 'mouseleave';
|
||||||
|
const delay = 100;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mounted(el: HTMLElement, binding, vn) {
|
mounted(el: HTMLElement, binding, vn) {
|
||||||
|
@ -47,13 +48,13 @@ export default {
|
||||||
el.addEventListener(start, () => {
|
el.addEventListener(start, () => {
|
||||||
clearTimeout(self.showTimer);
|
clearTimeout(self.showTimer);
|
||||||
clearTimeout(self.hideTimer);
|
clearTimeout(self.hideTimer);
|
||||||
self.showTimer = setTimeout(show, 300);
|
self.showTimer = setTimeout(show, delay);
|
||||||
}, { passive: true });
|
}, { passive: true });
|
||||||
|
|
||||||
el.addEventListener(end, () => {
|
el.addEventListener(end, () => {
|
||||||
clearTimeout(self.showTimer);
|
clearTimeout(self.showTimer);
|
||||||
clearTimeout(self.hideTimer);
|
clearTimeout(self.hideTimer);
|
||||||
self.hideTimer = setTimeout(self.close, 300);
|
self.hideTimer = setTimeout(self.close, delay);
|
||||||
}, { passive: true });
|
}, { passive: true });
|
||||||
|
|
||||||
el.addEventListener('click', () => {
|
el.addEventListener('click', () => {
|
||||||
|
|
|
@ -488,19 +488,6 @@ hr {
|
||||||
transform: scale(0.9);
|
transform: scale(0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
.zoom-in-top-enter-active,
|
|
||||||
.zoom-in-top-leave-active {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scaleY(1);
|
|
||||||
transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
|
|
||||||
transform-origin: center top;
|
|
||||||
}
|
|
||||||
.zoom-in-top-enter-from,
|
|
||||||
.zoom-in-top-leave-active {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scaleY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes blink {
|
@keyframes blink {
|
||||||
0% { opacity: 1; transform: scale(1); }
|
0% { opacity: 1; transform: scale(1); }
|
||||||
30% { opacity: 1; transform: scale(1); }
|
30% { opacity: 1; transform: scale(1); }
|
||||||
|
|
|
@ -9,11 +9,6 @@ export default defineComponent({
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
direction: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
default: 'down'
|
|
||||||
},
|
|
||||||
reversed: {
|
reversed: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
|
@ -37,14 +32,10 @@ export default defineComponent({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return h(!this.reversed ? TransitionGroup : 'div', !this.reversed ? {
|
return h(TransitionGroup, {
|
||||||
class: 'hmjzthxl',
|
class: 'hmjzthxl',
|
||||||
name: 'list',
|
name: this.reversed ? 'list-reversed' : 'list',
|
||||||
tag: 'div',
|
tag: 'div',
|
||||||
'data-direction': this.direction,
|
|
||||||
'data-reversed': this.reversed ? 'true' : 'false',
|
|
||||||
} : {
|
|
||||||
class: 'hmjzthxl',
|
|
||||||
}, this.items.map((item, i) => {
|
}, this.items.map((item, i) => {
|
||||||
const el = this.$slots.default({
|
const el = this.$slots.default({
|
||||||
item: item
|
item: item
|
||||||
|
@ -95,23 +86,20 @@ export default defineComponent({
|
||||||
> .list-move {
|
> .list-move {
|
||||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
> .list-enter-active {
|
> .list-enter-active {
|
||||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-direction="up"] {
|
|
||||||
> .list-enter-from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(64px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-direction="down"] {
|
|
||||||
> .list-enter-from {
|
> .list-enter-from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(-64px);
|
transform: translateY(-64px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .list-reversed-enter-active, > .list-reversed-leave-active {
|
||||||
|
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
|
}
|
||||||
|
> .list-reversed-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(64px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -120,6 +108,20 @@ export default defineComponent({
|
||||||
.hmjzthxl {
|
.hmjzthxl {
|
||||||
> .separator {
|
> .separator {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
margin: auto;
|
||||||
|
width: calc(100% - 32px);
|
||||||
|
height: 1px;
|
||||||
|
background: var(--divider);
|
||||||
|
}
|
||||||
|
|
||||||
> .date {
|
> .date {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -130,6 +132,7 @@ export default defineComponent({
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--dateLabelFg);
|
color: var(--dateLabelFg);
|
||||||
|
background: var(--panel);
|
||||||
|
|
||||||
> span {
|
> span {
|
||||||
&:first-child {
|
&:first-child {
|
||||||
|
|
44
src/client/ui/chat/header-clock.vue
Normal file
44
src/client/ui/chat/header-clock.vue
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
<template>
|
||||||
|
<div class="_monospace">
|
||||||
|
<span>
|
||||||
|
<span v-text="hh"></span>
|
||||||
|
<span :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span>
|
||||||
|
<span v-text="mm"></span>
|
||||||
|
<span :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span>
|
||||||
|
<span v-text="ss"></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import * as os from '@/os';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
clock: null,
|
||||||
|
hh: null,
|
||||||
|
mm: null,
|
||||||
|
ss: null,
|
||||||
|
showColon: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.tick();
|
||||||
|
this.clock = setInterval(this.tick, 1000);
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
clearInterval(this.clock);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
tick() {
|
||||||
|
const now = new Date();
|
||||||
|
this.hh = now.getHours().toString().padStart(2, '0');
|
||||||
|
this.mm = now.getMinutes().toString().padStart(2, '0');
|
||||||
|
this.ss = now.getSeconds().toString().padStart(2, '0');
|
||||||
|
this.showColon = now.getSeconds() % 2 === 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -6,11 +6,14 @@
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<div class="left">
|
<div class="left">
|
||||||
<button class="_button account" @click="openAccountMenu">
|
<button class="_button account" @click="openAccountMenu">
|
||||||
<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
|
<MkAvatar :user="$i" class="avatar"/><!--<MkAcct class="text" :user="$i"/>-->
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="right">
|
<div class="right">
|
||||||
<MkA class="item" to="/my/notifications"><Fa :icon="faBell"/><i v-if="$i.hasUnreadNotification"><Fa :icon="faCircle"/></i></MkA>
|
<MkA class="item" to="/my/messaging" v-tooltip="$ts.messaging"><Fa class="icon" :icon="faComments"/><i v-if="$i.hasUnreadMessagingMessage"><Fa :icon="faCircle"/></i></MkA>
|
||||||
|
<MkA class="item" to="/my/messages" v-tooltip="$ts.directNotes"><Fa class="icon" :icon="faEnvelope"/><i v-if="$i.hasUnreadSpecifiedNotes"><Fa :icon="faCircle"/></i></MkA>
|
||||||
|
<MkA class="item" to="/my/mentions" v-tooltip="$ts.mentions"><Fa class="icon" :icon="faAt"/><i v-if="$i.hasUnreadMentions"><Fa :icon="faCircle"/></i></MkA>
|
||||||
|
<MkA class="item" to="/my/notifications" v-tooltip="$ts.notifications"><Fa class="icon" :icon="faBell"/><i v-if="$i.hasUnreadNotification"><Fa :icon="faCircle"/></i></MkA>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
|
@ -18,65 +21,94 @@
|
||||||
<div class="header">{{ $ts.timeline }}</div>
|
<div class="header">{{ $ts.timeline }}</div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<MkA to="/timeline/home" class="item" :class="{ active: tl === 'home' }"><Fa :icon="faHome" class="icon"/>{{ $ts._timelines.home }}</MkA>
|
<MkA to="/timeline/home" class="item" :class="{ active: tl === 'home' }"><Fa :icon="faHome" class="icon"/>{{ $ts._timelines.home }}</MkA>
|
||||||
<MkA to="/timeline/local" class="item" :class="{ active: tl === 'local' }"><Fa :icon="faHome" class="icon"/>{{ $ts._timelines.local }}</MkA>
|
<MkA to="/timeline/local" class="item" :class="{ active: tl === 'local' }"><Fa :icon="faComments" class="icon"/>{{ $ts._timelines.local }}</MkA>
|
||||||
<MkA to="/timeline/social" class="item" :class="{ active: tl === 'social' }"><Fa :icon="faHome" class="icon"/>{{ $ts._timelines.social }}</MkA>
|
<MkA to="/timeline/social" class="item" :class="{ active: tl === 'social' }"><Fa :icon="faShareAlt" class="icon"/>{{ $ts._timelines.social }}</MkA>
|
||||||
<MkA to="/timeline/global" class="item" :class="{ active: tl === 'global' }"><Fa :icon="faHome" class="icon"/>{{ $ts._timelines.global }}</MkA>
|
<MkA to="/timeline/global" class="item" :class="{ active: tl === 'global' }"><Fa :icon="faGlobe" class="icon"/>{{ $ts._timelines.global }}</MkA>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="container" v-if="lists">
|
|
||||||
<div class="header">{{ $ts.lists }}</div>
|
|
||||||
<div class="body">
|
|
||||||
<MkA v-for="list in lists" :key="list.id" :to="`/my/list/${ list.id }`" class="item" :class="{ active: tl === `list:${ list.id }` }"><Fa :icon="faListUl" class="icon"/>{{ list.name }}</MkA>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="container" v-if="antennas">
|
|
||||||
<div class="header">{{ $ts.antennas }}</div>
|
|
||||||
<div class="body">
|
|
||||||
<MkA v-for="antenna in antennas" :key="antenna.id" :to="`/my/antenna/${ antenna.id }`" class="item" :class="{ active: tl === `antenna:${ antenna.id }` }"><Fa :icon="faSatellite" class="icon"/>{{ antenna.name }}</MkA>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container" v-if="followedChannels">
|
<div class="container" v-if="followedChannels">
|
||||||
<div class="header">{{ $ts.channel }}</div>
|
<div class="header">{{ $ts.channel }} ({{ $ts.following }})<button class="_button add" @click="addChannel"><Fa :icon="faPlus"/></button></div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<MkA v-for="channel in followedChannels" :key="channel.id" :to="`/channels/${ channel.id }`" class="item" :class="{ active: tl === `channel:${ channel.id }`, read: !channel.hasUnreadNote }"><Fa :icon="faSatelliteDish" class="icon"/>{{ channel.name }}</MkA>
|
<MkA v-for="channel in followedChannels" :key="channel.id" :to="`/channels/${ channel.id }`" class="item" :class="{ active: tl === `channel:${ channel.id }`, read: !channel.hasUnreadNote }"><Fa :icon="faSatelliteDish" class="icon"/>{{ channel.name }}</MkA>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container" v-if="featuredChannels">
|
<div class="container" v-if="featuredChannels">
|
||||||
<div class="header">{{ $ts.channel }}</div>
|
<div class="header">{{ $ts.channel }}<button class="_button add" @click="addChannel"><Fa :icon="faPlus"/></button></div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<MkA v-for="channel in featuredChannels" :key="channel.id" :to="`/channels/${ channel.id }`" class="item" :class="{ active: tl === `channel:${ channel.id }` }"><Fa :icon="faSatelliteDish" class="icon"/>{{ channel.name }}</MkA>
|
<MkA v-for="channel in featuredChannels" :key="channel.id" :to="`/channels/${ channel.id }`" class="item" :class="{ active: tl === `channel:${ channel.id }` }"><Fa :icon="faSatelliteDish" class="icon"/>{{ channel.name }}</MkA>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="container" v-if="lists">
|
||||||
|
<div class="header">{{ $ts.lists }}<button class="_button add" @click="addList"><Fa :icon="faPlus"/></button></div>
|
||||||
|
<div class="body">
|
||||||
|
<MkA v-for="list in lists" :key="list.id" :to="`/my/list/${ list.id }`" class="item" :class="{ active: tl === `list:${ list.id }` }"><Fa :icon="faListUl" class="icon"/>{{ list.name }}</MkA>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container" v-if="antennas">
|
||||||
|
<div class="header">{{ $ts.antennas }}<button class="_button add" @click="addAntenna"><Fa :icon="faPlus"/></button></div>
|
||||||
|
<div class="body">
|
||||||
|
<MkA v-for="antenna in antennas" :key="antenna.id" :to="`/my/antenna/${ antenna.id }`" class="item" :class="{ active: tl === `antenna:${ antenna.id }` }"><Fa :icon="faSatellite" class="icon"/>{{ antenna.name }}</MkA>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<div class="body">
|
||||||
|
<MkA to="/my/favorites" class="item"><Fa :icon="faStar" class="icon"/>{{ $ts.favorites }}</MkA>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<div class="left">
|
<div class="left">
|
||||||
<button class="_button menu" @click="showMenu">
|
<button class="_button menu" @click="showMenu">
|
||||||
<Fa :icon="faBars"/>
|
<Fa class="icon" :icon="faBars"/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="right">
|
<div class="right">
|
||||||
<MkA class="item" to="/settings"><Fa :icon="faCog"/></MkA>
|
<button class="_button item search" @click="search" v-tooltip="$ts.search">
|
||||||
|
<Fa :icon="faSearch"/>
|
||||||
|
</button>
|
||||||
|
<MkA class="item" to="/settings" v-tooltip="$ts.settings"><Fa class="icon" :icon="faCog"/></MkA>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<main class="main" @contextmenu.stop="onContextmenu">
|
<main class="main" @contextmenu.stop="onContextmenu">
|
||||||
<header class="header" ref="header" @click="onHeaderClick">
|
<header class="header" ref="header" @click="onHeaderClick">
|
||||||
<div v-if="tl === 'home'">
|
<div class="left">
|
||||||
|
<template v-if="tl === 'home'">
|
||||||
<Fa :icon="faHome" class="icon"/>
|
<Fa :icon="faHome" class="icon"/>
|
||||||
<div class="title">{{ $ts._timelines.home }}</div>
|
<div class="title">{{ $ts._timelines.home }}</div>
|
||||||
</div>
|
</template>
|
||||||
<div v-else-if="tl === 'local'">
|
<template v-else-if="tl === 'local'">
|
||||||
<Fa :icon="faShareAlt" class="icon"/>
|
<Fa :icon="faComments" class="icon"/>
|
||||||
<div class="title">{{ $ts._timelines.local }}</div>
|
<div class="title">{{ $ts._timelines.local }}</div>
|
||||||
</div>
|
</template>
|
||||||
<div v-else-if="tl === 'social'">
|
<template v-else-if="tl === 'social'">
|
||||||
<Fa :icon="faShareAlt" class="icon"/>
|
<Fa :icon="faShareAlt" class="icon"/>
|
||||||
<div class="title">{{ $ts._timelines.social }}</div>
|
<div class="title">{{ $ts._timelines.social }}</div>
|
||||||
</div>
|
</template>
|
||||||
<div v-else-if="tl === 'global'">
|
<template v-else-if="tl === 'global'">
|
||||||
<Fa :icon="faShareAlt" class="icon"/>
|
<Fa :icon="faGlobe" class="icon"/>
|
||||||
<div class="title">{{ $ts._timelines.global }}</div>
|
<div class="title">{{ $ts._timelines.global }}</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="tl.startsWith('channel:')">
|
||||||
|
<Fa :icon="faSatelliteDish" class="icon"/>
|
||||||
|
<div class="title" v-if="currentChannel">{{ currentChannel.name }}<div class="description">{{ currentChannel.description }}</div></div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="right">
|
||||||
|
<div class="instance">{{ instanceName }}</div>
|
||||||
|
<XHeaderClock class="clock"/>
|
||||||
|
<button class="_button button search" @click="search" v-tooltip="$ts.search">
|
||||||
|
<Fa :icon="faSearch"/>
|
||||||
|
</button>
|
||||||
|
<button class="_button button follow" v-if="tl.startsWith('channel:') && currentChannel" :class="{ followed: currentChannel.isFollowing }" @click="toggleChannelFollow" v-tooltip="currentChannel.isFollowing ? $ts.unfollow : $ts.follow">
|
||||||
|
<Fa v-if="currentChannel.isFollowing" :icon="faStar"/>
|
||||||
|
<Fa v-else :icon="farStar"/>
|
||||||
|
</button>
|
||||||
|
<button class="_button button menu" v-if="tl.startsWith('channel:') && currentChannel" @click="openChannelMenu">
|
||||||
|
<Fa :icon="faEllipsisH"/>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
|
@ -89,7 +121,10 @@
|
||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<XSide class="side" ref="side"/>
|
<XSide class="side" ref="side" @open="sideViewOpening = true" @close="sideViewOpening = false"/>
|
||||||
|
<div class="side widgets" :class="{ sideViewOpening }">
|
||||||
|
<XWidgets/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<XCommon/>
|
<XCommon/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -97,24 +132,31 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, defineAsyncComponent } from 'vue';
|
import { defineComponent, defineAsyncComponent } from 'vue';
|
||||||
import { faLayerGroup, faBars, faHome, faCircle, faWindowMaximize, faColumns, faPencilAlt, faShareAlt, faSatelliteDish, faListUl, faSatellite, faCog } from '@fortawesome/free-solid-svg-icons';
|
import { faLayerGroup, faBars, faHome, faCircle, faWindowMaximize, faColumns, faPencilAlt, faShareAlt, faSatelliteDish, faListUl, faSatellite, faCog, faSearch, faPlus, faStar, faAt, faLink, faEllipsisH, faGlobe } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { faBell } from '@fortawesome/free-regular-svg-icons';
|
import { faBell, faStar as farStar, faEnvelope, faComments } from '@fortawesome/free-regular-svg-icons';
|
||||||
import { instanceName } from '@/config';
|
import { instanceName, url } from '@/config';
|
||||||
import XSidebar from '@/components/sidebar.vue';
|
import XSidebar from '@/components/sidebar.vue';
|
||||||
|
import XWidgets from './widgets.vue';
|
||||||
import XCommon from '../_common_/common.vue';
|
import XCommon from '../_common_/common.vue';
|
||||||
import XSide from './side.vue';
|
import XSide from './side.vue';
|
||||||
import XTimeline from './timeline.vue';
|
import XTimeline from './timeline.vue';
|
||||||
import XPostForm from './post-form.vue';
|
import XPostForm from './post-form.vue';
|
||||||
|
import XHeaderClock from './header-clock.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
|
import { router } from '@/router';
|
||||||
import { sidebarDef } from '@/sidebar';
|
import { sidebarDef } from '@/sidebar';
|
||||||
|
import { search } from '@/scripts/search';
|
||||||
|
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
XCommon,
|
XCommon,
|
||||||
XSidebar,
|
XSidebar,
|
||||||
|
XWidgets,
|
||||||
XSide, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる
|
XSide, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる
|
||||||
XTimeline,
|
XTimeline,
|
||||||
XPostForm,
|
XPostForm,
|
||||||
|
XHeaderClock,
|
||||||
},
|
},
|
||||||
|
|
||||||
provide() {
|
provide() {
|
||||||
|
@ -149,12 +191,21 @@ export default defineComponent({
|
||||||
antennas: null,
|
antennas: null,
|
||||||
followedChannels: null,
|
followedChannels: null,
|
||||||
featuredChannels: null,
|
featuredChannels: null,
|
||||||
|
currentChannel: null,
|
||||||
menuDef: sidebarDef,
|
menuDef: sidebarDef,
|
||||||
faLayerGroup, faBars, faBell, faHome, faCircle, faPencilAlt, faShareAlt, faSatelliteDish, faListUl, faSatellite, faCog,
|
sideViewOpening: false,
|
||||||
|
instanceName,
|
||||||
|
faLayerGroup, faBars, faBell, faHome, faCircle, faPencilAlt, faShareAlt, faSatelliteDish, faListUl, faSatellite, faCog, faSearch, faPlus, faStar, farStar, faAt, faLink, faEllipsisH, faGlobe, faComments, faEnvelope,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
|
router.beforeEach((to, from) => {
|
||||||
|
this.$refs.side.navigate(to.fullPath);
|
||||||
|
// search?q=foo のようなクエリを受け取れるようにするため、return falseはできない
|
||||||
|
//return false;
|
||||||
|
});
|
||||||
|
|
||||||
os.api('users/lists/list').then(lists => {
|
os.api('users/lists/list').then(lists => {
|
||||||
this.lists = lists;
|
this.lists = lists;
|
||||||
});
|
});
|
||||||
|
@ -170,6 +221,14 @@ export default defineComponent({
|
||||||
os.api('channels/featured').then(channels => {
|
os.api('channels/featured').then(channels => {
|
||||||
this.featuredChannels = channels;
|
this.featuredChannels = channels;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.$watch('tl', () => {
|
||||||
|
if (this.tl.startsWith('channel:')) {
|
||||||
|
os.api('channels/show', { channelId: this.tl.replace('channel:', '') }).then(channel => {
|
||||||
|
this.currentChannel = channel;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -181,10 +240,38 @@ export default defineComponent({
|
||||||
os.post();
|
os.post();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
search() {
|
||||||
|
search();
|
||||||
|
},
|
||||||
|
|
||||||
top() {
|
top() {
|
||||||
window.scroll({ top: 0, behavior: 'smooth' });
|
window.scroll({ top: 0, behavior: 'smooth' });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async toggleChannelFollow() {
|
||||||
|
if (this.currentChannel.isFollowing) {
|
||||||
|
await os.apiWithDialog('channels/unfollow', {
|
||||||
|
channelId: this.currentChannel.id
|
||||||
|
});
|
||||||
|
this.currentChannel.isFollowing = false;
|
||||||
|
} else {
|
||||||
|
await os.apiWithDialog('channels/follow', {
|
||||||
|
channelId: this.currentChannel.id
|
||||||
|
});
|
||||||
|
this.currentChannel.isFollowing = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
openChannelMenu(ev) {
|
||||||
|
os.modalMenu([{
|
||||||
|
text: this.$ts.copyUrl,
|
||||||
|
icon: faLink,
|
||||||
|
action: () => {
|
||||||
|
copyToClipboard(`${url}/channels/${this.currentChannel.id}`);
|
||||||
|
}
|
||||||
|
}], ev.currentTarget || ev.target);
|
||||||
|
},
|
||||||
|
|
||||||
onTransition() {
|
onTransition() {
|
||||||
if (window._scroll) window._scroll();
|
if (window._scroll) window._scroll();
|
||||||
},
|
},
|
||||||
|
@ -231,8 +318,7 @@ export default defineComponent({
|
||||||
$ui-font-size: 1em; // TODO: どこかに集約したい
|
$ui-font-size: 1em; // TODO: どこかに集約したい
|
||||||
|
|
||||||
// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
|
// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
|
||||||
min-height: calc(var(--vh, 1vh) * 100);
|
height: calc(var(--vh, 1vh) * 100);
|
||||||
box-sizing: border-box;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
> .nav {
|
> .nav {
|
||||||
|
@ -245,11 +331,11 @@ export default defineComponent({
|
||||||
> .header, > .footer {
|
> .header, > .footer {
|
||||||
$padding: 8px;
|
$padding: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
height: $header-height;
|
height: $header-height;
|
||||||
padding: $padding;
|
padding: $padding;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
line-height: ($header-height - ($padding * 2));
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
&.header {
|
&.header {
|
||||||
|
@ -260,31 +346,29 @@ export default defineComponent({
|
||||||
border-top: solid 1px var(--divider);
|
border-top: solid 1px var(--divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
> .left {
|
> .left, > .right {
|
||||||
> .account {
|
> .item, > .menu {
|
||||||
display: flex;
|
display: inline-block;
|
||||||
align-items: center;
|
vertical-align: middle;
|
||||||
padding: 0 8px;
|
|
||||||
|
|
||||||
> .avatar {
|
|
||||||
width: 26px;
|
|
||||||
height: 26px;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .right {
|
|
||||||
margin-left: auto;
|
|
||||||
|
|
||||||
> .item {
|
|
||||||
height: ($header-height - ($padding * 2));
|
height: ($header-height - ($padding * 2));
|
||||||
width: ($header-height - ($padding * 2));
|
width: ($header-height - ($padding * 2));
|
||||||
padding: 10px;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin-right: 4px;
|
|
||||||
//opacity: 0.6;
|
//opacity: 0.6;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
> i {
|
> i {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -297,11 +381,40 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .left {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
> .account {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 8px;
|
||||||
|
|
||||||
|
> .avatar {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .text {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .right {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .body {
|
> .body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
padding: 8px 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
> .container {
|
> .container {
|
||||||
|
@ -310,9 +423,14 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
> .header {
|
> .header {
|
||||||
|
display: flex;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
|
|
||||||
|
> .add {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .body {
|
> .body {
|
||||||
|
@ -338,7 +456,7 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
> .icon {
|
> .icon {
|
||||||
margin-right: 6px;
|
margin-right: 8px;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -358,18 +476,20 @@ export default defineComponent({
|
||||||
|
|
||||||
> .header {
|
> .header {
|
||||||
$padding: 8px;
|
$padding: 8px;
|
||||||
|
display: flex;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
height: $header-height;
|
height: $header-height;
|
||||||
padding: $padding;
|
padding: $padding;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
line-height: ($header-height - ($padding * 2));
|
|
||||||
font-weight: bold;
|
|
||||||
background-color: var(--panel);
|
background-color: var(--panel);
|
||||||
border-bottom: solid 1px var(--divider);
|
border-bottom: solid 1px var(--divider);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
> div {
|
> .left {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
> .icon {
|
> .icon {
|
||||||
height: ($header-height - ($padding * 2));
|
height: ($header-height - ($padding * 2));
|
||||||
|
@ -379,11 +499,60 @@ export default defineComponent({
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .title {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
min-width: 0;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
> .description {
|
||||||
|
opacity: 0.6;
|
||||||
|
font-size: 0.8em;
|
||||||
|
font-weight: normal;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
padding-left: 8px;
|
||||||
|
|
||||||
|
> .instance {
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .clock {
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .button {
|
||||||
|
height: ($header-height - ($padding * 2));
|
||||||
|
width: ($header-height - ($padding * 2));
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.follow.followed {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .footer {
|
> .footer {
|
||||||
padding: 16px;
|
padding: 0 16px 16px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .body {
|
> .body {
|
||||||
|
@ -394,7 +563,14 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
> .side {
|
> .side {
|
||||||
|
width: 350px;
|
||||||
border-left: solid 1px var(--divider);
|
border-left: solid 1px var(--divider);
|
||||||
|
|
||||||
|
&.widgets.sideViewOpening {
|
||||||
|
@media (max-width: 1400px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="note"
|
class="vfzoeqcg"
|
||||||
v-if="!muted"
|
v-if="!muted"
|
||||||
v-show="!isDeleted"
|
v-show="!isDeleted"
|
||||||
:tabindex="!isDeleted ? '-1' : null"
|
:tabindex="!isDeleted ? '-1' : null"
|
||||||
:class="{ renote: isRenote }"
|
:class="{ renote: isRenote, highlighted: appearNote._prId_ || appearNote._featuredId_, operating }"
|
||||||
v-hotkey="keymap"
|
v-hotkey="keymap"
|
||||||
>
|
>
|
||||||
<XSub :note="appearNote.reply" class="reply-to" v-if="appearNote.reply"/>
|
<XSub :note="appearNote.reply" class="reply-to" v-if="appearNote.reply"/>
|
||||||
|
@ -171,6 +171,7 @@ export default defineComponent({
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
muted: false,
|
muted: false,
|
||||||
|
operating: false,
|
||||||
faEdit, faBolt, faTimes, faBullhorn, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faBiohazard, faPlug, faSatelliteDish
|
faEdit, faBolt, faTimes, faBullhorn, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faBiohazard, faPlug, faSatelliteDish
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -439,16 +440,19 @@ export default defineComponent({
|
||||||
|
|
||||||
reply(viaKeyboard = false) {
|
reply(viaKeyboard = false) {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
|
this.operating = true;
|
||||||
os.post({
|
os.post({
|
||||||
reply: this.appearNote,
|
reply: this.appearNote,
|
||||||
animation: !viaKeyboard,
|
animation: !viaKeyboard,
|
||||||
}, () => {
|
}, () => {
|
||||||
|
this.operating = false;
|
||||||
this.focus();
|
this.focus();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
renote(viaKeyboard = false) {
|
renote(viaKeyboard = false) {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
|
this.operating = true;
|
||||||
this.blur();
|
this.blur();
|
||||||
os.modalMenu([{
|
os.modalMenu([{
|
||||||
text: this.$ts.renote,
|
text: this.$ts.renote,
|
||||||
|
@ -468,6 +472,8 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
}], this.$refs.renoteButton, {
|
}], this.$refs.renoteButton, {
|
||||||
viaKeyboard
|
viaKeyboard
|
||||||
|
}).then(() => {
|
||||||
|
this.operating = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -494,10 +500,11 @@ export default defineComponent({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
react(viaKeyboard = false) {
|
async react(viaKeyboard = false) {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
|
this.operating = true;
|
||||||
this.blur();
|
this.blur();
|
||||||
os.popup(import('@/components/emoji-picker.vue'), {
|
const { dispose } = await os.popup(import('@/components/emoji-picker.vue'), {
|
||||||
src: this.$refs.reactButton,
|
src: this.$refs.reactButton,
|
||||||
asReactionPicker: true
|
asReactionPicker: true
|
||||||
}, {
|
}, {
|
||||||
|
@ -508,9 +515,13 @@ export default defineComponent({
|
||||||
reaction: reaction
|
reaction: reaction
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.focus();
|
|
||||||
},
|
},
|
||||||
}, 'closed');
|
closed: () => {
|
||||||
|
this.operating = false;
|
||||||
|
this.focus();
|
||||||
|
dispose();
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
reactDirectly(reaction) {
|
reactDirectly(reaction) {
|
||||||
|
@ -734,9 +745,13 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
menu(viaKeyboard = false) {
|
menu(viaKeyboard = false) {
|
||||||
|
this.operating = true;
|
||||||
os.modalMenu(this.getMenu(), this.$refs.menuButton, {
|
os.modalMenu(this.getMenu(), this.$refs.menuButton, {
|
||||||
viaKeyboard
|
viaKeyboard
|
||||||
}).then(this.focus);
|
}).then(() => {
|
||||||
|
this.operating = false;
|
||||||
|
this.focus();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
showRenoteMenu(viaKeyboard = false) {
|
showRenoteMenu(viaKeyboard = false) {
|
||||||
|
@ -857,10 +872,8 @@ export default defineComponent({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.note {
|
.vfzoeqcg {
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: box-shadow 0.1s ease;
|
|
||||||
overflow: hidden;
|
|
||||||
contain: content;
|
contain: content;
|
||||||
|
|
||||||
// これらの指定はパフォーマンス向上には有効だが、ノートの高さは一定でないため、
|
// これらの指定はパフォーマンス向上には有効だが、ノートの高さは一定でないため、
|
||||||
|
@ -879,18 +892,24 @@ export default defineComponent({
|
||||||
background: rgba(0, 0, 0, 0.05);
|
background: rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover > .article > .main > .footer {
|
&:hover, &.operating {
|
||||||
|
> .article > .main > .footer {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.renote {
|
&.renote {
|
||||||
background: rgba(128, 255, 0, 0.05);
|
background: rgba(128, 255, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.highlighted {
|
||||||
|
background: rgba(255, 128, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
> .info {
|
> .info {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 16px 32px 8px 32px;
|
padding: 12px 16px 4px 16px;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
|
@ -901,8 +920,9 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
> .hide {
|
> .hide {
|
||||||
margin-left: auto;
|
margin-left: 16px;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -918,7 +938,7 @@ export default defineComponent({
|
||||||
> .renote {
|
> .renote {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 12px 16px 8px 16px;
|
padding: 12px 16px 4px 16px;
|
||||||
line-height: 28px;
|
line-height: 28px;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
color: var(--renote);
|
color: var(--renote);
|
||||||
|
@ -983,8 +1003,8 @@ export default defineComponent({
|
||||||
> .avatar {
|
> .avatar {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: block;
|
display: block;
|
||||||
//position: sticky;
|
position: sticky;
|
||||||
//top: 72px;
|
top: 12px;
|
||||||
margin: 0 14px 0 0;
|
margin: 0 14px 0 0;
|
||||||
width: 46px;
|
width: 46px;
|
||||||
height: 46px;
|
height: 46px;
|
||||||
|
@ -1054,8 +1074,13 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .files {
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
> .url-preview {
|
> .url-preview {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
|
max-width: 500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .poll {
|
> .poll {
|
||||||
|
@ -1122,5 +1147,9 @@ export default defineComponent({
|
||||||
.muted {
|
.muted {
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -108,7 +108,7 @@ export default defineComponent({
|
||||||
autofocus: {
|
autofocus: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: false
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="qvzfzxam _narrow_" v-if="component">
|
<div class="mrajymqm _narrow_" v-if="component">
|
||||||
<div class="container">
|
|
||||||
<header class="header" @contextmenu.prevent.stop="onContextmenu">
|
<header class="header" @contextmenu.prevent.stop="onContextmenu">
|
||||||
<button class="_button" @click="back()" v-if="history.length > 0"><Fa :icon="faChevronLeft"/></button>
|
<button class="_button" @click="back()" v-if="history.length > 0"><Fa :icon="faChevronLeft"/></button>
|
||||||
<XHeader class="title" :info="pageInfo" :with-back="false" :center="false"/>
|
<XHeader class="title" :info="pageInfo" :with-back="false" :center="false"/>
|
||||||
<button class="_button" @click="close()"><Fa :icon="faTimes"/></button>
|
<button class="_button" @click="close()"><Fa :icon="faTimes"/></button>
|
||||||
</header>
|
</header>
|
||||||
<component :is="component" v-bind="props" :ref="changePage"/>
|
<component :is="component" v-bind="props" :ref="changePage"/>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -64,6 +62,7 @@ export default defineComponent({
|
||||||
const { component, props } = resolve(path);
|
const { component, props } = resolve(path);
|
||||||
this.component = component;
|
this.component = component;
|
||||||
this.props = props;
|
this.props = props;
|
||||||
|
this.$emit('open');
|
||||||
},
|
},
|
||||||
|
|
||||||
back() {
|
back() {
|
||||||
|
@ -74,6 +73,7 @@ export default defineComponent({
|
||||||
this.path = null;
|
this.path = null;
|
||||||
this.component = null;
|
this.component = null;
|
||||||
this.props = {};
|
this.props = {};
|
||||||
|
this.$emit('close');
|
||||||
},
|
},
|
||||||
|
|
||||||
onContextmenu(e) {
|
onContextmenu(e) {
|
||||||
|
@ -114,18 +114,13 @@ export default defineComponent({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.qvzfzxam {
|
.mrajymqm {
|
||||||
$header-height: 54px; // TODO: どこかに集約したい
|
$header-height: 54px; // TODO: どこかに集約したい
|
||||||
|
|
||||||
--section-padding: 16px;
|
--section-padding: 16px;
|
||||||
--margin: var(--marginHalf);
|
--margin: var(--marginHalf);
|
||||||
|
|
||||||
width: 390px;
|
height: 100%;
|
||||||
|
|
||||||
> .container {
|
|
||||||
position: fixed;
|
|
||||||
width: 390px;
|
|
||||||
height: 100vh;
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
@ -159,7 +154,6 @@ export default defineComponent({
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
13
src/client/ui/chat/store.ts
Normal file
13
src/client/ui/chat/store.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { markRaw } from 'vue';
|
||||||
|
import { Storage } from '../../pizzax';
|
||||||
|
|
||||||
|
export const store = markRaw(new Storage('chatUi', {
|
||||||
|
widgets: {
|
||||||
|
where: 'account',
|
||||||
|
default: [] as {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
data: Record<string, any>;
|
||||||
|
}[]
|
||||||
|
},
|
||||||
|
}));
|
|
@ -1,5 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<XNotes ref="tl" :pagination="pagination" @queue="$emit('queue', $event)" v-follow="pagination.reversed"/>
|
<div class="dbiokgaf">
|
||||||
|
<div class="new" v-if="queue > 0" :style="{ width: width + 'px', [pagination.reversed ? 'bottom' : 'top']: pagination.reversed ? bottom + 'px' : top + 'px' }"><button class="_buttonPrimary" @click="goTop()">{{ $ts.newNoteRecived }}</button></div>
|
||||||
|
<XNotes class="tl" ref="tl" :pagination="pagination" @queue="queueUpdated" v-follow="pagination.reversed"/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -7,7 +10,7 @@ import { defineComponent } from 'vue';
|
||||||
import XNotes from './notes.vue';
|
import XNotes from './notes.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import * as sound from '@/scripts/sound';
|
import * as sound from '@/scripts/sound';
|
||||||
import { scrollToBottom } from '@/scripts/scroll';
|
import { scrollToBottom, getScrollPosition, getScrollContainer } from '@/scripts/scroll';
|
||||||
import follow from '@/directives/follow-append';
|
import follow from '@/directives/follow-append';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
@ -62,6 +65,10 @@ export default defineComponent({
|
||||||
includeLocalRenotes: this.$store.state.showLocalRenotes
|
includeLocalRenotes: this.$store.state.showLocalRenotes
|
||||||
},
|
},
|
||||||
query: {},
|
query: {},
|
||||||
|
queue: 0,
|
||||||
|
width: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -185,6 +192,43 @@ export default defineComponent({
|
||||||
focus() {
|
focus() {
|
||||||
this.$refs.tl.focus();
|
this.$refs.tl.focus();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
goTop() {
|
||||||
|
const container = getScrollContainer(this.$el);
|
||||||
|
container.scrollTop = 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
queueUpdated(q) {
|
||||||
|
if (this.$el.offsetWidth !== 0) {
|
||||||
|
const rect = this.$el.getBoundingClientRect();
|
||||||
|
const scrollTop = getScrollPosition(this.$el);
|
||||||
|
this.width = this.$el.offsetWidth;
|
||||||
|
this.top = rect.top + scrollTop;
|
||||||
|
this.bottom = this.$el.offsetHeight;
|
||||||
|
}
|
||||||
|
this.queue = q;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.dbiokgaf {
|
||||||
|
padding: 16px 0;
|
||||||
|
|
||||||
|
// TODO: これはノート追加アニメーションによるスクロール発生を抑えるために必要だが、position stickyが効かなくなるので、両者を両立させる良い方法を考える
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
> .new {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
|
||||||
|
> button {
|
||||||
|
display: block;
|
||||||
|
margin: 16px auto;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
61
src/client/ui/chat/widgets.vue
Normal file
61
src/client/ui/chat/widgets.vue
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
<template>
|
||||||
|
<div class="qydbhufi">
|
||||||
|
<XWidgets :edit="edit" :widgets="widgets" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="edit = false"/>
|
||||||
|
|
||||||
|
<button v-if="edit" @click="edit = false" class="_textButton" style="font-size: 0.9em;">{{ $ts.editWidgetsExit }}</button>
|
||||||
|
<button v-else @click="edit = true" class="_textButton" style="font-size: 0.9em;">{{ $ts.editWidgets }}</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, defineAsyncComponent } from 'vue';
|
||||||
|
import XWidgets from '@/components/widgets.vue';
|
||||||
|
import { store } from './store.ts';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
XWidgets,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
edit: false,
|
||||||
|
widgets: store.reactiveState.widgets
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
addWidget(widget) {
|
||||||
|
store.set('widgets', [widget, ...store.state.widgets]);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeWidget(widget) {
|
||||||
|
store.set('widgets', store.state.widgets.filter(w => w.id != widget.id));
|
||||||
|
},
|
||||||
|
|
||||||
|
updateWidget({ id, data }) {
|
||||||
|
store.set('widgets', store.state.widgets.map(w => w.id === id ? {
|
||||||
|
...w,
|
||||||
|
data: data
|
||||||
|
} : w));
|
||||||
|
},
|
||||||
|
|
||||||
|
updateWidgets(widgets) {
|
||||||
|
store.set('widgets', widgets);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.qydbhufi {
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: auto;
|
||||||
|
padding: var(--margin);
|
||||||
|
|
||||||
|
::v-deep(._panel) {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -3,49 +3,22 @@
|
||||||
<template #header><Fa :icon="faWindowMaximize" style="margin-right: 8px;"/>{{ column.name }}</template>
|
<template #header><Fa :icon="faWindowMaximize" style="margin-right: 8px;"/>{{ column.name }}</template>
|
||||||
|
|
||||||
<div class="wtdtxvec">
|
<div class="wtdtxvec">
|
||||||
<template v-if="edit">
|
<XWidgets :edit="edit" :widgets="column.widgets" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="edit = false"/>
|
||||||
<header>
|
|
||||||
<MkSelect v-model:value="widgetAdderSelected" style="margin-bottom: var(--margin)">
|
|
||||||
<template #label>{{ $ts.selectWidget }}</template>
|
|
||||||
<option v-for="widget in widgets" :value="widget" :key="widget">{{ $t(`_widgets.${widget}`) }}</option>
|
|
||||||
</MkSelect>
|
|
||||||
<MkButton inline @click="addWidget" primary><Fa :icon="faPlus"/> {{ $ts.add }}</MkButton>
|
|
||||||
<MkButton inline @click="edit = false">{{ $ts.close }}</MkButton>
|
|
||||||
</header>
|
|
||||||
<XDraggable
|
|
||||||
v-model="_widgets"
|
|
||||||
item-key="id"
|
|
||||||
animation="150"
|
|
||||||
>
|
|
||||||
<template #item="{element}">
|
|
||||||
<div class="customize-container" @click="widgetFunc(element.id)">
|
|
||||||
<button class="remove _button" @click.prevent.stop="removeWidget(element)"><Fa :icon="faTimes"/></button>
|
|
||||||
<component :is="`mkw-${element.name}`" :widget="element" :setting-callback="setting => settings[element.id] = setting" :column="column" @updateProps="saveWidget(element.id, $event)"/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</XDraggable>
|
|
||||||
</template>
|
|
||||||
<component v-else class="widget" v-for="widget in column.widgets" :is="`mkw-${widget.name}`" :key="widget.id" :widget="widget" :column="column" @updateProps="saveWidget(widget.id, $event)"/>
|
|
||||||
</div>
|
</div>
|
||||||
</XColumn>
|
</XColumn>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, defineAsyncComponent } from 'vue';
|
import { defineComponent, defineAsyncComponent } from 'vue';
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import { faWindowMaximize, faTimes, faCog, faPlus } from '@fortawesome/free-solid-svg-icons';
|
import { faWindowMaximize, faTimes, faCog, faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||||
import MkSelect from '@/components/ui/select.vue';
|
import XWidgets from '@/components/widgets.vue';
|
||||||
import MkButton from '@/components/ui/button.vue';
|
|
||||||
import XColumn from './column.vue';
|
import XColumn from './column.vue';
|
||||||
import { widgets } from '../../widgets';
|
|
||||||
import { addColumnWidget, removeColumnWidget, setColumnWidgets, updateColumnWidget } from './deck-store';
|
import { addColumnWidget, removeColumnWidget, setColumnWidgets, updateColumnWidget } from './deck-store';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
XColumn,
|
XColumn,
|
||||||
XDraggable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)),
|
XWidgets,
|
||||||
MkSelect,
|
|
||||||
MkButton,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
|
@ -62,49 +35,27 @@ export default defineComponent({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
edit: false,
|
edit: false,
|
||||||
widgetAdderSelected: null,
|
|
||||||
widgets,
|
|
||||||
settings: {},
|
|
||||||
faWindowMaximize, faTimes, faPlus
|
faWindowMaximize, faTimes, faPlus
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
|
||||||
_widgets: {
|
|
||||||
get() {
|
|
||||||
return this.column.widgets;
|
|
||||||
},
|
|
||||||
set(value) {
|
|
||||||
setColumnWidgets(this.column.id, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
widgetFunc(id) {
|
addWidget(widget) {
|
||||||
this.settings[id]();
|
addColumnWidget(this.column.id, widget);
|
||||||
},
|
|
||||||
|
|
||||||
addWidget() {
|
|
||||||
if (this.widgetAdderSelected == null) return;
|
|
||||||
|
|
||||||
addColumnWidget(this.column.id, {
|
|
||||||
name: this.widgetAdderSelected,
|
|
||||||
id: uuid(),
|
|
||||||
data: {}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.widgetAdderSelected = null;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
removeWidget(widget) {
|
removeWidget(widget) {
|
||||||
removeColumnWidget(this.column.id, widget);
|
removeColumnWidget(this.column.id, widget);
|
||||||
},
|
},
|
||||||
|
|
||||||
saveWidget(id, data) {
|
updateWidget({ id, data }) {
|
||||||
updateColumnWidget(this.column.id, id, data);
|
updateColumnWidget(this.column.id, id, data);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateWidgets(widgets) {
|
||||||
|
setColumnWidgets(this.column.id, widgets);
|
||||||
|
},
|
||||||
|
|
||||||
func() {
|
func() {
|
||||||
this.edit = !this.edit;
|
this.edit = !this.edit;
|
||||||
}
|
}
|
||||||
|
@ -114,46 +65,12 @@ export default defineComponent({
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.wtdtxvec {
|
.wtdtxvec {
|
||||||
._panel {
|
--margin: 8px;
|
||||||
|
|
||||||
|
padding: 0 var(--margin);
|
||||||
|
|
||||||
|
::v-deep(._panel) {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
> 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>
|
</style>
|
||||||
|
|
|
@ -1,46 +1,21 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="efzpzdvf">
|
<div class="efzpzdvf">
|
||||||
<template v-if="editMode">
|
<XWidgets :edit="editMode" :widgets="$store.reactiveState.widgets.value" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="editMode = false"/>
|
||||||
<MkButton primary @click="addWidget" class="add"><Fa :icon="faPlus"/></MkButton>
|
|
||||||
<XDraggable
|
<button v-if="editMode" @click="editMode = false" class="_textButton" style="font-size: 0.9em;"><Fa :icon="faCheck"/> {{ $ts.editWidgetsExit }}</button>
|
||||||
v-model="widgets"
|
<button v-else @click="editMode = true" class="_textButton" style="font-size: 0.9em;"><Fa :icon="faPencilAlt"/> {{ $ts.editWidgets }}</button>
|
||||||
item-key="id"
|
|
||||||
handle=".handle"
|
|
||||||
animation="150"
|
|
||||||
class="sortable"
|
|
||||||
>
|
|
||||||
<template #item="{element}">
|
|
||||||
<div class="customize-container _panel">
|
|
||||||
<header>
|
|
||||||
<span class="handle"><Fa :icon="faBars"/></span>{{ $t('_widgets.' + element.name) }}<button class="remove _button" @click="removeWidget(element)"><Fa :icon="faTimes"/></button>
|
|
||||||
</header>
|
|
||||||
<div @click="widgetFunc(element.id)">
|
|
||||||
<component class="_inContainer_ _forceContainerFull_" :is="`mkw-${element.name}`" :widget="element" :ref="element.id" :setting-callback="setting => settings[element.id] = setting" @updateProps="saveWidget(element.id, $event)"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</XDraggable>
|
|
||||||
<button @click="editMode = false" class="_textButton" style="font-size: 0.9em;"><Fa :icon="faCheck"/> {{ $ts.editWidgetsExit }}</button>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<component v-for="widget in widgets" class="_inContainer_ _forceContainerFull_" :is="`mkw-${widget.name}`" :key="widget.id" :widget="widget" @updateProps="saveWidget(widget.id, $event)"/>
|
|
||||||
<button @click="editMode = true" class="_textButton" style="font-size: 0.9em;"><Fa :icon="faPencilAlt"/> {{ $ts.editWidgets }}</button>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, defineAsyncComponent } from 'vue';
|
import { defineComponent, defineAsyncComponent } from 'vue';
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import { faPencilAlt, faPlus, faBars, faTimes, faCheck } from '@fortawesome/free-solid-svg-icons';
|
import { faPencilAlt, faPlus, faBars, faTimes, faCheck } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { widgets } from '@/widgets';
|
import XWidgets from '@/components/widgets.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import MkButton from '@/components/ui/button.vue';
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
MkButton,
|
XWidgets
|
||||||
XDraggable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
emits: ['mounted'],
|
emits: ['mounted'],
|
||||||
|
@ -48,62 +23,35 @@ export default defineComponent({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
editMode: false,
|
editMode: false,
|
||||||
settings: {},
|
|
||||||
faPencilAlt, faPlus, faBars, faTimes, faCheck,
|
faPencilAlt, faPlus, faBars, faTimes, faCheck,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
|
||||||
widgets: {
|
|
||||||
get() {
|
|
||||||
return this.$store.reactiveState.widgets.value;
|
|
||||||
},
|
|
||||||
set(value) {
|
|
||||||
this.$store.set('widgets', value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$emit('mounted', this.$el);
|
this.$emit('mounted', this.$el);
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
widgetFunc(id) {
|
addWidget(widget) {
|
||||||
this.settings[id]();
|
this.$store.set('widgets', [{
|
||||||
},
|
...widget,
|
||||||
|
|
||||||
async addWidget() {
|
|
||||||
const { canceled, result: widget } = await os.dialog({
|
|
||||||
type: null,
|
|
||||||
title: this.$ts.chooseWidget,
|
|
||||||
select: {
|
|
||||||
items: widgets.map(widget => ({
|
|
||||||
value: widget,
|
|
||||||
text: this.$t('_widgets.' + widget),
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
showCancelButton: true
|
|
||||||
});
|
|
||||||
if (canceled) return;
|
|
||||||
|
|
||||||
this.$store.set('widgets', [...this.$store.state.widgets, {
|
|
||||||
name: widget,
|
|
||||||
id: uuid(),
|
|
||||||
place: null,
|
place: null,
|
||||||
data: {}
|
}, ...this.$store.state.widgets]);
|
||||||
}]);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
removeWidget(widget) {
|
removeWidget(widget) {
|
||||||
this.$store.set('widgets', this.$store.state.widgets.filter(w => w.id != widget.id));
|
this.$store.set('widgets', this.$store.state.widgets.filter(w => w.id != widget.id));
|
||||||
},
|
},
|
||||||
|
|
||||||
saveWidget(id, data) {
|
updateWidget({ id, data }) {
|
||||||
this.$store.set('widgets', this.$store.state.widgets.map(w => w.id === id ? {
|
this.$store.set('widgets', this.$store.state.widgets.map(w => w.id === id ? {
|
||||||
...w,
|
...w,
|
||||||
data: data
|
data: data
|
||||||
} : w));
|
} : w));
|
||||||
|
},
|
||||||
|
|
||||||
|
updateWidgets(widgets) {
|
||||||
|
this.$store.set('widgets', widgets);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -129,35 +77,5 @@ export default defineComponent({
|
||||||
> .add {
|
> .add {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customize-container {
|
|
||||||
margin: 8px 0;
|
|
||||||
|
|
||||||
> header {
|
|
||||||
position: relative;
|
|
||||||
line-height: 32px;
|
|
||||||
|
|
||||||
> .handle {
|
|
||||||
padding: 0 8px;
|
|
||||||
cursor: move;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .remove {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
padding: 0 8px;
|
|
||||||
line-height: 32px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> div {
|
|
||||||
padding: 8px;
|
|
||||||
|
|
||||||
> * {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
Loading…
Reference in a new issue