This commit is contained in:
syuilo
2018-02-19 23:37:09 +09:00
parent 55273807d2
commit 69a8e4f4b2
40 changed files with 356 additions and 303 deletions

View File

@ -0,0 +1,66 @@
<template>
<svg viewBox="0 0 21 7" preserveAspectRatio="none">
<rect v-for="record in data" class="day"
width="1" height="1"
:x="record.x" :y="record.date.weekday"
rx="1" ry="1"
fill="transparent">
<title>{{ record.date.year }}/{{ record.date.month }}/{{ record.date.day }}</title>
</rect>
<rect v-for="record in data" class="day"
:width="record.v" :height="record.v"
:x="record.x + ((1 - record.v) / 2)" :y="record.date.weekday + ((1 - record.v) / 2)"
rx="1" ry="1"
:fill="record.color"
style="pointer-events: none;"/>
<rect class="today"
width="1" height="1"
:x="data[data.length - 1].x" :y="data[data.length - 1].date.weekday"
rx="1" ry="1"
fill="none"
stroke-width="0.1"
stroke="#f73520"/>
</svg>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: ['data'],
created() {
this.data.forEach(d => d.total = d.posts + d.replies + d.reposts);
const peak = Math.max.apply(null, this.data.map(d => d.total));
let x = 0;
this.data.reverse().forEach(d => {
d.x = x;
d.date.weekday = (new Date(d.date.year, d.date.month - 1, d.date.day)).getDay();
d.v = d.total / (peak / 2);
if (d.v > 1) d.v = 1;
const ch = d.date.weekday == 0 || d.date.weekday == 6 ? 275 : 170;
const cs = d.v * 100;
const cl = 15 + ((1 - d.v) * 80);
d.color = `hsl(${ch}, ${cs}%, ${cl}%)`;
if (d.date.weekday == 6) x++;
});
}
});
</script>
<style lang="stylus" scoped>
svg
display block
padding 10px
width 100%
> rect
transform-origin center
&.day
&:hover
fill rgba(0, 0, 0, 0.05)
</style>

View File

@ -0,0 +1,101 @@
<template>
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" preserveAspectRatio="none" @mousedown.prevent="onMousedown">
<title>Black ... Total<br/>Blue ... Posts<br/>Red ... Replies<br/>Green ... Reposts</title>
<polyline
:points="pointsPost"
fill="none"
stroke-width="1"
stroke="#41ddde"/>
<polyline
:points="pointsReply"
fill="none"
stroke-width="1"
stroke="#f7796c"/>
<polyline
:points="pointsRepost"
fill="none"
stroke-width="1"
stroke="#a1de41"/>
<polyline
:points="pointsTotal"
fill="none"
stroke-width="1"
stroke="#555"
stroke-dasharray="2 2"/>
</svg>
</template>
<script lang="ts">
import Vue from 'vue';
function dragListen(fn) {
window.addEventListener('mousemove', fn);
window.addEventListener('mouseleave', dragClear.bind(null, fn));
window.addEventListener('mouseup', dragClear.bind(null, fn));
}
function dragClear(fn) {
window.removeEventListener('mousemove', fn);
window.removeEventListener('mouseleave', dragClear);
window.removeEventListener('mouseup', dragClear);
}
export default Vue.extend({
props: ['data'],
data() {
return {
viewBoxX: 140,
viewBoxY: 60,
zoom: 1,
pos: 0,
pointsPost: null,
pointsReply: null,
pointsRepost: null,
pointsTotal: null
};
},
created() {
this.data.reverse();
this.data.forEach(d => d.total = d.posts + d.replies + d.reposts);
this.render();
},
methods: {
render() {
const peak = Math.max.apply(null, this.data.map(d => d.total));
this.pointsPost = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.posts / peak)) * this.viewBoxY}`).join(' ');
this.pointsReply = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' ');
this.pointsRepost = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.reposts / peak)) * this.viewBoxY}`).join(' ');
this.pointsTotal = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ');
},
onMousedown(e) {
const clickX = e.clientX;
const clickY = e.clientY;
const baseZoom = this.zoom;
const basePos = this.pos;
// 動かした時
dragListen(me => {
let moveLeft = me.clientX - clickX;
let moveTop = me.clientY - clickY;
this.zoom = baseZoom + (-moveTop / 20);
this.pos = basePos + moveLeft;
if (this.zoom < 1) this.zoom = 1;
if (this.pos > 0) this.pos = 0;
if (this.pos < -(((this.data.length - 1) * this.zoom) - this.viewBoxX)) this.pos = -(((this.data.length - 1) * this.zoom) - this.viewBoxX);
this.render();
});
}
}
});
</script>
<style lang="stylus" scoped>
svg
display block
padding 10px
width 100%
cursor all-scroll
</style>

