整理した

This commit is contained in:
syuilo
2018-03-29 20:32:18 +09:00
parent 8a279a4656
commit cf33e483f7
552 changed files with 360 additions and 1311 deletions

View File

@@ -0,0 +1,52 @@
<template>
<div class="mk-drive-page">
<mk-drive :init-folder="folder" @move-root="onMoveRoot" @open-folder="onOpenFolder"/>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
data() {
return {
folder: null
};
},
created() {
this.folder = this.$route.params.folder;
},
mounted() {
document.title = 'Misskey Drive';
},
methods: {
onMoveRoot() {
const title = 'Misskey Drive';
// Rewrite URL
history.pushState(null, title, '/i/drive');
document.title = title;
},
onOpenFolder(folder) {
const title = folder.name + ' | Misskey Drive';
// Rewrite URL
history.pushState(null, title, '/i/drive/folder/' + folder.id);
document.title = title;
}
}
});
</script>
<style lang="stylus" scoped>
.mk-drive-page
position fixed
width 100%
height 100%
background #fff
> .mk-drive
height 100%
</style>

View File

@@ -0,0 +1,12 @@
<template>
<mk-home customize/>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
mounted() {
document.title = 'Misskey - ホームのカスタマイズ';
}
});
</script>

View File

@@ -0,0 +1,62 @@
<template>
<mk-ui>
<mk-home :mode="mode" @loaded="loaded"/>
</mk-ui>
</template>
<script lang="ts">
import Vue from 'vue';
import Progress from '../../../common/scripts/loading';
import getPostSummary from '../../../../../common/get-post-summary';
export default Vue.extend({
props: {
mode: {
type: String,
default: 'timeline'
}
},
data() {
return {
connection: null,
connectionId: null,
unreadCount: 0
};
},
mounted() {
document.title = 'Misskey';
this.connection = (this as any).os.stream.getConnection();
this.connectionId = (this as any).os.stream.use();
this.connection.on('post', this.onStreamPost);
document.addEventListener('visibilitychange', this.onVisibilitychange, false);
Progress.start();
},
beforeDestroy() {
this.connection.off('post', this.onStreamPost);
(this as any).os.stream.dispose(this.connectionId);
document.removeEventListener('visibilitychange', this.onVisibilitychange);
},
methods: {
loaded() {
Progress.done();
},
onStreamPost(post) {
if (document.hidden && post.userId != (this as any).os.i.id) {
this.unreadCount++;
document.title = `(${this.unreadCount}) ${getPostSummary(post)}`;
}
},
onVisibilitychange() {
if (!document.hidden) {
this.unreadCount = 0;
document.title = 'Misskey';
}
}
}
});
</script>

View File

@@ -0,0 +1,16 @@
<template>
<component :is="os.isSignedIn ? 'home' : 'welcome'"></component>
</template>
<script lang="ts">
import Vue from 'vue';
import Home from './home.vue';
import Welcome from './welcome.vue';
export default Vue.extend({
components: {
Home,
Welcome
}
});
</script>

View File

@@ -0,0 +1,54 @@
<template>
<div class="mk-messaging-room-page">
<mk-messaging-room v-if="user" :user="user" :is-naked="true"/>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import Progress from '../../../common/scripts/loading';
import parseAcct from '../../../../../common/user/parse-acct';
export default Vue.extend({
data() {
return {
fetching: true,
user: null
};
},
watch: {
$route: 'fetch'
},
created() {
this.fetch();
},
mounted() {
document.documentElement.style.background = '#fff';
},
methods: {
fetch() {
Progress.start();
this.fetching = true;
(this as any).api('users/show', parseAcct(this.$route.params.user)).then(user => {
this.user = user;
this.fetching = false;
document.title = 'メッセージ: ' + this.user.name;
Progress.done();
});
}
}
});
</script>
<style lang="stylus" scoped>
.mk-messaging-room-page
display flex
flex 1
flex-direction column
min-height 100%
background #fff
</style>

View File

