diff options
Diffstat (limited to 'src/client')
28 files changed, 673 insertions, 480 deletions
diff --git a/src/client/app/common/scripts/get-face.ts b/src/client/app/common/scripts/get-face.ts new file mode 100644 index 0000000000..79cf7a1be4 --- /dev/null +++ b/src/client/app/common/scripts/get-face.ts @@ -0,0 +1,10 @@ +const faces = [ + '(=^・・^=)', + 'v(\'ω\')v', + '🐡( \'-\' 🐡 )フグパンチ!!!!', + '🖕(´・_・`)🖕', + '(。>﹏<。)', + '(Δ・x・Δ)' +]; + +export default () => faces[Math.floor(Math.random() * faces.length)]; diff --git a/src/client/app/common/scripts/get-kao.ts b/src/client/app/common/scripts/get-kao.ts deleted file mode 100644 index ca83153b96..0000000000 --- a/src/client/app/common/scripts/get-kao.ts +++ /dev/null @@ -1,9 +0,0 @@ -const kaos = [ - '(=^・・^=)', - 'v(\'ω\')v', - '🐡( \'-\' 🐡 )フグパンチ!!!!', - '🖕(´・_・`)🖕', - '(。>﹏<。)' -]; - -export default () => kaos[Math.floor(Math.random() * kaos.length)]; 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 index d4d35f6a86..fa88aeaaf4 100644 --- a/src/client/app/common/views/components/games/reversi/reversi.index.vue +++ b/src/client/app/common/views/components/games/reversi/reversi.index.vue @@ -32,6 +32,7 @@ <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> + <mk-time :time="g.createdAt" /> </a> </section> <section v-if="games.length > 0"> @@ -41,6 +42,7 @@ <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> + <mk-time :time="g.createdAt" /> </a> </section> </div> diff --git a/src/client/app/common/views/components/signup.vue b/src/client/app/common/views/components/signup.vue index 45a183e144..1d33702159 100644 --- a/src/client/app/common/views/components/signup.vue +++ b/src/client/app/common/views/components/signup.vue @@ -1,5 +1,10 @@ <template> <form class="mk-signup" @submit.prevent="onSubmit" :autocomplete="Math.random()"> + <ui-input v-if="meta.disableRegistration" v-model="invitationCode" type="text" :autocomplete="Math.random()" spellcheck="false" required> + <span>%i18n:@invitation-code%</span> + <span slot="prefix">%fa:id-card-alt%</span> + <p slot="text" v-html="'%i18n:@invitation-info%'.replace('{}', meta.maintainer.url)"></p> + </ui-input> <ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @input="onChangeUsername"> <span>%i18n:@username%</span> <span slot="prefix">@</span> @@ -46,11 +51,13 @@ export default Vue.extend({ username: '', password: '', retypedPassword: '', + invitationCode: '', url, recaptchaSitekey, usernameState: null, passwordStrength: '', - passwordRetypeState: null + passwordRetypeState: null, + meta: null } }, computed: { @@ -61,6 +68,11 @@ export default Vue.extend({ this.usernameState != 'max-range'); } }, + created() { + (this as any).os.getMeta().then(meta => { + this.meta = meta; + }); + }, methods: { onChangeUsername() { if (this.username == '') { @@ -110,6 +122,7 @@ export default Vue.extend({ (this as any).api('signup', { username: this.username, password: this.password, + invitationCode: this.invitationCode, 'g-recaptcha-response': recaptchaSitekey != null ? (window as any).grecaptcha.getResponse() : null }).then(() => { (this as any).api('signin', { diff --git a/src/client/app/common/views/components/visibility-chooser.vue b/src/client/app/common/views/components/visibility-chooser.vue index cc9c75095e..4691604e57 100644 --- a/src/client/app/common/views/components/visibility-chooser.vue +++ b/src/client/app/common/views/components/visibility-chooser.vue @@ -44,7 +44,12 @@ import Vue from 'vue'; import * as anime from 'animejs'; export default Vue.extend({ - props: ['source', 'compact', 'v'], + props: ['source', 'compact'], + data() { + return { + v: this.$store.state.device.visibility || 'public' + } + }, mounted() { this.$nextTick(() => { const popover = this.$refs.popover as any; @@ -92,6 +97,7 @@ export default Vue.extend({ }, methods: { choose(visibility) { + this.$store.commit('device/setVisibility', visibility); this.$emit('chosen', visibility); this.$destroy(); }, diff --git a/src/client/app/desktop/views/components/post-form.vue b/src/client/app/desktop/views/components/post-form.vue index ea51144173..bacaea65ee 100644 --- a/src/client/app/desktop/views/components/post-form.vue +++ b/src/client/app/desktop/views/components/post-form.vue @@ -58,7 +58,7 @@ import Vue from 'vue'; import insertTextAtCursor from 'insert-text-at-cursor'; import * as XDraggable from 'vuedraggable'; -import getKao from '../../../common/scripts/get-kao'; +import getFace from '../../../common/scripts/get-face'; import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue'; import parse from '../../../../../mfm/parse'; import { host } from '../../../config'; @@ -99,7 +99,7 @@ export default Vue.extend({ useCw: false, cw: null, geo: null, - visibility: 'public', + visibility: this.$store.state.device.visibility || 'public', visibleUsers: [], autocomplete: null, draghover: false, @@ -326,8 +326,7 @@ export default Vue.extend({ setVisibility() { const w = (this as any).os.new(MkVisibilityChooser, { - source: this.$refs.visibilityButton, - v: this.visibility + source: this.$refs.visibilityButton }); w.$once('chosen', v => { this.visibility = v; @@ -422,7 +421,7 @@ export default Vue.extend({ }, kao() { - this.text += getKao(); + this.text += getFace(); } } }); diff --git a/src/client/app/desktop/views/pages/admin/admin.cpu-memory.vue b/src/client/app/desktop/views/pages/admin/admin.cpu-memory.vue new file mode 100644 index 0000000000..572974e248 --- /dev/null +++ b/src/client/app/desktop/views/pages/admin/admin.cpu-memory.vue @@ -0,0 +1,145 @@ +<template> +<div class="zyknedwtlthezamcjlolyusmipqmjgxz"> + <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`"> + <defs> + <linearGradient :id="cpuGradientId" x1="0" x2="0" y1="1" y2="0"> + <stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop> + <stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop> + </linearGradient> + <mask :id="cpuMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY"> + <polygon + :points="cpuPolygonPoints" + fill="#fff" + fill-opacity="0.5"/> + <polyline + :points="cpuPolylinePoints" + fill="none" + stroke="#fff" + stroke-width="0.3"/> + </mask> + </defs> + <rect + x="0" y="0" + :width="viewBoxX" :height="viewBoxY" + :style="`stroke: none; fill: url(#${ cpuGradientId }); mask: url(#${ cpuMaskId })`"/> + <text x="1" y="5">CPU <tspan>{{ cpuP }}%</tspan></text> + </svg> + <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`"> + <defs> + <linearGradient :id="memGradientId" x1="0" x2="0" y1="1" y2="0"> + <stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop> + <stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop> + </linearGradient> + <mask :id="memMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY"> + <polygon + :points="memPolygonPoints" + fill="#fff" + fill-opacity="0.5"/> + <polyline + :points="memPolylinePoints" + fill="none" + stroke="#fff" + stroke-width="0.3"/> + </mask> + </defs> + <rect + x="0" y="0" + :width="viewBoxX" :height="viewBoxY" + :style="`stroke: none; fill: url(#${ memGradientId }); mask: url(#${ memMaskId })`"/> + <text x="1" y="5">MEM <tspan>{{ memP }}%</tspan></text> + </svg> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as uuid from 'uuid'; + +export default Vue.extend({ + props: ['connection'], + data() { + return { + viewBoxX: 50, + viewBoxY: 20, + stats: [], + cpuGradientId: uuid(), + cpuMaskId: uuid(), + memGradientId: uuid(), + memMaskId: uuid(), + cpuPolylinePoints: '', + memPolylinePoints: '', + cpuPolygonPoints: '', + memPolygonPoints: '', + cpuP: '', + memP: '' + }; + }, + mounted() { + this.connection.on('stats', this.onStats); + this.connection.on('statsLog', this.onStatsLog); + this.connection.send({ + type: 'requestLog', + id: Math.random().toString() + }); + }, + beforeDestroy() { + this.connection.off('stats', this.onStats); + this.connection.off('statsLog', this.onStatsLog); + }, + methods: { + onStats(stats) { + this.stats.push(stats); + if (this.stats.length > 50) this.stats.shift(); + + const cpuPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - s.cpu_usage) * this.viewBoxY]); + const memPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - (s.mem.used / s.mem.total)) * this.viewBoxY]); + this.cpuPolylinePoints = cpuPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' '); + this.memPolylinePoints = memPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' '); + + this.cpuPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.cpuPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`; + this.memPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.memPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`; + + this.cpuP = (stats.cpu_usage * 100).toFixed(0); + this.memP = (stats.mem.used / stats.mem.total * 100).toFixed(0); + }, + onStatsLog(statsLog) { + statsLog.forEach(stats => this.onStats(stats)); + } + } +}); +</script> + +<style lang="stylus" scoped> +root(isDark) + margin-bottom 16px + + > svg + display block + width 50% + float left + + &:first-child + padding-right 5px + + &:last-child + padding-left 5px + + > text + font-size 2px + fill isDark ? rgba(#fff, 0.55) : rgba(#000, 0.55) + + > tspan + opacity 0.5 + + &:after + content "" + display block + clear both + +.zyknedwtlthezamcjlolyusmipqmjgxz[data-darkmode] + root(true) + +.zyknedwtlthezamcjlolyusmipqmjgxz:not([data-darkmode]) + root(false) + +</style> diff --git a/src/client/app/desktop/views/pages/admin/admin.dashboard.vue b/src/client/app/desktop/views/pages/admin/admin.dashboard.vue index b10e829965..e68d3a749e 100644 --- a/src/client/app/desktop/views/pages/admin/admin.dashboard.vue +++ b/src/client/app/desktop/views/pages/admin/admin.dashboard.vue @@ -1,37 +1,80 @@ <template> -<div> - <h1>%i18n:@dashboard%</h1> - <div v-if="stats"> - <p><b>%i18n:@all-users%</b>: <span>{{ stats.usersCount | number }}</span></p> - <p><b>%i18n:@original-users%</b>: <span>{{ stats.originalUsersCount | number }}</span></p> - <p><b>%i18n:@all-notes%</b>: <span>{{ stats.notesCount | number }}</span></p> - <p><b>%i18n:@original-notes%</b>: <span>{{ stats.originalNotesCount | number }}</span></p> +<div class="obdskegsannmntldydackcpzezagxqfy card"> + <header>%i18n:@dashboard%</header> + <div v-if="stats" class="stats"> + <div><b>%fa:user% {{ stats.originalUsersCount | number }}</b><span>%i18n:@original-users%</span></div> + <div><span>%fa:user% {{ stats.usersCount | number }}</span><span>%i18n:@all-users%</span></div> + <div><b>%fa:pen% {{ stats.originalNotesCount | number }}</b><span>%i18n:@original-notes%</span></div> + <div><span>%fa:pen% {{ stats.notesCount | number }}</span><span>%i18n:@all-notes%</span></div> + </div> + <div class="cpu-memory"> + <x-cpu-memory :connection="connection"/> + </div> + <div> + <button class="ui" @click="invite">%i18n:@invite%</button> + <p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p> </div> </div> </template> <script lang="ts"> import Vue from "vue"; +import XCpuMemory from "./admin.cpu-memory.vue"; export default Vue.extend({ + components: { + XCpuMemory + }, data() { return { - stats: null + stats: null, + inviteCode: null, + connection: null, + connectionId: null }; }, created() { + this.connection = (this as any).os.streams.serverStatsStream.getConnection(); + this.connectionId = (this as any).os.streams.serverStatsStream.use(); + (this as any).api('stats').then(stats => { this.stats = stats; }); + }, + beforeDestroy() { + (this as any).os.streams.serverStatsStream.dispose(this.connectionId); + }, + methods: { + invite() { + (this as any).api('admin/invite').then(x => { + this.inviteCode = x.code; + }); + } } }); </script> <style lang="stylus" scoped> -h1 - margin 0 0 1em 0 - padding 0 0 8px 0 - font-size 1em - color #555 - border-bottom solid 1px #eee +@import '~const.styl' + +.obdskegsannmntldydackcpzezagxqfy + > .stats + display flex + justify-content center + margin-bottom 16px + padding 16px + border solid 1px #eee + border-radius 8px + + > div + flex 1 + text-align center + + > *:first-child + display block + color $theme-color + + > *:last-child + font-size 70% + </style> diff --git a/src/client/app/desktop/views/pages/admin/admin.drive-chart.chart.vue b/src/client/app/desktop/views/pages/admin/admin.drive-chart.chart.vue new file mode 100644 index 0000000000..3c537d8d6d --- /dev/null +++ b/src/client/app/desktop/views/pages/admin/admin.drive-chart.chart.vue @@ -0,0 +1,51 @@ +<template> +<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`"> + <polyline + :points="points" + fill="none" + stroke-width="1" + stroke="#555"/> +</svg> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: { + chart: { + required: true + }, + type: { + type: String, + required: true + } + }, + data() { + return { + viewBoxX: 365, + viewBoxY: 70, + points: null + }; + }, + created() { + const peak = Math.max.apply(null, this.chart.map(d => this.type == 'local' ? d.drive.local.totalSize : d.drive.remote.totalSize)); + + if (peak != 0) { + const data = this.chart.slice().reverse().map(x => ({ + size: this.type == 'local' ? x.drive.local.totalSize : x.drive.remote.totalSize + })); + + this.points = data.map((d, i) => `${i},${(1 - (d.size / peak)) * this.viewBoxY}`).join(' '); + } + } +}); +</script> + +<style lang="stylus" scoped> +svg + display block + padding 10px + width 100% + +</style> diff --git a/src/client/app/desktop/views/pages/admin/admin.drive-chart.vue b/src/client/app/desktop/views/pages/admin/admin.drive-chart.vue new file mode 100644 index 0000000000..4f94fd2372 --- /dev/null +++ b/src/client/app/desktop/views/pages/admin/admin.drive-chart.vue @@ -0,0 +1,34 @@ +<template> +<div class="card"> + <header>%i18n:@title%</header> + <div class="card"> + <header>%i18n:@local%</header> + <x-chart v-if="chart" :chart="chart" type="local"/> + </div> + <div class="card"> + <header>%i18n:@remote%</header> + <x-chart v-if="chart" :chart="chart" type="remote"/> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from "vue"; +import XChart from "./admin.drive-chart.chart.vue"; + +export default Vue.extend({ + components: { + XChart + }, + props: { + chart: { + required: true + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +</style> diff --git a/src/client/app/desktop/views/pages/admin/admin.notes-chart.chart.vue b/src/client/app/desktop/views/pages/admin/admin.notes-chart.chart.vue new file mode 100644 index 0000000000..83c61c1313 --- /dev/null +++ b/src/client/app/desktop/views/pages/admin/admin.notes-chart.chart.vue @@ -0,0 +1,76 @@ +<template> +<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`"> + <polyline + :points="pointsNote" + fill="none" + stroke-width="1" + stroke="#41ddde"/> + <polyline + :points="pointsReply" + fill="none" + stroke-width="1" + stroke="#f7796c"/> + <polyline + :points="pointsRenote" + fill="none" + stroke-width="1" + stroke="#a1de41"/> + <polyline + :points="pointsTotal" + fill="none" + stroke-width="1" + stroke="#555" + stroke-dasharray="2 2"/> +</svg> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: { + chart: { + required: true + }, + type: { + type: String, + required: true + } + }, + data() { + return { + viewBoxX: 365, + viewBoxY: 70, + pointsNote: null, + pointsReply: null, + pointsRenote: null, + pointsTotal: null + }; + }, + created() { + const peak = Math.max.apply(null, this.chart.map(d => this.type == 'local' ? d.notes.local.diff : d.notes.remote.diff)); + + if (peak != 0) { + const data = this.chart.slice().reverse().map(x => ({ + normal: this.type == 'local' ? x.notes.local.diffs.normal : x.notes.remote.diffs.normal, + reply: this.type == 'local' ? x.notes.local.diffs.reply : x.notes.remote.diffs.reply, + renote: this.type == 'local' ? x.notes.local.diffs.renote : x.notes.remote.diffs.renote, + total: this.type == 'local' ? x.notes.local.diff : x.notes.remote.diff + })); + + this.pointsNote = data.map((d, i) => `${i},${(1 - (d.normal / peak)) * this.viewBoxY}`).join(' '); + this.pointsReply = data.map((d, i) => `${i},${(1 - (d.reply / peak)) * this.viewBoxY}`).join(' '); + this.pointsRenote = data.map((d, i) => `${i},${(1 - (d.renote / peak)) * this.viewBoxY}`).join(' '); + this.pointsTotal = data.map((d, i) => `${i},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' '); + } + } +}); +</script> + +<style lang="stylus" scoped> +svg + display block + padding 10px + width 100% + +</style> diff --git a/src/client/app/desktop/views/pages/admin/admin.notes-chart.vue b/src/client/app/desktop/views/pages/admin/admin.notes-chart.vue new file mode 100644 index 0000000000..e4d396d9c6 --- /dev/null +++ b/src/client/app/desktop/views/pages/admin/admin.notes-chart.vue @@ -0,0 +1,34 @@ +<template> +<div class="card"> + <header>%i18n:@title%</header> + <div class="card"> + <header>%i18n:@local%</header> + <x-chart v-if="chart" :chart="chart" type="local"/> + </div> + <div class="card"> + <header>%i18n:@remote%</header> + <x-chart v-if="chart" :chart="chart" type="remote"/> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from "vue"; +import XChart from "./admin.notes-chart.chart.vue"; + +export default Vue.extend({ + components: { + XChart + }, + props: { + chart: { + required: true + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +</style> diff --git a/src/client/app/desktop/views/pages/admin/admin.suspend-user.vue b/src/client/app/desktop/views/pages/admin/admin.suspend-user.vue index 6eb82f0a51..59932f4be7 100644 --- a/src/client/app/desktop/views/pages/admin/admin.suspend-user.vue +++ b/src/client/app/desktop/views/pages/admin/admin.suspend-user.vue @@ -1,5 +1,5 @@ <template> -<div> +<div class="card"> <header>%i18n:@suspend-user%</header> <input v-model="username" type="text" class="ui"/> <button class="ui" @click="suspendUser" :disabled="suspending">%i18n:@suspend%</button> diff --git a/src/client/app/desktop/views/pages/admin/admin.unsuspend-user.vue b/src/client/app/desktop/views/pages/admin/admin.unsuspend-user.vue index 8c6f63ce88..a75c0bd64e 100644 --- a/src/client/app/desktop/views/pages/admin/admin.unsuspend-user.vue +++ b/src/client/app/desktop/views/pages/admin/admin.unsuspend-user.vue @@ -1,5 +1,5 @@ <template> -<div> +<div class="card"> <header>%i18n:@unsuspend-user%</header> <input v-model="username" type="text" class="ui"/> <button class="ui" @click="unsuspendUser" :disabled="unsuspending">%i18n:@unsuspend%</button> diff --git a/src/client/app/desktop/views/pages/admin/admin.unverify-user.vue b/src/client/app/desktop/views/pages/admin/admin.unverify-user.vue new file mode 100644 index 0000000000..72962870d9 --- /dev/null +++ b/src/client/app/desktop/views/pages/admin/admin.unverify-user.vue @@ -0,0 +1,51 @@ +<template> +<div class="card"> + <header>%i18n:@unverify-user%</header> + <input v-model="username" type="text" class="ui"/> + <button class="ui" @click="unverifyUser" :disabled="unverifying">%i18n:@unverify%</button> +</div> +</template> + +<script lang="ts"> +import Vue from "vue"; +import parseAcct from "../../../../../../misc/acct/parse"; + +export default Vue.extend({ + data() { + return { + username: null, + unverifying: false + }; + }, + methods: { + async unverifyUser() { + this.unverifying = true; + + const user = await (this as any).os.api( + "users/show", + parseAcct(this.username) + ); + + await (this as any).os.api("admin/unverify-user", { + userId: user.id + }); + + this.unverifying = false; + + (this as any).os.apis.dialog({ text: "%i18n:@unverified%" }); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +header + margin 10px 0 + + +button + margin 16px 0 + +</style> diff --git a/src/client/app/desktop/views/pages/admin/admin.users-chart.chart.vue b/src/client/app/desktop/views/pages/admin/admin.users-chart.chart.vue new file mode 100644 index 0000000000..c2ab4a78e3 --- /dev/null +++ b/src/client/app/desktop/views/pages/admin/admin.users-chart.chart.vue @@ -0,0 +1,51 @@ +<template> +<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`"> + <polyline + :points="points" + fill="none" + stroke-width="1" + stroke="#555"/> +</svg> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: { + chart: { + required: true + }, + type: { + type: String, + required: true + } + }, + data() { + return { + viewBoxX: 365, + viewBoxY: 70, + points: null + }; + }, + created() { + const peak = Math.max.apply(null, this.chart.map(d => this.type == 'local' ? d.users.local.diff : d.users.remote.diff)); + + if (peak != 0) { + const data = this.chart.slice().reverse().map(x => ({ + count: this.type == 'local' ? x.users.local.diff : x.users.remote.diff + })); + + this.points = data.map((d, i) => `${i},${(1 - (d.count / peak)) * this.viewBoxY}`).join(' '); + } + } +}); +</script> + +<style lang="stylus" scoped> +svg + display block + padding 10px + width 100% + +</style> diff --git a/src/client/app/desktop/views/pages/admin/admin.users-chart.vue b/src/client/app/desktop/views/pages/admin/admin.users-chart.vue new file mode 100644 index 0000000000..e620012702 --- /dev/null +++ b/src/client/app/desktop/views/pages/admin/admin.users-chart.vue @@ -0,0 +1,34 @@ +<template> +<div class="card"> + <header>%i18n:@title%</header> + <div class="card"> + <header>%i18n:@local%</header> + <x-chart v-if="chart" :chart="chart" type="local"/> + </div> + <div class="card"> + <header>%i18n:@remote%</header> + <x-chart v-if="chart" :chart="chart" type="remote"/> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from "vue"; +import XChart from "./admin.users-chart.chart.vue"; + +export default Vue.extend({ + components: { + XChart + }, + props: { + chart: { + required: true + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +</style> diff --git a/src/client/app/desktop/views/pages/admin/admin.verify-user.vue b/src/client/app/desktop/views/pages/admin/admin.verify-user.vue new file mode 100644 index 0000000000..3902d4bddd --- /dev/null +++ b/src/client/app/desktop/views/pages/admin/admin.verify-user.vue @@ -0,0 +1,51 @@ +<template> +<div class="card"> + <header>%i18n:@verify-user%</header> + <input v-model="username" type="text" class="ui"/> + <button class="ui" @click="verifyUser" :disabled="verifying">%i18n:@verify%</button> +</div> +</template> + +<script lang="ts"> +import Vue from "vue"; +import parseAcct from "../../../../../../misc/acct/parse"; + +export default Vue.extend({ + data() { + return { + username: null, + verifying: false + }; + }, + methods: { + async verifyUser() { + this.verifying = true; + + const user = await (this as any).os.api( + "users/show", + parseAcct(this.username) + ); + + await (this as any).os.api("admin/verify-user", { + userId: user.id + }); + + this.verifying = false; + + (this as any).os.apis.dialog({ text: "%i18n:@verified%" }); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +header + margin 10px 0 + + +button + margin 16px 0 + +</style> diff --git a/src/client/app/desktop/views/pages/admin/admin.vue b/src/client/app/desktop/views/pages/admin/admin.vue index b581bea465..cbb1890cc3 100644 --- a/src/client/app/desktop/views/pages/admin/admin.vue +++ b/src/client/app/desktop/views/pages/admin/admin.vue @@ -9,12 +9,17 @@ </ul> </nav> <main> - <div v-if="page == 'dashboard'"> + <div v-show="page == 'dashboard'"> <x-dashboard/> + <x-users-chart :chart="chart"/> + <x-notes-chart :chart="chart"/> + <x-drive-chart :chart="chart"/> </div> <div v-if="page == 'users'"> <x-suspend-user/> <x-unsuspend-user/> + <x-verify-user/> + <x-unverify-user/> </div> <div v-if="page == 'drive'"></div> <div v-if="page == 'update'"></div> @@ -27,18 +32,34 @@ import Vue from "vue"; import XDashboard from "./admin.dashboard.vue"; import XSuspendUser from "./admin.suspend-user.vue"; import XUnsuspendUser from "./admin.unsuspend-user.vue"; +import XVerifyUser from "./admin.verify-user.vue"; +import XUnverifyUser from "./admin.unverify-user.vue"; +import XUsersChart from "./admin.users-chart.vue"; +import XNotesChart from "./admin.notes-chart.vue"; +import XDriveChart from "./admin.drive-chart.vue"; export default Vue.extend({ components: { XDashboard, XSuspendUser, - XUnsuspendUser + XUnsuspendUser, + XVerifyUser, + XUnverifyUser, + XUsersChart, + XNotesChart, + XDriveChart }, data() { return { - page: 'dashboard' + page: 'dashboard', + chart: null }; }, + created() { + (this as any).api('admin/chart').then(chart => { + this.chart = chart; + }); + }, methods: { nav(page: string) { this.page = page; @@ -47,7 +68,7 @@ export default Vue.extend({ }); </script> -<style lang="stylus" scoped> +<style lang="stylus"> @import '~const.styl' .mk-admin @@ -90,13 +111,23 @@ export default Vue.extend({ width 100% padding 16px 32px -header - margin 10px 0 + > div + > div + max-width 800px + +.card + padding 32px + background #fff + box-shadow 0 2px 8px rgba(#000, 0.1) + &:not(:last-child) + margin-bottom 16px -button - margin 16px 0 - position absolute - right 0 + > header + margin 0 0 1em 0 + padding 0 0 8px 0 + font-size 1em + color #555 + border-bottom solid 1px #eee </style> diff --git a/src/client/app/dev/views/new-app.vue b/src/client/app/dev/views/new-app.vue index bf19e6da57..87b35db259 100644 --- a/src/client/app/dev/views/new-app.vue +++ b/src/client/app/dev/views/new-app.vue @@ -95,7 +95,7 @@ export default Vue.extend({ callbackUrl: this.cb, permission: this.permission }).then(() => { - location.href = '/apps'; + location.href = '/dev/apps'; }).catch(() => { alert('アプリの作成に失敗しました。再度お試しください。'); }); diff --git a/src/client/app/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue index 702bc4c9e1..a74df67c0a 100644 --- a/src/client/app/mobile/views/components/post-form.vue +++ b/src/client/app/mobile/views/components/post-form.vue @@ -56,7 +56,7 @@ import Vue from 'vue'; import insertTextAtCursor from 'insert-text-at-cursor'; import * as XDraggable from 'vuedraggable'; import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue'; -import getKao from '../../../common/scripts/get-kao'; +import getFace from '../../../common/scripts/get-face'; import parse from '../../../../../mfm/parse'; import { host } from '../../../config'; @@ -94,7 +94,7 @@ export default Vue.extend({ files: [], poll: false, geo: null, - visibility: 'public', + visibility: this.$store.state.device.visibility || 'public', visibleUsers: [], useCw: false, cw: null, @@ -240,8 +240,7 @@ export default Vue.extend({ setVisibility() { const w = (this as any).os.new(MkVisibilityChooser, { source: this.$refs.visibilityButton, - compact: true, - v: this.visibility + compact: true }); w.$once('chosen', v => { this.visibility = v; @@ -314,7 +313,7 @@ export default Vue.extend({ }, kao() { - this.text += getKao(); + this.text += getFace(); } } }); diff --git a/src/client/app/stats/style.styl b/src/client/app/stats/style.styl deleted file mode 100644 index 5ae230ea56..0000000000 --- a/src/client/app/stats/style.styl +++ /dev/null @@ -1,10 +0,0 @@ -@import "../app" -@import "../reset" - -html - color #456267 - background #fff - -body - margin 0 - padding 0 diff --git a/src/client/app/stats/tags/index.tag b/src/client/app/stats/tags/index.tag deleted file mode 100644 index f8944c0832..0000000000 --- a/src/client/app/stats/tags/index.tag +++ /dev/null @@ -1,209 +0,0 @@ -<mk-index> - <h1>Misskey<i>Statistics</i></h1> - <main v-if="!initializing"> - <mk-users stats={ stats }/> - <mk-notes stats={ stats }/> - </main> - <footer><a href={ _URL_ }>{ _HOST_ }</a></footer> - <style lang="stylus" scoped> - :scope - display block - margin 0 auto - padding 0 16px - max-width 700px - - > h1 - margin 0 - padding 24px 0 0 0 - font-size 24px - font-weight normal - - > i - font-style normal - color #f43b16 - - > main - > * - margin 24px 0 - padding-top 24px - border-top solid 1px #eee - - > h2 - margin 0 0 12px 0 - font-size 18px - font-weight normal - - > footer - margin 24px 0 - text-align center - - > a - color #546567 - </style> - <script lang="typescript"> - this.mixin('api'); - - this.initializing = true; - - this.on('mount', () => { - this.$root.$data.os.api('stats').then(stats => { - this.update({ - initializing: false, - stats - }); - }); - }); - </script> -</mk-index> - -<mk-notes> - <h2>%i18n:stats.notes-count% <b>{ stats.notesCount }</b></h2> - <mk-notes-chart v-if="!initializing" data={ data }/> - <style lang="stylus" scoped> - :scope - display block - </style> - <script lang="typescript"> - this.mixin('api'); - - this.initializing = true; - this.stats = this.opts.stats; - - this.on('mount', () => { - this.$root.$data.os.api('aggregation/notes', { - limit: 365 - }).then(data => { - this.update({ - initializing: false, - data - }); - }); - }); - </script> -</mk-notes> - -<mk-users> - <h2>%i18n:stats.users-count% <b>{ stats.usersCount }</b></h2> - <mk-users-chart v-if="!initializing" data={ data }/> - <style lang="stylus" scoped> - :scope - display block - </style> - <script lang="typescript"> - this.mixin('api'); - - this.initializing = true; - this.stats = this.opts.stats; - - this.on('mount', () => { - this.$root.$data.os.api('aggregation/users', { - limit: 365 - }).then(data => { - this.update({ - initializing: false, - data - }); - }); - }); - </script> -</mk-users> - -<mk-notes-chart> - <svg riot-viewBox="0 0 { viewBoxX } { viewBoxY }" preserveAspectRatio="none"> - <title>Black ... Total<br/>Blue ... Notes<br/>Red ... Replies<br/>Green ... Renotes</title> - <polyline - riot-points={ pointsNote } - fill="none" - stroke-width="1" - stroke="#41ddde"/> - <polyline - riot-points={ pointsReply } - fill="none" - stroke-width="1" - stroke="#f7796c"/> - <polyline - riot-points={ pointsRenote } - fill="none" - stroke-width="1" - stroke="#a1de41"/> - <polyline - riot-points={ pointsTotal } - fill="none" - stroke-width="1" - stroke="#555" - stroke-dasharray="2 2"/> - </svg> - <style lang="stylus" scoped> - :scope - display block - - > svg - display block - padding 1px - width 100% - </style> - <script lang="typescript"> - this.viewBoxX = 365; - this.viewBoxY = 80; - - this.data = this.opts.data.reverse(); - this.data.forEach(d => d.total = d.notes + d.replies + d.renotes); - const peak = Math.max.apply(null, this.data.map(d => d.total)); - - this.on('mount', () => { - this.render(); - }); - - this.render = () => { - this.update({ - pointsNote: this.data.map((d, i) => `${i},${(1 - (d.notes / peak)) * this.viewBoxY}`).join(' '), - pointsReply: this.data.map((d, i) => `${i},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' '), - pointsRenote: this.data.map((d, i) => `${i},${(1 - (d.renotes / peak)) * this.viewBoxY}`).join(' '), - pointsTotal: this.data.map((d, i) => `${i},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ') - }); - }; - </script> -</mk-notes-chart> - -<mk-users-chart> - <svg riot-viewBox="0 0 { viewBoxX } { viewBoxY }" preserveAspectRatio="none"> - <polyline - riot-points={ createdPoints } - fill="none" - stroke-width="1" - stroke="#1cde84"/> - <polyline - riot-points={ totalPoints } - fill="none" - stroke-width="1" - stroke="#555"/> - </svg> - <style lang="stylus" scoped> - :scope - display block - - > svg - display block - padding 1px - width 100% - </style> - <script lang="typescript"> - this.viewBoxX = 365; - this.viewBoxY = 80; - - this.data = this.opts.data.reverse(); - const totalPeak = Math.max.apply(null, this.data.map(d => d.total)); - const createdPeak = Math.max.apply(null, this.data.map(d => d.created)); - - this.on('mount', () => { - this.render(); - }); - - this.render = () => { - this.update({ - totalPoints: this.data.map((d, i) => `${i},${(1 - (d.total / totalPeak)) * this.viewBoxY}`).join(' '), - createdPoints: this.data.map((d, i) => `${i},${(1 - (d.created / createdPeak)) * this.viewBoxY}`).join(' ') - }); - }; - </script> -</mk-users-chart> diff --git a/src/client/app/stats/tags/index.ts b/src/client/app/stats/tags/index.ts deleted file mode 100644 index f41151949f..0000000000 --- a/src/client/app/stats/tags/index.ts +++ /dev/null @@ -1 +0,0 @@ -require('./index.tag'); diff --git a/src/client/app/status/style.styl b/src/client/app/status/style.styl deleted file mode 100644 index 5ae230ea56..0000000000 --- a/src/client/app/status/style.styl +++ /dev/null @@ -1,10 +0,0 @@ -@import "../app" -@import "../reset" - -html - color #456267 - background #fff - -body - margin 0 - padding 0 diff --git a/src/client/app/status/tags/index.tag b/src/client/app/status/tags/index.tag deleted file mode 100644 index 899467097a..0000000000 --- a/src/client/app/status/tags/index.tag +++ /dev/null @@ -1,201 +0,0 @@ -<mk-index> - <h1>Misskey<i>Status</i></h1> - <p>%fa:info-circle%%i18n:status.all-systems-maybe-operational%</p> - <main> - <mk-cpu-usage connection={ connection }/> - <mk-mem-usage connection={ connection }/> - </main> - <footer><a href={ _URL_ }>{ _HOST_ }</a></footer> - <style lang="stylus" scoped> - :scope - display block - margin 0 auto - padding 0 16px - max-width 700px - - > h1 - margin 0 - padding 24px 0 16px 0 - font-size 24px - font-weight normal - - > [data-fa] - font-style normal - color #f43b16 - - > p - display block - margin 0 - padding 12px 16px - background #eaf4ef - //border solid 1px #99ccb2 - border-radius 4px - - > [data-fa] - margin-right 5px - - > main - > * - margin 24px 0 - - > h2 - margin 0 0 12px 0 - font-size 18px - font-weight normal - - > footer - margin 24px 0 - text-align center - - > a - color #546567 - </style> - <script lang="typescript"> - import Connection from '../../common/scripts/streaming/server-stream'; - - this.mixin('api'); - - this.initializing = true; - this.connection = new Connection(); - - this.on('mount', () => { - this.$root.$data.os.api('meta').then(meta => { - this.update({ - initializing: false, - meta - }); - }); - }); - - this.on('unmount', () => { - this.connection.close(); - }); - - </script> -</mk-index> - -<mk-cpu-usage> - <h2>CPU <b>{ percentage }%</b></h2> - <mk-line-chart ref="chart"/> - <style lang="stylus" scoped> - :scope - display block - </style> - <script lang="typescript"> - this.connection = this.opts.connection; - - this.on('mount', () => { - this.connection.on('stats', this.onStats); - }); - - this.on('unmount', () => { - this.connection.off('stats', this.onStats); - }); - - this.onStats = stats => { - this.$refs.chart.addData(1 - stats.cpu_usage); - - const percentage = (stats.cpu_usage * 100).toFixed(0); - - this.update({ - percentage - }); - }; - </script> -</mk-cpu-usage> - -<mk-mem-usage> - <h2>MEM <b>{ percentage }%</b></h2> - <mk-line-chart ref="chart"/> - <style lang="stylus" scoped> - :scope - display block - </style> - <script lang="typescript"> - this.connection = this.opts.connection; - - this.on('mount', () => { - this.connection.on('stats', this.onStats); - }); - - this.on('unmount', () => { - this.connection.off('stats', this.onStats); - }); - - this.onStats = stats => { - stats.mem.used = stats.mem.total - stats.mem.free; - this.$refs.chart.addData(1 - (stats.mem.used / stats.mem.total)); - - const percentage = (stats.mem.used / stats.mem.total * 100).toFixed(0); - - this.update({ - percentage - }); - }; - </script> -</mk-mem-usage> - -<mk-line-chart> - <svg riot-viewBox="0 0 { viewBoxX } { viewBoxY }" preserveAspectRatio="none"> - <defs> - <linearGradient id={ gradientId } x1="0" x2="0" y1="1" y2="0"> - <stop offset="0%" stop-color="rgba(244, 59, 22, 0)"></stop> - <stop offset="100%" stop-color="#f43b16"></stop> - </linearGradient> - <mask id={ maskId } x="0" y="0" riot-width={ viewBoxX } riot-height={ viewBoxY }> - <polygon - riot-points={ polygonPoints } - fill="#fff" - fill-opacity="0.5"/> - </mask> - </defs> - <line x1="0" y1="0" riot-x2={ viewBoxX } y2="0" stroke="rgba(255, 255, 255, 0.1)" stroke-width="0.25" stroke-dasharray="1"/> - <line x1="0" y1="25%" riot-x2={ viewBoxX } y2="25%" stroke="rgba(255, 255, 255, 0.1)" stroke-width="0.25" stroke-dasharray="1"/> - <line x1="0" y1="50%" riot-x2={ viewBoxX } y2="50%" stroke="rgba(255, 255, 255, 0.1)" stroke-width="0.25" stroke-dasharray="1"/> - <line x1="0" y1="75%" riot-x2={ viewBoxX } y2="75%" stroke="rgba(255, 255, 255, 0.1)" stroke-width="0.25" stroke-dasharray="1"/> - <line x1="0" y1="100%" riot-x2={ viewBoxX } y2="100%" stroke="rgba(255, 255, 255, 0.1)" stroke-width="0.25" stroke-dasharray="1"/> - <rect - x="-1" y="-1" - riot-width={ viewBoxX + 2 } riot-height={ viewBoxY + 2 } - style="stroke: none; fill: url(#{ gradientId }); mask: url(#{ maskId })"/> - <polyline - riot-points={ polylinePoints } - fill="none" - stroke="#f43b16" - stroke-width="0.5"/> - </svg> - <style lang="stylus" scoped> - :scope - display block - padding 16px - border-radius 8px - background #1c2531 - - > svg - display block - padding 1px - width 100% - </style> - <script lang="typescript"> - import uuid from 'uuid'; - - this.viewBoxX = 100; - this.viewBoxY = 30; - this.data = []; - this.gradientId = uuid(); - this.maskId = uuid(); - - this.addData = data => { - this.data.push(data); - if (this.data.length > 100) this.data.shift(); - - const polylinePoints = this.data.map((d, i) => `${this.viewBoxX - ((this.data.length - 1) - i)},${d * this.viewBoxY}`).join(' '); - const polygonPoints = `${this.viewBoxX - (this.data.length - 1)},${ this.viewBoxY } ${ polylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`; - - this.update({ - polylinePoints, - polygonPoints - }); - }; - </script> -</mk-line-chart> diff --git a/src/client/app/status/tags/index.ts b/src/client/app/status/tags/index.ts deleted file mode 100644 index f41151949f..0000000000 --- a/src/client/app/status/tags/index.ts +++ /dev/null @@ -1 +0,0 @@ -require('./index.tag'); diff --git a/src/client/app/store.ts b/src/client/app/store.ts index f85253a281..7e2cc3976b 100644 --- a/src/client/app/store.ts +++ b/src/client/app/store.ts @@ -110,6 +110,10 @@ export default (os: MiOS) => new Vuex.Store({ src: x.src, arg: x.arg }; + }, + + setVisibility(state, visibility) { + state.visibility = visibility; } } }, |