View File

@ -0,0 +1,116 @@
<template>
<div class="mk-activity">
<template v-if="design == 0">
<p class="title">%fa:chart-bar%%i18n:desktop.tags.mk-activity-widget.title%</p>
<button @click="toggle" title="%i18n:desktop.tags.mk-activity-widget.toggle%">%fa:sort%</button>
</template>
<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
<template v-else>
<mk-activity-widget-calender v-show="view == 0" :data="[].concat(activity)"/>
<mk-activity-widget-chart v-show="view == 1" :data="[].concat(activity)"/>
</template>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import Calendar from './activity.calendar.vue';
import Chart from './activity.chart.vue';
export default Vue.extend({
components: {
'mk-activity-widget-calender': Calendar,
'mk-activity-widget-chart': Chart
},
props: {
design: {
default: 0
},
initView: {
default: 0
},
user: {
type: Object,
required: true
}
},
data() {
return {
fetching: true,
activity: null,
view: this.initView
};
},
mounted() {
(this as any).api('aggregation/users/activity', {
user_id: this.user.id,
limit: 20 * 7
}).then(activity => {
this.activity = activity;
this.fetching = false;
});
},
methods: {
toggle() {
if (this.view == 1) {
this.view = 0;
this.$emit('viewChanged', this.view);
} else {
this.view++;
this.$emit('viewChanged', this.view);
}
}
}
});
</script>
<style lang="stylus" scoped>
.mk-activity
background #fff
border solid 1px rgba(0, 0, 0, 0.075)
border-radius 6px
&[data-melt]
background transparent !important
border none !important
> .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
> .fetching
margin 0
padding 16px
text-align center
color #aaa
> [data-fa]
margin-right 4px
</style>

View File

