Merge branch 'develop' into sw-notification-action

This commit is contained in:
tamaina 2021-02-17 02:21:11 +09:00
commit e41aa2390c
20 changed files with 740 additions and 377 deletions

View file

@ -97,6 +97,7 @@ cantRenote: "この投稿はRenoteできません。"
cantReRenote: "RenoteをRenoteすることはできません。" cantReRenote: "RenoteをRenoteすることはできません。"
quote: "引用" quote: "引用"
pinnedNote: "ピン留めされたノート" pinnedNote: "ピン留めされたノート"
pinned: "ピン留め"
you: "あなた" you: "あなた"
clickToShow: "クリックして表示" clickToShow: "クリックして表示"
sensitive: "閲覧注意" sensitive: "閲覧注意"

View file

@ -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;

View file

@ -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"/>

View file

@ -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;
} }
} }

View file

@ -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>

View 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>

View file

@ -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;

View file

@ -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', () => {

View file

@ -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); }

View file

@ -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);
} }
> .list-enter-from {
&[data-direction="up"] { opacity: 0;
> .list-enter-from { transform: translateY(-64px);
opacity: 0;
transform: translateY(64px);
}
} }
&[data-direction="down"] { > .list-reversed-enter-active, > .list-reversed-leave-active {
> .list-enter-from { transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
opacity: 0; }
transform: translateY(-64px); > .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 {

View 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>

View file

@ -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">
<Fa :icon="faHome" class="icon"/> <template v-if="tl === 'home'">
<div class="title">{{ $ts._timelines.home }}</div> <Fa :icon="faHome" class="icon"/>
<div class="title">{{ $ts._timelines.home }}</div>
</template>
<template v-else-if="tl === 'local'">
<Fa :icon="faComments" class="icon"/>
<div class="title">{{ $ts._timelines.local }}</div>
</template>
<template v-else-if="tl === 'social'">
<Fa :icon="faShareAlt" class="icon"/>
<div class="title">{{ $ts._timelines.social }}</div>
</template>
<template v-else-if="tl === 'global'">
<Fa :icon="faGlobe" class="icon"/>
<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>
<div v-else-if="tl === 'local'">
<Fa :icon="faShareAlt" class="icon"/> <div class="right">
<div class="title">{{ $ts._timelines.local }}</div> <div class="instance">{{ instanceName }}</div>
</div> <XHeaderClock class="clock"/>
<div v-else-if="tl === 'social'"> <button class="_button button search" @click="search" v-tooltip="$ts.search">
<Fa :icon="faShareAlt" class="icon"/> <Fa :icon="faSearch"/>
<div class="title">{{ $ts._timelines.social }}</div> </button>
</div> <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">
<div v-else-if="tl === 'global'"> <Fa v-if="currentChannel.isFollowing" :icon="faStar"/>
<Fa :icon="faShareAlt" class="icon"/> <Fa v-else :icon="farStar"/>
<div class="title">{{ $ts._timelines.global }}</div> </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 importAsyncComponentWrapperref XSide, // NOTE: dynamic importAsyncComponentWrapperref
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>

View file

@ -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 {
display: block; > .article > .main > .footer {
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>

View file

@ -108,7 +108,7 @@ export default defineComponent({
autofocus: { autofocus: {
type: Boolean, type: Boolean,
required: false, required: false,
default: true default: false
}, },
}, },

View file

@ -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,50 +114,44 @@ 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%;
overflow: auto;
box-sizing: border-box;
> .container { > .header {
position: fixed; display: flex;
width: 390px; position: sticky;
height: 100vh; z-index: 1000;
overflow: auto; top: 0;
height: $header-height;
width: 100%;
line-height: $header-height;
font-weight: bold;
//background-color: var(--panel);
-webkit-backdrop-filter: blur(32px);
backdrop-filter: blur(32px);
background-color: var(--header);
border-bottom: solid 1px var(--divider);
box-sizing: border-box; box-sizing: border-box;
> .header { > ._button {
display: flex;
position: sticky;
z-index: 1000;
top: 0;
height: $header-height; height: $header-height;
width: 100%; width: $header-height;
line-height: $header-height;
font-weight: bold;
//background-color: var(--panel);
-webkit-backdrop-filter: blur(32px);
backdrop-filter: blur(32px);
background-color: var(--header);
border-bottom: solid 1px var(--divider);
box-sizing: border-box;
> ._button { &:hover {
height: $header-height; color: var(--fgHighlighted);
width: $header-height;
&:hover {
color: var(--fgHighlighted);
}
} }
}
> .title { > .title {
flex: 1; flex: 1;
position: relative; position: relative;
}
} }
} }
} }

View 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>;
}[]
},
}));

View file

@ -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>

View 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>

View file

@ -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>

View file

@ -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>