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することはできません。"
|
||||
quote: "引用"
|
||||
pinnedNote: "ピン留めされたノート"
|
||||
pinned: "ピン留め"
|
||||
you: "あなた"
|
||||
clickToShow: "クリックして表示"
|
||||
sensitive: "閲覧注意"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div
|
||||
class="note _panel"
|
||||
class="tkcbzcuz _panel"
|
||||
v-if="!muted"
|
||||
v-show="!isDeleted"
|
||||
:tabindex="!isDeleted ? '-1' : null"
|
||||
|
@ -858,7 +858,7 @@ export default defineComponent({
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.note {
|
||||
.tkcbzcuz {
|
||||
position: relative;
|
||||
transition: box-shadow 0.1s ease;
|
||||
overflow: hidden;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="mfcuwfyp">
|
||||
<div class="mfcuwfyp _noGap_">
|
||||
<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"/>
|
||||
<XNotification v-else :notification="notification" :with-time="true" :full="true" class="_panel notification" :key="notification.id"/>
|
||||
|
|
|
@ -98,11 +98,11 @@ export default defineComponent({
|
|||
}
|
||||
} else {
|
||||
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) {
|
||||
top = window.innerHeight - height + window.pageYOffset;
|
||||
top = window.innerHeight - height + window.pageYOffset - 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<transition name="zoom-in-top" appear @after-leave="$emit('closed')">
|
||||
<div class="buebdbiu _acrylic _shadow" v-if="showing">
|
||||
<transition name="tooltip" appear @after-leave="$emit('closed')">
|
||||
<div class="buebdbiu _acrylic _shadow" v-show="showing" ref="content">
|
||||
<slot>{{ text }}</slot>
|
||||
</div>
|
||||
</transition>
|
||||
|
@ -35,19 +35,43 @@ export default defineComponent({
|
|||
|
||||
const rect = this.source.getBoundingClientRect();
|
||||
|
||||
let x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
|
||||
let y = rect.top + window.pageYOffset + this.source.offsetHeight;
|
||||
const contentWidth = this.$refs.content.offsetWidth;
|
||||
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';
|
||||
this.$el.style.top = y + 'px';
|
||||
left -= (this.$el.offsetWidth / 2);
|
||||
|
||||
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>
|
||||
|
||||
<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 {
|
||||
position: absolute;
|
||||
z-index: 11000;
|
||||
|
@ -57,6 +81,6 @@ export default defineComponent({
|
|||
text-align: center;
|
||||
border-radius: 4px;
|
||||
pointer-events: none;
|
||||
transform-origin: center -16px;
|
||||
transform-origin: center top;
|
||||
}
|
||||
</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 height = container.scrollHeight;
|
||||
isBottom = (pos + viewHeight > height - 32);
|
||||
console.log(isBottom);
|
||||
}, { passive: true });
|
||||
container.scrollTop = container.scrollHeight;
|
||||
|
||||
const ro = new ResizeObserver((entries, observer) => {
|
||||
console.log(isBottom);
|
||||
if (isBottom) {
|
||||
const height = container.scrollHeight;
|
||||
container.scrollTop = height;
|
||||
|
|
|
@ -4,6 +4,7 @@ import { popup } from '@/os';
|
|||
|
||||
const start = isDeviceTouch ? 'touchstart' : 'mouseover';
|
||||
const end = isDeviceTouch ? 'touchend' : 'mouseleave';
|
||||
const delay = 100;
|
||||
|
||||
export default {
|
||||
mounted(el: HTMLElement, binding, vn) {
|
||||
|
@ -47,13 +48,13 @@ export default {
|
|||
el.addEventListener(start, () => {
|
||||
clearTimeout(self.showTimer);
|
||||
clearTimeout(self.hideTimer);
|
||||
self.showTimer = setTimeout(show, 300);
|
||||
self.showTimer = setTimeout(show, delay);
|
||||
}, { passive: true });
|
||||
|
||||
el.addEventListener(end, () => {
|
||||
clearTimeout(self.showTimer);
|
||||
clearTimeout(self.hideTimer);
|
||||
self.hideTimer = setTimeout(self.close, 300);
|
||||
self.hideTimer = setTimeout(self.close, delay);
|
||||
}, { passive: true });
|
||||
|
||||
el.addEventListener('click', () => {
|
||||
|
|
|
@ -488,19 +488,6 @@ hr {
|
|||
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 {
|
||||
0% { opacity: 1; transform: scale(1); }
|
||||
30% { opacity: 1; transform: scale(1); }
|
||||
|
|
|
@ -9,11 +9,6 @@ export default defineComponent({
|
|||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'down'
|
||||
},
|
||||
reversed: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
@ -37,14 +32,10 @@ export default defineComponent({
|
|||
});
|
||||
}
|
||||
|
||||
return h(!this.reversed ? TransitionGroup : 'div', !this.reversed ? {
|
||||
return h(TransitionGroup, {
|
||||
class: 'hmjzthxl',
|
||||
name: 'list',
|
||||
name: this.reversed ? 'list-reversed' : 'list',
|
||||
tag: 'div',
|
||||
'data-direction': this.direction,
|
||||
'data-reversed': this.reversed ? 'true' : 'false',
|
||||
} : {
|
||||
class: 'hmjzthxl',
|
||||
}, this.items.map((item, i) => {
|
||||
const el = this.$slots.default({
|
||||
item: item
|
||||
|
@ -95,23 +86,20 @@ export default defineComponent({
|
|||
> .list-move {
|
||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
|
||||
> .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);
|
||||
}
|
||||
|
||||
&[data-direction="up"] {
|
||||
> .list-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(64px);
|
||||
}
|
||||
> .list-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(-64px);
|
||||
}
|
||||
|
||||
&[data-direction="down"] {
|
||||
> .list-enter-from {
|
||||
opacity: 0;
|
||||
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>
|
||||
|
@ -120,6 +108,20 @@ export default defineComponent({
|
|||
.hmjzthxl {
|
||||
> .separator {
|
||||
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 {
|
||||
display: inline-block;
|
||||
|
@ -130,6 +132,7 @@ export default defineComponent({
|
|||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: var(--dateLabelFg);
|
||||
background: var(--panel);
|
||||
|
||||
> span {
|
||||
&: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">
|
||||
<div class="left">
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
</header>
|
||||
<div class="body">
|
||||
|
@ -18,65 +21,94 @@
|
|||
<div class="header">{{ $ts.timeline }}</div>
|
||||
<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/local" class="item" :class="{ active: tl === 'local' }"><Fa :icon="faHome" 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/global" class="item" :class="{ active: tl === 'global' }"><Fa :icon="faHome" 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>
|
||||
<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="faShareAlt" class="icon"/>{{ $ts._timelines.social }}</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="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">
|
||||
<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 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">
|
||||
<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 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>
|
||||
<footer class="footer">
|
||||
<div class="left">
|
||||
<button class="_button menu" @click="showMenu">
|
||||
<Fa :icon="faBars"/>
|
||||
<Fa class="icon" :icon="faBars"/>
|
||||
</button>
|
||||
</div>
|
||||
<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>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<main class="main" @contextmenu.stop="onContextmenu">
|
||||
<header class="header" ref="header" @click="onHeaderClick">
|
||||
<div v-if="tl === 'home'">
|
||||
<Fa :icon="faHome" class="icon"/>
|
||||
<div class="title">{{ $ts._timelines.home }}</div>
|
||||
<div class="left">
|
||||
<template v-if="tl === 'home'">
|
||||
<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 v-else-if="tl === 'local'">
|
||||
<Fa :icon="faShareAlt" class="icon"/>
|
||||
<div class="title">{{ $ts._timelines.local }}</div>
|
||||
</div>
|
||||
<div v-else-if="tl === 'social'">
|
||||
<Fa :icon="faShareAlt" class="icon"/>
|
||||
<div class="title">{{ $ts._timelines.social }}</div>
|
||||
</div>
|
||||
<div v-else-if="tl === 'global'">
|
||||
<Fa :icon="faShareAlt" class="icon"/>
|
||||
<div class="title">{{ $ts._timelines.global }}</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>
|
||||
</header>
|
||||
<div class="body">
|
||||
|
@ -89,7 +121,10 @@
|
|||
</footer>
|
||||
</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/>
|
||||
</div>
|
||||
|
@ -97,24 +132,31 @@
|
|||
|
||||
<script lang="ts">
|
||||
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 { faBell } from '@fortawesome/free-regular-svg-icons';
|
||||
import { instanceName } from '@/config';
|
||||
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, faStar as farStar, faEnvelope, faComments } from '@fortawesome/free-regular-svg-icons';
|
||||
import { instanceName, url } from '@/config';
|
||||
import XSidebar from '@/components/sidebar.vue';
|
||||
import XWidgets from './widgets.vue';
|
||||
import XCommon from '../_common_/common.vue';
|
||||
import XSide from './side.vue';
|
||||
import XTimeline from './timeline.vue';
|
||||
import XPostForm from './post-form.vue';
|
||||
import XHeaderClock from './header-clock.vue';
|
||||
import * as os from '@/os';
|
||||
import { router } from '@/router';
|
||||
import { sidebarDef } from '@/sidebar';
|
||||
import { search } from '@/scripts/search';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XCommon,
|
||||
XSidebar,
|
||||
XWidgets,
|
||||
XSide, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる
|
||||
XTimeline,
|
||||
XPostForm,
|
||||
XHeaderClock,
|
||||
},
|
||||
|
||||
provide() {
|
||||
|
@ -149,12 +191,21 @@ export default defineComponent({
|
|||
antennas: null,
|
||||
followedChannels: null,
|
||||
featuredChannels: null,
|
||||
currentChannel: null,
|
||||
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() {
|
||||
router.beforeEach((to, from) => {
|
||||
this.$refs.side.navigate(to.fullPath);
|
||||
// search?q=foo のようなクエリを受け取れるようにするため、return falseはできない
|
||||
//return false;
|
||||
});
|
||||
|
||||
os.api('users/lists/list').then(lists => {
|
||||
this.lists = lists;
|
||||
});
|
||||
|
@ -170,6 +221,14 @@ export default defineComponent({
|
|||
os.api('channels/featured').then(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: {
|
||||
|
@ -181,10 +240,38 @@ export default defineComponent({
|
|||
os.post();
|
||||
},
|
||||
|
||||
search() {
|
||||
search();
|
||||
},
|
||||
|
||||
top() {
|
||||
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() {
|
||||
if (window._scroll) window._scroll();
|
||||
},
|
||||
|
@ -231,8 +318,7 @@ export default defineComponent({
|
|||
$ui-font-size: 1em; // TODO: どこかに集約したい
|
||||
|
||||
// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
|
||||
min-height: calc(var(--vh, 1vh) * 100);
|
||||
box-sizing: border-box;
|
||||
height: calc(var(--vh, 1vh) * 100);
|
||||
display: flex;
|
||||
|
||||
> .nav {
|
||||
|
@ -245,11 +331,11 @@ export default defineComponent({
|
|||
> .header, > .footer {
|
||||
$padding: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
height: $header-height;
|
||||
padding: $padding;
|
||||
box-sizing: border-box;
|
||||
line-height: ($header-height - ($padding * 2));
|
||||
user-select: none;
|
||||
|
||||
&.header {
|
||||
|
@ -260,31 +346,29 @@ export default defineComponent({
|
|||
border-top: solid 1px var(--divider);
|
||||
}
|
||||
|
||||
> .left {
|
||||
> .account {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
|
||||
> .avatar {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .right {
|
||||
margin-left: auto;
|
||||
|
||||
> .item {
|
||||
> .left, > .right {
|
||||
> .item, > .menu {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
height: ($header-height - ($padding * 2));
|
||||
width: ($header-height - ($padding * 2));
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
margin-right: 4px;
|
||||
//opacity: 0.6;
|
||||
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 {
|
||||
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 {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 8px 0;
|
||||
overflow: auto;
|
||||
|
||||
> .container {
|
||||
|
@ -310,9 +423,14 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
> .header {
|
||||
display: flex;
|
||||
font-size: 0.9em;
|
||||
padding: 8px 16px;
|
||||
opacity: 0.7;
|
||||
|
||||
> .add {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
> .body {
|
||||
|
@ -338,7 +456,7 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
> .icon {
|
||||
margin-right: 6px;
|
||||
margin-right: 8px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
@ -358,18 +476,20 @@ export default defineComponent({
|
|||
|
||||
> .header {
|
||||
$padding: 8px;
|
||||
display: flex;
|
||||
z-index: 1000;
|
||||
height: $header-height;
|
||||
padding: $padding;
|
||||
box-sizing: border-box;
|
||||
line-height: ($header-height - ($padding * 2));
|
||||
font-weight: bold;
|
||||
background-color: var(--panel);
|
||||
border-bottom: solid 1px var(--divider);
|
||||
user-select: none;
|
||||
|
||||
> div {
|
||||
> .left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
> .icon {
|
||||
height: ($header-height - ($padding * 2));
|
||||
|
@ -379,11 +499,60 @@ export default defineComponent({
|
|||
margin-right: 4px;
|
||||
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 {
|
||||
padding: 16px;
|
||||
padding: 0 16px 16px 16px;
|
||||
}
|
||||
|
||||
> .body {
|
||||
|
@ -394,7 +563,14 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
> .side {
|
||||
width: 350px;
|
||||
border-left: solid 1px var(--divider);
|
||||
|
||||
&.widgets.sideViewOpening {
|
||||
@media (max-width: 1400px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<div
|
||||
class="note"
|
||||
class="vfzoeqcg"
|
||||
v-if="!muted"
|
||||
v-show="!isDeleted"
|
||||
:tabindex="!isDeleted ? '-1' : null"
|
||||
:class="{ renote: isRenote }"
|
||||
:class="{ renote: isRenote, highlighted: appearNote._prId_ || appearNote._featuredId_, operating }"
|
||||
v-hotkey="keymap"
|
||||
>
|
||||
<XSub :note="appearNote.reply" class="reply-to" v-if="appearNote.reply"/>
|
||||
|
@ -171,6 +171,7 @@ export default defineComponent({
|
|||
collapsed: false,
|
||||
isDeleted: false,
|
||||
muted: false,
|
||||
operating: false,
|
||||
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) {
|
||||
pleaseLogin();
|
||||
this.operating = true;
|
||||
os.post({
|
||||
reply: this.appearNote,
|
||||
animation: !viaKeyboard,
|
||||
}, () => {
|
||||
this.operating = false;
|
||||
this.focus();
|
||||
});
|
||||
},
|
||||
|
||||
renote(viaKeyboard = false) {
|
||||
pleaseLogin();
|
||||
this.operating = true;
|
||||
this.blur();
|
||||
os.modalMenu([{
|
||||
text: this.$ts.renote,
|
||||
|
@ -468,6 +472,8 @@ export default defineComponent({
|
|||
}
|
||||
}], this.$refs.renoteButton, {
|
||||
viaKeyboard
|
||||
}).then(() => {
|
||||
this.operating = false;
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -494,10 +500,11 @@ export default defineComponent({
|
|||
});
|
||||
},
|
||||
|
||||
react(viaKeyboard = false) {
|
||||
async react(viaKeyboard = false) {
|
||||
pleaseLogin();
|
||||
this.operating = true;
|
||||
this.blur();
|
||||
os.popup(import('@/components/emoji-picker.vue'), {
|
||||
const { dispose } = await os.popup(import('@/components/emoji-picker.vue'), {
|
||||
src: this.$refs.reactButton,
|
||||
asReactionPicker: true
|
||||
}, {
|
||||
|
@ -508,9 +515,13 @@ export default defineComponent({
|
|||
reaction: reaction
|
||||
});
|
||||
}
|
||||
this.focus();
|
||||
},
|
||||
}, 'closed');
|
||||
closed: () => {
|
||||
this.operating = false;
|
||||
this.focus();
|
||||
dispose();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
reactDirectly(reaction) {
|
||||
|
@ -734,9 +745,13 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
menu(viaKeyboard = false) {
|
||||
this.operating = true;
|
||||
os.modalMenu(this.getMenu(), this.$refs.menuButton, {
|
||||
viaKeyboard
|
||||
}).then(this.focus);
|
||||
}).then(() => {
|
||||
this.operating = false;
|
||||
this.focus();
|
||||
});
|
||||
},
|
||||
|
||||
showRenoteMenu(viaKeyboard = false) {
|
||||
|
@ -857,10 +872,8 @@ export default defineComponent({
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.note {
|
||||
.vfzoeqcg {
|
||||
position: relative;
|
||||
transition: box-shadow 0.1s ease;
|
||||
overflow: hidden;
|
||||
contain: content;
|
||||
|
||||
// これらの指定はパフォーマンス向上には有効だが、ノートの高さは一定でないため、
|
||||
|
@ -879,18 +892,24 @@ export default defineComponent({
|
|||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&:hover > .article > .main > .footer {
|
||||
display: block;
|
||||
&:hover, &.operating {
|
||||
> .article > .main > .footer {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.renote {
|
||||
background: rgba(128, 255, 0, 0.05);
|
||||
}
|
||||
|
||||
&.highlighted {
|
||||
background: rgba(255, 128, 0, 0.05);
|
||||
}
|
||||
|
||||
> .info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px 32px 8px 32px;
|
||||
padding: 12px 16px 4px 16px;
|
||||
line-height: 24px;
|
||||
font-size: 90%;
|
||||
white-space: pre;
|
||||
|
@ -901,8 +920,9 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
> .hide {
|
||||
margin-left: auto;
|
||||
margin-left: 16px;
|
||||
color: inherit;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -918,7 +938,7 @@ export default defineComponent({
|
|||
> .renote {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px 8px 16px;
|
||||
padding: 12px 16px 4px 16px;
|
||||
line-height: 28px;
|
||||
white-space: pre;
|
||||
color: var(--renote);
|
||||
|
@ -983,8 +1003,8 @@ export default defineComponent({
|
|||
> .avatar {
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
//position: sticky;
|
||||
//top: 72px;
|
||||
position: sticky;
|
||||
top: 12px;
|
||||
margin: 0 14px 0 0;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
|
@ -1054,8 +1074,13 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
|
||||
> .files {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
> .url-preview {
|
||||
margin-top: 8px;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
> .poll {
|
||||
|
@ -1122,5 +1147,9 @@ export default defineComponent({
|
|||
.muted {
|
||||
padding: 8px 16px;
|
||||
opacity: 0.7;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -108,7 +108,7 @@ export default defineComponent({
|
|||
autofocus: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
default: false
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
<template>
|
||||
<div class="qvzfzxam _narrow_" v-if="component">
|
||||
<div class="container">
|
||||
<header class="header" @contextmenu.prevent.stop="onContextmenu">
|
||||
<button class="_button" @click="back()" v-if="history.length > 0"><Fa :icon="faChevronLeft"/></button>
|
||||
<XHeader class="title" :info="pageInfo" :with-back="false" :center="false"/>
|
||||
<button class="_button" @click="close()"><Fa :icon="faTimes"/></button>
|
||||
</header>
|
||||
<component :is="component" v-bind="props" :ref="changePage"/>
|
||||
</div>
|
||||
<div class="mrajymqm _narrow_" v-if="component">
|
||||
<header class="header" @contextmenu.prevent.stop="onContextmenu">
|
||||
<button class="_button" @click="back()" v-if="history.length > 0"><Fa :icon="faChevronLeft"/></button>
|
||||
<XHeader class="title" :info="pageInfo" :with-back="false" :center="false"/>
|
||||
<button class="_button" @click="close()"><Fa :icon="faTimes"/></button>
|
||||
</header>
|
||||
<component :is="component" v-bind="props" :ref="changePage"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -64,6 +62,7 @@ export default defineComponent({
|
|||
const { component, props } = resolve(path);
|
||||
this.component = component;
|
||||
this.props = props;
|
||||
this.$emit('open');
|
||||
},
|
||||
|
||||
back() {
|
||||
|
@ -74,6 +73,7 @@ export default defineComponent({
|
|||
this.path = null;
|
||||
this.component = null;
|
||||
this.props = {};
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
onContextmenu(e) {
|
||||
|
@ -114,50 +114,44 @@ export default defineComponent({
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.qvzfzxam {
|
||||
.mrajymqm {
|
||||
$header-height: 54px; // TODO: どこかに集約したい
|
||||
|
||||
--section-padding: 16px;
|
||||
--margin: var(--marginHalf);
|
||||
|
||||
width: 390px;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
box-sizing: border-box;
|
||||
|
||||
> .container {
|
||||
position: fixed;
|
||||
width: 390px;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
> .header {
|
||||
display: flex;
|
||||
position: sticky;
|
||||
z-index: 1000;
|
||||
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;
|
||||
|
||||
> .header {
|
||||
display: flex;
|
||||
position: sticky;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
> ._button {
|
||||
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;
|
||||
width: $header-height;
|
||||
|
||||
> ._button {
|
||||
height: $header-height;
|
||||
width: $header-height;
|
||||
|
||||
&:hover {
|
||||
color: var(--fgHighlighted);
|
||||
}
|
||||
&:hover {
|
||||
color: var(--fgHighlighted);
|
||||
}
|
||||
}
|
||||
|
||||
> .title {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
> .title {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
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>
|
||||
<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>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -7,7 +10,7 @@ import { defineComponent } from 'vue';
|
|||
import XNotes from './notes.vue';
|
||||
import * as os from '@/os';
|
||||
import * as sound from '@/scripts/sound';
|
||||
import { scrollToBottom } from '@/scripts/scroll';
|
||||
import { scrollToBottom, getScrollPosition, getScrollContainer } from '@/scripts/scroll';
|
||||
import follow from '@/directives/follow-append';
|
||||
|
||||
export default defineComponent({
|
||||
|
@ -62,6 +65,10 @@ export default defineComponent({
|
|||
includeLocalRenotes: this.$store.state.showLocalRenotes
|
||||
},
|
||||
query: {},
|
||||
queue: 0,
|
||||
width: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -185,6 +192,43 @@ export default defineComponent({
|
|||
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>
|
||||
|
||||
<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>
|
||||
|
||||
<div class="wtdtxvec">
|
||||
<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 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)"/>
|
||||
<XWidgets :edit="edit" :widgets="column.widgets" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="edit = false"/>
|
||||
</div>
|
||||
</XColumn>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, defineAsyncComponent } from 'vue';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { faWindowMaximize, faTimes, faCog, faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||
import MkSelect from '@/components/ui/select.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import XWidgets from '@/components/widgets.vue';
|
||||
import XColumn from './column.vue';
|
||||
import { widgets } from '../../widgets';
|
||||
import { addColumnWidget, removeColumnWidget, setColumnWidgets, updateColumnWidget } from './deck-store';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XColumn,
|
||||
XDraggable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)),
|
||||
MkSelect,
|
||||
MkButton,
|
||||
XWidgets,
|
||||
},
|
||||
|
||||
props: {
|
||||
|
@ -62,49 +35,27 @@ export default defineComponent({
|
|||
data() {
|
||||
return {
|
||||
edit: false,
|
||||
widgetAdderSelected: null,
|
||||
widgets,
|
||||
settings: {},
|
||||
faWindowMaximize, faTimes, faPlus
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
_widgets: {
|
||||
get() {
|
||||
return this.column.widgets;
|
||||
},
|
||||
set(value) {
|
||||
setColumnWidgets(this.column.id, value);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
widgetFunc(id) {
|
||||
this.settings[id]();
|
||||
},
|
||||
|
||||
addWidget() {
|
||||
if (this.widgetAdderSelected == null) return;
|
||||
|
||||
addColumnWidget(this.column.id, {
|
||||
name: this.widgetAdderSelected,
|
||||
id: uuid(),
|
||||
data: {}
|
||||
});
|
||||
|
||||
this.widgetAdderSelected = null;
|
||||
addWidget(widget) {
|
||||
addColumnWidget(this.column.id, widget);
|
||||
},
|
||||
|
||||
removeWidget(widget) {
|
||||
removeColumnWidget(this.column.id, widget);
|
||||
},
|
||||
|
||||
saveWidget(id, data) {
|
||||
updateWidget({ id, data }) {
|
||||
updateColumnWidget(this.column.id, id, data);
|
||||
},
|
||||
|
||||
updateWidgets(widgets) {
|
||||
setColumnWidgets(this.column.id, widgets);
|
||||
},
|
||||
|
||||
func() {
|
||||
this.edit = !this.edit;
|
||||
}
|
||||
|
@ -114,46 +65,12 @@ export default defineComponent({
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.wtdtxvec {
|
||||
._panel {
|
||||
--margin: 8px;
|
||||
|
||||
padding: 0 var(--margin);
|
||||
|
||||
::v-deep(._panel) {
|
||||
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>
|
||||
|
|
|
@ -1,46 +1,21 @@
|
|||
<template>
|
||||
<div class="efzpzdvf">
|
||||
<template v-if="editMode">
|
||||
<MkButton primary @click="addWidget" class="add"><Fa :icon="faPlus"/></MkButton>
|
||||
<XDraggable
|
||||
v-model="widgets"
|
||||
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>
|
||||
<XWidgets :edit="editMode" :widgets="$store.reactiveState.widgets.value" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="editMode = false"/>
|
||||
|
||||
<button v-if="editMode" @click="editMode = false" class="_textButton" style="font-size: 0.9em;"><Fa :icon="faCheck"/> {{ $ts.editWidgetsExit }}</button>
|
||||
<button v-else @click="editMode = true" class="_textButton" style="font-size: 0.9em;"><Fa :icon="faPencilAlt"/> {{ $ts.editWidgets }}</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, defineAsyncComponent } from 'vue';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
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 MkButton from '@/components/ui/button.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton,
|
||||
XDraggable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)),
|
||||
XWidgets
|
||||
},
|
||||
|
||||
emits: ['mounted'],
|
||||
|
@ -48,62 +23,35 @@ export default defineComponent({
|
|||
data() {
|
||||
return {
|
||||
editMode: false,
|
||||
settings: {},
|
||||
faPencilAlt, faPlus, faBars, faTimes, faCheck,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
widgets: {
|
||||
get() {
|
||||
return this.$store.reactiveState.widgets.value;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.set('widgets', value);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('mounted', this.$el);
|
||||
},
|
||||
|
||||
methods: {
|
||||
widgetFunc(id) {
|
||||
this.settings[id]();
|
||||
},
|
||||
|
||||
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(),
|
||||
addWidget(widget) {
|
||||
this.$store.set('widgets', [{
|
||||
...widget,
|
||||
place: null,
|
||||
data: {}
|
||||
}]);
|
||||
}, ...this.$store.state.widgets]);
|
||||
},
|
||||
|
||||
removeWidget(widget) {
|
||||
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 ? {
|
||||
...w,
|
||||
data: data
|
||||
} : w));
|
||||
},
|
||||
|
||||
updateWidgets(widgets) {
|
||||
this.$store.set('widgets', widgets);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -129,35 +77,5 @@ export default defineComponent({
|
|||
> .add {
|
||||
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>
|
||||
|
|
Loading…
Reference in a new issue