Compare commits
	
		
			32 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | ca668898f4 | ||
|   | fcd437c89f | ||
|   | 7f7d7edc7f | ||
|   | bd827f946a | ||
|   | ad8aa1c179 | ||
|   | 3ebaf83ce0 | ||
|   | 39b1978ff3 | ||
|   | bddff17e5e | ||
|   | 0ac9120064 | ||
|   | d90f75425f | ||
|   | dec7d537dc | ||
|   | 11e95ea092 | ||
|   | c5e9b69eb3 | ||
|   | 120c11b181 | ||
|   | a1ae832129 | ||
|   | 3a4833818f | ||
|   | 8814fc9c9c | ||
|   | e6e02ece89 | ||
|   | 9059c149dd | ||
|   | 7d8e70b2ac | ||
|   | 89105f5641 | ||
|   | 1813d17b4c | ||
|   | ce27b36fd0 | ||
|   | e635a87628 | ||
|   | 80c52433cc | ||
|   | 1472f0b141 | ||
|   | 4d914f5c0a | ||
|   | 0318f7344f | ||
|   | 413fbb3d0c | ||
|   | 8bc47baf4f | ||
|   | e3f6d42a47 | ||
|   | 8230935fd3 | 
| @@ -1,8 +1,8 @@ | |||||||
| { | { | ||||||
| 	"name": "misskey", | 	"name": "misskey", | ||||||
| 	"author": "syuilo <i@syuilo.com>", | 	"author": "syuilo <i@syuilo.com>", | ||||||
| 	"version": "2.37.7", | 	"version": "2.40.1", | ||||||
| 	"clientVersion": "1.0.6474", | 	"clientVersion": "1.0.6504", | ||||||
| 	"codename": "nighthike", | 	"codename": "nighthike", | ||||||
| 	"main": "./built/index.js", | 	"main": "./built/index.js", | ||||||
| 	"private": true, | 	"private": true, | ||||||
| @@ -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); | ||||||
|   | |||||||
| @@ -44,10 +44,10 @@ export default Vue.component('mk-note-html', { | |||||||
| 			return; | 			return; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		while ( | 		while (ast[ast.length - 1] && ( | ||||||
| 			ast[ast.length - 1].type == 'hashtag' || | 			ast[ast.length - 1].type == 'hashtag' || | ||||||
| 			(ast[ast.length - 1].type == 'text' && ast[ast.length - 1].content == ' ') || | 			(ast[ast.length - 1].type == 'text' && ast[ast.length - 1].content == ' ') || | ||||||
| 			(ast[ast.length - 1].type == 'text' && ast[ast.length - 1].content == '\n')) { | 			(ast[ast.length - 1].type == 'text' && ast[ast.length - 1].content == '\n'))) { | ||||||
| 			ast.pop(); | 			ast.pop(); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| @@ -103,7 +103,7 @@ export default Vue.component('mk-note-html', { | |||||||
| 				case 'hashtag': | 				case 'hashtag': | ||||||
| 					return createElement('a', { | 					return createElement('a', { | ||||||
| 						attrs: { | 						attrs: { | ||||||
| 							href: `${url}/tags/${token.content}`, | 							href: `${url}/tags/${token.hashtag}`, | ||||||
| 							target: '_blank' | 							target: '_blank' | ||||||
| 						} | 						} | ||||||
| 					}, token.content); | 					}, token.content); | ||||||
|   | |||||||
| @@ -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> | ||||||
							
								
								
									
										322
									
								
								src/client/app/common/views/components/ui/input.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										322
									
								
								src/client/app/common/views/components/ui/input.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,322 @@ | |||||||
|  | <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% | ||||||
|  | 			margin 0 | ||||||
|  | 			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> | ||||||
| @@ -76,13 +76,8 @@ root(isDark) | |||||||
| 				margin-right 4px | 				margin-right 4px | ||||||
|  |  | ||||||
| 		> div | 		> div | ||||||
| 			.chart-enter | 			.chart-move | ||||||
| 			.chart-leave-to | 				transition transform 1s ease | ||||||
| 				opacity 0 |  | ||||||
| 				transform translateY(-30px) |  | ||||||
|  |  | ||||||
| 			> * |  | ||||||
| 				transition transform .3s ease, opacity .3s ease |  | ||||||
|  |  | ||||||
| 			> div | 			> div | ||||||
| 				display flex | 				display flex | ||||||
|   | |||||||
| @@ -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_; | ||||||
|   | |||||||
| @@ -145,7 +145,7 @@ export default Vue.extend({ | |||||||
| 				(this as any).api('drive/files/update', { | 				(this as any).api('drive/files/update', { | ||||||
| 					fileId: this.file.id, | 					fileId: this.file.id, | ||||||
| 					name: name | 					name: name | ||||||
| 				}) | 				}); | ||||||
| 			}); | 			}); | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
| @@ -173,7 +173,9 @@ export default Vue.extend({ | |||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
| 		deleteFile() { | 		deleteFile() { | ||||||
| 			alert('not implemented yet'); | 			(this as any).api('drive/files/delete', { | ||||||
|  | 				fileId: this.file.id | ||||||
|  | 			}); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -118,6 +118,7 @@ export default Vue.extend({ | |||||||
|  |  | ||||||
| 		this.connection.on('file_created', this.onStreamDriveFileCreated); | 		this.connection.on('file_created', this.onStreamDriveFileCreated); | ||||||
| 		this.connection.on('file_updated', this.onStreamDriveFileUpdated); | 		this.connection.on('file_updated', this.onStreamDriveFileUpdated); | ||||||
|  | 		this.connection.on('file_deleted', this.onStreamDriveFileDeleted); | ||||||
| 		this.connection.on('folder_created', this.onStreamDriveFolderCreated); | 		this.connection.on('folder_created', this.onStreamDriveFolderCreated); | ||||||
| 		this.connection.on('folder_updated', this.onStreamDriveFolderUpdated); | 		this.connection.on('folder_updated', this.onStreamDriveFolderUpdated); | ||||||
|  |  | ||||||
| @@ -130,6 +131,7 @@ export default Vue.extend({ | |||||||
| 	beforeDestroy() { | 	beforeDestroy() { | ||||||
| 		this.connection.off('file_created', this.onStreamDriveFileCreated); | 		this.connection.off('file_created', this.onStreamDriveFileCreated); | ||||||
| 		this.connection.off('file_updated', this.onStreamDriveFileUpdated); | 		this.connection.off('file_updated', this.onStreamDriveFileUpdated); | ||||||
|  | 		this.connection.off('file_deleted', this.onStreamDriveFileDeleted); | ||||||
| 		this.connection.off('folder_created', this.onStreamDriveFolderCreated); | 		this.connection.off('folder_created', this.onStreamDriveFolderCreated); | ||||||
| 		this.connection.off('folder_updated', this.onStreamDriveFolderUpdated); | 		this.connection.off('folder_updated', this.onStreamDriveFolderUpdated); | ||||||
| 		(this as any).os.streams.driveStream.dispose(this.connectionId); | 		(this as any).os.streams.driveStream.dispose(this.connectionId); | ||||||
| @@ -167,6 +169,10 @@ export default Vue.extend({ | |||||||
| 			} | 			} | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
|  | 		onStreamDriveFileDeleted(fileId) { | ||||||
|  | 			this.removeFile(fileId); | ||||||
|  | 		}, | ||||||
|  |  | ||||||
| 		onStreamDriveFolderCreated(folder) { | 		onStreamDriveFolderCreated(folder) { | ||||||
| 			this.addFolder(folder, true); | 			this.addFolder(folder, true); | ||||||
| 		}, | 		}, | ||||||
|   | |||||||
| @@ -50,6 +50,7 @@ import * as XDraggable from 'vuedraggable'; | |||||||
| import getKao from '../../../common/scripts/get-kao'; | import getKao from '../../../common/scripts/get-kao'; | ||||||
| import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue'; | import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue'; | ||||||
| import parse from '../../../../../text/parse'; | import parse from '../../../../../text/parse'; | ||||||
|  | import { host } from '../../../config'; | ||||||
|  |  | ||||||
| export default Vue.extend({ | export default Vue.extend({ | ||||||
| 	components: { | 	components: { | ||||||
| @@ -129,6 +130,7 @@ export default Vue.extend({ | |||||||
|  |  | ||||||
| 				// 自分は除外 | 				// 自分は除外 | ||||||
| 				if (this.$store.state.i.username == x.username && x.host == null) return; | 				if (this.$store.state.i.username == x.username && x.host == null) return; | ||||||
|  | 				if (this.$store.state.i.username == x.username && x.host == host) return; | ||||||
|  |  | ||||||
| 				// 重複は除外 | 				// 重複は除外 | ||||||
| 				if (this.text.indexOf(`${mention} `) != -1) return; | 				if (this.text.indexOf(`${mention} `) != -1) return; | ||||||
|   | |||||||
| @@ -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'; | ||||||
|  |  | ||||||
| @@ -42,17 +36,7 @@ import MkUserLists from './views/pages/user-lists.vue'; | |||||||
| import MkUserList from './views/pages/user-list.vue'; | import MkUserList from './views/pages/user-list.vue'; | ||||||
| import MkSettings from './views/pages/settings.vue'; | import MkSettings from './views/pages/settings.vue'; | ||||||
| import MkOthello from './views/pages/othello.vue'; | import MkOthello from './views/pages/othello.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 | ||||||
| @@ -88,6 +72,7 @@ init((launch) => { | |||||||
| 			{ path: '/i/drive/file/:file', component: MkDrive }, | 			{ path: '/i/drive/file/:file', component: MkDrive }, | ||||||
| 			{ path: '/selectdrive', component: MkSelectDrive }, | 			{ path: '/selectdrive', component: MkSelectDrive }, | ||||||
| 			{ path: '/search', component: MkSearch }, | 			{ path: '/search', component: MkSearch }, | ||||||
|  | 			{ path: '/tags/:tag', component: MkTag }, | ||||||
| 			{ path: '/othello', name: 'othello', component: MkOthello }, | 			{ path: '/othello', name: 'othello', component: MkOthello }, | ||||||
| 			{ path: '/othello/:game', component: MkOthello }, | 			{ path: '/othello/:game', component: MkOthello }, | ||||||
| 			{ path: '/@:user', component: MkUser }, | 			{ path: '/@:user', component: MkUser }, | ||||||
|   | |||||||
| @@ -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 | ||||||
|  |  | ||||||
|   | |||||||
| @@ -34,15 +34,10 @@ | |||||||
| 	</div> | 	</div> | ||||||
| 	<div class="menu"> | 	<div class="menu"> | ||||||
| 		<div> | 		<div> | ||||||
| 			<a :href="`${file.url}?download`" :download="file.name"> | 			<a :href="`${file.url}?download`" :download="file.name">%fa:download%%i18n:@download%</a> | ||||||
| 				%fa:download%%i18n:@download% | 			<button @click="rename">%fa:pencil-alt%%i18n:@rename%</button> | ||||||
| 			</a> | 			<button @click="move">%fa:R folder-open%%i18n:@move%</button> | ||||||
| 			<button @click="rename"> | 			<button @click="del">%fa:trash-alt R%%i18n:@delete%</button> | ||||||
| 				%fa:pencil-alt%%i18n:@rename% |  | ||||||
| 			</button> |  | ||||||
| 			<button @click="move"> |  | ||||||
| 				%fa:R folder-open%%i18n:@move% |  | ||||||
| 			</button> |  | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="exif" v-show="exif"> | 	<div class="exif" v-show="exif"> | ||||||
| @@ -112,6 +107,13 @@ export default Vue.extend({ | |||||||
| 				}); | 				}); | ||||||
| 			}); | 			}); | ||||||
| 		}, | 		}, | ||||||
|  | 		del() { | ||||||
|  | 			(this as any).api('drive/files/delete', { | ||||||
|  | 				fileId: this.file.id | ||||||
|  | 			}).then(() => { | ||||||
|  | 				this.browser.cd(this.file.folderId, true); | ||||||
|  | 			}); | ||||||
|  | 		}, | ||||||
| 		showCreatedAt() { | 		showCreatedAt() { | ||||||
| 			alert(new Date(this.file.createdAt).toLocaleString()); | 			alert(new Date(this.file.createdAt).toLocaleString()); | ||||||
| 		}, | 		}, | ||||||
|   | |||||||
| @@ -100,6 +100,7 @@ export default Vue.extend({ | |||||||
|  |  | ||||||
| 		this.connection.on('file_created', this.onStreamDriveFileCreated); | 		this.connection.on('file_created', this.onStreamDriveFileCreated); | ||||||
| 		this.connection.on('file_updated', this.onStreamDriveFileUpdated); | 		this.connection.on('file_updated', this.onStreamDriveFileUpdated); | ||||||
|  | 		this.connection.on('file_deleted', this.onStreamDriveFileDeleted); | ||||||
| 		this.connection.on('folder_created', this.onStreamDriveFolderCreated); | 		this.connection.on('folder_created', this.onStreamDriveFolderCreated); | ||||||
| 		this.connection.on('folder_updated', this.onStreamDriveFolderUpdated); | 		this.connection.on('folder_updated', this.onStreamDriveFolderUpdated); | ||||||
|  |  | ||||||
| @@ -118,6 +119,7 @@ export default Vue.extend({ | |||||||
| 	beforeDestroy() { | 	beforeDestroy() { | ||||||
| 		this.connection.off('file_created', this.onStreamDriveFileCreated); | 		this.connection.off('file_created', this.onStreamDriveFileCreated); | ||||||
| 		this.connection.off('file_updated', this.onStreamDriveFileUpdated); | 		this.connection.off('file_updated', this.onStreamDriveFileUpdated); | ||||||
|  | 		this.connection.off('file_deleted', this.onStreamDriveFileDeleted); | ||||||
| 		this.connection.off('folder_created', this.onStreamDriveFolderCreated); | 		this.connection.off('folder_created', this.onStreamDriveFolderCreated); | ||||||
| 		this.connection.off('folder_updated', this.onStreamDriveFolderUpdated); | 		this.connection.off('folder_updated', this.onStreamDriveFolderUpdated); | ||||||
| 		(this as any).os.streams.driveStream.dispose(this.connectionId); | 		(this as any).os.streams.driveStream.dispose(this.connectionId); | ||||||
| @@ -136,6 +138,10 @@ export default Vue.extend({ | |||||||
| 			} | 			} | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
|  | 		onStreamDriveFileDeleted(fileId) { | ||||||
|  | 			this.removeFile(fileId); | ||||||
|  | 		}, | ||||||
|  |  | ||||||
| 		onStreamDriveFolderCreated(folder) { | 		onStreamDriveFolderCreated(folder) { | ||||||
| 			this.addFolder(folder, true); | 			this.addFolder(folder, true); | ||||||
| 		}, | 		}, | ||||||
|   | |||||||
| @@ -46,6 +46,7 @@ import * as XDraggable from 'vuedraggable'; | |||||||
| import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue'; | import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue'; | ||||||
| import getKao from '../../../common/scripts/get-kao'; | import getKao from '../../../common/scripts/get-kao'; | ||||||
| import parse from '../../../../../text/parse'; | import parse from '../../../../../text/parse'; | ||||||
|  | import { host } from '../../../config'; | ||||||
|  |  | ||||||
| export default Vue.extend({ | export default Vue.extend({ | ||||||
| 	components: { | 	components: { | ||||||
| @@ -123,6 +124,7 @@ export default Vue.extend({ | |||||||
|  |  | ||||||
| 				// 自分は除外 | 				// 自分は除外 | ||||||
| 				if (this.$store.state.i.username == x.username && x.host == null) return; | 				if (this.$store.state.i.username == x.username && x.host == null) return; | ||||||
|  | 				if (this.$store.state.i.username == x.username && x.host == host) return; | ||||||
|  |  | ||||||
| 				// 重複は除外 | 				// 重複は除外 | ||||||
| 				if (this.text.indexOf(`${mention} `) != -1) return; | 				if (this.text.indexOf(`${mention} `) != -1) return; | ||||||
|   | |||||||
| @@ -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> | 				<ui-switch v-model="darkmode">%i18n:@dark-mode%</ui-switch> | ||||||
|  | 				<ui-switch v-model="$store.state.settings.circleIcons" @change="onChangeCircleIcons">%i18n:@circle-icons%</ui-switch> | ||||||
|  |  | ||||||
| 				<md-card-content> |  | ||||||
| 				<div> | 				<div> | ||||||
| 						<md-switch v-model="darkmode">%i18n:@dark-mode%</md-switch> | 					<div>%i18n:@timeline%</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> | ||||||
| 						<md-switch v-model="$store.state.settings.circleIcons" @change="onChangeCircleIcons">%i18n:@circle-icons%</md-switch> | 					<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> | 				</div> | ||||||
|  | 			</ui-card> | ||||||
|  |  | ||||||
| 					<div> | 			<ui-card> | ||||||
| 						<div class="md-body-2">%i18n:@timeline%</div> | 				<div slot="title">%fa:cog% %i18n:@behavior%</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.showReplyTarget" @change="onChangeShowReplyTarget">%i18n:@show-reply-target%</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.showMyRenotes" @change="onChangeShowMyRenotes">%i18n:@show-my-renotes%</md-switch> | 					<optgroup label="%i18n:@recommended%"> | ||||||
| 						</div> | 						<option value="">%i18n:@auto%</option> | ||||||
|  | 					</optgroup> | ||||||
|  |  | ||||||
| 						<div> | 					<optgroup label="%i18n:@specify-language%"> | ||||||
| 							<md-switch v-model="$store.state.settings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes">%i18n:@show-renoted-my-notes%</md-switch> | 						<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option> | ||||||
| 						</div> | 					</optgroup> | ||||||
| 					</div> | 				</ui-select> | ||||||
|  | 				<span>%fa:info-circle% %i18n:@lang-tip%</span> | ||||||
|  | 			</ui-card> | ||||||
|  |  | ||||||
| 					<div> | 			<ui-card> | ||||||
| 						<div class="md-body-2">%i18n:@post-style%</div> | 				<div slot="title">%fa:B twitter% %i18n:@twitter%</div> | ||||||
|  |  | ||||||
| 						<md-radio v-model="postStyle" value="standard">%i18n:@post-style-standard%</md-radio> |  | ||||||
| 						<md-radio v-model="postStyle" value="smart">%i18n:@post-style-smart%</md-radio> |  | ||||||
| 					</div> |  | ||||||
| 				</md-card-content> |  | ||||||
| 			</md-card> |  | ||||||
|  |  | ||||||
| 			<md-card> |  | ||||||
| 				<md-card-header> |  | ||||||
| 					<div class="md-title">%fa:cog% %i18n:@behavior%</div> |  | ||||||
| 				</md-card-header> |  | ||||||
|  |  | ||||||
| 				<md-card-content> |  | ||||||
| 					<div> |  | ||||||
| 						<md-switch v-model="$store.state.settings.fetchOnScroll" @change="onChangeFetchOnScroll">%i18n:@fetch-on-scroll%</md-switch> |  | ||||||
| 					</div> |  | ||||||
|  |  | ||||||
| 					<div> |  | ||||||
| 						<md-switch v-model="$store.state.settings.disableViaMobile" @change="onChangeDisableViaMobile">%i18n:@disable-via-mobile%</md-switch> |  | ||||||
| 					</div> |  | ||||||
|  |  | ||||||
| 					<div> |  | ||||||
| 						<md-switch v-model="loadRawImages">%i18n:@load-raw-images%</md-switch> |  | ||||||
| 					</div> |  | ||||||
|  |  | ||||||
| 					<div> |  | ||||||
| 						<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 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> | 				<p> | ||||||
| 					<a :href="`${apiUrl}/connect/twitter`" target="_blank">{{ $store.state.i.twitter ? '%i18n:@twitter-reconnect%' : '%i18n:@twitter-connect%' }}</a> | 					<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> | 					<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> | 					<a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="$store.state.i.twitter">%i18n:@twitter-disconnect%</a> | ||||||
| 				</p> | 				</p> | ||||||
| 				</md-card-content> | 			</ui-card> | ||||||
| 			</md-card> |  | ||||||
|  |  | ||||||
| 			<md-card> | 			<ui-card> | ||||||
| 				<md-card-header> | 				<div slot="title">%fa:sync-alt% %i18n:@update%</div> | ||||||
| 					<div class="md-title">%fa:sync-alt% %i18n:@update%</div> |  | ||||||
| 				</md-card-header> |  | ||||||
|  |  | ||||||
| 				<md-card-content> |  | ||||||
| 				<div>%i18n:@version% <i>{{ version }}</i></div> | 				<div>%i18n:@version% <i>{{ version }}</i></div> | ||||||
| 				<template v-if="latestVersion !== undefined"> | 				<template v-if="latestVersion !== undefined"> | ||||||
| 					<div>%i18n:@latest-version% <i>{{ latestVersion ? latestVersion : version }}</i></div> | 					<div>%i18n:@latest-version% <i>{{ latestVersion ? latestVersion : version }}</i></div> | ||||||
| 				</template> | 				</template> | ||||||
| 					<md-button class="md-raised md-primary" @click="checkForUpdate" :disabled="checkingForUpdate"> | 				<ui-button @click="checkForUpdate" :disabled="checkingForUpdate"> | ||||||
| 					<template v-if="checkingForUpdate">%i18n:@update-checking%<mk-ellipsis/></template> | 					<template v-if="checkingForUpdate">%i18n:@update-checking%<mk-ellipsis/></template> | ||||||
| 					<template v-else>%i18n:@check-for-updates%</template> | 					<template v-else>%i18n:@check-for-updates%</template> | ||||||
| 					</md-button> | 				</ui-button> | ||||||
| 				</md-card-content> | 			</ui-card> | ||||||
| 			</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> |  | ||||||
| 	<div class="form"> |  | ||||||
| 		<p>新規登録</p> |  | ||||||
| 		<div> |  | ||||||
| 	<mk-signup/> | 	<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> | ||||||
|   | |||||||
							
								
								
									
										81
									
								
								src/client/app/mobile/views/pages/tag.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/client/app/mobile/views/pages/tag.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | |||||||
|  | <template> | ||||||
|  | <mk-ui> | ||||||
|  | 	<span slot="header">%fa:hashtag%{{ $route.params.tag }}</span> | ||||||
|  |  | ||||||
|  | 	<main> | ||||||
|  | 		<p v-if="!fetching && empty">%fa:search%「{{ q }}」に関する投稿は見つかりませんでした。</p> | ||||||
|  | 		<mk-notes ref="timeline" :more="existMore ? more : null"/> | ||||||
|  | 	</main> | ||||||
|  | </mk-ui> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script lang="ts"> | ||||||
|  | import Vue from 'vue'; | ||||||
|  | import Progress from '../../../common/scripts/loading'; | ||||||
|  |  | ||||||
|  | const limit = 20; | ||||||
|  |  | ||||||
|  | export default Vue.extend({ | ||||||
|  | 	data() { | ||||||
|  | 		return { | ||||||
|  | 			fetching: true, | ||||||
|  | 			moreFetching: false, | ||||||
|  | 			existMore: false, | ||||||
|  | 			offset: 0, | ||||||
|  | 			empty: false | ||||||
|  | 		}; | ||||||
|  | 	}, | ||||||
|  | 	watch: { | ||||||
|  | 		$route: 'fetch' | ||||||
|  | 	}, | ||||||
|  | 	mounted() { | ||||||
|  | 		this.$nextTick(() => { | ||||||
|  | 			this.fetch(); | ||||||
|  | 		}); | ||||||
|  | 	}, | ||||||
|  | 	methods: { | ||||||
|  | 		fetch() { | ||||||
|  | 			this.fetching = true; | ||||||
|  | 			Progress.start(); | ||||||
|  |  | ||||||
|  | 			(this.$refs.timeline as any).init(() => new Promise((res, rej) => { | ||||||
|  | 				(this as any).api('notes/search_by_tag', { | ||||||
|  | 					limit: limit + 1, | ||||||
|  | 					offset: this.offset, | ||||||
|  | 					tag: this.$route.params.tag | ||||||
|  | 				}).then(notes => { | ||||||
|  | 					if (notes.length == 0) this.empty = true; | ||||||
|  | 					if (notes.length == limit + 1) { | ||||||
|  | 						notes.pop(); | ||||||
|  | 						this.existMore = true; | ||||||
|  | 					} | ||||||
|  | 					res(notes); | ||||||
|  | 					this.fetching = false; | ||||||
|  | 					Progress.done(); | ||||||
|  | 				}, rej); | ||||||
|  | 			})); | ||||||
|  | 		}, | ||||||
|  | 		more() { | ||||||
|  | 			this.offset += limit; | ||||||
|  |  | ||||||
|  | 			const promise = (this as any).api('notes/search_by_tag', { | ||||||
|  | 				limit: limit + 1, | ||||||
|  | 				offset: this.offset, | ||||||
|  | 				tag: this.$route.params.tag | ||||||
|  | 			}); | ||||||
|  |  | ||||||
|  | 			promise.then(notes => { | ||||||
|  | 				if (notes.length == limit + 1) { | ||||||
|  | 					notes.pop(); | ||||||
|  | 				} else { | ||||||
|  | 					this.existMore = false; | ||||||
|  | 				} | ||||||
|  | 				notes.forEach(n => (this.$refs.timeline as any).append(n)); | ||||||
|  | 				this.moreFetching = false; | ||||||
|  | 			}); | ||||||
|  |  | ||||||
|  | 			return promise; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  | </script> | ||||||
| @@ -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> | ||||||
| 			<div> | 			<p v-html="description || '%i18n:common.about%'"></p> | ||||||
|  | 			<router-link class="signup" to="/signup">新規登録</router-link> | ||||||
|  | 		</div> | ||||||
|  | 		<div class="login"> | ||||||
| 			<form @submit.prevent="onSubmit"> | 			<form @submit.prevent="onSubmit"> | ||||||
| 					<input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" placeholder="ユーザー名" autofocus required @change="onUsernameChange"/> | 				<ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" autofocus required @change="onUsernameChange"> | ||||||
| 					<input v-model="password" type="password" placeholder="パスワード" required/> | 					<span>ユーザー名</span> | ||||||
| 					<input v-if="user && user.twoFactorEnabled" v-model="token" type="number" placeholder="トークン" required/> | 					<span slot="prefix">@</span> | ||||||
| 					<button type="submit" :disabled="signing">{{ signing ? 'ログインしています' : 'ログイン' }}</button> | 					<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> | 			</form> | ||||||
| 			<div> | 			<div> | ||||||
| 				<a :href="`${apiUrl}/signin/twitter`">Twitterでログイン</a> | 				<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,56 +89,49 @@ 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 | ||||||
|  |  | ||||||
|  | 		> .login | ||||||
|  | 			margin 16px 0 | ||||||
|  |  | ||||||
| 			> form | 			> form | ||||||
| 					padding 16px |  | ||||||
| 					border-bottom solid 1px #ddd |  | ||||||
|  |  | ||||||
| 					input |  | ||||||
| 						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 | ||||||
| @@ -156,40 +154,9 @@ export default Vue.extend({ | |||||||
| 						border-color #444 | 						border-color #444 | ||||||
| 						box-shadow 0 1px 3px rgba(#000, 0.075), inset 0 0 5px rgba(#000, 0.2) | 						box-shadow 0 1px 3px rgba(#000, 0.075), inset 0 0 5px rgba(#000, 0.2) | ||||||
|  |  | ||||||
| 				> div |  | ||||||
| 					padding 16px |  | ||||||
| 					text-align center |  | ||||||
|  |  | ||||||
| 		> .tl |  | ||||||
| 			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 |  | ||||||
|  |  | ||||||
| 			> .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 }; | ||||||
|   | |||||||
| @@ -48,6 +48,11 @@ export type INote = { | |||||||
| 	repliesCount: number; | 	repliesCount: number; | ||||||
| 	reactionCounts: any; | 	reactionCounts: any; | ||||||
| 	mentions: mongo.ObjectID[]; | 	mentions: mongo.ObjectID[]; | ||||||
|  | 	mentionedRemoteUsers: Array<{ | ||||||
|  | 		uri: string; | ||||||
|  | 		username: string; | ||||||
|  | 		host: string; | ||||||
|  | 	}>; | ||||||
|  |  | ||||||
| 	/** | 	/** | ||||||
| 	 * public ... 公開 | 	 * public ... 公開 | ||||||
| @@ -289,7 +294,7 @@ export const pack = async ( | |||||||
|  |  | ||||||
| 		// Poll | 		// Poll | ||||||
| 		if (meId && _note.poll && !hide) { | 		if (meId && _note.poll && !hide) { | ||||||
| 			_note.poll = (async (poll) => { | 			_note.poll = (async poll => { | ||||||
| 				const vote = await PollVote | 				const vote = await PollVote | ||||||
| 					.findOne({ | 					.findOne({ | ||||||
| 						userId: meId, | 						userId: meId, | ||||||
|   | |||||||
| @@ -15,6 +15,11 @@ const log = debug('misskey:activitypub'); | |||||||
| export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, note: INote): Promise<void> { | export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, note: INote): Promise<void> { | ||||||
| 	const uri = activity.id || activity; | 	const uri = activity.id || activity; | ||||||
|  |  | ||||||
|  | 	// アナウンサーが凍結されていたらスキップ | ||||||
|  | 	if (actor.isSuspended) { | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	if (typeof uri !== 'string') { | 	if (typeof uri !== 'string') { | ||||||
| 		throw new Error('invalid announce'); | 		throw new Error('invalid announce'); | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import config from '../../../config'; | import config from '../../../config'; | ||||||
|  |  | ||||||
| export default tag => ({ | export default (tag: string) => ({ | ||||||
| 	type: 'Hashtag', | 	type: 'Hashtag', | ||||||
| 	href: `${config.url}/tags/${encodeURIComponent(tag)}`, | 	href: `${config.url}/tags/${encodeURIComponent(tag)}`, | ||||||
| 	name: '#' + tag | 	name: '#' + tag | ||||||
|   | |||||||
							
								
								
									
										9
									
								
								src/remote/activitypub/renderer/mention.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/remote/activitypub/renderer/mention.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | export default (mention: { | ||||||
|  | 	uri: string; | ||||||
|  | 	username: string; | ||||||
|  | 	host: string; | ||||||
|  | }) => ({ | ||||||
|  | 	type: 'Mention', | ||||||
|  | 	href: mention.uri, | ||||||
|  | 	name: `@${mention.username}@${mention.host}` | ||||||
|  | }); | ||||||
| @@ -1,5 +1,6 @@ | |||||||
| import renderDocument from './document'; | import renderDocument from './document'; | ||||||
| import renderHashtag from './hashtag'; | import renderHashtag from './hashtag'; | ||||||
|  | import renderMention from './mention'; | ||||||
| import config from '../../../config'; | import config from '../../../config'; | ||||||
| import DriveFile from '../../../models/drive-file'; | import DriveFile from '../../../models/drive-file'; | ||||||
| import Note, { INote } from '../../../models/note'; | import Note, { INote } from '../../../models/note'; | ||||||
| @@ -45,6 +46,18 @@ export default async function renderNote(note: INote, dive = true) { | |||||||
|  |  | ||||||
| 	const attributedTo = `${config.url}/users/${user._id}`; | 	const attributedTo = `${config.url}/users/${user._id}`; | ||||||
|  |  | ||||||
|  | 	const mentions = note.mentionedRemoteUsers && note.mentionedRemoteUsers.length > 0 | ||||||
|  | 		? note.mentionedRemoteUsers.map(x => x.uri) | ||||||
|  | 		: []; | ||||||
|  |  | ||||||
|  | 	const cc = ['public', 'home', 'followers'].includes(note.visibility) | ||||||
|  | 		? [`${attributedTo}/followers`].concat(mentions) | ||||||
|  | 		: []; | ||||||
|  |  | ||||||
|  | 	const hashtagTags = (note.tags || []).map(renderHashtag); | ||||||
|  | 	const mentionTags = (note.mentionedRemoteUsers || []).map(renderMention); | ||||||
|  | 	const tag = hashtagTags.concat(mentionTags) | ||||||
|  |  | ||||||
| 	return { | 	return { | ||||||
| 		id: `${config.url}/notes/${note._id}`, | 		id: `${config.url}/notes/${note._id}`, | ||||||
| 		type: 'Note', | 		type: 'Note', | ||||||
| @@ -52,9 +65,9 @@ export default async function renderNote(note: INote, dive = true) { | |||||||
| 		content: toHtml(note), | 		content: toHtml(note), | ||||||
| 		published: note.createdAt.toISOString(), | 		published: note.createdAt.toISOString(), | ||||||
| 		to: 'https://www.w3.org/ns/activitystreams#Public', | 		to: 'https://www.w3.org/ns/activitystreams#Public', | ||||||
| 		cc: `${attributedTo}/followers`, | 		cc, | ||||||
| 		inReplyTo, | 		inReplyTo, | ||||||
| 		attachment: (await promisedFiles).map(renderDocument), | 		attachment: (await promisedFiles).map(renderDocument), | ||||||
| 		tag: (note.tags || []).map(renderHashtag) | 		tag | ||||||
| 	}; | 	}; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,34 +1,32 @@ | |||||||
| /** |  | ||||||
|  * Module dependencies |  | ||||||
|  */ |  | ||||||
| import DriveFile from '../../../models/drive-file'; | import DriveFile from '../../../models/drive-file'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Get drive information |  * Get drive information | ||||||
|  * |  | ||||||
|  * @param {any} params |  | ||||||
|  * @param {any} user |  | ||||||
|  * @return {Promise<any>} |  | ||||||
|  */ |  */ | ||||||
| module.exports = (params, user) => new Promise(async (res, rej) => { | module.exports = (params, user) => new Promise(async (res, rej) => { | ||||||
| 	// Calculate drive usage | 	// Calculate drive usage | ||||||
| 	const usage = ((await DriveFile | 	const usage = await DriveFile | ||||||
| 		.aggregate([ | 		.aggregate([{ | ||||||
| 			{ $match: { 'metadata.userId': user._id } }, | 			$match: { | ||||||
| 			{ | 				'metadata.userId': user._id, | ||||||
|  | 				'metadata.deletedAt': { $exists: false } | ||||||
|  | 			} | ||||||
|  | 		}, { | ||||||
| 			$project: { | 			$project: { | ||||||
| 				length: true | 				length: true | ||||||
| 			} | 			} | ||||||
| 			}, | 		}, { | ||||||
| 			{ |  | ||||||
| 			$group: { | 			$group: { | ||||||
| 				_id: null, | 				_id: null, | ||||||
| 				usage: { $sum: '$length' } | 				usage: { $sum: '$length' } | ||||||
| 			} | 			} | ||||||
|  | 		}]) | ||||||
|  | 		.then((aggregates: any[]) => { | ||||||
|  | 			if (aggregates.length > 0) { | ||||||
|  | 				return aggregates[0].usage; | ||||||
| 			} | 			} | ||||||
| 		]))[0] || { | 			return 0; | ||||||
| 			usage: 0 | 		}); | ||||||
| 		}).usage; |  | ||||||
|  |  | ||||||
| 	res({ | 	res({ | ||||||
| 		capacity: user.driveCapacity, | 		capacity: user.driveCapacity, | ||||||
|   | |||||||
| @@ -37,10 +37,13 @@ module.exports = async (params, user, app) => { | |||||||
| 	const sort = { | 	const sort = { | ||||||
| 		_id: -1 | 		_id: -1 | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	const query = { | 	const query = { | ||||||
| 		'metadata.userId': user._id, | 		'metadata.userId': user._id, | ||||||
| 		'metadata.folderId': folderId | 		'metadata.folderId': folderId, | ||||||
|  | 		'metadata.deletedAt': { $exists: false } | ||||||
| 	} as any; | 	} as any; | ||||||
|  |  | ||||||
| 	if (sinceId) { | 	if (sinceId) { | ||||||
| 		sort._id = 1; | 		sort._id = 1; | ||||||
| 		query._id = { | 		query._id = { | ||||||
| @@ -51,6 +54,7 @@ module.exports = async (params, user, app) => { | |||||||
| 			$lt: untilId | 			$lt: untilId | ||||||
| 		}; | 		}; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if (type) { | 	if (type) { | ||||||
| 		query.contentType = new RegExp(`^${type.replace(/\*/g, '.+?')}$`); | 		query.contentType = new RegExp(`^${type.replace(/\*/g, '.+?')}$`); | ||||||
| 	} | 	} | ||||||
|   | |||||||
							
								
								
									
										32
									
								
								src/server/api/endpoints/drive/files/delete.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/server/api/endpoints/drive/files/delete.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | import $ from 'cafy'; import ID from '../../../../../cafy-id'; | ||||||
|  | import DriveFile from '../../../../../models/drive-file'; | ||||||
|  | import del from '../../../../../services/drive/delete-file'; | ||||||
|  | import { publishDriveStream } from '../../../../../publishers/stream'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Delete a file | ||||||
|  |  */ | ||||||
|  | module.exports = async (params, user) => { | ||||||
|  | 	// Get 'fileId' parameter | ||||||
|  | 	const [fileId, fileIdErr] = $.type(ID).get(params.fileId); | ||||||
|  | 	if (fileIdErr) throw 'invalid fileId param'; | ||||||
|  |  | ||||||
|  | 	// Fetch file | ||||||
|  | 	const file = await DriveFile | ||||||
|  | 		.findOne({ | ||||||
|  | 			_id: fileId, | ||||||
|  | 			'metadata.userId': user._id | ||||||
|  | 		}); | ||||||
|  |  | ||||||
|  | 	if (file === null) { | ||||||
|  | 		throw 'file-not-found'; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Delete | ||||||
|  | 	await del(file); | ||||||
|  |  | ||||||
|  | 	// Publish file_deleted event | ||||||
|  | 	publishDriveStream(user._id, 'file_deleted', file._id); | ||||||
|  |  | ||||||
|  | 	return; | ||||||
|  | }; | ||||||
| @@ -4,7 +4,7 @@ import * as debug from 'debug'; | |||||||
|  |  | ||||||
| import User, { IUser } from '../../../models/user'; | import User, { IUser } from '../../../models/user'; | ||||||
| import Mute from '../../../models/mute'; | import Mute from '../../../models/mute'; | ||||||
| import { pack as packNote } from '../../../models/note'; | import { pack as packNote, pack } from '../../../models/note'; | ||||||
| import readNotification from '../common/read-notification'; | import readNotification from '../common/read-notification'; | ||||||
| import call from '../call'; | import call from '../call'; | ||||||
| import { IApp } from '../../../models/app'; | import { IApp } from '../../../models/app'; | ||||||
| @@ -48,6 +48,14 @@ export default async function( | |||||||
| 					} | 					} | ||||||
| 					//#endregion | 					//#endregion | ||||||
|  |  | ||||||
|  | 					// Renoteなら再pack | ||||||
|  | 					if (x.type == 'note' && x.body.renoteId != null) { | ||||||
|  | 						x.body.renote = await pack(x.body.renoteId, user, { | ||||||
|  | 							detail: true | ||||||
|  | 						}); | ||||||
|  | 						data = JSON.stringify(x); | ||||||
|  | 					} | ||||||
|  |  | ||||||
| 					connection.send(data); | 					connection.send(data); | ||||||
| 				} catch (e) { | 				} catch (e) { | ||||||
| 					connection.send(data); | 					connection.send(data); | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ import * as redis from 'redis'; | |||||||
|  |  | ||||||
| import { IUser } from '../../../models/user'; | import { IUser } from '../../../models/user'; | ||||||
| import Mute from '../../../models/mute'; | import Mute from '../../../models/mute'; | ||||||
|  | import { pack } from '../../../models/note'; | ||||||
|  |  | ||||||
| export default async function( | export default async function( | ||||||
| 	request: websocket.request, | 	request: websocket.request, | ||||||
| @@ -31,6 +32,13 @@ export default async function( | |||||||
| 		} | 		} | ||||||
| 		//#endregion | 		//#endregion | ||||||
|  |  | ||||||
|  | 		// Renoteなら再pack | ||||||
|  | 		if (note.renoteId != null) { | ||||||
|  | 			note.renote = await pack(note.renoteId, user, { | ||||||
|  | 				detail: true | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		connection.send(JSON.stringify({ | 		connection.send(JSON.stringify({ | ||||||
| 			type: 'note', | 			type: 'note', | ||||||
| 			body: note | 			body: note | ||||||
|   | |||||||
| @@ -9,13 +9,14 @@ import * as debug from 'debug'; | |||||||
| import fileType = require('file-type'); | import fileType = require('file-type'); | ||||||
| import prominence = require('prominence'); | import prominence = require('prominence'); | ||||||
|  |  | ||||||
| import DriveFile, { IMetadata, getDriveFileBucket, IDriveFile, DriveFileChunk } from '../../models/drive-file'; | import DriveFile, { IMetadata, getDriveFileBucket, IDriveFile } from '../../models/drive-file'; | ||||||
| import DriveFolder from '../../models/drive-folder'; | import DriveFolder from '../../models/drive-folder'; | ||||||
| import { pack } from '../../models/drive-file'; | import { pack } from '../../models/drive-file'; | ||||||
| import event, { publishDriveStream } from '../../publishers/stream'; | import event, { publishDriveStream } from '../../publishers/stream'; | ||||||
| import { isLocalUser, IUser, IRemoteUser } from '../../models/user'; | import { isLocalUser, IUser, IRemoteUser } from '../../models/user'; | ||||||
| import DriveFileThumbnail, { getDriveFileThumbnailBucket, DriveFileThumbnailChunk } from '../../models/drive-file-thumbnail'; | import { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail'; | ||||||
| import genThumbnail from '../../drive/gen-thumbnail'; | import genThumbnail from '../../drive/gen-thumbnail'; | ||||||
|  | import delFile from './delete-file'; | ||||||
|  |  | ||||||
| const gm = _gm.subClass({ | const gm = _gm.subClass({ | ||||||
| 	imageMagick: true | 	imageMagick: true | ||||||
| @@ -58,31 +59,7 @@ async function deleteOldFile(user: IRemoteUser) { | |||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| 	if (oldFile) { | 	if (oldFile) { | ||||||
| 		// チャンクをすべて削除 | 		delFile(oldFile, true); | ||||||
| 		DriveFileChunk.remove({ |  | ||||||
| 			files_id: oldFile._id |  | ||||||
| 		}); |  | ||||||
|  |  | ||||||
| 		DriveFile.update({ _id: oldFile._id }, { |  | ||||||
| 			$set: { |  | ||||||
| 				'metadata.deletedAt': new Date(), |  | ||||||
| 				'metadata.isExpired': true |  | ||||||
| 			} |  | ||||||
| 		}); |  | ||||||
|  |  | ||||||
| 		//#region サムネイルもあれば削除 |  | ||||||
| 		const thumbnail = await DriveFileThumbnail.findOne({ |  | ||||||
| 			'metadata.originalId': oldFile._id |  | ||||||
| 		}); |  | ||||||
|  |  | ||||||
| 		if (thumbnail) { |  | ||||||
| 			DriveFileThumbnailChunk.remove({ |  | ||||||
| 				files_id: thumbnail._id |  | ||||||
| 			}); |  | ||||||
|  |  | ||||||
| 			DriveFileThumbnail.remove({ _id: thumbnail._id }); |  | ||||||
| 		} |  | ||||||
| 		//#endregion |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										30
									
								
								src/services/drive/delete-file.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/services/drive/delete-file.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | |||||||
|  | import DriveFile, { DriveFileChunk, IDriveFile } from "../../models/drive-file"; | ||||||
|  | import DriveFileThumbnail, { DriveFileThumbnailChunk } from '../../models/drive-file-thumbnail'; | ||||||
|  |  | ||||||
|  | export default async function(file: IDriveFile, isExpired = false) { | ||||||
|  | 	// チャンクをすべて削除 | ||||||
|  | 	await DriveFileChunk.remove({ | ||||||
|  | 		files_id: file._id | ||||||
|  | 	}); | ||||||
|  |  | ||||||
|  | 	await DriveFile.update({ _id: file._id }, { | ||||||
|  | 		$set: { | ||||||
|  | 			'metadata.deletedAt': new Date(), | ||||||
|  | 			'metadata.isExpired': isExpired | ||||||
|  | 		} | ||||||
|  | 	}); | ||||||
|  |  | ||||||
|  | 	//#region サムネイルもあれば削除 | ||||||
|  | 	const thumbnail = await DriveFileThumbnail.findOne({ | ||||||
|  | 		'metadata.originalId': file._id | ||||||
|  | 	}); | ||||||
|  |  | ||||||
|  | 	if (thumbnail) { | ||||||
|  | 		await DriveFileThumbnailChunk.remove({ | ||||||
|  | 			files_id: thumbnail._id | ||||||
|  | 		}); | ||||||
|  |  | ||||||
|  | 		await DriveFileThumbnail.remove({ _id: thumbnail._id }); | ||||||
|  | 	} | ||||||
|  | 	//#endregion | ||||||
|  | } | ||||||
| @@ -204,6 +204,62 @@ export default async (user: IUser, data: { | |||||||
| 		return packAp(content); | 		return packAp(content); | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
|  | 	//#region メンション | ||||||
|  | 	if (data.text) { | ||||||
|  | 		// TODO: Drop dupulicates | ||||||
|  | 		const mentionTokens = tokens | ||||||
|  | 			.filter(t => t.type == 'mention'); | ||||||
|  |  | ||||||
|  | 		// TODO: Drop dupulicates | ||||||
|  | 		const mentionedUsers = (await Promise.all(mentionTokens.map(async m => { | ||||||
|  | 			try { | ||||||
|  | 				return await resolveUser(m.username, m.host); | ||||||
|  | 			} catch (e) { | ||||||
|  | 				return null; | ||||||
|  | 			} | ||||||
|  | 		}))).filter(x => x != null); | ||||||
|  |  | ||||||
|  | 		// Append mentions data | ||||||
|  | 		if (mentionedUsers.length > 0) { | ||||||
|  | 			const set = { | ||||||
|  | 				mentions: mentionedUsers.map(u => u._id), | ||||||
|  | 				mentionedRemoteUsers: mentionedUsers.filter(u => isRemoteUser(u)).map(u => ({ | ||||||
|  | 					uri: (u as IRemoteUser).uri, | ||||||
|  | 					username: u.username, | ||||||
|  | 					host: u.host | ||||||
|  | 				})) | ||||||
|  | 			}; | ||||||
|  |  | ||||||
|  | 			Note.update({ _id: note._id }, { | ||||||
|  | 				$set: set | ||||||
|  | 			}); | ||||||
|  |  | ||||||
|  | 			Object.assign(note, set); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		mentionedUsers.filter(u => isLocalUser(u)).forEach(async u => { | ||||||
|  | 			event(u, 'mention', noteObj); | ||||||
|  |  | ||||||
|  | 			// 既に言及されたユーザーに対する返信や引用renoteの場合も無視 | ||||||
|  | 			if (data.reply && data.reply.userId.equals(u._id)) return; | ||||||
|  | 			if (data.renote && data.renote.userId.equals(u._id)) return; | ||||||
|  |  | ||||||
|  | 			// Create notification | ||||||
|  | 			notify(u._id, user._id, 'mention', { | ||||||
|  | 				noteId: note._id | ||||||
|  | 			}); | ||||||
|  |  | ||||||
|  | 			nm.push(u._id, 'mention'); | ||||||
|  | 		}); | ||||||
|  |  | ||||||
|  | 		if (isLocalUser(user)) { | ||||||
|  | 			mentionedUsers.filter(u => isRemoteUser(u)).forEach(async u => { | ||||||
|  | 				deliver(user, await render(), (u as IRemoteUser).inbox); | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	//#endregion | ||||||
|  |  | ||||||
| 	if (!silent) { | 	if (!silent) { | ||||||
| 		if (isLocalUser(user)) { | 		if (isLocalUser(user)) { | ||||||
| 			if (note.visibility == 'private' || note.visibility == 'followers' || note.visibility == 'specified') { | 			if (note.visibility == 'private' || note.visibility == 'followers' || note.visibility == 'specified') { | ||||||
| @@ -287,55 +343,6 @@ export default async (user: IUser, data: { | |||||||
| 	} | 	} | ||||||
| 	//#endergion | 	//#endergion | ||||||
|  |  | ||||||
| 	//#region メンション |  | ||||||
| 	if (data.text) { |  | ||||||
| 		// TODO: Drop dupulicates |  | ||||||
| 		const mentions = tokens |  | ||||||
| 			.filter(t => t.type == 'mention'); |  | ||||||
|  |  | ||||||
| 		let mentionedUsers = await Promise.all(mentions.map(async m => { |  | ||||||
| 			try { |  | ||||||
| 				return await resolveUser(m.username, m.host); |  | ||||||
| 			} catch (e) { |  | ||||||
| 				return null; |  | ||||||
| 			} |  | ||||||
| 		})); |  | ||||||
|  |  | ||||||
| 		// TODO: Drop dupulicates |  | ||||||
| 		mentionedUsers = mentionedUsers.filter(x => x != null); |  | ||||||
|  |  | ||||||
| 		mentionedUsers.filter(u => isLocalUser(u)).forEach(async u => { |  | ||||||
| 			event(u, 'mention', noteObj); |  | ||||||
|  |  | ||||||
| 			// 既に言及されたユーザーに対する返信や引用renoteの場合も無視 |  | ||||||
| 			if (data.reply && data.reply.userId.equals(u._id)) return; |  | ||||||
| 			if (data.renote && data.renote.userId.equals(u._id)) return; |  | ||||||
|  |  | ||||||
| 			// Create notification |  | ||||||
| 			notify(u._id, user._id, 'mention', { |  | ||||||
| 				noteId: note._id |  | ||||||
| 			}); |  | ||||||
|  |  | ||||||
| 			nm.push(u._id, 'mention'); |  | ||||||
| 		}); |  | ||||||
|  |  | ||||||
| 		if (isLocalUser(user)) { |  | ||||||
| 			mentionedUsers.filter(u => isRemoteUser(u)).forEach(async u => { |  | ||||||
| 				deliver(user, await render(), (u as IRemoteUser).inbox); |  | ||||||
| 			}); |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		// Append mentions data |  | ||||||
| 		if (mentionedUsers.length > 0) { |  | ||||||
| 			Note.update({ _id: note._id }, { |  | ||||||
| 				$set: { |  | ||||||
| 					mentions: mentionedUsers.map(u => u._id) |  | ||||||
| 				} |  | ||||||
| 			}); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	//#endregion |  | ||||||
|  |  | ||||||
| 	// If has in reply to note | 	// If has in reply to note | ||||||
| 	if (data.reply) { | 	if (data.reply) { | ||||||
| 		// Increment replies count | 		// Increment replies count | ||||||
|   | |||||||
| @@ -20,6 +20,7 @@ export default async function(user: IUser, note: INote) { | |||||||
| 		$set: { | 		$set: { | ||||||
| 			deletedAt: new Date(), | 			deletedAt: new Date(), | ||||||
| 			text: null, | 			text: null, | ||||||
|  | 			tags: [], | ||||||
| 			mediaIds: [], | 			mediaIds: [], | ||||||
| 			poll: null | 			poll: null | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -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, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user