* 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:
syuilo 2020-07-11 10:13:11 +09:00 committed by GitHub
parent 5b28d7bf90
commit cf3fc97202
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 2695 additions and 907 deletions

View file

@ -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: "ダイレクト"

View file

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

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

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

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

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

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

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

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

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

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

View file

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

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -13,5 +13,6 @@
panelHeaderDivider: '@divider', panelHeaderDivider: '@divider',
panelBorder: '@divider', panelBorder: '@divider',
messageBg: '#1d1d1d', messageBg: '#1d1d1d',
deckColumnBorder: '@divider',
}, },
} }

View file

@ -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',
}, },
} }

View file

@ -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',
}, },
} }

View file

@ -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',
}, },
} }

View file

@ -1,18 +1,16 @@
<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>
<div> <div>
<mk-loading v-if="fetching"/> <mk-loading v-if="fetching"/>
<template v-else> <template v-else>
<x-calendar v-show="props.view === 0" :data="[].concat(activity)"/> <x-calendar v-show="props.view === 0" :data="[].concat(activity)"/>
<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;

View file

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

View file

@ -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"/>
<mk-analog-clock class="clock" :smooth="props.style < 2"/> </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>

View file

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

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

View file

@ -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',
];

View file

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

View file

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

View file

@ -1,19 +1,17 @@
<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="">
<mk-loading v-if="fetching"/> <mk-loading v-if="fetching"/>
<div v-else :class="$style.stream"> <div v-else :class="$style.stream">
<div v-for="(image, i) in images" :key="i" <div v-for="(image, i) in images" :key="i"
:class="$style.img" :class="$style.img"
:style="`background-image: url(${thumbnail(image)})`" :style="`background-image: url(${thumbnail(image)})`"
></div> ></div>
</div>
</div> </div>
</mk-container> </div>
</div> </mk-container>
</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;
} }

View file

@ -1,17 +1,15 @@
<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>
<div class="ekmkgxbj"> <div class="ekmkgxbj">
<mk-loading v-if="fetching"/> <mk-loading v-if="fetching"/>
<div class="feed" v-else> <div class="feed" v-else>
<a v-for="item in items" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a> <a v-for="item in items" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a>
</div>
</div> </div>
</mk-container> </div>
</div> </mk-container>
</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>

View file

@ -1,24 +1,22 @@
<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"/> <fa v-if="props.src === 'local'" :icon="faComments"/>
<fa v-if="props.src === 'local'" :icon="faComments"/> <fa v-if="props.src === 'social'" :icon="faShareAlt"/>
<fa v-if="props.src === 'social'" :icon="faShareAlt"/> <fa v-if="props.src === 'global'" :icon="faGlobe"/>
<fa v-if="props.src === 'global'" :icon="faGlobe"/> <fa v-if="props.src === 'list'" :icon="faListUl"/>
<fa v-if="props.src === 'list'" :icon="faListUl"/> <fa v-if="props.src === 'antenna'" :icon="faSatellite"/>
<fa v-if="props.src === 'antenna'" :icon="faSatellite"/> <span style="margin-left: 8px;">{{ props.src === 'list' ? props.list.name : props.src === 'antenna' ? props.antenna.name : $t('_timelines.' + props.src) }}</span>
<span style="margin-left: 8px;">{{ props.src === 'list' ? props.list.name : props.src === 'antenna' ? props.antenna.name : $t('_timelines.' + props.src) }}</span> <fa :icon="menuOpened ? faAngleUp : faAngleDown" style="margin-left: 8px;"/>
<fa :icon="menuOpened ? faAngleUp : faAngleDown" style="margin-left: 8px;"/> </button>
</button> </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>

View file

@ -1,22 +1,20 @@
<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">
<mk-loading v-if="fetching"/> <mk-loading v-if="fetching"/>
<transition-group tag="div" name="chart" class="tags" v-else> <transition-group tag="div" name="chart" class="tags" v-else>
<div v-for="stat in stats" :key="stat.tag"> <div v-for="stat in stats" :key="stat.tag">
<div class="tag"> <div class="tag">
<router-link class="a" :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</router-link> <router-link class="a" :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</router-link>
<p>{{ $t('nUsersMentioned', { n: stat.usersCount }) }}</p> <p>{{ $t('nUsersMentioned', { n: stat.usersCount }) }}</p>
</div>
<x-chart class="chart" :src="stat.chart"/>
</div> </div>
</transition-group> <x-chart class="chart" :src="stat.chart"/>
</div> </div>
</mk-container> </transition-group>
</div> </div>
</mk-container>
</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;