@@ -0,0 +1,50 @@
<template>
<component :is="ui ? 'mk-ui' : 'div'">
<mk-othello v-if="!fetching" :init-game="game" @gamed="onGamed"/>
</component>
</template>
<script lang="ts">
import Vue from 'vue';
import Progress from '../../../common/scripts/loading';
export default Vue.extend({
props: {
ui: {
default: false
}
},
data() {
return {
fetching: false,
game: null
};
},
watch: {
$route: 'fetch'
},
created() {
this.fetch();
},
methods: {
fetch() {
if (this.$route.params.game == null) return;
Progress.start();
this.fetching = true;
(this as any).api('othello/games/show', {
gameId: this.$route.params.game
}).then(game => {
this.game = game;
this.fetching = false;
Progress.done();
});
},
onGamed(game) {
history.pushState(null, null, '/othello/' + game.id);
}
}
});
</script>

View File

@@ -0,0 +1,67 @@
<template>
<mk-ui>
<main v-if="!fetching">
<a v-if="post.next" :href="post.next">%fa:angle-up%%i18n:desktop.tags.mk-post-page.next%</a>
<mk-post-detail :post="post"/>
<a v-if="post.prev" :href="post.prev">%fa:angle-down%%i18n:desktop.tags.mk-post-page.prev%</a>
</main>
</mk-ui>
</template>
<script lang="ts">
import Vue from 'vue';
import Progress from '../../../common/scripts/loading';
export default Vue.extend({
data() {
return {
fetching: true,
post: null
};
},
watch: {
$route: 'fetch'
},
created() {
this.fetch();
},
methods: {
fetch() {
Progress.start();
this.fetching = true;
(this as any).api('posts/show', {
postId: this.$route.params.post
}).then(post => {
this.post = post;
this.fetching = false;
Progress.done();
});
}
}
});
</script>
<style lang="stylus" scoped>
main
padding 16px
text-align center
> a
display inline-block
&:first-child
margin-bottom 4px
&:last-child
margin-top 4px
> [data-fa]
margin-right 4px
> .mk-post-detail
margin 0 auto
width 640px
</style>

View File

@@ -0,0 +1,138 @@
<template>
<mk-ui>
<header :class="$style.header">
<h1>{{ q }}</h1>
</header>
<div :class="$style.loading" v-if="fetching">
<mk-ellipsis-icon/>
</div>
<p :class="$style.empty" v-if="!fetching && empty">%fa:search%{{ q }}に関する投稿は見つかりませんでした</p>
<mk-posts ref="timeline" :class="$style.posts" :posts="posts">
<div slot="footer">
<template v-if="!moreFetching">%fa:search%</template>
<template v-if="moreFetching">%fa:spinner .pulse .fw%</template>
</div>
</mk-posts>
</mk-ui>
</template>
<script lang="ts">
import Vue from 'vue';
import Progress from '../../../common/scripts/loading';
import parse from '../../../common/scripts/parse-search-query';
const limit = 20;
export default Vue.extend({
data() {
return {
fetching: true,
moreFetching: false,
existMore: false,
offset: 0,
posts: []
};
},
watch: {
$route: 'fetch'
},
computed: {
empty(): boolean {
return this.posts.length == 0;
},
q(): string {
return this.$route.query.q;
}
},
mounted() {
document.addEventListener('keydown', this.onDocumentKeydown);
window.addEventListener('scroll', this.onScroll);
this.fetch();
},
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() {
this.fetching = true;
Progress.start();
(this as any).api('posts/search', Object.assign({
limit: limit + 1,
offset: this.offset
}, parse(this.q))).then(posts => {
if (posts.length == limit + 1) {
posts.pop();
this.existMore = true;
}
this.posts = posts;
this.fetching = false;
Progress.done();
});
},
more() {
if (this.moreFetching || this.fetching || this.posts.length == 0 || !this.existMore) return;
this.offset += limit;
this.moreFetching = true;
return (this as any).api('posts/search', Object.assign({
limit: limit + 1,
offset: this.offset
}, parse(this.q))).then(posts => {
if (posts.length == limit + 1) {
posts.pop();
} else {
this.existMore = false;
}
this.posts = this.posts.concat(posts);
this.moreFetching = false;
});
},
onScroll() {
const current = window.scrollY + window.innerHeight;
if (current > document.body.offsetHeight - 16) this.more();
}
}
});
</script>
<style lang="stylus" module>
.header
width 100%
max-width 600px
margin 0 auto
color #555
.posts
max-width 600px
margin 0 auto
border solid 1px rgba(0, 0, 0, 0.075)
border-radius 6px
overflow hidden
.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

