mirror of
https://git.joinsharkey.org/Sharkey/Sharkey.git
synced 2024-12-24 02:33:09 +02:00
Deck (#6504)
* wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip
This commit is contained in:
parent
5b28d7bf90
commit
cf3fc97202
56 changed files with 2695 additions and 907 deletions
|
@ -519,6 +519,8 @@ fixedWidgetsPosition: "ウィジェットの位置を固定する"
|
|||
enablePlayer: "プレイヤーを開く"
|
||||
disablePlayer: "プレイヤーを閉じる"
|
||||
expandTweet: "ツイートを展開する"
|
||||
deck: "デッキ"
|
||||
undeck: "デッキ解除"
|
||||
|
||||
_theme:
|
||||
explore: "テーマを探す"
|
||||
|
@ -651,6 +653,7 @@ _widgets:
|
|||
rss: "RSSリーダー"
|
||||
activity: "アクティビティ"
|
||||
photos: "フォト"
|
||||
digitalClock: "デジタル時計"
|
||||
|
||||
_cw:
|
||||
hide: "隠す"
|
||||
|
@ -1129,3 +1132,15 @@ _notification:
|
|||
yourFollowRequestAccepted: "フォローリクエストが承認されました"
|
||||
youWereInvitedToGroup: "グループに招待されました"
|
||||
|
||||
_deck:
|
||||
alwaysShowMainColumn: "常にメインカラムを表示"
|
||||
columnAlign: "カラムの寄せ"
|
||||
|
||||
_columns:
|
||||
widgets: "ウィジェット"
|
||||
notifications: "通知"
|
||||
tl: "タイムライン"
|
||||
antenna: "アンテナ"
|
||||
list: "リスト"
|
||||
mentions: "あなた宛て"
|
||||
direct: "ダイレクト"
|
||||
|
|
|
@ -29,47 +29,7 @@
|
|||
</div>
|
||||
</header>
|
||||
|
||||
<transition name="nav-back">
|
||||
<div class="nav-back"
|
||||
v-if="showNav"
|
||||
@click="showNav = false"
|
||||
@touchstart="showNav = false"
|
||||
></div>
|
||||
</transition>
|
||||
|
||||
<transition name="nav">
|
||||
<nav class="nav" ref="nav" v-show="showNav">
|
||||
<div>
|
||||
<button class="item _button account" @click="openAccountMenu" v-if="$store.getters.isSignedIn">
|
||||
<mk-avatar :user="$store.state.i" class="avatar"/><mk-acct class="text" :user="$store.state.i"/>
|
||||
</button>
|
||||
<button class="item _button index active" @click="top()" v-if="$route.name === 'index'">
|
||||
<fa :icon="faHome" fixed-width/><span class="text">{{ $store.getters.isSignedIn ? $t('timeline') : $t('home') }}</span>
|
||||
</button>
|
||||
<router-link class="item index" active-class="active" to="/" exact v-else>
|
||||
<fa :icon="faHome" fixed-width/><span class="text">{{ $store.getters.isSignedIn ? $t('timeline') : $t('home') }}</span>
|
||||
</router-link>
|
||||
<template v-for="item in menu">
|
||||
<div v-if="item === '-'" class="divider"></div>
|
||||
<component v-else-if="menuDef[item] && (menuDef[item].show !== false)" :is="menuDef[item].to ? 'router-link' : 'button'" class="item _button" :class="item" active-class="active" @click="() => { if (menuDef[item].action) menuDef[item].action() }" :to="menuDef[item].to">
|
||||
<fa :icon="menuDef[item].icon" fixed-width/><span class="text">{{ $t(menuDef[item].title) }}</span>
|
||||
<i v-if="menuDef[item].indicated"><fa :icon="faCircle"/></i>
|
||||
</component>
|
||||
</template>
|
||||
<div class="divider"></div>
|
||||
<button class="item _button" :class="{ active: $route.path === '/instance' || $route.path.startsWith('/instance/') }" v-if="$store.getters.isSignedIn && ($store.state.i.isAdmin || $store.state.i.isModerator)" @click="oepnInstanceMenu">
|
||||
<fa :icon="faServer" fixed-width/><span class="text">{{ $t('instance') }}</span>
|
||||
</button>
|
||||
<button class="item _button" @click="more">
|
||||
<fa :icon="faEllipsisH" fixed-width/><span class="text">{{ $t('more') }}</span>
|
||||
<i v-if="otherNavItemIndicated"><fa :icon="faCircle"/></i>
|
||||
</button>
|
||||
<router-link class="item" active-class="active" to="/preferences">
|
||||
<fa :icon="faCog" fixed-width/><span class="text">{{ $t('settings') }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</nav>
|
||||
</transition>
|
||||
<x-sidebar ref="nav"/>
|
||||
|
||||
<div class="contents" ref="contents" :class="{ wallpaper }">
|
||||
<main ref="main">
|
||||
|
@ -103,20 +63,20 @@
|
|||
<span class="handle"><fa :icon="faBars"/></span>{{ $t('_widgets.' + widget.name) }}<button class="remove _button" @click="removeWidget(widget)"><fa :icon="faTimes"/></button>
|
||||
</header>
|
||||
<div @click="widgetFunc(widget.id)">
|
||||
<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true"/>
|
||||
<component class="_close_ _forceContainerFull_" :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true"/>
|
||||
</div>
|
||||
</div>
|
||||
</x-draggable>
|
||||
</div>
|
||||
<div class="container" v-else>
|
||||
<component class="_widget" v-for="widget in widgets[place]" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget"/>
|
||||
<component v-for="widget in widgets[place]" class="_close_ _forceContainerFull_" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
<button class="button nav _button" @click="showNav = true" ref="navButton"><fa :icon="faBars"/><i v-if="navIndicated"><fa :icon="faCircle"/></i></button>
|
||||
<button class="button nav _button" @click="showNav" ref="navButton"><fa :icon="faBars"/><i v-if="navIndicated"><fa :icon="faCircle"/></i></button>
|
||||
<button v-if="$route.name === 'index'" class="button home _button" @click="top()"><fa :icon="faHome"/></button>
|
||||
<button v-else class="button home _button" @click="$router.push('/')"><fa :icon="faHome"/></button>
|
||||
<button v-if="$store.getters.isSignedIn" class="button notifications _button" @click="$router.push('/my/notifications')"><fa :icon="faBell"/><i v-if="$store.state.i.hasUnreadNotification"><fa :icon="faCircle"/></i></button>
|
||||
|
@ -135,14 +95,17 @@ import { faGripVertical, faChevronLeft, faHashtag, faBroadcastTower, faFireAlt,
|
|||
import { faBell, faEnvelope, faLaugh, faComments } from '@fortawesome/free-regular-svg-icons';
|
||||
import { ResizeObserver } from '@juggle/resize-observer';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { host, instanceName } from './config';
|
||||
import { host } from './config';
|
||||
import { search } from './scripts/search';
|
||||
import { StickySidebar } from './scripts/sticky-sidebar';
|
||||
import { widgets } from './widgets';
|
||||
import XSidebar from './components/sidebar.vue';
|
||||
|
||||
const DESKTOP_THRESHOLD = 1100;
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XSidebar,
|
||||
XClock: () => import('./components/header-clock.vue').then(m => m.default),
|
||||
MkButton: () => import('./components/ui/button.vue').then(m => m.default),
|
||||
XDraggable: () => import('vuedraggable'),
|
||||
|
@ -152,19 +115,14 @@ export default Vue.extend({
|
|||
return {
|
||||
host: host,
|
||||
pageKey: 0,
|
||||
showNav: false,
|
||||
searching: false,
|
||||
accounts: [],
|
||||
lists: [],
|
||||
connection: null,
|
||||
searchQuery: '',
|
||||
searchWait: false,
|
||||
widgetsEditMode: false,
|
||||
menuDef: this.$store.getters.nav({
|
||||
search: this.search
|
||||
}),
|
||||
isDesktop: window.innerWidth >= DESKTOP_THRESHOLD,
|
||||
canBack: false,
|
||||
menuDef: this.$store.getters.nav({}),
|
||||
wallpaper: localStorage.getItem('wallpaper') != null,
|
||||
faGripVertical, faChevronLeft, faComments, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faBell, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faEnvelope, faListUl, faPlus, faUserClock, faLaugh, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer, faProjectDiagram
|
||||
};
|
||||
|
@ -210,30 +168,19 @@ export default Vue.extend({
|
|||
return this.$store.state.deviceUser.menu;
|
||||
},
|
||||
|
||||
otherNavItemIndicated(): boolean {
|
||||
if (!this.$store.getters.isSignedIn) return false;
|
||||
for (const def in this.menuDef) {
|
||||
if (this.menu.includes(def)) continue;
|
||||
if (this.menuDef[def].indicated) return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
navIndicated(): boolean {
|
||||
if (!this.$store.getters.isSignedIn) return false;
|
||||
for (const def in this.menuDef) {
|
||||
if (def === 'timeline') continue;
|
||||
if (def === 'notifications') continue;
|
||||
if (def === 'notifications') continue; // 通知は下にボタンとして表示されてるから
|
||||
if (this.menuDef[def].indicated) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
watch:{
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
this.pageKey++;
|
||||
this.showNav = false;
|
||||
this.canBack = (window.history.length > 0 && !['index'].includes(to.name));
|
||||
},
|
||||
|
||||
|
@ -245,6 +192,8 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
created() {
|
||||
document.documentElement.style.overflowY = 'scroll';
|
||||
|
||||
if (this.$store.getters.isSignedIn) {
|
||||
this.connection = this.$root.stream.useSharedConnection('main');
|
||||
this.connection.on('notification', this.onNotification);
|
||||
|
@ -266,7 +215,7 @@ export default Vue.extend({
|
|||
|
||||
mounted() {
|
||||
const adjustTitlePosition = () => {
|
||||
const left = this.$refs.main.getBoundingClientRect().left - this.$refs.nav.offsetWidth;
|
||||
const left = this.$refs.main.getBoundingClientRect().left - this.$refs.nav.$el.offsetWidth;
|
||||
if (left >= 0) {
|
||||
this.$refs.title.style.left = left + 'px';
|
||||
}
|
||||
|
@ -293,6 +242,10 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
methods: {
|
||||
showNav() {
|
||||
this.$refs.nav.show();
|
||||
},
|
||||
|
||||
attachSticky() {
|
||||
if (!this.isDesktop) return;
|
||||
if (this.$store.state.device.fixedWidgetsPosition) return;
|
||||
|
@ -351,180 +304,6 @@ export default Vue.extend({
|
|||
}
|
||||
},
|
||||
|
||||
async openAccountMenu(ev) {
|
||||
const accounts = (await this.$root.api('users/show', { userIds: this.$store.state.device.accounts.map(x => x.id) })).filter(x => x.id !== this.$store.state.i.id);
|
||||
|
||||
const accountItems = accounts.map(account => ({
|
||||
type: 'user',
|
||||
user: account,
|
||||
action: () => { this.switchAccount(account); }
|
||||
}));
|
||||
|
||||
this.$root.menu({
|
||||
items: [...[{
|
||||
type: 'link',
|
||||
text: this.$t('profile'),
|
||||
to: `/@${ this.$store.state.i.username }`,
|
||||
avatar: this.$store.state.i,
|
||||
}, {
|
||||
type: 'link',
|
||||
text: this.$t('accountSettings'),
|
||||
to: '/my/settings',
|
||||
icon: faCog,
|
||||
}, null, ...accountItems, {
|
||||
icon: faPlus,
|
||||
text: this.$t('addAcount'),
|
||||
action: () => {
|
||||
this.$root.menu({
|
||||
items: [{
|
||||
text: this.$t('existingAcount'),
|
||||
action: () => { this.addAcount(); },
|
||||
}, {
|
||||
text: this.$t('createAccount'),
|
||||
action: () => { this.createAccount(); },
|
||||
}],
|
||||
align: 'left',
|
||||
fixed: true,
|
||||
width: 240,
|
||||
source: ev.currentTarget || ev.target,
|
||||
});
|
||||
},
|
||||
}]],
|
||||
align: 'left',
|
||||
fixed: true,
|
||||
width: 240,
|
||||
source: ev.currentTarget || ev.target,
|
||||
});
|
||||
},
|
||||
|
||||
oepnInstanceMenu(ev) {
|
||||
this.$root.menu({
|
||||
items: [{
|
||||
type: 'link',
|
||||
text: this.$t('dashboard'),
|
||||
to: '/instance',
|
||||
icon: faTachometerAlt,
|
||||
}, null, {
|
||||
type: 'link',
|
||||
text: this.$t('settings'),
|
||||
to: '/instance/settings',
|
||||
icon: faCog,
|
||||
}, {
|
||||
type: 'link',
|
||||
text: this.$t('customEmojis'),
|
||||
to: '/instance/emojis',
|
||||
icon: faLaugh,
|
||||
}, {
|
||||
type: 'link',
|
||||
text: this.$t('users'),
|
||||
to: '/instance/users',
|
||||
icon: faUsers,
|
||||
}, {
|
||||
type: 'link',
|
||||
text: this.$t('files'),
|
||||
to: '/instance/files',
|
||||
icon: faCloud,
|
||||
}, {
|
||||
type: 'link',
|
||||
text: this.$t('jobQueue'),
|
||||
to: '/instance/queue',
|
||||
icon: faExchangeAlt,
|
||||
}, {
|
||||
type: 'link',
|
||||
text: this.$t('federation'),
|
||||
to: '/instance/federation',
|
||||
icon: faGlobe,
|
||||
}, {
|
||||
type: 'link',
|
||||
text: this.$t('relays'),
|
||||
to: '/instance/relays',
|
||||
icon: faProjectDiagram,
|
||||
}, {
|
||||
type: 'link',
|
||||
text: this.$t('announcements'),
|
||||
to: '/instance/announcements',
|
||||
icon: faBroadcastTower,
|
||||
}],
|
||||
align: 'left',
|
||||
fixed: true,
|
||||
width: 200,
|
||||
source: ev.currentTarget || ev.target,
|
||||
});
|
||||
},
|
||||
|
||||
more(ev) {
|
||||
const items = Object.keys(this.menuDef).filter(k => !this.menu.includes(k)).map(k => this.menuDef[k]).filter(def => def.show == null ? true : def.show).map(def => ({
|
||||
type: def.to ? 'link' : 'button',
|
||||
text: this.$t(def.title),
|
||||
icon: def.icon,
|
||||
to: def.to,
|
||||
action: def.action,
|
||||
indicate: def.indicated,
|
||||
}));
|
||||
this.$root.menu({
|
||||
items: [...items, null, {
|
||||
type: 'link',
|
||||
text: this.$t('help'),
|
||||
to: '/docs',
|
||||
icon: faQuestionCircle,
|
||||
}, {
|
||||
type: 'link',
|
||||
text: this.$t('aboutX', { x: instanceName || host }),
|
||||
to: '/about',
|
||||
icon: faInfoCircle,
|
||||
}, {
|
||||
type: 'link',
|
||||
text: this.$t('aboutMisskey'),
|
||||
to: '/about-misskey',
|
||||
icon: faInfoCircle,
|
||||
}],
|
||||
align: 'left',
|
||||
fixed: true,
|
||||
width: 200,
|
||||
source: ev.currentTarget || ev.target,
|
||||
});
|
||||
},
|
||||
|
||||
async addAcount() {
|
||||
this.$root.new(await import('./components/signin-dialog.vue').then(m => m.default)).$once('login', res => {
|
||||
this.$store.dispatch('addAcount', res);
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
async createAccount() {
|
||||
this.$root.new(await import('./components/signup-dialog.vue').then(m => m.default)).$once('signup', res => {
|
||||
this.$store.dispatch('addAcount', res);
|
||||
this.switchAccountWithToken(res.i);
|
||||
});
|
||||
},
|
||||
|
||||
async switchAccount(account: any) {
|
||||
const token = this.$store.state.device.accounts.find((x: any) => x.id === account.id).token;
|
||||
this.switchAccountWithToken(token);
|
||||
},
|
||||
|
||||
switchAccountWithToken(token: string) {
|
||||
this.$root.dialog({
|
||||
type: 'waiting',
|
||||
iconOnly: true
|
||||
});
|
||||
|
||||
this.$root.api('i', {}, token).then((i: any) => {
|
||||
this.$store.dispatch('switchAccount', {
|
||||
...i,
|
||||
token: token
|
||||
}).then(() => {
|
||||
this.$nextTick(() => {
|
||||
location.reload();
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
async onNotification(notification) {
|
||||
if (document.visibilityState === 'visible') {
|
||||
this.$root.stream.send('readNotification', {
|
||||
|
@ -540,8 +319,7 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
widgetFunc(id) {
|
||||
const w = this.$refs[id][0];
|
||||
if (w.func) w.func();
|
||||
this.$refs[id][0].setting();
|
||||
},
|
||||
|
||||
onWidgetSort() {
|
||||
|
@ -549,18 +327,6 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
async addWidget(place) {
|
||||
const widgets = [
|
||||
'memo',
|
||||
'notifications',
|
||||
'timeline',
|
||||
'calendar',
|
||||
'rss',
|
||||
'trends',
|
||||
'clock',
|
||||
'activity',
|
||||
'photos',
|
||||
];
|
||||
|
||||
const { canceled, result: widget } = await this.$root.dialog({
|
||||
type: null,
|
||||
title: this.$t('chooseWidget'),
|
||||
|
@ -594,36 +360,14 @@ export default Vue.extend({
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.nav-enter-active,
|
||||
.nav-leave-active {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
.nav-enter,
|
||||
.nav-leave-active {
|
||||
opacity: 0;
|
||||
transform: translateX(-240px);
|
||||
}
|
||||
|
||||
.nav-back-enter-active,
|
||||
.nav-back-leave-active {
|
||||
opacity: 1;
|
||||
transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
.nav-back-enter,
|
||||
.nav-back-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.mk-app {
|
||||
$header-height: 60px;
|
||||
$nav-width: 250px;
|
||||
$nav-icon-only-width: 80px;
|
||||
$nav-width: 250px; // TODO: どこかに集約したい
|
||||
$nav-icon-only-width: 80px; // TODO: どこかに集約したい
|
||||
$main-width: 670px;
|
||||
$ui-font-size: 1em;
|
||||
$nav-icon-only-threshold: 1279px;
|
||||
$nav-hide-threshold: 650px;
|
||||
$ui-font-size: 1em; // TODO: どこかに集約したい
|
||||
$nav-icon-only-threshold: 1279px; // TODO: どこかに集約したい
|
||||
$nav-hide-threshold: 650px; // TODO: どこかに集約したい
|
||||
$header-sub-hide-threshold: 1090px;
|
||||
$left-widgets-hide-threshold: 1600px;
|
||||
$right-widgets-hide-threshold: 1090px;
|
||||
|
@ -780,176 +524,6 @@ export default Vue.extend({
|
|||
}
|
||||
}
|
||||
|
||||
> .nav-back {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1001;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--modalBg);
|
||||
}
|
||||
|
||||
> .nav {
|
||||
$avatar-size: 32px;
|
||||
$avatar-margin: ($header-height - $avatar-size) / 2;
|
||||
|
||||
flex: 0 0 $nav-width;
|
||||
width: $nav-width;
|
||||
box-sizing: border-box;
|
||||
|
||||
@media (max-width: $nav-icon-only-threshold) {
|
||||
flex: 0 0 $nav-icon-only-width;
|
||||
width: $nav-icon-only-width;
|
||||
}
|
||||
|
||||
@media (max-width: $nav-hide-threshold) {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
@media (min-width: $nav-hide-threshold + 1px) {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
> div {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1001;
|
||||
width: $nav-width;
|
||||
height: 100vh;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
background: var(--navBg);
|
||||
border-right: solid 1px var(--divider);
|
||||
|
||||
> .divider {
|
||||
margin: 16px 0;
|
||||
border-top: solid 1px var(--divider);
|
||||
}
|
||||
|
||||
@media (max-width: $nav-icon-only-threshold) and (min-width: $nav-hide-threshold + 1px) {
|
||||
width: $nav-icon-only-width;
|
||||
|
||||
> .divider {
|
||||
margin: 8px auto;
|
||||
width: calc(100% - 32px);
|
||||
}
|
||||
|
||||
> .item {
|
||||
&:first-child {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .item {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding-left: 32px;
|
||||
font-size: $ui-font-size;
|
||||
line-height: 3.2rem;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
box-sizing: border-box;
|
||||
color: var(--navFg);
|
||||
|
||||
> [data-icon] {
|
||||
width: ($header-height - ($avatar-margin * 2));
|
||||
}
|
||||
|
||||
> [data-icon],
|
||||
> .avatar {
|
||||
margin-right: $avatar-margin;
|
||||
}
|
||||
|
||||
> .avatar {
|
||||
width: $avatar-size;
|
||||
height: $avatar-size;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
> i {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 20px;
|
||||
color: var(--navIndicator);
|
||||
font-size: 8px;
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
color: var(--navHoverFg);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--navActive);
|
||||
}
|
||||
|
||||
&:first-child, &:last-child {
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
background: var(--X14);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
top: 0;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: solid 1px var(--divider);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
bottom: 0;
|
||||
margin-top: 16px;
|
||||
border-top: solid 1px var(--divider);
|
||||
}
|
||||
|
||||
@media (max-width: $nav-icon-only-threshold) and (min-width: $nav-hide-threshold + 1px) {
|
||||
padding-left: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: $ui-font-size * 1.2;
|
||||
line-height: 3.7rem;
|
||||
|
||||
> [data-icon],
|
||||
> .avatar {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
> i {
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
> .text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $nav-hide-threshold) {
|
||||
> .index,
|
||||
> .notifications {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .contents {
|
||||
display: flex;
|
||||
margin: 0 auto;
|
||||
|
|
80
src/client/components/deck/antenna-column.vue
Normal file
80
src/client/components/deck/antenna-column.vue
Normal file
|
@ -0,0 +1,80 @@
|
|||
<template>
|
||||
<x-column :menu="menu" :column="column" :is-stacked="isStacked">
|
||||
<template #header>
|
||||
<fa :icon="faSatellite"/><span style="margin-left: 8px;">{{ column.name }}</span>
|
||||
</template>
|
||||
|
||||
<x-timeline ref="timeline" src="antenna" :antenna="column.antennaId" @after="() => $emit('loaded')"/>
|
||||
</x-column>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faSatellite, faCog } from '@fortawesome/free-solid-svg-icons';
|
||||
import XColumn from './column.vue';
|
||||
import XTimeline from '../timeline.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XColumn,
|
||||
XTimeline,
|
||||
},
|
||||
|
||||
props: {
|
||||
column: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
isStacked: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
menu: null,
|
||||
faSatellite
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
mediaOnly() {
|
||||
(this.$refs.timeline as any).reload();
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.menu = [{
|
||||
icon: faCog,
|
||||
text: this.$t('antenna'),
|
||||
action: async () => {
|
||||
const antennas = await this.$root.api('antennas/list');
|
||||
this.$root.dialog({
|
||||
title: this.$t('antenna'),
|
||||
type: null,
|
||||
select: {
|
||||
items: antennas.map(x => ({
|
||||
value: x, text: x.name
|
||||
}))
|
||||
},
|
||||
showCancelButton: true
|
||||
}).then(({ canceled, result: antenna }) => {
|
||||
if (canceled) return;
|
||||
this.column.antennaId = antenna.id;
|
||||
this.$store.commit('deviceUser/updateDeckColumn', this.column);
|
||||
});
|
||||
}
|
||||
}];
|
||||
},
|
||||
|
||||
methods: {
|
||||
focus() {
|
||||
(this.$refs.timeline as any).focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
50
src/client/components/deck/column-core.vue
Normal file
50
src/client/components/deck/column-core.vue
Normal file
|
@ -0,0 +1,50 @@
|
|||
<template>
|
||||
<!-- TODO: リファクタの余地がありそう -->
|
||||
<x-widgets-column v-if="column.type === 'widgets'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
|
||||
<x-notifications-column v-else-if="column.type === 'notifications'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
|
||||
<x-tl-column v-else-if="column.type === 'tl'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
|
||||
<x-list-column v-else-if="column.type === 'list'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
|
||||
<x-antenna-column v-else-if="column.type === 'antenna'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
|
||||
<!-- TODO: <x-tl-column v-else-if="column.type === 'hashtag'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> -->
|
||||
<x-mentions-column v-else-if="column.type === 'mentions'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
|
||||
<x-direct-column v-else-if="column.type === 'direct'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import XTlColumn from './tl-column.vue';
|
||||
import XAntennaColumn from './antenna-column.vue';
|
||||
import XListColumn from './list-column.vue';
|
||||
import XNotificationsColumn from './notifications-column.vue';
|
||||
import XWidgetsColumn from './widgets-column.vue';
|
||||
import XMentionsColumn from './mentions-column.vue';
|
||||
import XDirectColumn from './direct-column.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XTlColumn,
|
||||
XAntennaColumn,
|
||||
XListColumn,
|
||||
XNotificationsColumn,
|
||||
XWidgetsColumn,
|
||||
XMentionsColumn,
|
||||
XDirectColumn
|
||||
},
|
||||
props: {
|
||||
column: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
isStacked: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
focus() {
|
||||
this.$children[0].focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
426
src/client/components/deck/column.vue
Normal file
426
src/client/components/deck/column.vue
Normal file
|
@ -0,0 +1,426 @@
|
|||
<template>
|
||||
<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
|
||||
<section class="dnpfarvg _panel _narrow_" :class="{ naked, paged: isMainColumn, _close_: !isMainColumn, active, isStacked, draghover, dragging, dropready }"
|
||||
@dragover.prevent.stop="onDragover"
|
||||
@dragleave="onDragleave"
|
||||
@drop.prevent.stop="onDrop"
|
||||
v-hotkey="keymap"
|
||||
:style="{ width: `${width}px` }"
|
||||
>
|
||||
<header :class="{ indicated }"
|
||||
draggable="true"
|
||||
@click="goTop"
|
||||
@dragstart="onDragstart"
|
||||
@dragend="onDragend"
|
||||
@contextmenu.prevent.stop="onContextmenu"
|
||||
>
|
||||
<button class="toggleActive _button" @click="toggleActive" v-if="isStacked">
|
||||
<template v-if="active"><fa :icon="faAngleUp"/></template>
|
||||
<template v-else><fa :icon="faAngleDown"/></template>
|
||||
</button>
|
||||
<div class="action">
|
||||
<slot name="action"></slot>
|
||||
</div>
|
||||
<span class="header"><slot name="header"></slot></span>
|
||||
<button v-if="!isMainColumn" class="menu _button" ref="menu" @click.stop="showMenu"><fa :icon="faCaretDown"/></button>
|
||||
<button v-else-if="$route.name !== 'index'" class="close _button" @click.stop="close"><fa :icon="faTimes"/></button>
|
||||
</header>
|
||||
<div ref="body" v-show="active">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faArrowUp, faArrowDown, faAngleUp, faAngleDown, faCaretDown, faTimes, faArrowRight, faArrowLeft, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faWindowMaximize, faTrashAlt, faWindowRestore } from '@fortawesome/free-regular-svg-icons';
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
column: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
isStacked: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
menu: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
naked: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
indicated: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
active: true,
|
||||
dragging: false,
|
||||
draghover: false,
|
||||
dropready: false,
|
||||
faArrowUp, faArrowDown, faAngleUp, faAngleDown, faCaretDown, faTimes,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
isMainColumn(): boolean {
|
||||
return this.column == null;
|
||||
},
|
||||
|
||||
width(): number {
|
||||
return this.isMainColumn ? 350 : this.column.width;
|
||||
},
|
||||
|
||||
keymap(): any {
|
||||
return {
|
||||
'shift+up': () => this.$parent.$emit('parentFocus', 'up'),
|
||||
'shift+down': () => this.$parent.$emit('parentFocus', 'down'),
|
||||
'shift+left': () => this.$parent.$emit('parentFocus', 'left'),
|
||||
'shift+right': () => this.$parent.$emit('parentFocus', 'right'),
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
active(v) {
|
||||
this.$emit('change-active-state', v);
|
||||
},
|
||||
|
||||
dragging(v) {
|
||||
this.$root.$emit(v ? 'deck.column.dragStart' : 'deck.column.dragEnd');
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (!this.isMainColumn) {
|
||||
this.$root.$on('deck.column.dragStart', this.onOtherDragStart);
|
||||
this.$root.$on('deck.column.dragEnd', this.onOtherDragEnd);
|
||||
}
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
if (!this.isMainColumn) {
|
||||
this.$root.$off('deck.column.dragStart', this.onOtherDragStart);
|
||||
this.$root.$off('deck.column.dragEnd', this.onOtherDragEnd);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onOtherDragStart() {
|
||||
this.dropready = true;
|
||||
},
|
||||
|
||||
onOtherDragEnd() {
|
||||
this.dropready = false;
|
||||
},
|
||||
|
||||
toggleActive() {
|
||||
if (!this.isStacked) return;
|
||||
this.active = !this.active;
|
||||
},
|
||||
|
||||
getMenu() {
|
||||
const items = [{
|
||||
icon: faPencilAlt,
|
||||
text: this.$t('rename'),
|
||||
action: () => {
|
||||
this.$root.dialog({
|
||||
title: this.$t('rename'),
|
||||
input: {
|
||||
default: this.column.name,
|
||||
allowEmpty: false
|
||||
}
|
||||
}).then(({ canceled, result: name }) => {
|
||||
if (canceled) return;
|
||||
this.$store.commit('deviceUser/renameDeckColumn', { id: this.column.id, name });
|
||||
});
|
||||
}
|
||||
}, null, {
|
||||
icon: faArrowLeft,
|
||||
text: this.$t('swap-left'),
|
||||
action: () => {
|
||||
this.$store.commit('deviceUser/swapLeftDeckColumn', this.column.id);
|
||||
}
|
||||
}, {
|
||||
icon: faArrowRight,
|
||||
text: this.$t('swap-right'),
|
||||
action: () => {
|
||||
this.$store.commit('deviceUser/swapRightDeckColumn', this.column.id);
|
||||
}
|
||||
}, this.isStacked ? {
|
||||
icon: faArrowUp,
|
||||
text: this.$t('swap-up'),
|
||||
action: () => {
|
||||
this.$store.commit('deviceUser/swapUpDeckColumn', this.column.id);
|
||||
}
|
||||
} : undefined, this.isStacked ? {
|
||||
icon: faArrowDown,
|
||||
text: this.$t('swap-down'),
|
||||
action: () => {
|
||||
this.$store.commit('deviceUser/swapDownDeckColumn', this.column.id);
|
||||
}
|
||||
} : undefined, null, {
|
||||
icon: faWindowRestore,
|
||||
text: this.$t('stack-left'),
|
||||
action: () => {
|
||||
this.$store.commit('deviceUser/stackLeftDeckColumn', this.column.id);
|
||||
}
|
||||
}, this.isStacked ? {
|
||||
icon: faWindowMaximize,
|
||||
text: this.$t('pop-right'),
|
||||
action: () => {
|
||||
this.$store.commit('deviceUser/popRightDeckColumn', this.column.id);
|
||||
}
|
||||
} : undefined, null, {
|
||||
icon: faTrashAlt,
|
||||
text: this.$t('remove'),
|
||||
action: () => {
|
||||
this.$store.commit('deviceUser/removeDeckColumn', this.column.id);
|
||||
}
|
||||
}];
|
||||
|
||||
if (this.menu) {
|
||||
for (const i of this.menu.reverse()) {
|
||||
items.unshift(i);
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
|
||||
onContextmenu(e) {
|
||||
if (this.isMainColumn) return;
|
||||
this.showMenu();
|
||||
},
|
||||
|
||||
showMenu() {
|
||||
this.$root.menu({
|
||||
items: this.getMenu(),
|
||||
source: this.$refs.menu,
|
||||
});
|
||||
},
|
||||
|
||||
close() {
|
||||
this.$router.push('/');
|
||||
},
|
||||
|
||||
goTop() {
|
||||
this.$refs.body.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
},
|
||||
|
||||
onDragstart(e) {
|
||||
// メインカラムはドラッグさせない
|
||||
if (this.isMainColumn) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('mk-deck-column', this.column.id);
|
||||
this.dragging = true;
|
||||
},
|
||||
|
||||
onDragend(e) {
|
||||
this.dragging = false;
|
||||
},
|
||||
|
||||
onDragover(e) {
|
||||
// メインカラムにはドロップさせない
|
||||
if (this.isMainColumn) {
|
||||
e.dataTransfer.dropEffect = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
// 自分自身がドラッグされている場合
|
||||
if (this.dragging) {
|
||||
// 自分自身にはドロップさせない
|
||||
e.dataTransfer.dropEffect = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const isDeckColumn = e.dataTransfer.types[0] == 'mk-deck-column';
|
||||
|
||||
e.dataTransfer.dropEffect = isDeckColumn ? 'move' : 'none';
|
||||
|
||||
if (!this.dragging && isDeckColumn) this.draghover = true;
|
||||
},
|
||||
|
||||
onDragleave() {
|
||||
this.draghover = false;
|
||||
},
|
||||
|
||||
onDrop(e) {
|
||||
this.draghover = false;
|
||||
this.$root.$emit('deck.column.dragEnd');
|
||||
|
||||
const id = e.dataTransfer.getData('mk-deck-column');
|
||||
if (id != null && id != '') {
|
||||
this.$store.commit('deviceUser/swapDeckColumn', {
|
||||
a: this.column.id,
|
||||
b: id
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dnpfarvg {
|
||||
$header-height: 42px;
|
||||
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 0 1px var(--deckColumnBorder);
|
||||
|
||||
&.draghover {
|
||||
box-shadow: 0 0 0 2px var(--focus);
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--focus);
|
||||
}
|
||||
}
|
||||
|
||||
&.dragging {
|
||||
box-shadow: 0 0 0 2px var(--focus);
|
||||
}
|
||||
|
||||
&.dropready {
|
||||
* {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.active) {
|
||||
flex-basis: $header-height;
|
||||
min-height: $header-height;
|
||||
|
||||
> header.indicated {
|
||||
box-shadow: 4px 0px var(--accent) inset;
|
||||
}
|
||||
}
|
||||
|
||||
&.naked {
|
||||
//background: var(--deckAcrylicColumnBg);
|
||||
background: transparent !important;
|
||||
|
||||
> header {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
|
||||
> button {
|
||||
color: var(--fg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.paged {
|
||||
> div {
|
||||
background: var(--bg);
|
||||
padding: var(--margin);
|
||||
}
|
||||
}
|
||||
|
||||
> header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
z-index: 2;
|
||||
line-height: $header-height;
|
||||
padding: 0 16px;
|
||||
font-size: 0.9em;
|
||||
color: var(--panelHeaderFg);
|
||||
background: var(--panelHeaderBg);
|
||||
box-shadow: 0 1px 0 0 var(--panelHeaderDivider);
|
||||
cursor: pointer;
|
||||
|
||||
&, * {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&.indicated {
|
||||
box-shadow: 0 3px 0 0 var(--accent);
|
||||
}
|
||||
|
||||
> .header {
|
||||
display: inline-block;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
> span:only-of-type {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
> .toggleActive,
|
||||
> .action > *,
|
||||
> .menu,
|
||||
> .close {
|
||||
z-index: 1;
|
||||
width: $header-height;
|
||||
line-height: $header-height;
|
||||
font-size: 16px;
|
||||
color: var(--faceTextButton);
|
||||
|
||||
&:hover {
|
||||
color: var(--faceTextButtonHover);
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--faceTextButtonActive);
|
||||
}
|
||||
}
|
||||
|
||||
> .toggleActive, > .action {
|
||||
margin-left: -16px;
|
||||
}
|
||||
|
||||
> .action {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
> .action:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> .menu,
|
||||
> .close {
|
||||
margin-left: auto;
|
||||
margin-right: -16px;
|
||||
}
|
||||
}
|
||||
|
||||
> div {
|
||||
height: calc(100% - #{$header-height});
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
</style>
|
39
src/client/components/deck/direct-column.vue
Normal file
39
src/client/components/deck/direct-column.vue
Normal file
|
@ -0,0 +1,39 @@
|
|||
<template>
|
||||
<x-column :name="name" :column="column" :is-stacked="isStacked" :menu="menu">
|
||||
<template #header><fa :icon="faEnvelope" style="margin-right: 8px;"/>{{ column.name }}</template>
|
||||
|
||||
<x-direct/>
|
||||
</x-column>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faEnvelope } from '@fortawesome/free-solid-svg-icons';
|
||||
import XColumn from './column.vue';
|
||||
import XDirect from '../../pages/messages.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XColumn,
|
||||
XDirect
|
||||
},
|
||||
|
||||
props: {
|
||||
column: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
isStacked: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
menu: null,
|
||||
faEnvelope
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
87
src/client/components/deck/list-column.vue
Normal file
87
src/client/components/deck/list-column.vue
Normal file
|
@ -0,0 +1,87 @@
|
|||
<template>
|
||||
<x-column :menu="menu" :column="column" :is-stacked="isStacked">
|
||||
<template #header>
|
||||
<fa :icon="faListUl"/><span style="margin-left: 8px;">{{ column.name }}</span>
|
||||
</template>
|
||||
|
||||
<x-timeline v-if="column.listId" ref="timeline" src="list" :list="column.listId" @after="() => $emit('loaded')"/>
|
||||
</x-column>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faListUl, faCog } from '@fortawesome/free-solid-svg-icons';
|
||||
import XColumn from './column.vue';
|
||||
import XTimeline from '../timeline.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XColumn,
|
||||
XTimeline,
|
||||
},
|
||||
|
||||
props: {
|
||||
column: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
isStacked: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
faListUl
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
mediaOnly() {
|
||||
(this.$refs.timeline as any).reload();
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.menu = [{
|
||||
icon: faCog,
|
||||
text: this.$t('list'),
|
||||
action: this.setList
|
||||
}];
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (this.column.listId == null) {
|
||||
this.setList();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async setList() {
|
||||
const lists = await this.$root.api('users/lists/list');
|
||||
const { canceled, result: list } = await this.$root.dialog({
|
||||
title: this.$t('list'),
|
||||
type: null,
|
||||
select: {
|
||||
items: lists.map(x => ({
|
||||
value: x, text: x.name
|
||||
})),
|
||||
default: this.column.listId
|
||||
},
|
||||
showCancelButton: true
|
||||
});
|
||||
if (canceled) return;
|
||||
Vue.set(this.column, 'listId', list.id);
|
||||
this.$store.commit('deviceUser/updateDeckColumn', this.column);
|
||||
},
|
||||
|
||||
focus() {
|
||||
(this.$refs.timeline as any).focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
39
src/client/components/deck/mentions-column.vue
Normal file
39
src/client/components/deck/mentions-column.vue
Normal file
|
@ -0,0 +1,39 @@
|
|||
<template>
|
||||
<x-column :column="column" :is-stacked="isStacked" :menu="menu">
|
||||
<template #header><fa :icon="faAt" style="margin-right: 8px;"/>{{ column.name }}</template>
|
||||
|
||||
<x-mentions/>
|
||||
</x-column>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faAt } from '@fortawesome/free-solid-svg-icons';
|
||||
import XColumn from './column.vue';
|
||||
import XMentions from '../../pages/mentions.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XColumn,
|
||||
XMentions
|
||||
},
|
||||
|
||||
props: {
|
||||
column: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
isStacked: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
menu: null,
|
||||
faAt
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
69
src/client/components/deck/notifications-column.vue
Normal file
69
src/client/components/deck/notifications-column.vue
Normal file
|
@ -0,0 +1,69 @@
|
|||
<template>
|
||||
<x-column :column="column" :is-stacked="isStacked" :menu="menu">
|
||||
<template #header><fa :icon="faBell" style="margin-right: 8px;"/>{{ column.name }}</template>
|
||||
|
||||
<x-notifications/>
|
||||
</x-column>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faCog } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faBell } from '@fortawesome/free-regular-svg-icons';
|
||||
import XColumn from './column.vue';
|
||||
import XNotifications from '../notifications.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XColumn,
|
||||
XNotifications
|
||||
},
|
||||
|
||||
props: {
|
||||
column: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
isStacked: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
menu: null,
|
||||
faBell
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
if (this.column.notificationType == null) {
|
||||
this.column.notificationType = 'all';
|
||||
this.$store.commit('deviceUser/updateDeckColumn', this.column);
|
||||
}
|
||||
|
||||
this.menu = [{
|
||||
icon: faCog,
|
||||
text: this.$t('@.notification-type'),
|
||||
action: () => {
|
||||
this.$root.dialog({
|
||||
title: this.$t('@.notification-type'),
|
||||
type: null,
|
||||
select: {
|
||||
items: ['all', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'].map(x => ({
|
||||
value: x, text: this.$t('@.notification-types.' + x)
|
||||
}))
|
||||
default: this.column.notificationType,
|
||||
},
|
||||
showCancelButton: true
|
||||
}).then(({ canceled, result: type }) => {
|
||||
if (canceled) return;
|
||||
this.column.notificationType = type;
|
||||
this.$store.commit('deviceUser/updateDeckColumn', this.column);
|
||||
});
|
||||
}
|
||||
}];
|
||||
},
|
||||
});
|
||||
</script>
|
141
src/client/components/deck/tl-column.vue
Normal file
141
src/client/components/deck/tl-column.vue
Normal file
|
@ -0,0 +1,141 @@
|
|||
<template>
|
||||
<x-column :menu="menu" :column="column" :is-stacked="isStacked" :indicated="indicated" @change-active-state="onChangeActiveState">
|
||||
<template #header>
|
||||
<fa v-if="column.tl === 'home'" :icon="faHome"/>
|
||||
<fa v-else-if="column.tl === 'local'" :icon="faComments"/>
|
||||
<fa v-else-if="column.tl === 'social'" :icon="faShareAlt"/>
|
||||
<fa v-else-if="column.tl === 'global'" :icon="faGlobe"/>
|
||||
<span style="margin-left: 8px;">{{ column.name }}</span>
|
||||
</template>
|
||||
|
||||
<div class="iwaalbte" v-if="disabled">
|
||||
<p>
|
||||
<fa :icon="faMinusCircle"/>
|
||||
{{ $t('disabled-timeline.title') }}
|
||||
</p>
|
||||
<p class="desc">{{ $t('disabled-timeline.description') }}</p>
|
||||
</div>
|
||||
<x-timeline v-else-if="column.tl" ref="timeline" :src="column.tl" @after="() => $emit('loaded')" @queue="queueUpdated" @note="onNote" :key="column.tl"/>
|
||||
</x-column>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faMinusCircle, faHome, faComments, faShareAlt, faGlobe, faCog } from '@fortawesome/free-solid-svg-icons';
|
||||
import XColumn from './column.vue';
|
||||
import XTimeline from '../timeline.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XColumn,
|
||||
XTimeline,
|
||||
},
|
||||
|
||||
props: {
|
||||
column: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
isStacked: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
menu: null,
|
||||
disabled: false,
|
||||
indicated: false,
|
||||
columnActive: true,
|
||||
faMinusCircle, faHome, faComments, faShareAlt, faGlobe,
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
mediaOnly() {
|
||||
(this.$refs.timeline as any).reload();
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.menu = [{
|
||||
icon: faCog,
|
||||
text: this.$t('timeline'),
|
||||
action: this.setType
|
||||
}];
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (this.column.tl == null) {
|
||||
this.setType();
|
||||
} else {
|
||||
this.disabled = !this.$store.state.i.isModerator && !this.$store.state.i.isAdmin && (
|
||||
this.$store.state.instance.meta.disableLocalTimeline && ['local', 'social'].includes(this.column.tl) ||
|
||||
this.$store.state.instance.meta.disableGlobalTimeline && ['global'].includes(this.column.tl));
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async setType() {
|
||||
const { canceled, result: src } = await this.$root.dialog({
|
||||
title: this.$t('timeline'),
|
||||
type: null,
|
||||
select: {
|
||||
items: [{
|
||||
value: 'home', text: this.$t('_timelines.home')
|
||||
}, {
|
||||
value: 'local', text: this.$t('_timelines.local')
|
||||
}, {
|
||||
value: 'social', text: this.$t('_timelines.social')
|
||||
}, {
|
||||
value: 'global', text: this.$t('_timelines.global')
|
||||
}]
|
||||
},
|
||||
showCancelButton: true
|
||||
});
|
||||
if (canceled) return;
|
||||
Vue.set(this.column, 'tl', src);
|
||||
this.$store.commit('deviceUser/updateDeckColumn', this.column);
|
||||
},
|
||||
|
||||
queueUpdated(q) {
|
||||
if (this.columnActive) {
|
||||
this.indicated = q !== 0;
|
||||
}
|
||||
},
|
||||
|
||||
onNote() {
|
||||
if (!this.columnActive) {
|
||||
this.indicated = true;
|
||||
}
|
||||
},
|
||||
|
||||
onChangeActiveState(state) {
|
||||
this.columnActive = state;
|
||||
|
||||
if (this.columnActive) {
|
||||
this.indicated = false;
|
||||
}
|
||||
},
|
||||
|
||||
focus() {
|
||||
(this.$refs.timeline as any).focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.iwaalbte {
|
||||
text-align: center;
|
||||
|
||||
> p {
|
||||
margin: 16px;
|
||||
|
||||
&.desc {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
151
src/client/components/deck/widgets-column.vue
Normal file
151
src/client/components/deck/widgets-column.vue
Normal file
|
@ -0,0 +1,151 @@
|
|||
<template>
|
||||
<x-column :menu="menu" :naked="true" :column="column" :is-stacked="isStacked">
|
||||
<template #header><fa :icon="faWindowMaximize" style="margin-right: 8px;"/>{{ column.name }}</template>
|
||||
|
||||
<div class="wtdtxvec">
|
||||
<template v-if="edit">
|
||||
<header>
|
||||
<select v-model="widgetAdderSelected" @change="addWidget">
|
||||
<option v-for="widget in widgets" :value="widget" :key="widget">{{ widget }}</option>
|
||||
</select>
|
||||
</header>
|
||||
<x-draggable
|
||||
:list="column.widgets"
|
||||
animation="150"
|
||||
@sort="onWidgetSort"
|
||||
>
|
||||
<div v-for="widget in column.widgets" class="customize-container" :key="widget.id" @click="widgetFunc(widget.id)">
|
||||
<button class="remove _button" @click="removeWidget(widget)"><fa :icon="faTimes"/></button>
|
||||
<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" :column="column"/>
|
||||
</div>
|
||||
</x-draggable>
|
||||
</template>
|
||||
<component v-else class="widget" v-for="widget in column.widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" :column="column"/>
|
||||
</div>
|
||||
</x-column>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import * as XDraggable from 'vuedraggable';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { faWindowMaximize, faTimes } from '@fortawesome/free-solid-svg-icons';
|
||||
import XColumn from './column.vue';
|
||||
import { widgets } from '../../widgets';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XColumn,
|
||||
XDraggable,
|
||||
},
|
||||
|
||||
props: {
|
||||
column: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isStacked: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
edit: false,
|
||||
menu: null,
|
||||
widgetAdderSelected: null,
|
||||
widgets,
|
||||
faWindowMaximize, faTimes
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.menu = [{
|
||||
icon: 'cog',
|
||||
text: this.$t('edit'),
|
||||
action: () => {
|
||||
this.edit = !this.edit;
|
||||
}
|
||||
}];
|
||||
},
|
||||
|
||||
methods: {
|
||||
widgetFunc(id) {
|
||||
this.$refs[id][0].setting();
|
||||
},
|
||||
|
||||
onWidgetSort() {
|
||||
this.saveWidgets();
|
||||
},
|
||||
|
||||
addWidget() {
|
||||
this.$store.commit('deviceUser/addDeckWidget', {
|
||||
id: this.column.id,
|
||||
widget: {
|
||||
name: this.widgetAdderSelected,
|
||||
id: uuid(),
|
||||
data: {}
|
||||
}
|
||||
});
|
||||
|
||||
this.widgetAdderSelected = null;
|
||||
},
|
||||
|
||||
removeWidget(widget) {
|
||||
this.$store.commit('deviceUser/removeDeckWidget', {
|
||||
id: this.column.id,
|
||||
widget
|
||||
});
|
||||
},
|
||||
|
||||
saveWidgets() {
|
||||
this.$store.commit('deviceUser/updateDeckColumn', this.column);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wtdtxvec {
|
||||
padding-top: 1px; // ウィジェットのbox-shadowを利用した1px borderを隠さないようにするため
|
||||
|
||||
> header {
|
||||
padding: 16px;
|
||||
|
||||
> * {
|
||||
width: 100%;
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
> .widget, .customize-container {
|
||||
margin: 8px;
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.customize-container {
|
||||
position: relative;
|
||||
cursor: move;
|
||||
|
||||
> *:not(.remove) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
> .remove {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
color: #fff;
|
||||
background: rgba(#000, 0.7);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -40,7 +40,7 @@ export default Vue.extend({
|
|||
|
||||
> img {
|
||||
vertical-align: bottom;
|
||||
height: 150px;
|
||||
height: 128px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
|
71
src/client/components/form-window.vue
Normal file
71
src/client/components/form-window.vue
Normal file
|
@ -0,0 +1,71 @@
|
|||
<template>
|
||||
<x-window ref="window" :width="400" :height="450" :no-padding="true" @closed="() => { $emit('closed'); destroyDom(); }" :with-ok-button="true" :ok-button-disabled="false" @ok="ok()" :can-close="false">
|
||||
<template #header>
|
||||
{{ title }}
|
||||
</template>
|
||||
<div class="xkpnjxcv">
|
||||
<label v-for="item in Object.keys(form).filter(item => !form[item].hidden)" :key="item">
|
||||
<mk-input v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1"><span v-text="form[item].label || item"></span></mk-input>
|
||||
<mk-input v-else-if="form[item].type === 'string' && !item.multiline" v-model="values[item]" type="text"><span v-text="form[item].label || item"></span></mk-input>
|
||||
<mk-textarea v-else-if="form[item].type === 'string' && item.multiline" v-model="values[item]"><span v-text="form[item].label || item"></span></mk-textarea>
|
||||
<mk-switch v-else-if="form[item].type === 'boolean'" v-model="values[item]"><span v-text="form[item].label || item"></span></mk-switch>
|
||||
</label>
|
||||
</div>
|
||||
</x-window>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import XWindow from './window.vue';
|
||||
import MkInput from './ui/input.vue';
|
||||
import MkTextarea from './ui/textarea.vue';
|
||||
import MkSwitch from './ui/switch.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XWindow,
|
||||
MkInput,
|
||||
MkTextarea,
|
||||
MkSwitch,
|
||||
},
|
||||
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
form: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
values: {}
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
for (const item in this.form) {
|
||||
Vue.set(this.values, item, this.form[item].default || null);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
ok() {
|
||||
this.$emit('ok', this.values);
|
||||
this.$refs.window.close();
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.xkpnjxcv {
|
||||
> label {
|
||||
display: block;
|
||||
padding: 16px 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<div class="mk-modal" v-hotkey.global="keymap">
|
||||
<transition :name="$store.state.device.animation ? 'bg-fade' : ''" appear>
|
||||
<div class="bg" ref="bg" v-if="show" @click="close()"></div>
|
||||
<div class="bg" ref="bg" v-if="show" @click="canClose ? close() : () => {}"></div>
|
||||
</transition>
|
||||
<transition :name="$store.state.device.animation ? 'modal' : ''" appear @after-leave="() => { $emit('closed'); destroyDom(); }">
|
||||
<div class="content" ref="content" v-if="show" @click.self="close()"><slot></slot></div>
|
||||
<div class="content" ref="content" v-if="show" @click.self="canClose ? close() : () => {}"><slot></slot></div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -14,6 +14,11 @@ import Vue from 'vue';
|
|||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
canClose: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
|
@ -54,7 +54,6 @@ export default Vue.extend({
|
|||
margin: 0 .5em 0 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
color: var(--noteHeaderName);
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
|
|
|
@ -724,61 +724,6 @@ export default Vue.extend({
|
|||
transition: box-shadow 0.1s ease;
|
||||
overflow: hidden;
|
||||
|
||||
&.max-width_500px {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
&.max-width_450px {
|
||||
> .renote {
|
||||
padding: 8px 16px 0 16px;
|
||||
}
|
||||
|
||||
> .article {
|
||||
padding: 14px 16px 9px;
|
||||
|
||||
> .avatar {
|
||||
margin: 0 10px 8px 0;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.max-width_350px {
|
||||
> .article {
|
||||
> .main {
|
||||
> .footer {
|
||||
> .button {
|
||||
&:not(:last-child) {
|
||||
margin-right: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.max-width_300px {
|
||||
font-size: 0.825em;
|
||||
|
||||
> .article {
|
||||
> .avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
> .main {
|
||||
> .footer {
|
||||
> .button {
|
||||
&:not(:last-child) {
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px var(--focus);
|
||||
|
@ -797,10 +742,6 @@ export default Vue.extend({
|
|||
white-space: pre;
|
||||
color: #d28a3f;
|
||||
|
||||
@media (max-width: 450px) {
|
||||
padding: 8px 16px 0 16px;
|
||||
}
|
||||
|
||||
> [data-icon] {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
@ -985,5 +926,64 @@ export default Vue.extend({
|
|||
> .reply {
|
||||
border-top: solid 1px var(--divider);
|
||||
}
|
||||
|
||||
&.max-width_500px {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
&.max-width_450px {
|
||||
> .renote {
|
||||
padding: 8px 16px 0 16px;
|
||||
}
|
||||
|
||||
> .info {
|
||||
padding: 8px 16px 0 16px;
|
||||
}
|
||||
|
||||
> .article {
|
||||
padding: 14px 16px 9px;
|
||||
|
||||
> .avatar {
|
||||
margin: 0 10px 8px 0;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.max-width_350px {
|
||||
> .article {
|
||||
> .main {
|
||||
> .footer {
|
||||
> .button {
|
||||
&:not(:last-child) {
|
||||
margin-right: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.max-width_300px {
|
||||
font-size: 0.825em;
|
||||
|
||||
> .article {
|
||||
> .avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
> .main {
|
||||
> .footer {
|
||||
> .button {
|
||||
&:not(:last-child) {
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
488
src/client/components/sidebar.vue
Normal file
488
src/client/components/sidebar.vue
Normal file
|
@ -0,0 +1,488 @@
|
|||
<template>
|
||||
<div class="mvcprjjd">
|
||||
<transition name="nav-back">
|
||||
<div class="nav-back"
|
||||
v-if="showing"
|
||||
@click="showing = false"
|
||||
@touchstart="showing = false"
|
||||
></div>
|
||||
</transition>
|
||||
|
||||
<transition name="nav">
|
||||
<nav class="nav" v-show="showing">
|
||||
<div>
|
||||
<button class="item _button account" @click="openAccountMenu" v-if="$store.getters.isSignedIn">
|
||||
<mk-avatar :user="$store.state.i" class="avatar"/><mk-acct class="text" :user="$store.state.i"/>
|
||||
</button>
|
||||
<button class="item _button index active" @click="top()" v-if="$route.name === 'index'">
|
||||
<fa :icon="faHome" fixed-width/><span class="text">{{ $store.getters.isSignedIn ? $t('timeline') : $t('home') }}</span>
|
||||
</button>
|
||||
<router-link class="item index" active-class="active" to="/" exact v-else>
|
||||
<fa :icon="faHome" fixed-width/><span class="text">{{ $store.getters.isSignedIn ? $t('timeline') : $t('home') }}</span>
|
||||
</router-link>
|
||||
<template v-for="item in menu">
|
||||
<div v-if="item === '-'" class="divider"></div>
|
||||
<component v-else-if="menuDef[item] && (menuDef[item].show !== false)" :is="menuDef[item].to ? 'router-link' : 'button'" class="item _button" :class="item" active-class="active" @click="() => { if (menuDef[item].action) menuDef[item].action() }" :to="menuDef[item].to">
|
||||
<fa :icon="menuDef[item].icon" fixed-width/><span class="text">{{ $t(menuDef[item].title) }}</span>
|
||||
<i v-if="menuDef[item].indicated"><fa :icon="faCircle"/></i>
|
||||
</component>
|
||||
</template>
|
||||
<div class="divider"></div>
|
||||
<button class="item _button" :class="{ active: $route.path === '/instance' || $route.path.startsWith('/instance/') }" v-if="$store.getters.isSignedIn && ($store.state.i.isAdmin || $store.state.i.isModerator)" @click="oepnInstanceMenu">
|
||||
<fa :icon="faServer" fixed-width/><span class="text">{{ $t('instance') }}</span>
|
||||
</button>
|
||||
<button class="item _button" @click="more">
|
||||
<fa :icon="faEllipsisH" fixed-width/><span class="text">{{ $t('more') }}</span>
|
||||
<i v-if="otherNavItemIndicated"><fa :icon="faCircle"/></i>
|
||||
</button>
|
||||
<router-link class="item" active-class="active" to="/preferences">
|
||||
<fa :icon="faCog" fixed-width/><span class="text">{{ $t('settings') }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</nav>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faGripVertical, faChevronLeft, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faListUl, faPlus, faUserClock, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer, faInfoCircle, faQuestionCircle, faProjectDiagram } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faBell, faEnvelope, faLaugh, faComments } from '@fortawesome/free-regular-svg-icons';
|
||||
import { host, instanceName } from '../config';
|
||||
import { search } from '../scripts/search';
|
||||
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
host: host,
|
||||
showing: false,
|
||||
searching: false,
|
||||
accounts: [],
|
||||
connection: null,
|
||||
menuDef: this.$store.getters.nav({
|
||||
search: this.search
|
||||
}),
|
||||
faGripVertical, faChevronLeft, faComments, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faBell, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faEnvelope, faListUl, faPlus, faUserClock, faLaugh, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer, faProjectDiagram
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
menu(): string[] {
|
||||
return this.$store.state.deviceUser.menu;
|
||||
},
|
||||
|
||||
otherNavItemIndicated(): boolean {
|
||||
if (!this.$store.getters.isSignedIn) return false;
|
||||
for (const def in this.menuDef) {
|
||||
if (this.menu.includes(def)) continue;
|
||||
if (this.menuDef[def].indicated) return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
this.showing = false;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
show() {
|
||||
this.showing = true;
|
||||
},
|
||||
|
||||
search() {
|
||||
if (this.searching) return;
|
||||
|
||||
this.$root.dialog({
|
||||
title: this.$t('search'),
|
||||
input: true
|
||||
}).then(async ({ canceled, result: query }) => {
|
||||
if (canceled || query == null || query === '') return;
|
||||
|
||||
this.searching = true;
|
||||
search(this, query).finally(() => {
|
||||
this.searching = false;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
async openAccountMenu(ev) {
|
||||
const accounts = (await this.$root.api('users/show', { userIds: this.$store.state.device.accounts.map(x => x.id) })).filter(x => x.id !== this.$store.state.i.id);
|
||||
|
||||
const accountItems = accounts.map(account => ({
|
||||
type: 'user',
|
||||
user: account,
|
||||
action: () => { this.switchAccount(account); }
|
||||
}));
|
||||
|
||||
this.$root.menu({
|
||||
items: [...[{
|
||||
type: 'link',
|
||||
text: this.$t('profile'),
|
||||
to: `/@${ this.$store.state.i.username }`,
|
||||
avatar: this.$store.state.i,
|
||||
}, {
|
||||
type: 'link',
|
||||
text: this.$t('accountSettings'),
|
||||
to: '/my/settings',
|
||||
icon: faCog,
|
||||
}, null, ...accountItems, {
|
||||
icon: faPlus,
|
||||
text: this.$t('addAcount'),
|
||||
action: () => {
|
||||
this.$root.menu({
|
||||
items: [{
|
||||
text: this.$t('existingAcount'),
|
||||
action: () => { this.addAcount(); },
|
||||
}, {
|
||||
text: this.$t('createAccount'),
|
||||
action: () => { this.createAccount(); },
|
||||
}],
|
||||
align: 'left',
|
||||
fixed: true,
|
||||
width: 240,
|
||||
source: ev.currentTarget || ev.target,
|
||||
});
|
||||
},
|
||||
}]],
|
||||
align: 'left',
|
||||
fixed: true,
|
||||
width: 240,
|
||||
source: ev.currentTarget || ev.target,
|
||||
});
|
||||
},
|
||||
|
||||
oepnInstanceMenu(ev) {
|
||||
this.$root.menu({
|
||||
items: [{
|
||||
type: 'link',
|
||||
text: this.$t('dashboard'),
|
||||
to: '/instance',
|
||||
icon: faTachometerAlt,
|
||||
}, null, {
|
||||
type: 'link',
|
||||
text: this.$t('settings'),
|
||||
to: '/instance/settings',
|
||||
icon: faCog,
|
||||
}, {
|
||||
type: 'link',
|
||||
text: this.$t('customEmojis'),
|
||||
to: '/instance/emojis',
|
||||
icon: faLaugh,
|
||||
}, {
|
||||
type: 'link',
|
||||
text: this.$t('users'),
|
||||
to: '/instance/users',
|
||||
icon: faUsers,
|
||||
}, {
|
||||
type: 'link',
|
||||
text: this.$t('files'),
|
||||
to: '/instance/files',
|
||||
icon: faCloud,
|
||||
}, {
|
||||
type: 'link',
|
||||
text: this.$t('jobQueue'),
|
||||
to: '/instance/queue',
|
||||
icon: faExchangeAlt,
|
||||
}, {
|
||||
type: 'link',
|
||||
text: this.$t('federation'),
|
||||
to: '/instance/federation',
|
||||
icon: faGlobe,
|
||||
}, {
|
||||
type: 'link',
|
||||
text: this.$t('relays'),
|
||||
to: '/instance/relays',
|
||||
icon: faProjectDiagram,
|
||||
}, {
|
||||
type: 'link',
|
||||
text: this.$t('announcements'),
|
||||
to: '/instance/announcements',
|
||||
icon: faBroadcastTower,
|
||||
}],
|
||||
align: 'left',
|
||||
fixed: true,
|
||||
width: 200,
|
||||
source: ev.currentTarget || ev.target,
|
||||
});
|
||||
},
|
||||
|
||||
more(ev) {
|
||||
const items = Object.keys(this.menuDef).filter(k => !this.menu.includes(k)).map(k => this.menuDef[k]).filter(def => def.show == null ? true : def.show).map(def => ({
|
||||
type: def.to ? 'link' : 'button',
|
||||
text: this.$t(def.title),
|
||||
icon: def.icon,
|
||||
to: def.to,
|
||||
action: def.action,
|
||||
indicate: def.indicated,
|
||||
}));
|
||||
this.$root.menu({
|
||||
items: [...items, null, {
|
||||
type: 'link',
|
||||
text: this.$t('help'),
|
||||
to: '/docs',
|
||||
icon: faQuestionCircle,
|
||||
}, {
|
||||
type: 'link',
|
||||
text: this.$t('aboutX', { x: instanceName || host }),
|
||||
to: '/about',
|
||||
icon: faInfoCircle,
|
||||
}, {
|
||||
type: 'link',
|
||||
text: this.$t('aboutMisskey'),
|
||||
to: '/about-misskey',
|
||||
icon: faInfoCircle,
|
||||
}],
|
||||
align: 'left',
|
||||
fixed: true,
|
||||
width: 200,
|
||||
source: ev.currentTarget || ev.target,
|
||||
});
|
||||
},
|
||||
|
||||
async addAcount() {
|
||||
this.$root.new(await import('./signin-dialog.vue').then(m => m.default)).$once('login', res => {
|
||||
this.$store.dispatch('addAcount', res);
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
async createAccount() {
|
||||
this.$root.new(await import('./signup-dialog.vue').then(m => m.default)).$once('signup', res => {
|
||||
this.$store.dispatch('addAcount', res);
|
||||
this.switchAccountWithToken(res.i);
|
||||
});
|
||||
},
|
||||
|
||||
async switchAccount(account: any) {
|
||||
const token = this.$store.state.device.accounts.find((x: any) => x.id === account.id).token;
|
||||
this.switchAccountWithToken(token);
|
||||
},
|
||||
|
||||
switchAccountWithToken(token: string) {
|
||||
this.$root.dialog({
|
||||
type: 'waiting',
|
||||
iconOnly: true
|
||||
});
|
||||
|
||||
this.$root.api('i', {}, token).then((i: any) => {
|
||||
this.$store.dispatch('switchAccount', {
|
||||
...i,
|
||||
token: token
|
||||
}).then(() => {
|
||||
this.$nextTick(() => {
|
||||
location.reload();
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.nav-enter-active,
|
||||
.nav-leave-active {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
.nav-enter,
|
||||
.nav-leave-active {
|
||||
opacity: 0;
|
||||
transform: translateX(-240px);
|
||||
}
|
||||
|
||||
.nav-back-enter-active,
|
||||
.nav-back-leave-active {
|
||||
opacity: 1;
|
||||
transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
.nav-back-enter,
|
||||
.nav-back-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.mvcprjjd {
|
||||
$ui-font-size: 1em; // TODO: どこかに集約したい
|
||||
$nav-width: 250px; // TODO: どこかに集約したい
|
||||
$nav-icon-only-width: 80px; // TODO: どこかに集約したい
|
||||
$nav-icon-only-threshold: 1279px; // TODO: どこかに集約したい
|
||||
$nav-hide-threshold: 650px; // TODO: どこかに集約したい
|
||||
|
||||
> .nav-back {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1001;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--modalBg);
|
||||
}
|
||||
|
||||
> .nav {
|
||||
$avatar-size: 32px;
|
||||
$avatar-margin: 8px;
|
||||
|
||||
flex: 0 0 $nav-width;
|
||||
width: $nav-width;
|
||||
box-sizing: border-box;
|
||||
|
||||
@media (max-width: $nav-icon-only-threshold) {
|
||||
flex: 0 0 $nav-icon-only-width;
|
||||
width: $nav-icon-only-width;
|
||||
}
|
||||
|
||||
@media (max-width: $nav-hide-threshold) {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
@media (min-width: $nav-hide-threshold + 1px) {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
> div {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1001;
|
||||
width: $nav-width;
|
||||
height: 100vh;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
background: var(--navBg);
|
||||
border-right: solid 1px var(--divider);
|
||||
|
||||
> .divider {
|
||||
margin: 16px 0;
|
||||
border-top: solid 1px var(--divider);
|
||||
}
|
||||
|
||||
@media (max-width: $nav-icon-only-threshold) and (min-width: $nav-hide-threshold + 1px) {
|
||||
width: $nav-icon-only-width;
|
||||
|
||||
> .divider {
|
||||
margin: 8px auto;
|
||||
width: calc(100% - 32px);
|
||||
}
|
||||
|
||||
> .item {
|
||||
&:first-child {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .item {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding-left: 32px;
|
||||
font-size: $ui-font-size;
|
||||
line-height: 3.2rem;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
box-sizing: border-box;
|
||||
color: var(--navFg);
|
||||
|
||||
> [data-icon] {
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
> [data-icon],
|
||||
> .avatar {
|
||||
margin-right: $avatar-margin;
|
||||
}
|
||||
|
||||
> .avatar {
|
||||
width: $avatar-size;
|
||||
height: $avatar-size;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
> i {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 20px;
|
||||
color: var(--navIndicator);
|
||||
font-size: 8px;
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
color: var(--navHoverFg);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--navActive);
|
||||
}
|
||||
|
||||
&:first-child, &:last-child {
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
background: var(--X14);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
top: 0;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: solid 1px var(--divider);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
bottom: 0;
|
||||
margin-top: 16px;
|
||||
border-top: solid 1px var(--divider);
|
||||
}
|
||||
|
||||
@media (max-width: $nav-icon-only-threshold) and (min-width: $nav-hide-threshold + 1px) {
|
||||
padding-left: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: $ui-font-size * 1.2;
|
||||
line-height: 3.7rem;
|
||||
|
||||
> [data-icon],
|
||||
> .avatar {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
> i {
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
> .text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $nav-hide-threshold) {
|
||||
> .index,
|
||||
> .notifications {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -17,9 +17,11 @@ export default Vue.extend({
|
|||
required: true
|
||||
},
|
||||
list: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
antenna: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
sound: {
|
||||
|
@ -53,6 +55,8 @@ export default Vue.extend({
|
|||
const _note = JSON.parse(JSON.stringify(note)); // deepcopy
|
||||
(this.$refs.tl as any).prepend(_note);
|
||||
|
||||
this.$emit('note');
|
||||
|
||||
if (this.sound) {
|
||||
this.$root.sound(note.userId === this.$store.state.i.id ? 'noteMy' : 'note');
|
||||
}
|
||||
|
@ -77,10 +81,10 @@ export default Vue.extend({
|
|||
if (this.src == 'antenna') {
|
||||
endpoint = 'antennas/notes';
|
||||
this.query = {
|
||||
antennaId: this.antenna.id
|
||||
antennaId: this.antenna
|
||||
};
|
||||
this.connection = this.$root.stream.connectToChannel('antenna', {
|
||||
antennaId: this.antenna.id
|
||||
antennaId: this.antenna
|
||||
});
|
||||
this.connection.on('note', prepend);
|
||||
} else if (this.src == 'home') {
|
||||
|
@ -106,10 +110,10 @@ export default Vue.extend({
|
|||
} else if (this.src == 'list') {
|
||||
endpoint = 'notes/user-list-timeline';
|
||||
this.query = {
|
||||
listId: this.list.id
|
||||
listId: this.list
|
||||
};
|
||||
this.connection = this.$root.stream.connectToChannel('userList', {
|
||||
listId: this.list.id
|
||||
listId: this.list
|
||||
});
|
||||
this.connection.on('note', prepend);
|
||||
this.connection.on('userAdded', onUserAdded);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="ukygtjoj _panel" :class="{ naked, hideHeader: !showHeader }">
|
||||
<div class="ukygtjoj _panel" :class="{ naked, hideHeader: !showHeader, scrollable }" v-size="[{ max: 500 }]">
|
||||
<header v-if="showHeader">
|
||||
<div class="title"><slot name="header"></slot></div>
|
||||
<slot name="func"></slot>
|
||||
|
@ -47,6 +47,11 @@ export default Vue.extend({
|
|||
required: false,
|
||||
default: true
|
||||
},
|
||||
scrollable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -107,10 +112,19 @@ export default Vue.extend({
|
|||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&.scrollable {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> div {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
> header {
|
||||
position: relative;
|
||||
box-shadow: 0 1px 0 0 var(--panelHeaderDivider);
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
background: var(--panelHeaderBg);
|
||||
color: var(--panelHeaderFg);
|
||||
|
||||
|
@ -118,10 +132,6 @@ export default Vue.extend({
|
|||
margin: 0;
|
||||
padding: 12px 16px;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
> [data-icon] {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
@ -141,5 +151,21 @@ export default Vue.extend({
|
|||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.max-width_500px {
|
||||
> header {
|
||||
> .title {
|
||||
padding: 8px 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
._forceContainerFull_ .ukygtjoj {
|
||||
> header {
|
||||
> .title {
|
||||
padding: 12px 16px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
:pattern="pattern"
|
||||
:autocomplete="autocomplete"
|
||||
:spellcheck="spellcheck"
|
||||
:step="step"
|
||||
@focus="focused = true"
|
||||
@blur="focused = false"
|
||||
@keydown="$emit('keydown', $event)"
|
||||
|
@ -36,6 +37,7 @@
|
|||
:pattern="pattern"
|
||||
:autocomplete="autocomplete"
|
||||
:spellcheck="spellcheck"
|
||||
:step="step"
|
||||
@focus="focused = true"
|
||||
@blur="focused = false"
|
||||
@keydown="$emit('keydown', $event)"
|
||||
|
@ -114,6 +116,9 @@ export default Vue.extend({
|
|||
spellcheck: {
|
||||
required: false
|
||||
},
|
||||
step: {
|
||||
required: false
|
||||
},
|
||||
debounce: {
|
||||
required: false
|
||||
},
|
||||
|
@ -164,7 +169,7 @@ export default Vue.extend({
|
|||
},
|
||||
v(v) {
|
||||
if (this.type === 'number') {
|
||||
this.$emit('input', parseInt(v, 10));
|
||||
this.$emit('input', parseFloat(v));
|
||||
} else {
|
||||
this.$emit('input', v);
|
||||
}
|
||||
|
@ -297,7 +302,7 @@ export default Vue.extend({
|
|||
pointer-events: none;
|
||||
transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
transition-duration: 0.3s;
|
||||
font-size: 16px;
|
||||
font-size: 1em;
|
||||
line-height: 32px;
|
||||
color: var(--inputLabel);
|
||||
pointer-events: none;
|
||||
|
@ -312,7 +317,7 @@ export default Vue.extend({
|
|||
top: -17px;
|
||||
left: 0 !important;
|
||||
pointer-events: none;
|
||||
font-size: 16px;
|
||||
font-size: 1em;
|
||||
line-height: 32px;
|
||||
color: var(--inputLabel);
|
||||
pointer-events: none;
|
||||
|
@ -343,7 +348,7 @@ export default Vue.extend({
|
|||
padding: 0;
|
||||
font: inherit;
|
||||
font-weight: normal;
|
||||
font-size: 16px;
|
||||
font-size: 1em;
|
||||
line-height: $height;
|
||||
color: var(--inputText);
|
||||
background: transparent;
|
||||
|
@ -364,7 +369,7 @@ export default Vue.extend({
|
|||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
font-size: 16px;
|
||||
font-size: 1em;
|
||||
line-height: 32px;
|
||||
color: var(--inputLabel);
|
||||
pointer-events: none;
|
||||
|
|
|
@ -135,7 +135,7 @@ export default Vue.extend({
|
|||
pointer-events: none;
|
||||
transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
transition-duration: 0.3s;
|
||||
font-size: 16px;
|
||||
font-size: 1em;
|
||||
line-height: 32px;
|
||||
pointer-events: none;
|
||||
//will-change transform
|
||||
|
@ -150,7 +150,7 @@ export default Vue.extend({
|
|||
padding: 0;
|
||||
font: inherit;
|
||||
font-weight: normal;
|
||||
font-size: 16px;
|
||||
font-size: 1em;
|
||||
height: 32px;
|
||||
background: none;
|
||||
border: none;
|
||||
|
@ -170,7 +170,7 @@ export default Vue.extend({
|
|||
display: block;
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
font-size: 16px;
|
||||
font-size: 1em;
|
||||
line-height: 32px;
|
||||
color: rgba(#000, 0.54);
|
||||
pointer-events: none;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
role="switch"
|
||||
:aria-checked="checked"
|
||||
:aria-disabled="disabled"
|
||||
@click="toggle"
|
||||
@click.prevent="toggle"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
|
|
|
@ -133,7 +133,7 @@ export default Vue.extend({
|
|||
pointer-events: none;
|
||||
transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
transition-duration: 0.3s;
|
||||
font-size: 16px;
|
||||
font-size: 1em;
|
||||
line-height: 32px;
|
||||
pointer-events: none;
|
||||
//will-change transform
|
||||
|
@ -151,7 +151,7 @@ export default Vue.extend({
|
|||
box-sizing: border-box;
|
||||
font: inherit;
|
||||
font-weight: normal;
|
||||
font-size: 16px;
|
||||
font-size: 1em;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<x-modal ref="modal" @closed="() => { $emit('closed'); destroyDom(); }">
|
||||
<x-modal ref="modal" @closed="() => { $emit('closed'); destroyDom(); }" :can-close="canClose">
|
||||
<div class="ebkgoccj" :class="{ noPadding }" @keydown="onKeydown" :style="{ width: `${width}px`, height: `${height}px` }">
|
||||
<div class="header">
|
||||
<button class="_button" v-if="withOkButton" @click="close()"><fa :icon="faTimes"/></button>
|
||||
|
@ -57,6 +57,11 @@ export default Vue.extend({
|
|||
required: false,
|
||||
default: 400
|
||||
},
|
||||
canClose: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
|
|
|
@ -18,3 +18,4 @@ export const getLocale = async () => Object.fromEntries((await entries(clientDb.
|
|||
export const version = _VERSION_;
|
||||
export const env = _ENV_;
|
||||
export const instanceName = siteName === 'Misskey' ? null : siteName;
|
||||
export const deckmode = localStorage.getItem('deckmode') === 'true';
|
||||
|
|
312
src/client/deck.vue
Normal file
312
src/client/deck.vue
Normal file
|
@ -0,0 +1,312 @@
|
|||
<template>
|
||||
<div class="mk-deck" :class="`${$store.state.device.deckColumnAlign}`" v-hotkey.global="keymap">
|
||||
<x-sidebar ref="nav"/>
|
||||
|
||||
<!-- TODO: deckMainColumnPlace を見て位置変える -->
|
||||
<deck-column class="column" v-if="$store.state.device.deckAlwaysShowMainColumn || $route.name !== 'index'">
|
||||
<template #action>
|
||||
<button class="_button back" v-if="canBack" @click="back()"><fa :icon="faChevronLeft"/></button>
|
||||
</template>
|
||||
|
||||
<template #header>
|
||||
<div class="iwnjqeul">
|
||||
<div class="default">
|
||||
<portal-target name="avatar" slim/>
|
||||
<span class="title"><portal-target name="icon" slim/><portal-target name="title" slim/></span>
|
||||
</div>
|
||||
<div class="custom">
|
||||
<portal-target name="header" slim/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<router-view></router-view>
|
||||
</deck-column>
|
||||
|
||||
<template v-for="ids in layout">
|
||||
<div v-if="ids.length > 1" class="folder column">
|
||||
<deck-column-core v-for="id, i in ids" :ref="id" :key="id" :column="columns.find(c => c.id === id)" :is-stacked="true" @parent-focus="moveFocus(id, $event)"/>
|
||||
</div>
|
||||
<deck-column-core v-else class="column" :ref="ids[0]" :key="ids[0]" :column="columns.find(c => c.id === ids[0])" @parent-focus="moveFocus(ids[0], $event)"/>
|
||||
</template>
|
||||
|
||||
<button @click="addColumn" class="_button add"><fa :icon="faPlus"/></button>
|
||||
|
||||
<button v-if="$store.getters.isSignedIn" class="nav _button" @click="showNav()"><fa :icon="faBars"/><i v-if="navIndicated"><fa :icon="faCircle"/></i></button>
|
||||
<button v-if="$store.getters.isSignedIn" class="post _buttonPrimary" @click="post()"><fa :icon="faPencilAlt"/></button>
|
||||
|
||||
<stream-indicator v-if="$store.getters.isSignedIn"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faPlus, faPencilAlt, faChevronLeft, faBars, faCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { } from '@fortawesome/free-regular-svg-icons';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { host } from './config';
|
||||
import { search } from './scripts/search';
|
||||
import DeckColumnCore from './components/deck/column-core.vue';
|
||||
import DeckColumn from './components/deck/column.vue';
|
||||
import XSidebar from './components/sidebar.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XSidebar,
|
||||
DeckColumn,
|
||||
DeckColumnCore,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
host: host,
|
||||
pageKey: 0,
|
||||
searching: false,
|
||||
connection: null,
|
||||
searchQuery: '',
|
||||
searchWait: false,
|
||||
canBack: false,
|
||||
menuDef: this.$store.getters.nav({}),
|
||||
wallpaper: localStorage.getItem('wallpaper') != null,
|
||||
faPlus, faPencilAlt, faChevronLeft, faBars, faCircle
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
deck() {
|
||||
return this.$store.state.deviceUser.deck;
|
||||
},
|
||||
columns(): any[] {
|
||||
return this.deck.columns;
|
||||
},
|
||||
layout(): any[] {
|
||||
return this.deck.layout;
|
||||
},
|
||||
navIndicated(): boolean {
|
||||
if (!this.$store.getters.isSignedIn) return false;
|
||||
for (const def in this.menuDef) {
|
||||
if (this.menuDef[def].indicated) return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
keymap(): any {
|
||||
return {
|
||||
'p': this.post,
|
||||
'n': this.post,
|
||||
's': this.search,
|
||||
'h|/': this.help
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
this.pageKey++;
|
||||
this.canBack = (window.history.length > 0 && !['index'].includes(to.name));
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
document.documentElement.style.overflowY = 'hidden';
|
||||
|
||||
if (this.$store.getters.isSignedIn) {
|
||||
this.connection = this.$root.stream.useSharedConnection('main');
|
||||
this.connection.on('notification', this.onNotification);
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
},
|
||||
|
||||
methods: {
|
||||
showNav() {
|
||||
this.$refs.nav.show();
|
||||
},
|
||||
|
||||
help() {
|
||||
this.$router.push('/docs/keyboard-shortcut');
|
||||
},
|
||||
|
||||
back() {
|
||||
if (this.canBack) window.history.back();
|
||||
},
|
||||
|
||||
post() {
|
||||
this.$root.post();
|
||||
},
|
||||
|
||||
search() {
|
||||
if (this.searching) return;
|
||||
|
||||
this.$root.dialog({
|
||||
title: this.$t('search'),
|
||||
input: true
|
||||
}).then(async ({ canceled, result: query }) => {
|
||||
if (canceled || query == null || query === '') return;
|
||||
|
||||
this.searching = true;
|
||||
search(this, query).finally(() => {
|
||||
this.searching = false;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
async onNotification(notification) {
|
||||
if (document.visibilityState === 'visible') {
|
||||
this.$root.stream.send('readNotification', {
|
||||
id: notification.id
|
||||
});
|
||||
|
||||
this.$root.new(await import('./components/toast.vue').then(m => m.default), {
|
||||
notification
|
||||
});
|
||||
}
|
||||
|
||||
this.$root.sound('notification');
|
||||
},
|
||||
|
||||
async addColumn(ev) {
|
||||
const columns = [
|
||||
'widgets',
|
||||
'notifications',
|
||||
'tl',
|
||||
'antenna',
|
||||
'list',
|
||||
'mentions',
|
||||
'direct',
|
||||
];
|
||||
|
||||
const { canceled, result: column } = await this.$root.dialog({
|
||||
title: this.$t('_deck.addColumn'),
|
||||
type: null,
|
||||
select: {
|
||||
items: columns.map(column => ({
|
||||
value: column, text: this.$t('_deck._columns.' + column)
|
||||
}))
|
||||
},
|
||||
showCancelButton: true
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
this.$store.commit('deviceUser/addDeckColumn', {
|
||||
type: column,
|
||||
id: uuid(),
|
||||
name: this.$t('_deck._columns.' + column),
|
||||
width: 330,
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mk-deck {
|
||||
$nav-hide-threshold: 650px; // TODO: どこかに集約したい
|
||||
|
||||
// TODO: この値を設定で変えられるようにする?
|
||||
$columnMargin: 12px;
|
||||
|
||||
$deckMargin: 12px;
|
||||
|
||||
--margin: var(--marginHalf);
|
||||
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
padding: $deckMargin 0 $deckMargin $deckMargin;
|
||||
|
||||
&.center {
|
||||
> .column:first-of-type {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
> .add {
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
> .column {
|
||||
flex-shrink: 0;
|
||||
margin-right: $columnMargin;
|
||||
|
||||
&.folder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> *:not(:last-child) {
|
||||
margin-bottom: $columnMargin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .post,
|
||||
> .nav {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
bottom: 32px;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 100%;
|
||||
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12);
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
> .post {
|
||||
right: 32px;
|
||||
}
|
||||
|
||||
> .nav {
|
||||
left: 32px;
|
||||
background: var(--panel);
|
||||
color: var(--fg);
|
||||
|
||||
@media (min-width: ($nav-hide-threshold + 1px)) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--X2);
|
||||
}
|
||||
|
||||
> i {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
color: var(--indicator);
|
||||
font-size: 16px;
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.iwnjqeul {
|
||||
$header-height: 42px; // TODO: column.vueのそれを参照するようにしたい(出来るのか?)
|
||||
|
||||
> .default {
|
||||
> .avatar {
|
||||
$size: 28px;
|
||||
display: inline-block;
|
||||
width: $size;
|
||||
height: $size;
|
||||
vertical-align: bottom;
|
||||
margin: (($header-height - $size) / 2) 8px (($header-height - $size) / 2) 0;
|
||||
}
|
||||
|
||||
> .title {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
line-height: $header-height;
|
||||
|
||||
> [data-icon] {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .custom {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* App entry point
|
||||
* Client entry point
|
||||
*/
|
||||
|
||||
import Vue from 'vue';
|
||||
|
@ -12,11 +12,13 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
|||
|
||||
import VueHotkey from './scripts/hotkey';
|
||||
import App from './app.vue';
|
||||
import Deck from './deck.vue';
|
||||
import MiOS from './mios';
|
||||
import { version, langs, instanceName, getLocale } from './config';
|
||||
import { version, langs, instanceName, getLocale, deckmode } from './config';
|
||||
import PostFormDialog from './components/post-form-dialog.vue';
|
||||
import Dialog from './components/dialog.vue';
|
||||
import Menu from './components/menu.vue';
|
||||
import Form from './components/form-window.vue';
|
||||
import { router } from './router';
|
||||
import { applyTheme, lightTheme } from './scripts/theme';
|
||||
import { isDeviceDarkmode } from './scripts/is-device-darkmode';
|
||||
|
@ -165,6 +167,7 @@ os.init(async () => {
|
|||
i18n // TODO: 消せないか考える SEE: https://github.com/syuilo/misskey/pull/6396#discussion_r429511030
|
||||
};
|
||||
},
|
||||
// TODO: ここらへんのメソッド全部Vuexに移したい
|
||||
methods: {
|
||||
api: (endpoint: string, data: { [x: string]: any } = {}, token?) => store.dispatch('api', { endpoint, data, token }),
|
||||
signout: os.signout,
|
||||
|
@ -194,6 +197,13 @@ os.init(async () => {
|
|||
});
|
||||
return p;
|
||||
},
|
||||
form(title, form) {
|
||||
const vm = this.new(Form, { title, form });
|
||||
return new Promise((res) => {
|
||||
vm.$once('ok', result => res({ canceled: false, result }));
|
||||
vm.$once('cancel', () => res({ canceled: true }));
|
||||
});
|
||||
},
|
||||
post(opts, cb) {
|
||||
if (!this.$store.getters.isSignedIn) return;
|
||||
const vm = this.new(PostFormDialog, opts);
|
||||
|
@ -210,11 +220,9 @@ os.init(async () => {
|
|||
}
|
||||
},
|
||||
router: router,
|
||||
render: createEl => createEl(App)
|
||||
render: createEl => createEl(deckmode ? Deck : App)
|
||||
});
|
||||
|
||||
os.app = app;
|
||||
|
||||
// マウント
|
||||
app.$mount('#app');
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
// TODO: このファイル消したい
|
||||
|
||||
import autobind from 'autobind-decorator';
|
||||
import Vue from 'vue';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
|
||||
import { apiUrl, version } from './config';
|
||||
|
@ -14,8 +13,6 @@ import store from './store';
|
|||
* Misskey Operating System
|
||||
*/
|
||||
export default class MiOS extends EventEmitter {
|
||||
public app: Vue;
|
||||
|
||||
public store: ReturnType<typeof store>;
|
||||
|
||||
/**
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<x-tutorial class="tutorial" v-if="$store.state.settings.tutorial != -1"/>
|
||||
|
||||
<x-post-form class="post-form _panel" fixed v-if="$store.state.device.showFixedPostForm"/>
|
||||
<x-timeline ref="tl" :key="src === 'list' ? `list:${list.id}` : src === 'antenna' ? `antenna:${antenna.id}` : src" :src="src" :list="list" :antenna="antenna" :sound="true" @before="before()" @after="after()" @queue="queueUpdated"/>
|
||||
<x-timeline ref="tl" :key="src === 'list' ? `list:${list.id}` : src === 'antenna' ? `antenna:${antenna.id}` : src" :src="src" :list="list ? list.id : null" :antenna="antenna ? antenna.id : null" :sound="true" @before="before()" @after="after()" @queue="queueUpdated"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -15,14 +15,15 @@
|
|||
|
||||
<mk-remote-caution v-if="note.user.host != null" :href="note.url || note.uri" style="margin-bottom: var(--margin)"/>
|
||||
<x-note :note="note" :key="note.id" :detail="true"/>
|
||||
<div v-if="error">
|
||||
<mk-error @retry="fetch()"/>
|
||||
</div>
|
||||
|
||||
<button class="_panel _button" v-if="hasPrev && !showPrev" @click="showPrev = true" style="margin: var(--margin) auto 0 auto;"><fa :icon="faChevronDown"/></button>
|
||||
<hr v-if="showPrev"/>
|
||||
<x-notes v-if="showPrev" ref="prev" :pagination="prev" style="margin-top: var(--margin);"/>
|
||||
</div>
|
||||
|
||||
<div v-if="error">
|
||||
<mk-error @retry="fetch()"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -51,6 +51,20 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faColumns"/> {{ $t('deck') }}</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="deckAlwaysShowMainColumn">
|
||||
{{ $t('_deck.alwaysShowMainColumn') }}
|
||||
</mk-switch>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<div>{{ $t('_deck.columnAlign') }}</div>
|
||||
<mk-radio v-model="deckColumnAlign" value="left">{{ $t('left') }}</mk-radio>
|
||||
<mk-radio v-model="deckColumnAlign" value="center">{{ $t('center') }}</mk-radio>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faCog"/> {{ $t('accessibility') }}</div>
|
||||
<div class="_content">
|
||||
|
@ -93,7 +107,7 @@
|
|||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faImage, faCog, faMusic, faPlay, faVolumeUp, faVolumeMute } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faImage, faCog, faMusic, faPlay, faVolumeUp, faVolumeMute, faColumns } from '@fortawesome/free-solid-svg-icons';
|
||||
import MkButton from '../../components/ui/button.vue';
|
||||
import MkSwitch from '../../components/ui/switch.vue';
|
||||
import MkSelect from '../../components/ui/select.vue';
|
||||
|
@ -145,7 +159,7 @@ export default Vue.extend({
|
|||
lang: localStorage.getItem('lang'),
|
||||
fontSize: localStorage.getItem('fontSize'),
|
||||
sounds,
|
||||
faImage, faCog, faMusic, faPlay, faVolumeUp, faVolumeMute
|
||||
faImage, faCog, faMusic, faPlay, faVolumeUp, faVolumeMute, faColumns
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -195,6 +209,16 @@ export default Vue.extend({
|
|||
set(value) { this.$store.commit('device/set', { key: 'fixedWidgetsPosition', value }); }
|
||||
},
|
||||
|
||||
deckAlwaysShowMainColumn: {
|
||||
get() { return this.$store.state.device.deckAlwaysShowMainColumn; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'deckAlwaysShowMainColumn', value }); }
|
||||
},
|
||||
|
||||
deckColumnAlign: {
|
||||
get() { return this.$store.state.device.deckColumnAlign; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'deckColumnAlign', value }); }
|
||||
},
|
||||
|
||||
sfxVolume: {
|
||||
get() { return this.$store.state.device.sfxVolume; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'sfxVolume', value: parseFloat(value, 10) }); }
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="kjeftjfm">
|
||||
<div class="kjeftjfm" v-size="[{ max: 500 }]">
|
||||
<div class="with">
|
||||
<button class="_button" @click="with_ = null" :class="{ active: with_ === null }">{{ $t('notes') }}</button>
|
||||
<button class="_button" @click="with_ = 'replies'" :class="{ active: with_ === 'replies' }">{{ $t('notesAndReplies') }}</button>
|
||||
|
@ -60,10 +60,6 @@ export default Vue.extend({
|
|||
display: flex;
|
||||
margin-bottom: var(--margin);
|
||||
|
||||
@media (max-width: 500px) {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
> button {
|
||||
flex: 1;
|
||||
padding: 11px 8px 8px 8px;
|
||||
|
@ -75,5 +71,11 @@ export default Vue.extend({
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.max-width_500px {
|
||||
> .with {
|
||||
font-size: 80%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="mk-user-page" v-if="user">
|
||||
<div class="mk-user-page" v-if="user" v-size="[{ max: 500 }]">
|
||||
<portal to="title" v-if="user"><mk-user-name :user="user" :nowrap="false" class="name"/></portal>
|
||||
<portal to="avatar" v-if="user"><mk-avatar class="avatar" :user="user" :disable-preview="true"/></portal>
|
||||
|
||||
|
@ -118,6 +118,7 @@ import MkContainer from '../../components/ui/container.vue';
|
|||
import MkRemoteCaution from '../../components/remote-caution.vue';
|
||||
import Progress from '../../scripts/loading';
|
||||
import parseAcct from '../../../misc/acct/parse';
|
||||
import { getScrollPosition } from '../../scripts/scroll';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
|
@ -168,12 +169,8 @@ export default Vue.extend({
|
|||
|
||||
mounted() {
|
||||
window.requestAnimationFrame(this.parallaxLoop);
|
||||
window.addEventListener('scroll', this.parallax, { passive: true });
|
||||
document.addEventListener('touchmove', this.parallax, { passive: true });
|
||||
this.$once('hook:beforeDestroy', () => {
|
||||
window.cancelAnimationFrame(this.parallaxAnimationId);
|
||||
window.removeEventListener('scroll', this.parallax);
|
||||
document.removeEventListener('touchmove', this.parallax);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -205,7 +202,7 @@ export default Vue.extend({
|
|||
const banner = this.$refs.banner as any;
|
||||
if (banner == null) return;
|
||||
|
||||
const top = window.scrollY;
|
||||
const top = getScrollPosition(this.$el);
|
||||
|
||||
if (top < 0) return;
|
||||
|
||||
|
@ -219,7 +216,6 @@ export default Vue.extend({
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.mk-user-page {
|
||||
|
||||
> .punished {
|
||||
font-size: 0.8em;
|
||||
padding: 16px;
|
||||
|
@ -237,10 +233,6 @@ export default Vue.extend({
|
|||
background-size: cover;
|
||||
background-position: center;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
> .banner {
|
||||
height: 100%;
|
||||
background-color: #4c5e6d;
|
||||
|
@ -257,10 +249,6 @@ export default Vue.extend({
|
|||
width: 100%;
|
||||
height: 78px;
|
||||
background: linear-gradient(transparent, rgba(#000, 0.7));
|
||||
|
||||
@media (max-width: 500px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
> .followed {
|
||||
|
@ -308,10 +296,6 @@ export default Vue.extend({
|
|||
box-sizing: border-box;
|
||||
color: #fff;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> .name {
|
||||
display: block;
|
||||
margin: 0;
|
||||
|
@ -343,10 +327,6 @@ export default Vue.extend({
|
|||
font-weight: bold;
|
||||
border-bottom: solid 1px var(--divider);
|
||||
|
||||
@media (max-width: 500px) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
> .bottom {
|
||||
> * {
|
||||
display: inline-block;
|
||||
|
@ -365,26 +345,12 @@ export default Vue.extend({
|
|||
width: 120px;
|
||||
height: 120px;
|
||||
box-shadow: 1px 1px 3px rgba(#000, 0.2);
|
||||
|
||||
@media (max-width: 500px) {
|
||||
top: 90px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 92px;
|
||||
height: 92px;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
> .description {
|
||||
padding: 24px 24px 24px 154px;
|
||||
font-size: 0.95em;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
> .empty {
|
||||
margin: 0;
|
||||
opacity: 0.5;
|
||||
|
@ -396,10 +362,6 @@ export default Vue.extend({
|
|||
font-size: 0.9em;
|
||||
border-top: solid 1px var(--divider);
|
||||
|
||||
@media (max-width: 500px) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
> .field {
|
||||
display: flex;
|
||||
padding: 0;
|
||||
|
@ -436,10 +398,6 @@ export default Vue.extend({
|
|||
padding: 24px;
|
||||
border-top: solid 1px var(--divider);
|
||||
|
||||
@media (max-width: 500px) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
> a {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
|
@ -473,5 +431,47 @@ export default Vue.extend({
|
|||
> .content {
|
||||
margin-bottom: var(--margin);
|
||||
}
|
||||
|
||||
&.max-width_500px {
|
||||
> .profile {
|
||||
> .banner-container {
|
||||
height: 140px;
|
||||
|
||||
> .fade {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> .title {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
> .title {
|
||||
display: block;
|
||||
}
|
||||
|
||||
> .avatar {
|
||||
top: 90px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 92px;
|
||||
height: 92px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
> .description {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
> .fields {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
> .status {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
26
src/client/scripts/form.ts
Normal file
26
src/client/scripts/form.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
export type FormItem = {
|
||||
label?: string;
|
||||
type: 'string';
|
||||
default: string | null;
|
||||
hidden?: boolean;
|
||||
multiline?: boolean;
|
||||
} | {
|
||||
label?: string;
|
||||
type: 'number';
|
||||
default: number | null;
|
||||
hidden?: boolean;
|
||||
step?: number;
|
||||
} | {
|
||||
label?: string;
|
||||
type: 'boolean';
|
||||
default: boolean | null;
|
||||
hidden?: boolean;
|
||||
} | {
|
||||
label?: string;
|
||||
type: 'enum';
|
||||
default: string | null;
|
||||
hidden?: boolean;
|
||||
enum: string[];
|
||||
};
|
||||
|
||||
export type Form = Record<string, FormItem>;
|
|
@ -13,7 +13,7 @@ export default (opts) => ({
|
|||
moreFetching: false,
|
||||
inited: false,
|
||||
more: false,
|
||||
backed: false,
|
||||
backed: false, // 遡り中か否か
|
||||
isBackTop: false,
|
||||
ilObserver: new IntersectionObserver(
|
||||
(entries) => entries.some((entry) => entry.isIntersecting)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
export function getScrollContainer(el: Element | null): Element | null {
|
||||
if (el == null || el.tagName === 'BODY') return null;
|
||||
const style = window.getComputedStyle(el);
|
||||
if (style.getPropertyValue('overflow') === 'auto') {
|
||||
const overflow = window.getComputedStyle(el).getPropertyValue('overflow');
|
||||
if (overflow.endsWith('auto')) { // xとyを個別に指定している場合、hidden auto みたいな値になる
|
||||
return el;
|
||||
} else {
|
||||
return getScrollContainer(el.parentElement);
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import Vuex from 'vuex';
|
||||
import createPersistedState from 'vuex-persistedstate';
|
||||
import * as nestedProperty from 'nested-property';
|
||||
import { faTerminal, faHashtag, faBroadcastTower, faFireAlt, faSearch, faStar, faAt, faListUl, faUserClock, faUsers, faCloud, faGamepad, faFileAlt, faSatellite, faDoorClosed } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faTerminal, faHashtag, faBroadcastTower, faFireAlt, faSearch, faStar, faAt, faListUl, faUserClock, faUsers, faCloud, faGamepad, faFileAlt, faSatellite, faDoorClosed, faColumns } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faBell, faEnvelope, faComments } from '@fortawesome/free-regular-svg-icons';
|
||||
import { apiUrl } from './config';
|
||||
import { apiUrl, deckmode } from './config';
|
||||
import { erase } from '../prelude/array';
|
||||
|
||||
export const defaultSettings = {
|
||||
tutorial: 0,
|
||||
|
@ -35,7 +36,13 @@ export const defaultDeviceUserSettings = {
|
|||
'explore',
|
||||
'announcements',
|
||||
'search',
|
||||
'-',
|
||||
'deck',
|
||||
],
|
||||
deck: {
|
||||
columns: [],
|
||||
layout: [],
|
||||
},
|
||||
};
|
||||
|
||||
export const defaultDeviceSettings = {
|
||||
|
@ -50,6 +57,7 @@ export const defaultDeviceSettings = {
|
|||
darkTheme: '8c539dc1-0fab-4d47-9194-39c508e9bfe1',
|
||||
lightTheme: '4eea646f-7afa-4645-83e9-83af0333cd37',
|
||||
darkMode: false,
|
||||
deckMode: false,
|
||||
syncDeviceDarkMode: true,
|
||||
animation: true,
|
||||
animatedMfm: true,
|
||||
|
@ -60,6 +68,9 @@ export const defaultDeviceSettings = {
|
|||
fixedWidgetsPosition: false,
|
||||
roomGraphicsQuality: 'medium',
|
||||
roomUseOrthographicCamera: true,
|
||||
deckColumnAlign: 'left',
|
||||
deckAlwaysShowMainColumn: true,
|
||||
deckMainColumnPlace: 'left',
|
||||
sfxVolume: 0.3,
|
||||
sfxNote: 'syuilo/down',
|
||||
sfxNoteMy: 'syuilo/up',
|
||||
|
@ -197,6 +208,14 @@ export default () => new Vuex.Store({
|
|||
get show() { return getters.isSignedIn; },
|
||||
get to() { return `/@${state.i.username}/room`; },
|
||||
},
|
||||
deck: {
|
||||
title: deckmode ? 'undeck' : 'deck',
|
||||
icon: faColumns,
|
||||
action: () => {
|
||||
localStorage.setItem('deckmode', (!deckmode).toString());
|
||||
location.reload();
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
|
@ -399,6 +418,137 @@ export default () => new Vuex.Store({
|
|||
w.data = x.data;
|
||||
}
|
||||
},
|
||||
|
||||
//#region Deck
|
||||
addDeckColumn(state, column) {
|
||||
if (column.name == undefined) column.name = null;
|
||||
state.deck.columns.push(column);
|
||||
state.deck.layout.push([column.id]);
|
||||
},
|
||||
|
||||
removeDeckColumn(state, id) {
|
||||
state.deck.columns = state.deck.columns.filter(c => c.id != id);
|
||||
state.deck.layout = state.deck.layout.map(ids => erase(id, ids));
|
||||
state.deck.layout = state.deck.layout.filter(ids => ids.length > 0);
|
||||
},
|
||||
|
||||
swapDeckColumn(state, x) {
|
||||
const a = x.a;
|
||||
const b = x.b;
|
||||
const aX = state.deck.layout.findIndex(ids => ids.indexOf(a) != -1);
|
||||
const aY = state.deck.layout[aX].findIndex(id => id == a);
|
||||
const bX = state.deck.layout.findIndex(ids => ids.indexOf(b) != -1);
|
||||
const bY = state.deck.layout[bX].findIndex(id => id == b);
|
||||
state.deck.layout[aX][aY] = b;
|
||||
state.deck.layout[bX][bY] = a;
|
||||
},
|
||||
|
||||
swapLeftDeckColumn(state, id) {
|
||||
state.deck.layout.some((ids, i) => {
|
||||
if (ids.indexOf(id) != -1) {
|
||||
const left = state.deck.layout[i - 1];
|
||||
if (left) {
|
||||
// https://vuejs.org/v2/guide/list.html#Caveats
|
||||
//state.deck.layout[i - 1] = state.deck.layout[i];
|
||||
//state.deck.layout[i] = left;
|
||||
state.deck.layout.splice(i - 1, 1, state.deck.layout[i]);
|
||||
state.deck.layout.splice(i, 1, left);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
swapRightDeckColumn(state, id) {
|
||||
state.deck.layout.some((ids, i) => {
|
||||
if (ids.indexOf(id) != -1) {
|
||||
const right = state.deck.layout[i + 1];
|
||||
if (right) {
|
||||
// https://vuejs.org/v2/guide/list.html#Caveats
|
||||
//state.deck.layout[i + 1] = state.deck.layout[i];
|
||||
//state.deck.layout[i] = right;
|
||||
state.deck.layout.splice(i + 1, 1, state.deck.layout[i]);
|
||||
state.deck.layout.splice(i, 1, right);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
swapUpDeckColumn(state, id) {
|
||||
const ids = state.deck.layout.find(ids => ids.indexOf(id) != -1);
|
||||
ids.some((x, i) => {
|
||||
if (x == id) {
|
||||
const up = ids[i - 1];
|
||||
if (up) {
|
||||
// https://vuejs.org/v2/guide/list.html#Caveats
|
||||
//ids[i - 1] = id;
|
||||
//ids[i] = up;
|
||||
ids.splice(i - 1, 1, id);
|
||||
ids.splice(i, 1, up);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
swapDownDeckColumn(state, id) {
|
||||
const ids = state.deck.layout.find(ids => ids.indexOf(id) != -1);
|
||||
ids.some((x, i) => {
|
||||
if (x == id) {
|
||||
const down = ids[i + 1];
|
||||
if (down) {
|
||||
// https://vuejs.org/v2/guide/list.html#Caveats
|
||||
//ids[i + 1] = id;
|
||||
//ids[i] = down;
|
||||
ids.splice(i + 1, 1, id);
|
||||
ids.splice(i, 1, down);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
stackLeftDeckColumn(state, id) {
|
||||
const i = state.deck.layout.findIndex(ids => ids.indexOf(id) != -1);
|
||||
state.deck.layout = state.deck.layout.map(ids => erase(id, ids));
|
||||
const left = state.deck.layout[i - 1];
|
||||
if (left) state.deck.layout[i - 1].push(id);
|
||||
state.deck.layout = state.deck.layout.filter(ids => ids.length > 0);
|
||||
},
|
||||
|
||||
popRightDeckColumn(state, id) {
|
||||
const i = state.deck.layout.findIndex(ids => ids.indexOf(id) != -1);
|
||||
state.deck.layout = state.deck.layout.map(ids => erase(id, ids));
|
||||
state.deck.layout.splice(i + 1, 0, [id]);
|
||||
state.deck.layout = state.deck.layout.filter(ids => ids.length > 0);
|
||||
},
|
||||
|
||||
addDeckWidget(state, x) {
|
||||
const column = state.deck.columns.find(c => c.id == x.id);
|
||||
if (column == null) return;
|
||||
if (column.widgets == null) column.widgets = [];
|
||||
column.widgets.unshift(x.widget);
|
||||
},
|
||||
|
||||
removeDeckWidget(state, x) {
|
||||
const column = state.deck.columns.find(c => c.id == x.id);
|
||||
if (column == null) return;
|
||||
column.widgets = column.widgets.filter(w => w.id != x.widget.id);
|
||||
},
|
||||
|
||||
renameDeckColumn(state, x) {
|
||||
const column = state.deck.columns.find(c => c.id == x.id);
|
||||
if (column == null) return;
|
||||
column.name = x.name;
|
||||
},
|
||||
|
||||
updateDeckColumn(state, x) {
|
||||
let column = state.deck.columns.find(c => c.id == x.id);
|
||||
if (column == null) return;
|
||||
column = x;
|
||||
},
|
||||
//#endregion
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
:root {
|
||||
--radius: 8px;
|
||||
--marginFull: 16px;
|
||||
--marginHalf: 8px;
|
||||
--marginHalf: 10px;
|
||||
|
||||
--margin: var(--marginFull);
|
||||
|
||||
|
@ -25,7 +25,6 @@ html {
|
|||
background-position: center;
|
||||
color: var(--fg);
|
||||
overflow: auto;
|
||||
overflow-y: scroll;
|
||||
|
||||
&, * {
|
||||
scrollbar-color: var(--scrollbarHandle) var(--panel);
|
||||
|
@ -278,13 +277,14 @@ hr {
|
|||
|
||||
._panel {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background: var(--panel);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 0 0 1px var(--panelBorder);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
._widget ._list_ ._panel {
|
||||
._close_ ._list_ > * {
|
||||
box-shadow: 0 1px 0 0 var(--divider), 0 -1px 0 0 var(--divider);
|
||||
border-radius: 0;
|
||||
margin: 0 !important;
|
||||
|
@ -348,31 +348,6 @@ hr {
|
|||
& + ._content {
|
||||
border-top: solid 1px var(--divider);
|
||||
}
|
||||
|
||||
&._list {
|
||||
padding: 16px;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
._listItem {
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--radius);
|
||||
|
||||
@media (max-width: 500px) {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--listItemHoverBg);
|
||||
}
|
||||
|
||||
> * {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> ._footer {
|
||||
|
@ -385,6 +360,21 @@ hr {
|
|||
}
|
||||
}
|
||||
|
||||
._narrow_ ._card {
|
||||
> ._title {
|
||||
padding: 16px;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
> ._content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
> ._footer {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
._fullinfo {
|
||||
padding: 64px 32px;
|
||||
text-align: center;
|
||||
|
|
|
@ -26,8 +26,8 @@
|
|||
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
|
||||
panelBorder: 'rgba(0, 0, 0, 0)',
|
||||
shadow: 'rgba(0, 0, 0, 0.1)',
|
||||
header: 'rgba(20, 20, 20, 0.75)',
|
||||
navBg: '@panel',
|
||||
header: ':alpha<0.7<@bg',
|
||||
navBg: '@bg',
|
||||
navFg: '@fg',
|
||||
navHoverFg: ':lighten<17<@fg',
|
||||
navActive: '@accent',
|
||||
|
@ -58,6 +58,7 @@
|
|||
wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
|
||||
badge: '#31b1ce',
|
||||
messageBg: ':lighten<5<@bg',
|
||||
deckColumnBorder: ':lighten<10<@panel',
|
||||
X1: ':alpha<0<@bg',
|
||||
X2: ':darken<2<@panel',
|
||||
X3: 'rgba(255, 255, 255, 0.05)',
|
||||
|
|
|
@ -26,8 +26,8 @@
|
|||
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
|
||||
panelBorder: 'rgba(0, 0, 0, 0)',
|
||||
shadow: 'rgba(0, 0, 0, 0.1)',
|
||||
header: 'rgba(255, 255, 255, 0.75)',
|
||||
navBg: '@panel',
|
||||
header: ':alpha<0.7<@bg',
|
||||
navBg: '@bg',
|
||||
navFg: '@fg',
|
||||
navHoverFg: ':darken<17<@fg',
|
||||
navActive: '@accent',
|
||||
|
@ -58,6 +58,7 @@
|
|||
wallpaperOverlay: 'rgba(255, 255, 255, 0.5)',
|
||||
badge: '#31b1ce',
|
||||
messageBg: '@panel',
|
||||
deckColumnBorder: ':darken<20<@panel',
|
||||
X1: ':alpha<0<@bg',
|
||||
X2: ':darken<2<@panel',
|
||||
X3: 'rgba(0, 0, 0, 0.05)',
|
||||
|
|
|
@ -13,5 +13,6 @@
|
|||
panelHeaderDivider: '@divider',
|
||||
panelBorder: '@divider',
|
||||
messageBg: '#1d1d1d',
|
||||
deckColumnBorder: '@divider',
|
||||
},
|
||||
}
|
||||
|
|
|
@ -10,9 +10,11 @@
|
|||
accent: 'rgb(206, 147, 191)',
|
||||
bg: 'rgb(253, 242, 243)',
|
||||
fg: 'rgb(161, 139, 146)',
|
||||
divider: '#ece7e7',
|
||||
renote: '@accent',
|
||||
link: '@accent',
|
||||
mention: '@accent',
|
||||
hashtag: '@accent',
|
||||
panelHeaderDivider: '@divider',
|
||||
},
|
||||
}
|
||||
|
|
|
@ -11,5 +11,6 @@
|
|||
bg: 'rgb(220, 229, 232)',
|
||||
fg: 'rgb(139, 153, 161)',
|
||||
renote: '@accent',
|
||||
panelHeaderDivider: '@divider',
|
||||
},
|
||||
}
|
||||
|
|
|
@ -8,7 +8,11 @@
|
|||
base: 'light',
|
||||
|
||||
props: {
|
||||
bg: '#f2f2f2',
|
||||
header: ':alpha<0.7<@bg',
|
||||
navBg: '@bg',
|
||||
panelHeaderDivider: '@divider',
|
||||
messageBg: '#dedede',
|
||||
deckColumnBorder: '#cccccc',
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,18 +1,16 @@
|
|||
<template>
|
||||
<div>
|
||||
<mk-container :show-header="props.design === 0" :naked="props.design === 2">
|
||||
<template #header><fa :icon="faChartBar"/>{{ $t('_widgets.activity') }}</template>
|
||||
<template #func><button @click="toggleView()" class="_button"><fa :icon="faSort"/></button></template>
|
||||
<mk-container :show-header="props.showHeader" :naked="props.transparent">
|
||||
<template #header><fa :icon="faChartBar"/>{{ $t('_widgets.activity') }}</template>
|
||||
<template #func><button @click="toggleView()" class="_button"><fa :icon="faSort"/></button></template>
|
||||
|
||||
<div>
|
||||
<mk-loading v-if="fetching"/>
|
||||
<template v-else>
|
||||
<x-calendar v-show="props.view === 0" :data="[].concat(activity)"/>
|
||||
<x-chart v-show="props.view === 1" :data="[].concat(activity)"/>
|
||||
</template>
|
||||
</div>
|
||||
</mk-container>
|
||||
</div>
|
||||
<div>
|
||||
<mk-loading v-if="fetching"/>
|
||||
<template v-else>
|
||||
<x-calendar v-show="props.view === 0" :data="[].concat(activity)"/>
|
||||
<x-chart v-show="props.view === 1" :data="[].concat(activity)"/>
|
||||
</template>
|
||||
</div>
|
||||
</mk-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -25,8 +23,19 @@ import XChart from './activity.chart.vue';
|
|||
export default define({
|
||||
name: 'activity',
|
||||
props: () => ({
|
||||
design: 0,
|
||||
view: 0
|
||||
showHeader: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
transparent: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
view: {
|
||||
type: 'number',
|
||||
default: 0,
|
||||
hidden: true,
|
||||
},
|
||||
})
|
||||
}).extend({
|
||||
components: {
|
||||
|
@ -57,14 +66,6 @@ export default define({
|
|||
});
|
||||
},
|
||||
methods: {
|
||||
func() {
|
||||
if (this.props.design === 2) {
|
||||
this.props.design = 0;
|
||||
} else {
|
||||
this.props.design++;
|
||||
}
|
||||
this.save();
|
||||
},
|
||||
toggleView() {
|
||||
if (this.props.view === 1) {
|
||||
this.props.view = 0;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="mkw-calendar" :class="{ _panel: props.design === 0 }">
|
||||
<div class="mkw-calendar" :class="{ _panel: !props.transparent }">
|
||||
<div class="calendar" :data-is-holiday="isHoliday">
|
||||
<p class="month-and-year">
|
||||
<span class="year">{{ $t('yearX', { year }) }}</span>
|
||||
|
@ -37,7 +37,10 @@ import define from './define';
|
|||
export default define({
|
||||
name: 'calendar',
|
||||
props: () => ({
|
||||
design: 0
|
||||
transparent: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
}).extend({
|
||||
data() {
|
||||
|
@ -62,14 +65,6 @@ export default define({
|
|||
clearInterval(this.clock);
|
||||
},
|
||||
methods: {
|
||||
func() {
|
||||
if (this.props.design === 2) {
|
||||
this.props.design = 0;
|
||||
} else {
|
||||
this.props.design++;
|
||||
}
|
||||
this.save();
|
||||
},
|
||||
tick() {
|
||||
const now = new Date();
|
||||
const nd = now.getDate();
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
<template>
|
||||
<div>
|
||||
<mk-container :naked="props.style % 2 === 0" :show-header="false">
|
||||
<div class="vubelbmv">
|
||||
<mk-analog-clock class="clock" :smooth="props.style < 2"/>
|
||||
</div>
|
||||
</mk-container>
|
||||
</div>
|
||||
<mk-container :naked="props.transparent" :show-header="false">
|
||||
<div class="vubelbmv">
|
||||
<mk-analog-clock class="clock"/>
|
||||
</div>
|
||||
</mk-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -16,19 +14,16 @@ import MkAnalogClock from '../components/analog-clock.vue';
|
|||
export default define({
|
||||
name: 'clock',
|
||||
props: () => ({
|
||||
style: 0
|
||||
transparent: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
}).extend({
|
||||
components: {
|
||||
MkContainer,
|
||||
MkAnalogClock
|
||||
},
|
||||
methods: {
|
||||
func() {
|
||||
this.props.style = (this.props.style + 1) % 4;
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Vue from 'vue';
|
||||
import { Form } from '../scripts/form';
|
||||
|
||||
export default function <T extends object>(data: {
|
||||
export default function <T extends Form>(data: {
|
||||
name: string;
|
||||
props?: () => T;
|
||||
}) {
|
||||
|
@ -15,22 +16,22 @@ export default function <T extends object>(data: {
|
|||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
bakedOldProps: null
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
id(): string {
|
||||
return this.widget.id;
|
||||
},
|
||||
|
||||
props(): T {
|
||||
props(): Record<string, any> {
|
||||
return this.widget.data;
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
bakedOldProps: null
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.mergeProps();
|
||||
|
||||
|
@ -45,11 +46,26 @@ export default function <T extends object>(data: {
|
|||
const defaultProps = data.props();
|
||||
for (const prop of Object.keys(defaultProps)) {
|
||||
if (this.props.hasOwnProperty(prop)) continue;
|
||||
Vue.set(this.props, prop, defaultProps[prop]);
|
||||
Vue.set(this.props, prop, defaultProps[prop].default);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async setting() {
|
||||
const form = data.props();
|
||||
for (const item of Object.keys(form)) {
|
||||
form[item].default = this.props[item];
|
||||
}
|
||||
const { canceled, result } = await this.$root.form(data.name, form);
|
||||
if (canceled) return;
|
||||
|
||||
for (const key of Object.keys(result)) {
|
||||
Vue.set(this.props, key, result[key]);
|
||||
}
|
||||
|
||||
this.save();
|
||||
},
|
||||
|
||||
save() {
|
||||
this.$store.commit('deviceUser/updateWidget', this.widget);
|
||||
}
|
||||
|
|
75
src/client/widgets/digital-clock.vue
Normal file
75
src/client/widgets/digital-clock.vue
Normal file
|
@ -0,0 +1,75 @@
|
|||
<template>
|
||||
<div class="mkw-digitalClock" :class="{ _panel: !props.transparent }" :style="{ fontSize: `${props.fontSize}em` }">
|
||||
<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 :style="{ visibility: showColon ? 'visible' : 'hidden' }" v-if="props.showMs">:</span>
|
||||
<span v-text="ms" v-if="props.showMs"></span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import define from './define';
|
||||
|
||||
export default define({
|
||||
name: 'digitalClock',
|
||||
props: () => ({
|
||||
transparent: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
fontSize: {
|
||||
type: 'number',
|
||||
default: 1.5,
|
||||
step: 0.1,
|
||||
},
|
||||
showMs: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
}).extend({
|
||||
data() {
|
||||
return {
|
||||
clock: null,
|
||||
hh: null,
|
||||
mm: null,
|
||||
ss: null,
|
||||
ms: null,
|
||||
showColon: true,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.tick();
|
||||
this.$watch('props.showMs', () => {
|
||||
if (this.clock) clearInterval(this.clock);
|
||||
this.clock = setInterval(this.tick, this.props.showMs ? 10 : 1000);
|
||||
}, { immediate: true });
|
||||
},
|
||||
beforeDestroy() {
|
||||
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.ms = Math.floor(now.getMilliseconds() / 10).toString().padStart(2, '0');
|
||||
this.showColon = now.getSeconds() % 2 === 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mkw-digitalClock {
|
||||
padding: 16px 0;
|
||||
font-family: Lucida Console, Courier, monospace;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
|
@ -10,3 +10,17 @@ Vue.component('mkw-trends', () => import('./trends.vue').then(m => m.default));
|
|||
Vue.component('mkw-clock', () => import('./clock.vue').then(m => m.default));
|
||||
Vue.component('mkw-activity', () => import('./activity.vue').then(m => m.default));
|
||||
Vue.component('mkw-photos', () => import('./photos.vue').then(m => m.default));
|
||||
Vue.component('mkw-digitalClock', () => import('./digital-clock.vue').then(m => m.default));
|
||||
|
||||
export const widgets = [
|
||||
'memo',
|
||||
'notifications',
|
||||
'timeline',
|
||||
'calendar',
|
||||
'rss',
|
||||
'trends',
|
||||
'clock',
|
||||
'activity',
|
||||
'photos',
|
||||
'digitalClock',
|
||||
];
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
<template>
|
||||
<div>
|
||||
<mk-container :show-header="!props.compact">
|
||||
<template #header><fa :icon="faStickyNote"/>{{ $t('_widgets.memo') }}</template>
|
||||
<mk-container :show-header="props.showHeader">
|
||||
<template #header><fa :icon="faStickyNote"/>{{ $t('_widgets.memo') }}</template>
|
||||
|
||||
<div class="otgbylcu">
|
||||
<textarea v-model="text" :placeholder="$t('placeholder')" @input="onChange"></textarea>
|
||||
<button @click="saveMemo" :disabled="!changed" class="_buttonPrimary">{{ $t('save') }}</button>
|
||||
</div>
|
||||
</mk-container>
|
||||
</div>
|
||||
<div class="otgbylcu">
|
||||
<textarea v-model="text" :placeholder="$t('placeholder')" @input="onChange"></textarea>
|
||||
<button @click="saveMemo" :disabled="!changed" class="_buttonPrimary">{{ $t('save') }}</button>
|
||||
</div>
|
||||
</mk-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -19,10 +17,12 @@ import define from './define';
|
|||
export default define({
|
||||
name: 'memo',
|
||||
props: () => ({
|
||||
compact: false
|
||||
showHeader: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
}).extend({
|
||||
|
||||
components: {
|
||||
MkContainer
|
||||
},
|
||||
|
@ -45,11 +45,6 @@ export default define({
|
|||
},
|
||||
|
||||
methods: {
|
||||
func() {
|
||||
this.props.compact = !this.props.compact;
|
||||
this.save();
|
||||
},
|
||||
|
||||
onChange() {
|
||||
this.changed = true;
|
||||
clearTimeout(this.timeoutId);
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
<template>
|
||||
<div class="mkw-notifications" :style="`flex-basis: calc(${basis}% - var(--margin)); height: ${previewHeight}px;`">
|
||||
<mk-container :show-header="!props.compact" class="container">
|
||||
<template #header><fa :icon="faBell"/>{{ $t('notifications') }}</template>
|
||||
<mk-container :style="`height: ${props.height}px;`" :show-header="props.showHeader" :scrollable="true">
|
||||
<template #header><fa :icon="faBell"/>{{ $t('notifications') }}</template>
|
||||
|
||||
<div>
|
||||
<x-notifications/>
|
||||
</div>
|
||||
</mk-container>
|
||||
</div>
|
||||
<div>
|
||||
<x-notifications/>
|
||||
</div>
|
||||
</mk-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -16,17 +14,19 @@ import MkContainer from '../components/ui/container.vue';
|
|||
import XNotifications from '../components/notifications.vue';
|
||||
import define from './define';
|
||||
|
||||
const basisSteps = [25, 50, 75, 100]
|
||||
const previewHeights = [200, 300, 400, 500]
|
||||
|
||||
export default define({
|
||||
name: 'notifications',
|
||||
props: () => ({
|
||||
compact: false,
|
||||
basisStep: 0
|
||||
showHeader: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
height: {
|
||||
type: 'number',
|
||||
default: 300,
|
||||
},
|
||||
})
|
||||
}).extend({
|
||||
|
||||
components: {
|
||||
MkContainer,
|
||||
XNotifications,
|
||||
|
@ -37,47 +37,5 @@ export default define({
|
|||
faBell
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
basis(): number {
|
||||
return basisSteps[this.props.basisStep] || 25
|
||||
},
|
||||
|
||||
previewHeight(): number {
|
||||
return previewHeights[this.props.basisStep] || 200
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
func() {
|
||||
if (this.props.basisStep === basisSteps.length - 1) {
|
||||
this.props.basisStep = 0
|
||||
this.props.compact = !this.props.compact;
|
||||
} else {
|
||||
this.props.basisStep += 1
|
||||
}
|
||||
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.mkw-notifications {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 0;
|
||||
min-height: 0; // https://www.gwtcenter.com/min-height-required-on-firefox-flexbox
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
> div {
|
||||
overflow: auto;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,19 +1,17 @@
|
|||
<template>
|
||||
<div>
|
||||
<mk-container :show-header="props.design === 0" :naked="props.design === 2" :class="$style.root" :data-melt="props.design === 2">
|
||||
<template #header><fa :icon="faCamera"/>{{ $t('_widgets.photos') }}</template>
|
||||
<mk-container :show-header="props.showHeader" :naked="props.transparent" :class="$style.root" :data-transparent="props.transparent">
|
||||
<template #header><fa :icon="faCamera"/>{{ $t('_widgets.photos') }}</template>
|
||||
|
||||
<div class="">
|
||||
<mk-loading v-if="fetching"/>
|
||||
<div v-else :class="$style.stream">
|
||||
<div v-for="(image, i) in images" :key="i"
|
||||
:class="$style.img"
|
||||
:style="`background-image: url(${thumbnail(image)})`"
|
||||
></div>
|
||||
</div>
|
||||
<div class="">
|
||||
<mk-loading v-if="fetching"/>
|
||||
<div v-else :class="$style.stream">
|
||||
<div v-for="(image, i) in images" :key="i"
|
||||
:class="$style.img"
|
||||
:style="`background-image: url(${thumbnail(image)})`"
|
||||
></div>
|
||||
</div>
|
||||
</mk-container>
|
||||
</div>
|
||||
</div>
|
||||
</mk-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -25,7 +23,14 @@ import { getStaticImageUrl } from '../scripts/get-static-image-url';
|
|||
export default define({
|
||||
name: 'photos',
|
||||
props: () => ({
|
||||
design: 0,
|
||||
showHeader: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
transparent: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
}).extend({
|
||||
components: {
|
||||
|
@ -63,15 +68,6 @@ export default define({
|
|||
}
|
||||
},
|
||||
|
||||
func() {
|
||||
if (this.props.design === 2) {
|
||||
this.props.design = 0;
|
||||
} else {
|
||||
this.props.design++;
|
||||
}
|
||||
this.save();
|
||||
},
|
||||
|
||||
thumbnail(image: any): string {
|
||||
return this.$store.state.device.disableShowingAnimatedImages
|
||||
? getStaticImageUrl(image.thumbnailUrl)
|
||||
|
@ -82,7 +78,7 @@ export default define({
|
|||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root[data-melt] {
|
||||
.root[data-transparent] {
|
||||
.stream {
|
||||
padding: 0;
|
||||
}
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
<template>
|
||||
<div>
|
||||
<mk-container :show-header="!props.compact">
|
||||
<template #header><fa :icon="faRssSquare"/>RSS</template>
|
||||
<template #func><button class="_button" @click="setting"><fa :icon="faCog"/></button></template>
|
||||
<mk-container :show-header="props.showHeader">
|
||||
<template #header><fa :icon="faRssSquare"/>RSS</template>
|
||||
<template #func><button class="_button" @click="setting"><fa :icon="faCog"/></button></template>
|
||||
|
||||
<div class="ekmkgxbj">
|
||||
<mk-loading v-if="fetching"/>
|
||||
<div class="feed" v-else>
|
||||
<a v-for="item in items" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a>
|
||||
</div>
|
||||
<div class="ekmkgxbj">
|
||||
<mk-loading v-if="fetching"/>
|
||||
<div class="feed" v-else>
|
||||
<a v-for="item in items" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a>
|
||||
</div>
|
||||
</mk-container>
|
||||
</div>
|
||||
</div>
|
||||
</mk-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -22,8 +20,14 @@ import define from './define';
|
|||
export default define({
|
||||
name: 'rss',
|
||||
props: () => ({
|
||||
compact: false,
|
||||
url: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews'
|
||||
showHeader: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews',
|
||||
},
|
||||
})
|
||||
}).extend({
|
||||
components: {
|
||||
|
@ -40,15 +44,12 @@ export default define({
|
|||
mounted() {
|
||||
this.fetch();
|
||||
this.clock = setInterval(this.fetch, 60000);
|
||||
this.$watch('props.url', this.fetch);
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.clock);
|
||||
},
|
||||
methods: {
|
||||
func() {
|
||||
this.props.compact = !this.props.compact;
|
||||
this.save();
|
||||
},
|
||||
fetch() {
|
||||
fetch(`https://api.rss2json.com/v1/api.json?rss_url=${this.props.url}`, {
|
||||
}).then(res => {
|
||||
|
@ -58,20 +59,6 @@ export default define({
|
|||
});
|
||||
});
|
||||
},
|
||||
setting() {
|
||||
this.$root.dialog({
|
||||
title: 'URL',
|
||||
input: {
|
||||
type: 'url',
|
||||
default: this.props.url
|
||||
}
|
||||
}).then(({ canceled, result: url }) => {
|
||||
if (canceled) return;
|
||||
this.props.url = url;
|
||||
this.save();
|
||||
this.fetch();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,24 +1,22 @@
|
|||
<template>
|
||||
<div class="mkw-timeline" :style="`flex-basis: calc(${basis}% - var(--margin)); height: ${previewHeight}px;`">
|
||||
<mk-container :show-header="!props.compact" class="container">
|
||||
<template #header>
|
||||
<button @click="choose" class="_button">
|
||||
<fa v-if="props.src === 'home'" :icon="faHome"/>
|
||||
<fa v-if="props.src === 'local'" :icon="faComments"/>
|
||||
<fa v-if="props.src === 'social'" :icon="faShareAlt"/>
|
||||
<fa v-if="props.src === 'global'" :icon="faGlobe"/>
|
||||
<fa v-if="props.src === 'list'" :icon="faListUl"/>
|
||||
<fa v-if="props.src === 'antenna'" :icon="faSatellite"/>
|
||||
<span style="margin-left: 8px;">{{ props.src === 'list' ? props.list.name : props.src === 'antenna' ? props.antenna.name : $t('_timelines.' + props.src) }}</span>
|
||||
<fa :icon="menuOpened ? faAngleUp : faAngleDown" style="margin-left: 8px;"/>
|
||||
</button>
|
||||
</template>
|
||||
<mk-container :show-header="props.showHeader" :style="`height: ${props.height}px;`" :scrollable="true">
|
||||
<template #header>
|
||||
<button @click="choose" class="_button">
|
||||
<fa v-if="props.src === 'home'" :icon="faHome"/>
|
||||
<fa v-if="props.src === 'local'" :icon="faComments"/>
|
||||
<fa v-if="props.src === 'social'" :icon="faShareAlt"/>
|
||||
<fa v-if="props.src === 'global'" :icon="faGlobe"/>
|
||||
<fa v-if="props.src === 'list'" :icon="faListUl"/>
|
||||
<fa v-if="props.src === 'antenna'" :icon="faSatellite"/>
|
||||
<span style="margin-left: 8px;">{{ props.src === 'list' ? props.list.name : props.src === 'antenna' ? props.antenna.name : $t('_timelines.' + props.src) }}</span>
|
||||
<fa :icon="menuOpened ? faAngleUp : faAngleDown" style="margin-left: 8px;"/>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<x-timeline :key="props.src === 'list' ? `list:${props.list.id}` : props.src === 'antenna' ? `antenna:${props.antenna.id}` : props.src" :src="props.src" :list="props.list" :antenna="props.antenna"/>
|
||||
</div>
|
||||
</mk-container>
|
||||
</div>
|
||||
<div>
|
||||
<x-timeline :key="props.src === 'list' ? `list:${props.list.id}` : props.src === 'antenna' ? `antenna:${props.antenna.id}` : props.src" :src="props.src" :list="props.list ? props.list.id : null" :antenna="props.antenna ? props.antenna.id : null"/>
|
||||
</div>
|
||||
</mk-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -28,19 +26,25 @@ import MkContainer from '../components/ui/container.vue';
|
|||
import XTimeline from '../components/timeline.vue';
|
||||
import define from './define';
|
||||
|
||||
const basisSteps = [25, 50, 75, 100]
|
||||
const previewHeights = [200, 300, 400, 500]
|
||||
|
||||
export default define({
|
||||
name: 'timeline',
|
||||
props: () => ({
|
||||
src: 'home',
|
||||
list: null,
|
||||
compact: false,
|
||||
basisStep: 0
|
||||
showHeader: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
src: {
|
||||
type: 'string',
|
||||
default: 'home',
|
||||
hidden: true,
|
||||
},
|
||||
list: {
|
||||
type: 'object',
|
||||
default: null,
|
||||
hidden: true,
|
||||
},
|
||||
})
|
||||
}).extend({
|
||||
|
||||
components: {
|
||||
MkContainer,
|
||||
XTimeline,
|
||||
|
@ -53,28 +57,7 @@ export default define({
|
|||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
basis(): number {
|
||||
return basisSteps[this.props.basisStep] || 25
|
||||
},
|
||||
|
||||
previewHeight(): number {
|
||||
return previewHeights[this.props.basisStep] || 200
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
func() {
|
||||
if (this.props.basisStep === basisSteps.length - 1) {
|
||||
this.props.basisStep = 0
|
||||
this.props.compact = !this.props.compact;
|
||||
} else {
|
||||
this.props.basisStep += 1
|
||||
}
|
||||
|
||||
this.save();
|
||||
},
|
||||
|
||||
async choose(ev) {
|
||||
this.menuOpened = true;
|
||||
const [antennas, lists] = await Promise.all([
|
||||
|
@ -129,22 +112,3 @@ export default define({
|
|||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.mkw-timeline {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 0;
|
||||
min-height: 0; // https://www.gwtcenter.com/min-height-required-on-firefox-flexbox
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
> div {
|
||||
overflow: auto;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,22 +1,20 @@
|
|||
<template>
|
||||
<div>
|
||||
<mk-container :show-header="!props.compact">
|
||||
<template #header><fa :icon="faHashtag"/>{{ $t('_widgets.trends') }}</template>
|
||||
<mk-container :show-header="props.showHeader">
|
||||
<template #header><fa :icon="faHashtag"/>{{ $t('_widgets.trends') }}</template>
|
||||
|
||||
<div class="wbrkwala">
|
||||
<mk-loading v-if="fetching"/>
|
||||
<transition-group tag="div" name="chart" class="tags" v-else>
|
||||
<div v-for="stat in stats" :key="stat.tag">
|
||||
<div class="tag">
|
||||
<router-link class="a" :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</router-link>
|
||||
<p>{{ $t('nUsersMentioned', { n: stat.usersCount }) }}</p>
|
||||
</div>
|
||||
<x-chart class="chart" :src="stat.chart"/>
|
||||
<div class="wbrkwala">
|
||||
<mk-loading v-if="fetching"/>
|
||||
<transition-group tag="div" name="chart" class="tags" v-else>
|
||||
<div v-for="stat in stats" :key="stat.tag">
|
||||
<div class="tag">
|
||||
<router-link class="a" :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</router-link>
|
||||
<p>{{ $t('nUsersMentioned', { n: stat.usersCount }) }}</p>
|
||||
</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
</mk-container>
|
||||
</div>
|
||||
<x-chart class="chart" :src="stat.chart"/>
|
||||
</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
</mk-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -28,7 +26,10 @@ import XChart from './trends.chart.vue';
|
|||
export default define({
|
||||
name: 'hashtags',
|
||||
props: () => ({
|
||||
compact: false
|
||||
showHeader: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
}).extend({
|
||||
components: {
|
||||
|
@ -49,10 +50,6 @@ export default define({
|
|||
clearInterval(this.clock);
|
||||
},
|
||||
methods: {
|
||||
func() {
|
||||
this.props.compact = !this.props.compact;
|
||||
this.save();
|
||||
},
|
||||
fetch() {
|
||||
this.$root.api('hashtags/trend').then(stats => {
|
||||
this.stats = stats;
|
||||
|
|
Loading…
Reference in a new issue