1
0
mirror of https://github.com/hotomoe/hotomoe synced 2024-12-02 00:38:14 +09:00

Merge branch 'master' into l10n_master

This commit is contained in:
syuilo 2018-08-03 23:01:14 +09:00 committed by GitHub
commit e66ec6823d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 873 additions and 524 deletions

View File

@ -1041,6 +1041,8 @@ mobile/views/pages/settings/settings.profile.vue:
mobile/views/pages/search.vue: mobile/views/pages/search.vue:
search: "Search" search: "Search"
empty: "No posts were found for '{}'" empty: "No posts were found for '{}'"
mobile/views/pages/share.vue:
share-with: "Share with {}."
mobile/views/pages/selectdrive.vue: mobile/views/pages/selectdrive.vue:
select-file: "Choose files" select-file: "Choose files"
mobile/views/pages/settings.vue: mobile/views/pages/settings.vue:

View File

@ -6,6 +6,11 @@ common:
misskey: "A ⭐ of fediverse" misskey: "A ⭐ of fediverse"
about-title: "A ⭐ of fediverse." about-title: "A ⭐ of fediverse."
about: "Misskeyを見つけていただき、ありがとうございます。Misskeyは、地球で生まれた<b>分散マイクロブログSNS</b>です。Fediverse(様々なSNSで構成される宇宙)の中に存在するため、他のSNSと相互に繋がっています。暫し都会の喧騒から離れて、新しいインターネットにダイブしてみませんか。" about: "Misskeyを見つけていただき、ありがとうございます。Misskeyは、地球で生まれた<b>分散マイクロブログSNS</b>です。Fediverse(様々なSNSで構成される宇宙)の中に存在するため、他のSNSと相互に繋がっています。暫し都会の喧騒から離れて、新しいインターネットにダイブしてみませんか。"
adblock:
detected: "広告ブロッカーを無効にしてください"
warning: "<strong>Misskeyは広告を掲載していません</strong>が、広告をブロックする機能が有効だと一部の機能が利用できなかったり、不具合が発生する場合があります。"
application-authorization: "アプリの連携"
close: "閉じる"
customization-tips: customization-tips:
title: "カスタマイズのヒント" title: "カスタマイズのヒント"
paragraph1: "ホームのカスタマイズでは、ウィジェットを追加/削除したり、ドラッグ&ドロップして並べ替えたりすることができます。" paragraph1: "ホームのカスタマイズでは、ウィジェットを追加/削除したり、ドラッグ&ドロップして並べ替えたりすることができます。"
@ -13,6 +18,14 @@ common:
paragraph3: "ウィジェットを削除するには、ヘッダーの<strong>「ゴミ箱」</strong>と書かれたエリアにウィジェットをドラッグ&ドロップします。" paragraph3: "ウィジェットを削除するには、ヘッダーの<strong>「ゴミ箱」</strong>と書かれたエリアにウィジェットをドラッグ&ドロップします。"
paragraph4: "カスタマイズを終了するには、右上の「完了」をクリックします。" paragraph4: "カスタマイズを終了するには、右上の「完了」をクリックします。"
gotit: "Got it!" gotit: "Got it!"
notification:
file-uploaded: "ファイルがアップロードされました"
message-from: "{}さんからメッセージ:"
reversi-invited: "対局への招待があります"
reversi-invited-by: "{}さんから"
notified-by: "{}さんから"
reply-from: "{}さんから返信:"
quoted-by: "{}さんが引用:"
name: "Misskey" name: "Misskey"
time: time:
unknown: "なぞのじかん" unknown: "なぞのじかん"
@ -28,6 +41,13 @@ common:
trash: "ゴミ箱" trash: "ゴミ箱"
date:
full-year: "年"
month: "月"
day: "日"
hours: "時"
minutes: "分"
weekday-short: weekday-short:
sunday: "日" sunday: "日"
monday: "月" monday: "月"
@ -131,7 +151,38 @@ common:
stack-left: "左に重ねる" stack-left: "左に重ねる"
pop-right: "右に出す" pop-right: "右に出す"
auth/views/form.vue:
share-access: "<i>{{ app.name }}</i>があなたのアカウントにアクセスすることを<b>許可</b>しますか?"
permission-ask: "このアプリは次の権限を要求しています:"
account-read: "アカウントの情報を見る。"
account-write: "アカウントの情報を操作する。"
note-write: "投稿する。"
like-write: "いいねしたりいいね解除する。"
following-write: "フォローしたりフォロー解除する。"
drive-read: "ドライブを見る。"
drive-write: "ドライブを操作する。"
notification-read: "通知を見る。"
notification-write: "通知を操作する。"
cancel: "キャンセル"
accept: "アクセスを許可"
auth/views/index.vue:
loading: "読み込み中"
denied: "アプリケーションの連携をキャンセルしました。"
denied-paragraph: "このアプリがあなたのアカウントにアクセスすることはありません。"
already-authorized: "このアプリは既に連携済みです"
allowed: "アプリケーションの連携を許可しました"
callback-url: "アプリケーションに戻っています"
please-go-back: "アプリケーションに戻って、やっていってください。"
error: "セッションが存在しません。"
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: "招待"
@ -146,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: "ゲームの設定"
@ -870,8 +918,7 @@ desktop/views/pages/search.vue:
not-found: "「{}」に関する投稿は見つかりませんでした。" not-found: "「{}」に関する投稿は見つかりませんでした。"
desktop/views/pages/share.vue: desktop/views/pages/share.vue:
share-with: "Misskeyで共有" share-with: "{}で共有"
close: "閉じる"
desktop/views/pages/tag.vue: desktop/views/pages/tag.vue:
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
@ -1127,10 +1174,14 @@ mobile/views/pages/welcome.vue:
mobile/views/pages/widgets.vue: mobile/views/pages/widgets.vue:
dashboard: "ダッシュボード" dashboard: "ダッシュボード"
widgets-hints: "ウィジェットを追加/削除したり並べ替えたりできます。ウィジェットを移動するには「三」をドラッグします。ウィジェットを削除するには「x」をタップします。いくつかのウィジェットはタップすることで表示を変更できます。"
mobile/views/pages/widgets/activity.vue: mobile/views/pages/widgets/activity.vue:
activity: "アクティビティ" activity: "アクティビティ"
mobile/views/pages/share.vue:
share-with: "{}で共有"
mobile/views/pages/messaging.vue: mobile/views/pages/messaging.vue:
messaging: "メッセージ" messaging: "メッセージ"
@ -1151,7 +1202,7 @@ mobile/views/pages/notifications.vue:
notifications: "通知" notifications: "通知"
read-all: "すべての通知を既読にしますか?" read-all: "すべての通知を既読にしますか?"
mobile/views/pages/reversi.vue: mobile/views/pages/games/reversi.vue:
reversi: "リバーシ" reversi: "リバーシ"
mobile/views/pages/settings/settings.profile.vue: mobile/views/pages/settings/settings.profile.vue:

View File

@ -58,7 +58,7 @@
"@types/minio": "6.0.2", "@types/minio": "6.0.2",
"@types/mkdirp": "0.5.2", "@types/mkdirp": "0.5.2",
"@types/mocha": "5.2.3", "@types/mocha": "5.2.3",
"@types/mongodb": "3.1.2", "@types/mongodb": "3.1.3",
"@types/ms": "0.7.30", "@types/ms": "0.7.30",
"@types/node": "10.5.5", "@types/node": "10.5.5",
"@types/parse5": "5.0.0", "@types/parse5": "5.0.0",
@ -78,7 +78,7 @@
"@types/systeminformation": "3.23.0", "@types/systeminformation": "3.23.0",
"@types/tmp": "0.0.33", "@types/tmp": "0.0.33",
"@types/uuid": "3.4.3", "@types/uuid": "3.4.3",
"@types/webpack": "4.4.8", "@types/webpack": "4.4.9",
"@types/webpack-stream": "3.2.10", "@types/webpack-stream": "3.2.10",
"@types/websocket": "0.0.39", "@types/websocket": "0.0.39",
"@types/ws": "5.1.2", "@types/ws": "5.1.2",
@ -189,7 +189,7 @@
"stylus": "0.54.5", "stylus": "0.54.5",
"stylus-loader": "3.0.2", "stylus-loader": "3.0.2",
"summaly": "2.0.6", "summaly": "2.0.6",
"systeminformation": "3.42.4", "systeminformation": "3.42.8",
"syuilo-password-strength": "0.0.1", "syuilo-password-strength": "0.0.1",
"textarea-caret": "3.1.0", "textarea-caret": "3.1.0",
"tmp": "0.0.33", "tmp": "0.0.33",
@ -215,7 +215,7 @@
"vuex-persistedstate": "2.5.4", "vuex-persistedstate": "2.5.4",
"web-push": "3.3.2", "web-push": "3.3.2",
"webfinger.js": "2.6.6", "webfinger.js": "2.6.6",
"webpack": "4.16.3", "webpack": "4.16.4",
"webpack-cli": "3.1.0", "webpack-cli": "3.1.0",
"websocket": "1.0.26", "websocket": "1.0.26",
"ws": "6.0.0", "ws": "6.0.0",

View File