@@ -0,0 +1,177 @@
<template>
<div class="mkp-selectdrive">
<mk-drive ref="browser"
:multiple="multiple"
@selected="onSelected"
@change-selection="onChangeSelection"
/>
<footer>
<button class="upload" title="%i18n:desktop.tags.mk-selectdrive-page.upload%" @click="upload">%fa:upload%</button>
<button class="cancel" @click="close">%i18n:desktop.tags.mk-selectdrive-page.cancel%</button>
<button class="ok" @click="ok">%i18n:desktop.tags.mk-selectdrive-page.ok%</button>
</footer>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
data() {
return {
files: []
};
},
computed: {
multiple(): boolean {
const q = (new URL(location.toString())).searchParams;
return q.get('multiple') == 'true';
}
},
mounted() {
document.title = '%i18n:desktop.tags.mk-selectdrive-page.title%';
},
methods: {
onSelected(file) {
this.files = [file];
this.ok();
},
onChangeSelection(files) {
this.files = files;
},
upload() {
(this.$refs.browser as any).selectLocalFile();
},
close() {
window.close();
},
ok() {
window.opener.cb(this.multiple ? this.files : this.files[0]);
this.close();
}
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
.mkp-selectdrive
display block
position fixed
width 100%
height 100%
background #fff
> .mk-drive
height calc(100% - 72px)
> footer
position fixed
bottom 0
left 0
width 100%
height 72px
background lighten($theme-color, 95%)
.upload
display inline-block
position absolute
top 8px
left 16px
cursor pointer
padding 0
margin 8px 4px 0 0
width 40px
height 40px
font-size 1em
color rgba($theme-color, 0.5)
background transparent
outline none
border solid 1px transparent
border-radius 4px
&:hover
background transparent
border-color rgba($theme-color, 0.3)
&:active
color rgba($theme-color, 0.6)
background transparent
border-color rgba($theme-color, 0.5)
box-shadow 0 2px 4px rgba(darken($theme-color, 50%), 0.15) inset
&:focus
&:after
content ""
pointer-events none
position absolute
top -5px
right -5px
bottom -5px
left -5px
border 2px solid rgba($theme-color, 0.3)
border-radius 8px
.ok
.cancel
display block
position absolute
bottom 16px
cursor pointer
padding 0
margin 0
width 120px
height 40px
font-size 1em
outline none
border-radius 4px
&:focus
&:after
content ""
pointer-events none
position absolute
top -5px
right -5px
bottom -5px
left -5px
border 2px solid rgba($theme-color, 0.3)
border-radius 8px
&:disabled
opacity 0.7
cursor default
.ok
right 16px
color $theme-color-foreground
background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
border solid 1px lighten($theme-color, 15%)
&:not(:disabled)
font-weight bold
&:hover:not(:disabled)
background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
border-color $theme-color
&:active:not(:disabled)
background $theme-color
border-color $theme-color
.cancel
right 148px
color #888
background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
border solid 1px #e2e2e2
&:hover
background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
border-color #dcdcdc
&:active
background #ececec
border-color #dcdcdc
</style>

View File

@@ -0,0 +1,84 @@
<template>
<div class="followers-you-know">
<p class="title">%fa:users%%i18n:desktop.tags.mk-user.followers-you-know.title%</p>
<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.followers-you-know.loading%<mk-ellipsis/></p>
<div v-if="!fetching && users.length > 0">
<router-link v-for="user in users" :to="`/@${getAcct(user)}`" :key="user.id">
<img :src="`${user.avatarUrl}?thumbnail&size=64`" :alt="user.name" v-user-preview="user.id"/>
</router-link>
</div>
<p class="empty" v-if="!fetching && users.length == 0">%i18n:desktop.tags.mk-user.followers-you-know.no-users%</p>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import getAcct from '../../../../../../common/user/get-acct';
export default Vue.extend({
props: ['user'],
data() {
return {
users: [],
fetching: true
};
},
method() {
getAcct
},
mounted() {
(this as any).api('users/followers', {
userId: this.user.id,
iknow: true,
limit: 16
}).then(x => {
this.users = x.users;
this.fetching = false;
});
}
});
</script>
<style lang="stylus" scoped>
.followers-you-know
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)
> i
margin-right 4px
> div
padding 8px
> a
display inline-block
margin 4px
> img
width 48px
height 48px
vertical-align bottom
border-radius 100%
> .initializing
> .empty
margin 0
padding 16px
text-align center
color #aaa
> i
margin-right 4px
</style>

