Improve desktop UX (#4262)

* wip

* wip

* wip

* wip

* wip

* wip

* Merge

* wip

* wip

* wip

* wip

* wip

* wip
This commit is contained in:
syuilo 2019-02-15 05:08:59 +09:00 committed by GitHub
parent 38ca514f53
commit 53422ffcb2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 1132 additions and 1222 deletions

View File

@ -28,6 +28,7 @@ common:
load-more: "もっと読み込む"
enter-password: "パスワードを入力してください"
2fa: "二段階認証"
customize-home: "ホームをカスタマイズ"
got-it: "わかった"
customization-tips:
@ -893,14 +894,10 @@ desktop/views/components/settings.vue:
web-search-engine-desc: "例: https://www.google.com/?#q={{query}}"
auto-popout: "ウィンドウの自動ポップアウト"
auto-popout-desc: "ウィンドウが開かれるとき、ポップアウト(ブラウザ外に切り離す)可能なら自動でポップアウトします。この設定はブラウザに記憶されます。"
deck-nav: "デッキ内ナビゲーション"
deck-nav-desc: "デッキを使用しているとき、ナビゲーションが発生する際にページ遷移を行わずに一時的なカラムで受けるようにします。"
keep-cw: "CW保持"
keep-cw-desc: "投稿にリプライする際、リプライ元の投稿にCWが設定されていたとき、デフォルトで同じCWを設定するようにします。"
deck-default: "デッキをデフォルトのUIにする"
display: "デザインと表示"
customize: "ホームをカスタマイズ"
wallpaper: "壁紙"
choose-wallpaper: "壁紙を選択"
delete-wallpaper: "壁紙を削除"
@ -1076,7 +1073,6 @@ desktop/views/components/ui.header.account.vue:
favorites: "お気に入り"
lists: "リスト"
follow-requests: "フォロー申請"
customize: "ホームのカスタマイズ"
admin: "管理"
settings: "設定"
signout: "サインアウト"
@ -1447,9 +1443,6 @@ desktop/views/pages/welcome.vue:
desktop/views/pages/drive.vue:
title: "Misskey Drive"
desktop/views/pages/home-customize.vue:
title: "ホームのカスタマイズ"
desktop/views/pages/note.vue:
prev: "前の投稿"
next: "次の投稿"
@ -1490,10 +1483,6 @@ desktop/views/pages/user/user.photos.vue:
loading: "読み込み中"
no-photos: "写真はありません"
desktop/views/pages/user/user.profile.vue:
follows-you: "フォローされています"
menu: "メニュー"
desktop/views/pages/user/user.header.vue:
posts: "投稿"
following: "フォロー"
@ -1503,6 +1492,7 @@ desktop/views/pages/user/user.header.vue:
year: "年"
month: "月"
day: "日"
follows-you: "フォローされています"
desktop/views/pages/user/user.timeline.vue:
default: "投稿"

View File

@ -1,5 +1,5 @@
<template>
<div class="mk-activity">
<div>
<div ref="chart"></div>
</div>
</template>
@ -9,7 +9,17 @@ import Vue from 'vue';
import ApexCharts from 'apexcharts';
export default Vue.extend({
props: ['user'],
props: {
user: {
type: Object,
required: true
},
limit: {
type: Number,
required: false,
default: 21
}
},
data() {
return {
fetching: true,
@ -21,7 +31,7 @@ export default Vue.extend({
this.$root.api('charts/user/notes', {
userId: this.user.id,
span: 'day',
limit: 21
limit: this.limit
}).then(stats => {
const normal = [];
const reply = [];
@ -32,7 +42,7 @@ export default Vue.extend({
const m = now.getMonth();
const d = now.getDate();
for (let i = 0; i < 21; i++) {
for (let i = 0; i < this.limit; i++) {
const x = new Date(y, m, d - i);
normal.push([
x,
@ -99,10 +109,3 @@ export default Vue.extend({
}
});
</script>
<style lang="stylus" scoped>
.mk-activity
max-width 600px
margin 0 auto
</style>

View File

@ -0,0 +1,11 @@
<template>
<div>
<slot></slot>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
});
</script>

View File

@ -1,8 +1,9 @@
<template>
<button class="wfliddvnhxvyusikowhxozkyxyenqxqr"
:class="{ wait, block, mini, active: isFollowing || hasPendingFollowRequestFromYou }"
:class="{ wait, block, inline, mini, active: isFollowing || hasPendingFollowRequestFromYou }"
@click="onClick"
:disabled="wait"
:inline="inline"
>
<template v-if="!wait">
<fa :icon="iconAndText[0]"/> <template v-if="!mini">{{ iconAndText[1] }}</template>
@ -28,6 +29,11 @@ export default Vue.extend({
required: false,
default: false
},
inline: {
type: Boolean,
required: false,
default: false
},
mini: {
type: Boolean,
required: false,
@ -128,6 +134,9 @@ export default Vue.extend({
border solid 1px var(--primary)
border-radius 36px
&.inline
display inline-block
&.mini
padding 0
min-width 0

View File

@ -1,5 +1,6 @@
import Vue from 'vue';
import dummy from './dummy.vue';
import userName from './user-name.vue';
import followButton from './follow-button.vue';
import error from './error.vue';
@ -46,6 +47,7 @@ import formButton from './ui/form/button.vue';
import formRadio from './ui/form/radio.vue';
Vue.component('mfm', misskeyFlavoredMarkdown);
Vue.component('mk-dummy', dummy);
Vue.component('mk-user-name', userName);
Vue.component('mk-follow-button', followButton);
Vue.component('mk-error', error);

View File

@ -13,6 +13,7 @@ import wSlideshow from './slideshow.vue';
import wTips from './tips.vue';
import wNav from './nav.vue';
import wHashtags from './hashtags.vue';
import wInstance from './instance.vue';
Vue.component('mkw-analog-clock', wAnalogClock);
Vue.component('mkw-nav', wNav);
@ -27,3 +28,4 @@ Vue.component('mkw-memo', wMemo);
Vue.component('mkw-rss', wRss);
Vue.component('mkw-version', wVersion);
Vue.component('mkw-hashtags', wHashtags);
Vue.component('mkw-instance', wInstance);

View File

@ -0,0 +1,14 @@
<template>
<div class="mkw-instance">
<mk-widget-container>
<mk-instance/>
</mk-widget-container>
</div>
</template>
<script lang="ts">
import define from '../../../common/define-widget';
export default define({
name: 'instance'
});
</script>

View File

@ -12,19 +12,13 @@ import init from '../init';
import fuckAdBlock from '../common/scripts/fuck-ad-block';
import composeNotification from '../common/scripts/compose-notification';
import MkIndex from './views/pages/index.vue';
import MkHome from './views/pages/home.vue';
import MkDeck from './views/pages/deck/deck.vue';
import MkUser from './views/pages/user/user.vue';
import MkHome from './views/home/home.vue';
import MkDeck from './views/deck/deck.vue';
import MkUserFollowingOrFollowers from './views/pages/user-following-or-followers.vue';
import MkFavorites from './views/pages/favorites.vue';
import MkSelectDrive from './views/pages/selectdrive.vue';
import MkDrive from './views/pages/drive.vue';
import MkHomeCustomize from './views/pages/home-customize.vue';
import MkMessagingRoom from './views/pages/messaging-room.vue';
import MkNote from './views/pages/note.vue';
import MkSearch from './views/pages/search.vue';
import MkTag from './views/pages/tag.vue';
import MkReversi from './views/pages/games/reversi.vue';
import MkShare from './views/pages/share.vue';
import MkFollow from '../common/views/pages/follow.vue';
@ -36,6 +30,7 @@ import PostFormWindow from './views/components/post-form-window.vue';
import RenoteFormWindow from './views/components/renote-form-window.vue';
import MkChooseFileFromDriveWindow from './views/components/choose-file-from-drive-window.vue';
import MkChooseFolderFromDriveWindow from './views/components/choose-folder-from-drive-window.vue';
import MkHomeTimeline from './views/home/timeline.vue';
import Notification from './views/components/ui-notification.vue';
import { url } from '../config';
@ -44,7 +39,7 @@ import MiOS from '../mios';
/**
* init
*/
init(async (launch) => {
init(async (launch, os) => {
Vue.mixin({
methods: {
$contextmenu(e, menu, opts?) {
@ -134,31 +129,37 @@ init(async (launch) => {
const router = new VueRouter({
mode: 'history',
routes: [
{ path: '/', name: 'index', component: MkIndex },
{ path: '/home', name: 'home', component: MkHome },
{ path: '/deck', name: 'deck', component: MkDeck },
{ path: '/i/customize-home', component: MkHomeCustomize },
{ path: '/i/favorites', component: MkFavorites },
os.store.getters.isSignedIn && os.store.state.device.deckMode
? { path: '/', name: 'index', component: MkDeck, children: [
{ path: '/@:user', name: 'user', component: () => import('./views/deck/deck.user-column.vue').then(m => m.default) },
{ path: '/notes/:note', name: 'note', component: () => import('./views/deck/deck.note-column.vue').then(m => m.default) },
{ path: '/tags/:tag', name: 'tag', component: () => import('./views/deck/deck.hashtag-column.vue').then(m => m.default) },
{ path: '/i/favorites', component: () => import('./views/deck/deck.favorites-column.vue').then(m => m.default) }
]}
: { path: '/', component: MkHome, children: [
{ path: '', name: 'index', component: MkHomeTimeline },
{ path: '/@:user', name: 'user', component: () => import('./views/home/user/user.vue').then(m => m.default) },
{ path: '/notes/:note', name: 'note', component: () => import('./views/home/note.vue').then(m => m.default) },
{ path: '/tags/:tag', name: 'tag', component: () => import('./views/home/tag.vue').then(m => m.default) },
{ path: '/i/favorites', component: () => import('./views/home/favorites.vue').then(m => m.default) }
]},
{ path: '/i/messaging/:user', component: MkMessagingRoom },
{ path: '/i/drive', component: MkDrive },
{ path: '/i/drive/folder/:folder', component: MkDrive },
{ path: '/i/settings', component: MkSettings },
{ path: '/selectdrive', component: MkSelectDrive },
{ path: '/search', component: MkSearch },
{ path: '/tags/:tag', name: 'tag', component: MkTag },
{ path: '/share', component: MkShare },
{ path: '/games/reversi/:game?', component: MkReversi },
{ path: '/@:user', name: 'user', component: MkUser },
{ path: '/@:user/following', name: 'userFollowing', component: MkUserFollowingOrFollowers },
{ path: '/@:user/followers', name: 'userFollowers', component: MkUserFollowingOrFollowers },
{ path: '/notes/:note', name: 'note', component: MkNote },
{ path: '/authorize-follow', component: MkFollow },
{ path: '*', component: MkNotFound }
]
});
// Launch the app
const [app, os] = launch(router);
const [app, _] = launch(router);
if (os.store.getters.isSignedIn) {
/**

View File

@ -1,396 +0,0 @@
<template>
<div class="mk-home" :data-customize="customize">
<div class="customize" v-if="customize">
<router-link to="/"><fa icon="check"/>{{ $t('done') }}</router-link>
<div>
<div class="adder">
<p>{{ $t('add-widget') }}</p>
<select v-model="widgetAdderSelected">
<option value="profile">{{ $t('@.widgets.profile') }}</option>
<option value="analog-clock">{{ $t('@.widgets.analog-clock') }}</option>
<option value="calendar">{{ $t('@.widgets.calendar') }}</option>
<option value="timemachine">{{ $t('@.widgets.timemachine') }}</option>
<option value="activity">{{ $t('@.widgets.activity') }}</option>
<option value="rss">{{ $t('@.widgets.rss') }}</option>
<option value="trends">{{ $t('@.widgets.trends') }}</option>
<option value="photo-stream">{{ $t('@.widgets.photo-stream') }}</option>
<option value="slideshow">{{ $t('@.widgets.slideshow') }}</option>
<option value="version">{{ $t('@.widgets.version') }}</option>
<option value="broadcast">{{ $t('@.widgets.broadcast') }}</option>
<option value="notifications">{{ $t('@.widgets.notifications') }}</option>
<option value="users">{{ $t('@.widgets.users') }}</option>
<option value="polls">{{ $t('@.widgets.polls') }}</option>
<option value="post-form">{{ $t('@.widgets.post-form') }}</option>
<option value="messaging">{{ $t('@.widgets.messaging') }}</option>
<option value="memo">{{ $t('@.widgets.memo') }}</option>
<option value="hashtags">{{ $t('@.widgets.hashtags') }}</option>
<option value="posts-monitor">{{ $t('@.widgets.posts-monitor') }}</option>
<option value="server">{{ $t('@.widgets.server') }}</option>
<option value="nav">{{ $t('@.widgets.nav') }}</option>
<option value="tips">{{ $t('@.widgets.tips') }}</option>
</select>
<button @click="addWidget">{{ $t('add') }}</button>
</div>
<div class="trash">
<x-draggable v-model="trash" :options="{ group: 'x' }" @add="onTrash"></x-draggable>
<p>{{ $t('@.trash') }}</p>
</div>
</div>
</div>
<div class="main" :class="{ side: widgets.left.length == 0 || widgets.right.length == 0 }">
<template v-if="customize">
<x-draggable v-for="place in ['left', 'right']"
:list="widgets[place]"
:class="place"
:data-place="place"
:options="{ group: 'x', animation: 150 }"
@sort="onWidgetSort"
:key="place"
>
<div v-for="widget in widgets[place]" class="customize-container" :key="widget.id" @contextmenu.stop.prevent="onWidgetContextmenu(widget.id)">
<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="desktop"/>
</div>
</x-draggable>
<div class="main">
<a @click="hint">{{ $t('@.customization-tips.title') }}</a>
<div>
<mk-post-form v-if="$store.state.settings.showPostFormOnTopOfTl"/>
<mk-timeline ref="tl" @loaded="onTlLoaded"/>
</div>
</div>
</template>
<template v-else>
<div v-for="place in ['left', 'right']" :class="place">
<component v-for="widget in widgets[place]" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" @chosen="warp" platform="desktop"/>
</div>
<div class="main">
<mk-post-form class="form" v-if="$store.state.settings.showPostFormOnTopOfTl"/>
<mk-timeline class="tl" ref="tl" @loaded="onTlLoaded" v-if="mode == 'timeline'"/>
</div>
</template>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
import * as XDraggable from 'vuedraggable';
import * as uuid from 'uuid';
const defaultDesktopHomeWidgets = {
left: [
'profile',
'calendar',
'activity',
'rss',
'hashtags',
'photo-stream',
'version'
],
right: [
'broadcast',
'notifications',
'users',
'polls',
'server',
'nav',
'tips'
]
};
//#region Construct home data
const _defaultDesktopHomeWidgets = [];
for (const widget of defaultDesktopHomeWidgets.left) {
_defaultDesktopHomeWidgets.push({
name: widget,
id: uuid(),
place: 'left',
data: {}
});
}
for (const widget of defaultDesktopHomeWidgets.right) {
_defaultDesktopHomeWidgets.push({
name: widget,
id: uuid(),
place: 'right',
data: {}
});
}
//#endregion
export default Vue.extend({
i18n: i18n('desktop/views/components/home.vue'),
components: {
XDraggable
},
props: {
customize: {
type: Boolean,
default: false
},
mode: {
type: String,
default: 'timeline'
}
},
data() {
return {
connection: null,
widgetAdderSelected: null,
trash: []
};
},
computed: {
home(): any[] {
return this.$store.state.settings.home || [];
},
left(): any[] {
return this.home.filter(w => w.place == 'left');
},
right(): any[] {
return this.home.filter(w => w.place == 'right');
},
widgets(): any {
return {
left: this.left,
right: this.right
};
}
},
created() {
if (this.$store.state.settings.home == null) {
this.$root.api('i/update_home', {
home: _defaultDesktopHomeWidgets
}).then(() => {
this.$store.commit('settings/setHome', _defaultDesktopHomeWidgets);
});
}
},
mounted() {
this.connection = this.$root.stream.useSharedConnection('main');
},
beforeDestroy() {
this.connection.dispose();
},
methods: {
hint() {
this.$root.dialog({
title: this.$t('@.customization-tips.title'),
text: this.$t('@.customization-tips.paragraph')
});
},
onTlLoaded() {
this.$emit('loaded');
},
onWidgetContextmenu(widgetId) {
const w = (this.$refs[widgetId] as any)[0];
if (w.func) w.func();
},
onWidgetSort() {
this.saveHome();
},
onTrash(evt) {
this.saveHome();
},
addWidget() {
this.$store.dispatch('settings/addHomeWidget', {
name: this.widgetAdderSelected,
id: uuid(),
place: 'left',
data: {}
});
},
saveHome() {
const left = this.widgets.left;
const right = this.widgets.right;
this.$store.commit('settings/setHome', left.concat(right));
for (const w of left) w.place = 'left';
for (const w of right) w.place = 'right';
this.$root.api('i/update_home', {
home: this.home
});
},
warp(date) {
(this.$refs.tl as any).warp(date);
},
focus() {
(this.$refs.tl as any).focus();
}
}
});
</script>
<style lang="stylus" scoped>
.mk-home
display block
&[data-customize]
padding-top 48px
background-image url('/assets/desktop/grid.svg')
> .main > .main
> a
display block
margin-bottom 8px
text-align center
> div
cursor not-allowed !important
> *
pointer-events none
&:not([data-customize])
> .main > *:empty
display none
> .customize
position fixed
z-index 1000
top 0
left 0
width 100%
height 48px
color var(--text)
background var(--desktopHeaderBg)
box-shadow 0 1px 1px rgba(#000, 0.075)
> a
display block
position absolute
z-index 1001
top 0
right 0
padding 0 16px
line-height 48px
text-decoration none
color var(--primaryForeground)
background var(--primary)
transition background 0.1s ease
&:hover
background var(--primaryLighten10)
&:active
background var(--primaryDarken10)
transition background 0s ease
> [data-icon]
margin-right 8px
> div
display flex
margin 0 auto
max-width 1220px - 32px
> div
width 50%
&.adder
> p
display inline
line-height 48px
&.trash
border-left solid 1px var(--faceDivider)
> div
width 100%
height 100%
> p
position absolute
top 0
left 0
width 100%
line-height 48px
margin 0
text-align center
pointer-events none
> .main
display flex
justify-content center
margin 0 auto
max-width 1240px
> *
.customize-container
cursor move
border-radius 6px
&:hover
box-shadow 0 0 8px rgba(64, 120, 200, 0.3)
> *
pointer-events none
> .main
padding 16px
width calc(100% - 280px * 2)
order 2
> .form
margin-bottom 16px
box-shadow var(--shadow)
border-radius var(--round)
&.side
> .main
width calc(100% - 280px)
max-width 680px
> *:not(.main)
width 280px
padding 16px 0 16px 0
> *:not(:last-child)
margin-bottom 16px
> .left
padding-left 16px
order 1
> .right
padding-right 16px
order 3
&.side
@media (max-width 1000px)
> *:not(.main)
display none
> .main
width 100%
max-width 700px
margin 0 auto
&:not(.side)
@media (max-width 1200px)
> *:not(.main)
display none
> .main
width 100%
max-width 700px
margin 0 auto
</style>

View File

@ -2,8 +2,6 @@ import Vue from 'vue';
import ui from './ui.vue';
import uiNotification from './ui-notification.vue';
import home from './home.vue';
import timeline from './timeline.vue';
import notes from './notes.vue';
import subNoteContent from './sub-note-content.vue';
import window from './window.vue';
@ -24,8 +22,6 @@ import widgetContainer from './widget-container.vue';
Vue.component('mk-ui', ui);
Vue.component('mk-ui-notification', uiNotification);
Vue.component('mk-home', home);
Vue.component('mk-timeline', timeline);
Vue.component('mk-notes', notes);
Vue.component('mk-sub-note-content', subNoteContent);
Vue.component('mk-window', window);

View File

@ -31,9 +31,6 @@
<ui-switch v-model="autoPopout">{{ $t('auto-popout') }}
<span slot="desc">{{ $t('auto-popout-desc') }}</span>
</ui-switch>
<ui-switch v-model="deckNav">{{ $t('deck-nav') }}
<span slot="desc">{{ $t('deck-nav-desc') }}</span>
</ui-switch>
<ui-switch v-model="keepCw">{{ $t('keep-cw') }}
<span slot="desc">{{ $t('keep-cw-desc') }}</span>
</ui-switch>
@ -89,9 +86,6 @@
<ui-radio v-model="navbar" value="left">{{ $t('navbar-position-left') }}</ui-radio>
<ui-radio v-model="navbar" value="right">{{ $t('navbar-position-right') }}</ui-radio>
</section>
<section>
<ui-switch v-model="deckDefault">{{ $t('deck-default') }}</ui-switch>
</section>
<section>
<ui-switch v-model="darkmode">{{ $t('dark-mode') }}</ui-switch>
<ui-switch v-model="useShadow">{{ $t('use-shadow') }}</ui-switch>
@ -337,11 +331,6 @@ export default Vue.extend({
set(value) { this.$store.commit('device/set', { key: 'autoPopout', value }); }
},
deckNav: {
get() { return this.$store.state.settings.deckNav; },
set(value) { this.$store.commit('settings/set', { key: 'deckNav', value }); }
},
keepCw: {
get() { return this.$store.state.settings.keepCw; },
set(value) { this.$store.commit('settings/set', { key: 'keepCw', value }); }
@ -367,11 +356,6 @@ export default Vue.extend({
set(value) { this.$store.commit('device/set', { key: 'deckColumnWidth', value }); }
},
deckDefault: {
get() { return this.$store.state.device.deckDefault; },
set(value) { this.$store.commit('device/set', { key: 'deckDefault', value }); }
},
enableSounds: {
get() { return this.$store.state.device.enableSounds; },
set(value) { this.$store.commit('device/set', { key: 'enableSounds', value }); }
@ -534,8 +518,7 @@ export default Vue.extend({
},
methods: {
customizeHome() {
this.$router.push('/i/customize-home');
this.$emit('done');
location.href = '/?customize';
},
updateWallpaper() {
this.$chooseDriveFile({

View File

@ -44,13 +44,6 @@
</li>
</ul>
<ul>
<li>
<router-link to="/i/customize-home">
<i><fa icon="wrench"/></i>
<span>{{ $t('customize') }}</span>
<i><fa icon="angle-right"/></i>
</router-link>
</li>
<li>
<router-link to="/i/settings">
<i><fa icon="cog"/></i>

View File

@ -2,20 +2,20 @@
<div class="nav">
<ul>
<template v-if="$store.getters.isSignedIn">
<template v-if="$store.state.device.deckDefault">
<li class="deck" :class="{ active: $route.name == 'deck' || $route.name == 'index' }" @click="goToTop">
<template v-if="$store.state.device.deckMode">
<li class="deck active" @click="goToTop">
<router-link to="/"><fa icon="columns"/><p>{{ $t('deck') }}</p></router-link>
</li>
<li class="home" :class="{ active: $route.name == 'home' }" @click="goToTop">
<router-link to="/home"><fa icon="home"/><p>{{ $t('home') }}</p></router-link>
<li class="home">
<a @click="toggleDeckMode(false)"><fa icon="home"/><p>{{ $t('home') }}</p></a>
</li>
</template>
<template v-else>
<li class="home" :class="{ active: $route.name == 'home' || $route.name == 'index' }" @click="goToTop">
<li class="home active" @click="goToTop">
<router-link to="/"><fa icon="home"/><p>{{ $t('home') }}</p></router-link>
</li>
<li class="deck" :class="{ active: $route.name == 'deck' }" @click="goToTop">
<router-link to="/deck"><fa icon="columns"/><p>{{ $t('deck') }}</p></router-link>
<li class="deck">
<a @click="toggleDeckMode(true)"><fa icon="columns"/><p>{{ $t('deck') }}</p></a>
</li>
</template>
<li class="messaging">
@ -70,6 +70,11 @@ export default Vue.extend({
}
},
methods: {
toggleDeckMode(deck) {
this.$store.commit('device/set', { key: 'deckMode', value: deck });
location.reload();
},
onReversiInvited() {
this.hasGameInvitations = true;
},

View File

@ -6,7 +6,7 @@
</div>
<div class="nav" v-if="$store.getters.isSignedIn">
<template v-if="$store.state.device.deckDefault">
<template v-if="$store.state.device.deckMode">
<div class="deck" :class="{ active: $route.name == 'deck' || $route.name == 'index' }" @click="goToTop">
<router-link to="/"><fa icon="columns"/></router-link>
</div>

View File

@ -27,9 +27,9 @@
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../../i18n';
import Menu from '../../../../common/views/components/menu.vue';
import { countIf } from '../../../../../../prelude/array';
import i18n from '../../../i18n';
import Menu from '../../../common/views/components/menu.vue';
import { countIf } from '../../../../../prelude/array';
export default Vue.extend({
i18n: i18n('deck'),
@ -245,10 +245,7 @@ export default Vue.extend({
},
close() {
this.$store.commit('device/set', {
key: 'deckTemporaryColumn',
value: null
});
this.$router.push('/');
},
goTop() {

View File

@ -8,7 +8,7 @@
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../../i18n';
import i18n from '../../../i18n';
import XColumn from './deck.column.vue';
import XDirect from './deck.direct.vue';

View File

@ -0,0 +1,88 @@
<template>
<x-column>
<span slot="header">
<fa :icon="['fa', 'star']"/>{{ $t('favorites') }}
</span>
<div>
<x-notes ref="timeline" :more="existMore ? more : null"/>
</div>
</x-column>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
import XColumn from './deck.column.vue';
import XNotes from './deck.notes.vue';
const fetchLimit = 10;
export default Vue.extend({
i18n: i18n(),
components: {
XColumn,
XNotes,
},
data() {
return {
fetching: true,
moreFetching: false,
existMore: false,
};
},
mounted() {
this.fetch();
},
methods: {
fetch() {
this.fetching = true;
(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
this.$root.api('i/favorites', {
limit: fetchLimit + 1,
}).then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
this.existMore = true;
}
res(notes.map(x => x.note));
this.fetching = false;
this.$emit('loaded');
}, rej);
}));
},
more() {
this.moreFetching = true;
const promise = this.$root.api('i/favorites', {
limit: fetchLimit + 1,
untilId: (this.$refs.timeline as any).tail().id,
});
promise.then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
} else {
this.existMore = false;
}
for (const n of notes) {
(this.$refs.timeline as any).append(n);
}
this.moreFetching = false;
});
return promise;
},
focus() {
this.$refs.timeline.focus();
}
}
});
</script>

View File

@ -0,0 +1,119 @@
<template>
<x-column>
<span slot="header">
<fa icon="hashtag"/><span>{{ tag }}</span>
</span>
<div class="xroyrflcmhhtmlwmyiwpfqiirqokfueb">
<div ref="chart" class="chart"></div>
<x-hashtag-tl :tag-tl="tagTl" class="tl"/>
</div>
</x-column>
</template>
<script lang="ts">
import Vue from 'vue';
import XColumn from './deck.column.vue';
import XHashtagTl from './deck.hashtag-tl.vue';
import ApexCharts from 'apexcharts';
export default Vue.extend({
components: {
XColumn,
XHashtagTl
},
computed: {
tag(): string {
return this.$route.params.tag;
},
tagTl(): any {
return {
query: [[this.tag]]
};
}
},
watch: {
$route: 'fetch'
},
created() {
this.fetch();
},
methods: {
fetch() {
this.$root.api('charts/hashtag', {
tag: this.tag,
span: 'hour',
limit: 24
}).then(stats => {
const local = [];
const remote = [];
const now = new Date();
const y = now.getFullYear();
const m = now.getMonth();
const d = now.getDate();
const h = now.getHours();
for (let i = 0; i < 24; i++) {
const x = new Date(y, m, d, h - i);
local.push([x, stats.local.count[i]]);
remote.push([x, stats.remote.count[i]]);
}
const chart = new ApexCharts(this.$refs.chart, {
chart: {
type: 'area',
height: 70,
sparkline: {
enabled: true
},
},
grid: {
clipMarkers: false,
padding: {
top: 16,
right: 16,
bottom: 16,
left: 16
}
},
stroke: {
curve: 'straight',
width: 2
},
series: [{
name: 'Local',
data: local
}, {
name: 'Remote',
data: remote
}],
xaxis: {
type: 'datetime',
}
});
chart.render();
});
}
}
});
</script>
<style lang="stylus" scoped>
.xroyrflcmhhtmlwmyiwpfqiirqokfueb
background var(--deckColumnBg)
> .chart
margin-bottom 16px
background var(--face)
> .tl
background var(--face)
</style>

View File

@ -8,7 +8,7 @@
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../../i18n';
import i18n from '../../../i18n';
import XColumn from './deck.column.vue';
import XMentions from './deck.mentions.vue';

View File

@ -18,10 +18,10 @@
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../../i18n';
import i18n from '../../../i18n';
import XColumn from './deck.column.vue';
import XNotes from './deck.notes.vue';
import XNote from '../../components/note.vue';
import XNote from '../components/note.vue';
export default Vue.extend({
i18n: i18n(),
@ -31,13 +31,6 @@ export default Vue.extend({
XNote
},
props: {
noteId: {
type: String,
required: true
}
},
data() {
return {
note: null,
@ -45,12 +38,26 @@ export default Vue.extend({
};
},
watch: {
$route: 'fetch'
},
created() {
this.$root.api('notes/show', { noteId: this.noteId }).then(note => {
this.fetch();
},
methods: {
fetch() {
this.fetching = true;
this.$root.api('notes/show', {
noteId: this.$route.params.note
}).then(note => {
this.note = note;
this.fetching = false;
});
}
}
});
</script>

View File

@ -38,10 +38,10 @@
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../../i18n';
import shouldMuteNote from '../../../../common/scripts/should-mute-note';
import i18n from '../../../i18n';
import shouldMuteNote from '../../../common/scripts/should-mute-note';
import XNote from '../../components/note.vue';
import XNote from '../components/note.vue';
const displayLimit = 20;

View File

@ -96,8 +96,8 @@
<script lang="ts">
import Vue from 'vue';
import getNoteSummary from '../../../../../../misc/get-note-summary';
import XNote from '../../components/note.vue';
import getNoteSummary from '../../../../../misc/get-note-summary';
import XNote from '../components/note.vue';
export default Vue.extend({
components: {

View File

@ -8,7 +8,7 @@
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../../i18n';
import i18n from '../../../i18n';
import XColumn from './deck.column.vue';
import XNotifications from './deck.notifications.vue';

View File

@ -25,7 +25,7 @@
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../../i18n';
import i18n from '../../../i18n';
import XNotification from './deck.notification.vue';
const displayLimit = 20;

View File

@ -38,7 +38,7 @@
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../../i18n';
import i18n from '../../../i18n';
import XColumn from './deck.column.vue';
import XTl from './deck.tl.vue';
import XListTl from './deck.list-tl.vue';

View File

@ -93,13 +93,13 @@
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../../i18n';
import parseAcct from '../../../../../../misc/acct/parse';
import i18n from '../../../i18n';
import parseAcct from '../../../../../misc/acct/parse';
import XColumn from './deck.column.vue';
import XNotes from './deck.notes.vue';
import XNote from '../../components/note.vue';
import XUserMenu from '../../../../common/views/components/user-menu.vue';
import { concat } from '../../../../../../prelude/array';
import XNote from '../components/note.vue';
import XUserMenu from '../../../common/views/components/user-menu.vue';
import { concat } from '../../../../../prelude/array';
import ApexCharts from 'apexcharts';
const fetchLimit = 10;
@ -112,13 +112,6 @@ export default Vue.extend({
XNote
},
props: {
acct: {
type: String,
required: true
}
},
data() {
return {
user: null,
@ -144,8 +137,18 @@ export default Vue.extend({
},
},
watch: {
$route: 'fetch'
},
created() {
this.$root.api('users/show', parseAcct(this.acct)).then(user => {
this.fetch();
},
methods: {
fetch() {
this.fetching = true;
this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => {
this.user = user;
this.fetching = false;
@ -256,7 +259,6 @@ export default Vue.extend({
});
},
methods: {
initTl() {
return new Promise((res, rej) => {
this.$root.api('users/notes', {

View File

@ -9,11 +9,7 @@
</div>
<x-column-core v-else :ref="ids[0]" :key="ids[0]" :column="columns.find(c => c.id == ids[0])" @parentFocus="moveFocus(ids[0], $event)"/>
</template>
<template v-if="temporaryColumn">
<x-user-column v-if="temporaryColumn.type == 'user'" :acct="temporaryColumn.acct" :key="temporaryColumn.acct"/>
<x-note-column v-else-if="temporaryColumn.type == 'note'" :note-id="temporaryColumn.noteId" :key="temporaryColumn.noteId"/>
<x-hashtag-column v-else-if="temporaryColumn.type == 'tag'" :tag="temporaryColumn.tag" :key="temporaryColumn.tag"/>
</template>
<router-view></router-view>
<button ref="add" @click="add" :title="$t('@deck.add-column')"><fa icon="plus"/></button>
</div>
</mk-ui>
@ -21,20 +17,17 @@
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../../i18n';
import i18n from '../../../i18n';
import XColumnCore from './deck.column-core.vue';
import Menu from '../../../../common/views/components/menu.vue';
import MkUserListsWindow from '../../components/user-lists-window.vue';
import Menu from '../../../common/views/components/menu.vue';
import MkUserListsWindow from '../components/user-lists-window.vue';
import * as uuid from 'uuid';
export default Vue.extend({
i18n: i18n('deck'),
components: {
XColumnCore,
XUserColumn: () => import('./deck.user-column.vue').then(m => m.default),
XNoteColumn: () => import('./deck.note-column.vue').then(m => m.default),
XHashtagColumn: () => import('./deck.hashtag-column.vue').then(m => m.default)
XColumnCore
},
computed: {
@ -55,10 +48,6 @@ export default Vue.extend({
};
},
temporaryColumn(): any {
return this.$store.state.device.deckTemporaryColumn;
},
keymap(): any {
return {
't': this.focus
@ -66,7 +55,7 @@ export default Vue.extend({
}
},
watch: {
watch: {/*
temporaryColumn() {
if (this.temporaryColumn != null) {
this.$nextTick(() => {
@ -76,7 +65,7 @@ export default Vue.extend({
});
});
}
}
}*/
},
provide() {
@ -86,8 +75,6 @@ export default Vue.extend({
},
created() {
this.$store.commit('navHook', this.onNav);
if (this.$store.state.settings.deck == null) {
const deck = {
columns: [/*{
@ -133,8 +120,6 @@ export default Vue.extend({
},
beforeDestroy() {
this.$store.commit('navHook', null);
document.documentElement.style.overflow = 'auto';
},
@ -143,39 +128,6 @@ export default Vue.extend({
return this.$refs[id][0];
},
onNav(to) {
if (!this.$store.state.settings.deckNav) return false;
if (to.name == 'user') {
this.$store.commit('device/set', {
key: 'deckTemporaryColumn',
value: {
type: 'user',
acct: to.params.user
}
});
return true;
} else if (to.name == 'note') {
this.$store.commit('device/set', {
key: 'deckTemporaryColumn',
value: {
type: 'note',
noteId: to.params.note
}
});
return true;
} else if (to.name == 'tag') {
this.$store.commit('device/set', {
key: 'deckTemporaryColumn',
value: {
type: 'tag',
tag: to.params.tag
}
});
return true;
}
},
add() {
this.$root.new(Menu, {
source: this.$refs.add,

View File

@ -50,7 +50,7 @@
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../../i18n';
import i18n from '../../../i18n';
import XColumn from './deck.column.vue';
import * as XDraggable from 'vuedraggable';
import * as uuid from 'uuid';

View File

@ -1,6 +1,5 @@
<template>
<mk-ui>
<main v-if="!fetching">
<div class="ecsvsegy" v-if="!fetching">
<sequential-entrance animation="entranceFromTop" delay="25">
<template v-for="favorite in favorites">
<mk-note-detail class="post" :note="favorite.note" :key="favorite.note.id"/>
@ -9,8 +8,7 @@
<div class="more" v-if="existMore">
<ui-button inline @click="more">{{ $t('@.load-more') }}</ui-button>
</div>
</main>
</mk-ui>
</div>
</template>
<script lang="ts">
@ -72,10 +70,8 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
main
.ecsvsegy
margin 0 auto
padding 16px
max-width 700px
> * > .post
margin-bottom 16px

View File

@ -0,0 +1,400 @@
<template>
<component :is="customize ? 'mk-dummy' : 'mk-ui'" v-hotkey.global="keymap" v-if="$store.getters.isSignedIn || $route.name != 'index'">
<div class="wqsofvpm" :data-customize="customize">
<div class="customize" v-if="customize">
<a @click="done()"><fa icon="check"/>{{ $t('done') }}</a>
<div>
<div class="adder">
<p>{{ $t('add-widget') }}</p>
<select v-model="widgetAdderSelected">
<option value="profile">{{ $t('@.widgets.profile') }}</option>
<option value="analog-clock">{{ $t('@.widgets.analog-clock') }}</option>
<option value="calendar">{{ $t('@.widgets.calendar') }}</option>
<option value="timemachine">{{ $t('@.widgets.timemachine') }}</option>
<option value="activity">{{ $t('@.widgets.activity') }}</option>
<option value="rss">{{ $t('@.widgets.rss') }}</option>
<option value="trends">{{ $t('@.widgets.trends') }}</option>
<option value="photo-stream">{{ $t('@.widgets.photo-stream') }}</option>
<option value="slideshow">{{ $t('@.widgets.slideshow') }}</option>
<option value="version">{{ $t('@.widgets.version') }}</option>
<option value="broadcast">{{ $t('@.widgets.broadcast') }}</option>
<option value="notifications">{{ $t('@.widgets.notifications') }}</option>
<option value="users">{{ $t('@.widgets.users') }}</option>
<option value="polls">{{ $t('@.widgets.polls') }}</option>
<option value="post-form">{{ $t('@.widgets.post-form') }}</option>
<option value="messaging">{{ $t('@.widgets.messaging') }}</option>
<option value="memo">{{ $t('@.widgets.memo') }}</option>
<option value="hashtags">{{ $t('@.widgets.hashtags') }}</option>
<option value="posts-monitor">{{ $t('@.widgets.posts-monitor') }}</option>
<option value="server">{{ $t('@.widgets.server') }}</option>
<option value="nav">{{ $t('@.widgets.nav') }}</option>
<option value="tips">{{ $t('@.widgets.tips') }}</option>
</select>
<button @click="addWidget">{{ $t('add') }}</button>
</div>
<div class="trash">
<x-draggable v-model="trash" :options="{ group: 'x' }" @add="onTrash"></x-draggable>
<p>{{ $t('@.trash') }}</p>
</div>
</div>
</div>
<div class="main" :class="{ side: widgets.left.length == 0 || widgets.right.length == 0 }">
<template v-if="customize">
<x-draggable v-for="place in ['left', 'right']"
:list="widgets[place]"
:class="place"
:data-place="place"
:options="{ group: 'x', animation: 150 }"
@sort="onWidgetSort"
:key="place"
>
<div v-for="widget in widgets[place]" class="customize-container" :key="widget.id" @contextmenu.stop.prevent="onWidgetContextmenu(widget.id)">
<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="desktop"/>
</div>
</x-draggable>
<div class="main">
<a @click="hint">{{ $t('@.customization-tips.title') }}</a>
<div>
<x-timeline/>
</div>
</div>
</template>
<template v-else>
<div v-for="place in ['left', 'right']" :class="place">
<component v-for="widget in widgets[place]" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" platform="desktop"/>
</div>
<div class="main">
<router-view ref="content"></router-view>
</div>
</template>
</div>
</div>
</component>
<x-welcome v-else/>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
import * as XDraggable from 'vuedraggable';
import * as uuid from 'uuid';
import XWelcome from '../pages/welcome.vue';
export default Vue.extend({
i18n: i18n('desktop/views/components/home.vue'),
components: {
XDraggable,
XWelcome
},
data() {
return {
customize: window.location.search == '?customize',
connection: null,
widgetAdderSelected: null,
trash: [],
view: null
};
},
computed: {
home(): any[] {
if (this.$store.getters.isSignedIn) {
return this.$store.state.settings.home || [];
} else {
return [{
name: 'instance',
place: 'right'
}];
}
},
left(): any[] {
return this.home.filter(w => w.place == 'left');
},
right(): any[] {
return this.home.filter(w => w.place == 'right');
},
widgets(): any {
return {
left: this.left,
right: this.right
};
},
keymap(): any {
return {
't': this.focus
};
}
},
created() {
if (this.$store.getters.isSignedIn) {
const defaultDesktopHomeWidgets = {
left: [
'profile',
'calendar',
'activity',
'rss',
'hashtags',
'photo-stream',
'version'
],
right: [
'customize',
'broadcast',
'notifications',
'users',
'polls',
'server',
'nav',
'tips'
]
};
//#region Construct home data
const _defaultDesktopHomeWidgets = [];
for (const widget of defaultDesktopHomeWidgets.left) {
_defaultDesktopHomeWidgets.push({
name: widget,
id: uuid(),
place: 'left',
data: {}
});
}
for (const widget of defaultDesktopHomeWidgets.right) {
_defaultDesktopHomeWidgets.push({
name: widget,
id: uuid(),
place: 'right',
data: {}
});
}
//#endregion
if (this.$store.state.settings.home == null) {
this.$root.api('i/update_home', {
home: _defaultDesktopHomeWidgets
}).then(() => {
this.$store.commit('settings/setHome', _defaultDesktopHomeWidgets);
});
}
}
},
mounted() {
this.connection = this.$root.stream.useSharedConnection('main');
},
beforeDestroy() {
this.connection.dispose();
},
methods: {
hint() {
this.$root.dialog({
title: this.$t('@.customization-tips.title'),
text: this.$t('@.customization-tips.paragraph')
});
},
onTlLoaded() {
this.$emit('loaded');
},
onWidgetContextmenu(widgetId) {
const w = (this.$refs[widgetId] as any)[0];
if (w.func) w.func();
},
onWidgetSort() {
this.saveHome();
},
onTrash(evt) {
this.saveHome();
},
addWidget() {
this.$store.dispatch('settings/addHomeWidget', {
name: this.widgetAdderSelected,
id: uuid(),
place: 'left',
data: {}
});
},
saveHome() {
const left = this.widgets.left;
const right = this.widgets.right;
this.$store.commit('settings/setHome', left.concat(right));
for (const w of left) w.place = 'left';
for (const w of right) w.place = 'right';
this.$root.api('i/update_home', {
home: this.home
});
},
done() {
location.href = '/';
},
focus() {
(this.$refs.content as any).focus();
}
}
});
</script>
<style lang="stylus" scoped>
.wqsofvpm
display block
&[data-customize]
padding-top 48px
background-image url('/assets/desktop/grid.svg')
> .main > .main
> a
display block
margin-bottom 8px
text-align center
> div
cursor not-allowed !important
> *
pointer-events none
&:not([data-customize])
> .main > *:not(.main):empty
display none
> .customize
position fixed
z-index 1000
top 0
left 0
width 100%
height 48px
color var(--text)
background var(--desktopHeaderBg)
box-shadow 0 1px 1px rgba(#000, 0.075)
> a
display block
position absolute
z-index 1001
top 0
right 0
padding 0 16px
line-height 48px
text-decoration none
color var(--primaryForeground)
background var(--primary)
transition background 0.1s ease
&:hover
background var(--primaryLighten10)
&:active
background var(--primaryDarken10)
transition background 0s ease
> [data-icon]
margin-right 8px
> div
display flex
margin 0 auto
max-width 1220px - 32px
> div
width 50%
&.adder
> p
display inline
line-height 48px
&.trash
border-left solid 1px var(--faceDivider)
> div
width 100%
height 100%
> p
position absolute
top 0
left 0
width 100%
line-height 48px
margin 0
text-align center
pointer-events none
> .main
display flex
justify-content center
margin 0 auto
max-width 1240px
> *
.customize-container
cursor move
border-radius 6px
&:hover
box-shadow 0 0 8px rgba(64, 120, 200, 0.3)
> *
pointer-events none
> .main
padding 16px
width calc(100% - 280px * 2)
order 2
&.side
> .main
width calc(100% - 280px)
max-width 680px
> *:not(.main)
width 280px
padding 16px 0 16px 0
> *:not(:last-child)
margin-bottom 16px
> .left
padding-left 16px
order 1
> .right
padding-right 16px
order 3
&.side
@media (max-width 1000px)
> *:not(.main)
display none
> .main
width 100%
max-width 700px
margin 0 auto
&:not(.side)
@media (max-width 1200px)
> *:not(.main)
display none
> .main
width 100%
max-width 700px
margin 0 auto
</style>

View File

@ -1,13 +1,11 @@
<template>
<mk-ui>
<main v-if="!fetching">
<div v-if="!fetching" class="kcthdwmv">
<mk-note-detail :note="note"/>
<footer>
<router-link v-if="note.next" :to="note.next"><fa icon="angle-left"/> {{ $t('next') }}</router-link>
<router-link v-if="note.prev" :to="note.prev">{{ $t('prev') }} <fa icon="angle-right"/></router-link>
</footer>
</main>
</mk-ui>
</div>
</template>
<script lang="ts">
@ -48,8 +46,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
main
padding 16px
.kcthdwmv
text-align center
> footer

View File

@ -1,11 +1,8 @@
<template>
<mk-ui>
<header :class="$style.header">
<h1>#{{ $route.params.tag }}</h1>
</header>
<div>
<p :class="$style.empty" v-if="!fetching && empty"><fa icon="search"/> {{ $t('no-posts-found', { q: $route.params.tag }) }}</p>
<mk-notes ref="timeline" :class="$style.notes" :more="existMore ? more : null"/>
</mk-ui>
</div>
</template>
<script lang="ts">
@ -96,17 +93,11 @@ export default Vue.extend({
</script>
<style lang="stylus" module>
.header
width 100%
max-width 600px
margin 0 auto
color #555
.notes
width 600px
margin 0 auto
border solid 1px rgba(#000, 0.075)
border-radius 6px
background var(--face)
box-shadow var(--shadow)
border-radius var(--round)
overflow hidden
overflow hidden
.empty

View File

@ -1,5 +1,7 @@
<template>
<div class="mk-timeline">
<mk-post-form class="form" v-if="$store.state.settings.showPostFormOnTopOfTl"/>
<div class="main">
<header>
<span :data-active="src == 'home'" @click="src = 'home'"><fa icon="home"/> {{ $t('home') }}</span>
<span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline"><fa :icon="['far', 'comments']"/> {{ $t('local') }}</span>
@ -23,6 +25,7 @@
<x-core v-if="src == 'tag'" ref="tl" key="tag" src="tag" :tag-tl="tagTl"/>
<mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/>
</div>
</div>
</template>
<script lang="ts">
@ -30,7 +33,7 @@ import Vue from 'vue';
import i18n from '../../../i18n';
import XCore from './timeline.core.vue';
import Menu from '../../../common/views/components/menu.vue';
import MkSettingsWindow from './settings-window.vue';
import MkSettingsWindow from '../components/settings-window.vue';
export default Vue.extend({
i18n: i18n('desktop/views/components/timeline.vue'),
@ -184,6 +187,12 @@ export default Vue.extend({
<style lang="stylus" scoped>
.mk-timeline
> .form
margin-bottom 16px
box-shadow var(--shadow)
border-radius var(--round)
> .main
background var(--face)
box-shadow var(--shadow)
border-radius var(--round)

View File

@ -15,6 +15,13 @@
</div>
<mk-avatar class="avatar" :user="user" :disable-preview="true"/>
<div class="body">
<div class="actions" v-if="$store.getters.isSignedIn">
<template v-if="$store.state.i.id != user.id">
<p class="followed" v-if="user.isFollowed">{{ $t('follows-you') }}</p>
<mk-follow-button :user="user" :inline="true" class="follow"/>
</template>
<ui-button @click="menu" ref="menu" :inline="true"><fa icon="ellipsis-h"/></ui-button>
</div>
<div class="description">
<mfm v-if="user.description" :text="user.description" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
</div>
@ -45,6 +52,7 @@
import Vue from 'vue';
import i18n from '../../../../i18n';
import * as age from 's-age';
import XUserMenu from '../../../../common/views/components/user-menu.vue';
export default Vue.extend({
i18n: i18n('desktop/views/pages/user/user.header.vue'),
@ -99,6 +107,13 @@ export default Vue.extend({
this.$updateBanner().then(i => {
this.user.bannerUrl = i.bannerUrl;
});
},
menu() {
this.$root.new(XUserMenu, {
source: this.$refs.menu.$el,
user: this.user
});
}
}
});
@ -187,6 +202,10 @@ export default Vue.extend({
padding 16px 16px 16px 154px
color var(--text)
> .actions
> .follow
width 200px
> .fields
margin-top 16px

View File

@ -87,7 +87,7 @@ export default Vue.extend({
> .img
flex 1 1 33%
width 33%
height 80px
height 120px
background-position center center
background-size cover
background-clip content-box

View File

@ -0,0 +1,109 @@
<template>
<div class="xygkxeaeontfaokvqmiblezmhvhostak" v-if="!fetching">
<div class="is-suspended" v-if="user.isSuspended"><fa icon="exclamation-triangle"/> {{ $t('@.user-suspended') }}</div>
<div class="is-remote" v-if="user.host != null"><fa icon="exclamation-triangle"/> {{ $t('@.is-remote-user') }}<a :href="user.url || user.uri" target="_blank">{{ $t('@.view-on-remote') }}</a></div>
<div class="main">
<x-header :user="user"/>
<mk-note-detail v-for="n in user.pinnedNotes" :key="n.id" :note="n" :compact="true"/>
<x-integrations :user="user"/>
<!--<mk-calendar @chosen="warp" :start="new Date(user.createdAt)"/>-->
<div class="activity">
<mk-widget-container :show-header="true" :naked="false">
<template slot="header"><fa icon="chart-bar"/>{{ $t('activity') }}</template>
<x-activity :user="user" :limit="35" style="padding: 16px;"/>
</mk-widget-container>
</div>
<x-photos :user="user"/>
<x-friends :user="user"/>
<x-followers-you-know v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/>
<x-timeline class="timeline" ref="tl" :user="user"/>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../../i18n';
import parseAcct from '../../../../../../misc/acct/parse';
import Progress from '../../../../common/scripts/loading';
import XHeader from './user.header.vue';
import XTimeline from './user.timeline.vue';
import XPhotos from './user.photos.vue';
import XFollowersYouKnow from './user.followers-you-know.vue';
import XFriends from './user.friends.vue';
import XIntegrations from './user.integrations.vue';
import XActivity from '../../../../common/views/components/activity.vue';
export default Vue.extend({
i18n: i18n(),
components: {
XHeader,
XTimeline,
XPhotos,
XFollowersYouKnow,
XFriends,
XIntegrations,
XActivity
},
data() {
return {
fetching: true,
user: null
};
},
watch: {
$route: 'fetch'
},
created() {
this.fetch();
},
methods: {
fetch() {
this.fetching = true;
Progress.start();
this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => {
this.user = user;
this.fetching = false;
Progress.done();
});
},
warp(date) {
(this.$refs.tl as any).warp(date);
}
}
});
</script>
<style lang="stylus" scoped>
.xygkxeaeontfaokvqmiblezmhvhostak
width 100%
margin 0 auto
> .is-suspended
> .is-remote
margin-bottom 16px
padding 14px 16px
font-size 14px
box-shadow var(--shadow)
border-radius var(--round)
&.is-suspended
color var(--suspendedInfoFg)
background var(--suspendedInfoBg)
&.is-remote
color var(--remoteInfoFg)
background var(--remoteInfoBg)
> a
font-weight bold
> .main
> *
margin-bottom 16px
> .timeline
box-shadow var(--shadow)
</style>

View File

@ -1,112 +0,0 @@
<template>
<x-column>
<span slot="header">
<fa icon="hashtag"/><span>{{ tag }}</span>
</span>
<div class="xroyrflcmhhtmlwmyiwpfqiirqokfueb">
<div ref="chart" class="chart"></div>
<x-hashtag-tl :tag-tl="tagTl" class="tl"/>
</div>
</x-column>
</template>
<script lang="ts">
import Vue from 'vue';
import XColumn from './deck.column.vue';
import XHashtagTl from './deck.hashtag-tl.vue';
import ApexCharts from 'apexcharts';
export default Vue.extend({
components: {
XColumn,
XHashtagTl
},
props: {
tag: {
type: String,
required: true
}
},
computed: {
tagTl(): any {
return {
query: [[this.tag]]
};
}
},
mounted() {
this.$root.api('charts/hashtag', {
tag: this.tag,
span: 'hour',
limit: 24
}).then(stats => {
const local = [];
const remote = [];
const now = new Date();
const y = now.getFullYear();
const m = now.getMonth();
const d = now.getDate();
const h = now.getHours();
for (let i = 0; i < 24; i++) {
const x = new Date(y, m, d, h - i);
local.push([x, stats.local.count[i]]);
remote.push([x, stats.remote.count[i]]);
}
const chart = new ApexCharts(this.$refs.chart, {
chart: {
type: 'area',
height: 70,
sparkline: {
enabled: true
},
},
grid: {
clipMarkers: false,
padding: {
top: 16,
right: 16,
bottom: 16,
left: 16
}
},
stroke: {
curve: 'straight',
width: 2
},
series: [{
name: 'Local',
data: local
}, {
name: 'Remote',
data: remote
}],
xaxis: {
type: 'datetime',
}
});
chart.render();
});
}
});
</script>
<style lang="stylus" scoped>
.xroyrflcmhhtmlwmyiwpfqiirqokfueb
background var(--deckColumnBg)
> .chart
margin-bottom 16px
background var(--face)
> .tl
background var(--face)
</style>

View File

@ -1,3 +0,0 @@
<template>
<mk-home customize/>
</template>

View File

@ -1,39 +0,0 @@
<template>
<mk-ui>
<mk-home :mode="mode" @loaded="loaded" ref="home" v-hotkey.global="keymap"/>
</mk-ui>
</template>
<script lang="ts">
import Vue from 'vue';
import Progress from '../../../common/scripts/loading';
export default Vue.extend({
props: {
mode: {
type: String,
default: 'timeline'
}
},
computed: {
keymap(): any {
return {
't': this.focus
};
}
},
mounted() {
document.title = this.$root.instanceName;
Progress.start();
},
methods: {
loaded() {
Progress.done();
},
focus() {
this.$refs.home.focus();
}
}
});
</script>

View File

@ -1,25 +0,0 @@
<template>
<component :is="page"></component>
</template>
<script lang="ts">
import Vue from 'vue';
import Home from './home.vue';
import Welcome from './welcome.vue';
import Deck from './deck/deck.vue';
export default Vue.extend({
components: {
Home,
Deck,
Welcome
},
computed: {
page(): string {
if (!this.$store.getters.isSignedIn) return 'welcome';
return this.$store.state.device.deckDefault ? 'deck' : 'home';
}
}
});
</script>

View File

@ -1,66 +0,0 @@
<template>
<div class="profile" v-if="$store.getters.isSignedIn">
<div class="friend-form" v-if="$store.state.i.id != user.id">
<mk-follow-button :user="user" block/>
<p class="followed" v-if="user.isFollowed">{{ $t('follows-you') }}</p>
</div>
<div class="action-form">
<ui-button @click="menu" ref="menu">{{ $t('menu') }}</ui-button>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../../i18n';
import XUserMenu from '../../../../common/views/components/user-menu.vue';
export default Vue.extend({
i18n: i18n('desktop/views/pages/user/user.profile.vue'),
props: ['user'],
methods: {
menu() {
this.$root.new(XUserMenu, {
source: this.$refs.menu.$el,
user: this.user
});
},
}
});
</script>
<style lang="stylus" scoped>
.profile
background var(--face)
box-shadow var(--shadow)
border-radius var(--round)
> *:first-child
border-top none !important
> .friend-form
padding 16px
text-align center
border-bottom solid 1px var(--faceDivider)
> .followed
margin 12px 0 0 0
padding 0
text-align center
line-height 24px
font-size 0.8em
color var(--text)
border-radius 4px
> .action-form
padding 16px
text-align center
> *
width 100%
&:not(:last-child)
margin-bottom 12px
</style>

View File

@ -1,155 +0,0 @@
<template>
<mk-ui>
<div class="xygkxeaeontfaokvqmiblezmhvhostak" v-if="!fetching">
<div class="is-suspended" v-if="user.isSuspended"><fa icon="exclamation-triangle"/> {{ $t('@.user-suspended') }}</div>
<div class="is-remote" v-if="user.host != null"><fa icon="exclamation-triangle"/> {{ $t('@.is-remote-user') }}<a :href="user.url || user.uri" target="_blank">{{ $t('@.view-on-remote') }}</a></div>
<main>
<div class="main">
<x-header :user="user"/>
<mk-note-detail v-for="n in user.pinnedNotes" :key="n.id" :note="n" :compact="true"/>
<x-timeline class="timeline" ref="tl" :user="user"/>
</div>
<div class="side">
<div class="instance" v-if="!$store.getters.isSignedIn"><mk-instance/></div>
<x-profile :user="user"/>
<x-integrations :user="user"/>
<mk-calendar @chosen="warp" :start="new Date(user.createdAt)"/>
<mk-activity :user="user"/>
<x-photos :user="user"/>
<x-friends :user="user"/>
<x-followers-you-know v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/>
<div class="nav"><mk-nav/></div>
</div>
</main>
</div>
</mk-ui>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../../i18n';
import parseAcct from '../../../../../../misc/acct/parse';
import Progress from '../../../../common/scripts/loading';
import XHeader from './user.header.vue';
import XTimeline from './user.timeline.vue';
import XProfile from './user.profile.vue';
import XPhotos from './user.photos.vue';
import XFollowersYouKnow from './user.followers-you-know.vue';
import XFriends from './user.friends.vue';
import XIntegrations from './user.integrations.vue';
export default Vue.extend({
i18n: i18n(),
components: {
XHeader,
XTimeline,
XProfile,
XPhotos,
XFollowersYouKnow,
XFriends,
XIntegrations
},
data() {
return {
fetching: true,
user: null
};
},
watch: {
$route: 'fetch'
},
created() {
this.fetch();
},
methods: {
fetch() {
this.fetching = true;
Progress.start();
this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => {
this.user = user;
this.fetching = false;
Progress.done();
});
},
warp(date) {
(this.$refs.tl as any).warp(date);
}
}
});
</script>
<style lang="stylus" scoped>
.xygkxeaeontfaokvqmiblezmhvhostak
max-width 980px
min-width 720px
padding 16px
margin 0 auto
> .is-suspended
> .is-remote
margin-bottom 16px
padding 14px 16px
font-size 14px
box-shadow var(--shadow)
border-radius var(--round)
&.is-suspended
color var(--suspendedInfoFg)
background var(--suspendedInfoBg)
&.is-remote
color var(--remoteInfoFg)
background var(--remoteInfoBg)
> a
font-weight bold
> main
display flex
justify-content center
> .main
> .side
> *:not(:last-child)
margin-bottom 16px
> .main
flex 1
min-width 0 // SEE: http://kudakurage.hatenadiary.com/entry/2016/04/01/232722
margin-right 16px
> .timeline
box-shadow var(--shadow)
> .side
width 275px
flex-shrink 0
> p
display block
margin 0
padding 0 12px
text-align center
font-size 0.8em
color var(--text)
> .instance
box-shadow var(--shadow)
border-radius var(--round)
> .nav
padding 16px
font-size 12px
color var(--text)
background var(--face)
box-shadow var(--shadow)
border-radius var(--round)
a
color var(--text)99
i
color var(--text)
</style>

View File

@ -0,0 +1,21 @@
<template>
<div class="mkw-customize">
<ui-button @click="customize()">{{ $t('@.customize-home') }}</ui-button>
</div>
</template>
<script lang="ts">
import define from '../../../common/define-widget';
import i18n from '../../../i18n';
export default define({
name: 'customize',
}).extend({
i18n: i18n(),
methods: {
customize(date) {
location.href = '/?customize';
}
}
});
</script>

View File

@ -9,6 +9,7 @@ import wPolls from './polls.vue';
import wPostForm from './post-form.vue';
import wMessaging from './messaging.vue';
import wProfile from './profile.vue';
import wCustomize from './customize.vue';
Vue.component('mkw-notifications', wNotifications);
Vue.component('mkw-timemachine', wTimemachine);
@ -19,3 +20,4 @@ Vue.component('mkw-polls', wPolls);
Vue.component('mkw-post-form', wPostForm);
Vue.component('mkw-messaging', wMessaging);
Vue.component('mkw-profile', wProfile);
Vue.component('mkw-customize', wCustomize);

View File

@ -350,7 +350,7 @@ if (localStorage.getItem('should-refresh') == 'true') {
}
// MiOSを初期化してコールバックする
export default (callback: (launch: (router: VueRouter) => [Vue, MiOS]) => void, sw = false) => {
export default (callback: (launch: (router: VueRouter) => [Vue, MiOS], os: MiOS) => void, sw = false) => {
const os = new MiOS(sw);
os.init(() => {
@ -436,11 +436,6 @@ export default (callback: (launch: (router: VueRouter) => [Vue, MiOS]) => void,
});
//#endregion
// Navigation hook
router.beforeEach((to, from, next) => {
next(os.store.state.navHook && os.store.state.navHook(to) ? false : undefined);
});
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
os.store.commit('clearBehindNotes');
@ -507,6 +502,6 @@ export default (callback: (launch: (router: VueRouter) => [Vue, MiOS]) => void,
return [app, os] as [Vue, MiOS];
};
callback(launch);
callback(launch, os);
});
};

View File

@ -49,7 +49,7 @@ export default Vue.extend({
XPhotos,
XFriends,
XFollowersYouKnow,
XActivity: () => import('../../components/activity.vue').then(m => m.default)
XActivity: () => import('../../../../common/views/components/activity.vue').then(m => m.default)
},
props: ['user']
});

View File

@ -21,7 +21,7 @@ export default define({
}).extend({
i18n: i18n(),
components: {
XActivity: () => import('../components/activity.vue').then(m => m.default)
XActivity: () => import('../../../common/views/components/activity.vue').then(m => m.default)
},
methods: {
func() {

View File

@ -10,7 +10,6 @@ const defaultSettings = {
home: null,
mobileHome: [],
deck: null,
deckNav: true,
keepCw: false,
tagTimelines: [],
fetchOnScroll: true,
@ -67,8 +66,7 @@ const defaultDeviceSettings = {
deckColumnAlign: 'center',
deckColumnWidth: 'normal',
mobileNotificationPosition: 'bottom',
deckTemporaryColumn: null,
deckDefault: false,
deckMode: false,
useOsDefaultEmojis: false,
disableShowingAnimatedImages: false
};
@ -82,7 +80,6 @@ export default (os: MiOS) => new Vuex.Store({
i: null,
indicate: false,
uiHeaderHeight: 0,
navHook: null,
behindNotes: []
},
@ -107,10 +104,6 @@ export default (os: MiOS) => new Vuex.Store({
state.uiHeaderHeight = height;
},
navHook(state, callback) {
state.navHook = callback;
},
pushBehindNote(state, note) {
if (note.userId === state.i.id) return;
if (state.behindNotes.some(n => n.id === note.id)) return;