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: "プレイヤーを開く"
|
enablePlayer: "プレイヤーを開く"
|
||||||
disablePlayer: "プレイヤーを閉じる"
|
disablePlayer: "プレイヤーを閉じる"
|
||||||
expandTweet: "ツイートを展開する"
|
expandTweet: "ツイートを展開する"
|
||||||
|
deck: "デッキ"
|
||||||
|
undeck: "デッキ解除"
|
||||||
|
|
||||||
_theme:
|
_theme:
|
||||||
explore: "テーマを探す"
|
explore: "テーマを探す"
|
||||||
|
@ -651,6 +653,7 @@ _widgets:
|
||||||
rss: "RSSリーダー"
|
rss: "RSSリーダー"
|
||||||
activity: "アクティビティ"
|
activity: "アクティビティ"
|
||||||
photos: "フォト"
|
photos: "フォト"
|
||||||
|
digitalClock: "デジタル時計"
|
||||||
|
|
||||||
_cw:
|
_cw:
|
||||||
hide: "隠す"
|
hide: "隠す"
|
||||||
|
@ -1129,3 +1132,15 @@ _notification:
|
||||||
yourFollowRequestAccepted: "フォローリクエストが承認されました"
|
yourFollowRequestAccepted: "フォローリクエストが承認されました"
|
||||||
youWereInvitedToGroup: "グループに招待されました"
|
youWereInvitedToGroup: "グループに招待されました"
|
||||||
|
|
||||||
|
_deck:
|
||||||
|
alwaysShowMainColumn: "常にメインカラムを表示"
|
||||||
|
columnAlign: "カラムの寄せ"
|
||||||
|
|
||||||
|
_columns:
|
||||||
|
widgets: "ウィジェット"
|
||||||
|
notifications: "通知"
|
||||||
|
tl: "タイムライン"
|
||||||
|
antenna: "アンテナ"
|
||||||
|
list: "リスト"
|
||||||
|
mentions: "あなた宛て"
|
||||||
|
direct: "ダイレクト"
|
||||||
|
|
|
@ -29,47 +29,7 @@
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<transition name="nav-back">
|
<x-sidebar ref="nav"/>
|
||||||
<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>
|
|
||||||
|
|
||||||
<div class="contents" ref="contents" :class="{ wallpaper }">
|
<div class="contents" ref="contents" :class="{ wallpaper }">
|
||||||
<main ref="main">
|
<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>
|
<span class="handle"><fa :icon="faBars"/></span>{{ $t('_widgets.' + widget.name) }}<button class="remove _button" @click="removeWidget(widget)"><fa :icon="faTimes"/></button>
|
||||||
</header>
|
</header>
|
||||||
<div @click="widgetFunc(widget.id)">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</x-draggable>
|
</x-draggable>
|
||||||
</div>
|
</div>
|
||||||
<div class="container" v-else>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="buttons">
|
<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-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-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>
|
<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 { faBell, faEnvelope, faLaugh, faComments } from '@fortawesome/free-regular-svg-icons';
|
||||||
import { ResizeObserver } from '@juggle/resize-observer';
|
import { ResizeObserver } from '@juggle/resize-observer';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { host, instanceName } from './config';
|
import { host } from './config';
|
||||||
import { search } from './scripts/search';
|
import { search } from './scripts/search';
|
||||||
import { StickySidebar } from './scripts/sticky-sidebar';
|
import { StickySidebar } from './scripts/sticky-sidebar';
|
||||||
|
import { widgets } from './widgets';
|
||||||
|
import XSidebar from './components/sidebar.vue';
|
||||||
|
|
||||||
const DESKTOP_THRESHOLD = 1100;
|
const DESKTOP_THRESHOLD = 1100;
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
|
XSidebar,
|
||||||
XClock: () => import('./components/header-clock.vue').then(m => m.default),
|
XClock: () => import('./components/header-clock.vue').then(m => m.default),
|
||||||
MkButton: () => import('./components/ui/button.vue').then(m => m.default),
|
MkButton: () => import('./components/ui/button.vue').then(m => m.default),
|
||||||
XDraggable: () => import('vuedraggable'),
|
XDraggable: () => import('vuedraggable'),
|
||||||
|
@ -152,19 +115,14 @@ export default Vue.extend({
|
||||||
return {
|
return {
|
||||||
host: host,
|
host: host,
|
||||||
pageKey: 0,
|
pageKey: 0,
|
||||||
showNav: false,
|
|
||||||
searching: false,
|
searching: false,
|
||||||
accounts: [],
|
|
||||||
lists: [],
|
|
||||||
connection: null,
|
connection: null,
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
searchWait: false,
|
searchWait: false,
|
||||||
widgetsEditMode: false,
|
widgetsEditMode: false,
|
||||||
menuDef: this.$store.getters.nav({
|
|
||||||
search: this.search
|
|
||||||
}),
|
|
||||||
isDesktop: window.innerWidth >= DESKTOP_THRESHOLD,
|
isDesktop: window.innerWidth >= DESKTOP_THRESHOLD,
|
||||||
canBack: false,
|
canBack: false,
|
||||||
|
menuDef: this.$store.getters.nav({}),
|
||||||
wallpaper: localStorage.getItem('wallpaper') != null,
|
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
|
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;
|
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 {
|
navIndicated(): boolean {
|
||||||
if (!this.$store.getters.isSignedIn) return false;
|
if (!this.$store.getters.isSignedIn) return false;
|
||||||
for (const def in this.menuDef) {
|
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;
|
if (this.menuDef[def].indicated) return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
watch:{
|
watch: {
|
||||||
$route(to, from) {
|
$route(to, from) {
|
||||||
this.pageKey++;
|
this.pageKey++;
|
||||||
this.showNav = false;
|
|
||||||
this.canBack = (window.history.length > 0 && !['index'].includes(to.name));
|
this.canBack = (window.history.length > 0 && !['index'].includes(to.name));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -245,6 +192,8 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
|
document.documentElement.style.overflowY = 'scroll';
|
||||||
|
|
||||||
if (this.$store.getters.isSignedIn) {
|
if (this.$store.getters.isSignedIn) {
|
||||||
this.connection = this.$root.stream.useSharedConnection('main');
|
this.connection = this.$root.stream.useSharedConnection('main');
|
||||||
this.connection.on('notification', this.onNotification);
|
this.connection.on('notification', this.onNotification);
|
||||||
|
@ -266,7 +215,7 @@ export default Vue.extend({
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
const adjustTitlePosition = () => {
|
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) {
|
if (left >= 0) {
|
||||||
this.$refs.title.style.left = left + 'px';
|
this.$refs.title.style.left = left + 'px';
|
||||||
}
|
}
|
||||||
|
@ -293,6 +242,10 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
showNav() {
|
||||||
|
this.$refs.nav.show();
|
||||||
|
},
|
||||||
|
|
||||||
attachSticky() {
|
attachSticky() {
|
||||||
if (!this.isDesktop) return;
|
if (!this.isDesktop) return;
|
||||||
if (this.$store.state.device.fixedWidgetsPosition) 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) {
|
async onNotification(notification) {
|
||||||
if (document.visibilityState === 'visible') {
|
if (document.visibilityState === 'visible') {
|
||||||
this.$root.stream.send('readNotification', {
|
this.$root.stream.send('readNotification', {
|
||||||
|
@ -540,8 +319,7 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
widgetFunc(id) {
|
widgetFunc(id) {
|
||||||
const w = this.$refs[id][0];
|
this.$refs[id][0].setting();
|
||||||
if (w.func) w.func();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onWidgetSort() {
|
onWidgetSort() {
|
||||||
|
@ -549,18 +327,6 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
async addWidget(place) {
|
async addWidget(place) {
|
||||||
const widgets = [
|
|
||||||
'memo',
|
|
||||||
'notifications',
|
|
||||||
'timeline',
|
|
||||||
'calendar',
|
|
||||||
'rss',
|
|
||||||
'trends',
|
|
||||||
'clock',
|
|
||||||
'activity',
|
|
||||||
'photos',
|
|
||||||
];
|
|
||||||
|
|
||||||
const { canceled, result: widget } = await this.$root.dialog({
|
const { canceled, result: widget } = await this.$root.dialog({
|
||||||
type: null,
|
type: null,
|
||||||
title: this.$t('chooseWidget'),
|
title: this.$t('chooseWidget'),
|
||||||
|
@ -594,36 +360,14 @@ export default Vue.extend({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<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 {
|
.mk-app {
|
||||||
$header-height: 60px;
|
$header-height: 60px;
|
||||||
$nav-width: 250px;
|
$nav-width: 250px; // TODO: どこかに集約したい
|
||||||
$nav-icon-only-width: 80px;
|
$nav-icon-only-width: 80px; // TODO: どこかに集約したい
|
||||||
$main-width: 670px;
|
$main-width: 670px;
|
||||||
$ui-font-size: 1em;
|
$ui-font-size: 1em; // TODO: どこかに集約したい
|
||||||
$nav-icon-only-threshold: 1279px;
|
$nav-icon-only-threshold: 1279px; // TODO: どこかに集約したい
|
||||||
$nav-hide-threshold: 650px;
|
$nav-hide-threshold: 650px; // TODO: どこかに集約したい
|
||||||
$header-sub-hide-threshold: 1090px;
|
$header-sub-hide-threshold: 1090px;
|
||||||
$left-widgets-hide-threshold: 1600px;
|
$left-widgets-hide-threshold: 1600px;
|
||||||
$right-widgets-hide-threshold: 1090px;
|
$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 {
|
> .contents {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: 0 auto;
|
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 {
|
> img {
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
height: 150px;
|
height: 128px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
border-radius: 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>
|
<template>
|
||||||
<div class="mk-modal" v-hotkey.global="keymap">
|
<div class="mk-modal" v-hotkey.global="keymap">
|
||||||
<transition :name="$store.state.device.animation ? 'bg-fade' : ''" appear>
|
<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>
|
||||||
<transition :name="$store.state.device.animation ? 'modal' : ''" appear @after-leave="() => { $emit('closed'); destroyDom(); }">
|
<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>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -14,6 +14,11 @@ import Vue from 'vue';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
props: {
|
props: {
|
||||||
|
canClose: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -54,7 +54,6 @@ export default Vue.extend({
|
||||||
margin: 0 .5em 0 0;
|
margin: 0 .5em 0 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--noteHeaderName);
|
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
|
@ -724,61 +724,6 @@ export default Vue.extend({
|
||||||
transition: box-shadow 0.1s ease;
|
transition: box-shadow 0.1s ease;
|
||||||
overflow: hidden;
|
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 {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
box-shadow: 0 0 0 3px var(--focus);
|
box-shadow: 0 0 0 3px var(--focus);
|
||||||
|
@ -797,10 +742,6 @@ export default Vue.extend({
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
color: #d28a3f;
|
color: #d28a3f;
|
||||||
|
|
||||||
@media (max-width: 450px) {
|
|
||||||
padding: 8px 16px 0 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> [data-icon] {
|
> [data-icon] {
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
@ -985,5 +926,64 @@ export default Vue.extend({
|
||||||
> .reply {
|
> .reply {
|
||||||
border-top: solid 1px var(--divider);
|
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>
|
</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
|
required: true
|
||||||
},
|
},
|
||||||
list: {
|
list: {
|
||||||
|
type: String,
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
antenna: {
|
antenna: {
|
||||||
|
type: String,
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
sound: {
|
sound: {
|
||||||
|
@ -53,6 +55,8 @@ export default Vue.extend({
|
||||||
const _note = JSON.parse(JSON.stringify(note)); // deepcopy
|
const _note = JSON.parse(JSON.stringify(note)); // deepcopy
|
||||||
(this.$refs.tl as any).prepend(_note);
|
(this.$refs.tl as any).prepend(_note);
|
||||||
|
|
||||||
|
this.$emit('note');
|
||||||
|
|
||||||
if (this.sound) {
|
if (this.sound) {
|
||||||
this.$root.sound(note.userId === this.$store.state.i.id ? 'noteMy' : 'note');
|
this.$root.sound(note.userId === this.$store.state.i.id ? 'noteMy' : 'note');
|
||||||
}
|
}
|
||||||
|
@ -77,10 +81,10 @@ export default Vue.extend({
|
||||||
if (this.src == 'antenna') {
|
if (this.src == 'antenna') {
|
||||||
endpoint = 'antennas/notes';
|
endpoint = 'antennas/notes';
|
||||||
this.query = {
|
this.query = {
|
||||||
antennaId: this.antenna.id
|
antennaId: this.antenna
|
||||||
};
|
};
|
||||||
this.connection = this.$root.stream.connectToChannel('antenna', {
|
this.connection = this.$root.stream.connectToChannel('antenna', {
|
||||||
antennaId: this.antenna.id
|
antennaId: this.antenna
|
||||||
});
|
});
|
||||||
this.connection.on('note', prepend);
|
this.connection.on('note', prepend);
|
||||||
} else if (this.src == 'home') {
|
} else if (this.src == 'home') {
|
||||||
|
@ -106,10 +110,10 @@ export default Vue.extend({
|
||||||
} else if (this.src == 'list') {
|
} else if (this.src == 'list') {
|
||||||
endpoint = 'notes/user-list-timeline';
|
endpoint = 'notes/user-list-timeline';
|
||||||
this.query = {
|
this.query = {
|
||||||
listId: this.list.id
|
listId: this.list
|
||||||
};
|
};
|
||||||
this.connection = this.$root.stream.connectToChannel('userList', {
|
this.connection = this.$root.stream.connectToChannel('userList', {
|
||||||
listId: this.list.id
|
listId: this.list
|
||||||
});
|
});
|
||||||
this.connection.on('note', prepend);
|
this.connection.on('note', prepend);
|
||||||
this.connection.on('userAdded', onUserAdded);
|
this.connection.on('userAdded', onUserAdded);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<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">
|
<header v-if="showHeader">
|
||||||
<div class="title"><slot name="header"></slot></div>
|
<div class="title"><slot name="header"></slot></div>
|
||||||
<slot name="func"></slot>
|
<slot name="func"></slot>
|
||||||
|
@ -47,6 +47,11 @@ export default Vue.extend({
|
||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
scrollable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -107,10 +112,19 @@ export default Vue.extend({
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.scrollable {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
> header {
|
> header {
|
||||||
position: relative;
|
position: relative;
|
||||||
box-shadow: 0 1px 0 0 var(--panelHeaderDivider);
|
box-shadow: 0 1px 0 0 var(--panelHeaderDivider);
|
||||||
z-index: 1;
|
z-index: 2;
|
||||||
background: var(--panelHeaderBg);
|
background: var(--panelHeaderBg);
|
||||||
color: var(--panelHeaderFg);
|
color: var(--panelHeaderFg);
|
||||||
|
|
||||||
|
@ -118,10 +132,6 @@ export default Vue.extend({
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
|
||||||
padding: 8px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> [data-icon] {
|
> [data-icon] {
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
}
|
}
|
||||||
|
@ -141,5 +151,21 @@ export default Vue.extend({
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.max-width_500px {
|
||||||
|
> header {
|
||||||
|
> .title {
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
._forceContainerFull_ .ukygtjoj {
|
||||||
|
> header {
|
||||||
|
> .title {
|
||||||
|
padding: 12px 16px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
:pattern="pattern"
|
:pattern="pattern"
|
||||||
:autocomplete="autocomplete"
|
:autocomplete="autocomplete"
|
||||||
:spellcheck="spellcheck"
|
:spellcheck="spellcheck"
|
||||||
|
:step="step"
|
||||||
@focus="focused = true"
|
@focus="focused = true"
|
||||||
@blur="focused = false"
|
@blur="focused = false"
|
||||||
@keydown="$emit('keydown', $event)"
|
@keydown="$emit('keydown', $event)"
|
||||||
|
@ -36,6 +37,7 @@
|
||||||
:pattern="pattern"
|
:pattern="pattern"
|
||||||
:autocomplete="autocomplete"
|
:autocomplete="autocomplete"
|
||||||
:spellcheck="spellcheck"
|
:spellcheck="spellcheck"
|
||||||
|
:step="step"
|
||||||
@focus="focused = true"
|
@focus="focused = true"
|
||||||
@blur="focused = false"
|
@blur="focused = false"
|
||||||
@keydown="$emit('keydown', $event)"
|
@keydown="$emit('keydown', $event)"
|
||||||
|
@ -114,6 +116,9 @@ export default Vue.extend({
|
||||||
spellcheck: {
|
spellcheck: {
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
|
step: {
|
||||||
|
required: false
|
||||||
|
},
|
||||||
debounce: {
|
debounce: {
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
|
@ -164,7 +169,7 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
v(v) {
|
v(v) {
|
||||||
if (this.type === 'number') {
|
if (this.type === 'number') {
|
||||||
this.$emit('input', parseInt(v, 10));
|
this.$emit('input', parseFloat(v));
|
||||||
} else {
|
} else {
|
||||||
this.$emit('input', v);
|
this.$emit('input', v);
|
||||||
}
|
}
|
||||||
|
@ -297,7 +302,7 @@ export default Vue.extend({
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||||
transition-duration: 0.3s;
|
transition-duration: 0.3s;
|
||||||
font-size: 16px;
|
font-size: 1em;
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
color: var(--inputLabel);
|
color: var(--inputLabel);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
@ -312,7 +317,7 @@ export default Vue.extend({
|
||||||
top: -17px;
|
top: -17px;
|
||||||
left: 0 !important;
|
left: 0 !important;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
font-size: 16px;
|
font-size: 1em;
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
color: var(--inputLabel);
|
color: var(--inputLabel);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
@ -343,7 +348,7 @@ export default Vue.extend({
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-size: 16px;
|
font-size: 1em;
|
||||||
line-height: $height;
|
line-height: $height;
|
||||||
color: var(--inputText);
|
color: var(--inputText);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
@ -364,7 +369,7 @@ export default Vue.extend({
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
top: 0;
|
top: 0;
|
||||||
font-size: 16px;
|
font-size: 1em;
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
color: var(--inputLabel);
|
color: var(--inputLabel);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
|
@ -135,7 +135,7 @@ export default Vue.extend({
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||||
transition-duration: 0.3s;
|
transition-duration: 0.3s;
|
||||||
font-size: 16px;
|
font-size: 1em;
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
//will-change transform
|
//will-change transform
|
||||||
|
@ -150,7 +150,7 @@ export default Vue.extend({
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-size: 16px;
|
font-size: 1em;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
|
@ -170,7 +170,7 @@ export default Vue.extend({
|
||||||
display: block;
|
display: block;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
font-size: 16px;
|
font-size: 1em;
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
color: rgba(#000, 0.54);
|
color: rgba(#000, 0.54);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
role="switch"
|
role="switch"
|
||||||
:aria-checked="checked"
|
:aria-checked="checked"
|
||||||
:aria-disabled="disabled"
|
:aria-disabled="disabled"
|
||||||
@click="toggle"
|
@click.prevent="toggle"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|
|
@ -133,7 +133,7 @@ export default Vue.extend({
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||||
transition-duration: 0.3s;
|
transition-duration: 0.3s;
|
||||||
font-size: 16px;
|
font-size: 1em;
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
//will-change transform
|
//will-change transform
|
||||||
|
@ -151,7 +151,7 @@ export default Vue.extend({
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-size: 16px;
|
font-size: 1em;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<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="ebkgoccj" :class="{ noPadding }" @keydown="onKeydown" :style="{ width: `${width}px`, height: `${height}px` }">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<button class="_button" v-if="withOkButton" @click="close()"><fa :icon="faTimes"/></button>
|
<button class="_button" v-if="withOkButton" @click="close()"><fa :icon="faTimes"/></button>
|
||||||
|
@ -57,6 +57,11 @@ export default Vue.extend({
|
||||||
required: false,
|
required: false,
|
||||||
default: 400
|
default: 400
|
||||||
},
|
},
|
||||||
|
canClose: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
|
|
@ -18,3 +18,4 @@ export const getLocale = async () => Object.fromEntries((await entries(clientDb.
|
||||||
export const version = _VERSION_;
|
export const version = _VERSION_;
|
||||||
export const env = _ENV_;
|
export const env = _ENV_;
|
||||||
export const instanceName = siteName === 'Misskey' ? null : siteName;
|
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';
|
import Vue from 'vue';
|
||||||
|
@ -12,11 +12,13 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||||
|
|
||||||
import VueHotkey from './scripts/hotkey';
|
import VueHotkey from './scripts/hotkey';
|
||||||
import App from './app.vue';
|
import App from './app.vue';
|
||||||
|
import Deck from './deck.vue';
|
||||||
import MiOS from './mios';
|
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 PostFormDialog from './components/post-form-dialog.vue';
|
||||||
import Dialog from './components/dialog.vue';
|
import Dialog from './components/dialog.vue';
|
||||||
import Menu from './components/menu.vue';
|
import Menu from './components/menu.vue';
|
||||||
|
import Form from './components/form-window.vue';
|
||||||
import { router } from './router';
|
import { router } from './router';
|
||||||
import { applyTheme, lightTheme } from './scripts/theme';
|
import { applyTheme, lightTheme } from './scripts/theme';
|
||||||
import { isDeviceDarkmode } from './scripts/is-device-darkmode';
|
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
|
i18n // TODO: 消せないか考える SEE: https://github.com/syuilo/misskey/pull/6396#discussion_r429511030
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
// TODO: ここらへんのメソッド全部Vuexに移したい
|
||||||
methods: {
|
methods: {
|
||||||
api: (endpoint: string, data: { [x: string]: any } = {}, token?) => store.dispatch('api', { endpoint, data, token }),
|
api: (endpoint: string, data: { [x: string]: any } = {}, token?) => store.dispatch('api', { endpoint, data, token }),
|
||||||
signout: os.signout,
|
signout: os.signout,
|
||||||
|
@ -194,6 +197,13 @@ os.init(async () => {
|
||||||
});
|
});
|
||||||
return p;
|
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) {
|
post(opts, cb) {
|
||||||
if (!this.$store.getters.isSignedIn) return;
|
if (!this.$store.getters.isSignedIn) return;
|
||||||
const vm = this.new(PostFormDialog, opts);
|
const vm = this.new(PostFormDialog, opts);
|
||||||
|
@ -210,11 +220,9 @@ os.init(async () => {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
router: router,
|
router: router,
|
||||||
render: createEl => createEl(App)
|
render: createEl => createEl(deckmode ? Deck : App)
|
||||||
});
|
});
|
||||||
|
|
||||||
os.app = app;
|
|
||||||
|
|
||||||
// マウント
|
// マウント
|
||||||
app.$mount('#app');
|
app.$mount('#app');
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
// TODO: このファイル消したい
|
// TODO: このファイル消したい
|
||||||
|
|
||||||
import autobind from 'autobind-decorator';
|
import autobind from 'autobind-decorator';
|
||||||
import Vue from 'vue';
|
|
||||||
import { EventEmitter } from 'eventemitter3';
|
import { EventEmitter } from 'eventemitter3';
|
||||||
|
|
||||||
import { apiUrl, version } from './config';
|
import { apiUrl, version } from './config';
|
||||||
|
@ -14,8 +13,6 @@ import store from './store';
|
||||||
* Misskey Operating System
|
* Misskey Operating System
|
||||||
*/
|
*/
|
||||||
export default class MiOS extends EventEmitter {
|
export default class MiOS extends EventEmitter {
|
||||||
public app: Vue;
|
|
||||||
|
|
||||||
public store: ReturnType<typeof store>;
|
public store: ReturnType<typeof store>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
<x-tutorial class="tutorial" v-if="$store.state.settings.tutorial != -1"/>
|
<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-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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -15,14 +15,15 @@
|
||||||
|
|
||||||
<mk-remote-caution v-if="note.user.host != null" :href="note.url || note.uri" style="margin-bottom: var(--margin)"/>
|
<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"/>
|
<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>
|
<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"/>
|
<hr v-if="showPrev"/>
|
||||||
<x-notes v-if="showPrev" ref="prev" :pagination="prev" style="margin-top: var(--margin);"/>
|
<x-notes v-if="showPrev" ref="prev" :pagination="prev" style="margin-top: var(--margin);"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="error">
|
||||||
|
<mk-error @retry="fetch()"/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,20 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<section class="_card">
|
||||||
<div class="_title"><fa :icon="faCog"/> {{ $t('accessibility') }}</div>
|
<div class="_title"><fa :icon="faCog"/> {{ $t('accessibility') }}</div>
|
||||||
<div class="_content">
|
<div class="_content">
|
||||||
|
@ -93,7 +107,7 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
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 MkButton from '../../components/ui/button.vue';
|
||||||
import MkSwitch from '../../components/ui/switch.vue';
|
import MkSwitch from '../../components/ui/switch.vue';
|
||||||
import MkSelect from '../../components/ui/select.vue';
|
import MkSelect from '../../components/ui/select.vue';
|
||||||
|
@ -145,7 +159,7 @@ export default Vue.extend({
|
||||||
lang: localStorage.getItem('lang'),
|
lang: localStorage.getItem('lang'),
|
||||||
fontSize: localStorage.getItem('fontSize'),
|
fontSize: localStorage.getItem('fontSize'),
|
||||||
sounds,
|
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 }); }
|
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: {
|
sfxVolume: {
|
||||||
get() { return this.$store.state.device.sfxVolume; },
|
get() { return this.$store.state.device.sfxVolume; },
|
||||||
set(value) { this.$store.commit('device/set', { key: 'sfxVolume', value: parseFloat(value, 10) }); }
|
set(value) { this.$store.commit('device/set', { key: 'sfxVolume', value: parseFloat(value, 10) }); }
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="kjeftjfm">
|
<div class="kjeftjfm" v-size="[{ max: 500 }]">
|
||||||
<div class="with">
|
<div class="with">
|
||||||
<button class="_button" @click="with_ = null" :class="{ active: with_ === null }">{{ $t('notes') }}</button>
|
<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>
|
<button class="_button" @click="with_ = 'replies'" :class="{ active: with_ === 'replies' }">{{ $t('notesAndReplies') }}</button>
|
||||||
|
@ -60,10 +60,6 @@ export default Vue.extend({
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: var(--margin);
|
margin-bottom: var(--margin);
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
|
||||||
font-size: 80%;
|
|
||||||
}
|
|
||||||
|
|
||||||
> button {
|
> button {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 11px 8px 8px 8px;
|
padding: 11px 8px 8px 8px;
|
||||||
|
@ -75,5 +71,11 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.max-width_500px {
|
||||||
|
> .with {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<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="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>
|
<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 MkRemoteCaution from '../../components/remote-caution.vue';
|
||||||
import Progress from '../../scripts/loading';
|
import Progress from '../../scripts/loading';
|
||||||
import parseAcct from '../../../misc/acct/parse';
|
import parseAcct from '../../../misc/acct/parse';
|
||||||
|
import { getScrollPosition } from '../../scripts/scroll';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
|
@ -168,12 +169,8 @@ export default Vue.extend({
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
window.requestAnimationFrame(this.parallaxLoop);
|
window.requestAnimationFrame(this.parallaxLoop);
|
||||||
window.addEventListener('scroll', this.parallax, { passive: true });
|
|
||||||
document.addEventListener('touchmove', this.parallax, { passive: true });
|
|
||||||
this.$once('hook:beforeDestroy', () => {
|
this.$once('hook:beforeDestroy', () => {
|
||||||
window.cancelAnimationFrame(this.parallaxAnimationId);
|
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;
|
const banner = this.$refs.banner as any;
|
||||||
if (banner == null) return;
|
if (banner == null) return;
|
||||||
|
|
||||||
const top = window.scrollY;
|
const top = getScrollPosition(this.$el);
|
||||||
|
|
||||||
if (top < 0) return;
|
if (top < 0) return;
|
||||||
|
|
||||||
|
@ -219,7 +216,6 @@ export default Vue.extend({
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.mk-user-page {
|
.mk-user-page {
|
||||||
|
|
||||||
> .punished {
|
> .punished {
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
@ -237,10 +233,6 @@ export default Vue.extend({
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
|
||||||
height: 140px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .banner {
|
> .banner {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: #4c5e6d;
|
background-color: #4c5e6d;
|
||||||
|
@ -257,10 +249,6 @@ export default Vue.extend({
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 78px;
|
height: 78px;
|
||||||
background: linear-gradient(transparent, rgba(#000, 0.7));
|
background: linear-gradient(transparent, rgba(#000, 0.7));
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
> .followed {
|
> .followed {
|
||||||
|
@ -308,10 +296,6 @@ export default Vue.extend({
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .name {
|
> .name {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -343,10 +327,6 @@ export default Vue.extend({
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
border-bottom: solid 1px var(--divider);
|
border-bottom: solid 1px var(--divider);
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .bottom {
|
> .bottom {
|
||||||
> * {
|
> * {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -365,26 +345,12 @@ export default Vue.extend({
|
||||||
width: 120px;
|
width: 120px;
|
||||||
height: 120px;
|
height: 120px;
|
||||||
box-shadow: 1px 1px 3px rgba(#000, 0.2);
|
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 {
|
> .description {
|
||||||
padding: 24px 24px 24px 154px;
|
padding: 24px 24px 24px 154px;
|
||||||
font-size: 0.95em;
|
font-size: 0.95em;
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
|
||||||
padding: 16px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .empty {
|
> .empty {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
@ -396,10 +362,6 @@ export default Vue.extend({
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
border-top: solid 1px var(--divider);
|
border-top: solid 1px var(--divider);
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .field {
|
> .field {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
@ -436,10 +398,6 @@ export default Vue.extend({
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
border-top: solid 1px var(--divider);
|
border-top: solid 1px var(--divider);
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> a {
|
> a {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -473,5 +431,47 @@ export default Vue.extend({
|
||||||
> .content {
|
> .content {
|
||||||
margin-bottom: var(--margin);
|
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>
|
</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,
|
moreFetching: false,
|
||||||
inited: false,
|
inited: false,
|
||||||
more: false,
|
more: false,
|
||||||
backed: false,
|
backed: false, // 遡り中か否か
|
||||||
isBackTop: false,
|
isBackTop: false,
|
||||||
ilObserver: new IntersectionObserver(
|
ilObserver: new IntersectionObserver(
|
||||||
(entries) => entries.some((entry) => entry.isIntersecting)
|
(entries) => entries.some((entry) => entry.isIntersecting)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
export function getScrollContainer(el: Element | null): Element | null {
|
export function getScrollContainer(el: Element | null): Element | null {
|
||||||
if (el == null || el.tagName === 'BODY') return null;
|
if (el == null || el.tagName === 'BODY') return null;
|
||||||
const style = window.getComputedStyle(el);
|
const overflow = window.getComputedStyle(el).getPropertyValue('overflow');
|
||||||
if (style.getPropertyValue('overflow') === 'auto') {
|
if (overflow.endsWith('auto')) { // xとyを個別に指定している場合、hidden auto みたいな値になる
|
||||||
return el;
|
return el;
|
||||||
} else {
|
} else {
|
||||||
return getScrollContainer(el.parentElement);
|
return getScrollContainer(el.parentElement);
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import Vuex from 'vuex';
|
import Vuex from 'vuex';
|
||||||
import createPersistedState from 'vuex-persistedstate';
|
import createPersistedState from 'vuex-persistedstate';
|
||||||
import * as nestedProperty from 'nested-property';
|
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 { 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 = {
|
export const defaultSettings = {
|
||||||
tutorial: 0,
|
tutorial: 0,
|
||||||
|
@ -35,7 +36,13 @@ export const defaultDeviceUserSettings = {
|
||||||
'explore',
|
'explore',
|
||||||
'announcements',
|
'announcements',
|
||||||
'search',
|
'search',
|
||||||
|
'-',
|
||||||
|
'deck',
|
||||||
],
|
],
|
||||||
|
deck: {
|
||||||
|
columns: [],
|
||||||
|
layout: [],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultDeviceSettings = {
|
export const defaultDeviceSettings = {
|
||||||
|
@ -50,6 +57,7 @@ export const defaultDeviceSettings = {
|
||||||
darkTheme: '8c539dc1-0fab-4d47-9194-39c508e9bfe1',
|
darkTheme: '8c539dc1-0fab-4d47-9194-39c508e9bfe1',
|
||||||
lightTheme: '4eea646f-7afa-4645-83e9-83af0333cd37',
|
lightTheme: '4eea646f-7afa-4645-83e9-83af0333cd37',
|
||||||
darkMode: false,
|
darkMode: false,
|
||||||
|
deckMode: false,
|
||||||
syncDeviceDarkMode: true,
|
syncDeviceDarkMode: true,
|
||||||
animation: true,
|
animation: true,
|
||||||
animatedMfm: true,
|
animatedMfm: true,
|
||||||
|
@ -60,6 +68,9 @@ export const defaultDeviceSettings = {
|
||||||
fixedWidgetsPosition: false,
|
fixedWidgetsPosition: false,
|
||||||
roomGraphicsQuality: 'medium',
|
roomGraphicsQuality: 'medium',
|
||||||
roomUseOrthographicCamera: true,
|
roomUseOrthographicCamera: true,
|
||||||
|
deckColumnAlign: 'left',
|
||||||
|
deckAlwaysShowMainColumn: true,
|
||||||
|
deckMainColumnPlace: 'left',
|
||||||
sfxVolume: 0.3,
|
sfxVolume: 0.3,
|
||||||
sfxNote: 'syuilo/down',
|
sfxNote: 'syuilo/down',
|
||||||
sfxNoteMy: 'syuilo/up',
|
sfxNoteMy: 'syuilo/up',
|
||||||
|
@ -197,6 +208,14 @@ export default () => new Vuex.Store({
|
||||||
get show() { return getters.isSignedIn; },
|
get show() { return getters.isSignedIn; },
|
||||||
get to() { return `/@${state.i.username}/room`; },
|
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;
|
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 {
|
:root {
|
||||||
--radius: 8px;
|
--radius: 8px;
|
||||||
--marginFull: 16px;
|
--marginFull: 16px;
|
||||||
--marginHalf: 8px;
|
--marginHalf: 10px;
|
||||||
|
|
||||||
--margin: var(--marginFull);
|
--margin: var(--marginFull);
|
||||||
|
|
||||||
|
@ -25,7 +25,6 @@ html {
|
||||||
background-position: center;
|
background-position: center;
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
overflow-y: scroll;
|
|
||||||
|
|
||||||
&, * {
|
&, * {
|
||||||
scrollbar-color: var(--scrollbarHandle) var(--panel);
|
scrollbar-color: var(--scrollbarHandle) var(--panel);
|
||||||
|
@ -278,13 +277,14 @@ hr {
|
||||||
|
|
||||||
._panel {
|
._panel {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
box-shadow: 0 0 0 1px var(--panelBorder);
|
box-shadow: 0 0 0 1px var(--panelBorder);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
._widget ._list_ ._panel {
|
._close_ ._list_ > * {
|
||||||
box-shadow: 0 1px 0 0 var(--divider), 0 -1px 0 0 var(--divider);
|
box-shadow: 0 1px 0 0 var(--divider), 0 -1px 0 0 var(--divider);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
|
@ -348,31 +348,6 @@ hr {
|
||||||
& + ._content {
|
& + ._content {
|
||||||
border-top: solid 1px var(--divider);
|
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 {
|
> ._footer {
|
||||||
|
@ -385,6 +360,21 @@ hr {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
._narrow_ ._card {
|
||||||
|
> ._title {
|
||||||
|
padding: 16px;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
> ._content {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> ._footer {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
._fullinfo {
|
._fullinfo {
|
||||||
padding: 64px 32px;
|
padding: 64px 32px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
|
@ -26,8 +26,8 @@
|
||||||
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
|
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
|
||||||
panelBorder: 'rgba(0, 0, 0, 0)',
|
panelBorder: 'rgba(0, 0, 0, 0)',
|
||||||
shadow: 'rgba(0, 0, 0, 0.1)',
|
shadow: 'rgba(0, 0, 0, 0.1)',
|
||||||
header: 'rgba(20, 20, 20, 0.75)',
|
header: ':alpha<0.7<@bg',
|
||||||
navBg: '@panel',
|
navBg: '@bg',
|
||||||
navFg: '@fg',
|
navFg: '@fg',
|
||||||
navHoverFg: ':lighten<17<@fg',
|
navHoverFg: ':lighten<17<@fg',
|
||||||
navActive: '@accent',
|
navActive: '@accent',
|
||||||
|
@ -58,6 +58,7 @@
|
||||||
wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
|
wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
|
||||||
badge: '#31b1ce',
|
badge: '#31b1ce',
|
||||||
messageBg: ':lighten<5<@bg',
|
messageBg: ':lighten<5<@bg',
|
||||||
|
deckColumnBorder: ':lighten<10<@panel',
|
||||||
X1: ':alpha<0<@bg',
|
X1: ':alpha<0<@bg',
|
||||||
X2: ':darken<2<@panel',
|
X2: ':darken<2<@panel',
|
||||||
X3: 'rgba(255, 255, 255, 0.05)',
|
X3: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
|
|
@ -26,8 +26,8 @@
|
||||||
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
|
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
|
||||||
panelBorder: 'rgba(0, 0, 0, 0)',
|
panelBorder: 'rgba(0, 0, 0, 0)',
|
||||||
shadow: 'rgba(0, 0, 0, 0.1)',
|
shadow: 'rgba(0, 0, 0, 0.1)',
|
||||||
header: 'rgba(255, 255, 255, 0.75)',
|
header: ':alpha<0.7<@bg',
|
||||||
navBg: '@panel',
|
navBg: '@bg',
|
||||||
navFg: '@fg',
|
navFg: '@fg',
|
||||||
navHoverFg: ':darken<17<@fg',
|
navHoverFg: ':darken<17<@fg',
|
||||||
navActive: '@accent',
|
navActive: '@accent',
|
||||||
|
@ -58,6 +58,7 @@
|
||||||
wallpaperOverlay: 'rgba(255, 255, 255, 0.5)',
|
wallpaperOverlay: 'rgba(255, 255, 255, 0.5)',
|
||||||
badge: '#31b1ce',
|
badge: '#31b1ce',
|
||||||
messageBg: '@panel',
|
messageBg: '@panel',
|
||||||
|
deckColumnBorder: ':darken<20<@panel',
|
||||||
X1: ':alpha<0<@bg',
|
X1: ':alpha<0<@bg',
|
||||||
X2: ':darken<2<@panel',
|
X2: ':darken<2<@panel',
|
||||||
X3: 'rgba(0, 0, 0, 0.05)',
|
X3: 'rgba(0, 0, 0, 0.05)',
|
||||||
|
|
|
@ -13,5 +13,6 @@
|
||||||
panelHeaderDivider: '@divider',
|
panelHeaderDivider: '@divider',
|
||||||
panelBorder: '@divider',
|
panelBorder: '@divider',
|
||||||
messageBg: '#1d1d1d',
|
messageBg: '#1d1d1d',
|
||||||
|
deckColumnBorder: '@divider',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,9 +10,11 @@
|
||||||
accent: 'rgb(206, 147, 191)',
|
accent: 'rgb(206, 147, 191)',
|
||||||
bg: 'rgb(253, 242, 243)',
|
bg: 'rgb(253, 242, 243)',
|
||||||
fg: 'rgb(161, 139, 146)',
|
fg: 'rgb(161, 139, 146)',
|
||||||
|
divider: '#ece7e7',
|
||||||
renote: '@accent',
|
renote: '@accent',
|
||||||
link: '@accent',
|
link: '@accent',
|
||||||
mention: '@accent',
|
mention: '@accent',
|
||||||
hashtag: '@accent',
|
hashtag: '@accent',
|
||||||
|
panelHeaderDivider: '@divider',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,5 +11,6 @@
|
||||||
bg: 'rgb(220, 229, 232)',
|
bg: 'rgb(220, 229, 232)',
|
||||||
fg: 'rgb(139, 153, 161)',
|
fg: 'rgb(139, 153, 161)',
|
||||||
renote: '@accent',
|
renote: '@accent',
|
||||||
|
panelHeaderDivider: '@divider',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,11 @@
|
||||||
base: 'light',
|
base: 'light',
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
|
bg: '#f2f2f2',
|
||||||
|
header: ':alpha<0.7<@bg',
|
||||||
|
navBg: '@bg',
|
||||||
panelHeaderDivider: '@divider',
|
panelHeaderDivider: '@divider',
|
||||||
messageBg: '#dedede',
|
messageBg: '#dedede',
|
||||||
|
deckColumnBorder: '#cccccc',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<mk-container :show-header="props.showHeader" :naked="props.transparent">
|
||||||
<mk-container :show-header="props.design === 0" :naked="props.design === 2">
|
|
||||||
<template #header><fa :icon="faChartBar"/>{{ $t('_widgets.activity') }}</template>
|
<template #header><fa :icon="faChartBar"/>{{ $t('_widgets.activity') }}</template>
|
||||||
<template #func><button @click="toggleView()" class="_button"><fa :icon="faSort"/></button></template>
|
<template #func><button @click="toggleView()" class="_button"><fa :icon="faSort"/></button></template>
|
||||||
|
|
||||||
|
@ -11,8 +10,7 @@
|
||||||
<x-chart v-show="props.view === 1" :data="[].concat(activity)"/>
|
<x-chart v-show="props.view === 1" :data="[].concat(activity)"/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</mk-container>
|
</mk-container>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -25,8 +23,19 @@ import XChart from './activity.chart.vue';
|
||||||
export default define({
|
export default define({
|
||||||
name: 'activity',
|
name: 'activity',
|
||||||
props: () => ({
|
props: () => ({
|
||||||
design: 0,
|
showHeader: {
|
||||||
view: 0
|
type: 'boolean',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
transparent: {
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
view: {
|
||||||
|
type: 'number',
|
||||||
|
default: 0,
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}).extend({
|
}).extend({
|
||||||
components: {
|
components: {
|
||||||
|
@ -57,14 +66,6 @@ export default define({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
func() {
|
|
||||||
if (this.props.design === 2) {
|
|
||||||
this.props.design = 0;
|
|
||||||
} else {
|
|
||||||
this.props.design++;
|
|
||||||
}
|
|
||||||
this.save();
|
|
||||||
},
|
|
||||||
toggleView() {
|
toggleView() {
|
||||||
if (this.props.view === 1) {
|
if (this.props.view === 1) {
|
||||||
this.props.view = 0;
|
this.props.view = 0;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<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">
|
<div class="calendar" :data-is-holiday="isHoliday">
|
||||||
<p class="month-and-year">
|
<p class="month-and-year">
|
||||||
<span class="year">{{ $t('yearX', { year }) }}</span>
|
<span class="year">{{ $t('yearX', { year }) }}</span>
|
||||||
|
@ -37,7 +37,10 @@ import define from './define';
|
||||||
export default define({
|
export default define({
|
||||||
name: 'calendar',
|
name: 'calendar',
|
||||||
props: () => ({
|
props: () => ({
|
||||||
design: 0
|
transparent: {
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}).extend({
|
}).extend({
|
||||||
data() {
|
data() {
|
||||||
|
@ -62,14 +65,6 @@ export default define({
|
||||||
clearInterval(this.clock);
|
clearInterval(this.clock);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
func() {
|
|
||||||
if (this.props.design === 2) {
|
|
||||||
this.props.design = 0;
|
|
||||||
} else {
|
|
||||||
this.props.design++;
|
|
||||||
}
|
|
||||||
this.save();
|
|
||||||
},
|
|
||||||
tick() {
|
tick() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const nd = now.getDate();
|
const nd = now.getDate();
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<mk-container :naked="props.transparent" :show-header="false">
|
||||||
<mk-container :naked="props.style % 2 === 0" :show-header="false">
|
|
||||||
<div class="vubelbmv">
|
<div class="vubelbmv">
|
||||||
<mk-analog-clock class="clock" :smooth="props.style < 2"/>
|
<mk-analog-clock class="clock"/>
|
||||||
</div>
|
</div>
|
||||||
</mk-container>
|
</mk-container>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -16,19 +14,16 @@ import MkAnalogClock from '../components/analog-clock.vue';
|
||||||
export default define({
|
export default define({
|
||||||
name: 'clock',
|
name: 'clock',
|
||||||
props: () => ({
|
props: () => ({
|
||||||
style: 0
|
transparent: {
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}).extend({
|
}).extend({
|
||||||
components: {
|
components: {
|
||||||
MkContainer,
|
MkContainer,
|
||||||
MkAnalogClock
|
MkAnalogClock
|
||||||
},
|
},
|
||||||
methods: {
|
|
||||||
func() {
|
|
||||||
this.props.style = (this.props.style + 1) % 4;
|
|
||||||
this.save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import Vue from 'vue';
|
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;
|
name: string;
|
||||||
props?: () => T;
|
props?: () => T;
|
||||||
}) {
|
}) {
|
||||||
|
@ -15,22 +16,22 @@ export default function <T extends object>(data: {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
bakedOldProps: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
id(): string {
|
id(): string {
|
||||||
return this.widget.id;
|
return this.widget.id;
|
||||||
},
|
},
|
||||||
|
|
||||||
props(): T {
|
props(): Record<string, any> {
|
||||||
return this.widget.data;
|
return this.widget.data;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
bakedOldProps: null
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.mergeProps();
|
this.mergeProps();
|
||||||
|
|
||||||
|
@ -45,11 +46,26 @@ export default function <T extends object>(data: {
|
||||||
const defaultProps = data.props();
|
const defaultProps = data.props();
|
||||||
for (const prop of Object.keys(defaultProps)) {
|
for (const prop of Object.keys(defaultProps)) {
|
||||||
if (this.props.hasOwnProperty(prop)) continue;
|
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() {
|
save() {
|
||||||
this.$store.commit('deviceUser/updateWidget', this.widget);
|
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-clock', () => import('./clock.vue').then(m => m.default));
|
||||||
Vue.component('mkw-activity', () => import('./activity.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-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>
|
<template>
|
||||||
<div>
|
<mk-container :show-header="props.showHeader">
|
||||||
<mk-container :show-header="!props.compact">
|
|
||||||
<template #header><fa :icon="faStickyNote"/>{{ $t('_widgets.memo') }}</template>
|
<template #header><fa :icon="faStickyNote"/>{{ $t('_widgets.memo') }}</template>
|
||||||
|
|
||||||
<div class="otgbylcu">
|
<div class="otgbylcu">
|
||||||
<textarea v-model="text" :placeholder="$t('placeholder')" @input="onChange"></textarea>
|
<textarea v-model="text" :placeholder="$t('placeholder')" @input="onChange"></textarea>
|
||||||
<button @click="saveMemo" :disabled="!changed" class="_buttonPrimary">{{ $t('save') }}</button>
|
<button @click="saveMemo" :disabled="!changed" class="_buttonPrimary">{{ $t('save') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</mk-container>
|
</mk-container>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -19,10 +17,12 @@ import define from './define';
|
||||||
export default define({
|
export default define({
|
||||||
name: 'memo',
|
name: 'memo',
|
||||||
props: () => ({
|
props: () => ({
|
||||||
compact: false
|
showHeader: {
|
||||||
|
type: 'boolean',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}).extend({
|
}).extend({
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
MkContainer
|
MkContainer
|
||||||
},
|
},
|
||||||
|
@ -45,11 +45,6 @@ export default define({
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
func() {
|
|
||||||
this.props.compact = !this.props.compact;
|
|
||||||
this.save();
|
|
||||||
},
|
|
||||||
|
|
||||||
onChange() {
|
onChange() {
|
||||||
this.changed = true;
|
this.changed = true;
|
||||||
clearTimeout(this.timeoutId);
|
clearTimeout(this.timeoutId);
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="mkw-notifications" :style="`flex-basis: calc(${basis}% - var(--margin)); height: ${previewHeight}px;`">
|
<mk-container :style="`height: ${props.height}px;`" :show-header="props.showHeader" :scrollable="true">
|
||||||
<mk-container :show-header="!props.compact" class="container">
|
|
||||||
<template #header><fa :icon="faBell"/>{{ $t('notifications') }}</template>
|
<template #header><fa :icon="faBell"/>{{ $t('notifications') }}</template>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<x-notifications/>
|
<x-notifications/>
|
||||||
</div>
|
</div>
|
||||||
</mk-container>
|
</mk-container>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -16,17 +14,19 @@ import MkContainer from '../components/ui/container.vue';
|
||||||
import XNotifications from '../components/notifications.vue';
|
import XNotifications from '../components/notifications.vue';
|
||||||
import define from './define';
|
import define from './define';
|
||||||
|
|
||||||
const basisSteps = [25, 50, 75, 100]
|
|
||||||
const previewHeights = [200, 300, 400, 500]
|
|
||||||
|
|
||||||
export default define({
|
export default define({
|
||||||
name: 'notifications',
|
name: 'notifications',
|
||||||
props: () => ({
|
props: () => ({
|
||||||
compact: false,
|
showHeader: {
|
||||||
basisStep: 0
|
type: 'boolean',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: 'number',
|
||||||
|
default: 300,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}).extend({
|
}).extend({
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
MkContainer,
|
MkContainer,
|
||||||
XNotifications,
|
XNotifications,
|
||||||
|
@ -37,47 +37,5 @@ export default define({
|
||||||
faBell
|
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>
|
</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,6 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<mk-container :show-header="props.showHeader" :naked="props.transparent" :class="$style.root" :data-transparent="props.transparent">
|
||||||
<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>
|
<template #header><fa :icon="faCamera"/>{{ $t('_widgets.photos') }}</template>
|
||||||
|
|
||||||
<div class="">
|
<div class="">
|
||||||
|
@ -12,8 +11,7 @@
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</mk-container>
|
</mk-container>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -25,7 +23,14 @@ import { getStaticImageUrl } from '../scripts/get-static-image-url';
|
||||||
export default define({
|
export default define({
|
||||||
name: 'photos',
|
name: 'photos',
|
||||||
props: () => ({
|
props: () => ({
|
||||||
design: 0,
|
showHeader: {
|
||||||
|
type: 'boolean',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
transparent: {
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}).extend({
|
}).extend({
|
||||||
components: {
|
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 {
|
thumbnail(image: any): string {
|
||||||
return this.$store.state.device.disableShowingAnimatedImages
|
return this.$store.state.device.disableShowingAnimatedImages
|
||||||
? getStaticImageUrl(image.thumbnailUrl)
|
? getStaticImageUrl(image.thumbnailUrl)
|
||||||
|
@ -82,7 +78,7 @@ export default define({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.root[data-melt] {
|
.root[data-transparent] {
|
||||||
.stream {
|
.stream {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<mk-container :show-header="props.showHeader">
|
||||||
<mk-container :show-header="!props.compact">
|
|
||||||
<template #header><fa :icon="faRssSquare"/>RSS</template>
|
<template #header><fa :icon="faRssSquare"/>RSS</template>
|
||||||
<template #func><button class="_button" @click="setting"><fa :icon="faCog"/></button></template>
|
<template #func><button class="_button" @click="setting"><fa :icon="faCog"/></button></template>
|
||||||
|
|
||||||
|
@ -10,8 +9,7 @@
|
||||||
<a v-for="item in items" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a>
|
<a v-for="item in items" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</mk-container>
|
</mk-container>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -22,8 +20,14 @@ import define from './define';
|
||||||
export default define({
|
export default define({
|
||||||
name: 'rss',
|
name: 'rss',
|
||||||
props: () => ({
|
props: () => ({
|
||||||
compact: false,
|
showHeader: {
|
||||||
url: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews'
|
type: 'boolean',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
type: 'string',
|
||||||
|
default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}).extend({
|
}).extend({
|
||||||
components: {
|
components: {
|
||||||
|
@ -40,15 +44,12 @@ export default define({
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetch();
|
this.fetch();
|
||||||
this.clock = setInterval(this.fetch, 60000);
|
this.clock = setInterval(this.fetch, 60000);
|
||||||
|
this.$watch('props.url', this.fetch);
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
clearInterval(this.clock);
|
clearInterval(this.clock);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
func() {
|
|
||||||
this.props.compact = !this.props.compact;
|
|
||||||
this.save();
|
|
||||||
},
|
|
||||||
fetch() {
|
fetch() {
|
||||||
fetch(`https://api.rss2json.com/v1/api.json?rss_url=${this.props.url}`, {
|
fetch(`https://api.rss2json.com/v1/api.json?rss_url=${this.props.url}`, {
|
||||||
}).then(res => {
|
}).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>
|
</script>
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="mkw-timeline" :style="`flex-basis: calc(${basis}% - var(--margin)); height: ${previewHeight}px;`">
|
<mk-container :show-header="props.showHeader" :style="`height: ${props.height}px;`" :scrollable="true">
|
||||||
<mk-container :show-header="!props.compact" class="container">
|
|
||||||
<template #header>
|
<template #header>
|
||||||
<button @click="choose" class="_button">
|
<button @click="choose" class="_button">
|
||||||
<fa v-if="props.src === 'home'" :icon="faHome"/>
|
<fa v-if="props.src === 'home'" :icon="faHome"/>
|
||||||
|
@ -15,10 +14,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<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" :antenna="props.antenna"/>
|
<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>
|
</div>
|
||||||
</mk-container>
|
</mk-container>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -28,19 +26,25 @@ import MkContainer from '../components/ui/container.vue';
|
||||||
import XTimeline from '../components/timeline.vue';
|
import XTimeline from '../components/timeline.vue';
|
||||||
import define from './define';
|
import define from './define';
|
||||||
|
|
||||||
const basisSteps = [25, 50, 75, 100]
|
|
||||||
const previewHeights = [200, 300, 400, 500]
|
|
||||||
|
|
||||||
export default define({
|
export default define({
|
||||||
name: 'timeline',
|
name: 'timeline',
|
||||||
props: () => ({
|
props: () => ({
|
||||||
src: 'home',
|
showHeader: {
|
||||||
list: null,
|
type: 'boolean',
|
||||||
compact: false,
|
default: true,
|
||||||
basisStep: 0
|
},
|
||||||
|
src: {
|
||||||
|
type: 'string',
|
||||||
|
default: 'home',
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
list: {
|
||||||
|
type: 'object',
|
||||||
|
default: null,
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}).extend({
|
}).extend({
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
MkContainer,
|
MkContainer,
|
||||||
XTimeline,
|
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: {
|
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) {
|
async choose(ev) {
|
||||||
this.menuOpened = true;
|
this.menuOpened = true;
|
||||||
const [antennas, lists] = await Promise.all([
|
const [antennas, lists] = await Promise.all([
|
||||||
|
@ -129,22 +112,3 @@ export default define({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</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,6 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<mk-container :show-header="props.showHeader">
|
||||||
<mk-container :show-header="!props.compact">
|
|
||||||
<template #header><fa :icon="faHashtag"/>{{ $t('_widgets.trends') }}</template>
|
<template #header><fa :icon="faHashtag"/>{{ $t('_widgets.trends') }}</template>
|
||||||
|
|
||||||
<div class="wbrkwala">
|
<div class="wbrkwala">
|
||||||
|
@ -15,8 +14,7 @@
|
||||||
</div>
|
</div>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
</div>
|
</div>
|
||||||
</mk-container>
|
</mk-container>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -28,7 +26,10 @@ import XChart from './trends.chart.vue';
|
||||||
export default define({
|
export default define({
|
||||||
name: 'hashtags',
|
name: 'hashtags',
|
||||||
props: () => ({
|
props: () => ({
|
||||||
compact: false
|
showHeader: {
|
||||||
|
type: 'boolean',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}).extend({
|
}).extend({
|
||||||
components: {
|
components: {
|
||||||
|
@ -49,10 +50,6 @@ export default define({
|
||||||
clearInterval(this.clock);
|
clearInterval(this.clock);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
func() {
|
|
||||||
this.props.compact = !this.props.compact;
|
|
||||||
this.save();
|
|
||||||
},
|
|
||||||
fetch() {
|
fetch() {
|
||||||
this.$root.api('hashtags/trend').then(stats => {
|
this.$root.api('hashtags/trend').then(stats => {
|
||||||
this.stats = stats;
|
this.stats = stats;
|
||||||
|
|
Loading…
Reference in a new issue