mirror of
https://iceshrimp.dev/limepotato/jormungandr-bite.git
synced 2024-11-30 05:47:46 -07:00
Merge pull request #1713 from syuilo/without-vue-material
Without vue material
This commit is contained in:
commit
8c1ba7fdca
23 changed files with 1383 additions and 519 deletions
|
@ -211,7 +211,6 @@
|
||||||
"vue-js-modal": "1.3.13",
|
"vue-js-modal": "1.3.13",
|
||||||
"vue-json-tree-view": "2.1.4",
|
"vue-json-tree-view": "2.1.4",
|
||||||
"vue-loader": "15.2.1",
|
"vue-loader": "15.2.1",
|
||||||
"vue-material": "^1.0.0-beta-10.2",
|
|
||||||
"vue-router": "3.0.1",
|
"vue-router": "3.0.1",
|
||||||
"vue-template-compiler": "2.5.16",
|
"vue-template-compiler": "2.5.16",
|
||||||
"vuedraggable": "2.16.0",
|
"vuedraggable": "2.16.0",
|
||||||
|
|
|
@ -7,11 +7,6 @@ html
|
||||||
cursor progress !important
|
cursor progress !important
|
||||||
|
|
||||||
body
|
body
|
||||||
// for md
|
|
||||||
font-size 16px !important
|
|
||||||
line-height initial !important
|
|
||||||
letter-spacing initial !important
|
|
||||||
|
|
||||||
overflow-wrap break-word
|
overflow-wrap break-word
|
||||||
|
|
||||||
#error
|
#error
|
||||||
|
|
|
@ -29,6 +29,14 @@ import fileTypeIcon from './file-type-icon.vue';
|
||||||
import Switch from './switch.vue';
|
import Switch from './switch.vue';
|
||||||
import Othello from './othello.vue';
|
import Othello from './othello.vue';
|
||||||
import welcomeTimeline from './welcome-timeline.vue';
|
import welcomeTimeline from './welcome-timeline.vue';
|
||||||
|
import uiInput from './ui/input.vue';
|
||||||
|
import uiButton from './ui/button.vue';
|
||||||
|
import uiCard from './ui/card.vue';
|
||||||
|
import uiForm from './ui/form.vue';
|
||||||
|
import uiTextarea from './ui/textarea.vue';
|
||||||
|
import uiSwitch from './ui/switch.vue';
|
||||||
|
import uiRadio from './ui/radio.vue';
|
||||||
|
import uiSelect from './ui/select.vue';
|
||||||
|
|
||||||
Vue.component('mk-analog-clock', analogClock);
|
Vue.component('mk-analog-clock', analogClock);
|
||||||
Vue.component('mk-menu', menu);
|
Vue.component('mk-menu', menu);
|
||||||
|
@ -59,3 +67,11 @@ Vue.component('mk-file-type-icon', fileTypeIcon);
|
||||||
Vue.component('mk-switch', Switch);
|
Vue.component('mk-switch', Switch);
|
||||||
Vue.component('mk-othello', Othello);
|
Vue.component('mk-othello', Othello);
|
||||||
Vue.component('mk-welcome-timeline', welcomeTimeline);
|
Vue.component('mk-welcome-timeline', welcomeTimeline);
|
||||||
|
Vue.component('ui-input', uiInput);
|
||||||
|
Vue.component('ui-button', uiButton);
|
||||||
|
Vue.component('ui-card', uiCard);
|
||||||
|
Vue.component('ui-form', uiForm);
|
||||||
|
Vue.component('ui-textarea', uiTextarea);
|
||||||
|
Vue.component('ui-switch', uiSwitch);
|
||||||
|
Vue.component('ui-radio', uiRadio);
|
||||||
|
Vue.component('ui-select', uiSelect);
|
||||||
|
|
|
@ -1,60 +1,58 @@
|
||||||
<template>
|
<template>
|
||||||
<form class="mk-signup" @submit.prevent="onSubmit" autocomplete="off">
|
<form class="mk-signup" @submit.prevent="onSubmit" :autocomplete="Math.random()">
|
||||||
<label class="username">
|
<ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" required @input="onChangeUsername">
|
||||||
<p class="caption">%fa:at%%i18n:@username%</p>
|
<span>%i18n:@username%</span>
|
||||||
<input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" placeholder="a~z、A~Z、0~9、-" autocomplete="off" required @input="onChangeUsername"/>
|
<span slot="prefix">@</span>
|
||||||
<p class="profile-page-url-preview" v-if="shouldShowProfileUrl">{{ `${url}/@${username}` }}</p>
|
<span slot="suffix">@{{ host }}</span>
|
||||||
<p class="info" v-if="usernameState == 'wait'" style="color:#999">%fa:spinner .pulse .fw%%i18n:@checking%</p>
|
<p slot="text" v-if="usernameState == 'wait'" style="color:#999">%fa:spinner .pulse .fw% %i18n:@checking%</p>
|
||||||
<p class="info" v-if="usernameState == 'ok'" style="color:#3CB7B5">%fa:check .fw%%i18n:@available%</p>
|
<p slot="text" v-if="usernameState == 'ok'" style="color:#3CB7B5">%fa:check .fw% %i18n:@available%</p>
|
||||||
<p class="info" v-if="usernameState == 'unavailable'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@unavailable%</p>
|
<p slot="text" v-if="usernameState == 'unavailable'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@unavailable%</p>
|
||||||
<p class="info" v-if="usernameState == 'error'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@error%</p>
|
<p slot="text" v-if="usernameState == 'error'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@error%</p>
|
||||||
<p class="info" v-if="usernameState == 'invalid-format'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@invalid-format%</p>
|
<p slot="text" v-if="usernameState == 'invalid-format'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@invalid-format%</p>
|
||||||
<p class="info" v-if="usernameState == 'min-range'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@too-short%</p>
|
<p slot="text" v-if="usernameState == 'min-range'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@too-short%</p>
|
||||||
<p class="info" v-if="usernameState == 'max-range'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@too-long%</p>
|
<p slot="text" v-if="usernameState == 'max-range'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@too-long%</p>
|
||||||
</label>
|
</ui-input>
|
||||||
<label class="password">
|
<ui-input v-model="password" type="password" :autocomplete="Math.random()" required @input="onChangePassword" :with-password-meter="true">
|
||||||
<p class="caption">%fa:lock%%i18n:@password%</p>
|
<span>%i18n:@password%</span>
|
||||||
<input v-model="password" type="password" placeholder="%i18n:@password-placeholder%" autocomplete="off" required @input="onChangePassword"/>
|
<span slot="prefix">%fa:lock%</span>
|
||||||
<div class="meter" v-show="passwordStrength != ''" :data-strength="passwordStrength">
|
<div slot="text">
|
||||||
<div class="value" ref="passwordMetar"></div>
|
<p slot="text" v-if="passwordStrength == 'low'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@weak-password%</p>
|
||||||
|
<p slot="text" v-if="passwordStrength == 'medium'" style="color:#3CB7B5">%fa:check .fw% %i18n:@normal-password%</p>
|
||||||
|
<p slot="text" v-if="passwordStrength == 'high'" style="color:#3CB7B5">%fa:check .fw% %i18n:@strong-password%</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="info" v-if="passwordStrength == 'low'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@weak-password%</p>
|
</ui-input>
|
||||||
<p class="info" v-if="passwordStrength == 'medium'" style="color:#3CB7B5">%fa:check .fw%%i18n:@normal-password%</p>
|
<ui-input v-model="retypedPassword" type="password" :autocomplete="Math.random()" required @input="onChangePasswordRetype">
|
||||||
<p class="info" v-if="passwordStrength == 'high'" style="color:#3CB7B5">%fa:check .fw%%i18n:@strong-password%</p>
|
<span>%i18n:@password% (%i18n:@retype%)</span>
|
||||||
</label>
|
<span slot="prefix">%fa:lock%</span>
|
||||||
<label class="retype-password">
|
<div slot="text">
|
||||||
<p class="caption">%fa:lock%%i18n:@password%(%i18n:@retype%)</p>
|
<p slot="text" v-if="passwordRetypeState == 'match'" style="color:#3CB7B5">%fa:check .fw% %i18n:@password-matched%</p>
|
||||||
<input v-model="retypedPassword" type="password" placeholder="%i18n:@retype-placeholder%" autocomplete="off" required @input="onChangePasswordRetype"/>
|
<p slot="text" v-if="passwordRetypeState == 'not-match'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@password-not-matched%</p>
|
||||||
<p class="info" v-if="passwordRetypeState == 'match'" style="color:#3CB7B5">%fa:check .fw%%i18n:@password-matched%</p>
|
</div>
|
||||||
<p class="info" v-if="passwordRetypeState == 'not-match'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@password-not-matched%</p>
|
</ui-input>
|
||||||
</label>
|
<div class="g-recaptcha" :data-sitekey="recaptchaSitekey" style="margin: 16px 0;"></div>
|
||||||
<label class="recaptcha">
|
<label class="agree-tou" style="display: block; margin: 16px 0;">
|
||||||
<p class="caption"><template v-if="recaptchaed">%fa:toggle-on%</template><template v-if="!recaptchaed">%fa:toggle-off%</template>%i18n:@recaptcha%</p>
|
<input name="agree-tou" type="checkbox" required/>
|
||||||
<div class="g-recaptcha" data-callback="onRecaptchaed" data-expired-callback="onRecaptchaExpired" :data-sitekey="recaptchaSitekey"></div>
|
|
||||||
</label>
|
|
||||||
<label class="agree-tou">
|
|
||||||
<input name="agree-tou" type="checkbox" autocomplete="off" required/>
|
|
||||||
<p><a :href="touUrl" target="_blank">利用規約</a>に同意する</p>
|
<p><a :href="touUrl" target="_blank">利用規約</a>に同意する</p>
|
||||||
</label>
|
</label>
|
||||||
<button type="submit">%i18n:@create%</button>
|
<ui-button type="submit">%i18n:@create%</ui-button>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
const getPasswordStrength = require('syuilo-password-strength');
|
const getPasswordStrength = require('syuilo-password-strength');
|
||||||
import { url, docsUrl, lang, recaptchaSitekey } from '../../../config';
|
import { host, url, docsUrl, lang, recaptchaSitekey } from '../../../config';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
host,
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
retypedPassword: '',
|
retypedPassword: '',
|
||||||
url,
|
url,
|
||||||
touUrl: `${docsUrl}/${lang}/tou`,
|
touUrl: `${docsUrl}/${lang}/tou`,
|
||||||
recaptchaSitekey,
|
recaptchaSitekey,
|
||||||
recaptchaed: false,
|
|
||||||
usernameState: null,
|
usernameState: null,
|
||||||
passwordStrength: '',
|
passwordStrength: '',
|
||||||
passwordRetypeState: null
|
passwordRetypeState: null
|
||||||
|
@ -104,7 +102,6 @@ export default Vue.extend({
|
||||||
|
|
||||||
const strength = getPasswordStrength(this.password);
|
const strength = getPasswordStrength(this.password);
|
||||||
this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
|
this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
|
||||||
(this.$refs.passwordMetar as any).style.width = `${strength * 100}%`;
|
|
||||||
},
|
},
|
||||||
onChangePasswordRetype() {
|
onChangePasswordRetype() {
|
||||||
if (this.retypedPassword == '') {
|
if (this.retypedPassword == '') {
|
||||||
|
@ -130,19 +127,9 @@ export default Vue.extend({
|
||||||
alert('%i18n:@some-error%');
|
alert('%i18n:@some-error%');
|
||||||
|
|
||||||
(window as any).grecaptcha.reset();
|
(window as any).grecaptcha.reset();
|
||||||
this.recaptchaed = false;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
|
||||||
(window as any).onRecaptchaed = () => {
|
|
||||||
this.recaptchaed = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
(window as any).onRecaptchaExpired = () => {
|
|
||||||
this.recaptchaed = false;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
mounted() {
|
mounted() {
|
||||||
const head = document.getElementsByTagName('head')[0];
|
const head = document.getElementsByTagName('head')[0];
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
|
@ -158,100 +145,6 @@ export default Vue.extend({
|
||||||
.mk-signup
|
.mk-signup
|
||||||
min-width 302px
|
min-width 302px
|
||||||
|
|
||||||
label
|
|
||||||
display block
|
|
||||||
margin 0 0 16px 0
|
|
||||||
|
|
||||||
> .caption
|
|
||||||
margin 0 0 4px 0
|
|
||||||
color #828888
|
|
||||||
font-size 0.95em
|
|
||||||
|
|
||||||
> [data-fa]
|
|
||||||
margin-right 0.25em
|
|
||||||
color #96adac
|
|
||||||
|
|
||||||
> .info
|
|
||||||
display block
|
|
||||||
margin 4px 0
|
|
||||||
font-size 0.8em
|
|
||||||
|
|
||||||
> [data-fa]
|
|
||||||
margin-right 0.3em
|
|
||||||
|
|
||||||
&.username
|
|
||||||
.profile-page-url-preview
|
|
||||||
display block
|
|
||||||
margin 4px 8px 0 4px
|
|
||||||
font-size 0.8em
|
|
||||||
color #888
|
|
||||||
|
|
||||||
&:empty
|
|
||||||
display none
|
|
||||||
|
|
||||||
&:not(:empty) + .info
|
|
||||||
margin-top 0
|
|
||||||
|
|
||||||
&.password
|
|
||||||
.meter
|
|
||||||
display block
|
|
||||||
margin-top 8px
|
|
||||||
width 100%
|
|
||||||
height 8px
|
|
||||||
|
|
||||||
&[data-strength='']
|
|
||||||
display none
|
|
||||||
|
|
||||||
&[data-strength='low']
|
|
||||||
> .value
|
|
||||||
background #d73612
|
|
||||||
|
|
||||||
&[data-strength='medium']
|
|
||||||
> .value
|
|
||||||
background #d7ca12
|
|
||||||
|
|
||||||
&[data-strength='high']
|
|
||||||
> .value
|
|
||||||
background #61bb22
|
|
||||||
|
|
||||||
> .value
|
|
||||||
display block
|
|
||||||
width 0%
|
|
||||||
height 100%
|
|
||||||
background transparent
|
|
||||||
border-radius 4px
|
|
||||||
transition all 0.1s ease
|
|
||||||
|
|
||||||
[type=text], [type=password]
|
|
||||||
user-select text
|
|
||||||
display inline-block
|
|
||||||
cursor auto
|
|
||||||
padding 0 12px
|
|
||||||
margin 0
|
|
||||||
width 100%
|
|
||||||
line-height 44px
|
|
||||||
font-size 1em
|
|
||||||
color #333 !important
|
|
||||||
background #fff !important
|
|
||||||
outline none
|
|
||||||
border solid 1px rgba(#000, 0.1)
|
|
||||||
border-radius 4px
|
|
||||||
box-shadow 0 0 0 114514px #fff inset
|
|
||||||
transition all .3s ease
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
border-color rgba(#000, 0.2)
|
|
||||||
transition all .1s ease
|
|
||||||
|
|
||||||
&:focus
|
|
||||||
color $theme-color !important
|
|
||||||
border-color $theme-color
|
|
||||||
box-shadow 0 0 0 1024px #fff inset, 0 0 0 4px rgba($theme-color, 10%)
|
|
||||||
transition all 0s ease
|
|
||||||
|
|
||||||
&:disabled
|
|
||||||
opacity 0.5
|
|
||||||
|
|
||||||
.agree-tou
|
.agree-tou
|
||||||
padding 4px
|
padding 4px
|
||||||
border-radius 4px
|
border-radius 4px
|
||||||
|
@ -269,19 +162,4 @@ export default Vue.extend({
|
||||||
display inline
|
display inline
|
||||||
color #555
|
color #555
|
||||||
|
|
||||||
button
|
|
||||||
margin 0
|
|
||||||
padding 16px
|
|
||||||
width 100%
|
|
||||||
font-size 1em
|
|
||||||
color #fff
|
|
||||||
background $theme-color
|
|
||||||
border-radius 3px
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
background lighten($theme-color, 5%)
|
|
||||||
|
|
||||||
&:active
|
|
||||||
background darken($theme-color, 5%)
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
82
src/client/app/common/views/components/ui/button.vue
Normal file
82
src/client/app/common/views/components/ui/button.vue
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
<template>
|
||||||
|
<div class="ui-button" :class="[styl]">
|
||||||
|
<button :type="type" @click="$emit('click')">
|
||||||
|
<slot></slot>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
styl: 'fill'
|
||||||
|
};
|
||||||
|
},
|
||||||
|
inject: {
|
||||||
|
isCardChild: { default: false }
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
if (this.isCardChild) {
|
||||||
|
this.styl = 'line';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
@import '~const.styl'
|
||||||
|
|
||||||
|
root(isDark, fill)
|
||||||
|
> button
|
||||||
|
display block
|
||||||
|
width 100%
|
||||||
|
margin 0
|
||||||
|
padding 0
|
||||||
|
font-weight bold
|
||||||
|
font-size 16px
|
||||||
|
line-height 44px
|
||||||
|
border none
|
||||||
|
border-radius 6px
|
||||||
|
outline none
|
||||||
|
box-shadow none
|
||||||
|
|
||||||
|
if fill
|
||||||
|
color $theme-color-foreground
|
||||||
|
background $theme-color
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background lighten($theme-color, 5%)
|
||||||
|
|
||||||
|
&:active
|
||||||
|
background darken($theme-color, 5%)
|
||||||
|
else
|
||||||
|
color $theme-color
|
||||||
|
background none
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
color darken($theme-color, 5%)
|
||||||
|
|
||||||
|
&:active
|
||||||
|
background rgba($theme-color, 0.3)
|
||||||
|
|
||||||
|
.ui-button[data-darkmode]
|
||||||
|
&.fill
|
||||||
|
root(true, true)
|
||||||
|
&:not(.fill)
|
||||||
|
root(true, false)
|
||||||
|
|
||||||
|
.ui-button:not([data-darkmode])
|
||||||
|
&.fill
|
||||||
|
root(false, true)
|
||||||
|
&:not(.fill)
|
||||||
|
root(false, false)
|
||||||
|
|
||||||
|
</style>
|
46
src/client/app/common/views/components/ui/card.vue
Normal file
46
src/client/app/common/views/components/ui/card.vue
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
<template>
|
||||||
|
<div class="ui-card">
|
||||||
|
<header>
|
||||||
|
<slot name="title"></slot>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
export default Vue.extend({
|
||||||
|
provide() {
|
||||||
|
return {
|
||||||
|
isCardChild: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
@import '~const.styl'
|
||||||
|
|
||||||
|
root(isDark)
|
||||||
|
margin 16px
|
||||||
|
padding 16px
|
||||||
|
color isDark ? #fff : #000
|
||||||
|
background isDark ? #282C37 : #fff
|
||||||
|
box-shadow 0 3px 1px -2px rgba(#000, 0.2), 0 2px 2px 0 rgba(#000, 0.14), 0 1px 5px 0 rgba(#000, 0.12)
|
||||||
|
|
||||||
|
@media (min-width 500px)
|
||||||
|
padding 32px
|
||||||
|
|
||||||
|
> header
|
||||||
|
font-weight normal
|
||||||
|
font-size 24px
|
||||||
|
color isDark ? #fff : #444
|
||||||
|
|
||||||
|
.ui-card[data-darkmode]
|
||||||
|
root(true)
|
||||||
|
|
||||||
|
.ui-card:not([data-darkmode])
|
||||||
|
root(false)
|
||||||
|
|
||||||
|
</style>
|
30
src/client/app/common/views/components/ui/form.vue
Normal file
30
src/client/app/common/views/components/ui/form.vue
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<template>
|
||||||
|
<div class="ui-form">
|
||||||
|
<fieldset :disabled="disabled">
|
||||||
|
<slot></slot>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
@import '~const.styl'
|
||||||
|
|
||||||
|
.ui-form
|
||||||
|
> fieldset
|
||||||
|
margin 0
|
||||||
|
padding 0
|
||||||
|
border none
|
||||||
|
|
||||||
|
</style>
|
321
src/client/app/common/views/components/ui/input.vue
Normal file
321
src/client/app/common/views/components/ui/input.vue
Normal file
|
@ -0,0 +1,321 @@
|
||||||
|
<template>
|
||||||
|
<div class="ui-input" :class="[{ focused, filled }, styl]">
|
||||||
|
<div class="icon" ref="icon"><slot name="icon"></slot></div>
|
||||||
|
<div class="input" @click="focus" @mousedown="focus">
|
||||||
|
<div class="password-meter" v-if="withPasswordMeter" v-show="passwordStrength != ''" :data-strength="passwordStrength">
|
||||||
|
<div class="value" ref="passwordMetar"></div>
|
||||||
|
</div>
|
||||||
|
<span class="label" ref="label"><slot></slot></span>
|
||||||
|
<div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
|
||||||
|
<template v-if="type != 'file'">
|
||||||
|
<input ref="input"
|
||||||
|
:type="type"
|
||||||
|
:value="v"
|
||||||
|
:required="required"
|
||||||
|
:readonly="readonly"
|
||||||
|
:pattern="pattern"
|
||||||
|
:autocomplete="autocomplete"
|
||||||
|
@input="$emit('input', $event.target.value)"
|
||||||
|
@focus="focused = true"
|
||||||
|
@blur="focused = false">
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<input ref="input"
|
||||||
|
type="text"
|
||||||
|
:value="placeholder"
|
||||||
|
readonly
|
||||||
|
@click="chooseFile">
|
||||||
|
<input ref="file"
|
||||||
|
type="file"
|
||||||
|
:value="value"
|
||||||
|
@change="onChangeFile">
|
||||||
|
</template>
|
||||||
|
<div class="suffix"><slot name="suffix"></slot></div>
|
||||||
|
</div>
|
||||||
|
<div class="text"><slot name="text"></slot></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
const getPasswordStrength = require('syuilo-password-strength');
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
required: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
readonly: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
pattern: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
autocomplete: {
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
withPasswordMeter: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
v: this.value,
|
||||||
|
focused: false,
|
||||||
|
passwordStrength: '',
|
||||||
|
styl: 'fill'
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
filled(): boolean {
|
||||||
|
return this.v != '' && this.v != null;
|
||||||
|
},
|
||||||
|
placeholder(): string {
|
||||||
|
if (this.type != 'file') return null;
|
||||||
|
if (this.v == null) return null;
|
||||||
|
|
||||||
|
if (typeof this.v == 'string') return this.v;
|
||||||
|
|
||||||
|
if (Array.isArray(this.v)) {
|
||||||
|
return this.v.map(file => file.name).join(', ');
|
||||||
|
} else {
|
||||||
|
return this.v.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value(v) {
|
||||||
|
this.v = v;
|
||||||
|
},
|
||||||
|
v(v) {
|
||||||
|
if (this.withPasswordMeter) {
|
||||||
|
if (v == '') {
|
||||||
|
this.passwordStrength = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const strength = getPasswordStrength(v);
|
||||||
|
this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
|
||||||
|
(this.$refs.passwordMetar as any).style.width = `${strength * 100}%`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
inject: {
|
||||||
|
isCardChild: { default: false }
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
if (this.isCardChild) {
|
||||||
|
this.styl = 'line';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.$refs.prefix) {
|
||||||
|
this.$refs.label.style.left = (this.$refs.prefix.offsetLeft + this.$refs.prefix.offsetWidth) + 'px';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
focus() {
|
||||||
|
this.$refs.input.focus();
|
||||||
|
},
|
||||||
|
chooseFile() {
|
||||||
|
this.$refs.file.click();
|
||||||
|
},
|
||||||
|
onChangeFile() {
|
||||||
|
this.v = Array.from((this.$refs.file as any).files);
|
||||||
|
this.$emit('input', this.v);
|
||||||
|
this.$emit('change', this.v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
@import '~const.styl'
|
||||||
|
|
||||||
|
root(isDark, fill)
|
||||||
|
margin 32px 0
|
||||||
|
|
||||||
|
> .icon
|
||||||
|
position absolute
|
||||||
|
top 0
|
||||||
|
left 0
|
||||||
|
width 24px
|
||||||
|
text-align center
|
||||||
|
line-height 32px
|
||||||
|
color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
|
||||||
|
|
||||||
|
&:not(:empty) + .input
|
||||||
|
margin-left 28px
|
||||||
|
|
||||||
|
> .input
|
||||||
|
display flex
|
||||||
|
cursor text
|
||||||
|
|
||||||
|
if fill
|
||||||
|
padding 6px 12px
|
||||||
|
background rgba(#000, 0.035)
|
||||||
|
border-radius 6px
|
||||||
|
else
|
||||||
|
&:before
|
||||||
|
content ''
|
||||||
|
display block
|
||||||
|
position absolute
|
||||||
|
bottom 0
|
||||||
|
left 0
|
||||||
|
right 0
|
||||||
|
height 1px
|
||||||
|
background isDark ? rgba(#fff, 0.7) : rgba(#000, 0.42)
|
||||||
|
|
||||||
|
&:after
|
||||||
|
content ''
|
||||||
|
display block
|
||||||
|
position absolute
|
||||||
|
bottom 0
|
||||||
|
left 0
|
||||||
|
right 0
|
||||||
|
height 2px
|
||||||
|
background $theme-color
|
||||||
|
opacity 0
|
||||||
|
transform scaleX(0.12)
|
||||||
|
transition border 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)
|
||||||
|
will-change border opacity transform
|
||||||
|
|
||||||
|
> .password-meter
|
||||||
|
position absolute
|
||||||
|
top 0
|
||||||
|
left 0
|
||||||
|
width 100%
|
||||||
|
height 100%
|
||||||
|
border-radius 6px
|
||||||
|
overflow hidden
|
||||||
|
opacity 0.3
|
||||||
|
|
||||||
|
&[data-strength='']
|
||||||
|
display none
|
||||||
|
|
||||||
|
&[data-strength='low']
|
||||||
|
> .value
|
||||||
|
background #d73612
|
||||||
|
|
||||||
|
&[data-strength='medium']
|
||||||
|
> .value
|
||||||
|
background #d7ca12
|
||||||
|
|
||||||
|
&[data-strength='high']
|
||||||
|
> .value
|
||||||
|
background #61bb22
|
||||||
|
|
||||||
|
> .value
|
||||||
|
display block
|
||||||
|
width 0%
|
||||||
|
height 100%
|
||||||
|
background transparent
|
||||||
|
border-radius 6px
|
||||||
|
transition all 0.1s ease
|
||||||
|
|
||||||
|
> .label
|
||||||
|
position absolute
|
||||||
|
top fill ? 6px : 0
|
||||||
|
left 0
|
||||||
|
pointer-events none
|
||||||
|
transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1)
|
||||||
|
transition-duration 0.3s
|
||||||
|
font-size 16px
|
||||||
|
line-height 32px
|
||||||
|
color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
|
||||||
|
pointer-events none
|
||||||
|
//will-change transform
|
||||||
|
transform-origin top left
|
||||||
|
transform scale(1)
|
||||||
|
|
||||||
|
> input
|
||||||
|
display block
|
||||||
|
flex 1
|
||||||
|
width 100%
|
||||||
|
padding 0
|
||||||
|
font inherit
|
||||||
|
font-weight fill ? bold : normal
|
||||||
|
font-size 16px
|
||||||
|
line-height 32px
|
||||||
|
color isDark ? #fff : #000
|
||||||
|
background transparent
|
||||||
|
border none
|
||||||
|
border-radius 0
|
||||||
|
outline none
|
||||||
|
box-shadow none
|
||||||
|
|
||||||
|
&[type='file']
|
||||||
|
display none
|
||||||
|
|
||||||
|
> .prefix
|
||||||
|
> .suffix
|
||||||
|
display block
|
||||||
|
align-self center
|
||||||
|
justify-self center
|
||||||
|
font-size 16px
|
||||||
|
line-height 32px
|
||||||
|
color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
|
||||||
|
pointer-events none
|
||||||
|
|
||||||
|
> *
|
||||||
|
display block
|
||||||
|
min-width 16px
|
||||||
|
|
||||||
|
> .prefix
|
||||||
|
padding-right 4px
|
||||||
|
|
||||||
|
> .suffix
|
||||||
|
padding-left 4px
|
||||||
|
|
||||||
|
> .text
|
||||||
|
margin 6px 0
|
||||||
|
font-size 13px
|
||||||
|
|
||||||
|
*
|
||||||
|
margin 0
|
||||||
|
|
||||||
|
&.focused
|
||||||
|
> .input
|
||||||
|
if fill
|
||||||
|
background rgba(#000, 0.05)
|
||||||
|
else
|
||||||
|
&:after
|
||||||
|
opacity 1
|
||||||
|
transform scaleX(1)
|
||||||
|
|
||||||
|
> .label
|
||||||
|
color $theme-color
|
||||||
|
|
||||||
|
&.focused
|
||||||
|
&.filled
|
||||||
|
> .input
|
||||||
|
> .label
|
||||||
|
top fill ? -24px : -17px
|
||||||
|
left 0 !important
|
||||||
|
transform scale(0.75)
|
||||||
|
|
||||||
|
.ui-input[data-darkmode]
|
||||||
|
&.fill
|
||||||
|
root(true, true)
|
||||||
|
&:not(.fill)
|
||||||
|
root(true, false)
|
||||||
|
|
||||||
|
.ui-input:not([data-darkmode])
|
||||||
|
&.fill
|
||||||
|
root(false, true)
|
||||||
|
&:not(.fill)
|
||||||
|
root(false, false)
|
||||||
|
|
||||||
|
</style>
|
120
src/client/app/common/views/components/ui/radio.vue
Normal file
120
src/client/app/common/views/components/ui/radio.vue
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="ui-radio"
|
||||||
|
: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: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
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-block
|
||||||
|
margin 32px 32px 32px 0
|
||||||
|
cursor pointer
|
||||||
|
transition all 0.3s
|
||||||
|
|
||||||
|
> *
|
||||||
|
user-select none
|
||||||
|
|
||||||
|
&.disabled
|
||||||
|
opacity 0.6
|
||||||
|
cursor not-allowed
|
||||||
|
|
||||||
|
&.checked
|
||||||
|
> .button
|
||||||
|
border-color $theme-color
|
||||||
|
|
||||||
|
&:after
|
||||||
|
background-color $theme-color
|
||||||
|
transform scale(1)
|
||||||
|
opacity 1
|
||||||
|
|
||||||
|
> input
|
||||||
|
position absolute
|
||||||
|
width 0
|
||||||
|
height 0
|
||||||
|
opacity 0
|
||||||
|
margin 0
|
||||||
|
|
||||||
|
> .button
|
||||||
|
position absolute
|
||||||
|
width 20px
|
||||||
|
height 20px
|
||||||
|
background none
|
||||||
|
border solid 2px isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
|
||||||
|
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 28px
|
||||||
|
display block
|
||||||
|
font-size 16px
|
||||||
|
line-height 20px
|
||||||
|
cursor pointer
|
||||||
|
|
||||||
|
.ui-radio[data-darkmode]
|
||||||
|
root(true)
|
||||||
|
|
||||||
|
.ui-radio:not([data-darkmode])
|
||||||
|
root(false)
|
||||||
|
|
||||||
|
</style>
|
215
src/client/app/common/views/components/ui/select.vue
Normal file
215
src/client/app/common/views/components/ui/select.vue
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
<template>
|
||||||
|
<div class="ui-select" :class="[{ focused, filled }, styl]">
|
||||||
|
<div class="icon" ref="icon"><slot name="icon"></slot></div>
|
||||||
|
<div class="input" @click="focus">
|
||||||
|
<span class="label" ref="label"><slot name="label"></slot></span>
|
||||||
|
<div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
|
||||||
|
<select ref="input"
|
||||||
|
:value="v"
|
||||||
|
:required="required"
|
||||||
|
@input="$emit('input', $event.target.value)"
|
||||||
|
@focus="focused = true"
|
||||||
|
@blur="focused = false">
|
||||||
|
<slot></slot>
|
||||||
|
</select>
|
||||||
|
<div class="suffix"><slot name="suffix"></slot></div>
|
||||||
|
</div>
|
||||||
|
<div class="text"><slot name="text"></slot></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
required: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
v: this.value,
|
||||||
|
focused: false,
|
||||||
|
styl: 'fill'
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
filled(): boolean {
|
||||||
|
return this.v != '' && this.v != null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value(v) {
|
||||||
|
this.v = v;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
inject: {
|
||||||
|
isCardChild: { default: false }
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
if (this.isCardChild) {
|
||||||
|
this.styl = 'line';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.$refs.prefix) {
|
||||||
|
this.$refs.label.style.left = (this.$refs.prefix.offsetLeft + this.$refs.prefix.offsetWidth) + 'px';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
focus() {
|
||||||
|
this.$refs.input.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
@import '~const.styl'
|
||||||
|
|
||||||
|
root(isDark, fill)
|
||||||
|
margin 32px 0
|
||||||
|
|
||||||
|
> .icon
|
||||||
|
position absolute
|
||||||
|
top 0
|
||||||
|
left 0
|
||||||
|
width 24px
|
||||||
|
text-align center
|
||||||
|
line-height 32px
|
||||||
|
color rgba(#000, 0.54)
|
||||||
|
|
||||||
|
&:not(:empty) + .input
|
||||||
|
margin-left 28px
|
||||||
|
|
||||||
|
> .input
|
||||||
|
display flex
|
||||||
|
|
||||||
|
if fill
|
||||||
|
padding 6px 12px
|
||||||
|
background rgba(#000, 0.035)
|
||||||
|
border-radius 6px
|
||||||
|
else
|
||||||
|
&:before
|
||||||
|
content ''
|
||||||
|
display block
|
||||||
|
position absolute
|
||||||
|
bottom 0
|
||||||
|
left 0
|
||||||
|
right 0
|
||||||
|
height 1px
|
||||||
|
background isDark ? rgba(#fff, 0.7) : rgba(#000, 0.42)
|
||||||
|
|
||||||
|
&:after
|
||||||
|
content ''
|
||||||
|
display block
|
||||||
|
position absolute
|
||||||
|
bottom 0
|
||||||
|
left 0
|
||||||
|
right 0
|
||||||
|
height 2px
|
||||||
|
background $theme-color
|
||||||
|
opacity 0
|
||||||
|
transform scaleX(0.12)
|
||||||
|
transition border 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)
|
||||||
|
will-change border opacity transform
|
||||||
|
|
||||||
|
> .label
|
||||||
|
position absolute
|
||||||
|
top fill ? 6px : 0
|
||||||
|
left 0
|
||||||
|
pointer-events none
|
||||||
|
transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1)
|
||||||
|
transition-duration 0.3s
|
||||||
|
font-size 16px
|
||||||
|
line-height 32px
|
||||||
|
color rgba(#000, 0.54)
|
||||||
|
pointer-events none
|
||||||
|
//will-change transform
|
||||||
|
transform-origin top left
|
||||||
|
transform scale(1)
|
||||||
|
|
||||||
|
> select
|
||||||
|
display block
|
||||||
|
flex 1
|
||||||
|
width 100%
|
||||||
|
padding 0
|
||||||
|
font inherit
|
||||||
|
font-weight fill ? bold : normal
|
||||||
|
font-size 16px
|
||||||
|
height 32px
|
||||||
|
color isDark ? #fff : #000
|
||||||
|
background transparent
|
||||||
|
border none
|
||||||
|
border-radius 0
|
||||||
|
outline none
|
||||||
|
box-shadow none
|
||||||
|
|
||||||
|
*
|
||||||
|
color #000
|
||||||
|
|
||||||
|
> .prefix
|
||||||
|
> .suffix
|
||||||
|
display block
|
||||||
|
align-self center
|
||||||
|
justify-self center
|
||||||
|
font-size 16px
|
||||||
|
line-height 32px
|
||||||
|
color rgba(#000, 0.54)
|
||||||
|
pointer-events none
|
||||||
|
|
||||||
|
> *
|
||||||
|
display block
|
||||||
|
min-width 16px
|
||||||
|
|
||||||
|
> .prefix
|
||||||
|
padding-right 4px
|
||||||
|
|
||||||
|
> .suffix
|
||||||
|
padding-left 4px
|
||||||
|
|
||||||
|
> .text
|
||||||
|
margin 6px 0
|
||||||
|
font-size 13px
|
||||||
|
|
||||||
|
*
|
||||||
|
margin 0
|
||||||
|
|
||||||
|
&.focused
|
||||||
|
> .input
|
||||||
|
if fill
|
||||||
|
background rgba(#000, 0.05)
|
||||||
|
else
|
||||||
|
&:after
|
||||||
|
opacity 1
|
||||||
|
transform scaleX(1)
|
||||||
|
|
||||||
|
> .label
|
||||||
|
color $theme-color
|
||||||
|
|
||||||
|
&.focused
|
||||||
|
&.filled
|
||||||
|
> .input
|
||||||
|
> .label
|
||||||
|
top fill ? -24px : -17px
|
||||||
|
left 0 !important
|
||||||
|
transform scale(0.75)
|
||||||
|
|
||||||
|
.ui-select[data-darkmode]
|
||||||
|
&.fill
|
||||||
|
root(true, true)
|
||||||
|
&:not(.fill)
|
||||||
|
root(true, false)
|
||||||
|
|
||||||
|
.ui-select:not([data-darkmode])
|
||||||
|
&.fill
|
||||||
|
root(false, true)
|
||||||
|
&:not(.fill)
|
||||||
|
root(false, false)
|
||||||
|
|
||||||
|
</style>
|
135
src/client/app/common/views/components/ui/switch.vue
Normal file
135
src/client/app/common/views/components/ui/switch.vue
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="ui-switch"
|
||||||
|
:class="{ disabled, checked }"
|
||||||
|
role="switch"
|
||||||
|
:aria-checked="checked"
|
||||||
|
:aria-disabled="disabled"
|
||||||
|
@click="toggle"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
ref="input"
|
||||||
|
:disabled="disabled"
|
||||||
|
@keydown.enter="toggle"
|
||||||
|
>
|
||||||
|
<span class="button">
|
||||||
|
<span></span>
|
||||||
|
</span>
|
||||||
|
<span class="label">
|
||||||
|
<span :aria-hidden="!checked"><slot></slot></span>
|
||||||
|
<p :aria-hidden="!checked">
|
||||||
|
<slot name="text"></slot>
|
||||||
|
</p>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
export default Vue.extend({
|
||||||
|
model: {
|
||||||
|
prop: 'value',
|
||||||
|
event: 'change'
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
checked(): boolean {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggle() {
|
||||||
|
this.$emit('change', !this.checked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
@import '~const.styl'
|
||||||
|
|
||||||
|
root(isDark)
|
||||||
|
display flex
|
||||||
|
margin 32px 0
|
||||||
|
cursor pointer
|
||||||
|
transition all 0.3s
|
||||||
|
|
||||||
|
> *
|
||||||
|
user-select none
|
||||||
|
|
||||||
|
&.disabled
|
||||||
|
opacity 0.6
|
||||||
|
cursor not-allowed
|
||||||
|
|
||||||
|
&.checked
|
||||||
|
> .button
|
||||||
|
background-color rgba($theme-color, 0.4)
|
||||||
|
border-color rgba($theme-color, 0.4)
|
||||||
|
|
||||||
|
> *
|
||||||
|
background-color $theme-color
|
||||||
|
transform translateX(14px)
|
||||||
|
|
||||||
|
> input
|
||||||
|
position absolute
|
||||||
|
width 0
|
||||||
|
height 0
|
||||||
|
opacity 0
|
||||||
|
margin 0
|
||||||
|
|
||||||
|
> .button
|
||||||
|
display inline-block
|
||||||
|
margin 3px 0 0 0
|
||||||
|
width 34px
|
||||||
|
height 14px
|
||||||
|
background isDark ? rgba(#fff, 0.15) : rgba(#000, 0.25)
|
||||||
|
outline none
|
||||||
|
border-radius 14px
|
||||||
|
transition inherit
|
||||||
|
|
||||||
|
> *
|
||||||
|
position absolute
|
||||||
|
top -3px
|
||||||
|
left 0
|
||||||
|
border-radius 100%
|
||||||
|
transition background-color 0.3s, transform 0.3s
|
||||||
|
width 20px
|
||||||
|
height 20px
|
||||||
|
background-color #fff
|
||||||
|
box-shadow 0 2px 1px -1px rgba(#000, 0.2), 0 1px 1px 0 rgba(#000, 0.14), 0 1px 3px 0 rgba(#000, 0.12)
|
||||||
|
|
||||||
|
> .label
|
||||||
|
margin-left 8px
|
||||||
|
display block
|
||||||
|
font-size 16px
|
||||||
|
cursor pointer
|
||||||
|
transition inherit
|
||||||
|
|
||||||
|
> span
|
||||||
|
display block
|
||||||
|
line-height 20px
|
||||||
|
color isDark ? #c4ccd2 : rgba(#000, 0.75)
|
||||||
|
transition inherit
|
||||||
|
|
||||||
|
> p
|
||||||
|
margin 0
|
||||||
|
//font-size 90%
|
||||||
|
color isDark ? #78858e : #9daab3
|
||||||
|
|
||||||
|
.ui-switch[data-darkmode]
|
||||||
|
root(true)
|
||||||
|
|
||||||
|
.ui-switch:not([data-darkmode])
|
||||||
|
root(false)
|
||||||
|
|
||||||
|
</style>
|
174
src/client/app/common/views/components/ui/textarea.vue
Normal file
174
src/client/app/common/views/components/ui/textarea.vue
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
<template>
|
||||||
|
<div class="ui-textarea" :class="{ focused, filled }">
|
||||||
|
<div class="input">
|
||||||
|
<span class="label" ref="label"><slot></slot></span>
|
||||||
|
<textarea ref="input"
|
||||||
|
:value="value"
|
||||||
|
:required="required"
|
||||||
|
:readonly="readonly"
|
||||||
|
:pattern="pattern"
|
||||||
|
:autocomplete="autocomplete"
|
||||||
|
@input="$emit('input', $event.target.value)"
|
||||||
|
@focus="focused = true"
|
||||||
|
@blur="focused = false">
|
||||||
|
</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="text"><slot name="text"></slot></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
const getPasswordStrength = require('syuilo-password-strength');
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
required: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
readonly: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
pattern: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
autocomplete: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
focused: false,
|
||||||
|
passwordStrength: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
filled(): boolean {
|
||||||
|
return this.value != '' && this.value != null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
focus() {
|
||||||
|
this.$refs.input.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
@import '~const.styl'
|
||||||
|
|
||||||
|
root(isDark, fill)
|
||||||
|
margin 32px 0
|
||||||
|
|
||||||
|
> .input
|
||||||
|
padding 12px
|
||||||
|
|
||||||
|
if fill
|
||||||
|
background rgba(#000, 0.035)
|
||||||
|
border-radius 6px
|
||||||
|
else
|
||||||
|
&:before
|
||||||
|
content ''
|
||||||
|
display block
|
||||||
|
position absolute
|
||||||
|
top 0
|
||||||
|
bottom 0
|
||||||
|
left 0
|
||||||
|
right 0
|
||||||
|
background none
|
||||||
|
border solid 1px isDark ? rgba(#fff, 0.7) : rgba(#000, 0.42)
|
||||||
|
border-radius 3px
|
||||||
|
pointer-events none
|
||||||
|
|
||||||
|
&:after
|
||||||
|
content ''
|
||||||
|
display block
|
||||||
|
position absolute
|
||||||
|
top 0
|
||||||
|
bottom 0
|
||||||
|
left 0
|
||||||
|
right 0
|
||||||
|
background none
|
||||||
|
border solid 2px $theme-color
|
||||||
|
border-radius 3px
|
||||||
|
opacity 0
|
||||||
|
transition opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1)
|
||||||
|
pointer-events none
|
||||||
|
|
||||||
|
> .label
|
||||||
|
position absolute
|
||||||
|
top 6px
|
||||||
|
left 12px
|
||||||
|
pointer-events none
|
||||||
|
transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1)
|
||||||
|
transition-duration 0.3s
|
||||||
|
font-size 16px
|
||||||
|
line-height 32px
|
||||||
|
color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
|
||||||
|
pointer-events none
|
||||||
|
//will-change transform
|
||||||
|
transform-origin top left
|
||||||
|
transform scale(1)
|
||||||
|
|
||||||
|
> textarea
|
||||||
|
display block
|
||||||
|
width 100%
|
||||||
|
min-height 100px
|
||||||
|
padding 0
|
||||||
|
font inherit
|
||||||
|
font-weight fill ? bold : normal
|
||||||
|
font-size 16px
|
||||||
|
color isDark ? #fff : #000
|
||||||
|
background transparent
|
||||||
|
border none
|
||||||
|
border-radius 0
|
||||||
|
outline none
|
||||||
|
box-shadow none
|
||||||
|
|
||||||
|
> .text
|
||||||
|
margin 6px 0
|
||||||
|
font-size 13px
|
||||||
|
|
||||||
|
*
|
||||||
|
margin 0
|
||||||
|
|
||||||
|
&.focused
|
||||||
|
> .input
|
||||||
|
if fill
|
||||||
|
background rgba(#000, 0.05)
|
||||||
|
else
|
||||||
|
&:after
|
||||||
|
opacity 1
|
||||||
|
|
||||||
|
> .label
|
||||||
|
color $theme-color
|
||||||
|
|
||||||
|
&.focused
|
||||||
|
&.filled
|
||||||
|
> .input
|
||||||
|
> .label
|
||||||
|
top -24px
|
||||||
|
left 0 !important
|
||||||
|
transform scale(0.75)
|
||||||
|
|
||||||
|
.ui-textarea[data-darkmode]
|
||||||
|
&.fill
|
||||||
|
root(true, true)
|
||||||
|
&:not(.fill)
|
||||||
|
root(true, false)
|
||||||
|
|
||||||
|
.ui-textarea:not([data-darkmode])
|
||||||
|
&.fill
|
||||||
|
root(false, true)
|
||||||
|
&:not(.fill)
|
||||||
|
root(false, false)
|
||||||
|
|
||||||
|
</style>
|
|
@ -1,6 +1,8 @@
|
||||||
declare const _HOST_: string;
|
declare const _HOST_: string;
|
||||||
declare const _HOSTNAME_: string;
|
declare const _HOSTNAME_: string;
|
||||||
declare const _URL_: string;
|
declare const _URL_: string;
|
||||||
|
declare const _NAME_: string;
|
||||||
|
declare const _DESCRIPTION_: string;
|
||||||
declare const _API_URL_: string;
|
declare const _API_URL_: string;
|
||||||
declare const _WS_URL_: string;
|
declare const _WS_URL_: string;
|
||||||
declare const _DOCS_URL_: string;
|
declare const _DOCS_URL_: string;
|
||||||
|
@ -21,6 +23,8 @@ declare const _GOOGLE_MAPS_API_KEY_: string;
|
||||||
export const host = _HOST_;
|
export const host = _HOST_;
|
||||||
export const hostname = _HOSTNAME_;
|
export const hostname = _HOSTNAME_;
|
||||||
export const url = _URL_;
|
export const url = _URL_;
|
||||||
|
export const name = _NAME_;
|
||||||
|
export const description = _DESCRIPTION_;
|
||||||
export const apiUrl = _API_URL_;
|
export const apiUrl = _API_URL_;
|
||||||
export const wsUrl = _WS_URL_;
|
export const wsUrl = _WS_URL_;
|
||||||
export const docsUrl = _DOCS_URL_;
|
export const docsUrl = _DOCS_URL_;
|
||||||
|
|
|
@ -2,17 +2,11 @@
|
||||||
* Mobile Client
|
* Mobile Client
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Vue from 'vue';
|
|
||||||
import VueRouter from 'vue-router';
|
import VueRouter from 'vue-router';
|
||||||
|
|
||||||
import { MdCard, MdButton, MdField, MdMenu, MdList, MdSwitch, MdSubheader, MdDialog, MdDialogAlert, MdRadio } from 'vue-material/dist/components';
|
|
||||||
import 'vue-material/dist/vue-material.min.css';
|
|
||||||
import 'vue-material/dist/theme/default.css';
|
|
||||||
|
|
||||||
// Style
|
// Style
|
||||||
import './style.styl';
|
import './style.styl';
|
||||||
import '../../element.scss';
|
import '../../element.scss';
|
||||||
import '../../md.scss';
|
|
||||||
|
|
||||||
import init from '../init';
|
import init from '../init';
|
||||||
|
|
||||||
|
@ -44,17 +38,6 @@ import MkSettings from './views/pages/settings.vue';
|
||||||
import MkOthello from './views/pages/othello.vue';
|
import MkOthello from './views/pages/othello.vue';
|
||||||
import MkTag from './views/pages/tag.vue';
|
import MkTag from './views/pages/tag.vue';
|
||||||
|
|
||||||
Vue.use(MdCard);
|
|
||||||
Vue.use(MdButton);
|
|
||||||
Vue.use(MdField);
|
|
||||||
Vue.use(MdMenu);
|
|
||||||
Vue.use(MdList);
|
|
||||||
Vue.use(MdSwitch);
|
|
||||||
Vue.use(MdSubheader);
|
|
||||||
Vue.use(MdDialog);
|
|
||||||
Vue.use(MdDialogAlert);
|
|
||||||
Vue.use(MdRadio);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* init
|
* init
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -10,9 +10,6 @@ html
|
||||||
height 100%
|
height 100%
|
||||||
background #ececed !important
|
background #ececed !important
|
||||||
|
|
||||||
// for md
|
|
||||||
transition none !important
|
|
||||||
|
|
||||||
&[data-darkmode]
|
&[data-darkmode]
|
||||||
background #191B22 !important
|
background #191B22 !important
|
||||||
|
|
||||||
|
|
|
@ -1,132 +1,84 @@
|
||||||
<template>
|
<template>
|
||||||
<mk-ui>
|
<mk-ui>
|
||||||
<span slot="header">%fa:cog%%i18n:@settings%</span>
|
<span slot="header">%fa:cog%%i18n:@settings%</span>
|
||||||
<main>
|
<main :data-darkmode="$store.state.device.darkmode">
|
||||||
<p v-html="'%i18n:@signed-in-as%'.replace('{}', '<b>' + name + '</b>')"></p>
|
<div class="signin-as" v-html="'%i18n:@signed-in-as%'.replace('{}', '<b>' + name + '</b>')"></div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<x-profile/>
|
<x-profile/>
|
||||||
|
|
||||||
<md-card>
|
<ui-card>
|
||||||
<md-card-header>
|
<div slot="title">%fa:palette% %i18n:@design%</div>
|
||||||
<div class="md-title">%fa:palette% %i18n:@design%</div>
|
|
||||||
</md-card-header>
|
|
||||||
|
|
||||||
<md-card-content>
|
<ui-switch v-model="darkmode">%i18n:@dark-mode%</ui-switch>
|
||||||
<div>
|
<ui-switch v-model="$store.state.settings.circleIcons" @change="onChangeCircleIcons">%i18n:@circle-icons%</ui-switch>
|
||||||
<md-switch v-model="darkmode">%i18n:@dark-mode%</md-switch>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<md-switch v-model="$store.state.settings.circleIcons" @change="onChangeCircleIcons">%i18n:@circle-icons%</md-switch>
|
<div>%i18n:@timeline%</div>
|
||||||
</div>
|
<ui-switch v-model="$store.state.settings.showReplyTarget" @change="onChangeShowReplyTarget">%i18n:@show-reply-target%</ui-switch>
|
||||||
|
<ui-switch v-model="$store.state.settings.showMyRenotes" @change="onChangeShowMyRenotes">%i18n:@show-my-renotes%</ui-switch>
|
||||||
|
<ui-switch v-model="$store.state.settings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes">%i18n:@show-renoted-my-notes%</ui-switch>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="md-body-2">%i18n:@timeline%</div>
|
<div>%i18n:@post-style%</div>
|
||||||
|
<ui-radio v-model="postStyle" value="standard">%i18n:@post-style-standard%</ui-radio>
|
||||||
|
<ui-radio v-model="postStyle" value="smart">%i18n:@post-style-smart%</ui-radio>
|
||||||
|
</div>
|
||||||
|
</ui-card>
|
||||||
|
|
||||||
<div>
|
<ui-card>
|
||||||
<md-switch v-model="$store.state.settings.showReplyTarget" @change="onChangeShowReplyTarget">%i18n:@show-reply-target%</md-switch>
|
<div slot="title">%fa:cog% %i18n:@behavior%</div>
|
||||||
</div>
|
<ui-switch v-model="$store.state.settings.fetchOnScroll" @change="onChangeFetchOnScroll">%i18n:@fetch-on-scroll%</ui-switch>
|
||||||
|
<ui-switch v-model="$store.state.settings.disableViaMobile" @change="onChangeDisableViaMobile">%i18n:@disable-via-mobile%</ui-switch>
|
||||||
|
<ui-switch v-model="loadRawImages">%i18n:@load-raw-images%</ui-switch>
|
||||||
|
<ui-switch v-model="$store.state.settings.loadRemoteMedia" @change="onChangeLoadRemoteMedia">%i18n:@load-remote-media%</ui-switch>
|
||||||
|
<ui-switch v-model="lightmode">%i18n:@i-am-under-limited-internet%</ui-switch>
|
||||||
|
</ui-card>
|
||||||
|
|
||||||
<div>
|
<ui-card>
|
||||||
<md-switch v-model="$store.state.settings.showMyRenotes" @change="onChangeShowMyRenotes">%i18n:@show-my-renotes%</md-switch>
|
<div slot="title">%fa:language% %i18n:@lang%</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<ui-select v-model="lang" placeholder="%i18n:@auto%">
|
||||||
<md-switch v-model="$store.state.settings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes">%i18n:@show-renoted-my-notes%</md-switch>
|
<optgroup label="%i18n:@recommended%">
|
||||||
</div>
|
<option value="">%i18n:@auto%</option>
|
||||||
</div>
|
</optgroup>
|
||||||
|
|
||||||
<div>
|
<optgroup label="%i18n:@specify-language%">
|
||||||
<div class="md-body-2">%i18n:@post-style%</div>
|
<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
|
||||||
|
</optgroup>
|
||||||
|
</ui-select>
|
||||||
|
<span>%fa:info-circle% %i18n:@lang-tip%</span>
|
||||||
|
</ui-card>
|
||||||
|
|
||||||
<md-radio v-model="postStyle" value="standard">%i18n:@post-style-standard%</md-radio>
|
<ui-card>
|
||||||
<md-radio v-model="postStyle" value="smart">%i18n:@post-style-smart%</md-radio>
|
<div slot="title">%fa:B twitter% %i18n:@twitter%</div>
|
||||||
</div>
|
|
||||||
</md-card-content>
|
|
||||||
</md-card>
|
|
||||||
|
|
||||||
<md-card>
|
<p class="account" v-if="$store.state.i.twitter"><a :href="`https://twitter.com/${$store.state.i.twitter.screenName}`" target="_blank">@{{ $store.state.i.twitter.screenName }}</a></p>
|
||||||
<md-card-header>
|
<p>
|
||||||
<div class="md-title">%fa:cog% %i18n:@behavior%</div>
|
<a :href="`${apiUrl}/connect/twitter`" target="_blank">{{ $store.state.i.twitter ? '%i18n:@twitter-reconnect%' : '%i18n:@twitter-connect%' }}</a>
|
||||||
</md-card-header>
|
<span v-if="$store.state.i.twitter"> or </span>
|
||||||
|
<a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="$store.state.i.twitter">%i18n:@twitter-disconnect%</a>
|
||||||
|
</p>
|
||||||
|
</ui-card>
|
||||||
|
|
||||||
<md-card-content>
|
<ui-card>
|
||||||
<div>
|
<div slot="title">%fa:sync-alt% %i18n:@update%</div>
|
||||||
<md-switch v-model="$store.state.settings.fetchOnScroll" @change="onChangeFetchOnScroll">%i18n:@fetch-on-scroll%</md-switch>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>%i18n:@version% <i>{{ version }}</i></div>
|
||||||
<md-switch v-model="$store.state.settings.disableViaMobile" @change="onChangeDisableViaMobile">%i18n:@disable-via-mobile%</md-switch>
|
<template v-if="latestVersion !== undefined">
|
||||||
</div>
|
<div>%i18n:@latest-version% <i>{{ latestVersion ? latestVersion : version }}</i></div>
|
||||||
|
</template>
|
||||||
<div>
|
<ui-button @click="checkForUpdate" :disabled="checkingForUpdate">
|
||||||
<md-switch v-model="loadRawImages">%i18n:@load-raw-images%</md-switch>
|
<template v-if="checkingForUpdate">%i18n:@update-checking%<mk-ellipsis/></template>
|
||||||
</div>
|
<template v-else>%i18n:@check-for-updates%</template>
|
||||||
|
</ui-button>
|
||||||
<div>
|
</ui-card>
|
||||||
<md-switch v-model="$store.state.settings.loadRemoteMedia" @change="onChangeLoadRemoteMedia">%i18n:@load-remote-media%</md-switch>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<md-switch v-model="lightmode">%i18n:@i-am-under-limited-internet%</md-switch>
|
|
||||||
</div>
|
|
||||||
</md-card-content>
|
|
||||||
</md-card>
|
|
||||||
|
|
||||||
<md-card>
|
|
||||||
<md-card-header>
|
|
||||||
<div class="md-title">%fa:language% %i18n:@lang%</div>
|
|
||||||
</md-card-header>
|
|
||||||
|
|
||||||
<md-card-content>
|
|
||||||
<md-field>
|
|
||||||
<md-select v-model="lang" placeholder="%i18n:@auto%">
|
|
||||||
<md-optgroup label="%i18n:@recommended%">
|
|
||||||
<md-option value="">%i18n:@auto%</md-option>
|
|
||||||
</md-optgroup>
|
|
||||||
|
|
||||||
<md-optgroup label="%i18n:@specify-language%">
|
|
||||||
<md-option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</md-option>
|
|
||||||
</md-optgroup>
|
|
||||||
</md-select>
|
|
||||||
</md-field>
|
|
||||||
<span class="md-helper-text">%fa:info-circle% %i18n:@lang-tip%</span>
|
|
||||||
</md-card-content>
|
|
||||||
</md-card>
|
|
||||||
|
|
||||||
<md-card>
|
|
||||||
<md-card-header>
|
|
||||||
<div class="md-title">%fa:B twitter% %i18n:@twitter%</div>
|
|
||||||
</md-card-header>
|
|
||||||
|
|
||||||
<md-card-content>
|
|
||||||
<p class="account" v-if="$store.state.i.twitter"><a :href="`https://twitter.com/${$store.state.i.twitter.screenName}`" target="_blank">@{{ $store.state.i.twitter.screenName }}</a></p>
|
|
||||||
<p>
|
|
||||||
<a :href="`${apiUrl}/connect/twitter`" target="_blank">{{ $store.state.i.twitter ? '%i18n:@twitter-reconnect%' : '%i18n:@twitter-connect%' }}</a>
|
|
||||||
<span v-if="$store.state.i.twitter"> or </span>
|
|
||||||
<a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="$store.state.i.twitter">%i18n:@twitter-disconnect%</a>
|
|
||||||
</p>
|
|
||||||
</md-card-content>
|
|
||||||
</md-card>
|
|
||||||
|
|
||||||
<md-card>
|
|
||||||
<md-card-header>
|
|
||||||
<div class="md-title">%fa:sync-alt% %i18n:@update%</div>
|
|
||||||
</md-card-header>
|
|
||||||
|
|
||||||
<md-card-content>
|
|
||||||
<div>%i18n:@version% <i>{{ version }}</i></div>
|
|
||||||
<template v-if="latestVersion !== undefined">
|
|
||||||
<div>%i18n:@latest-version% <i>{{ latestVersion ? latestVersion : version }}</i></div>
|
|
||||||
</template>
|
|
||||||
<md-button class="md-raised md-primary" @click="checkForUpdate" :disabled="checkingForUpdate">
|
|
||||||
<template v-if="checkingForUpdate">%i18n:@update-checking%<mk-ellipsis/></template>
|
|
||||||
<template v-else>%i18n:@check-for-updates%</template>
|
|
||||||
</md-button>
|
|
||||||
</md-card-content>
|
|
||||||
</md-card>
|
|
||||||
</div>
|
</div>
|
||||||
<p><small>ver {{ version }} ({{ codename }})</small></p>
|
|
||||||
|
<footer>
|
||||||
|
<small>ver {{ version }} ({{ codename }})</small>
|
||||||
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
</mk-ui>
|
</mk-ui>
|
||||||
</template>
|
</template>
|
||||||
|
@ -267,20 +219,22 @@ export default Vue.extend({
|
||||||
|
|
||||||
<style lang="stylus" scoped>
|
<style lang="stylus" scoped>
|
||||||
root(isDark)
|
root(isDark)
|
||||||
padding 0 16px
|
|
||||||
margin 0 auto
|
margin 0 auto
|
||||||
max-width 500px
|
max-width 500px
|
||||||
width 100%
|
width 100%
|
||||||
|
|
||||||
> div
|
> .signin-as
|
||||||
> *
|
margin 16px
|
||||||
margin-bottom 16px
|
padding 16px
|
||||||
|
|
||||||
> p
|
|
||||||
display block
|
|
||||||
margin 24px
|
|
||||||
text-align center
|
text-align center
|
||||||
color isDark ? #cad2da : #a2a9b1
|
color isDark ? #49ab63 : #2c662d
|
||||||
|
background isDark ? #273c34 : #fcfff5
|
||||||
|
box-shadow 0 3px 1px -2px rgba(#000, 0.2), 0 2px 2px 0 rgba(#000, 0.14), 0 1px 5px 0 rgba(#000, 0.12)
|
||||||
|
|
||||||
|
> footer
|
||||||
|
margin 16px
|
||||||
|
text-align center
|
||||||
|
color isDark ? #c9d2e0 : #888
|
||||||
|
|
||||||
main[data-darkmode]
|
main[data-darkmode]
|
||||||
root(true)
|
root(true)
|
||||||
|
|
|
@ -1,62 +1,49 @@
|
||||||
<template>
|
<template>
|
||||||
<md-card>
|
<ui-card>
|
||||||
<md-card-header>
|
<div slot="title">%fa:user% %i18n:@title%</div>
|
||||||
<div class="md-title">%fa:pencil-alt% %i18n:@title%</div>
|
|
||||||
</md-card-header>
|
|
||||||
|
|
||||||
<md-card-content>
|
<ui-form :disabled="saving">
|
||||||
<md-field>
|
<ui-input v-model="name" :max="30">
|
||||||
<label>%i18n:@name%</label>
|
<span>%i18n:@name%</span>
|
||||||
<md-input v-model="name" :disabled="saving" md-counter="30"/>
|
</ui-input>
|
||||||
</md-field>
|
|
||||||
|
|
||||||
<md-field>
|
<ui-input v-model="username" readonly>
|
||||||
<label>%i18n:@account%</label>
|
<span>%i18n:@account%</span>
|
||||||
<span class="md-prefix">@</span>
|
<span slot="prefix">@</span>
|
||||||
<md-input v-model="username" readonly></md-input>
|
<span slot="suffix">@{{ host }}</span>
|
||||||
<span class="md-suffix">@{{ host }}</span>
|
</ui-input>
|
||||||
</md-field>
|
|
||||||
|
|
||||||
<md-field>
|
<ui-input v-model="location">
|
||||||
<md-icon>%fa:map-marker-alt%</md-icon>
|
<span>%i18n:@location%</span>
|
||||||
<label>%i18n:@location%</label>
|
<span slot="prefix">%fa:map-marker-alt%</span>
|
||||||
<md-input v-model="location" :disabled="saving"/>
|
</ui-input>
|
||||||
</md-field>
|
|
||||||
|
|
||||||
<md-field>
|
<ui-input v-model="birthday" type="date">
|
||||||
<md-icon>%fa:birthday-cake%</md-icon>
|
<span>%i18n:@birthday%</span>
|
||||||
<label>%i18n:@birthday%</label>
|
<span slot="prefix">%fa:birthday-cake%</span>
|
||||||
<md-input type="date" v-model="birthday" :disabled="saving"/>
|
</ui-input>
|
||||||
</md-field>
|
|
||||||
|
|
||||||
<md-field>
|
<ui-textarea v-model="description" :max="500">
|
||||||
<label>%i18n:@description%</label>
|
<span>%i18n:@description%</span>
|
||||||
<md-textarea v-model="description" :disabled="saving" md-counter="500"/>
|
</ui-textarea>
|
||||||
</md-field>
|
|
||||||
|
|
||||||
<md-field>
|
<ui-input type="file" @change="onAvatarChange">
|
||||||
<label>%i18n:@avatar%</label>
|
<span>%i18n:@avatar%</span>
|
||||||
<md-file @md-change="onAvatarChange"/>
|
<span slot="icon">%fa:image%</span>
|
||||||
</md-field>
|
<span slot="text" v-if="avatarUploading">%i18n:@uploading%<mk-ellipsis/></span>
|
||||||
|
</ui-input>
|
||||||
|
|
||||||
<md-field>
|
<ui-input type="file" @change="onBannerChange">
|
||||||
<label>%i18n:@banner%</label>
|
<span>%i18n:@banner%</span>
|
||||||
<md-file @md-change="onBannerChange"/>
|
<span slot="icon">%fa:image%</span>
|
||||||
</md-field>
|
<span slot="text" v-if="bannerUploading">%i18n:@uploading%<mk-ellipsis/></span>
|
||||||
|
</ui-input>
|
||||||
|
|
||||||
<md-dialog-alert
|
<ui-switch v-model="isCat">%i18n:@is-cat%</ui-switch>
|
||||||
:md-active.sync="uploading"
|
|
||||||
md-content="%18n:!@uploading%"/>
|
|
||||||
|
|
||||||
<div>
|
<ui-button @click="save">%i18n:@save%</ui-button>
|
||||||
<md-switch v-model="isCat">%i18n:@is-cat%</md-switch>
|
</ui-form>
|
||||||
</div>
|
</ui-card>
|
||||||
</md-card-content>
|
|
||||||
|
|
||||||
<md-card-actions>
|
|
||||||
<md-button class="md-primary" :disabled="saving" @click="save">%i18n:@save%</md-button>
|
|
||||||
</md-card-actions>
|
|
||||||
</md-card>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -77,7 +64,8 @@ export default Vue.extend({
|
||||||
isBot: false,
|
isBot: false,
|
||||||
isCat: false,
|
isCat: false,
|
||||||
saving: false,
|
saving: false,
|
||||||
uploading: false
|
avatarUploading: false,
|
||||||
|
bannerUploading: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -95,7 +83,7 @@ export default Vue.extend({
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onAvatarChange([file]) {
|
onAvatarChange([file]) {
|
||||||
this.uploading = true;
|
this.avatarUploading = true;
|
||||||
|
|
||||||
const data = new FormData();
|
const data = new FormData();
|
||||||
data.append('file', file);
|
data.append('file', file);
|
||||||
|
@ -108,16 +96,16 @@ export default Vue.extend({
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(f => {
|
.then(f => {
|
||||||
this.avatarId = f.id;
|
this.avatarId = f.id;
|
||||||
this.uploading = false;
|
this.avatarUploading = false;
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
this.uploading = false;
|
this.avatarUploading = false;
|
||||||
alert('%18n:!@upload-failed%');
|
alert('%18n:!@upload-failed%');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onBannerChange([file]) {
|
onBannerChange([file]) {
|
||||||
this.uploading = true;
|
this.bannerUploading = true;
|
||||||
|
|
||||||
const data = new FormData();
|
const data = new FormData();
|
||||||
data.append('file', file);
|
data.append('file', file);
|
||||||
|
@ -130,10 +118,10 @@ export default Vue.extend({
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(f => {
|
.then(f => {
|
||||||
this.bannerId = f.id;
|
this.bannerId = f.id;
|
||||||
this.uploading = false;
|
this.bannerUploading = false;
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
this.uploading = false;
|
this.bannerUploading = false;
|
||||||
alert('%18n:!@upload-failed%');
|
alert('%18n:!@upload-failed%');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,57 +1,26 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="signup">
|
<div class="signup">
|
||||||
<h1>Misskeyをはじめる</h1>
|
<h1>Misskeyをはじめる</h1>
|
||||||
<p>いつでも、どこからでもMisskeyを利用できます。もちろん、無料です。</p>
|
<mk-signup/>
|
||||||
<div class="form">
|
|
||||||
<p>新規登録</p>
|
|
||||||
<div>
|
|
||||||
<mk-signup/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
export default Vue.extend({
|
export default Vue.extend({});
|
||||||
mounted() {
|
|
||||||
document.documentElement.style.background = '#293946';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="stylus" scoped>
|
<style lang="stylus" scoped>
|
||||||
.signup
|
.signup
|
||||||
padding 16px
|
padding 32px
|
||||||
margin 0 auto
|
margin 0 auto
|
||||||
max-width 500px
|
max-width 500px
|
||||||
|
|
||||||
h1
|
h1
|
||||||
margin 0
|
margin 0
|
||||||
padding 8px
|
padding 8px 0 0 0
|
||||||
font-size 1.5em
|
font-size 1.5em
|
||||||
font-weight normal
|
font-weight bold
|
||||||
color #c3c6ca
|
color #444
|
||||||
|
|
||||||
& + p
|
|
||||||
margin 0 0 16px 0
|
|
||||||
padding 0 8px 0 8px
|
|
||||||
color #949fa9
|
|
||||||
|
|
||||||
.form
|
|
||||||
background #fff
|
|
||||||
border solid 1px rgba(#000, 0.2)
|
|
||||||
border-radius 8px
|
|
||||||
overflow hidden
|
|
||||||
|
|
||||||
> p
|
|
||||||
margin 0
|
|
||||||
padding 12px 20px
|
|
||||||
color #555
|
|
||||||
background #f5f5f5
|
|
||||||
border-bottom solid 1px #ddd
|
|
||||||
|
|
||||||
> div
|
|
||||||
padding 16px
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,29 +1,31 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="welcome">
|
<div class="welcome">
|
||||||
<div>
|
<div>
|
||||||
<h1><b>Misskey</b>へようこそ</h1>
|
<img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" alt="Misskey">
|
||||||
<p>Twitter風ミニブログSNS、Misskeyへようこそ。共有したいことを投稿したり、タイムラインでみんなの投稿を読むこともできます。<br><a href="/signup">アカウントを作成する</a></p>
|
<p class="host">{{ host }}</p>
|
||||||
<div class="form">
|
<div class="about">
|
||||||
<p>%fa:lock% ログイン</p>
|
<h2>{{ name || 'unidentified' }}</h2>
|
||||||
|
<p v-html="description || '%i18n:common.about%'"></p>
|
||||||
|
<router-link class="signup" to="/signup">新規登録</router-link>
|
||||||
|
</div>
|
||||||
|
<div class="login">
|
||||||
|
<form @submit.prevent="onSubmit">
|
||||||
|
<ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" autofocus required @change="onUsernameChange">
|
||||||
|
<span>ユーザー名</span>
|
||||||
|
<span slot="prefix">@</span>
|
||||||
|
<span slot="suffix">@{{ host }}</span>
|
||||||
|
</ui-input>
|
||||||
|
<ui-input v-model="password" type="password" required>
|
||||||
|
<span>パスワード</span>
|
||||||
|
<span slot="prefix">%fa:lock%</span>
|
||||||
|
</ui-input>
|
||||||
|
<ui-input v-if="user && user.twoFactorEnabled" v-model="token" type="number" required/>
|
||||||
|
<ui-button type="submit" :disabled="signing">{{ signing ? 'ログインしています' : 'ログイン' }}</ui-button>
|
||||||
|
</form>
|
||||||
<div>
|
<div>
|
||||||
<form @submit.prevent="onSubmit">
|
<a :href="`${apiUrl}/signin/twitter`">Twitterでログイン</a>
|
||||||
<input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" placeholder="ユーザー名" autofocus required @change="onUsernameChange"/>
|
|
||||||
<input v-model="password" type="password" placeholder="パスワード" required/>
|
|
||||||
<input v-if="user && user.twoFactorEnabled" v-model="token" type="number" placeholder="トークン" required/>
|
|
||||||
<button type="submit" :disabled="signing">{{ signing ? 'ログインしています' : 'ログイン' }}</button>
|
|
||||||
</form>
|
|
||||||
<div>
|
|
||||||
<a :href="`${apiUrl}/signin/twitter`">Twitterでログイン</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tl">
|
|
||||||
<p>%fa:comments R% タイムラインを見てみる</p>
|
|
||||||
<mk-welcome-timeline/>
|
|
||||||
</div>
|
|
||||||
<div class="users">
|
|
||||||
<mk-avatar class="avatar" v-for="user in users" :key="user.id" :user="user"/>
|
|
||||||
</div>
|
|
||||||
<footer>
|
<footer>
|
||||||
<small>{{ copyright }}</small>
|
<small>{{ copyright }}</small>
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -33,7 +35,7 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import { apiUrl, copyright } from '../../../config';
|
import { apiUrl, copyright, host, name, description } from '../../../config';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
data() {
|
data() {
|
||||||
|
@ -45,7 +47,10 @@ export default Vue.extend({
|
||||||
token: '',
|
token: '',
|
||||||
apiUrl,
|
apiUrl,
|
||||||
copyright,
|
copyright,
|
||||||
users: []
|
users: [],
|
||||||
|
host,
|
||||||
|
name,
|
||||||
|
description
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
@ -84,112 +89,74 @@ export default Vue.extend({
|
||||||
|
|
||||||
<style lang="stylus" scoped>
|
<style lang="stylus" scoped>
|
||||||
.welcome
|
.welcome
|
||||||
background linear-gradient(to bottom, #1e1d65, #bd6659)
|
text-align center
|
||||||
|
//background #fff
|
||||||
|
|
||||||
> div
|
> div
|
||||||
padding 16px
|
padding 32px
|
||||||
margin 0 auto
|
margin 0 auto
|
||||||
max-width 500px
|
max-width 500px
|
||||||
|
|
||||||
h1
|
> img
|
||||||
margin 0
|
display block
|
||||||
padding 8px
|
max-width 200px
|
||||||
font-size 1.5em
|
margin 0 auto
|
||||||
font-weight normal
|
|
||||||
color #cacac3
|
|
||||||
|
|
||||||
& + p
|
> .host
|
||||||
margin 0 0 16px 0
|
display block
|
||||||
padding 0 8px 0 8px
|
text-align center
|
||||||
color #949fa9
|
padding 6px 12px
|
||||||
|
line-height 32px
|
||||||
|
font-weight bold
|
||||||
|
color #333
|
||||||
|
background rgba(#000, 0.035)
|
||||||
|
border-radius 6px
|
||||||
|
|
||||||
.form
|
> .about
|
||||||
margin-bottom 16px
|
margin-top 16px
|
||||||
|
padding 16px
|
||||||
|
color #555
|
||||||
background #fff
|
background #fff
|
||||||
border solid 1px rgba(#000, 0.2)
|
border-radius 6px
|
||||||
border-radius 8px
|
|
||||||
overflow hidden
|
> h2
|
||||||
|
margin 0
|
||||||
|
|
||||||
> p
|
> p
|
||||||
margin 0
|
margin 8px
|
||||||
padding 12px 20px
|
|
||||||
color #555
|
|
||||||
background #f5f5f5
|
|
||||||
border-bottom solid 1px #ddd
|
|
||||||
|
|
||||||
> div
|
> .signup
|
||||||
|
font-weight bold
|
||||||
|
|
||||||
> form
|
> .login
|
||||||
padding 16px
|
margin 16px 0
|
||||||
border-bottom solid 1px #ddd
|
|
||||||
|
|
||||||
input
|
> form
|
||||||
display block
|
|
||||||
padding 12px
|
|
||||||
margin 0 0 16px 0
|
|
||||||
width 100%
|
|
||||||
font-size 1em
|
|
||||||
color rgba(#000, 0.7)
|
|
||||||
background #fff
|
|
||||||
outline none
|
|
||||||
border solid 1px #ddd
|
|
||||||
border-radius 4px
|
|
||||||
|
|
||||||
button
|
button
|
||||||
display block
|
display block
|
||||||
width 100%
|
width 100%
|
||||||
padding 10px
|
padding 10px
|
||||||
margin 0
|
margin 0
|
||||||
color #333
|
color #333
|
||||||
font-size 1em
|
font-size 1em
|
||||||
text-align center
|
|
||||||
text-decoration none
|
|
||||||
text-shadow 0 1px 0 rgba(255, 255, 255, 0.9)
|
|
||||||
background-image linear-gradient(#fafafa, #eaeaea)
|
|
||||||
border 1px solid #ddd
|
|
||||||
border-bottom-color #cecece
|
|
||||||
border-radius 4px
|
|
||||||
|
|
||||||
&:active
|
|
||||||
background-color #767676
|
|
||||||
background-image none
|
|
||||||
border-color #444
|
|
||||||
box-shadow 0 1px 3px rgba(#000, 0.075), inset 0 0 5px rgba(#000, 0.2)
|
|
||||||
|
|
||||||
> div
|
|
||||||
padding 16px
|
|
||||||
text-align center
|
text-align center
|
||||||
|
text-decoration none
|
||||||
|
text-shadow 0 1px 0 rgba(255, 255, 255, 0.9)
|
||||||
|
background-image linear-gradient(#fafafa, #eaeaea)
|
||||||
|
border 1px solid #ddd
|
||||||
|
border-bottom-color #cecece
|
||||||
|
border-radius 4px
|
||||||
|
|
||||||
> .tl
|
&:active
|
||||||
background #fff
|
background-color #767676
|
||||||
border solid 1px rgba(#000, 0.2)
|
background-image none
|
||||||
border-radius 8px
|
border-color #444
|
||||||
overflow hidden
|
box-shadow 0 1px 3px rgba(#000, 0.075), inset 0 0 5px rgba(#000, 0.2)
|
||||||
|
|
||||||
> p
|
|
||||||
margin 0
|
|
||||||
padding 12px 20px
|
|
||||||
color #555
|
|
||||||
background #f5f5f5
|
|
||||||
border-bottom solid 1px #ddd
|
|
||||||
|
|
||||||
> .mk-welcome-timeline
|
|
||||||
max-height 300px
|
|
||||||
overflow auto
|
|
||||||
|
|
||||||
> .users
|
|
||||||
margin 12px 0 0 0
|
|
||||||
|
|
||||||
> *
|
|
||||||
display inline-block
|
|
||||||
margin 4px
|
|
||||||
width 38px
|
|
||||||
height 38px
|
|
||||||
border-radius 6px
|
|
||||||
|
|
||||||
> footer
|
> footer
|
||||||
text-align center
|
text-align center
|
||||||
color #fff
|
color #444
|
||||||
|
|
||||||
> small
|
> small
|
||||||
display block
|
display block
|
||||||
|
|
|
@ -56,7 +56,7 @@ export default define({
|
||||||
left 92px
|
left 92px
|
||||||
margin 0
|
margin 0
|
||||||
line-height 100px
|
line-height 100px
|
||||||
color #fff !important // !important is for md
|
color #fff
|
||||||
font-weight bold
|
font-weight bold
|
||||||
text-shadow 0 0 8px rgba(#000, 0.5)
|
text-shadow 0 0 8px rgba(#000, 0.5)
|
||||||
|
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
/* SEE: https://vuematerial.io/themes/configuration */
|
|
||||||
|
|
||||||
@import '../const.json';
|
|
||||||
|
|
||||||
@import "~vue-material/dist/theme/engine";
|
|
||||||
|
|
||||||
@include md-register-theme("default", (
|
|
||||||
primary: $themeColor,
|
|
||||||
accent: $themeColor
|
|
||||||
));
|
|
||||||
|
|
||||||
@import "~vue-material/dist/components/MdButton/theme";
|
|
||||||
@import "~vue-material/dist/components/MdField/theme";
|
|
|
@ -15,6 +15,8 @@ export type Source = {
|
||||||
*/
|
*/
|
||||||
url: string;
|
url: string;
|
||||||
};
|
};
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
url: string;
|
url: string;
|
||||||
port: number;
|
port: number;
|
||||||
https?: { [x: string]: string };
|
https?: { [x: string]: string };
|
||||||
|
|
|
@ -79,6 +79,8 @@ const consts = {
|
||||||
_DEV_URL_: config.dev_url,
|
_DEV_URL_: config.dev_url,
|
||||||
_LANG_: '%lang%',
|
_LANG_: '%lang%',
|
||||||
_LANGS_: Object.keys(locales).map(l => [l, locales[l].meta.lang]),
|
_LANGS_: Object.keys(locales).map(l => [l, locales[l].meta.lang]),
|
||||||
|
_NAME_: config.name,
|
||||||
|
_DESCRIPTION_: config.description,
|
||||||
_HOST_: config.host,
|
_HOST_: config.host,
|
||||||
_HOSTNAME_: config.hostname,
|
_HOSTNAME_: config.hostname,
|
||||||
_URL_: config.url,
|
_URL_: config.url,
|
||||||
|
|
Loading…
Reference in a new issue