@ -15,7 +15,7 @@ import Index from './views/index.vue';
* init * init
*/ */
init(launch => { init(launch => {
document.title = 'Misskey | アプリの連携'; document.title = '%i18n:common.name% | %i18n:common.application-authorization%';
// Init router // Init router
const router = new VueRouter({ const router = new VueRouter({

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="form"> <div class="form">
<header> <header>
<h1><i>{{ app.name }}</i>があなたのアカウントにアクセスすることを<b>許可</b>しますか</h1> <h1>%i18n:@share-access%</h1>
<img :src="app.iconUrl"/> <img :src="app.iconUrl"/>
</header> </header>
<div class="app"> <div class="app">
@ -11,25 +11,25 @@
<p class="description">{{ app.description }}</p> <p class="description">{{ app.description }}</p>
</section> </section>
<section> <section>
<h2>このアプリは次の権限を要求しています:</h2> <h2>%i18n:@permission-ask%</h2>
<ul> <ul>
<template v-for="p in app.permission"> <template v-for="p in app.permission">
<li v-if="p == 'account-read'">アカウントの情報を見る</li> <li v-if="p == 'account-read'">%i18n:@account-read%</li>
<li v-if="p == 'account-write'">アカウントの情報を操作する</li> <li v-if="p == 'account-write'">%i18n:@account-write%</li>
<li v-if="p == 'note-write'">投稿する</li> <li v-if="p == 'note-write'">%i18n:@note-write%</li>
<li v-if="p == 'like-write'">いいねしたりいいね解除する</li> <li v-if="p == 'like-write'">%i18n:@like-write%</li>
<li v-if="p == 'following-write'">フォローしたりフォロー解除する</li> <li v-if="p == 'following-write'">%i18n:@following-write%</li>
<li v-if="p == 'drive-read'">ドライブを見る</li> <li v-if="p == 'drive-read'">%i18n:@drive-read%</li>
<li v-if="p == 'drive-write'">ドライブを操作する</li> <li v-if="p == 'drive-write'">%i18n:@drive-write%</li>
<li v-if="p == 'notification-read'">通知を見る</li> <li v-if="p == 'notification-read'">%i18n:@notification-read%</li>
<li v-if="p == 'notification-write'">通知を操作する</li> <li v-if="p == 'notification-write'">%i18n:@notification-write%</li>
</template> </template>
</ul> </ul>
</section> </section>
</div> </div>
<div class="action"> <div class="action">
<button @click="cancel">キャンセル</button> <button @click="cancel">%i18n:@cancel%</button>
<button @click="accept">アクセスを許可</button> <button @click="accept">%i18n:@accept%</button>
</div> </div>
</div> </div>
</template> </template>

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="index"> <div class="index">
<main v-if="$store.getters.isSignedIn"> <main v-if="$store.getters.isSignedIn">
<p class="fetching" v-if="fetching">読み込み中<mk-ellipsis/></p> <p class="fetching" v-if="fetching">%i18n:@loading%<mk-ellipsis/></p>
<x-form <x-form
class="form" class="form"
ref="form" ref="form"
@ -11,20 +11,20 @@
@accepted="accepted" @accepted="accepted"
/> />
<div class="denied" v-if="state == 'denied'"> <div class="denied" v-if="state == 'denied'">
<h1>アプリケーションの連携をキャンセルしました</h1> <h1>%i18n:@denied%</h1>
<p>このアプリがあなたのアカウントにアクセスすることはありません</p> <p>%i18n:@denied-paragraph%</p>
</div> </div>
<div class="accepted" v-if="state == 'accepted'"> <div class="accepted" v-if="state == 'accepted'">
<h1>{{ session.app.isAuthorized ? 'このアプリは既に連携済みです' : 'アプリケーションの連携を許可しました' }}</h1> <h1>{{ session.app.isAuthorized ? '%i18n:@already-authorized%' : '%i18n:@allowed%' }}</h1>
<p v-if="session.app.callbackUrl">アプリケーションに戻っています<mk-ellipsis/></p> <p v-if="session.app.callbackUrl">%i18n:@callback-url%<mk-ellipsis/></p>
<p v-if="!session.app.callbackUrl">アプリケーションに戻ってやっていってください</p> <p v-if="!session.app.callbackUrl">%i18n:@please-go-back%</p>
</div> </div>
<div class="error" v-if="state == 'fetch-session-error'"> <div class="error" v-if="state == 'fetch-session-error'">
<p>セッションが存在しません</p> <p>%i18n:@error%</p>
</div> </div>
</main> </main>
<main class="signin" v-if="!$store.getters.isSignedIn"> <main class="signin" v-if="!$store.getters.isSignedIn">
<h1>サインインしてください</h1> <h1>%i18n:@sign-in%</h1>
<mk-signin/> <mk-signin/>
</main> </main>
<footer><img src="/assets/auth/icon.svg" alt="Misskey"/></footer> <footer><img src="/assets/auth/icon.svg" alt="Misskey"/></footer>

View File

@ -15,22 +15,22 @@ export default function(type, data): Notification {
switch (type) { switch (type) {
case 'drive_file_created': case 'drive_file_created':
return { return {
title: 'ファイルがアップロードされました', title: '%i18n:common.notification.file-uploaded%',
body: data.name, body: data.name,
icon: data.url icon: data.url
}; };
case 'unread_messaging_message': case 'unread_messaging_message':
return { return {
title: `${getUserName(data.user)}さんからメッセージ:`, title: '%i18n:common.notification.message-from%'.split("{}")[0] + `${getUserName(data.user)}` + '%i18n:common.notification.message-from%'.split("{}")[1] ,
body: data.text, // TODO: getMessagingMessageSummary(data), body: data.text, // TODO: getMessagingMessageSummary(data),
icon: data.user.avatarUrl icon: data.user.avatarUrl
}; };
case 'reversi_invited': case 'reversi_invited':
return { return {
title: '対局への招待があります', title: '%i18n:common.notification.reversi-invited%',
body: `${getUserName(data.parent)}さんから`, body: '%i18n:common.notification.reversi-invited-by%'.split("{}")[0] + `${getUserName(data.parent)}` + '%i18n:common.notification.reversi-invited-by%'.split("{}")[1],
icon: data.parent.avatarUrl icon: data.parent.avatarUrl
}; };
@ -38,21 +38,21 @@ export default function(type, data): Notification {
switch (data.type) { switch (data.type) {
case 'mention': case 'mention':
return { return {
title: `${getUserName(data.user)}さんから:`, title: '%i18n:common.notification.notified-by%'.split("{}")[0] + `${getUserName(data.user)}さんから:` + '%i18n:common.notification.notified-by%'.split("{}")[1],
body: getNoteSummary(data), body: getNoteSummary(data),
icon: data.user.avatarUrl icon: data.user.avatarUrl
}; };
case 'reply': case 'reply':
return { return {
title: `${getUserName(data.user)}さんから返信:`, title: '%i18n:common.notification.reply-from%'.split("{}")[0] + `${getUserName(data.user)}` + '%i18n:common.notification.reply-from%'.split("{}")[1],
body: getNoteSummary(data), body: getNoteSummary(data),
icon: data.user.avatarUrl icon: data.user.avatarUrl
}; };
case 'quote': case 'quote':
return { return {
title: `${getUserName(data.user)}さんが引用:`, title: '%i18n:common.notification.quoted-by%'.split("{}")[0] + `${getUserName(data.user)}` + '%i18n:common.notification.quoted-by%'.split("{}")[1],
body: getNoteSummary(data), body: getNoteSummary(data),
icon: data.user.avatarUrl icon: data.user.avatarUrl
}; };

View File

@ -1,12 +1,12 @@
export default date => { export default date => {
if (typeof date == 'string') date = new Date(date); if (typeof date == 'string') date = new Date(date);
return ( return (
date.getFullYear() + '' + date.getFullYear() + '%i18n:common.date.full-year%' +
(date.getMonth() + 1) + '' + (date.getMonth() + 1) + '%i18n:common.date.month%' +
date.getDate() + '' + date.getDate() + '%i18n:common.date.day%' +
' ' + ' ' +
date.getHours() + '' + date.getHours() + '%i18n:common.date.hours%' +
date.getMinutes() + '' + date.getMinutes() + '%i18n:common.date.minutes%' +
' ' + ' ' +
`(${['日', '月', '火', '水', '木', '金', '土'][date.getDay()]})` `(${['日', '月', '火', '水', '木', '金', '土'][date.getDay()]})`
); );

View File

@ -5,8 +5,8 @@ declare const fuckAdBlock: any;
export default (os) => { export default (os) => {
function adBlockDetected() { function adBlockDetected() {
os.apis.dialog({ os.apis.dialog({
title: '%fa:exclamation-triangle%広告ブロッカーを無効にしてください', title: '%fa:exclamation-triangle%%i18n:common.adblock.detected%',
text: '<strong>Misskeyは広告を掲載していません</strong>が、広告をブロックする機能が有効だと一部の機能が利用できなかったり、不具合が発生する場合があります。', text: '%i18n:common.adblock.warning%',
actins: [{ actins: [{
text: 'OK' text: 'OK'
}] }]

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('{}', turnUser | userName) }}<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('{}', turnUser | userName) }}</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('{}', game.winner | userName) }}{{ 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="nav" @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,23 +18,26 @@
<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';
import Progress from '../../../../scripts/loading';
export default Vue.extend({ export default Vue.extend({
components: { components: {
XGameroom XGameroom,
XIndex
}, },
props: ['initGame'], props: {
gameId: {
type: String,
required: false
}
},
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
@ -84,14 +45,18 @@ export default Vue.extend({
}, },
watch: { watch: {
game(g) { gameId(id) {
this.$emit('gamed', g); if (id == null) {
} this.game = null;
}, } else {
Progress.start();
created() { (this as any).api('games/reversi/games/show', {
if (this.initGame) { gameId: id
this.game = this.initGame; }).then(game => {
this.nav(game, true);
Progress.done();
});
}
} }
}, },
@ -101,17 +66,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 +76,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 +88,17 @@ export default Vue.extend({
}, },
methods: { methods: {
go(game) { nav(game, silent) {
(this as any).api('games/reversi/games/show', { this.matching = null;
gameId: game.id this.game = game;
}).then(game => {
this.matching = null; if (!silent) {
this.game = game; this.$emit('nav', this.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 +120,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 +128,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 +147,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>

View File

@ -71,7 +71,7 @@ export default Vue.extend({
this.user = user; this.user = user;
this.fetching = false; this.fetching = false;
Progress.done(); Progress.done();
document.title = getUserName(this.user) + ' | Misskey'; document.title = getUserName(this.user) + ' | %i18n:common.name%';
}); });
}, },

View File

@ -34,7 +34,7 @@ import MkMessagingRoom from './views/pages/messaging-room.vue';
import MkNote from './views/pages/note.vue'; import MkNote from './views/pages/note.vue';
import MkSearch from './views/pages/search.vue'; import MkSearch from './views/pages/search.vue';
import MkTag from './views/pages/tag.vue'; import MkTag from './views/pages/tag.vue';
import MkReversi from './views/pages/reversi.vue'; import MkReversi from './views/pages/games/reversi.vue';
import MkShare from './views/pages/share.vue'; import MkShare from './views/pages/share.vue';
import MkFollow from '../common/views/pages/follow.vue'; import MkFollow from '../common/views/pages/follow.vue';
@ -65,8 +65,7 @@ init(async (launch) => {
{ path: '/search', component: MkSearch }, { path: '/search', component: MkSearch },
{ path: '/tags/:tag', component: MkTag }, { path: '/tags/:tag', component: MkTag },
{ path: '/share', component: MkShare }, { path: '/share', component: MkShare },
{ path: '/reversi', component: MkReversi }, { path: '/reversi/:game?', component: MkReversi },
{ path: '/reversi/:game', component: MkReversi },
{ path: '/@:user', component: MkUser }, { path: '/@:user', component: MkUser },
{ path: '/notes/:note', component: MkNote }, { path: '/notes/:note', component: MkNote },
{ path: '/authorize-follow', component: MkFollow } { path: '/authorize-follow', component: MkFollow }

View File

@ -187,7 +187,7 @@ export default Vue.extend({
clearNotification() { clearNotification() {
this.unreadCount = 0; this.unreadCount = 0;
document.title = 'Misskey'; document.title = '%i18n:common.name%';
}, },
onVisibilitychange() { onVisibilitychange() {

View File

@ -0,0 +1,22 @@
<template>
<component :is="ui ? 'mk-ui' : 'div'">
<mk-reversi :game-id="$route.params.game" @nav="nav"/>
</component>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
ui: {
default: false
}
},
methods: {
nav(game) {
history.pushState(null, null, '/reversi/' + game.id);
},
}
});
</script>

View File

@ -6,7 +6,7 @@
import Vue from 'vue'; import Vue from 'vue';
export default Vue.extend({ export default Vue.extend({
mounted() { mounted() {
document.title = 'Misskey - %i18n:@title%'; document.title = '%i18n:common.name% - %i18n:@title%';
} }
}); });
</script> </script>

View File

@ -16,7 +16,7 @@ export default Vue.extend({
} }
}, },
mounted() { mounted() {
document.title = 'Misskey'; document.title = '%i18n:common.name%';
Progress.start(); Progress.start();
}, },

View File

@ -1,50 +0,0 @@
<template>
<component :is="ui ? 'mk-ui' : 'div'">
<mk-reversi 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('games/reversi/games/show', {
gameId: this.$route.params.game
}).then(game => {
this.game = game;
this.fetching = false;
Progress.done();
});
},
onGamed(game) {
history.pushState(null, null, '/reversi/' + game.id);
}
}
});
</script>

View File

@ -1,12 +1,12 @@
<template> <template>
<div class="pptjhabgjtt7kwskbfv4y3uml6fpuhmr"> <div class="pptjhabgjtt7kwskbfv4y3uml6fpuhmr">
<h1>%i18n:@share-with%</h1> <h1>{{'%i18n:@share-with%'.split("{}")[0] + '%i18n:common.name%' + '%i18n:@share-with%'.split("{}")[1]}}</h1>
<div> <div>
<mk-signin v-if="!$store.getters.isSignedIn"/> <mk-signin v-if="!$store.getters.isSignedIn"/>
<mk-post-form v-else-if="!posted" :initial-text="text" :instant="true" @posted="posted = true"/> <mk-post-form v-else-if="!posted" :initial-text="text" :instant="true" @posted="posted = true"/>
<p v-if="posted" class="posted">%fa:check%</p> <p v-if="posted" class="posted">%fa:check%</p>
</div> </div>
<button v-if="posted" class="ui button" @click="close">%i18n:@close%</button> <button v-if="posted" class="ui button" @click="close">%i18n:common.close%</button>
</div> </div>
</template> </template>

View File

@ -68,7 +68,7 @@ export default Vue.extend({
this.user = user; this.user = user;
this.fetching = false; this.fetching = false;
Progress.done(); Progress.done();
document.title = getUserName(this.user) + ' | Misskey'; document.title = getUserName(this.user) + ' | %i18n:common.name%';
}); });
}, },

View File

@ -17,7 +17,7 @@
<main> <main>
<div class="about"> <div class="about">
<h1 v-if="name">{{ name }}</h1> <h1 v-if="name">{{ name }}</h1>
<h1 v-else><img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" alt="Misskey"></h1> <h1 v-else><img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" alt="%i18n:common.name%"></h1>
<p class="powerd-by" v-if="name">%i18n:@powered-by-misskey%</p> <p class="powerd-by" v-if="name">%i18n:@powered-by-misskey%</p>
<p class="desc" v-html="description || '%i18n:common.about%'"></p> <p class="desc" v-html="description || '%i18n:common.about%'"></p>
<a ref="signup" @click="signup">📦 %i18n:@signup%</a> <a ref="signup" @click="signup">📦 %i18n:@signup%</a>
@ -32,7 +32,7 @@
<mk-nav class="nav"/> <mk-nav class="nav"/>
</div> </div>
<mk-forkit class="forkit"/> <mk-forkit class="forkit"/>
<img src="assets/title.dark.svg" alt="Misskey"> <img src="assets/title.dark.svg" alt="%i18n:common.name%">
</div> </div>
<div class="tl"> <div class="tl">
<mk-welcome-timeline :max="20"/> <mk-welcome-timeline :max="20"/>

View File

@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<b-navbar toggleable="md" type="dark" variant="info"> <b-navbar toggleable="md" type="dark" variant="info">
<b-navbar-brand>Misskey Developers</b-navbar-brand> <b-navbar-brand>%i18n:common.name% Developers</b-navbar-brand>
<b-navbar-nav> <b-navbar-nav>
<b-nav-item to="/">Home</b-nav-item> <b-nav-item to="/">Home</b-nav-item>
<b-nav-item to="/apps">Apps</b-nav-item> <b-nav-item to="/apps">Apps</b-nav-item>

View File

@ -35,7 +35,7 @@ import MkFavorites from './views/pages/favorites.vue';
import MkUserLists from './views/pages/user-lists.vue'; import MkUserLists from './views/pages/user-lists.vue';
import MkUserList from './views/pages/user-list.vue'; import MkUserList from './views/pages/user-list.vue';
import MkSettings from './views/pages/settings.vue'; import MkSettings from './views/pages/settings.vue';
import MkReversi from './views/pages/reversi.vue'; import MkReversi from './views/pages/games/reversi.vue';
import MkTag from './views/pages/tag.vue'; import MkTag from './views/pages/tag.vue';
import MkShare from './views/pages/share.vue'; import MkShare from './views/pages/share.vue';
import MkFollow from '../common/views/pages/follow.vue'; import MkFollow from '../common/views/pages/follow.vue';
@ -76,8 +76,7 @@ init((launch) => {
{ path: '/search', component: MkSearch }, { path: '/search', component: MkSearch },
{ path: '/tags/:tag', component: MkTag }, { path: '/tags/:tag', component: MkTag },
{ path: '/share', component: MkShare }, { path: '/share', component: MkShare },
{ path: '/reversi', name: 'reversi', component: MkReversi }, { path: '/reversi/:game?', name: 'reversi', component: MkReversi },
{ path: '/reversi/:game', component: MkReversi },
{ path: '/@:user', component: MkUser }, { path: '/@:user', component: MkUser },
{ path: '/@:user/followers', component: MkFollowers }, { path: '/@:user/followers', component: MkFollowers },
{ path: '/@:user/following', component: MkFollowing }, { path: '/@:user/following', component: MkFollowing },

View File

@ -183,7 +183,7 @@ export default Vue.extend({
clearNotification() { clearNotification() {
this.unreadCount = 0; this.unreadCount = 0;
document.title = 'Misskey'; document.title = '%i18n:common.name%';
}, },
onVisibilitychange() { onVisibilitychange() {

View File

@ -43,7 +43,7 @@ export default Vue.extend({
window.addEventListener('popstate', this.onPopState); window.addEventListener('popstate', this.onPopState);
}, },
mounted() { mounted() {
document.title = 'Misskey Drive'; document.title = '%i18n:common.name% Drive';
document.documentElement.style.background = '#fff'; document.documentElement.style.background = '#fff';
}, },
beforeDestroy() { beforeDestroy() {
@ -63,7 +63,7 @@ export default Vue.extend({
(this.$refs as any).browser.openContextMenu(); (this.$refs as any).browser.openContextMenu();
}, },
onMoveRoot(silent) { onMoveRoot(silent) {
const title = 'Misskey Drive'; const title = '%i18n:common.name% Drive';
if (!silent) { if (!silent) {
// Rewrite URL // Rewrite URL
@ -76,7 +76,7 @@ export default Vue.extend({
this.folder = null; this.folder = null;
}, },
onOpenFolder(folder, silent) { onOpenFolder(folder, silent) {
const title = folder.name + ' | Misskey Drive'; const title = folder.name + ' | %i18n:common.name% Drive';
if (!silent) { if (!silent) {
// Rewrite URL // Rewrite URL
@ -89,7 +89,7 @@ export default Vue.extend({
this.folder = folder; this.folder = folder;
}, },
onOpenFile(file, silent) { onOpenFile(file, silent) {
const title = file.name + ' | Misskey Drive'; const title = file.name + ' | %i18n:common.name% Drive';
if (!silent) { if (!silent) {
// Rewrite URL // Rewrite URL

View File

@ -28,7 +28,7 @@ export default Vue.extend({
this.fetch(); this.fetch();
}, },
mounted() { mounted() {
document.title = 'Misskey | %i18n:@notifications%'; document.title = '%i18n:common.name% | %i18n:@notifications%';
}, },
methods: { methods: {
fetch() { fetch() {

View File

@ -49,7 +49,7 @@ export default Vue.extend({
this.user = user; this.user = user;
this.fetching = false; this.fetching = false;
document.title = '%i18n:@followers-of%'.replace('{}', this.name) + ' | Misskey'; document.title = '%i18n:@followers-of%'.replace('{}', this.name) + ' | %i18n:common.name%';
}); });
}, },
onLoaded() { onLoaded() {

View File

@ -48,7 +48,7 @@ export default Vue.extend({
this.user = user; this.user = user;
this.fetching = false; this.fetching = false;
document.title = '%i18n:@followers-of%'.replace('{}', this.name) + ' | Misskey'; document.title = '%i18n:@followers-of%'.replace('{}', this.name) + ' | %i18n:common.name%';
}); });
}, },
onLoaded() { onLoaded() {

View File

@ -0,0 +1,22 @@
<template>
<mk-ui>
<span slot="header">%fa:gamepad%%i18n:@reversi%</span>
<mk-reversi :game-id="$route.params.game" @nav="nav"/>
</mk-ui>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
mounted() {
document.title = '%i18n:common.name% %i18n:@reversi%';
document.documentElement.style.background = '#fff';
},
methods: {
nav(game) {
history.pushState(null, null, '/reversi/' + game.id);
}
}
});
</script>

View File

@ -96,7 +96,7 @@ export default Vue.extend({
}, },
mounted() { mounted() {
document.title = 'Misskey'; document.title = '%i18n:common.name%';
Progress.start(); Progress.start();

View File

@ -47,7 +47,7 @@ export default Vue.extend({
this.user = user; this.user = user;
this.fetching = false; this.fetching = false;
document.title = `%i18n:@messaging%: ${Vue.filter('userName')(this.user)} | Misskey`; document.title = `%i18n:@messaging%: ${Vue.filter('userName')(this.user)} | %i18n:common.name%`;
}); });
} }
} }

View File

@ -11,7 +11,7 @@ import getAcct from '../../../../../misc/acct/render';
export default Vue.extend({ export default Vue.extend({
mounted() { mounted() {
document.title = 'Misskey %i18n:@messaging%'; document.title = '%i18n:common.name% %i18n:@messaging%';
}, },
methods: { methods: {
navigate(user) { navigate(user) {

View File

@ -31,7 +31,7 @@ export default Vue.extend({
this.fetch(); this.fetch();
}, },
mounted() { mounted() {
document.title = 'Misskey'; document.title = '%i18n:common.name%';
}, },
methods: { methods: {
fetch() { fetch() {

View File

@ -15,7 +15,7 @@ import Progress from '../../../common/scripts/loading';
export default Vue.extend({ export default Vue.extend({
mounted() { mounted() {
document.title = 'Misskey | %i18n:@notifications%'; document.title = '%i18n:common.name% | %i18n:@notifications%';
Progress.start(); Progress.start();
}, },

View File

@ -25,7 +25,7 @@ export default Vue.extend({
}; };
}, },
mounted() { mounted() {
document.title = 'Misskey | %i18n:@title%'; document.title = '%i18n:common.name% | %i18n:@title%';
Progress.start(); Progress.start();

View File

@ -1,50 +0,0 @@
<template>
<mk-ui>
<span slot="header">%fa:gamepad%%i18n:@reversi%</span>
<mk-reversi v-if="!fetching" :init-game="game" @gamed="onGamed"/>
</mk-ui>
</template>
<script lang="ts">
import Vue from 'vue';
import Progress from '../../../common/scripts/loading';
export default Vue.extend({
data() {
return {
fetching: false,
game: null
};
},
watch: {
$route: 'fetch'
},
created() {
this.fetch();
},
mounted() {
document.title = '%i18n:common.name% %i18n:@reversi%';
document.documentElement.style.background = '#fff';
},
methods: {
fetch() {
if (this.$route.params.game == null) return;
Progress.start();
this.fetching = true;
(this as any).api('games/reversi/games/show', {
gameId: this.$route.params.game
}).then(game => {
this.game = game;
this.fetching = false;
Progress.done();
});
},
onGamed(game) {
history.pushState(null, null, '/reversi/' + game.id);
}
}
});
</script>

View File

@ -34,7 +34,7 @@ export default Vue.extend({
} }
}, },
mounted() { mounted() {
document.title = `%i18n:@search%: ${this.q} | Misskey`; document.title = `%i18n:@search%: ${this.q} | %i18n:common.name%`;
this.fetch(); this.fetch();
}, },

View File

@ -142,7 +142,7 @@ export default Vue.extend({
}, },
mounted() { mounted() {
document.title = 'Misskey | %i18n:@settings%'; document.title = '%i18n:common.name% | %i18n:@settings%';
}, },
methods: { methods: {

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="azibmfpleajagva420swmu4c3r7ni7iw"> <div class="azibmfpleajagva420swmu4c3r7ni7iw">
<h1>Misskeyで共有</h1> <h1>{{'%i18n:@share-with%'.split("{}")[0] + '%i18n:common.name%' + '%i18n:@share-with%'.split("{}")[1]}}</h1>
<div> <div>
<mk-signin v-if="!$store.getters.isSignedIn"/> <mk-signin v-if="!$store.getters.isSignedIn"/>
<mk-post-form v-else-if="!posted" :initial-text="text" :instant="true" @posted="posted = true"/> <mk-post-form v-else-if="!posted" :initial-text="text" :instant="true" @posted="posted = true"/>

View File

@ -23,7 +23,7 @@ export default Vue.extend({
}; };
}, },
mounted() { mounted() {
document.title = 'Misskey | %i18n:@title%'; document.title = '%i18n:common.name% | %i18n:@title%';
Progress.start(); Progress.start();

View File

@ -106,7 +106,7 @@ export default Vue.extend({
this.fetching = false; this.fetching = false;
Progress.done(); Progress.done();
document.title = Vue.filter('userName')(this.user) + ' | Misskey'; document.title = Vue.filter('userName')(this.user) + ' | %i18n:common.name%';
}); });
} }
} }

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="welcome"> <div class="welcome">
<div> <div>
<img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" alt="Misskey"> <img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" alt="%i18n:common.name%">
<p class="host">{{ host }}</p> <p class="host">{{ host }}</p>
<div class="about"> <div class="about">
<h2>{{ name || 'unidentified' }}</h2> <h2>{{ name || 'unidentified' }}</h2>

View File

@ -102,12 +102,12 @@ export default Vue.extend({
}, },
mounted() { mounted() {
document.title = 'Misskey'; document.title = '%i18n:common.name%';
}, },
methods: { methods: {
hint() { hint() {
alert('ウィジェットを追加/削除したり並べ替えたりできます。ウィジェットを移動するには「三」をドラッグします。ウィジェットを削除するには「x」をタップします。いくつかのウィジェットはタップすることで表示を変更できます。'); alert('%i18n:@widgets-hints%');
}, },
widgetFunc(id) { widgetFunc(id) {

View File

@ -20,18 +20,13 @@ export default (user: ILocalUser, url: string, object: any) => new Promise((reso
method: 'POST', method: 'POST',
path: pathname + search, path: pathname + search,
}, res => { }, res => {
res.on('end', () => { log(`${url} --> ${res.statusCode}`);
log(`${url} --> ${res.statusCode}`);
if (res.statusCode >= 200 && res.statusCode < 300) { if (res.statusCode >= 400) {
resolve(); reject();
} else { } else {
reject(res); resolve();
} }
});
res.on('data', () => {});
res.on('error', reject);
}); });
sign(req, { sign(req, {