整理した
This commit is contained in:
31
src/client/app/desktop/views/widgets/activity.vue
Normal file
31
src/client/app/desktop/views/widgets/activity.vue
Normal file
@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<mk-activity
|
||||
:design="props.design"
|
||||
:init-view="props.view"
|
||||
:user="os.i"
|
||||
@view-changed="viewChanged"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import define from '../../../common/define-widget';
|
||||
export default define({
|
||||
name: 'activity',
|
||||
props: () => ({
|
||||
design: 0,
|
||||
view: 0
|
||||
})
|
||||
}).extend({
|
||||
methods: {
|
||||
func() {
|
||||
if (this.props.design == 2) {
|
||||
this.props.design = 0;
|
||||
} else {
|
||||
this.props.design++;
|
||||
}
|
||||
},
|
||||
viewChanged(view) {
|
||||
this.props.view = view;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="form">
|
||||
<input v-model="text" :disabled="wait" @keydown="onKeydown" placeholder="書いて">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
text: '',
|
||||
wait: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onKeydown(e) {
|
||||
if (e.which == 10 || e.which == 13) this.post();
|
||||
},
|
||||
post() {
|
||||
this.wait = true;
|
||||
|
||||
let reply = null;
|
||||
|
||||
if (/^>>([0-9]+) /.test(this.text)) {
|
||||
const index = this.text.match(/^>>([0-9]+) /)[1];
|
||||
reply = (this.$parent as any).posts.find(p => p.index.toString() == index);
|
||||
this.text = this.text.replace(/^>>([0-9]+) /, '');
|
||||
}
|
||||
|
||||
(this as any).api('posts/create', {
|
||||
text: this.text,
|
||||
replyId: reply ? reply.id : undefined,
|
||||
channelId: (this.$parent as any).channel.id
|
||||
}).then(data => {
|
||||
this.text = '';
|
||||
}).catch(err => {
|
||||
alert('失敗した');
|
||||
}).then(() => {
|
||||
this.wait = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.form
|
||||
width 100%
|
||||
height 38px
|
||||
padding 4px
|
||||
border-top solid 1px #ddd
|
||||
|
||||
> input
|
||||
padding 0 8px
|
||||
width 100%
|
||||
height 100%
|
||||
font-size 14px
|
||||
color #55595c
|
||||
border solid 1px #dadada
|
||||
border-radius 4px
|
||||
|
||||
&:hover
|
||||
&:focus
|
||||
border-color #aeaeae
|
||||
|
||||
</style>
|
@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div class="post">
|
||||
<header>
|
||||
<a class="index" @click="reply">{{ post.index }}:</a>
|
||||
<router-link class="name" :to="`/@${acct}`" v-user-preview="post.user.id"><b>{{ post.user.name }}</b></router-link>
|
||||
<span>ID:<i>{{ acct }}</i></span>
|
||||
</header>
|
||||
<div>
|
||||
<a v-if="post.reply">>>{{ post.reply.index }}</a>
|
||||
{{ post.text }}
|
||||
<div class="media" v-if="post.media">
|
||||
<a v-for="file in post.media" :href="file.url" target="_blank">
|
||||
<img :src="`${file.url}?thumbnail&size=512`" :alt="file.name" :title="file.name"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import getAcct from '../../../../../common/user/get-acct';
|
||||
|
||||
export default Vue.extend({
|
||||
props: ['post'],
|
||||
computed: {
|
||||
acct() {
|
||||
return getAcct(this.post.user);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
reply() {
|
||||
this.$emit('reply', this.post);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.post
|
||||
margin 0
|
||||
padding 0
|
||||
color #444
|
||||
|
||||
> header
|
||||
position -webkit-sticky
|
||||
position sticky
|
||||
z-index 1
|
||||
top 0
|
||||
padding 8px 4px 4px 16px
|
||||
background rgba(255, 255, 255, 0.9)
|
||||
|
||||
> .index
|
||||
margin-right 0.25em
|
||||
|
||||
> .name
|
||||
margin-right 0.5em
|
||||
color #008000
|
||||
|
||||
> div
|
||||
padding 0 16px 16px 16px
|
||||
|
||||
> .media
|
||||
> a
|
||||
display inline-block
|
||||
|
||||
> img
|
||||
max-width 100%
|
||||
vertical-align bottom
|
||||
|
||||
</style>
|
106
src/client/app/desktop/views/widgets/channel.channel.vue
Normal file
106
src/client/app/desktop/views/widgets/channel.channel.vue
Normal file
@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<div class="channel">
|
||||
<p v-if="fetching">読み込み中<mk-ellipsis/></p>
|
||||
<div v-if="!fetching" ref="posts" class="posts">
|
||||
<p v-if="posts.length == 0">まだ投稿がありません</p>
|
||||
<x-post class="post" v-for="post in posts.slice().reverse()" :post="post" :key="post.id" @reply="reply"/>
|
||||
</div>
|
||||
<x-form class="form" ref="form"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import ChannelStream from '../../../common/scripts/streaming/channel';
|
||||
import XForm from './channel.channel.form.vue';
|
||||
import XPost from './channel.channel.post.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XForm,
|
||||
XPost
|
||||
},
|
||||
props: ['channel'],
|
||||
data() {
|
||||
return {
|
||||
fetching: true,
|
||||
posts: [],
|
||||
connection: null
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
channel() {
|
||||
this.zap();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.zap();
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.disconnect();
|
||||
},
|
||||
methods: {
|
||||
zap() {
|
||||
this.fetching = true;
|
||||
|
||||
(this as any).api('channels/posts', {
|
||||
channelId: this.channel.id
|
||||
}).then(posts => {
|
||||
this.posts = posts;
|
||||
this.fetching = false;
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.scrollToBottom();
|
||||
});
|
||||
|
||||
this.disconnect();
|
||||
this.connection = new ChannelStream((this as any).os, this.channel.id);
|
||||
this.connection.on('post', this.onPost);
|
||||
});
|
||||
},
|
||||
disconnect() {
|
||||
if (this.connection) {
|
||||
this.connection.off('post', this.onPost);
|
||||
this.connection.close();
|
||||
}
|
||||
},
|
||||
onPost(post) {
|
||||
this.posts.unshift(post);
|
||||
this.scrollToBottom();
|
||||
},
|
||||
scrollToBottom() {
|
||||
(this.$refs.posts as any).scrollTop = (this.$refs.posts as any).scrollHeight;
|
||||
},
|
||||
reply(post) {
|
||||
(this.$refs.form as any).text = `>>${ post.index } `;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.channel
|
||||
|
||||
> p
|
||||
margin 0
|
||||
padding 16px
|
||||
text-align center
|
||||
color #aaa
|
||||
|
||||
> .posts
|
||||
height calc(100% - 38px)
|
||||
overflow auto
|
||||
font-size 0.9em
|
||||
|
||||
> .post
|
||||
border-bottom solid 1px #eee
|
||||
|
||||
&:last-child
|
||||
border-bottom none
|
||||
|
||||
> .form
|
||||
position absolute
|
||||
left 0
|
||||
bottom 0
|
||||
|
||||
</style>
|
107
src/client/app/desktop/views/widgets/channel.vue
Normal file
107
src/client/app/desktop/views/widgets/channel.vue
Normal file
@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div class="mkw-channel">
|
||||
<template v-if="!props.compact">
|
||||
<p class="title">%fa:tv%{{ channel ? channel.title : '%i18n:desktop.tags.mk-channel-home-widget.title%' }}</p>
|
||||
<button @click="settings" title="%i18n:desktop.tags.mk-channel-home-widget.settings%">%fa:cog%</button>
|
||||
</template>
|
||||
<p class="get-started" v-if="props.channel == null">%i18n:desktop.tags.mk-channel-home-widget.get-started%</p>
|
||||
<x-channel class="channel" :channel="channel" v-if="channel != null"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import define from '../../../common/define-widget';
|
||||
import XChannel from './channel.channel.vue';
|
||||
|
||||
export default define({
|
||||
name: 'server',
|
||||
props: () => ({
|
||||
channel: null,
|
||||
compact: false
|
||||
})
|
||||
}).extend({
|
||||
components: {
|
||||
XChannel
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fetching: true,
|
||||
channel: null
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
if (this.props.channel) {
|
||||
this.zap();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
func() {
|
||||
this.props.compact = !this.props.compact;
|
||||
},
|
||||
settings() {
|
||||
const id = window.prompt('チャンネルID');
|
||||
if (!id) return;
|
||||
this.props.channel = id;
|
||||
this.zap();
|
||||
},
|
||||
zap() {
|
||||
this.fetching = true;
|
||||
|
||||
(this as any).api('channels/show', {
|
||||
channelId: this.props.channel
|
||||
}).then(channel => {
|
||||
this.channel = channel;
|
||||
this.fetching = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mkw-channel
|
||||
background #fff
|
||||
border solid 1px rgba(0, 0, 0, 0.075)
|
||||
border-radius 6px
|
||||
overflow hidden
|
||||
|
||||
> .title
|
||||
z-index 2
|
||||
margin 0
|
||||
padding 0 16px
|
||||
line-height 42px
|
||||
font-size 0.9em
|
||||
font-weight bold
|
||||
color #888
|
||||
box-shadow 0 1px rgba(0, 0, 0, 0.07)
|
||||
|
||||
> [data-fa]
|
||||
margin-right 4px
|
||||
|
||||
> button
|
||||
position absolute
|
||||
z-index 2
|
||||
top 0
|
||||
right 0
|
||||
padding 0
|
||||
width 42px
|
||||
font-size 0.9em
|
||||
line-height 42px
|
||||
color #ccc
|
||||
|
||||
&:hover
|
||||
color #aaa
|
||||
|
||||
&:active
|
||||
color #999
|
||||
|
||||
> .get-started
|
||||
margin 0
|
||||
padding 16px
|
||||
text-align center
|
||||
color #aaa
|
||||
|
||||
> .channel
|
||||
height 200px
|
||||
|
||||
</style>
|
23
src/client/app/desktop/views/widgets/index.ts
Normal file
23
src/client/app/desktop/views/widgets/index.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
import wNotifications from './notifications.vue';
|
||||
import wTimemachine from './timemachine.vue';
|
||||
import wActivity from './activity.vue';
|
||||
import wTrends from './trends.vue';
|
||||
import wUsers from './users.vue';
|
||||
import wPolls from './polls.vue';
|
||||
import wPostForm from './post-form.vue';
|
||||
import wMessaging from './messaging.vue';
|
||||
import wChannel from './channel.vue';
|
||||
import wProfile from './profile.vue';
|
||||
|
||||
Vue.component('mkw-notifications', wNotifications);
|
||||
Vue.component('mkw-timemachine', wTimemachine);
|
||||
Vue.component('mkw-activity', wActivity);
|
||||
Vue.component('mkw-trends', wTrends);
|
||||
Vue.component('mkw-users', wUsers);
|
||||
Vue.component('mkw-polls', wPolls);
|
||||
Vue.component('mkw-post-form', wPostForm);
|
||||
Vue.component('mkw-messaging', wMessaging);
|
||||
Vue.component('mkw-channel', wChannel);
|
||||
Vue.component('mkw-profile', wProfile);
|
59
src/client/app/desktop/views/widgets/messaging.vue
Normal file
59
src/client/app/desktop/views/widgets/messaging.vue
Normal file
@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div class="mkw-messaging">
|
||||
<p class="title" v-if="props.design == 0">%fa:comments%%i18n:desktop.tags.mk-messaging-home-widget.title%</p>
|
||||
<mk-messaging ref="index" compact @navigate="navigate"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import define from '../../../common/define-widget';
|
||||
import MkMessagingRoomWindow from '../components/messaging-room-window.vue';
|
||||
|
||||
export default define({
|
||||
name: 'messaging',
|
||||
props: () => ({
|
||||
design: 0
|
||||
})
|
||||
}).extend({
|
||||
methods: {
|
||||
navigate(user) {
|
||||
(this as any).os.new(MkMessagingRoomWindow, {
|
||||
user: user
|
||||
});
|
||||
},
|
||||
func() {
|
||||
if (this.props.design == 1) {
|
||||
this.props.design = 0;
|
||||
} else {
|
||||
this.props.design++;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mkw-messaging
|
||||
overflow hidden
|
||||
background #fff
|
||||
border solid 1px rgba(0, 0, 0, 0.075)
|
||||
border-radius 6px
|
||||
|
||||
> .title
|
||||
z-index 2
|
||||
margin 0
|
||||
padding 0 16px
|
||||
line-height 42px
|
||||
font-size 0.9em
|
||||
font-weight bold
|
||||
color #888
|
||||
box-shadow 0 1px rgba(0, 0, 0, 0.07)
|
||||
|
||||
> [data-fa]
|
||||
margin-right 4px
|
||||
|
||||
> .mk-messaging
|
||||
max-height 250px
|
||||
overflow auto
|
||||
|
||||
</style>
|
70
src/client/app/desktop/views/widgets/notifications.vue
Normal file
70
src/client/app/desktop/views/widgets/notifications.vue
Normal file
@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div class="mkw-notifications">
|
||||
<template v-if="!props.compact">
|
||||
<p class="title">%fa:R bell%%i18n:desktop.tags.mk-notifications-home-widget.title%</p>
|
||||
<button @click="settings" title="%i18n:desktop.tags.mk-notifications-home-widget.settings%">%fa:cog%</button>
|
||||
</template>
|
||||
<mk-notifications/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import define from '../../../common/define-widget';
|
||||
export default define({
|
||||
name: 'notifications',
|
||||
props: () => ({
|
||||
compact: false
|
||||
})
|
||||
}).extend({
|
||||
methods: {
|
||||
settings() {
|
||||
alert('not implemented yet');
|
||||
},
|
||||
func() {
|
||||
this.props.compact = !this.props.compact;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mkw-notifications
|
||||
background #fff
|
||||
border solid 1px rgba(0, 0, 0, 0.075)
|
||||
border-radius 6px
|
||||
|
||||
> .title
|
||||
z-index 1
|
||||
margin 0
|
||||
padding 0 16px
|
||||
line-height 42px
|
||||
font-size 0.9em
|
||||
font-weight bold
|
||||
color #888
|
||||
box-shadow 0 1px rgba(0, 0, 0, 0.07)
|
||||
|
||||
> [data-fa]
|
||||
margin-right 4px
|
||||
|
||||
> button
|
||||
position absolute
|
||||
z-index 2
|
||||
top 0
|
||||
right 0
|
||||
padding 0
|
||||
width 42px
|
||||
font-size 0.9em
|
||||
line-height 42px
|
||||
color #ccc
|
||||
|
||||
&:hover
|
||||
color #aaa
|
||||
|
||||
&:active
|
||||
color #999
|
||||
|
||||
> .mk-notifications
|
||||
max-height 300px
|
||||
overflow auto
|
||||
|
||||
</style>
|
129
src/client/app/desktop/views/widgets/polls.vue
Normal file
129
src/client/app/desktop/views/widgets/polls.vue
Normal file
@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<div class="mkw-polls">
|
||||
<template v-if="!props.compact">
|
||||
<p class="title">%fa:chart-pie%%i18n:desktop.tags.mk-recommended-polls-home-widget.title%</p>
|
||||
<button @click="fetch" title="%i18n:desktop.tags.mk-recommended-polls-home-widget.refresh%">%fa:sync%</button>
|
||||
</template>
|
||||
<div class="poll" v-if="!fetching && poll != null">
|
||||
<p v-if="poll.text"><router-link to="`/@${ acct }/${ poll.id }`">{{ poll.text }}</router-link></p>
|
||||
<p v-if="!poll.text"><router-link to="`/@${ acct }/${ poll.id }`">%fa:link%</router-link></p>
|
||||
<mk-poll :post="poll"/>
|
||||
</div>
|
||||
<p class="empty" v-if="!fetching && poll == null">%i18n:desktop.tags.mk-recommended-polls-home-widget.nothing%</p>
|
||||
<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import define from '../../../common/define-widget';
|
||||
import getAcct from '../../../../../common/user/get-acct';
|
||||
|
||||
export default define({
|
||||
name: 'polls',
|
||||
props: () => ({
|
||||
compact: false
|
||||
})
|
||||
}).extend({
|
||||
computed: {
|
||||
acct() {
|
||||
return getAcct(this.poll.user);
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
poll: null,
|
||||
fetching: true,
|
||||
offset: 0
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.fetch();
|
||||
},
|
||||
methods: {
|
||||
func() {
|
||||
this.props.compact = !this.props.compact;
|
||||
},
|
||||
fetch() {
|
||||
this.fetching = true;
|
||||
this.poll = null;
|
||||
|
||||
(this as any).api('posts/polls/recommendation', {
|
||||
limit: 1,
|
||||
offset: this.offset
|
||||
}).then(posts => {
|
||||
const poll = posts ? posts[0] : null;
|
||||
if (poll == null) {
|
||||
this.offset = 0;
|
||||
} else {
|
||||
this.offset++;
|
||||
}
|
||||
this.poll = poll;
|
||||
this.fetching = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mkw-polls
|
||||
background #fff
|
||||
border solid 1px rgba(0, 0, 0, 0.075)
|
||||
border-radius 6px
|
||||
|
||||
> .title
|
||||
margin 0
|
||||
padding 0 16px
|
||||
line-height 42px
|
||||
font-size 0.9em
|
||||
font-weight bold
|
||||
color #888
|
||||
border-bottom solid 1px #eee
|
||||
|
||||
> [data-fa]
|
||||
margin-right 4px
|
||||
|
||||
> button
|
||||
position absolute
|
||||
z-index 2
|
||||
top 0
|
||||
right 0
|
||||
padding 0
|
||||
width 42px
|
||||
font-size 0.9em
|
||||
line-height 42px
|
||||
color #ccc
|
||||
|
||||
&:hover
|
||||
color #aaa
|
||||
|
||||
&:active
|
||||
color #999
|
||||
|
||||
> .poll
|
||||
padding 16px
|
||||
font-size 12px
|
||||
color #555
|
||||
|
||||
> p
|
||||
margin 0 0 8px 0
|
||||
|
||||
> a
|
||||
color inherit
|
||||
|
||||
> .empty
|
||||
margin 0
|
||||
padding 16px
|
||||
text-align center
|
||||
color #aaa
|
||||
|
||||
> .fetching
|
||||
margin 0
|
||||
padding 16px
|
||||
text-align center
|
||||
color #aaa
|
||||
|
||||
> [data-fa]
|
||||
margin-right 4px
|
||||
|
||||
</style>
|
111
src/client/app/desktop/views/widgets/post-form.vue
Normal file
111
src/client/app/desktop/views/widgets/post-form.vue
Normal file
@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<div class="mkw-post-form">
|
||||
<template v-if="props.design == 0">
|
||||
<p class="title">%fa:pencil-alt%%i18n:desktop.tags.mk-post-form-home-widget.title%</p>
|
||||
</template>
|
||||
<textarea :disabled="posting" v-model="text" @keydown="onKeydown" placeholder="%i18n:desktop.tags.mk-post-form-home-widget.placeholder%"></textarea>
|
||||
<button @click="post" :disabled="posting">%i18n:desktop.tags.mk-post-form-home-widget.post%</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import define from '../../../common/define-widget';
|
||||
export default define({
|
||||
name: 'post-form',
|
||||
props: () => ({
|
||||
design: 0
|
||||
})
|
||||
}).extend({
|
||||
data() {
|
||||
return {
|
||||
posting: false,
|
||||
text: ''
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
func() {
|
||||
if (this.props.design == 1) {
|
||||
this.props.design = 0;
|
||||
} else {
|
||||
this.props.design++;
|
||||
}
|
||||
},
|
||||
onKeydown(e) {
|
||||
if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey)) this.post();
|
||||
},
|
||||
post() {
|
||||
this.posting = true;
|
||||
|
||||
(this as any).api('posts/create', {
|
||||
text: this.text
|
||||
}).then(data => {
|
||||
this.clear();
|
||||
}).catch(err => {
|
||||
alert('失敗した');
|
||||
}).then(() => {
|
||||
this.posting = false;
|
||||
});
|
||||
},
|
||||
clear() {
|
||||
this.text = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import '~const.styl'
|
||||
|
||||
.mkw-post-form
|
||||
background #fff
|
||||
overflow hidden
|
||||
border solid 1px rgba(0, 0, 0, 0.075)
|
||||
border-radius 6px
|
||||
|
||||
> .title
|
||||
z-index 1
|
||||
margin 0
|
||||
padding 0 16px
|
||||
line-height 42px
|
||||
font-size 0.9em
|
||||
font-weight bold
|
||||
color #888
|
||||
box-shadow 0 1px rgba(0, 0, 0, 0.07)
|
||||
|
||||
> [data-fa]
|
||||
margin-right 4px
|
||||
|
||||
> textarea
|
||||
display block
|
||||
width 100%
|
||||
max-width 100%
|
||||
min-width 100%
|
||||
padding 16px
|
||||
margin-bottom 28px + 16px
|
||||
border none
|
||||
border-bottom solid 1px #eee
|
||||
|
||||
> button
|
||||
display block
|
||||
position absolute
|
||||
bottom 8px
|
||||
right 8px
|
||||
margin 0
|
||||
padding 0 10px
|
||||
height 28px
|
||||
color $theme-color-foreground
|
||||
background $theme-color !important
|
||||
outline none
|
||||
border none
|
||||
border-radius 4px
|
||||
transition background 0.1s ease
|
||||
cursor pointer
|
||||
|
||||
&:hover
|
||||
background lighten($theme-color, 10%) !important
|
||||
|
||||
&:active
|
||||
background darken($theme-color, 10%) !important
|
||||
transition background 0s ease
|
||||
|
||||
</style>
|
125
src/client/app/desktop/views/widgets/profile.vue
Normal file
125
src/client/app/desktop/views/widgets/profile.vue
Normal file
@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<div class="mkw-profile"
|
||||
:data-compact="props.design == 1 || props.design == 2"
|
||||
:data-melt="props.design == 2"
|
||||
>
|
||||
<div class="banner"
|
||||
:style="os.i.bannerUrl ? `background-image: url(${os.i.bannerUrl}?thumbnail&size=256)` : ''"
|
||||
title="クリックでバナー編集"
|
||||
@click="os.apis.updateBanner"
|
||||
></div>
|
||||
<img class="avatar"
|
||||
:src="`${os.i.avatarUrl}?thumbnail&size=96`"
|
||||
@click="os.apis.updateAvatar"
|
||||
alt="avatar"
|
||||
title="クリックでアバター編集"
|
||||
v-user-preview="os.i.id"
|
||||
/>
|
||||
<router-link class="name" :to="`/@${os.i.username}`">{{ os.i.name }}</router-link>
|
||||
<p class="username">@{{ os.i.username }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import define from '../../../common/define-widget';
|
||||
export default define({
|
||||
name: 'profile',
|
||||
props: () => ({
|
||||
design: 0
|
||||
})
|
||||
}).extend({
|
||||
methods: {
|
||||
func() {
|
||||
if (this.props.design == 2) {
|
||||
this.props.design = 0;
|
||||
} else {
|
||||
this.props.design++;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mkw-profile
|
||||
overflow hidden
|
||||
background #fff
|
||||
border solid 1px rgba(0, 0, 0, 0.075)
|
||||
border-radius 6px
|
||||
|
||||
&[data-compact]
|
||||
> .banner:before
|
||||
content ""
|
||||
display block
|
||||
width 100%
|
||||
height 100%
|
||||
background rgba(0, 0, 0, 0.5)
|
||||
|
||||
> .avatar
|
||||
top ((100px - 58px) / 2)
|
||||
left ((100px - 58px) / 2)
|
||||
border none
|
||||
border-radius 100%
|
||||
box-shadow 0 0 16px rgba(0, 0, 0, 0.5)
|
||||
|
||||
> .name
|
||||
position absolute
|
||||
top 0
|
||||
left 92px
|
||||
margin 0
|
||||
line-height 100px
|
||||
color #fff
|
||||
text-shadow 0 0 8px rgba(0, 0, 0, 0.5)
|
||||
|
||||
> .username
|
||||
display none
|
||||
|
||||
&[data-melt]
|
||||
background transparent !important
|
||||
border none !important
|
||||
|
||||
> .banner
|
||||
visibility hidden
|
||||
|
||||
> .avatar
|
||||
box-shadow none
|
||||
|
||||
> .name
|
||||
color #666
|
||||
text-shadow none
|
||||
|
||||
> .banner
|
||||
height 100px
|
||||
background-color #f5f5f5
|
||||
background-size cover
|
||||
background-position center
|
||||
cursor pointer
|
||||
|
||||
> .avatar
|
||||
display block
|
||||
position absolute
|
||||
top 76px
|
||||
left 16px
|
||||
width 58px
|
||||
height 58px
|
||||
margin 0
|
||||
border solid 3px #fff
|
||||
border-radius 8px
|
||||
vertical-align bottom
|
||||
cursor pointer
|
||||
|
||||
> .name
|
||||
display block
|
||||
margin 10px 0 0 84px
|
||||
line-height 16px
|
||||
font-weight bold
|
||||
color #555
|
||||
|
||||
> .username
|
||||
display block
|
||||
margin 4px 0 8px 84px
|
||||
line-height 16px
|
||||
font-size 0.9em
|
||||
color #999
|
||||
|
||||
</style>
|
28
src/client/app/desktop/views/widgets/timemachine.vue
Normal file
28
src/client/app/desktop/views/widgets/timemachine.vue
Normal file
@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div class="mkw-timemachine">
|
||||
<mk-calendar :design="props.design" @chosen="chosen"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import define from '../../../common/define-widget';
|
||||
export default define({
|
||||
name: 'timemachine',
|
||||
props: () => ({
|
||||
design: 0
|
||||
})
|
||||
}).extend({
|
||||
methods: {
|
||||
chosen(date) {
|
||||
this.$emit('chosen', date);
|
||||
},
|
||||
func() {
|
||||
if (this.props.design == 5) {
|
||||
this.props.design = 0;
|
||||
} else {
|
||||
this.props.design++;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
135
src/client/app/desktop/views/widgets/trends.vue
Normal file
135
src/client/app/desktop/views/widgets/trends.vue
Normal file
@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<div class="mkw-trends">
|
||||
<template v-if="!props.compact">
|
||||
<p class="title">%fa:fire%%i18n:desktop.tags.mk-trends-home-widget.title%</p>
|
||||
<button @click="fetch" title="%i18n:desktop.tags.mk-trends-home-widget.refresh%">%fa:sync%</button>
|
||||
</template>
|
||||
<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
|
||||
<div class="post" v-else-if="post != null">
|
||||
<p class="text"><router-link :to="`/@${ acct }/${ post.id }`">{{ post.text }}</router-link></p>
|
||||
<p class="author">―<router-link :to="`/@${ acct }`">@{{ acct }}</router-link></p>
|
||||
</div>
|
||||
<p class="empty" v-else>%i18n:desktop.tags.mk-trends-home-widget.nothing%</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import define from '../../../common/define-widget';
|
||||
import getAcct from '../../../../../common/user/get-acct';
|
||||
|
||||
export default define({
|
||||
name: 'trends',
|
||||
props: () => ({
|
||||
compact: false
|
||||
})
|
||||
}).extend({
|
||||
computed: {
|
||||
acct() {
|
||||
return getAcct(this.post.user);
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
post: null,
|
||||
fetching: true,
|
||||
offset: 0
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.fetch();
|
||||
},
|
||||
methods: {
|
||||
func() {
|
||||
this.props.compact = !this.props.compact;
|
||||
},
|
||||
fetch() {
|
||||
this.fetching = true;
|
||||
this.post = null;
|
||||
|
||||
(this as any).api('posts/trend', {
|
||||
limit: 1,
|
||||
offset: this.offset,
|
||||
repost: false,
|
||||
reply: false,
|
||||
media: false,
|
||||
poll: false
|
||||
}).then(posts => {
|
||||
const post = posts ? posts[0] : null;
|
||||
if (post == null) {
|
||||
this.offset = 0;
|
||||
} else {
|
||||
this.offset++;
|
||||
}
|
||||
this.post = post;
|
||||
this.fetching = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mkw-trends
|
||||
background #fff
|
||||
border solid 1px rgba(0, 0, 0, 0.075)
|
||||
border-radius 6px
|
||||
|
||||
> .title
|
||||
margin 0
|
||||
padding 0 16px
|
||||
line-height 42px
|
||||
font-size 0.9em
|
||||
font-weight bold
|
||||
color #888
|
||||
border-bottom solid 1px #eee
|
||||
|
||||
> [data-fa]
|
||||
margin-right 4px
|
||||
|
||||
> button
|
||||
position absolute
|
||||
z-index 2
|
||||
top 0
|
||||
right 0
|
||||
padding 0
|
||||
width 42px
|
||||
font-size 0.9em
|
||||
line-height 42px
|
||||
color #ccc
|
||||
|
||||
&:hover
|
||||
color #aaa
|
||||
|
||||
&:active
|
||||
color #999
|
||||
|
||||
> .post
|
||||
padding 16px
|
||||
font-size 12px
|
||||
font-style oblique
|
||||
color #555
|
||||
|
||||
> p
|
||||
margin 0
|
||||
|
||||
> .text,
|
||||
> .author
|
||||
> a
|
||||
color inherit
|
||||
|
||||
> .empty
|
||||
margin 0
|
||||
padding 16px
|
||||
text-align center
|
||||
color #aaa
|
||||
|
||||
> .fetching
|
||||
margin 0
|
||||
padding 16px
|
||||
text-align center
|
||||
color #aaa
|
||||
|
||||
> [data-fa]
|
||||
margin-right 4px
|
||||
|
||||
</style>
|
172
src/client/app/desktop/views/widgets/users.vue
Normal file
172
src/client/app/desktop/views/widgets/users.vue
Normal file
@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<div class="mkw-users">
|
||||
<template v-if="!props.compact">
|
||||
<p class="title">%fa:users%%i18n:desktop.tags.mk-user-recommendation-home-widget.title%</p>
|
||||
<button @click="refresh" title="%i18n:desktop.tags.mk-user-recommendation-home-widget.refresh%">%fa:sync%</button>
|
||||
</template>
|
||||
<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
|
||||
<template v-else-if="users.length != 0">
|
||||
<div class="user" v-for="_user in users">
|
||||
<router-link class="avatar-anchor" :to="`/@${getAcct(_user)}`">
|
||||
<img class="avatar" :src="`${_user.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="_user.id"/>
|
||||
</router-link>
|
||||
<div class="body">
|
||||
<router-link class="name" :to="`/@${getAcct(_user)}`" v-user-preview="_user.id">{{ _user.name }}</router-link>
|
||||
<p class="username">@{{ getAcct(_user) }}</p>
|
||||
</div>
|
||||
<mk-follow-button :user="_user"/>
|
||||
</div>
|
||||
</template>
|
||||
<p class="empty" v-else>%i18n:desktop.tags.mk-user-recommendation-home-widget.no-one%</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import define from '../../../common/define-widget';
|
||||
import getAcct from '../../../../../common/user/get-acct';
|
||||
|
||||
const limit = 3;
|
||||
|
||||
export default define({
|
||||
name: 'users',
|
||||
props: () => ({
|
||||
compact: false
|
||||
})
|
||||
}).extend({
|
||||
data() {
|
||||
return {
|
||||
users: [],
|
||||
fetching: true,
|
||||
page: 0
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.fetch();
|
||||
},
|
||||
methods: {
|
||||
getAcct,
|
||||
func() {
|
||||
this.props.compact = !this.props.compact;
|
||||
},
|
||||
fetch() {
|
||||
this.fetching = true;
|
||||
this.users = [];
|
||||
|
||||
(this as any).api('users/recommendation', {
|
||||
limit: limit,
|
||||
offset: limit * this.page
|
||||
}).then(users => {
|
||||
this.users = users;
|
||||
this.fetching = false;
|
||||
});
|
||||
},
|
||||
refresh() {
|
||||
if (this.users.length < limit) {
|
||||
this.page = 0;
|
||||
} else {
|
||||
this.page++;
|
||||
}
|
||||
this.fetch();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mkw-users
|
||||
background #fff
|
||||
border solid 1px rgba(0, 0, 0, 0.075)
|
||||
border-radius 6px
|
||||
|
||||
> .title
|
||||
margin 0
|
||||
padding 0 16px
|
||||
line-height 42px
|
||||
font-size 0.9em
|
||||
font-weight bold
|
||||
color #888
|
||||
border-bottom solid 1px #eee
|
||||
|
||||
> [data-fa]
|
||||
margin-right 4px
|
||||
|
||||
> button
|
||||
position absolute
|
||||
z-index 2
|
||||
top 0
|
||||
right 0
|
||||
padding 0
|
||||
width 42px
|
||||
font-size 0.9em
|
||||
line-height 42px
|
||||
color #ccc
|
||||
|
||||
&:hover
|
||||
color #aaa
|
||||
|
||||
&:active
|
||||
color #999
|
||||
|
||||
> .user
|
||||
padding 16px
|
||||
border-bottom solid 1px #eee
|
||||
|
||||
&:last-child
|
||||
border-bottom none
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
clear both
|
||||
|
||||
> .avatar-anchor
|
||||
display block
|
||||
float left
|
||||
margin 0 12px 0 0
|
||||
|
||||
> .avatar
|
||||
display block
|
||||
width 42px
|
||||
height 42px
|
||||
margin 0
|
||||
border-radius 8px
|
||||
vertical-align bottom
|
||||
|
||||
> .body
|
||||
float left
|
||||
width calc(100% - 54px)
|
||||
|
||||
> .name
|
||||
margin 0
|
||||
font-size 16px
|
||||
line-height 24px
|
||||
color #555
|
||||
|
||||
> .username
|
||||
display block
|
||||
margin 0
|
||||
font-size 15px
|
||||
line-height 16px
|
||||
color #ccc
|
||||
|
||||
> .mk-follow-button
|
||||
position absolute
|
||||
top 16px
|
||||
right 16px
|
||||
|
||||
> .empty
|
||||
margin 0
|
||||
padding 16px
|
||||
text-align center
|
||||
color #aaa
|
||||
|
||||
> .fetching
|
||||
margin 0
|
||||
padding 16px
|
||||
text-align center
|
||||
color #aaa
|
||||
|
||||
> [data-fa]
|
||||
margin-right 4px
|
||||
|
||||
</style>
|
Reference in New Issue
Block a user