Merge pull request #2077 from syuilo/#2066

なんか
This commit is contained in:
syuilo 2018-08-03 21:32:59 +09:00 committed by GitHub
commit 6da9da0e8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 670 additions and 323 deletions

View File

@ -178,6 +178,11 @@ auth/views/index.vue:
sign-in: "サインインしてください" sign-in: "サインインしてください"
common/views/components/games/reversi/reversi.vue: common/views/components/games/reversi/reversi.vue:
matching:
waiting-for: "{}を待っています"
cancel: "キャンセル"
common/views/components/games/reversi/reversi.index.vue:
title: "Misskey Reversi" title: "Misskey Reversi"
sub-title: "他のMisskeyユーザーとリバーシで対戦しよう" sub-title: "他のMisskeyユーザーとリバーシで対戦しよう"
invite: "招待" invite: "招待"
@ -192,9 +197,6 @@ common/views/components/games/reversi/reversi.vue:
game-state: game-state:
ended: "終了" ended: "終了"
playing: "進行中" playing: "進行中"
matching:
waiting-for: "{}を待っています"
cancel: "キャンセル"
common/views/components/games/reversi/reversi.room.vue: common/views/components/games/reversi/reversi.room.vue:
settings-of-the-game: "ゲームの設定" settings-of-the-game: "ゲームの設定"

View File