View File

@@ -0,0 +1,124 @@
<template>
<div class="friends">
<p class="title">%fa:users%%i18n:desktop.tags.mk-user.frequently-replied-users.title%</p>
<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.frequently-replied-users.loading%<mk-ellipsis/></p>
<template v-if="!fetching && users.length != 0">
<div class="user" v-for="friend in users">
<router-link class="avatar-anchor" :to="`/@${getAcct(friend)}`">
<img class="avatar" :src="`${friend.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="friend.id"/>
</router-link>
<div class="body">
<router-link class="name" :to="`/@${getAcct(friend)}`" v-user-preview="friend.id">{{ friend.name }}</router-link>
<p class="username">@{{ getAcct(friend) }}</p>
</div>
<mk-follow-button :user="friend"/>
</div>
</template>
<p class="empty" v-if="!fetching && users.length == 0">%i18n:desktop.tags.mk-user.frequently-replied-users.no-users%</p>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import getAcct from '../../../../../../common/user/get-acct';
export default Vue.extend({
props: ['user'],
data() {
return {
users: [],
fetching: true
};
},
method() {
getAcct
},
mounted() {
(this as any).api('users/get_frequently_replied_users', {
userId: this.user.id,
limit: 4
}).then(docs => {
this.users = docs.map(doc => doc.user);
this.fetching = false;
});
}
});
</script>
<style lang="stylus" scoped>
.friends
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)
> i
margin-right 4px
> .initializing
> .empty
margin 0
padding 16px
text-align center
color #aaa
> i
margin-right 4px
> .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
</style>

View File