@ -47,7 +47,7 @@ export default Vue.extend({
default: 0
},
start: {
type: Object,
type: Date,
required: false
}
},
@ -94,7 +94,7 @@ export default Vue.extend({
isOutOfRange(day) {
const test = (new Date(this.year, this.month - 1, day)).getTime();
return test > this.today.getTime() ||
(this.start ? test < this.start.getTime() : false);
(this.start ? test < (this.start as any).getTime() : false);
},
isDonichi(day) {

View File

@ -1,9 +1,9 @@
<template>
<mk-window width='400px' height='550px' @closed="$destroy">
<mk-window width="400px" height="550px" @closed="$destroy">
<span slot="header" :class="$style.header">
<img :src="`${user.avatar_url}?thumbnail&size=64`" alt=""/>{{ user.name }}のフォロワー
</span>
<mk-user-followers :user="user"/>
<mk-followers-list :user="user"/>
</mk-window>
</template>

View File

@ -1,9 +1,9 @@
<template>
<mk-window width='400px' height='550px' @closed="$destroy">
<mk-window width="400px" height="550px" @closed="$destroy">
<span slot="header" :class="$style.header">
<img :src="`${user.avatar_url}?thumbnail&size=64`" alt=""/>{{ user.name }}のフォロー
</span>
<mk-user-following :user="user"/>
<mk-following-list :user="user"/>
</mk-window>
</template>

View File

@ -43,8 +43,8 @@ export default Vue.extend({
limit: this.limit,
offset: this.limit * this.page
}).then(users => {
this.fetching = false;
this.users = users;
this.fetching = false;
});
},
refresh() {

View File

@ -34,6 +34,7 @@ import driveNavFolder from './drive-nav-folder.vue';
import postDetail from './post-detail.vue';
import settings from './settings.vue';
import calendar from './calendar.vue';
import activity from './activity.vue';
import wNav from './widgets/nav.vue';
import wCalendar from './widgets/calendar.vue';
import wPhotoStream from './widgets/photo-stream.vue';
@ -78,6 +79,7 @@ Vue.component('mk-drive-nav-folder', driveNavFolder);
Vue.component('mk-post-detail', postDetail);
Vue.component('mk-settings', settings);
Vue.component('mk-calendar', calendar);
Vue.component('mk-activity', activity);
Vue.component('mkw-nav', wNav);
Vue.component('mkw-calendar', wCalendar);
Vue.component('mkw-photo-stream', wPhotoStream);

View File

@ -23,8 +23,8 @@ export default Vue.extend({
},
mounted() {
(this as any).api('mute/list').then(x => {
this.fetching = false;
this.users = x.users;
this.fetching = false;
});
}
});

View File

@ -4,7 +4,7 @@
class="read-more"
v-if="p.reply && p.reply.reply_id && context == null"
title="会話をもっと読み込む"
@click="loadContext"
@click="fetchContext"
:disabled="contextFetching"
>
<template v-if="!contextFetching">%fa:ellipsis-v%</template>

View File

@ -57,8 +57,8 @@ export default Vue.extend({
(this as any).api('posts/timeline', {
until_date: this.date ? this.date.getTime() : undefined
}).then(posts => {
this.fetching = false;
this.posts = posts;
this.fetching = false;
if (cb) cb();
});
},

View File

@ -1,133 +0,0 @@
<template>
<div class="mk-user-timeline">
<header>
<span :data-is-active="mode == 'default'" @click="mode = 'default'">投稿</span>
<span :data-is-active="mode == 'with-replies'" @click="mode = 'with-replies'">投稿と返信</span>
</header>
<div class="loading" v-if="fetching">
<mk-ellipsis-icon/>
</div>
<p class="empty" v-if="empty">%fa:R comments%このユーザーはまだ何も投稿していないようです</p>
<mk-posts ref="timeline" :posts="posts">
<div slot="footer">
<template v-if="!moreFetching">%fa:moon%</template>
<template v-if="moreFetching">%fa:spinner .pulse .fw%</template>
</div>
</mk-posts>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: ['user'],
data() {
return {
fetching: true,
moreFetching: false,
mode: 'default',
unreadCount: 0,
posts: [],
date: null
};
},
watch: {
mode() {
this.fetch();
}
},
computed: {
empty(): boolean {
return this.posts.length == 0;
}
},
mounted() {
document.addEventListener('keydown', this.onDocumentKeydown);
window.addEventListener('scroll', this.onScroll);
this.fetch(() => this.$emit('loaded'));
},
beforeDestroy() {
document.removeEventListener('keydown', this.onDocumentKeydown);
window.removeEventListener('scroll', this.onScroll);
},
methods: {
onDocumentKeydown(e) {
if (e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA') {
if (e.which == 84) { // [t]
(this.$refs.timeline as any).focus();
}
}
},
fetch(cb?) {
(this as any).api('users/posts', {
user_id: this.user.id,
until_date: this.date ? this.date.getTime() : undefined,
with_replies: this.mode == 'with-replies'
}).then(posts => {
this.fetching = false;
this.posts = posts;
if (cb) cb();
});
},
more() {
if (this.moreFetching || this.fetching || this.posts.length == 0) return;
this.moreFetching = true;
(this as any).api('users/posts', {
user_id: this.user.id,
with_replies: this.mode == 'with-replies',
until_id: this.posts[this.posts.length - 1].id
}).then(posts => {
this.moreFetching = false;
this.posts = this.posts.concat(posts);
});
},
onScroll() {
const current = window.scrollY + window.innerHeight;
if (current > document.body.offsetHeight - 16/*遊び*/) {
this.more();
}
}
}
});
</script>
<style lang="stylus" scoped>
.mk-user-timeline
background #fff
> header
padding 8px 16px
border-bottom solid 1px #eee
> span
margin-right 16px
line-height 27px
font-size 18px
color #555
&:not([data-is-active])
color $theme-color
cursor pointer
&:hover
text-decoration underline
> .loading
padding 64px 0
> .empty
display block
margin 0 auto
padding 32px
max-width 400px
text-align center
color #999
> [data-fa]
display block
margin-bottom 16px
font-size 3em
color #ccc
</style>

View File

@ -45,9 +45,9 @@ export default Vue.extend({
_fetch(cb) {
this.fetching = true;
this.fetch(this.mode == 'iknow', this.limit, null, obj => {
this.fetching = false;
this.users = obj.users;
this.next = obj.next;
this.fetching = false;
if (cb) cb();
});
},

View File

@ -46,8 +46,8 @@ export default define({
}
});
}
this.fetching = false;
this.broadcasts = broadcasts;
this.fetching = false;
});
},
methods: {

View File

@ -35,8 +35,8 @@ export default define({
type: 'image/*',
limit: 9
}).then(images => {
this.fetching = false;
this.images = images;
this.fetching = false;
});
},
beforeDestroy() {

View File

@ -93,8 +93,8 @@ export default define({
type: 'image/*',
limit: 100
}).then(images => {
this.fetching = false;
this.images = images;
this.fetching = false;
(this.$refs.slideA as any).style.backgroundImage = '';
(this.$refs.slideB as any).style.backgroundImage = '';
this.change();