@ -1,14 +1,14 @@
<template> <template>
<div class="root"> <div class="xqnhankfuuilcwvhgsopeqncafzsquya">
<header><b>{{ blackUser | userName }}</b>(%i18n:common.reversi.black%) vs <b>{{ whiteUser | userName }}</b>(%i18n:common.reversi.white%)</header> <header><b>{{ blackUser | userName }}</b>(%i18n:common.reversi.black%) vs <b>{{ whiteUser | userName }}</b>(%i18n:common.reversi.white%)</header>
<div style="overflow: hidden"> <div style="overflow: hidden">
<p class="turn" v-if="!iAmPlayer && !game.isEnded">{{ '%i18n:common.reversi.turn-of%'.replace('{}', Vue.filter('userName')(turnUser)) }}<mk-ellipsis/></p> <p class="turn" v-if="!iAmPlayer && !game.isEnded">{{ '%i18n:common.reversi.turn-of%'.replace('{}', $options.filters.userName(turnUser)) }}<mk-ellipsis/></p>
<p class="turn" v-if="logPos != logs.length">{{ '%i18n:common.reversi.past-turn-of%'.replace('{}', Vue.filter('userName')(turnUser)) }}</p> <p class="turn" v-if="logPos != logs.length">{{ '%i18n:common.reversi.past-turn-of%'.replace('{}', $options.filters.userName(turnUser)) }}</p>
<p class="turn1" v-if="iAmPlayer && !game.isEnded && !isMyTurn">%i18n:common.reversi.opponent-turn%<mk-ellipsis/></p> <p class="turn1" v-if="iAmPlayer && !game.isEnded && !isMyTurn">%i18n:common.reversi.opponent-turn%<mk-ellipsis/></p>
<p class="turn2" v-if="iAmPlayer && !game.isEnded && isMyTurn" v-animate-css="{ classes: 'tada', iteration: 'infinite' }">%i18n:common.reversi.my-turn%</p> <p class="turn2" v-if="iAmPlayer && !game.isEnded && isMyTurn" v-animate-css="{ classes: 'tada', iteration: 'infinite' }">%i18n:common.reversi.my-turn%</p>
<p class="result" v-if="game.isEnded && logPos == logs.length"> <p class="result" v-if="game.isEnded && logPos == logs.length">
<template v-if="game.winner">{{ '%i18n:common.reversi.won%'.replace('{}', Vue.filter('userName')(game.winner)) }}{{ game.settings.isLlotheo ? ' (ロセオ)' : '' }}</template> <template v-if="game.winner">{{ '%i18n:common.reversi.won%'.replace('{}', $options.filters.userName(game.winner)) }}{{ game.settings.isLlotheo ? ' (ロセオ)' : '' }}</template>
<template v-else>%i18n:common.reversi.drawn%</template> <template v-else>%i18n:common.reversi.drawn%</template>
</p> </p>
</div> </div>
@ -258,12 +258,12 @@ export default Vue.extend({
<style lang="stylus" scoped> <style lang="stylus" scoped>
@import '~const.styl' @import '~const.styl'
.root root(isDark)
text-align center text-align center
> header > header
padding 8px padding 8px
border-bottom dashed 1px #c4cdd4 border-bottom dashed 1px isDark ? #4c5761 : #c4cdd4
> .board > .board
width calc(100% - 16px) width calc(100% - 16px)
@ -327,16 +327,16 @@ export default Vue.extend({
user-select none user-select none
&.empty &.empty
border solid 2px #eee border solid 2px isDark ? #51595f : #eee
&.empty.can &.empty.can
background #eee background isDark ? #51595f : #eee
&.empty.myTurn &.empty.myTurn
border-color #ddd border-color isDark ? #6a767f : #ddd
&.can &.can
background #eee background isDark ? #51595f : #eee
cursor pointer cursor pointer
&:hover &:hover
@ -350,7 +350,7 @@ export default Vue.extend({
box-shadow 0 0 0 4px rgba($theme-color, 0.7) box-shadow 0 0 0 4px rgba($theme-color, 0.7)
&.isEnded &.isEnded
border-color #ddd border-color isDark ? #6a767f : #ddd
&.none &.none
border-color transparent !important border-color transparent !important
@ -388,4 +388,11 @@ export default Vue.extend({
display inline-block display inline-block
margin 0 8px margin 0 8px
min-width 70px min-width 70px
.xqnhankfuuilcwvhgsopeqncafzsquya[data-darkmode]
root(true)
.xqnhankfuuilcwvhgsopeqncafzsquya:not([data-darkmode])
root(false)
</style> </style>

View File

@ -0,0 +1,258 @@
<template>
<div class="phgnkghfpyvkrvwiajkiuoxyrdaqpzcx">
<h1>%i18n:@title%</h1>
<p>%i18n:@sub-title%</p>
<div class="play">
<!--<el-button round>フリーマッチ(準備中)</el-button>-->
<form-button primary round @click="match">%i18n:@invite%</form-button>
<details>
<summary>%i18n:@rule%</summary>
<div>
<p>%i18n:@rule-desc%</p>
<dl>
<dt><b>%i18n:@mode-invite%</b></dt>
<dd>%i18n:@mode-invite-desc%</dd>
</dl>
</div>
</details>
</div>
<section v-if="invitations.length > 0">
<h2>%i18n:@invitations%</h2>
<div class="invitation" v-for="i in invitations" tabindex="-1" @click="accept(i)">
<mk-avatar class="avatar" :user="i.parent"/>
<span class="name"><b>{{ i.parent | userName }}</b></span>
<span class="username">@{{ i.parent.username }}</span>
<mk-time :time="i.createdAt"/>
</div>
</section>
<section v-if="myGames.length > 0">
<h2>%i18n:@my-games%</h2>
<a class="game" v-for="g in myGames" tabindex="-1" @click.prevent="go(g)" :href="`/reversi/${g.id}`">
<mk-avatar class="avatar" :user="g.user1"/>
<mk-avatar class="avatar" :user="g.user2"/>
<span><b>{{ g.user1 | userName }}</b> vs <b>{{ g.user2 | userName }}</b></span>
<span class="state">{{ g.isEnded ? '%i18n:@game-state.ended%' : '%i18n:@game-state.playing%' }}</span>
</a>
</section>
<section v-if="games.length > 0">
<h2>%i18n:@all-games%</h2>
<a class="game" v-for="g in games" tabindex="-1" @click.prevent="go(g)" :href="`/reversi/${g.id}`">
<mk-avatar class="avatar" :user="g.user1"/>
<mk-avatar class="avatar" :user="g.user2"/>
<span><b>{{ g.user1 | userName }}</b> vs <b>{{ g.user2 | userName }}</b></span>
<span class="state">{{ g.isEnded ? '%i18n:@game-state.ended%' : '%i18n:@game-state.playing%' }}</span>
</a>
</section>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
data() {
return {
games: [],
gamesFetching: true,
gamesMoreFetching: false,
myGames: [],
matching: null,
invitations: [],
connection: null,
connectionId: null
};
},
mounted() {
if (this.$store.getters.isSignedIn) {
this.connection = (this as any).os.streams.reversiStream.getConnection();
this.connectionId = (this as any).os.streams.reversiStream.use();
this.connection.on('invited', this.onInvited);
(this as any).api('games/reversi/games', {
my: true
}).then(games => {
this.myGames = games;
});
(this as any).api('games/reversi/invitations').then(invitations => {
this.invitations = this.invitations.concat(invitations);
});
}
(this as any).api('games/reversi/games').then(games => {
this.games = games;
this.gamesFetching = false;
});
},
beforeDestroy() {
if (this.connection) {
this.connection.off('invited', this.onInvited);
(this as any).os.streams.reversiStream.dispose(this.connectionId);
}
},
methods: {
go(game) {
(this as any).api('games/reversi/games/show', {
gameId: game.id
}).then(game => {
this.$emit('go', game);
});
},
match() {
(this as any).apis.input({
title: '%i18n:@enter-username%'
}).then(username => {
(this as any).api('users/show', {
username
}).then(user => {
(this as any).api('games/reversi/match', {
userId: user.id
}).then(res => {
if (res == null) {
this.$emit('matching', user);
} else {
this.$emit('go', res);
}
});
});
});
},
accept(invitation) {
(this as any).api('games/reversi/match', {
userId: invitation.parent.id
}).then(game => {
if (game) {
this.$emit('go', game);
}
});
},
onInvited(invite) {
this.invitations.unshift(invite);
}
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark)
> h1
margin 0
padding 24px
font-size 24px
text-align center
font-weight normal
color #fff
background linear-gradient(to bottom, isDark ? #45730e : #8bca3e, isDark ? #464300 : #d6cf31)
& + p
margin 0
padding 12px
margin-bottom 12px
text-align center
font-size 14px
border-bottom solid 1px isDark ? #535f65 : #d3d9dc
> .play
margin 0 auto
padding 0 16px
max-width 500px
text-align center
> details
margin 8px 0
> div
padding 16px
font-size 14px
text-align left
background isDark ? #282c37 : #f5f5f5
border-radius 8px
> section
margin 0 auto
padding 0 16px 16px 16px
max-width 500px
border-top solid 1px isDark ? #535f65 : #d3d9dc
> h2
margin 0
padding 16px 0 8px 0
font-size 16px
font-weight bold
.invitation
margin 8px 0
padding 8px
color isDark ? #fff : #677f84
background isDark ? #282c37 : #fff
box-shadow 0 2px 16px rgba(#000, isDark ? 0.7 : 0.15)
border-radius 6px
cursor pointer
*
pointer-events none
user-select none
&:focus
border-color $theme-color
&:hover
background isDark ? #313543 : #f5f5f5
&:active
background isDark ? #1e222b : #eee
> .avatar
width 32px
height 32px
border-radius 100%
> span
margin 0 8px
line-height 32px
.game
display block
margin 8px 0
padding 8px
color isDark ? #fff : #677f84
background isDark ? #282c37 : #fff
box-shadow 0 2px 16px rgba(#000, isDark ? 0.7 : 0.15)
border-radius 6px
cursor pointer
*
pointer-events none
user-select none
&:hover
background isDark ? #313543 : #f5f5f5
&:active
background isDark ? #1e222b : #eee
> .avatar
width 32px
height 32px
border-radius 100%
> span
margin 0 8px
line-height 32px
.phgnkghfpyvkrvwiajkiuoxyrdaqpzcx[data-darkmode]
root(true)
.phgnkghfpyvkrvwiajkiuoxyrdaqpzcx:not([data-darkmode])
root(false)
</style>

View File

@ -1,78 +1,94 @@
<template> <template>
<div class="root"> <div class="urbixznjwwuukfsckrwzwsqzsxornqij">
<header><b>{{ game.user1 | userName }}</b> vs <b>{{ game.user2 | userName }}</b></header> <header><b>{{ game.user1 | userName }}</b> vs <b>{{ game.user2 | userName }}</b></header>
<div> <div>
<p>%i18n:@settings-of-the-game%</p> <p>%i18n:@settings-of-the-game%</p>
<el-card class="map"> <div class="card map">
<div slot="header"> <header>
<el-select :class="$style.mapSelect" v-model="mapName" placeholder="%i18n:@choose-map%" @change="onMapChange"> <select v-model="mapName" placeholder="%i18n:@choose-map%" @change="onMapChange">
<el-option label="%i18n:@random%" :value="null"/> <option label="-Custom-" :value="mapName" v-if="mapName == '-Custom-'"/>
<el-option-group v-for="c in mapCategories" :key="c" :label="c"> <option label="%i18n:@random%" :value="null"/>
<el-option v-for="m in maps" v-if="m.category == c" :key="m.name" :label="m.name" :value="m.name"> <optgroup v-for="c in mapCategories" :key="c" :label="c">
<span style="float: left">{{ m.name }}</span> <option v-for="m in maps" v-if="m.category == c" :key="m.name" :label="m.name" :value="m.name">{{ m.name }}</option>
<span style="float: right; color: #8492a6; font-size: 13px" v-if="m.author">(by <i>{{ m.author }}</i>)</span> </optgroup>
</el-option> </select>
</el-option-group> </header>
</el-select>
</div> <div>
<div :class="$style.board" v-if="game.settings.map != null" :style="{ 'grid-template-rows': `repeat(${ game.settings.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map[0].length }, 1fr)` }"> <div class="random" v-if="game.settings.map == null">%fa:dice%</div>
<div v-for="(x, i) in game.settings.map.join('')" <div class="board" v-else :style="{ 'grid-template-rows': `repeat(${ game.settings.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map[0].length }, 1fr)` }">
:data-none="x == ' '" <div v-for="(x, i) in game.settings.map.join('')"
@click="onPixelClick(i, x)" :data-none="x == ' '"
> @click="onPixelClick(i, x)">
<template v-if="x == 'b'">%fa:circle%</template> <template v-if="x == 'b'"><template v-if="$store.state.device.darkmode">%fa:circle R%</template><template v-else>%fa:circle%</template></template>
<template v-if="x == 'w'">%fa:circle R%</template> <template v-if="x == 'w'"><template v-if="$store.state.device.darkmode">%fa:circle%</template><template v-else>%fa:circle R%</template></template>
</div>
</div> </div>
</div> </div>
</el-card> </div>
<el-card class="bw"> <div class="card">
<div slot="header"> <header>
<span>%i18n:@black-or-white%</span> <span>%i18n:@black-or-white%</span>
</div> </header>
<el-radio v-model="game.settings.bw" label="random" @change="updateSettings">%i18n:@random%</el-radio>
<el-radio v-model="game.settings.bw" :label="1" @change="updateSettings">{{ '%i18n:@black-is%'.split('{}')[0] }}{{ game.user1 | userName }}{{ '%i18n:@black-is%'.split('{}')[1] }}</el-radio>
<el-radio v-model="game.settings.bw" :label="2" @change="updateSettings">{{ '%i18n:@black-is%'.split('{}')[0] }}{{ game.user2 | userName }}{{ '%i18n:@black-is%'.split('{}')[1] }}</el-radio>
</el-card>
<el-card class="rules"> <div>
<div slot="header"> <form-radio v-model="game.settings.bw" value="random" @change="updateSettings">%i18n:@random%</form-radio>
<form-radio v-model="game.settings.bw" :value="1" @change="updateSettings">{{ '%i18n:@black-is%'.split('{}')[0] }}<b>{{ game.user1 | userName }}</b>{{ '%i18n:@black-is%'.split('{}')[1] }}</form-radio>
<form-radio v-model="game.settings.bw" :value="2" @change="updateSettings">{{ '%i18n:@black-is%'.split('{}')[0] }}<b>{{ game.user2 | userName }}</b>{{ '%i18n:@black-is%'.split('{}')[1] }}</form-radio>
</div>
</div>
<div class="card">
<header>
<span>%i18n:@rules%</span> <span>%i18n:@rules%</span>
</div> </header>
<mk-switch v-model="game.settings.isLlotheo" @change="updateSettings" text="%i18n:@is-llotheo%"/>
<mk-switch v-model="game.settings.loopedBoard" @change="updateSettings" text="%i18n:@looped-map%"/>
<mk-switch v-model="game.settings.canPutEverywhere" @change="updateSettings" text="%i18n:@can-put-everywhere%"/>
</el-card>
<el-card class="bot-form" v-if="form"> <div>
<div slot="header"> <mk-switch v-model="game.settings.isLlotheo" @change="updateSettings" text="%i18n:@is-llotheo%"/>
<mk-switch v-model="game.settings.loopedBoard" @change="updateSettings" text="%i18n:@looped-map%"/>
<mk-switch v-model="game.settings.canPutEverywhere" @change="updateSettings" text="%i18n:@can-put-everywhere%"/>
</div>
</div>
<div class="card" v-if="form">
<header>
<span>%i18n:@settings-of-the-bot%</span> <span>%i18n:@settings-of-the-bot%</span>
</header>
<div>
<el-alert v-for="message in messages"
:title="message.text"
:type="message.type"
:key="message.id"/>
<template v-for="item in form">
<mk-switch v-if="item.type == 'button'" v-model="item.value" :key="item.id" :text="item.label" @change="onChangeForm($event, item)">{{ item.desc || '' }}</mk-switch>
<div class="card" v-if="item.type == 'radio'" :key="item.id">
<header>
<span>{{ item.label }}</span>
</header>
<div>
<el-radio v-for="(r, i) in item.items" :key="item.id + ':' + i" v-model="item.value" :label="r.value" @change="onChangeForm($event, item)">{{ r.label }}</el-radio>
</div>
</div>
<div class="card" v-if="item.type == 'textbox'" :key="item.id">
<header>
<span>{{ item.label }}</span>
</header>
<div>
<el-input v-model="item.value" @change="onChangeForm($event, item)"/>
</div>
</div>
</template>
</div> </div>
<el-alert v-for="message in messages" </div>
:title="message.text"
:type="message.type"
:key="message.id"
/>
<template v-for="item in form">
<mk-switch v-if="item.type == 'button'" v-model="item.value" :key="item.id" :text="item.label" @change="onChangeForm($event, item)">{{ item.desc || '' }}</mk-switch>
<el-card v-if="item.type == 'radio'" :key="item.id">
<div slot="header">
<span>{{ item.label }}</span>
</div>
<el-radio v-for="(r, i) in item.items" :key="item.id + ':' + i" v-model="item.value" :label="r.value" @change="onChangeForm($event, item)">{{ r.label }}</el-radio>
</el-card>
<el-card v-if="item.type == 'textbox'" :key="item.id">
<div slot="header">
<span>{{ item.label }}</span>
</div>
<el-input v-model="item.value" @change="onChangeForm($event, item)"/>
</el-card>
</template>
</el-card>
</div> </div>
<footer> <footer>
@ -84,9 +100,9 @@
</p> </p>
<div class="actions"> <div class="actions">
<el-button @click="exit">%i18n:@cancel%</el-button> <form-button @click="exit">%i18n:@cancel%</form-button>
<el-button type="primary" @click="accept" v-if="!isAccepted">%i18n:@ready%</el-button> <form-button primary @click="accept" v-if="!isAccepted">%i18n:@ready%</form-button>
<el-button type="primary" @click="cancel" v-if="isAccepted">%i18n:@cancel-ready%</el-button> <form-button primary @click="cancel" v-if="isAccepted">%i18n:@cancel-ready%</form-button>
</div> </div>
</footer> </footer>
</div> </div>
@ -202,11 +218,11 @@ export default Vue.extend({
}); });
}, },
onMapChange(v) { onMapChange() {
if (v == null) { if (this.mapName == null) {
this.game.settings.map = null; this.game.settings.map = null;
} else { } else {
this.game.settings.map = Object.values(maps).find(x => x.name == v).data; this.game.settings.map = Object.values(maps).find(x => x.name == this.mapName).data;
} }
this.$forceUpdate(); this.$forceUpdate();
this.updateSettings(); this.updateSettings();
@ -233,9 +249,9 @@ export default Vue.extend({
<style lang="stylus" scoped> <style lang="stylus" scoped>
@import '~const.styl' @import '~const.styl'
.root root(isDark)
text-align center text-align center
background #f9f9f9 background isDark ? #191b22 : #f9f9f9
> header > header
padding 8px padding 8px
@ -244,54 +260,87 @@ export default Vue.extend({
> div > div
padding 0 16px padding 0 16px
> .map > .card
> .bw
> .rules
> .bot-form
max-width 400px
margin 0 auto 16px auto margin 0 auto 16px auto
&.map
> header
> select
width 100%
padding 12px 14px
background isDark ? #282C37 : #fff
border 1px solid isDark ? #6a707d : #dcdfe6
border-radius 4px
color isDark ? #fff : #606266
cursor pointer
transition border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1)
&:hover
border-color isDark ? #a7aebd : #c0c4cc
&:focus
&:active
border-color $theme-color
> div
> .random
padding 32px 0
font-size 64px
color isDark ? #4e5961 : #d8d8d8
> .board
display grid
grid-gap 4px
width 300px
height 300px
margin 0 auto
color isDark ? #fff : #444
> div
background transparent
border solid 2px isDark ? #6a767f : #ddd
border-radius 6px
overflow hidden
cursor pointer
*
pointer-events none
user-select none
width 100%
height 100%
&[data-none]
border-color transparent
.card
max-width 400px
border-radius 4px
background isDark ? #282C37 : #fff
color isDark ? #fff : #303133
box-shadow 0 2px 12px 0 rgba(#000, 0.1)
> header
padding 18px 20px
border-bottom 1px solid isDark ? #1c2023 : #ebeef5
> div
padding 20px
color isDark ? #fff : #606266
> footer > footer
position sticky position sticky
bottom 0 bottom 0
padding 16px padding 16px
background rgba(255, 255, 255, 0.9) background rgba(isDark ? #191b22 : #fff, 0.9)
border-top solid 1px #c4cdd4 border-top solid 1px isDark ? #606266 : #c4cdd4
> .status > .status
margin 0 0 16px 0 margin 0 0 16px 0
</style>
<style lang="stylus" module> .urbixznjwwuukfsckrwzwsqzsxornqij[data-darkmode]
.mapSelect root(true)
width 100%
.board .urbixznjwwuukfsckrwzwsqzsxornqij:not([data-darkmode])
display grid root(false)
grid-gap 4px
width 300px
height 300px
margin 0 auto
> div
background transparent
border solid 2px #ddd
border-radius 6px
overflow hidden
cursor pointer
*
pointer-events none
user-select none
width 100%
height 100%
&[data-none]
border-color transparent
</style> </style>
<style lang="stylus">
.el-alert__content
position initial !important
</style>

View File

@ -1,58 +1,16 @@
<template> <template>
<div class="mk-reversi"> <div class="vchtoekanapleubgzioubdtmlkribzfd">
<div v-if="game"> <div v-if="game">
<x-gameroom :game="game"/> <x-gameroom :game="game"/>
</div> </div>
<div class="matching" v-else-if="matching"> <div class="matching" v-else-if="matching">
<h1>{{ '%i18n:@matching.waiting-for%'.split('{}')[0] }}<b>{{ matching | userName }}</b>{{ '%i18n:@matching.waiting-for%'.split('{}')[1] }}<mk-ellipsis/></h1> <h1>{{ '%i18n:@matching.waiting-for%'.split('{}')[0] }}<b>{{ matching | userName }}</b>{{ '%i18n:@matching.waiting-for%'.split('{}')[1] }}<mk-ellipsis/></h1>
<div class="cancel"> <div class="cancel">
<el-button round @click="cancel">%i18n:@matching.cancel%</el-button> <form-button round @click="cancel">%i18n:@matching.cancel%</form-button>
</div> </div>
</div> </div>
<div class="index" v-else> <div class="index" v-else>
<h1>%i18n:@title%</h1> <x-index @go="onGo" @matching="onMatching"/>
<p>%i18n:@sub-title%</p>
<div class="play">
<!--<el-button round>フリーマッチ(準備中)</el-button>-->
<el-button type="primary" round @click="match">%i18n:@invite%</el-button>
<details>
<summary>%i18n:@rule%</summary>
<div>
<p>%i18n:@rule-desc%</p>
<dl>
<dt><b>%i18n:@mode-invite%</b></dt>
<dd>%i18n:@mode-invite-desc%</dd>
</dl>
</div>
</details>
</div>
<section v-if="invitations.length > 0">
<h2>%i18n:@invitations%</h2>
<div class="invitation" v-for="i in invitations" tabindex="-1" @click="accept(i)">
<mk-avatar class="avatar" :user="i.parent"/>
<span class="name"><b>{{ i.parent | userName }}</b></span>
<span class="username">@{{ i.parent.username }}</span>
<mk-time :time="i.createdAt"/>
</div>
</section>
<section v-if="myGames.length > 0">
<h2>%i18n:@my-games%</h2>
<a class="game" v-for="g in myGames" tabindex="-1" @click.prevent="go(g)" :href="`/reversi/${g.id}`">
<mk-avatar class="avatar" :user="g.user1"/>
<mk-avatar class="avatar" :user="g.user2"/>
<span><b>{{ g.user1 | userName }}</b> vs <b>{{ g.user2 | userName }}</b></span>
<span class="state">{{ g.isEnded ? '%i18n:@game-state.ended%' : '%i18n:@game-state.playing%' }}</span>
</a>
</section>
<section v-if="games.length > 0">
<h2>%i18n:@all-games%</h2>
<a class="game" v-for="g in games" tabindex="-1" @click.prevent="go(g)" :href="`/reversi/${g.id}`">
<mk-avatar class="avatar" :user="g.user1"/>
<mk-avatar class="avatar" :user="g.user2"/>
<span><b>{{ g.user1 | userName }}</b> vs <b>{{ g.user2 | userName }}</b></span>
<span class="state">{{ g.isEnded ? '%i18n:@game-state.ended%' : '%i18n:@game-state.playing%' }}</span>
</a>
</section>
</div> </div>
</div> </div>
</template> </template>
@ -60,10 +18,12 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import XGameroom from './reversi.gameroom.vue'; import XGameroom from './reversi.gameroom.vue';
import XIndex from './reversi.index.vue';
export default Vue.extend({ export default Vue.extend({
components: { components: {
XGameroom XGameroom,
XIndex
}, },
props: ['initGame'], props: ['initGame'],
@ -71,12 +31,7 @@ export default Vue.extend({
data() { data() {
return { return {
game: null, game: null,
games: [],
gamesFetching: true,
gamesMoreFetching: false,
myGames: [],
matching: null, matching: null,
invitations: [],
connection: null, connection: null,
connectionId: null, connectionId: null,
pingClock: null pingClock: null
@ -101,17 +56,6 @@ export default Vue.extend({
this.connectionId = (this as any).os.streams.reversiStream.use(); this.connectionId = (this as any).os.streams.reversiStream.use();
this.connection.on('matched', this.onMatched); this.connection.on('matched', this.onMatched);
this.connection.on('invited', this.onInvited);
(this as any).api('games/reversi/games', {
my: true
}).then(games => {
this.myGames = games;
});
(this as any).api('games/reversi/invitations').then(invitations => {
this.invitations = this.invitations.concat(invitations);
});
this.pingClock = setInterval(() => { this.pingClock = setInterval(() => {
if (this.matching) { if (this.matching) {
@ -122,17 +66,11 @@ export default Vue.extend({
} }
}, 3000); }, 3000);
} }
(this as any).api('games/reversi/games').then(games => {
this.games = games;
this.gamesFetching = false;
});
}, },
beforeDestroy() { beforeDestroy() {
if (this.connection) { if (this.connection) {
this.connection.off('matched', this.onMatched); this.connection.off('matched', this.onMatched);
this.connection.off('invited', this.onInvited);
(this as any).os.streams.reversiStream.dispose(this.connectionId); (this as any).os.streams.reversiStream.dispose(this.connectionId);
clearInterval(this.pingClock); clearInterval(this.pingClock);
@ -140,33 +78,13 @@ export default Vue.extend({
}, },
methods: { methods: {
go(game) { onGo(game) {
(this as any).api('games/reversi/games/show', { this.matching = null;
gameId: game.id this.game = game;
}).then(game => {
this.matching = null;
this.game = game;
});
}, },
match() { onMatching(user) {
(this as any).apis.input({ this.matching = user;
title: '%i18n:@enter-username%'
}).then(username => {
(this as any).api('users/show', {
username
}).then(user => {
(this as any).api('games/reversi/match', {
userId: user.id
}).then(res => {
if (res == null) {
this.matching = user;
} else {
this.game = res;
}
});
});
});
}, },
cancel() { cancel() {
@ -188,10 +106,6 @@ export default Vue.extend({
onMatched(game) { onMatched(game) {
this.matching = null; this.matching = null;
this.game = game; this.game = game;
},
onInvited(invite) {
this.invitations.unshift(invite);
} }
} }
}); });
@ -200,9 +114,9 @@ export default Vue.extend({
<style lang="stylus" scoped> <style lang="stylus" scoped>
@import '~const.styl' @import '~const.styl'
.mk-reversi root(isDark)
color #677f84 color isDark ? #fff : #677f84
background #fff background isDark ? #191b22 : #fff
> .matching > .matching
> h1 > h1
@ -219,109 +133,10 @@ export default Vue.extend({
text-align center text-align center
border-top dashed 1px #c4cdd4 border-top dashed 1px #c4cdd4
> .index .vchtoekanapleubgzioubdtmlkribzfd[data-darkmode]
> h1 root(true)
margin 0
padding 24px
font-size 24px
text-align center
font-weight normal
color #fff
background linear-gradient(to bottom, #8bca3e, #d6cf31)
& + p .vchtoekanapleubgzioubdtmlkribzfd:not([data-darkmode])
margin 0 root(false)
padding 12px
margin-bottom 12px
text-align center
font-size 14px
border-bottom solid 1px #d3d9dc
> .play
margin 0 auto
padding 0 16px
max-width 500px
text-align center
> details
margin 8px 0
> div
padding 16px
font-size 14px
text-align left
background #f5f5f5
border-radius 8px
> section
margin 0 auto
padding 0 16px 16px 16px
max-width 500px
border-top solid 1px #d3d9dc
> h2
margin 0
padding 16px 0 8px 0
font-size 16px
font-weight bold
.invitation
margin 8px 0
padding 8px
border solid 1px #e1e5e8
border-radius 6px
cursor pointer
*
pointer-events none
user-select none
&:focus
border-color $theme-color
&:hover
background #f5f5f5
&:active
background #eee
> .avatar
width 32px
height 32px
border-radius 100%
> span
margin 0 8px
line-height 32px
.game
display block
margin 8px 0
padding 8px
color #677f84
border solid 1px #e1e5e8
border-radius 6px
cursor pointer
*
pointer-events none
user-select none
&:focus
border-color $theme-color
&:hover
background #f5f5f5
&:active
background #eee
> .avatar
width 32px
height 32px
border-radius 100%
> span
margin 0 8px
line-height 32px
</style> </style>

View File

@ -37,6 +37,8 @@ import uiTextarea from './ui/textarea.vue';
import uiSwitch from './ui/switch.vue'; import uiSwitch from './ui/switch.vue';
import uiRadio from './ui/radio.vue'; import uiRadio from './ui/radio.vue';
import uiSelect from './ui/select.vue'; import uiSelect from './ui/select.vue';
import formButton from './ui/form/button.vue';
import formRadio from './ui/form/radio.vue';
Vue.component('mk-analog-clock', analogClock); Vue.component('mk-analog-clock', analogClock);
Vue.component('mk-menu', menu); Vue.component('mk-menu', menu);
@ -75,3 +77,5 @@ Vue.component('ui-textarea', uiTextarea);
Vue.component('ui-switch', uiSwitch); Vue.component('ui-switch', uiSwitch);
Vue.component('ui-radio', uiRadio); Vue.component('ui-radio', uiRadio);
Vue.component('ui-select', uiSelect); Vue.component('ui-select', uiSelect);
Vue.component('form-button', formButton);
Vue.component('form-radio', formRadio);

View File

@ -0,0 +1,86 @@
<template>
<div class="nvemkhtwcnnpkdrwfcbzuwhfulejhmzg" :class="{ round, primary }">
<button @click="$emit('click')">
<slot></slot>
</button>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
round: {
type: Boolean,
required: false,
default: false
},
primary: {
type: Boolean,
required: false,
default: false
}
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark)
display inline-block
& + .nvemkhtwcnnpkdrwfcbzuwhfulejhmzg
margin-left 12px
> button
display inline-block
margin 0
padding 12px 20px
font-size 14px
border 1px solid isDark ? #6d727d : #dcdfe6
border-radius 4px
outline none
box-shadow none
color isDark ? #fff : #606266
transition 0.1s
&:hover
&:focus
color $theme-color
background rgba($theme-color, isDark ? 0.2 : 0.12)
border-color rgba($theme-color, isDark ? 0.5 : 0.3)
&:active
color darken($theme-color, 20%)
background rgba($theme-color, 0.12)
border-color $theme-color
transition all 0s
&.primary
> button
border 1px solid $theme-color
background $theme-color
color $theme-color-foreground
&:hover
&:focus
background lighten($theme-color, 20%)
border-color lighten($theme-color, 20%)
&:active
background darken($theme-color, 20%)
border-color darken($theme-color, 20%)
transition all 0s
&.round
> button
border-radius 64px
.nvemkhtwcnnpkdrwfcbzuwhfulejhmzg[data-darkmode]
root(true)
.nvemkhtwcnnpkdrwfcbzuwhfulejhmzg:not([data-darkmode])
root(false)
</style>

View File

@ -0,0 +1,126 @@
<template>
<div
class="uywduthvrdnlpsvsjkqigicixgyfctto"
:class="{ disabled, checked }"
:aria-checked="checked"
:aria-disabled="disabled"
@click="toggle"
>
<input type="radio"
:disabled="disabled"
>
<span class="button">
<span></span>
</span>
<span class="label"><slot></slot></span>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
model: {
prop: 'model',
event: 'change'
},
props: {
model: {
required: false
},
value: {
required: false
},
disabled: {
type: Boolean,
default: false
}
},
computed: {
checked(): boolean {
return this.model === this.value;
}
},
methods: {
toggle() {
this.$emit('change', this.value);
}
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark)
display inline-flex
margin 0 16px 0 0
cursor pointer
transition all 0.3s
> *
user-select none
&:hover
> .button
border solid 2px isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
&.disabled
opacity 0.6
cursor not-allowed
&.checked
> .button
border-color $theme-color
&:after
background-color $theme-color
transform scale(1)
opacity 1
> .label
color $theme-color
> input
position absolute
width 0
height 0
opacity 0
margin 0
> .button
display inline-block
flex-shrink 0
width 20px
height 20px
background none
border solid 2px isDark ? rgba(#fff, 0.6) : rgba(#000, 0.4)
border-radius 100%
transition inherit
&:after
content ''
display block
position absolute
top 3px
right 3px
bottom 3px
left 3px
border-radius 100%
opacity 0
transform scale(0)
transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1)
> .label
margin-left 8px
display block
font-size 14px
line-height 20px
cursor pointer
.uywduthvrdnlpsvsjkqigicixgyfctto[data-darkmode]
root(true)
.uywduthvrdnlpsvsjkqigicixgyfctto:not([data-darkmode])
root(false)
</style>