@@ -0,0 +1,196 @@
<template>
<div class="header" :data-is-dark-background="user.bannerUrl != null">
<div class="banner-container" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=2048)` : ''">
<div class="banner" ref="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=2048)` : ''" @click="onBannerClick"></div>
</div>
<div class="fade"></div>
<div class="container">
<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=150`" alt="avatar"/>
<div class="title">
<p class="name">{{ user.name }}</p>
<p class="username">@{{ acct }}</p>
<p class="location" v-if="user.host === null && user.account.profile.location">%fa:map-marker%{{ user.account.profile.location }}</p>
</div>
<footer>
<router-link :to="`/@${acct}`" :data-active="$parent.page == 'home'">%fa:home%概要</router-link>
<router-link :to="`/@${acct}/media`" :data-active="$parent.page == 'media'">%fa:image%メディア</router-link>
<router-link :to="`/@${acct}/graphs`" :data-active="$parent.page == 'graphs'">%fa:chart-bar%グラフ</router-link>
</footer>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import getAcct from '../../../../../../common/user/get-acct';
export default Vue.extend({
props: ['user'],
computed: {
acct() {
return getAcct(this.user);
}
},
mounted() {
window.addEventListener('load', this.onScroll);
window.addEventListener('scroll', this.onScroll);
window.addEventListener('resize', this.onScroll);
},
beforeDestroy() {
window.removeEventListener('load', this.onScroll);
window.removeEventListener('scroll', this.onScroll);
window.removeEventListener('resize', this.onScroll);
},
methods: {
onScroll() {
const banner = this.$refs.banner as any;
const top = window.scrollY;
const z = 1.25; // 奥行き(小さいほど奥)
const pos = -(top / z);
banner.style.backgroundPosition = `center calc(50% - ${pos}px)`;
const blur = top / 32
if (blur <= 10) banner.style.filter = `blur(${blur}px)`;
},
onBannerClick() {
if (!(this as any).os.isSignedIn || (this as any).os.i.id != this.user.id) return;
(this as any).apis.updateBanner((this as any).os.i, i => {
this.user.bannerUrl = i.bannerUrl;
});
}
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
.header
$banner-height = 320px
$footer-height = 58px
overflow hidden
background #f7f7f7
box-shadow 0 1px 1px rgba(0, 0, 0, 0.075)
&[data-is-dark-background]
> .banner-container
> .banner
background-color #383838
> .fade
background linear-gradient(transparent, rgba(0, 0, 0, 0.7))
> .container
> .title
color #fff
> .name
text-shadow 0 0 8px #000
> .banner-container
height $banner-height
overflow hidden
background-size cover
background-position center
> .banner
height 100%
background-color #f5f5f5
background-size cover
background-position center
> .fade
$fade-hight = 78px
position absolute
top ($banner-height - $fade-hight)
left 0
width 100%
height $fade-hight
> .container
max-width 1200px
margin 0 auto
> .avatar
display block
position absolute
bottom 16px
left 16px
z-index 2
width 160px
height 160px
margin 0
border solid 3px #fff
border-radius 8px
box-shadow 1px 1px 3px rgba(0, 0, 0, 0.2)
> .title
position absolute
bottom $footer-height
left 0
width 100%
padding 0 0 8px 195px
color #656565
font-family '游ゴシック', 'YuGothic', 'ヒラギノ角ゴ ProN W3', 'Hiragino Kaku Gothic ProN', 'Meiryo', 'メイリオ', sans-serif
> .name
display block
margin 0
line-height 40px
font-weight bold
font-size 2em
> .username
> .location
display inline-block
margin 0 16px 0 0
line-height 20px
opacity 0.8
> i
margin-right 4px
> footer
z-index 1
height $footer-height
padding-left 195px
> a
display inline-block
margin 0
padding 0 16px
height $footer-height
line-height $footer-height
color #555
&[data-active]
border-bottom solid 4px $theme-color
> i
margin-right 6px
> button
display block
position absolute
top 0
right 0
margin 8px
padding 0
width $footer-height - 16px
line-height $footer-height - 16px - 2px
font-size 1.2em
color #777
border solid 1px #eee
border-radius 4px
&:hover
color #555
border solid 1px #ddd
</style>

View File

@@ -0,0 +1,103 @@
<template>
<div class="home">
<div>
<div ref="left">
<x-profile :user="user"/>
<x-photos :user="user"/>
<x-followers-you-know v-if="os.isSignedIn && os.i.id != user.id" :user="user"/>
<p v-if="user.host === null">%i18n:desktop.tags.mk-user.last-used-at%: <b><mk-time :time="user.account.lastUsedAt"/></b></p>
</div>
</div>
<main>
<mk-post-detail v-if="user.pinnedPost" :post="user.pinnedPost" :compact="true"/>
<x-timeline class="timeline" ref="tl" :user="user"/>
</main>
<div>
<div ref="right">
<mk-calendar @chosen="warp" :start="new Date(user.createdAt)"/>
<mk-activity :user="user"/>
<x-friends :user="user"/>
<div class="nav"><mk-nav/></div>
</div>
</div>
</div>
</template>
<script lang="ts">
import Vue from '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';
export default Vue.extend({
components: {
XTimeline,
XProfile,
XPhotos,
XFollowersYouKnow,
XFriends
},
props: ['user'],
methods: {
warp(date) {
(this.$refs.tl as any).warp(date);
}
}
});
</script>
<style lang="stylus" scoped>
.home
display flex
justify-content center
margin 0 auto
max-width 1200px
> main
> div > div
> *:not(:last-child)
margin-bottom 16px
> main
padding 16px
width calc(100% - 275px * 2)
> .timeline
border solid 1px rgba(0, 0, 0, 0.075)
border-radius 6px
> div
width 275px
margin 0
&:first-child > div
padding 16px 0 16px 16px
> p
display block
margin 0
padding 0 12px
text-align center
font-size 0.8em
color #aaa
&:last-child > div
padding 16px 16px 16px 0
> .nav
padding 16px
font-size 12px
color #aaa
background #fff
border solid 1px rgba(0, 0, 0, 0.075)
border-radius 6px
a
color #999
i
color #ccc
</style>

View File

@@ -0,0 +1,88 @@
<template>
<div class="photos">
<p class="title">%fa:camera%%i18n:desktop.tags.mk-user.photos.title%</p>
<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.photos.loading%<mk-ellipsis/></p>
<div class="stream" v-if="!fetching && images.length > 0">
<div v-for="image in images" class="img"
:style="`background-image: url(${image.url}?thumbnail&size=256)`"
></div>
</div>
<p class="empty" v-if="!fetching && images.length == 0">%i18n:desktop.tags.mk-user.photos.no-photos%</p>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: ['user'],
data() {
return {
images: [],
fetching: true
};
},
mounted() {
(this as any).api('users/posts', {
userId: this.user.id,
withMedia: true,
limit: 9
}).then(posts => {
posts.forEach(post => {
post.media.forEach(media => {
if (this.images.length < 9) this.images.push(media);
});
});
this.fetching = false;
});
}
});
</script>
<style lang="stylus" scoped>
.photos
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)
> i
margin-right 4px
> .stream
display -webkit-flex
display -moz-flex
display -ms-flex
display flex
justify-content center
flex-wrap wrap
padding 8px
> .img
flex 1 1 33%
width 33%
height 80px
background-position center center
background-size cover
background-clip content-box
border solid 2px transparent
> .initializing
> .empty
margin 0
padding 16px
text-align center
color #aaa
> i
margin-right 4px
</style>

View File

@@ -0,0 +1,138 @@
<template>
<div class="profile">
<div class="friend-form" v-if="os.isSignedIn && os.i.id != user.id">
<mk-follow-button :user="user" size="big"/>
<p class="followed" v-if="user.isFollowed">%i18n:desktop.tags.mk-user.follows-you%</p>
<p v-if="user.isMuted">%i18n:desktop.tags.mk-user.muted% <a @click="unmute">%i18n:desktop.tags.mk-user.unmute%</a></p>
<p v-if="!user.isMuted"><a @click="mute">%i18n:desktop.tags.mk-user.mute%</a></p>
</div>
<div class="description" v-if="user.description">{{ user.description }}</div>
<div class="birthday" v-if="user.host === null && user.account.profile.birthday">
<p>%fa:birthday-cake%{{ user.account.profile.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ age }})</p>
</div>
<div class="twitter" v-if="user.host === null && user.account.twitter">
<p>%fa:B twitter%<a :href="`https://twitter.com/${user.account.twitter.screenName}`" target="_blank">@{{ user.account.twitter.screenName }}</a></p>
</div>
<div class="status">
<p class="posts-count">%fa:angle-right%<a>{{ user.postsCount }}</a><b>投稿</b></p>
<p class="following">%fa:angle-right%<a @click="showFollowing">{{ user.followingCount }}</a>人を<b>フォロー</b></p>
<p class="followers">%fa:angle-right%<a @click="showFollowers">{{ user.followersCount }}</a>人の<b>フォロワー</b></p>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import * as age from 's-age';
import MkFollowingWindow from '../../components/following-window.vue';
import MkFollowersWindow from '../../components/followers-window.vue';
export default Vue.extend({
props: ['user'],
computed: {
age(): number {
return age(this.user.account.profile.birthday);
}
},
methods: {
showFollowing() {
(this as any).os.new(MkFollowingWindow, {
user: this.user
});
},
showFollowers() {
(this as any).os.new(MkFollowersWindow, {
user: this.user
});
},
mute() {
(this as any).api('mute/create', {
userId: this.user.id
}).then(() => {
this.user.isMuted = true;
}, () => {
alert('error');
});
},
unmute() {
(this as any).api('mute/delete', {
userId: this.user.id
}).then(() => {
this.user.isMuted = false;
}, () => {
alert('error');
});
}
}
});
</script>
<style lang="stylus" scoped>
.profile
background #fff
border solid 1px rgba(0, 0, 0, 0.075)
border-radius 6px
> *:first-child
border-top none !important
> .friend-form
padding 16px
border-top solid 1px #eee
> .mk-big-follow-button
width 100%
> .followed
margin 12px 0 0 0
padding 0
text-align center
line-height 24px
font-size 0.8em
color #71afc7
background #eefaff
border-radius 4px
> .description
padding 16px
color #555
border-top solid 1px #eee
> .birthday
padding 16px
color #555
border-top solid 1px #eee
> p
margin 0
> i
margin-right 8px
> .twitter
padding 16px
color #555
border-top solid 1px #eee
> p
margin 0
> i
margin-right 8px
> .status
padding 16px
color #555
border-top solid 1px #eee
> p
margin 8px 0
> i
margin-left 8px
margin-right 8px
</style>

View File

@@ -0,0 +1,139 @@
<template>
<div class="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', {
userId: this.user.id,
untilDate: this.date ? this.date.getTime() : undefined,
with_replies: this.mode == 'with-replies'
}).then(posts => {
this.posts = posts;
this.fetching = false;
if (cb) cb();
});
},
more() {
if (this.moreFetching || this.fetching || this.posts.length == 0) return;
this.moreFetching = true;
(this as any).api('users/posts', {
userId: this.user.id,
with_replies: this.mode == 'with-replies',
untilId: 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();
}
},
warp(date) {
this.date = date;
this.fetch();
}
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
.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

@@ -0,0 +1,53 @@
<template>
<mk-ui>
<div class="user" v-if="!fetching">
<x-header :user="user"/>
<x-home v-if="page == 'home'" :user="user"/>
</div>
</mk-ui>
</template>
<script lang="ts">
import Vue from 'vue';
import parseAcct from '../../../../../../common/user/parse-acct';
import Progress from '../../../../common/scripts/loading';
import XHeader from './user.header.vue';
import XHome from './user.home.vue';
export default Vue.extend({
components: {
XHeader,
XHome
},
props: {
page: {
default: 'home'
}
},
data() {
return {
fetching: true,
user: null
};
},
watch: {
$route: 'fetch'
},
created() {
this.fetch();
},
methods: {
fetch() {
this.fetching = true;
Progress.start();
(this as any).api('users/show', parseAcct(this.$route.params.user)).then(user => {
this.user = user;
this.fetching = false;
Progress.done();
document.title = user.name + ' | Misskey';
});
}
}
});
</script>

View File

@@ -0,0 +1,319 @@
<template>
<div class="mk-welcome">
<main>
<div class="top">
<div>
<div>
<h1>Share<br><span ref="share">Everything!</span><span class="cursor">_</span></h1>
<p>ようこそ <b>Misskey</b>はTwitter風ミニブログSNSです思ったことや皆と共有したいことを投稿しましょうタイムラインを見れば皆の関心事をすぐにチェックすることもできます<a :href="aboutUrl">詳しく...</a></p>
<p><button class="signup" @click="signup">はじめる</button><button class="signin" @click="signin">ログイン</button></p>
<div class="users">
<router-link v-for="user in users" :key="user.id" class="avatar-anchor" :to="`/@${getAcct(user)}`" v-user-preview="user.id">
<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
</router-link>
</div>
</div>
<div>
<div>
<header>%fa:comments R% タイムライン<div><span></span><span></span><span></span></div></header>
<mk-welcome-timeline/>
</div>
</div>
</div>
</div>
</main>
<mk-forkit/>
<footer>
<div>
<mk-nav :class="$style.nav"/>
<p class="c">{{ copyright }}</p>
</div>
</footer>
<modal name="signup" width="500px" height="auto" scrollable>
<header :class="$style.signupFormHeader">新規登録</header>
<mk-signup :class="$style.signupForm"/>
</modal>
<modal name="signin" width="500px" height="auto" scrollable>
<header :class="$style.signinFormHeader">ログイン</header>
<mk-signin :class="$style.signinForm"/>
</modal>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { docsUrl, copyright, lang } from '../../../config';
import getAcct from '../../../../../common/user/get-acct';
const shares = [
'Everything!',
'Webpages',
'Photos',
'Interests',
'Favorites'
];
export default Vue.extend({
data() {
return {
aboutUrl: `${docsUrl}/${lang}/about`,
copyright,
users: [],
clock: null,
i: 0
};
},
mounted() {
(this as any).api('users', {
sort: '+follower',
limit: 20
}).then(users => {
this.users = users;
});
this.clock = setInterval(() => {
if (++this.i == shares.length) this.i = 0;
const speed = 70;
const text = (this.$refs.share as any).innerText;
for (let i = 0; i < text.length; i++) {
setTimeout(() => {
if (this.$refs.share) {
(this.$refs.share as any).innerText = text.substr(0, text.length - i);
}
}, i * speed)
}
setTimeout(() => {
const newText = shares[this.i];
for (let i = 0; i <= newText.length; i++) {
setTimeout(() => {
if (this.$refs.share) {
(this.$refs.share as any).innerText = newText.substr(0, i);
}
}, i * speed)
}
}, text.length * speed);
}, 4000);
},
beforeDestroy() {
clearInterval(this.clock);
},
methods: {
getAcct,
signup() {
this.$modal.show('signup');
},
signin() {
this.$modal.show('signin');
}
}
});
</script>
<style>
#wait {
right: auto;
left: 15px;
}
</style>
<style lang="stylus" scoped>
@import '~const.styl'
@import url('https://fonts.googleapis.com/css?family=Sarpanch:700')
.mk-welcome
display flex
flex-direction column
flex 1
$width = 1000px
background-image url('/assets/welcome-bg.svg')
background-size cover
background-position top center
&:before
content ""
display block
position fixed
bottom 0
left 0
width 100%
height 100%
background-image url('/assets/welcome-fg.svg')
background-size cover
background-position bottom center
> main
display flex
flex 1
> .top
display flex
width 100%
> div
display flex
max-width $width + 64px
margin 0 auto
padding 80px 32px 0 32px
> *
margin-bottom 48px
> div:first-child
margin-right 48px
color #fff
text-shadow 0 0 12px #172062
> h1
margin 0
font-weight bold
//font-variant small-caps
letter-spacing 12px
font-family 'Sarpanch', sans-serif
font-size 42px
line-height 48px
> .cursor
animation cursor 1s infinite linear both
@keyframes cursor
0%
opacity 1
50%
opacity 0
> p
margin 1em 0
line-height 2em
button
padding 8px 16px
font-size inherit
.signup
color $theme-color
border solid 2px $theme-color
border-radius 4px
&:focus
box-shadow 0 0 0 3px rgba($theme-color, 0.2)
&:hover
color $theme-color-foreground
background $theme-color
&:active
color $theme-color-foreground
background darken($theme-color, 10%)
border-color darken($theme-color, 10%)
.signin
&:hover
color #fff
> .users
margin 16px 0 0 0
> *
display inline-block
margin 4px
> *
display inline-block
width 38px
height 38px
vertical-align top
border-radius 6px
> div:last-child
> div
width 410px
background #fff
border-radius 8px
box-shadow 0 0 0 12px rgba(0, 0, 0, 0.1)
overflow hidden
> header
z-index 1
padding 12px 16px
color #888d94
box-shadow 0 1px 0px rgba(0, 0, 0, 0.1)
> div
position absolute
top 0
right 0
padding inherit
> span
display inline-block
height 11px
width 11px
margin-left 6px
background #ccc
border-radius 100%
vertical-align middle
&:nth-child(1)
background #5BCC8B
&:nth-child(2)
background #E6BB46
&:nth-child(3)
background #DF7065
> .mk-welcome-timeline
max-height 350px
overflow auto
> footer
font-size 12px
color #949ea5
> div
max-width $width
margin 0 auto
padding 0 0 42px 0
text-align center
> .c
margin 16px 0 0 0
font-size 10px
opacity 0.7
</style>
<style lang="stylus" module>
.signupForm
padding 24px 48px 48px 48px
.signupFormHeader
padding 48px 0 12px 0
margin: 0 48px
font-size 1.5em
color #777
border-bottom solid 1px #eee
.signinForm
padding 24px 48px 48px 48px
.signinFormHeader
padding 48px 0 12px 0
margin: 0 48px
font-size 1.5em
color #777
border-bottom solid 1px #eee
.nav
a
color #666
</style>
<style lang="stylus">
html
body
background linear-gradient(to bottom, #1e1d65, #bd6659)
</style>