diff --git a/locales/ja.yml b/locales/ja.yml index b3b50a5c8f..9dfff667ae 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -178,6 +178,11 @@ auth/views/index.vue: sign-in: "サインインしてください" common/views/components/games/reversi/reversi.vue: + matching: + waiting-for: "{}を待っています" + cancel: "キャンセル" + +common/views/components/games/reversi/reversi.index.vue: title: "Misskey Reversi" sub-title: "他のMisskeyユーザーとリバーシで対戦しよう" invite: "招待" @@ -192,9 +197,6 @@ common/views/components/games/reversi/reversi.vue: game-state: ended: "終了" playing: "進行中" - matching: - waiting-for: "{}を待っています" - cancel: "キャンセル" common/views/components/games/reversi/reversi.room.vue: settings-of-the-game: "ゲームの設定" diff --git a/src/client/app/common/views/components/games/reversi/reversi.game.vue b/src/client/app/common/views/components/games/reversi/reversi.game.vue index 34e9705dd4..bbfec2c1cc 100644 --- a/src/client/app/common/views/components/games/reversi/reversi.game.vue +++ b/src/client/app/common/views/components/games/reversi/reversi.game.vue @@ -1,14 +1,14 @@ <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> <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="logPos != logs.length">{{ '%i18n:common.reversi.past-turn-of%'.replace('{}', Vue.filter('userName')(turnUser)) }}</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('{}', $options.filters.userName(turnUser)) }}</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="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> </p> </div> @@ -258,12 +258,12 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -.root +root(isDark) text-align center > header padding 8px - border-bottom dashed 1px #c4cdd4 + border-bottom dashed 1px isDark ? #4c5761 : #c4cdd4 > .board width calc(100% - 16px) @@ -327,16 +327,16 @@ export default Vue.extend({ user-select none &.empty - border solid 2px #eee + border solid 2px isDark ? #51595f : #eee &.empty.can - background #eee + background isDark ? #51595f : #eee &.empty.myTurn - border-color #ddd + border-color isDark ? #6a767f : #ddd &.can - background #eee + background isDark ? #51595f : #eee cursor pointer &:hover @@ -350,7 +350,7 @@ export default Vue.extend({ box-shadow 0 0 0 4px rgba($theme-color, 0.7) &.isEnded - border-color #ddd + border-color isDark ? #6a767f : #ddd &.none border-color transparent !important @@ -388,4 +388,11 @@ export default Vue.extend({ display inline-block margin 0 8px min-width 70px + +.xqnhankfuuilcwvhgsopeqncafzsquya[data-darkmode] + root(true) + +.xqnhankfuuilcwvhgsopeqncafzsquya:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/common/views/components/games/reversi/reversi.index.vue b/src/client/app/common/views/components/games/reversi/reversi.index.vue new file mode 100644 index 0000000000..026159a0fd --- /dev/null +++ b/src/client/app/common/views/components/games/reversi/reversi.index.vue @@ -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> diff --git a/src/client/app/common/views/components/games/reversi/reversi.room.vue b/src/client/app/common/views/components/games/reversi/reversi.room.vue index 94b36d0870..de5040f630 100644 --- a/src/client/app/common/views/components/games/reversi/reversi.room.vue +++ b/src/client/app/common/views/components/games/reversi/reversi.room.vue @@ -1,78 +1,94 @@ <template> -<div class="root"> +<div class="urbixznjwwuukfsckrwzwsqzsxornqij"> <header><b>{{ game.user1 | userName }}</b> vs <b>{{ game.user2 | userName }}</b></header> <div> <p>%i18n:@settings-of-the-game%</p> - <el-card class="map"> - <div slot="header"> - <el-select :class="$style.mapSelect" v-model="mapName" placeholder="%i18n:@choose-map%" @change="onMapChange"> - <el-option label="%i18n:@random%" :value="null"/> - <el-option-group v-for="c in mapCategories" :key="c" :label="c"> - <el-option v-for="m in maps" v-if="m.category == c" :key="m.name" :label="m.name" :value="m.name"> - <span style="float: left">{{ m.name }}</span> - <span style="float: right; color: #8492a6; font-size: 13px" v-if="m.author">(by <i>{{ m.author }}</i>)</span> - </el-option> - </el-option-group> - </el-select> - </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 v-for="(x, i) in game.settings.map.join('')" - :data-none="x == ' '" - @click="onPixelClick(i, x)" - > - <template v-if="x == 'b'">%fa:circle%</template> - <template v-if="x == 'w'">%fa:circle R%</template> + <div class="card map"> + <header> + <select v-model="mapName" placeholder="%i18n:@choose-map%" @change="onMapChange"> + <option label="-Custom-" :value="mapName" v-if="mapName == '-Custom-'"/> + <option label="%i18n:@random%" :value="null"/> + <optgroup v-for="c in mapCategories" :key="c" :label="c"> + <option v-for="m in maps" v-if="m.category == c" :key="m.name" :label="m.name" :value="m.name">{{ m.name }}</option> + </optgroup> + </select> + </header> + + <div> + <div class="random" v-if="game.settings.map == null">%fa:dice%</div> + <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)` }"> + <div v-for="(x, i) in game.settings.map.join('')" + :data-none="x == ' '" + @click="onPixelClick(i, x)"> + <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'"><template v-if="$store.state.device.darkmode">%fa:circle%</template><template v-else>%fa:circle R%</template></template> + </div> </div> </div> - </el-card> + </div> - <el-card class="bw"> - <div slot="header"> + <div class="card"> + <header> <span>%i18n:@black-or-white%</span> - </div> - <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> + </header> - <el-card class="rules"> - <div slot="header"> + <div> + <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> - </div> - <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> + </header> - <el-card class="bot-form" v-if="form"> - <div slot="header"> + <div> + <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> + </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> - <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> - - <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> @@ -84,9 +100,9 @@ </p> <div class="actions"> - <el-button @click="exit">%i18n:@cancel%</el-button> - <el-button type="primary" @click="accept" v-if="!isAccepted">%i18n:@ready%</el-button> - <el-button type="primary" @click="cancel" v-if="isAccepted">%i18n:@cancel-ready%</el-button> + <form-button @click="exit">%i18n:@cancel%</form-button> + <form-button primary @click="accept" v-if="!isAccepted">%i18n:@ready%</form-button> + <form-button primary @click="cancel" v-if="isAccepted">%i18n:@cancel-ready%</form-button> </div> </footer> </div> @@ -202,11 +218,11 @@ export default Vue.extend({ }); }, - onMapChange(v) { - if (v == null) { + onMapChange() { + if (this.mapName == null) { this.game.settings.map = null; } 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.updateSettings(); @@ -233,9 +249,9 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -.root +root(isDark) text-align center - background #f9f9f9 + background isDark ? #191b22 : #f9f9f9 > header padding 8px @@ -244,54 +260,87 @@ export default Vue.extend({ > div padding 0 16px - > .map - > .bw - > .rules - > .bot-form - max-width 400px + > .card 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 position sticky bottom 0 padding 16px - background rgba(255, 255, 255, 0.9) - border-top solid 1px #c4cdd4 + background rgba(isDark ? #191b22 : #fff, 0.9) + border-top solid 1px isDark ? #606266 : #c4cdd4 > .status margin 0 0 16px 0 -</style> -<style lang="stylus" module> -.mapSelect - width 100% +.urbixznjwwuukfsckrwzwsqzsxornqij[data-darkmode] + root(true) -.board - display grid - 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 +.urbixznjwwuukfsckrwzwsqzsxornqij:not([data-darkmode]) + root(false) </style> - -<style lang="stylus"> -.el-alert__content - position initial !important -</style> diff --git a/src/client/app/common/views/components/games/reversi/reversi.vue b/src/client/app/common/views/components/games/reversi/reversi.vue index 43f1c6656a..4169a5465a 100644 --- a/src/client/app/common/views/components/games/reversi/reversi.vue +++ b/src/client/app/common/views/components/games/reversi/reversi.vue @@ -1,58 +1,16 @@ <template> -<div class="mk-reversi"> +<div class="vchtoekanapleubgzioubdtmlkribzfd"> <div v-if="game"> <x-gameroom :game="game"/> </div> <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> <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 class="index" v-else> - <h1>%i18n:@title%</h1> - <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> + <x-index @go="onGo" @matching="onMatching"/> </div> </div> </template> @@ -60,10 +18,12 @@ <script lang="ts"> import Vue from 'vue'; import XGameroom from './reversi.gameroom.vue'; +import XIndex from './reversi.index.vue'; export default Vue.extend({ components: { - XGameroom + XGameroom, + XIndex }, props: ['initGame'], @@ -71,12 +31,7 @@ export default Vue.extend({ data() { return { game: null, - games: [], - gamesFetching: true, - gamesMoreFetching: false, - myGames: [], matching: null, - invitations: [], connection: null, connectionId: null, pingClock: null @@ -101,17 +56,6 @@ export default Vue.extend({ this.connectionId = (this as any).os.streams.reversiStream.use(); 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(() => { if (this.matching) { @@ -122,17 +66,11 @@ export default Vue.extend({ } }, 3000); } - - (this as any).api('games/reversi/games').then(games => { - this.games = games; - this.gamesFetching = false; - }); }, beforeDestroy() { if (this.connection) { this.connection.off('matched', this.onMatched); - this.connection.off('invited', this.onInvited); (this as any).os.streams.reversiStream.dispose(this.connectionId); clearInterval(this.pingClock); @@ -140,33 +78,13 @@ export default Vue.extend({ }, methods: { - go(game) { - (this as any).api('games/reversi/games/show', { - gameId: game.id - }).then(game => { - this.matching = null; - this.game = game; - }); + onGo(game) { + this.matching = null; + this.game = 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.matching = user; - } else { - this.game = res; - } - }); - }); - }); + onMatching(user) { + this.matching = user; }, cancel() { @@ -188,10 +106,6 @@ export default Vue.extend({ onMatched(game) { this.matching = null; this.game = game; - }, - - onInvited(invite) { - this.invitations.unshift(invite); } } }); @@ -200,9 +114,9 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -.mk-reversi - color #677f84 - background #fff +root(isDark) + color isDark ? #fff : #677f84 + background isDark ? #191b22 : #fff > .matching > h1 @@ -219,109 +133,10 @@ export default Vue.extend({ text-align center border-top dashed 1px #c4cdd4 - > .index - > h1 - margin 0 - padding 24px - font-size 24px - text-align center - font-weight normal - color #fff - background linear-gradient(to bottom, #8bca3e, #d6cf31) +.vchtoekanapleubgzioubdtmlkribzfd[data-darkmode] + root(true) - & + p - margin 0 - padding 12px - margin-bottom 12px - text-align center - font-size 14px - border-bottom solid 1px #d3d9dc +.vchtoekanapleubgzioubdtmlkribzfd:not([data-darkmode]) + root(false) - > .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> diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts index c18a1c3b68..422a3da050 100644 --- a/src/client/app/common/views/components/index.ts +++ b/src/client/app/common/views/components/index.ts @@ -37,6 +37,8 @@ import uiTextarea from './ui/textarea.vue'; import uiSwitch from './ui/switch.vue'; import uiRadio from './ui/radio.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-menu', menu); @@ -75,3 +77,5 @@ Vue.component('ui-textarea', uiTextarea); Vue.component('ui-switch', uiSwitch); Vue.component('ui-radio', uiRadio); Vue.component('ui-select', uiSelect); +Vue.component('form-button', formButton); +Vue.component('form-radio', formRadio); diff --git a/src/client/app/common/views/components/ui/form/button.vue b/src/client/app/common/views/components/ui/form/button.vue new file mode 100644 index 0000000000..6e1475bc38 --- /dev/null +++ b/src/client/app/common/views/components/ui/form/button.vue @@ -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> diff --git a/src/client/app/common/views/components/ui/form/radio.vue b/src/client/app/common/views/components/ui/form/radio.vue new file mode 100644 index 0000000000..831981bb3e --- /dev/null +++ b/src/client/app/common/views/components/ui/form/radio.vue @@ -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>