mirror of
https://github.com/paricafe/misskey.git
synced 2024-11-24 19:56:43 -06:00
Improve desktop UX (#4262)
* wip * wip * wip * wip * wip * wip * Merge * wip * wip * wip * wip * wip * wip
This commit is contained in:
parent
38ca514f53
commit
53422ffcb2
60 changed files with 1132 additions and 1222 deletions
|
@ -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: "投稿"
|
||||
|
|
|
@ -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>
|
11
src/client/app/common/views/components/dummy.vue
Normal file
11
src/client/app/common/views/components/dummy.vue
Normal file
|
@ -0,0 +1,11 @@
|
|||
<template>
|
||||
<div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
export default Vue.extend({
|
||||
});
|
||||
</script>
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
14
src/client/app/common/views/widgets/instance.vue
Normal file
14
src/client/app/common/views/widgets/instance.vue
Normal 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>
|
|
@ -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) {
|
||||
/**
|
||||
|
|
|
@ -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>
|
|
@ -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);
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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() {
|
|
@ -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';
|
||||
|
88
src/client/app/desktop/views/deck/deck.favorites-column.vue
Normal file
88
src/client/app/desktop/views/deck/deck.favorites-column.vue
Normal 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>
|
119
src/client/app/desktop/views/deck/deck.hashtag-column.vue
Normal file
119
src/client/app/desktop/views/deck/deck.hashtag-column.vue
Normal 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>
|
|
@ -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';
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
|
|
@ -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: {
|
|
@ -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';
|
||||
|
|
@ -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;
|
|
@ -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';
|
|
@ -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', {
|
|
@ -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,
|
|
@ -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';
|
|
@ -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
|
400
src/client/app/desktop/views/home/home.vue
Normal file
400
src/client/app/desktop/views/home/home.vue
Normal 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>
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
||||
|
|
@ -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
|
109
src/client/app/desktop/views/home/user/user.vue
Normal file
109
src/client/app/desktop/views/home/user/user.vue
Normal 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>
|
|
@ -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>
|
|
@ -1,3 +0,0 @@
|
|||
<template>
|
||||
<mk-home customize/>
|
||||
</template>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
21
src/client/app/desktop/views/widgets/customize.vue
Normal file
21
src/client/app/desktop/views/widgets/customize.vue
Normal 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>
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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']
|
||||
});
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue