Merge branch 'develop' into improve-media
This commit is contained in:
		| @@ -18,6 +18,8 @@ | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	const langs = LANGS; | ||||
|  | ||||
| 	//#region Load settings | ||||
| 	let settings = null; | ||||
| 	const vuex = localStorage.getItem('vuex'); | ||||
| @@ -40,10 +42,10 @@ | ||||
| 	//#region Detect the user language | ||||
| 	let lang = null; | ||||
|  | ||||
| 	if (LANGS.includes(navigator.language)) { | ||||
| 	if (langs.includes(navigator.language)) { | ||||
| 		lang = navigator.language; | ||||
| 	} else { | ||||
| 		lang = LANGS.find(x => x.split('-')[0] == navigator.language); | ||||
| 		lang = langs.find(x => x.split('-')[0] == navigator.language); | ||||
|  | ||||
| 		if (lang == null) { | ||||
| 			// Fallback | ||||
| @@ -52,7 +54,7 @@ | ||||
| 	} | ||||
|  | ||||
| 	if (settings && settings.device.lang && | ||||
| 		LANGS.includes(settings.device.lang)) { | ||||
| 		langs.includes(settings.device.lang)) { | ||||
| 		lang = settings.device.lang; | ||||
| 	} | ||||
| 	//#endregion | ||||
| @@ -140,7 +142,7 @@ | ||||
| 		// Random | ||||
| 		localStorage.setItem('salt', Math.random().toString()); | ||||
|  | ||||
| 		// Clear cache (serive worker) | ||||
| 		// Clear cache (service worker) | ||||
| 		try { | ||||
| 			navigator.serviceWorker.controller.postMessage('clear'); | ||||
|  | ||||
|   | ||||
| @@ -9,7 +9,7 @@ export default async function(mios: MiOS, force = false, silent = false) { | ||||
| 		localStorage.setItem('should-refresh', 'true'); | ||||
| 		localStorage.setItem('v', newer); | ||||
|  | ||||
| 		// Clear cache (serive worker) | ||||
| 		// Clear cache (service worker) | ||||
| 		try { | ||||
| 			if (navigator.serviceWorker.controller) { | ||||
| 				navigator.serviceWorker.controller.postMessage('clear'); | ||||
|   | ||||
| @@ -1,2 +0,0 @@ | ||||
| const gcd = (a, b) => !b ? a : gcd(b, a % b); | ||||
| export default gcd; | ||||
| @@ -1,53 +0,0 @@ | ||||
| export default function(qs: string) { | ||||
| 	const q = { | ||||
| 		text: '' | ||||
| 	}; | ||||
|  | ||||
| 	qs.split(' ').forEach(x => { | ||||
| 		if (/^([a-z_]+?):(.+?)$/.test(x)) { | ||||
| 			const [key, value] = x.split(':'); | ||||
| 			switch (key) { | ||||
| 				case 'user': | ||||
| 					q['includeUserUsernames'] = value.split(','); | ||||
| 					break; | ||||
| 				case 'exclude_user': | ||||
| 					q['excludeUserUsernames'] = value.split(','); | ||||
| 					break; | ||||
| 				case 'follow': | ||||
| 					q['following'] = value == 'null' ? null : value == 'true'; | ||||
| 					break; | ||||
| 				case 'reply': | ||||
| 					q['reply'] = value == 'null' ? null : value == 'true'; | ||||
| 					break; | ||||
| 				case 'renote': | ||||
| 					q['renote'] = value == 'null' ? null : value == 'true'; | ||||
| 					break; | ||||
| 				case 'media': | ||||
| 					q['media'] = value == 'null' ? null : value == 'true'; | ||||
| 					break; | ||||
| 				case 'poll': | ||||
| 					q['poll'] = value == 'null' ? null : value == 'true'; | ||||
| 					break; | ||||
| 				case 'until': | ||||
| 				case 'since': | ||||
| 					// YYYY-MM-DD | ||||
| 					if (/^[0-9]+\-[0-9]+\-[0-9]+$/) { | ||||
| 						const [yyyy, mm, dd] = value.split('-'); | ||||
| 						q[`${key}_date`] = (new Date(parseInt(yyyy, 10), parseInt(mm, 10) - 1, parseInt(dd, 10))).getTime(); | ||||
| 					} | ||||
| 					break; | ||||
| 				default: | ||||
| 					q[key] = value; | ||||
| 					break; | ||||
| 			} | ||||
| 		} else { | ||||
| 			q.text += x + ' '; | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
| 	if (q.text) { | ||||
| 		q.text = q.text.trim(); | ||||
| 	} | ||||
|  | ||||
| 	return q; | ||||
| } | ||||
| @@ -1,6 +1,7 @@ | ||||
| import { EventEmitter } from 'eventemitter3'; | ||||
| import * as uuid from 'uuid'; | ||||
| import Connection from './stream'; | ||||
| import { erase } from '../../../../../prelude/array'; | ||||
|  | ||||
| /** | ||||
|  * ストリーム接続を管理するクラス | ||||
| @@ -89,7 +90,7 @@ export default abstract class StreamManager<T extends Connection> extends EventE | ||||
| 	 * @param userId use で発行したユーザーID | ||||
| 	 */ | ||||
| 	public dispose(userId) { | ||||
| 		this.users = this.users.filter(id => id != userId); | ||||
| 		this.users = erase(userId, this.users); | ||||
|  | ||||
| 		this._connection.user = `Managed (${ this.users.length })`; | ||||
|  | ||||
|   | ||||
| @@ -1,19 +1,25 @@ | ||||
| <template> | ||||
| <span class="mk-acct"> | ||||
| 	<span class="name">@{{ user.username }}</span> | ||||
| 	<span class="host" v-if="user.host">@{{ user.host }}</span> | ||||
| 	<span class="host" :class="{ fade: $store.state.settings.contrastedAcct }" v-if="user.host || detail || $store.state.settings.showFullAcct">@{{ user.host || host }}</span> | ||||
| </span> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import { host } from '../../../config'; | ||||
| export default Vue.extend({ | ||||
| 	props: ['user'] | ||||
| 	props: ['user', 'detail'], | ||||
| 	data() { | ||||
| 		return { | ||||
| 			host | ||||
| 		}; | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| .mk-acct | ||||
| 	> .host | ||||
| 	> .host.fade | ||||
| 		opacity 0.5 | ||||
| </style> | ||||
|   | ||||
| @@ -1,15 +1,15 @@ | ||||
| <template> | ||||
| 	<span class="mk-avatar" :class="{ cat }" :title="user | acct" v-if="disableLink && !disablePreview" v-user-preview="user.id" @click="onClick"> | ||||
| 		<span class="inner" :style="style"></span> | ||||
| 	<span class="mk-avatar" :style="style" :class="{ cat }" :title="user | acct" v-if="disableLink && !disablePreview" v-user-preview="user.id" @click="onClick"> | ||||
| 		<span class="inner" :style="icon"></span> | ||||
| 	</span> | ||||
| 	<span class="mk-avatar" :class="{ cat }" :title="user | acct" v-else-if="disableLink && disablePreview" @click="onClick"> | ||||
| 		<span class="inner" :style="style"></span> | ||||
| 	<span class="mk-avatar" :style="style" :class="{ cat }" :title="user | acct" v-else-if="disableLink && disablePreview" @click="onClick"> | ||||
| 		<span class="inner" :style="icon"></span> | ||||
| 	</span> | ||||
| 	<router-link class="mk-avatar" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && !disablePreview" v-user-preview="user.id"> | ||||
| 		<span class="inner" :style="style"></span> | ||||
| 	<router-link class="mk-avatar" :style="style" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && !disablePreview" v-user-preview="user.id"> | ||||
| 		<span class="inner" :style="icon"></span> | ||||
| 	</router-link> | ||||
| 	<router-link class="mk-avatar" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && disablePreview"> | ||||
| 		<span class="inner" :style="style"></span> | ||||
| 	<router-link class="mk-avatar" :style="style" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && disablePreview"> | ||||
| 		<span class="inner" :style="icon"></span> | ||||
| 	</router-link> | ||||
| </template> | ||||
|  | ||||
| @@ -42,6 +42,11 @@ export default Vue.extend({ | ||||
| 			return this.user.isCat && this.$store.state.settings.circleIcons; | ||||
| 		}, | ||||
| 		style(): any { | ||||
| 			return { | ||||
| 				borderRadius: this.$store.state.settings.circleIcons ? '100%' : null | ||||
| 			}; | ||||
| 		}, | ||||
| 		icon(): any { | ||||
| 			return { | ||||
| 				backgroundColor: this.lightmode | ||||
| 					? `rgb(${this.user.avatarColor.slice(0, 3).join(',')})` | ||||
|   | ||||
							
								
								
									
										44
									
								
								src/client/app/common/views/components/cw-button.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/client/app/common/views/components/cw-button.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| <template> | ||||
| <button class="nrvgflfuaxwgkxoynpnumyookecqrrvh" @click="toggle">{{ value ? '%i18n:@hide%' : '%i18n:@show%' }}</button> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	props: { | ||||
| 		value: { | ||||
| 			type: Boolean, | ||||
| 			required: true | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		toggle() { | ||||
| 			this.$emit('input', !this.value); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| root(isDark) | ||||
| 	display inline-block | ||||
| 	padding 4px 8px | ||||
| 	font-size 0.7em | ||||
| 	color isDark ? #393f4f : #fff | ||||
| 	background isDark ? #687390 : #b1b9c1 | ||||
| 	border-radius 2px | ||||
| 	cursor pointer | ||||
| 	user-select none | ||||
|  | ||||
| 	&:hover | ||||
| 		background isDark ? #707b97 : #bbc4ce | ||||
|  | ||||
| .nrvgflfuaxwgkxoynpnumyookecqrrvh[data-darkmode] | ||||
| 	root(true) | ||||
|  | ||||
| .nrvgflfuaxwgkxoynpnumyookecqrrvh:not([data-darkmode]) | ||||
| 	root(false) | ||||
|  | ||||
| </style> | ||||
| @@ -50,15 +50,15 @@ | ||||
| 	</div> | ||||
|  | ||||
| 	<div class="player" v-if="game.isEnded"> | ||||
| 		<el-button-group> | ||||
| 			<el-button type="primary" @click="logPos = 0" :disabled="logPos == 0">%fa:angle-double-left%</el-button> | ||||
| 			<el-button type="primary" @click="logPos--" :disabled="logPos == 0">%fa:angle-left%</el-button> | ||||
| 		</el-button-group> | ||||
| 		<div> | ||||
| 			<button @click="logPos = 0" :disabled="logPos == 0">%fa:angle-double-left%</button> | ||||
| 			<button @click="logPos--" :disabled="logPos == 0">%fa:angle-left%</button> | ||||
| 		</div> | ||||
| 		<span>{{ logPos }} / {{ logs.length }}</span> | ||||
| 		<el-button-group> | ||||
| 			<el-button type="primary" @click="logPos++" :disabled="logPos == logs.length">%fa:angle-right%</el-button> | ||||
| 			<el-button type="primary" @click="logPos = logs.length" :disabled="logPos == logs.length">%fa:angle-double-right%</el-button> | ||||
| 		</el-button-group> | ||||
| 		<div> | ||||
| 			<button @click="logPos++" :disabled="logPos == logs.length">%fa:angle-right%</button> | ||||
| 			<button @click="logPos = logs.length" :disabled="logPos == logs.length">%fa:angle-double-right%</button> | ||||
| 		</div> | ||||
| 	</div> | ||||
|  | ||||
| 	<div class="info"> | ||||
|   | ||||
| @@ -3,7 +3,6 @@ | ||||
| 	<h1>%i18n:@title%</h1> | ||||
| 	<p>%i18n:@sub-title%</p> | ||||
| 	<div class="play"> | ||||
| 		<!--<el-button round>フリーマッチ(準備中)</el-button>--> | ||||
| 		<form-button primary round @click="match">%i18n:@invite%</form-button> | ||||
| 		<details> | ||||
| 			<summary>%i18n:@rule%</summary> | ||||
|   | ||||
| @@ -59,11 +59,6 @@ | ||||
| 			</header> | ||||
|  | ||||
| 			<div> | ||||
| 				<el-alert v-for="message in messages" | ||||
| 						:title="message.text" | ||||
| 						:type="message.type" | ||||
| 						:key="message.id"/> | ||||
|  | ||||
| 				<template v-for="item in form"> | ||||
| 					<mk-switch v-if="item.type == 'switch'" v-model="item.value" :key="item.id" :text="item.label" @change="onChangeForm(item)">{{ item.desc || '' }}</mk-switch> | ||||
|  | ||||
| @@ -93,7 +88,7 @@ | ||||
| 						</header> | ||||
|  | ||||
| 						<div> | ||||
| 							<el-input v-model="item.value" @change="onChangeForm(item)"/> | ||||
| 							<input v-model="item.value" @change="onChangeForm(item)"/> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</template> | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| import cwButton from './cw-button.vue'; | ||||
| import tagCloud from './tag-cloud.vue'; | ||||
| import trends from './trends.vue'; | ||||
| import analogClock from './analog-clock.vue'; | ||||
| import menu from './menu.vue'; | ||||
| @@ -42,6 +44,8 @@ import uiSelect from './ui/select.vue'; | ||||
| import formButton from './ui/form/button.vue'; | ||||
| import formRadio from './ui/form/radio.vue'; | ||||
|  | ||||
| Vue.component('mk-cw-button', cwButton); | ||||
| Vue.component('mk-tag-cloud', tagCloud); | ||||
| Vue.component('mk-trends', trends); | ||||
| Vue.component('mk-analog-clock', analogClock); | ||||
| Vue.component('mk-menu', menu); | ||||
|   | ||||
| @@ -108,7 +108,7 @@ export default Vue.extend({ | ||||
| 				easing: 'easeInBack', | ||||
| 				complete: () => { | ||||
| 					this.$emit('closed'); | ||||
| 					this.$destroy(); | ||||
| 					this.destroyDom(); | ||||
| 				} | ||||
| 			}); | ||||
| 		} | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import Vue from 'vue'; | ||||
| import Vue, { VNode } from 'vue'; | ||||
| import * as emojilib from 'emojilib'; | ||||
| import { length } from 'stringz'; | ||||
| import parse from '../../../../../mfm/parse'; | ||||
| @@ -6,10 +6,7 @@ import getAcct from '../../../../../misc/acct/render'; | ||||
| import { url } from '../../../config'; | ||||
| import MkUrl from './url.vue'; | ||||
| import MkGoogle from './google.vue'; | ||||
|  | ||||
| const flatten = list => list.reduce( | ||||
| 	(a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), [] | ||||
| ); | ||||
| import { concat } from '../../../../../prelude/array'; | ||||
|  | ||||
| export default Vue.component('misskey-flavored-markdown', { | ||||
| 	props: { | ||||
| @@ -32,20 +29,20 @@ export default Vue.component('misskey-flavored-markdown', { | ||||
| 	}, | ||||
|  | ||||
| 	render(createElement) { | ||||
| 		let ast; | ||||
| 		let ast: any[]; | ||||
|  | ||||
| 		if (this.ast == null) { | ||||
| 			// Parse text to ast | ||||
| 			ast = parse(this.text); | ||||
| 		} else { | ||||
| 			ast = this.ast; | ||||
| 			ast = this.ast as any[]; | ||||
| 		} | ||||
|  | ||||
| 		let bigCount = 0; | ||||
| 		let motionCount = 0; | ||||
|  | ||||
| 		// Parse ast to DOM | ||||
| 		const els = flatten(ast.map(token => { | ||||
| 		const els = concat(ast.map((token): VNode[] => { | ||||
| 			switch (token.type) { | ||||
| 				case 'text': { | ||||
| 					const text = token.content.replace(/(\r\n|\n|\r)/g, '\n'); | ||||
| @@ -56,12 +53,12 @@ export default Vue.component('misskey-flavored-markdown', { | ||||
| 						x[x.length - 1].pop(); | ||||
| 						return x; | ||||
| 					} else { | ||||
| 						return createElement('span', text.replace(/\n/g, ' ')); | ||||
| 						return [createElement('span', text.replace(/\n/g, ' '))]; | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 				case 'bold': { | ||||
| 					return createElement('b', token.bold); | ||||
| 					return [createElement('b', token.bold)]; | ||||
| 				} | ||||
|  | ||||
| 				case 'big': { | ||||
| @@ -95,23 +92,23 @@ export default Vue.component('misskey-flavored-markdown', { | ||||
| 				} | ||||
|  | ||||
| 				case 'url': { | ||||
| 					return createElement(MkUrl, { | ||||
| 					return [createElement(MkUrl, { | ||||
| 						props: { | ||||
| 							url: token.content, | ||||
| 							target: '_blank' | ||||
| 						} | ||||
| 					}); | ||||
| 					})]; | ||||
| 				} | ||||
|  | ||||
| 				case 'link': { | ||||
| 					return createElement('a', { | ||||
| 					return [createElement('a', { | ||||
| 						attrs: { | ||||
| 							class: 'link', | ||||
| 							href: token.url, | ||||
| 							target: '_blank', | ||||
| 							title: token.url | ||||
| 						} | ||||
| 					}, token.title); | ||||
| 					}, token.title)]; | ||||
| 				} | ||||
|  | ||||
| 				case 'mention': { | ||||
| @@ -129,16 +126,16 @@ export default Vue.component('misskey-flavored-markdown', { | ||||
| 				} | ||||
|  | ||||
| 				case 'hashtag': { | ||||
| 					return createElement('a', { | ||||
| 					return [createElement('a', { | ||||
| 						attrs: { | ||||
| 							href: `${url}/tags/${encodeURIComponent(token.hashtag)}`, | ||||
| 							target: '_blank' | ||||
| 						} | ||||
| 					}, token.content); | ||||
| 					}, token.content)]; | ||||
| 				} | ||||
|  | ||||
| 				case 'code': { | ||||
| 					return createElement('pre', { | ||||
| 					return [createElement('pre', { | ||||
| 						class: 'code' | ||||
| 					}, [ | ||||
| 						createElement('code', { | ||||
| @@ -146,15 +143,15 @@ export default Vue.component('misskey-flavored-markdown', { | ||||
| 								innerHTML: token.html | ||||
| 							} | ||||
| 						}) | ||||
| 					]); | ||||
| 					])]; | ||||
| 				} | ||||
|  | ||||
| 				case 'inline-code': { | ||||
| 					return createElement('code', { | ||||
| 					return [createElement('code', { | ||||
| 						domProps: { | ||||
| 							innerHTML: token.html | ||||
| 						} | ||||
| 					}); | ||||
| 					})]; | ||||
| 				} | ||||
|  | ||||
| 				case 'quote': { | ||||
| @@ -164,43 +161,45 @@ export default Vue.component('misskey-flavored-markdown', { | ||||
| 						const x = text2.split('\n') | ||||
| 							.map(t => [createElement('span', t), createElement('br')]); | ||||
| 						x[x.length - 1].pop(); | ||||
| 						return createElement('div', { | ||||
| 						return [createElement('div', { | ||||
| 							attrs: { | ||||
| 								class: 'quote' | ||||
| 							} | ||||
| 						}, x); | ||||
| 						}, x)]; | ||||
| 					} else { | ||||
| 						return createElement('span', { | ||||
| 						return [createElement('span', { | ||||
| 							attrs: { | ||||
| 								class: 'quote' | ||||
| 							} | ||||
| 						}, text2.replace(/\n/g, ' ')); | ||||
| 						}, text2.replace(/\n/g, ' '))]; | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 				case 'title': { | ||||
| 					return createElement('div', { | ||||
| 					return [createElement('div', { | ||||
| 						attrs: { | ||||
| 							class: 'title' | ||||
| 						} | ||||
| 					}, token.title); | ||||
| 					}, token.title)]; | ||||
| 				} | ||||
|  | ||||
| 				case 'emoji': { | ||||
| 					const emoji = emojilib.lib[token.emoji]; | ||||
| 					return createElement('span', emoji ? emoji.char : token.content); | ||||
| 					return [createElement('span', emoji ? emoji.char : token.content)]; | ||||
| 				} | ||||
|  | ||||
| 				case 'search': { | ||||
| 					return createElement(MkGoogle, { | ||||
| 					return [createElement(MkGoogle, { | ||||
| 						props: { | ||||
| 							q: token.query | ||||
| 						} | ||||
| 					}); | ||||
| 					})]; | ||||
| 				} | ||||
|  | ||||
| 				default: { | ||||
| 					console.log('unknown ast type:', token.type); | ||||
|  | ||||
| 					return []; | ||||
| 				} | ||||
| 			} | ||||
| 		})); | ||||
|   | ||||
| @@ -64,7 +64,7 @@ export default Vue.extend({ | ||||
| 			(this as any).api('i/pin', { | ||||
| 				noteId: this.note.id | ||||
| 			}).then(() => { | ||||
| 				this.$destroy(); | ||||
| 				this.destroyDom(); | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| @@ -73,7 +73,7 @@ export default Vue.extend({ | ||||
| 			(this as any).api('notes/delete', { | ||||
| 				noteId: this.note.id | ||||
| 			}).then(() => { | ||||
| 				this.$destroy(); | ||||
| 				this.destroyDom(); | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| @@ -81,13 +81,13 @@ export default Vue.extend({ | ||||
| 			(this as any).api('notes/favorites/create', { | ||||
| 				noteId: this.note.id | ||||
| 			}).then(() => { | ||||
| 				this.$destroy(); | ||||
| 				this.destroyDom(); | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		closed() { | ||||
| 			this.$nextTick(() => { | ||||
| 				this.$destroy(); | ||||
| 				this.destroyDom(); | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -20,6 +20,7 @@ | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import { erase } from '../../../../../prelude/array'; | ||||
| export default Vue.extend({ | ||||
| 	data() { | ||||
| 		return { | ||||
| @@ -53,7 +54,7 @@ export default Vue.extend({ | ||||
|  | ||||
| 		get() { | ||||
| 			return { | ||||
| 				choices: this.choices.filter(choice => choice != '') | ||||
| 				choices: erase('', this.choices) | ||||
| 			} | ||||
| 		}, | ||||
|  | ||||
|   | ||||
| @@ -21,6 +21,7 @@ | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import { sum } from '../../../../../prelude/array'; | ||||
| export default Vue.extend({ | ||||
| 	props: ['note'], | ||||
| 	data() { | ||||
| @@ -33,7 +34,7 @@ export default Vue.extend({ | ||||
| 			return this.note.poll; | ||||
| 		}, | ||||
| 		total(): number { | ||||
| 			return this.poll.choices.reduce((a, b) => a + b.votes, 0); | ||||
| 			return sum(this.poll.choices.map(x => x.votes)); | ||||
| 		}, | ||||
| 		isVoted(): boolean { | ||||
| 			return this.poll.choices.some(c => c.isVoted); | ||||
|   | ||||
| @@ -1,17 +1,17 @@ | ||||
| <template> | ||||
| <span class="mk-reaction-icon"> | ||||
| 	<img v-if="reaction == 'like'" src="/assets/reactions/like.png" alt="%i18n:common.reactions.like%"> | ||||
| 	<img v-if="reaction == 'love'" src="/assets/reactions/love.png" alt="%i18n:common.reactions.love%"> | ||||
| 	<img v-if="reaction == 'laugh'" src="/assets/reactions/laugh.png" alt="%i18n:common.reactions.laugh%"> | ||||
| 	<img v-if="reaction == 'hmm'" src="/assets/reactions/hmm.png" alt="%i18n:common.reactions.hmm%"> | ||||
| 	<img v-if="reaction == 'surprise'" src="/assets/reactions/surprise.png" alt="%i18n:common.reactions.surprise%"> | ||||
| 	<img v-if="reaction == 'congrats'" src="/assets/reactions/congrats.png" alt="%i18n:common.reactions.congrats%"> | ||||
| 	<img v-if="reaction == 'angry'" src="/assets/reactions/angry.png" alt="%i18n:common.reactions.angry%"> | ||||
| 	<img v-if="reaction == 'confused'" src="/assets/reactions/confused.png" alt="%i18n:common.reactions.confused%"> | ||||
| 	<img v-if="reaction == 'rip'" src="/assets/reactions/rip.png" alt="%i18n:common.reactions.rip%"> | ||||
| 	<img v-if="reaction == 'like'" src="https://twemoji.maxcdn.com/2/svg/1f44d.svg" alt="%i18n:common.reactions.like%"> | ||||
| 	<img v-if="reaction == 'love'" src="https://twemoji.maxcdn.com/2/svg/2764.svg" alt="%i18n:common.reactions.love%"> | ||||
| 	<img v-if="reaction == 'laugh'" src="https://twemoji.maxcdn.com/2/svg/1f606.svg" alt="%i18n:common.reactions.laugh%"> | ||||
| 	<img v-if="reaction == 'hmm'" src="https://twemoji.maxcdn.com/2/svg/1f914.svg" alt="%i18n:common.reactions.hmm%"> | ||||
| 	<img v-if="reaction == 'surprise'" src="https://twemoji.maxcdn.com/2/svg/1f62e.svg" alt="%i18n:common.reactions.surprise%"> | ||||
| 	<img v-if="reaction == 'congrats'" src="https://twemoji.maxcdn.com/2/svg/1f389.svg" alt="%i18n:common.reactions.congrats%"> | ||||
| 	<img v-if="reaction == 'angry'" src="https://twemoji.maxcdn.com/2/svg/1f4a2.svg" alt="%i18n:common.reactions.angry%"> | ||||
| 	<img v-if="reaction == 'confused'" src="https://twemoji.maxcdn.com/2/svg/1f625.svg" alt="%i18n:common.reactions.confused%"> | ||||
| 	<img v-if="reaction == 'rip'" src="https://twemoji.maxcdn.com/2/svg/1f607.svg" alt="%i18n:common.reactions.rip%"> | ||||
| 	<template v-if="reaction == 'pudding'"> | ||||
| 		<img v-if="$store.getters.isSignedIn && $store.state.settings.iLikeSushi" src="/assets/reactions/sushi.png" alt="%i18n:common.reactions.pudding%"> | ||||
| 		<img v-else src="/assets/reactions/pudding.png" alt="%i18n:common.reactions.pudding%"> | ||||
| 		<img v-if="$store.getters.isSignedIn && $store.state.settings.iLikeSushi" src="https://twemoji.maxcdn.com/2/svg/1f363.svg" alt="%i18n:common.reactions.pudding%"> | ||||
| 		<img v-else src="https://twemoji.maxcdn.com/2/svg/1f36e.svg" alt="%i18n:common.reactions.pudding%"> | ||||
| 	</template> | ||||
| </span> | ||||
| </template> | ||||
|   | ||||
| @@ -95,7 +95,7 @@ export default Vue.extend({ | ||||
| 				reaction: reaction | ||||
| 			}).then(() => { | ||||
| 				if (this.cb) this.cb(); | ||||
| 				this.$destroy(); | ||||
| 				this.destroyDom(); | ||||
| 			}); | ||||
| 		}, | ||||
| 		onMouseover(e) { | ||||
| @@ -120,7 +120,7 @@ export default Vue.extend({ | ||||
| 				scale: 0.5, | ||||
| 				duration: 200, | ||||
| 				easing: 'easeInBack', | ||||
| 				complete: () => this.$destroy() | ||||
| 				complete: () => this.destroyDom() | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
							
								
								
									
										90
									
								
								src/client/app/common/views/components/tag-cloud.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								src/client/app/common/views/components/tag-cloud.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | ||||
| <template> | ||||
| <div class="jtivnzhfwquxpsfidertopbmwmchmnmo"> | ||||
| 	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> | ||||
| 	<p class="empty" v-else-if="tags.length == 0">%fa:exclamation-circle%%i18n:@empty%</p> | ||||
| 	<div v-else> | ||||
| 		<vue-word-cloud | ||||
| 				:words="tags.slice(0, 20).map(x => [x.name, x.count])" | ||||
| 				:color="color" | ||||
| 				:spacing="1"> | ||||
| 			<template slot-scope="{word, text, weight}"> | ||||
| 				<div style="cursor: pointer;" :title="weight"> | ||||
| 					{{ text }} | ||||
| 				</div> | ||||
| 			</template> | ||||
| 		</vue-word-cloud> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import * as VueWordCloud from 'vuewordcloud'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		[VueWordCloud.name]: VueWordCloud | ||||
| 	}, | ||||
| 	data() { | ||||
| 		return { | ||||
| 			tags: [], | ||||
| 			fetching: true, | ||||
| 			clock: null | ||||
| 		}; | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		this.fetch(); | ||||
| 		this.clock = setInterval(this.fetch, 1000 * 60); | ||||
| 	}, | ||||
| 	beforeDestroy() { | ||||
| 		clearInterval(this.clock); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		fetch() { | ||||
| 			(this as any).api('aggregation/hashtags').then(tags => { | ||||
| 				this.tags = tags; | ||||
| 				this.fetching = false; | ||||
| 			}); | ||||
| 		}, | ||||
| 		color([, weight]) { | ||||
| 			const peak = Math.max.apply(null, this.tags.map(x => x.count)); | ||||
| 			const w = weight / peak; | ||||
|  | ||||
| 			if (w > 0.9) { | ||||
| 				return this.$store.state.device.darkmode ? '#ff4e69' : '#ff4e69'; | ||||
| 			} else if (w > 0.5) { | ||||
| 				return this.$store.state.device.darkmode ? '#3bc4c7' : '#3bc4c7'; | ||||
| 			} else { | ||||
| 				return this.$store.state.device.darkmode ? '#fff' : '#555'; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| root(isDark) | ||||
| 	height 100% | ||||
| 	width 100% | ||||
|  | ||||
| 	> .fetching | ||||
| 	> .empty | ||||
| 		margin 0 | ||||
| 		padding 16px | ||||
| 		text-align center | ||||
| 		color #aaa | ||||
|  | ||||
| 		> [data-fa] | ||||
| 			margin-right 4px | ||||
|  | ||||
| 	> div | ||||
| 		height 100% | ||||
| 		width 100% | ||||
|  | ||||
| .jtivnzhfwquxpsfidertopbmwmchmnmo[data-darkmode] | ||||
| 	root(true) | ||||
|  | ||||
| .jtivnzhfwquxpsfidertopbmwmchmnmo:not([data-darkmode]) | ||||
| 	root(false) | ||||
|  | ||||
| </style> | ||||
| @@ -24,19 +24,34 @@ export default Vue.extend({ | ||||
|  | ||||
| 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 | ||||
| 		padding 16px | ||||
| 		font-weight bold | ||||
| 		font-size 20px | ||||
| 		color isDark ? #fff : #444 | ||||
|  | ||||
| 		@media (min-width 500px) | ||||
| 			padding 24px 32px | ||||
|  | ||||
| 	> section | ||||
| 		padding 20px 16px | ||||
| 		border-top solid 1px isDark ? rgba(#000, 0.3) : rgba(#000, 0.1) | ||||
|  | ||||
| 		@media (min-width 500px) | ||||
| 			padding 32px | ||||
|  | ||||
| 		&.fit-top | ||||
| 			padding-top 0 | ||||
|  | ||||
| 		> header | ||||
| 			margin-bottom 16px | ||||
| 			font-weight bold | ||||
| 			color isDark ? #fff : #444 | ||||
|  | ||||
| .ui-card[data-darkmode] | ||||
| 	root(true) | ||||
|  | ||||
|   | ||||
| @@ -55,7 +55,7 @@ export default Vue.extend({ | ||||
|  | ||||
| root(isDark) | ||||
| 	display inline-block | ||||
| 	margin 32px 32px 32px 0 | ||||
| 	margin 0 32px 0 0 | ||||
| 	cursor pointer | ||||
| 	transition all 0.3s | ||||
|  | ||||
|   | ||||
| @@ -64,6 +64,12 @@ root(isDark) | ||||
| 	cursor pointer | ||||
| 	transition all 0.3s | ||||
|  | ||||
| 	&:first-child | ||||
| 		margin-top 0 | ||||
|  | ||||
| 	&:last-child | ||||
| 		margin-bottom 0 | ||||
|  | ||||
| 	> * | ||||
| 		user-select none | ||||
|  | ||||
| @@ -89,6 +95,7 @@ root(isDark) | ||||
|  | ||||
| 	> .button | ||||
| 		display inline-block | ||||
| 		flex-shrink 0 | ||||
| 		margin 3px 0 0 0 | ||||
| 		width 34px | ||||
| 		height 14px | ||||
|   | ||||
| @@ -14,7 +14,7 @@ | ||||
| 			<header> | ||||
| 				<h1>{{ title }}</h1> | ||||
| 			</header> | ||||
| 			<p>{{ description }}</p> | ||||
| 			<p>{{ description.length > 85 ? description.slice(0, 85) + '…' : description }}</p> | ||||
| 			<footer> | ||||
| 				<img class="icon" v-if="icon" :src="icon"/> | ||||
| 				<p>{{ sitename }}</p> | ||||
|   | ||||
| @@ -47,7 +47,7 @@ export default Vue.extend({ | ||||
| 	props: ['source', 'compact'], | ||||
| 	data() { | ||||
| 		return { | ||||
| 			v: this.$store.state.device.visibility || 'public' | ||||
| 			v: this.$store.state.settings.rememberNoteVisibility ? (this.$store.state.device.visibility || this.$store.state.settings.defaultNoteVisibility) : this.$store.state.settings.defaultNoteVisibility | ||||
| 		} | ||||
| 	}, | ||||
| 	mounted() { | ||||
| @@ -97,9 +97,11 @@ export default Vue.extend({ | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		choose(visibility) { | ||||
| 			this.$store.commit('device/setVisibility', visibility); | ||||
| 			if (this.$store.state.settings.rememberNoteVisibility) { | ||||
| 				this.$store.commit('device/setVisibility', visibility); | ||||
| 			} | ||||
| 			this.$emit('chosen', visibility); | ||||
| 			this.$destroy(); | ||||
| 			this.destroyDom(); | ||||
| 		}, | ||||
| 		close() { | ||||
| 			(this.$refs.backdrop as any).style.pointerEvents = 'none'; | ||||
| @@ -117,7 +119,7 @@ export default Vue.extend({ | ||||
| 				scale: 0.5, | ||||
| 				duration: 200, | ||||
| 				easing: 'easeInBack', | ||||
| 				complete: () => this.$destroy() | ||||
| 				complete: () => this.destroyDom() | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -1,22 +1,24 @@ | ||||
| <template> | ||||
| <div class="mk-welcome-timeline"> | ||||
| 	<div v-for="note in notes"> | ||||
| 		<mk-avatar class="avatar" :user="note.user" target="_blank"/> | ||||
| 		<div class="body"> | ||||
| 			<header> | ||||
| 				<router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">{{ note.user | userName }}</router-link> | ||||
| 				<span class="username">@{{ note.user | acct }}</span> | ||||
| 				<div class="info"> | ||||
| 					<router-link class="created-at" :to="note | notePage"> | ||||
| 						<mk-time :time="note.createdAt"/> | ||||
| 					</router-link> | ||||
| 	<transition-group name="ldzpakcixzickvggyixyrhqwjaefknon" tag="div"> | ||||
| 		<div v-for="note in notes" :key="note.id"> | ||||
| 			<mk-avatar class="avatar" :user="note.user" target="_blank"/> | ||||
| 			<div class="body"> | ||||
| 				<header> | ||||
| 					<router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">{{ note.user | userName }}</router-link> | ||||
| 					<span class="username">@{{ note.user | acct }}</span> | ||||
| 					<div class="info"> | ||||
| 						<router-link class="created-at" :to="note | notePage"> | ||||
| 							<mk-time :time="note.createdAt"/> | ||||
| 						</router-link> | ||||
| 					</div> | ||||
| 				</header> | ||||
| 				<div class="text"> | ||||
| 					<misskey-flavored-markdown v-if="note.text" :text="note.text"/> | ||||
| 				</div> | ||||
| 			</header> | ||||
| 			<div class="text"> | ||||
| 				<misskey-flavored-markdown v-if="note.text" :text="note.text"/> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	</transition-group> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| @@ -63,7 +65,7 @@ export default Vue.extend({ | ||||
| 				local: true, | ||||
| 				reply: false, | ||||
| 				renote: false, | ||||
| 				media: false, | ||||
| 				file: false, | ||||
| 				poll: false | ||||
| 			}).then(notes => { | ||||
| 				this.notes = notes; | ||||
| @@ -83,64 +85,73 @@ export default Vue.extend({ | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| .ldzpakcixzickvggyixyrhqwjaefknon-enter | ||||
| .ldzpakcixzickvggyixyrhqwjaefknon-leave-to | ||||
| 	opacity 0 | ||||
| 	transform translateY(-30px) | ||||
|  | ||||
| root(isDark) | ||||
| 	background isDark ? #282C37 : #fff | ||||
|  | ||||
| 	> div | ||||
| 		padding 16px | ||||
| 		overflow-wrap break-word | ||||
| 		font-size .9em | ||||
| 		color isDark ? #fff : #4C4C4C | ||||
| 		border-bottom 1px solid isDark ? rgba(#000, 0.1) : rgba(#000, 0.05) | ||||
| 		> * | ||||
| 			transition transform .3s ease, opacity .3s ease | ||||
|  | ||||
| 		&:after | ||||
| 			content "" | ||||
| 			display block | ||||
| 			clear both | ||||
| 		> div | ||||
| 			padding 16px | ||||
| 			overflow-wrap break-word | ||||
| 			font-size .9em | ||||
| 			color isDark ? #fff : #4C4C4C | ||||
| 			border-bottom 1px solid isDark ? rgba(#000, 0.1) : rgba(#000, 0.05) | ||||
|  | ||||
| 		> .avatar | ||||
| 			display block | ||||
| 			float left | ||||
| 			position -webkit-sticky | ||||
| 			position sticky | ||||
| 			top 16px | ||||
| 			width 42px | ||||
| 			height 42px | ||||
| 			border-radius 6px | ||||
| 			&:after | ||||
| 				content "" | ||||
| 				display block | ||||
| 				clear both | ||||
|  | ||||
| 		> .body | ||||
| 			float right | ||||
| 			width calc(100% - 42px) | ||||
| 			padding-left 12px | ||||
| 			> .avatar | ||||
| 				display block | ||||
| 				float left | ||||
| 				position -webkit-sticky | ||||
| 				position sticky | ||||
| 				top 16px | ||||
| 				width 42px | ||||
| 				height 42px | ||||
| 				border-radius 6px | ||||
|  | ||||
| 			> header | ||||
| 				display flex | ||||
| 				align-items center | ||||
| 				margin-bottom 4px | ||||
| 				white-space nowrap | ||||
| 			> .body | ||||
| 				float right | ||||
| 				width calc(100% - 42px) | ||||
| 				padding-left 12px | ||||
|  | ||||
| 				> .name | ||||
| 					display block | ||||
| 					margin 0 .5em 0 0 | ||||
| 					padding 0 | ||||
| 					overflow hidden | ||||
| 					font-weight bold | ||||
| 					text-overflow ellipsis | ||||
| 					color isDark ? #fff : #627079 | ||||
| 				> header | ||||
| 					display flex | ||||
| 					align-items center | ||||
| 					margin-bottom 4px | ||||
| 					white-space nowrap | ||||
|  | ||||
| 				> .username | ||||
| 					margin 0 .5em 0 0 | ||||
| 					color isDark ? #606984 : #ccc | ||||
| 					> .name | ||||
| 						display block | ||||
| 						margin 0 .5em 0 0 | ||||
| 						padding 0 | ||||
| 						overflow hidden | ||||
| 						font-weight bold | ||||
| 						text-overflow ellipsis | ||||
| 						color isDark ? #fff : #627079 | ||||
|  | ||||
| 				> .info | ||||
| 					margin-left auto | ||||
| 					font-size 0.9em | ||||
| 					> .username | ||||
| 						margin 0 .5em 0 0 | ||||
| 						color isDark ? #606984 : #ccc | ||||
|  | ||||
| 					> .created-at | ||||
| 						color isDark ? #606984 : #c0c0c0 | ||||
| 					> .info | ||||
| 						margin-left auto | ||||
| 						font-size 0.9em | ||||
|  | ||||
| 			> .text | ||||
| 				text-align left | ||||
| 						> .created-at | ||||
| 							color isDark ? #606984 : #c0c0c0 | ||||
|  | ||||
| 				> .text | ||||
| 					text-align left | ||||
|  | ||||
| .mk-welcome-timeline[data-darkmode] | ||||
| 	root(true) | ||||
|   | ||||
| @@ -167,7 +167,7 @@ class Autocomplete { | ||||
| 	private close() { | ||||
| 		if (this.suggestion == null) return; | ||||
|  | ||||
| 		this.suggestion.$destroy(); | ||||
| 		this.suggestion.destroyDom(); | ||||
| 		this.suggestion = null; | ||||
|  | ||||
| 		this.textarea.focus(); | ||||
|   | ||||
| @@ -32,7 +32,6 @@ | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import parseAcct from '../../../../../misc/acct/parse'; | ||||
| import getUserName from '../../../../../misc/get-user-name'; | ||||
| import Progress from '../../../common/scripts/loading'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| <template> | ||||
| <div class="mkw-analog-clock"> | ||||
| 	<mk-widget-container :naked="!(props.design % 2)" :show-header="false"> | ||||
| 	<mk-widget-container :naked="props.style % 2 === 0" :show-header="false"> | ||||
| 		<div class="mkw-analog-clock--body"> | ||||
| 			<mk-analog-clock :dark="$store.state.device.darkmode" :smooth="!(props.design && ~props.design)"/> | ||||
| 			<mk-analog-clock :dark="$store.state.device.darkmode" :smooth="props.style < 2"/> | ||||
| 		</div> | ||||
| 	</mk-widget-container> | ||||
| </div> | ||||
| @@ -13,13 +13,12 @@ import define from '../../../common/define-widget'; | ||||
| export default define({ | ||||
| 	name: 'analog-clock', | ||||
| 	props: () => ({ | ||||
| 		design: -1 | ||||
| 		style: 0 | ||||
| 	}) | ||||
| }).extend({ | ||||
| 	methods: { | ||||
| 		func() { | ||||
| 			if (++this.props.design > 2) | ||||
| 				this.props.design = -1; | ||||
| 			this.props.style = (this.props.style + 1) % 4; | ||||
| 			this.save(); | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <template> | ||||
| <div class="anltbovirfeutcigvwgmgxipejaeozxi" | ||||
| 	:data-found="broadcasts.length != 0" | ||||
| 	:data-found="announcements && announcements.length != 0" | ||||
| 	:data-melt="props.design == 1" | ||||
| 	:data-mobile="platform == 'mobile'" | ||||
| > | ||||
| @@ -14,12 +14,12 @@ | ||||
| 		</svg> | ||||
| 	</div> | ||||
| 	<p class="fetching" v-if="fetching">%i18n:@fetching%<mk-ellipsis/></p> | ||||
| 	<h1 v-if="!fetching">{{ broadcasts.length == 0 ? '%i18n:@no-broadcasts%' : broadcasts[i].title }}</h1> | ||||
| 	<h1 v-if="!fetching">{{ announcements.length == 0 ? '%i18n:@no-broadcasts%' : announcements[i].title }}</h1> | ||||
| 	<p v-if="!fetching"> | ||||
| 		<span v-if="broadcasts.length != 0" v-html="broadcasts[i].text"></span> | ||||
| 		<template v-if="broadcasts.length == 0">%i18n:@have-a-nice-day%</template> | ||||
| 		<span v-if="announcements.length != 0" v-html="announcements[i].text"></span> | ||||
| 		<template v-if="announcements.length == 0">%i18n:@have-a-nice-day%</template> | ||||
| 	</p> | ||||
| 	<a v-if="broadcasts.length > 1" @click="next">%i18n:@next% >></a> | ||||
| 	<a v-if="announcements.length > 1" @click="next">%i18n:@next% >></a> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| @@ -36,18 +36,18 @@ export default define({ | ||||
| 		return { | ||||
| 			i: 0, | ||||
| 			fetching: true, | ||||
| 			broadcasts: [] | ||||
| 			announcements: [] | ||||
| 		}; | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		(this as any).os.getMeta().then(meta => { | ||||
| 			this.broadcasts = meta.broadcasts; | ||||
| 			this.announcements = meta.broadcasts; | ||||
| 			this.fetching = false; | ||||
| 		}); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		next() { | ||||
| 			if (this.i == this.broadcasts.length - 1) { | ||||
| 			if (this.i == this.announcements.length - 1) { | ||||
| 				this.i = 0; | ||||
| 			} else { | ||||
| 				this.i++; | ||||
| @@ -126,7 +126,7 @@ root(isDark) | ||||
| 		margin 0 | ||||
| 		font-size 0.95em | ||||
| 		font-weight normal | ||||
| 		color #4078c0 | ||||
| 		color isDark ? #539eff : #4078c0 | ||||
|  | ||||
| 	> p | ||||
| 		display block | ||||
|   | ||||
| @@ -6,7 +6,6 @@ import VueRouter from 'vue-router'; | ||||
|  | ||||
| // Style | ||||
| import './style.styl'; | ||||
| import '../../element.scss'; | ||||
|  | ||||
| import init from '../init'; | ||||
| import fuckAdBlock from '../common/scripts/fuck-ad-block'; | ||||
|   | ||||
| @@ -19,6 +19,11 @@ | ||||
| 				<option value="drive">%i18n:@charts.drive%</option> | ||||
| 				<option value="drive-total">%i18n:@charts.drive-total%</option> | ||||
| 			</optgroup> | ||||
| 			<optgroup label="%i18n:@network%"> | ||||
| 				<option value="network-requests">%i18n:@charts.network-requests%</option> | ||||
| 				<option value="network-time">%i18n:@charts.network-time%</option> | ||||
| 				<option value="network-usage">%i18n:@charts.network-usage%</option> | ||||
| 			</optgroup> | ||||
| 		</select> | ||||
| 		<div> | ||||
| 			<span @click="span = 'day'" :class="{ active: span == 'day' }">%i18n:@per-day%</span> | <span @click="span = 'hour'" :class="{ active: span == 'hour' }">%i18n:@per-hour%</span> | ||||
| @@ -41,7 +46,10 @@ const colors = { | ||||
| 	localPlus: 'rgb(52, 178, 118)', | ||||
| 	remotePlus: 'rgb(158, 255, 209)', | ||||
| 	localMinus: 'rgb(255, 97, 74)', | ||||
| 	remoteMinus: 'rgb(255, 149, 134)' | ||||
| 	remoteMinus: 'rgb(255, 149, 134)', | ||||
|  | ||||
| 	incoming: 'rgb(52, 178, 118)', | ||||
| 	outgoing: 'rgb(255, 97, 74)', | ||||
| }; | ||||
|  | ||||
| const rgba = (color: string): string => { | ||||
| @@ -75,6 +83,9 @@ export default Vue.extend({ | ||||
| 				case 'drive-total': return this.driveTotalChart(); | ||||
| 				case 'drive-files': return this.driveFilesChart(); | ||||
| 				case 'drive-files-total': return this.driveFilesTotalChart(); | ||||
| 				case 'network-requests': return this.networkRequestsChart(); | ||||
| 				case 'network-time': return this.networkTimeChart(); | ||||
| 				case 'network-usage': return this.networkUsageChart(); | ||||
| 			} | ||||
| 		}, | ||||
|  | ||||
| @@ -544,7 +555,95 @@ export default Vue.extend({ | ||||
| 					} | ||||
| 				} | ||||
| 			}]; | ||||
| 		} | ||||
| 		}, | ||||
|  | ||||
| 		networkRequestsChart(): any { | ||||
| 			const data = this.stats.slice().reverse().map(x => ({ | ||||
| 				date: new Date(x.date), | ||||
| 				requests: x.network.requests | ||||
| 			})); | ||||
|  | ||||
| 			return [{ | ||||
| 				datasets: [{ | ||||
| 					label: 'Requests', | ||||
| 					fill: true, | ||||
| 					backgroundColor: rgba(colors.localPlus), | ||||
| 					borderColor: colors.localPlus, | ||||
| 					borderWidth: 2, | ||||
| 					pointBackgroundColor: '#fff', | ||||
| 					lineTension: 0, | ||||
| 					data: data.map(x => ({ t: x.date, y: x.requests })) | ||||
| 				}] | ||||
| 			}]; | ||||
| 		}, | ||||
|  | ||||
| 		networkTimeChart(): any { | ||||
| 			const data = this.stats.slice().reverse().map(x => ({ | ||||
| 				date: new Date(x.date), | ||||
| 				time: x.network.requests != 0 ? (x.network.totalTime / x.network.requests) : 0, | ||||
| 			})); | ||||
|  | ||||
| 			return [{ | ||||
| 				datasets: [{ | ||||
| 					label: 'Avg time (ms)', | ||||
| 					fill: true, | ||||
| 					backgroundColor: rgba(colors.localPlus), | ||||
| 					borderColor: colors.localPlus, | ||||
| 					borderWidth: 2, | ||||
| 					pointBackgroundColor: '#fff', | ||||
| 					lineTension: 0, | ||||
| 					data: data.map(x => ({ t: x.date, y: x.time })) | ||||
| 				}] | ||||
| 			}]; | ||||
| 		}, | ||||
|  | ||||
| 		networkUsageChart(): any { | ||||
| 			const data = this.stats.slice().reverse().map(x => ({ | ||||
| 				date: new Date(x.date), | ||||
| 				incoming: x.network.incomingBytes, | ||||
| 				outgoing: x.network.outgoingBytes | ||||
| 			})); | ||||
|  | ||||
| 			return [{ | ||||
| 				datasets: [{ | ||||
| 					label: 'Incoming', | ||||
| 					fill: true, | ||||
| 					backgroundColor: rgba(colors.incoming), | ||||
| 					borderColor: colors.incoming, | ||||
| 					borderWidth: 2, | ||||
| 					pointBackgroundColor: '#fff', | ||||
| 					lineTension: 0, | ||||
| 					data: data.map(x => ({ t: x.date, y: x.incoming })) | ||||
| 				}, { | ||||
| 					label: 'Outgoing', | ||||
| 					fill: true, | ||||
| 					backgroundColor: rgba(colors.outgoing), | ||||
| 					borderColor: colors.outgoing, | ||||
| 					borderWidth: 2, | ||||
| 					pointBackgroundColor: '#fff', | ||||
| 					lineTension: 0, | ||||
| 					data: data.map(x => ({ t: x.date, y: x.outgoing })) | ||||
| 				}] | ||||
| 			}, { | ||||
| 				scales: { | ||||
| 					yAxes: [{ | ||||
| 						ticks: { | ||||
| 							callback: value => { | ||||
| 								return Vue.filter('bytes')(value, 1); | ||||
| 							} | ||||
| 						} | ||||
| 					}] | ||||
| 				}, | ||||
| 				tooltips: { | ||||
| 					callbacks: { | ||||
| 						label: (tooltipItem, data) => { | ||||
| 							const label = data.datasets[tooltipItem.datasetIndex].label || ''; | ||||
| 							return `${label}: ${Vue.filter('bytes')(tooltipItem.yLabel, 1)}`; | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			}]; | ||||
| 		}, | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|   | ||||
| @@ -64,7 +64,7 @@ export default Vue.extend({ | ||||
| 			}); | ||||
|  | ||||
| 			this.$emit('closed'); | ||||
| 			this.$destroy(); | ||||
| 			this.destroyDom(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
|   | ||||
| @@ -78,7 +78,7 @@ export default Vue.extend({ | ||||
| 				scale: 0.8, | ||||
| 				duration: 300, | ||||
| 				easing: [ 0.5, -0.5, 1, 0.5 ], | ||||
| 				complete: () => this.$destroy() | ||||
| 				complete: () => this.destroyDom() | ||||
| 			}); | ||||
| 		}, | ||||
| 		onBgClick() { | ||||
|   | ||||
| @@ -14,7 +14,7 @@ | ||||
| 	<p class="empty" v-if="!fetching && users.length == 0">%i18n:@empty%</p> | ||||
| 	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:@fetching%<mk-ellipsis/></p> | ||||
| 	<a class="refresh" @click="refresh">%i18n:@refresh%</a> | ||||
| 	<button class="close" @click="$destroy()" title="%i18n:@close%">%fa:times%</button> | ||||
| 	<button class="close" @click="destroyDom()" title="%i18n:@close%">%fa:times%</button> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
|   | ||||
| @@ -26,7 +26,7 @@ export default Vue.extend({ | ||||
| 				opacity: 0, | ||||
| 				duration: 100, | ||||
| 				easing: 'linear', | ||||
| 				complete: () => this.$destroy() | ||||
| 				complete: () => this.destroyDom() | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| <template> | ||||
| <div class="ldwbgwstjsdgcjruamauqdrffetqudry" v-if="image.isSensitive && hide" @click="hide = false"> | ||||
| <div class="ldwbgwstjsdgcjruamauqdrffetqudry" v-if="image.isSensitive && hide && !$store.state.device.alwaysShowNsfw" @click="hide = false"> | ||||
| 	<div> | ||||
| 		<b>%fa:exclamation-triangle% %i18n:@sensitive%</b> | ||||
| 		<span>%i18n:@click-to-show%</span> | ||||
|   | ||||
| @@ -28,7 +28,7 @@ export default Vue.extend({ | ||||
| 				opacity: 0, | ||||
| 				duration: 100, | ||||
| 				easing: 'linear', | ||||
| 				complete: () => this.$destroy() | ||||
| 				complete: () => this.destroyDom() | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -37,20 +37,26 @@ | ||||
| 			</router-link> | ||||
| 		</header> | ||||
| 		<div class="body"> | ||||
| 			<div class="text"> | ||||
| 				<span v-if="p.isHidden" style="opacity: 0.5">%i18n:@private%</span> | ||||
| 				<span v-if="p.deletedAt" style="opacity: 0.5">%i18n:@deleted%</span> | ||||
| 				<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/> | ||||
| 			</div> | ||||
| 			<div class="media" v-if="p.media.length > 0"> | ||||
| 				<mk-media-list :media-list="p.media" :raw="true"/> | ||||
| 			</div> | ||||
| 			<mk-poll v-if="p.poll" :note="p"/> | ||||
| 			<mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/> | ||||
| 			<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> | ||||
| 			<div class="map" v-if="p.geo" ref="map"></div> | ||||
| 			<div class="renote" v-if="p.renote"> | ||||
| 				<mk-note-preview :note="p.renote"/> | ||||
| 			<p v-if="p.cw != null" class="cw"> | ||||
| 				<span class="text" v-if="p.cw != ''">{{ p.cw }}</span> | ||||
| 				<mk-cw-button v-model="showContent"/> | ||||
| 			</p> | ||||
| 			<div class="content" v-show="p.cw == null || showContent"> | ||||
| 				<div class="text"> | ||||
| 					<span v-if="p.isHidden" style="opacity: 0.5">%i18n:@private%</span> | ||||
| 					<span v-if="p.deletedAt" style="opacity: 0.5">%i18n:@deleted%</span> | ||||
| 					<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/> | ||||
| 				</div> | ||||
| 				<div class="files" v-if="p.files.length > 0"> | ||||
| 					<mk-media-list :media-list="p.files" :raw="true"/> | ||||
| 				</div> | ||||
| 				<mk-poll v-if="p.poll" :note="p"/> | ||||
| 				<mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/> | ||||
| 				<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> | ||||
| 				<div class="map" v-if="p.geo" ref="map"></div> | ||||
| 				<div class="renote" v-if="p.renote"> | ||||
| 					<mk-note-preview :note="p.renote"/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<footer> | ||||
| @@ -86,6 +92,7 @@ import MkRenoteFormWindow from './renote-form-window.vue'; | ||||
| import MkNoteMenu from '../../../common/views/components/note-menu.vue'; | ||||
| import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; | ||||
| import XSub from './notes.note.sub.vue'; | ||||
| import { sum } from '../../../../../prelude/array'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| @@ -104,6 +111,7 @@ export default Vue.extend({ | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			showContent: false, | ||||
| 			conversation: [], | ||||
| 			conversationFetching: false, | ||||
| 			replies: [] | ||||
| @@ -114,22 +122,24 @@ export default Vue.extend({ | ||||
| 		isRenote(): boolean { | ||||
| 			return (this.note.renote && | ||||
| 				this.note.text == null && | ||||
| 				this.note.mediaIds.length == 0 && | ||||
| 				this.note.fileIds.length == 0 && | ||||
| 				this.note.poll == null); | ||||
| 		}, | ||||
|  | ||||
| 		p(): any { | ||||
| 			return this.isRenote ? this.note.renote : this.note; | ||||
| 		}, | ||||
|  | ||||
| 		reactionsCount(): number { | ||||
| 			return this.p.reactionCounts | ||||
| 				? Object.keys(this.p.reactionCounts) | ||||
| 					.map(key => this.p.reactionCounts[key]) | ||||
| 					.reduce((a, b) => a + b) | ||||
| 				? sum(Object.values(this.p.reactionCounts)) | ||||
| 				: 0; | ||||
| 		}, | ||||
|  | ||||
| 		title(): string { | ||||
| 			return new Date(this.p.createdAt).toLocaleString(); | ||||
| 		}, | ||||
|  | ||||
| 		urls(): string[] { | ||||
| 			if (this.p.text) { | ||||
| 				const ast = parse(this.p.text); | ||||
| @@ -184,22 +194,26 @@ export default Vue.extend({ | ||||
| 				this.conversation = conversation.reverse(); | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		reply() { | ||||
| 			(this as any).os.new(MkPostFormWindow, { | ||||
| 				reply: this.p | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		renote() { | ||||
| 			(this as any).os.new(MkRenoteFormWindow, { | ||||
| 				note: this.p | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		react() { | ||||
| 			(this as any).os.new(MkReactionPicker, { | ||||
| 				source: this.$refs.reactButton, | ||||
| 				note: this.p | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		menu() { | ||||
| 			(this as any).os.new(MkNoteMenu, { | ||||
| 				source: this.$refs.menuButton, | ||||
| @@ -327,37 +341,49 @@ root(isDark) | ||||
| 		> .body | ||||
| 			padding 8px 0 | ||||
|  | ||||
| 			> .text | ||||
| 			> .cw | ||||
| 				cursor default | ||||
| 				display block | ||||
| 				margin 0 | ||||
| 				padding 0 | ||||
| 				overflow-wrap break-word | ||||
| 				font-size 1.5em | ||||
| 				color isDark ? #fff : #717171 | ||||
|  | ||||
| 			> .renote | ||||
| 				margin 8px 0 | ||||
| 				> .text | ||||
| 					margin-right 8px | ||||
|  | ||||
| 				> .mk-note-preview | ||||
| 					padding 16px | ||||
| 					border dashed 1px #c0dac6 | ||||
| 					border-radius 8px | ||||
| 			> .content | ||||
| 				> .text | ||||
| 					cursor default | ||||
| 					display block | ||||
| 					margin 0 | ||||
| 					padding 0 | ||||
| 					overflow-wrap break-word | ||||
| 					font-size 1.5em | ||||
| 					color isDark ? #fff : #717171 | ||||
|  | ||||
| 			> .location | ||||
| 				margin 4px 0 | ||||
| 				font-size 12px | ||||
| 				color #ccc | ||||
| 				> .renote | ||||
| 					margin 8px 0 | ||||
|  | ||||
| 			> .map | ||||
| 				width 100% | ||||
| 				height 300px | ||||
| 					> * | ||||
| 						padding 16px | ||||
| 						border dashed 1px #c0dac6 | ||||
| 						border-radius 8px | ||||
|  | ||||
| 				&:empty | ||||
| 					display none | ||||
| 				> .location | ||||
| 					margin 4px 0 | ||||
| 					font-size 12px | ||||
| 					color #ccc | ||||
|  | ||||
| 			> .mk-url-preview | ||||
| 				margin-top 8px | ||||
| 				> .map | ||||
| 					width 100% | ||||
| 					height 300px | ||||
|  | ||||
| 					&:empty | ||||
| 						display none | ||||
|  | ||||
| 				> .mk-url-preview | ||||
| 					margin-top 8px | ||||
|  | ||||
| 		> footer | ||||
| 			font-size 1.2em | ||||
|   | ||||
| @@ -1,10 +1,16 @@ | ||||
| <template> | ||||
| <div class="mk-note-preview" :title="title"> | ||||
| <div class="qiziqtywpuaucsgarwajitwaakggnisj" :title="title"> | ||||
| 	<mk-avatar class="avatar" :user="note.user" v-if="!mini"/> | ||||
| 	<div class="main"> | ||||
| 		<mk-note-header class="header" :note="note" :mini="true"/> | ||||
| 		<div class="body"> | ||||
| 			<mk-sub-note-content class="text" :note="note"/> | ||||
| 			<p v-if="note.cw != null" class="cw"> | ||||
| 				<span class="text" v-if="note.cw != ''">{{ note.cw }}</span> | ||||
| 				<mk-cw-button v-model="showContent"/> | ||||
| 			</p> | ||||
| 			<div class="content" v-show="note.cw == null || showContent"> | ||||
| 				<mk-sub-note-content class="text" :note="note"/> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
| @@ -25,6 +31,13 @@ export default Vue.extend({ | ||||
| 			default: false | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			showContent: false | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| 	computed: { | ||||
| 		title(): string { | ||||
| 			return new Date(this.note.createdAt).toLocaleString(); | ||||
| @@ -52,16 +65,28 @@ root(isDark) | ||||
|  | ||||
| 		> .body | ||||
|  | ||||
| 			> .text | ||||
| 			> .cw | ||||
| 				cursor default | ||||
| 				display block | ||||
| 				margin 0 | ||||
| 				padding 0 | ||||
| 				color isDark ? #959ba7 : #717171 | ||||
| 				overflow-wrap break-word | ||||
| 				color isDark ? #fff : #717171 | ||||
|  | ||||
| .mk-note-preview[data-darkmode] | ||||
| 				> .text | ||||
| 					margin-right 8px | ||||
|  | ||||
| 			> .content | ||||
| 				> .text | ||||
| 					cursor default | ||||
| 					margin 0 | ||||
| 					padding 0 | ||||
| 					color isDark ? #959ba7 : #717171 | ||||
|  | ||||
| .qiziqtywpuaucsgarwajitwaakggnisj[data-darkmode] | ||||
| 	root(true) | ||||
|  | ||||
| .mk-note-preview:not([data-darkmode]) | ||||
| .qiziqtywpuaucsgarwajitwaakggnisj:not([data-darkmode]) | ||||
| 	root(false) | ||||
|  | ||||
| </style> | ||||
|   | ||||
| @@ -1,10 +1,16 @@ | ||||
| <template> | ||||
| <div class="sub" :title="title"> | ||||
| <div class="tkfdzaxtkdeianobciwadajxzbddorql" :title="title"> | ||||
| 	<mk-avatar class="avatar" :user="note.user"/> | ||||
| 	<div class="main"> | ||||
| 		<mk-note-header class="header" :note="note"/> | ||||
| 		<div class="body"> | ||||
| 			<mk-sub-note-content class="text" :note="note"/> | ||||
| 			<p v-if="note.cw != null" class="cw"> | ||||
| 				<span class="text" v-if="note.cw != ''">{{ note.cw }}</span> | ||||
| 				<mk-cw-button v-model="showContent"/> | ||||
| 			</p> | ||||
| 			<div class="content" v-show="note.cw == null || showContent"> | ||||
| 				<mk-sub-note-content class="text" :note="note"/> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
| @@ -14,7 +20,19 @@ | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	props: ['note'], | ||||
| 	props: { | ||||
| 		note: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			showContent: false | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| 	computed: { | ||||
| 		title(): string { | ||||
| 			return new Date(this.note.createdAt).toLocaleString(); | ||||
| @@ -48,20 +66,32 @@ root(isDark) | ||||
|  | ||||
| 		> .body | ||||
|  | ||||
| 			> .text | ||||
| 			> .cw | ||||
| 				cursor default | ||||
| 				display block | ||||
| 				margin 0 | ||||
| 				padding 0 | ||||
| 				color isDark ? #959ba7 : #717171 | ||||
| 				overflow-wrap break-word | ||||
| 				color isDark ? #fff : #717171 | ||||
|  | ||||
| 				pre | ||||
| 					max-height 120px | ||||
| 					font-size 80% | ||||
| 				> .text | ||||
| 					margin-right 8px | ||||
|  | ||||
| .sub[data-darkmode] | ||||
| 			> .content | ||||
| 				> .text | ||||
| 					cursor default | ||||
| 					margin 0 | ||||
| 					padding 0 | ||||
| 					color isDark ? #959ba7 : #717171 | ||||
|  | ||||
| 					pre | ||||
| 						max-height 120px | ||||
| 						font-size 80% | ||||
|  | ||||
| .tkfdzaxtkdeianobciwadajxzbddorql[data-darkmode] | ||||
| 	root(true) | ||||
|  | ||||
| .sub:not([data-darkmode]) | ||||
| .tkfdzaxtkdeianobciwadajxzbddorql:not([data-darkmode]) | ||||
| 	root(false) | ||||
|  | ||||
| </style> | ||||
|   | ||||
| @@ -18,7 +18,7 @@ | ||||
| 			<div class="body"> | ||||
| 				<p v-if="p.cw != null" class="cw"> | ||||
| 					<span class="text" v-if="p.cw != ''">{{ p.cw }}</span> | ||||
| 					<span class="toggle" @click="showContent = !showContent">{{ showContent ? '%i18n:@hide%' : '%i18n:@see-more%' }}</span> | ||||
| 					<mk-cw-button v-model="showContent"/> | ||||
| 				</p> | ||||
| 				<div class="content" v-show="p.cw == null || showContent"> | ||||
| 					<div class="text"> | ||||
| @@ -28,15 +28,13 @@ | ||||
| 						<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i" :class="$style.text"/> | ||||
| 						<a class="rp" v-if="p.renote">RP:</a> | ||||
| 					</div> | ||||
| 					<div class="media" v-if="p.media.length > 0"> | ||||
| 						<mk-media-list :media-list="p.media"/> | ||||
| 					<div class="files" v-if="p.files.length > 0"> | ||||
| 						<mk-media-list :media-list="p.files"/> | ||||
| 					</div> | ||||
| 					<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/> | ||||
| 					<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a> | ||||
| 					<div class="map" v-if="p.geo" ref="map"></div> | ||||
| 					<div class="renote" v-if="p.renote"> | ||||
| 						<mk-note-preview :note="p.renote"/> | ||||
| 					</div> | ||||
| 					<div class="renote" v-if="p.renote"><mk-note-preview :note="p.renote"/></div> | ||||
| 					<mk-url-preview v-for="url in urls" :url="url" :key="url"/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| @@ -78,6 +76,7 @@ import MkRenoteFormWindow from './renote-form-window.vue'; | ||||
| import MkNoteMenu from '../../../common/views/components/note-menu.vue'; | ||||
| import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; | ||||
| import XSub from './notes.note.sub.vue'; | ||||
| import { sum } from '../../../../../prelude/array'; | ||||
|  | ||||
| function focus(el, fn) { | ||||
| 	const target = fn(el); | ||||
| @@ -95,7 +94,12 @@ export default Vue.extend({ | ||||
| 		XSub | ||||
| 	}, | ||||
|  | ||||
| 	props: ['note'], | ||||
| 	props: { | ||||
| 		note: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| @@ -110,7 +114,7 @@ export default Vue.extend({ | ||||
| 		isRenote(): boolean { | ||||
| 			return (this.note.renote && | ||||
| 				this.note.text == null && | ||||
| 				this.note.mediaIds.length == 0 && | ||||
| 				this.note.fileIds.length == 0 && | ||||
| 				this.note.poll == null); | ||||
| 		}, | ||||
|  | ||||
| @@ -120,9 +124,7 @@ export default Vue.extend({ | ||||
|  | ||||
| 		reactionsCount(): number { | ||||
| 			return this.p.reactionCounts | ||||
| 				? Object.keys(this.p.reactionCounts) | ||||
| 					.map(key => this.p.reactionCounts[key]) | ||||
| 					.reduce((a, b) => a + b) | ||||
| 				? sum(Object.values(this.p.reactionCounts)) | ||||
| 				: 0; | ||||
| 		}, | ||||
|  | ||||
| @@ -399,19 +401,6 @@ root(isDark) | ||||
| 					> .text | ||||
| 						margin-right 8px | ||||
|  | ||||
| 					> .toggle | ||||
| 						display inline-block | ||||
| 						padding 4px 8px | ||||
| 						font-size 0.7em | ||||
| 						color isDark ? #393f4f : #fff | ||||
| 						background isDark ? #687390 : #b1b9c1 | ||||
| 						border-radius 2px | ||||
| 						cursor pointer | ||||
| 						user-select none | ||||
|  | ||||
| 						&:hover | ||||
| 							background isDark ? #707b97 : #bbc4ce | ||||
|  | ||||
| 				> .content | ||||
|  | ||||
| 					> .text | ||||
| @@ -470,7 +459,7 @@ root(isDark) | ||||
| 					> .renote | ||||
| 						margin 8px 0 | ||||
|  | ||||
| 						> .mk-note-preview | ||||
| 						> * | ||||
| 							padding 16px | ||||
| 							border dashed 1px isDark ? #4e945e : #c0dac6 | ||||
| 							border-radius 8px | ||||
|   | ||||
| @@ -10,8 +10,7 @@ | ||||
| 	</div> | ||||
|  | ||||
| 	<!-- トランジションを有効にするとなぜかメモリリークする --> | ||||
| 	<!--<transition-group name="mk-notes" class="transition">--> | ||||
| 	<div class="notes"> | ||||
| 	<transition-group name="mk-notes" class="notes transition" tag="div"> | ||||
| 		<template v-for="(note, i) in _notes"> | ||||
| 			<x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/> | ||||
| 			<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date"> | ||||
| @@ -19,8 +18,7 @@ | ||||
| 				<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span> | ||||
| 			</p> | ||||
| 		</template> | ||||
| 	</div> | ||||
| 	<!--</transition-group>--> | ||||
| 	</transition-group> | ||||
|  | ||||
| 	<footer v-if="more"> | ||||
| 		<button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> | ||||
| @@ -122,7 +120,7 @@ export default Vue.extend({ | ||||
| 		prepend(note, silent = false) { | ||||
| 			//#region 弾く | ||||
| 			const isMyNote = note.userId == this.$store.state.i.id; | ||||
| 			const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null; | ||||
| 			const isPureRenote = note.renoteId != null && note.text == null && note.fileIds.length == 0 && note.poll == null; | ||||
|  | ||||
| 			if (this.$store.state.settings.showMyRenotes === false) { | ||||
| 				if (isMyNote && isPureRenote) { | ||||
|   | ||||
| @@ -2,8 +2,7 @@ | ||||
| <div class="mk-notifications"> | ||||
| 	<div class="notifications" v-if="notifications.length != 0"> | ||||
| 		<!-- トランジションを有効にするとなぜかメモリリークする --> | ||||
| 		<!-- <transition-group name="mk-notifications" class="transition"> --> | ||||
| 		<div> | ||||
| 		<transition-group name="mk-notifications" class="transition" tag="div"> | ||||
| 			<template v-for="(notification, i) in _notifications"> | ||||
| 				<div class="notification" :class="notification.type" :key="notification.id"> | ||||
| 					<mk-time :time="notification.createdAt"/> | ||||
| @@ -97,8 +96,7 @@ | ||||
| 					<span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span> | ||||
| 				</p> | ||||
| 			</template> | ||||
| 		</div> | ||||
| 		<!-- </transition-group> --> | ||||
| 		</transition-group> | ||||
| 	</div> | ||||
| 	<button class="more" :class="{ fetching: fetchingMoreNotifications }" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications"> | ||||
| 		<template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:@more%' }} | ||||
|   | ||||
| @@ -4,7 +4,7 @@ | ||||
| 		<span class="icon" v-if="geo">%fa:map-marker-alt%</span> | ||||
| 		<span v-if="!reply">%i18n:@note%</span> | ||||
| 		<span v-if="reply">%i18n:@reply%</span> | ||||
| 		<span class="count" v-if="media.length != 0">{{ '%i18n:@attaches%'.replace('{}', media.length) }}</span> | ||||
| 		<span class="count" v-if="files.length != 0">{{ '%i18n:@attaches%'.replace('{}', files.length) }}</span> | ||||
| 		<span class="count" v-if="uploadings.length != 0">{{ '%i18n:@uploading-media%'.replace('{}', uploadings.length) }}<mk-ellipsis/></span> | ||||
| 	</span> | ||||
|  | ||||
| @@ -14,7 +14,7 @@ | ||||
| 			:reply="reply" | ||||
| 			@posted="onPosted" | ||||
| 			@change-uploadings="onChangeUploadings" | ||||
| 			@change-attached-media="onChangeMedia" | ||||
| 			@change-attached-files="onChangeFiles" | ||||
| 			@geo-attached="onGeoAttached" | ||||
| 			@geo-dettached="onGeoDettached"/> | ||||
| 	</div> | ||||
| @@ -29,7 +29,7 @@ export default Vue.extend({ | ||||
| 	data() { | ||||
| 		return { | ||||
| 			uploadings: [], | ||||
| 			media: [], | ||||
| 			files: [], | ||||
| 			geo: null | ||||
| 		}; | ||||
| 	}, | ||||
| @@ -42,8 +42,8 @@ export default Vue.extend({ | ||||
| 		onChangeUploadings(files) { | ||||
| 			this.uploadings = files; | ||||
| 		}, | ||||
| 		onChangeMedia(media) { | ||||
| 			this.media = media; | ||||
| 		onChangeFiles(files) { | ||||
| 			this.files = files; | ||||
| 		}, | ||||
| 		onGeoAttached(geo) { | ||||
| 			this.geo = geo; | ||||
|   | ||||
| @@ -20,7 +20,7 @@ | ||||
| 			@keydown="onKeydown" @paste="onPaste" :placeholder="placeholder" | ||||
| 			v-autocomplete="'text'" | ||||
| 		></textarea> | ||||
| 		<div class="medias" :class="{ with: poll }" v-show="files.length != 0"> | ||||
| 		<div class="files" :class="{ with: poll }" v-show="files.length != 0"> | ||||
| 			<x-draggable :list="files" :options="{ animation: 150 }"> | ||||
| 				<div v-for="file in files" :key="file.id"> | ||||
| 					<div class="img" :style="{ backgroundImage: `url(${file.thumbnailUrl})` }" :title="file.name"></div> | ||||
| @@ -45,7 +45,7 @@ | ||||
| 		<span v-if="visibility === 'specified'">%fa:envelope%</span> | ||||
| 		<span v-if="visibility === 'private'">%fa:lock%</span> | ||||
| 	</button> | ||||
| 	<p class="text-count" :class="{ over: text.length > 1000 }">{{ 1000 - text.length }}</p> | ||||
| 	<p class="text-count" :class="{ over: this.trimmedLength(text) > 1000 }">{{ 1000 - this.trimmedLength(text) }}</p> | ||||
| 	<button :class="{ posting }" class="submit" :disabled="!canPost" @click="post"> | ||||
| 		{{ posting ? '%i18n:@posting%' : submitText }}<mk-ellipsis v-if="posting"/> | ||||
| 	</button> | ||||
| @@ -62,6 +62,9 @@ import getFace from '../../../common/scripts/get-face'; | ||||
| import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue'; | ||||
| import parse from '../../../../../mfm/parse'; | ||||
| import { host } from '../../../config'; | ||||
| import { erase, unique } from '../../../../../prelude/array'; | ||||
| import { length } from 'stringz'; | ||||
| import parseAcct from '../../../../../misc/acct/parse'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| @@ -99,7 +102,7 @@ export default Vue.extend({ | ||||
| 			useCw: false, | ||||
| 			cw: null, | ||||
| 			geo: null, | ||||
| 			visibility: this.$store.state.device.visibility || 'public', | ||||
| 			visibility: this.$store.state.settings.rememberNoteVisibility ? (this.$store.state.device.visibility || this.$store.state.settings.defaultNoteVisibility) : this.$store.state.settings.defaultNoteVisibility, | ||||
| 			visibleUsers: [], | ||||
| 			autocomplete: null, | ||||
| 			draghover: false, | ||||
| @@ -145,7 +148,7 @@ export default Vue.extend({ | ||||
| 		canPost(): boolean { | ||||
| 			return !this.posting && | ||||
| 				(1 <= this.text.length || 1 <= this.files.length || this.poll || this.renote) && | ||||
| 				(this.text.trim().length <= 1000); | ||||
| 				(length(this.text.trim()) <= 1000); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| @@ -188,7 +191,7 @@ export default Vue.extend({ | ||||
| 							(this.$refs.poll as any).set(draft.data.poll); | ||||
| 						}); | ||||
| 					} | ||||
| 					this.$emit('change-attached-media', this.files); | ||||
| 					this.$emit('change-attached-files', this.files); | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| @@ -197,6 +200,10 @@ export default Vue.extend({ | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 	  trimmedLength(text: string) { | ||||
| 			return length(text.trim()); | ||||
| 		}, | ||||
|  | ||||
| 		addTag(tag: string) { | ||||
| 			insertTextAtCursor(this.$refs.text, ` #${tag} `); | ||||
| 		}, | ||||
| @@ -225,12 +232,12 @@ export default Vue.extend({ | ||||
|  | ||||
| 		attachMedia(driveFile) { | ||||
| 			this.files.push(driveFile); | ||||
| 			this.$emit('change-attached-media', this.files); | ||||
| 			this.$emit('change-attached-files', this.files); | ||||
| 		}, | ||||
|  | ||||
| 		detachMedia(id) { | ||||
| 			this.files = this.files.filter(x => x.id != id); | ||||
| 			this.$emit('change-attached-media', this.files); | ||||
| 			this.$emit('change-attached-files', this.files); | ||||
| 		}, | ||||
|  | ||||
| 		onChangeFile() { | ||||
| @@ -249,7 +256,7 @@ export default Vue.extend({ | ||||
| 			this.text = ''; | ||||
| 			this.files = []; | ||||
| 			this.poll = false; | ||||
| 			this.$emit('change-attached-media', this.files); | ||||
| 			this.$emit('change-attached-files', this.files); | ||||
| 		}, | ||||
|  | ||||
| 		onKeydown(e) { | ||||
| @@ -297,7 +304,7 @@ export default Vue.extend({ | ||||
| 			if (driveFile != null && driveFile != '') { | ||||
| 				const file = JSON.parse(driveFile); | ||||
| 				this.files.push(file); | ||||
| 				this.$emit('change-attached-media', this.files); | ||||
| 				this.$emit('change-attached-files', this.files); | ||||
| 				e.preventDefault(); | ||||
| 			} | ||||
| 			//#endregion | ||||
| @@ -336,17 +343,16 @@ export default Vue.extend({ | ||||
| 		addVisibleUser() { | ||||
| 			(this as any).apis.input({ | ||||
| 				title: '%i18n:@enter-username%' | ||||
| 			}).then(username => { | ||||
| 				(this as any).api('users/show', { | ||||
| 					username | ||||
| 				}).then(user => { | ||||
| 			}).then(acct => { | ||||
| 				if (acct.startsWith('@')) acct = acct.substr(1); | ||||
| 				(this as any).api('users/show', parseAcct(acct)).then(user => { | ||||
| 					this.visibleUsers.push(user); | ||||
| 				}); | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		removeVisibleUser(user) { | ||||
| 			this.visibleUsers = this.visibleUsers.filter(u => u != user); | ||||
| 			this.visibleUsers = erase(user, this.visibleUsers); | ||||
| 		}, | ||||
|  | ||||
| 		post() { | ||||
| @@ -354,7 +360,7 @@ export default Vue.extend({ | ||||
|  | ||||
| 			(this as any).api('notes/create', { | ||||
| 				text: this.text == '' ? undefined : this.text, | ||||
| 				mediaIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined, | ||||
| 				fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined, | ||||
| 				replyId: this.reply ? this.reply.id : undefined, | ||||
| 				renoteId: this.renote ? this.renote.id : undefined, | ||||
| 				poll: this.poll ? (this.$refs.poll as any).get() : undefined, | ||||
| @@ -391,7 +397,7 @@ export default Vue.extend({ | ||||
| 			if (this.text && this.text != '') { | ||||
| 				const hashtags = parse(this.text).filter(x => x.type == 'hashtag').map(x => x.hashtag); | ||||
| 				const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[]; | ||||
| 				localStorage.setItem('hashtags', JSON.stringify(hashtags.concat(history).reduce((a, c) => a.includes(c) ? a : [...a, c], []))); | ||||
| 				localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history)))); | ||||
| 			} | ||||
| 		}, | ||||
|  | ||||
| @@ -514,7 +520,7 @@ root(isDark) | ||||
| 				margin-right 8px | ||||
| 				white-space nowrap | ||||
|  | ||||
| 		> .medias | ||||
| 		> .files | ||||
| 			margin 0 | ||||
| 			padding 0 | ||||
| 			background isDark ? #181b23 : lighten($theme-color, 98%) | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <template> | ||||
| <div class="mk-renote-form"> | ||||
| 	<mk-note-preview :note="note"/> | ||||
| 	<mk-note-preview class="preview" :note="note"/> | ||||
| 	<template v-if="!quote"> | ||||
| 		<footer> | ||||
| 			<a class="quote" v-if="!quote" @click="onQuote">%i18n:@quote%</a> | ||||
| @@ -61,7 +61,7 @@ export default Vue.extend({ | ||||
|  | ||||
| root(isDark) | ||||
|  | ||||
| 	> .mk-note-preview | ||||
| 	> .preview | ||||
| 		margin 16px 22px | ||||
|  | ||||
| 	> footer | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| <template> | ||||
| <div class="root"> | ||||
| 	<template v-if="!fetching"> | ||||
| 		<el-progress :text-inside="true" :stroke-width="18" :percentage="Math.floor((usage / capacity) * 100)"/> | ||||
| 		<p><b>{{ capacity | bytes }}</b>%i18n:max%<b>{{ usage | bytes }}</b>%i18n:in-use%</p> | ||||
| 	</template> | ||||
| </div> | ||||
|   | ||||
| @@ -19,7 +19,7 @@ | ||||
| 	</label> | ||||
| 	<label class="ui from group"> | ||||
| 		<p>%i18n:@birthday%</p> | ||||
| 		<el-date-picker v-model="birthday" type="date" value-format="yyyy-MM-dd"/> | ||||
| 		<input type="date" v-model="birthday"/> | ||||
| 	</label> | ||||
| 	<button class="ui primary" @click="save">%i18n:@save%</button> | ||||
| 	<section> | ||||
| @@ -30,6 +30,7 @@ | ||||
| 		<h2>%i18n:@other%</h2> | ||||
| 		<mk-switch v-model="$store.state.i.isBot" @change="onChangeIsBot" text="%i18n:@is-bot%"/> | ||||
| 		<mk-switch v-model="$store.state.i.isCat" @change="onChangeIsCat" text="%i18n:@is-cat%"/> | ||||
| 		<mk-switch v-model="alwaysMarkNsfw" text="%i18n:common.always-mark-nsfw%"/> | ||||
| 	</section> | ||||
| </div> | ||||
| </template> | ||||
| @@ -46,6 +47,12 @@ export default Vue.extend({ | ||||
| 			birthday: null, | ||||
| 		}; | ||||
| 	}, | ||||
| 	computed: { | ||||
| 		alwaysMarkNsfw: { | ||||
| 			get() { return this.$store.state.i.settings.alwaysMarkNsfw; }, | ||||
| 			set(value) { (this as any).api('i/update', { alwaysMarkNsfw: value }); } | ||||
| 		}, | ||||
| 	}, | ||||
| 	created() { | ||||
| 		this.name = this.$store.state.i.name || ''; | ||||
| 		this.location = this.$store.state.i.profile.location; | ||||
|   | ||||
| @@ -20,12 +20,28 @@ | ||||
|  | ||||
| 		<section class="web" v-show="page == 'web'"> | ||||
| 			<h1>%i18n:@behaviour%</h1> | ||||
| 			<mk-switch v-model="$store.state.settings.fetchOnScroll" @change="onChangeFetchOnScroll" text="%i18n:@fetch-on-scroll%"> | ||||
| 			<mk-switch v-model="fetchOnScroll" text="%i18n:@fetch-on-scroll%"> | ||||
| 				<span>%i18n:@fetch-on-scroll-desc%</span> | ||||
| 			</mk-switch> | ||||
| 			<mk-switch v-model="autoPopout" text="%i18n:@auto-popout%"> | ||||
| 				<span>%i18n:@auto-popout-desc%</span> | ||||
| 			</mk-switch> | ||||
|  | ||||
| 			<section> | ||||
| 				<header>%i18n:@note-visibility%</header> | ||||
| 				<mk-switch v-model="rememberNoteVisibility" text="%i18n:@remember-note-visibility%"/> | ||||
| 				<section> | ||||
| 					<header>%i18n:@default-note-visibility%</header> | ||||
| 					<ui-select v-model="defaultNoteVisibility"> | ||||
| 						<option value="public">%i18n:common.note-visibility.public%</option> | ||||
| 						<option value="home">%i18n:common.note-visibility.home%</option> | ||||
| 						<option value="followers">%i18n:common.note-visibility.followers%</option> | ||||
| 						<option value="specified">%i18n:common.note-visibility.specified%</option> | ||||
| 						<option value="private">%i18n:common.note-visibility.private%</option> | ||||
| 					</ui-select> | ||||
| 				</section> | ||||
| 			</section> | ||||
|  | ||||
| 			<details> | ||||
| 				<summary>%i18n:@advanced%</summary> | ||||
| 				<mk-switch v-model="apiViaStream" text="%i18n:@api-via-stream%"> | ||||
| @@ -43,23 +59,26 @@ | ||||
| 				<button class="ui" @click="updateWallpaper">%i18n:@choose-wallpaper%</button> | ||||
| 				<button class="ui" @click="deleteWallpaper">%i18n:@delete-wallpaper%</button> | ||||
| 				<mk-switch v-model="darkmode" text="%i18n:@dark-mode%"/> | ||||
| 				<mk-switch v-model="$store.state.settings.circleIcons" @change="onChangeCircleIcons" text="%i18n:@circle-icons%"/> | ||||
| 				<mk-switch v-model="$store.state.settings.gradientWindowHeader" @change="onChangeGradientWindowHeader" text="%i18n:@gradient-window-header%"/> | ||||
| 				<mk-switch v-model="$store.state.settings.iLikeSushi" @change="onChangeILikeSushi" text="%i18n:common.i-like-sushi%"/> | ||||
| 				<mk-switch v-model="circleIcons" text="%i18n:@circle-icons%"/> | ||||
| 				<mk-switch v-model="contrastedAcct" text="%i18n:@contrasted-acct%"/> | ||||
| 				<mk-switch v-model="showFullAcct" text="%i18n:common.show-full-acct%"/> | ||||
| 				<mk-switch v-model="gradientWindowHeader" text="%i18n:@gradient-window-header%"/> | ||||
| 				<mk-switch v-model="iLikeSushi" text="%i18n:common.i-like-sushi%"/> | ||||
| 			</div> | ||||
| 			<mk-switch v-model="$store.state.settings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="%i18n:@post-form-on-timeline%"/> | ||||
| 			<mk-switch v-model="$store.state.settings.suggestRecentHashtags" @change="onChangeSuggestRecentHashtags" text="%i18n:@suggest-recent-hashtags%"/> | ||||
| 			<mk-switch v-model="$store.state.settings.showClockOnHeader" @change="onChangeShowClockOnHeader" text="%i18n:@show-clock-on-header%"/> | ||||
| 			<mk-switch v-model="$store.state.settings.showReplyTarget" @change="onChangeShowReplyTarget" text="%i18n:@show-reply-target%"/> | ||||
| 			<mk-switch v-model="$store.state.settings.showMyRenotes" @change="onChangeShowMyRenotes" text="%i18n:@show-my-renotes%"/> | ||||
| 			<mk-switch v-model="$store.state.settings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes" text="%i18n:@show-renoted-my-notes%"/> | ||||
| 			<mk-switch v-model="$store.state.settings.showLocalRenotes" @change="onChangeShowLocalRenotes" text="%i18n:@show-local-renotes%"/> | ||||
| 			<mk-switch v-model="$store.state.settings.showMaps" @change="onChangeShowMaps" text="%i18n:@show-maps%"> | ||||
| 			<mk-switch v-model="showPostFormOnTopOfTl" text="%i18n:@post-form-on-timeline%"/> | ||||
| 			<mk-switch v-model="suggestRecentHashtags" text="%i18n:@suggest-recent-hashtags%"/> | ||||
| 			<mk-switch v-model="showClockOnHeader" text="%i18n:@show-clock-on-header%"/> | ||||
| 			<mk-switch v-model="alwaysShowNsfw" text="%i18n:common.always-show-nsfw%"/> | ||||
| 			<mk-switch v-model="showReplyTarget" text="%i18n:@show-reply-target%"/> | ||||
| 			<mk-switch v-model="showMyRenotes" text="%i18n:@show-my-renotes%"/> | ||||
| 			<mk-switch v-model="showRenotedMyNotes" text="%i18n:@show-renoted-my-notes%"/> | ||||
| 			<mk-switch v-model="showLocalRenotes" text="%i18n:@show-local-renotes%"/> | ||||
| 			<mk-switch v-model="showMaps" text="%i18n:@show-maps%"> | ||||
| 				<span>%i18n:@show-maps-desc%</span> | ||||
| 			</mk-switch> | ||||
| 			<mk-switch v-model="$store.state.settings.disableAnimatedMfm" @change="onChangeDisableAnimatedMfm" text="%i18n:common.disable-animated-mfm%"/> | ||||
| 			<mk-switch v-model="$store.state.settings.games.reversi.showBoardLabels" @change="onChangeReversiBoardLabels" text="%i18n:common.show-reversi-board-labels%"/> | ||||
| 			<mk-switch v-model="$store.state.settings.games.reversi.useContrastStones" @change="onChangeUseContrastReversiStones" text="%i18n:common.use-contrast-reversi-stones%"/> | ||||
| 			<mk-switch v-model="disableAnimatedMfm" text="%i18n:common.disable-animated-mfm%"/> | ||||
| 			<mk-switch v-model="games_reversi_showBoardLabels" text="%i18n:common.show-reversi-board-labels%"/> | ||||
| 			<mk-switch v-model="games_reversi_useContrastStones" text="%i18n:common.use-contrast-reversi-stones%"/> | ||||
| 		</section> | ||||
|  | ||||
| 		<section class="web" v-show="page == 'web'"> | ||||
| @@ -68,32 +87,31 @@ | ||||
| 				<span>%i18n:@enable-sounds-desc%</span> | ||||
| 			</mk-switch> | ||||
| 			<label>%i18n:@volume%</label> | ||||
| 			<el-slider | ||||
| 			<input type="range" | ||||
| 				v-model="soundVolume" | ||||
| 				:show-input="true" | ||||
| 				:format-tooltip="v => `${v * 100}%`" | ||||
| 				:disabled="!enableSounds" | ||||
| 				:max="1" | ||||
| 				:step="0.1" | ||||
| 				max="1" | ||||
| 				step="0.1" | ||||
| 			/> | ||||
| 			<button class="ui button" @click="soundTest">%fa:volume-up% %i18n:@test%</button> | ||||
| 		</section> | ||||
|  | ||||
| 		<section class="web" v-show="page == 'web'"> | ||||
| 			<h1>%i18n:@mobile%</h1> | ||||
| 			<mk-switch v-model="$store.state.settings.disableViaMobile" @change="onChangeDisableViaMobile" text="%i18n:@disable-via-mobile%"/> | ||||
| 			<mk-switch v-model="disableViaMobile" text="%i18n:@disable-via-mobile%"/> | ||||
| 		</section> | ||||
|  | ||||
| 		<section class="web" v-show="page == 'web'"> | ||||
| 			<h1>%i18n:@language%</h1> | ||||
| 			<el-select v-model="lang" placeholder="%i18n:@pick-language%"> | ||||
| 				<el-option-group label="%i18n:@recommended%"> | ||||
| 					<el-option label="%i18n:@auto%" :value="null"/> | ||||
| 				</el-option-group> | ||||
| 				<el-option-group label="%i18n:@specify-language%"> | ||||
| 					<el-option v-for="x in langs" :label="x[1]" :value="x[0]" :key="x[0]"/> | ||||
| 				</el-option-group> | ||||
| 			</el-select> | ||||
| 			<select v-model="lang" placeholder="%i18n:@pick-language%"> | ||||
| 				<optgroup label="%i18n:@recommended%"> | ||||
| 					<option value="">%i18n:@auto%</option> | ||||
| 				</optgroup> | ||||
|  | ||||
| 				<optgroup label="%i18n:@specify-language%"> | ||||
| 					<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option> | ||||
| 				</optgroup> | ||||
| 			</select> | ||||
| 			<div class="none ui info"> | ||||
| 				<p>%fa:info-circle%%i18n:@language-desc%</p> | ||||
| 			</div> | ||||
| @@ -188,10 +206,6 @@ | ||||
| 			<mk-switch v-model="enableExperimentalFeatures" text="%i18n:@experimental%"> | ||||
| 				<span>%i18n:@experimental-desc%</span> | ||||
| 			</mk-switch> | ||||
| 			<details v-if="debug"> | ||||
| 				<summary>%i18n:@tools%</summary> | ||||
| 				<button class="ui button block" @click="taskmngr">%i18n:@task-manager%</button> | ||||
| 			</details> | ||||
| 		</section> | ||||
| 	</div> | ||||
| </div> | ||||
| @@ -209,7 +223,6 @@ import XSignins from './settings.signins.vue'; | ||||
| import XDrive from './settings.drive.vue'; | ||||
| import { url, langs, version } from '../../../config'; | ||||
| import checkForUpdate from '../../../common/scripts/check-for-update'; | ||||
| import MkTaskManager from './taskmanager.vue'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| @@ -276,7 +289,112 @@ export default Vue.extend({ | ||||
| 		enableExperimentalFeatures: { | ||||
| 			get() { return this.$store.state.device.enableExperimentalFeatures; }, | ||||
| 			set(value) { this.$store.commit('device/set', { key: 'enableExperimentalFeatures', value }); } | ||||
| 		} | ||||
| 		}, | ||||
|  | ||||
| 		alwaysShowNsfw: { | ||||
| 			get() { return this.$store.state.device.alwaysShowNsfw; }, | ||||
| 			set(value) { this.$store.commit('device/set', { key: 'alwaysShowNsfw', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		fetchOnScroll: { | ||||
| 			get() { return this.$store.state.settings.fetchOnScroll; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'fetchOnScroll', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		rememberNoteVisibility: { | ||||
| 			get() { return this.$store.state.settings.rememberNoteVisibility; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'rememberNoteVisibility', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		defaultNoteVisibility: { | ||||
| 			get() { return this.$store.state.settings.defaultNoteVisibility; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'defaultNoteVisibility', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		showReplyTarget: { | ||||
| 			get() { return this.$store.state.settings.showReplyTarget; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'showReplyTarget', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		showMyRenotes: { | ||||
| 			get() { return this.$store.state.settings.showMyRenotes; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'showMyRenotes', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		showRenotedMyNotes: { | ||||
| 			get() { return this.$store.state.settings.showRenotedMyNotes; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'showRenotedMyNotes', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		showLocalRenotes: { | ||||
| 			get() { return this.$store.state.settings.showLocalRenotes; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'showLocalRenotes', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		showPostFormOnTopOfTl: { | ||||
| 			get() { return this.$store.state.settings.showPostFormOnTopOfTl; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'showPostFormOnTopOfTl', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		suggestRecentHashtags: { | ||||
| 			get() { return this.$store.state.settings.suggestRecentHashtags; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'suggestRecentHashtags', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		showClockOnHeader: { | ||||
| 			get() { return this.$store.state.settings.showClockOnHeader; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'showClockOnHeader', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		showMaps: { | ||||
| 			get() { return this.$store.state.settings.showMaps; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'showMaps', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		circleIcons: { | ||||
| 			get() { return this.$store.state.settings.circleIcons; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'circleIcons', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		contrastedAcct: { | ||||
| 			get() { return this.$store.state.settings.contrastedAcct; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'contrastedAcct', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		showFullAcct: { | ||||
| 			get() { return this.$store.state.settings.showFullAcct; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'showFullAcct', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		iLikeSushi: { | ||||
| 			get() { return this.$store.state.settings.iLikeSushi; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'iLikeSushi', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		games_reversi_showBoardLabels: { | ||||
| 			get() { return this.$store.state.settings.games.reversi.showBoardLabels; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'games.reversi.showBoardLabels', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		games_reversi_useContrastStones: { | ||||
| 			get() { return this.$store.state.settings.games.reversi.useContrastStones; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'games.reversi.useContrastStones', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		disableAnimatedMfm: { | ||||
| 			get() { return this.$store.state.settings.disableAnimatedMfm; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'disableAnimatedMfm', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		disableViaMobile: { | ||||
| 			get() { return this.$store.state.settings.disableViaMobile; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'disableViaMobile', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		gradientWindowHeader: { | ||||
| 			get() { return this.$store.state.settings.gradientWindowHeader; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'gradientWindowHeader', value }); } | ||||
| 		}, | ||||
| 	}, | ||||
| 	created() { | ||||
| 		(this as any).os.getMeta().then(meta => { | ||||
| @@ -284,9 +402,6 @@ export default Vue.extend({ | ||||
| 		}); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		taskmngr() { | ||||
| 			(this as any).os.new(MkTaskManager); | ||||
| 		}, | ||||
| 		customizeHome() { | ||||
| 			this.$router.push('/i/customize-home'); | ||||
| 			this.$emit('done'); | ||||
| @@ -305,113 +420,11 @@ export default Vue.extend({ | ||||
| 				wallpaperId: null | ||||
| 			}); | ||||
| 		}, | ||||
| 		onChangeFetchOnScroll(v) { | ||||
| 			this.$store.dispatch('settings/set', { | ||||
| 				key: 'fetchOnScroll', | ||||
| 				value: v | ||||
| 			}); | ||||
| 		}, | ||||
| 		onChangeAutoWatch(v) { | ||||
| 			(this as any).api('i/update', { | ||||
| 				autoWatch: v | ||||
| 			}); | ||||
| 		}, | ||||
| 		onChangeDark(v) { | ||||
| 			this.$store.dispatch('settings/set', { | ||||
| 				key: 'dark', | ||||
| 				value: v | ||||
| 			}); | ||||
| 		}, | ||||
| 		onChangeShowPostFormOnTopOfTl(v) { | ||||
| 			this.$store.dispatch('settings/set', { | ||||
| 				key: 'showPostFormOnTopOfTl', | ||||
| 				value: v | ||||
| 			}); | ||||
| 		}, | ||||
| 		onChangeSuggestRecentHashtags(v) { | ||||
| 			this.$store.dispatch('settings/set', { | ||||
| 				key: 'suggestRecentHashtags', | ||||
| 				value: v | ||||
| 			}); | ||||
| 		}, | ||||
| 		onChangeShowClockOnHeader(v) { | ||||
| 			this.$store.dispatch('settings/set', { | ||||
| 				key: 'showClockOnHeader', | ||||
| 				value: v | ||||
| 			}); | ||||
| 		}, | ||||
| 		onChangeShowReplyTarget(v) { | ||||
| 			this.$store.dispatch('settings/set', { | ||||
| 				key: 'showReplyTarget', | ||||
| 				value: v | ||||
| 			}); | ||||
| 		}, | ||||
| 		onChangeShowMyRenotes(v) { | ||||
| 			this.$store.dispatch('settings/set', { | ||||
| 				key: 'showMyRenotes', | ||||
| 				value: v | ||||
| 			}); | ||||
| 		}, | ||||
| 		onChangeShowRenotedMyNotes(v) { | ||||
| 			this.$store.dispatch('settings/set', { | ||||
| 				key: 'showRenotedMyNotes', | ||||
| 				value: v | ||||
| 			}); | ||||
| 		}, | ||||
| 		onChangeShowLocalRenotes(v) { | ||||
| 			this.$store.dispatch('settings/set', { | ||||
| 				key: 'showLocalRenotes', | ||||
| 				value: v | ||||
| 			}); | ||||
| 		}, | ||||
| 		onChangeShowMaps(v) { | ||||
| 			this.$store.dispatch('settings/set', { | ||||
| 				key: 'showMaps', | ||||
| 				value: v | ||||
| 			}); | ||||
| 		}, | ||||
| 		onChangeCircleIcons(v) { | ||||
| 			this.$store.dispatch('settings/set', { | ||||
| 				key: 'circleIcons', | ||||
| 				value: v | ||||
| 			}); | ||||
| 		}, | ||||
| 		onChangeILikeSushi(v) { | ||||
| 			this.$store.dispatch('settings/set', { | ||||
| 				key: 'iLikeSushi', | ||||
| 				value: v | ||||
| 			}); | ||||
| 		}, | ||||
| 		onChangeReversiBoardLabels(v) { | ||||
| 			this.$store.dispatch('settings/set', { | ||||
| 				key: 'games.reversi.showBoardLabels', | ||||
| 				value: v | ||||
| 			}); | ||||
| 		}, | ||||
| 		onChangeUseContrastReversiStones(v) { | ||||
| 			this.$store.dispatch('settings/set', { | ||||
| 				key: 'games.reversi.useContrastStones', | ||||
| 				value: v | ||||
| 			}); | ||||
| 		}, | ||||
| 		onChangeDisableAnimatedMfm(v) { | ||||
| 			this.$store.dispatch('settings/set', { | ||||
| 				key: 'disableAnimatedMfm', | ||||
| 				value: v | ||||
| 			}); | ||||
| 		}, | ||||
| 		onChangeGradientWindowHeader(v) { | ||||
| 			this.$store.dispatch('settings/set', { | ||||
| 				key: 'gradientWindowHeader', | ||||
| 				value: v | ||||
| 			}); | ||||
| 		}, | ||||
| 		onChangeDisableViaMobile(v) { | ||||
| 			this.$store.dispatch('settings/set', { | ||||
| 				key: 'disableViaMobile', | ||||
| 				value: v | ||||
| 			}); | ||||
| 		}, | ||||
| 		checkForUpdate() { | ||||
| 			this.checkingForUpdate = true; | ||||
| 			checkForUpdate((this as any).os, true, true).then(newer => { | ||||
|   | ||||
| @@ -7,9 +7,9 @@ | ||||
| 		<misskey-flavored-markdown v-if="note.text" :text="note.text" :i="$store.state.i"/> | ||||
| 		<a class="rp" v-if="note.renoteId" :href="`/notes/${note.renoteId}`">RP: ...</a> | ||||
| 	</div> | ||||
| 	<details v-if="note.media.length > 0"> | ||||
| 		<summary>({{ '%i18n:@media-count%'.replace('{}', note.media.length) }})</summary> | ||||
| 		<mk-media-list :media-list="note.media"/> | ||||
| 	<details v-if="note.files.length > 0"> | ||||
| 		<summary>({{ '%i18n:@media-count%'.replace('{}', note.files.length) }})</summary> | ||||
| 		<mk-media-list :media-list="note.files"/> | ||||
| 	</details> | ||||
| 	<details v-if="note.poll"> | ||||
| 		<summary>%i18n:@poll%</summary> | ||||
|   | ||||
| @@ -1,219 +0,0 @@ | ||||
| <template> | ||||
| <mk-window ref="window" width="750px" height="500px" @closed="$destroy" name="TaskManager"> | ||||
| 	<span slot="header" :class="$style.header">%fa:stethoscope%%i18n:@title%</span> | ||||
| 	<el-tabs :class="$style.content"> | ||||
| 		<el-tab-pane label="Requests"> | ||||
| 			<el-table | ||||
| 				:data="os.requests" | ||||
| 				style="width: 100%" | ||||
| 				:default-sort="{prop: 'date', order: 'descending'}" | ||||
| 			> | ||||
| 				<el-table-column type="expand"> | ||||
| 					<template slot-scope="props"> | ||||
| 						<pre>{{ props.row.data }}</pre> | ||||
| 						<pre>{{ props.row.res }}</pre> | ||||
| 					</template> | ||||
| 				</el-table-column> | ||||
|  | ||||
| 				<el-table-column | ||||
| 					label="Requested at" | ||||
| 					prop="date" | ||||
| 					sortable | ||||
| 				> | ||||
| 					<template slot-scope="scope"> | ||||
| 						<b style="margin-right: 8px">{{ scope.row.date.getTime() }}</b> | ||||
| 						<span>(<mk-time :time="scope.row.date"/>)</span> | ||||
| 					</template> | ||||
| 				</el-table-column> | ||||
|  | ||||
| 				<el-table-column | ||||
| 					label="Name" | ||||
| 				> | ||||
| 					<template slot-scope="scope"> | ||||
| 						<b>{{ scope.row.name }}</b> | ||||
| 					</template> | ||||
| 				</el-table-column> | ||||
|  | ||||
| 				<el-table-column | ||||
| 					label="Status" | ||||
| 				> | ||||
| 					<template slot-scope="scope"> | ||||
| 						<span>{{ scope.row.status || '(pending)' }}</span> | ||||
| 					</template> | ||||
| 				</el-table-column> | ||||
| 			</el-table> | ||||
| 		</el-tab-pane> | ||||
|  | ||||
| 		<el-tab-pane label="Streams"> | ||||
| 			<el-table | ||||
| 				:data="os.connections" | ||||
| 				style="width: 100%" | ||||
| 			> | ||||
| 				<el-table-column | ||||
| 					label="Uptime" | ||||
| 				> | ||||
| 					<template slot-scope="scope"> | ||||
| 						<mk-timer v-if="scope.row.connectedAt" :time="scope.row.connectedAt"/> | ||||
| 						<span v-else>-</span> | ||||
| 					</template> | ||||
| 				</el-table-column> | ||||
|  | ||||
| 				<el-table-column | ||||
| 					label="Name" | ||||
| 				> | ||||
| 					<template slot-scope="scope"> | ||||
| 						<b>{{ scope.row.name == '' ? '[Home]' : scope.row.name }}</b> | ||||
| 					</template> | ||||
| 				</el-table-column> | ||||
|  | ||||
| 				<el-table-column | ||||
| 					label="User" | ||||
| 				> | ||||
| 					<template slot-scope="scope"> | ||||
| 						<span>{{ scope.row.user || '(anonymous)' }}</span> | ||||
| 					</template> | ||||
| 				</el-table-column> | ||||
|  | ||||
| 				<el-table-column | ||||
| 					prop="state" | ||||
| 					label="State" | ||||
| 				/> | ||||
|  | ||||
| 				<el-table-column | ||||
| 					prop="in" | ||||
| 					label="In" | ||||
| 				/> | ||||
|  | ||||
| 				<el-table-column | ||||
| 					prop="out" | ||||
| 					label="Out" | ||||
| 				/> | ||||
| 			</el-table> | ||||
| 		</el-tab-pane> | ||||
|  | ||||
| 		<el-tab-pane label="Streams (Inspect)"> | ||||
| 			<el-tabs type="card" style="height:50%"> | ||||
| 				<el-tab-pane v-for="c in os.connections" :label="c.name == '' ? '[Home]' : c.name" :key="c.id" :name="c.id" ref="connectionsTab"> | ||||
| 					<div style="padding: 12px 0 0 12px"> | ||||
| 					<el-button size="mini" @click="send(c)">Send</el-button> | ||||
| 					<el-button size="mini" type="warning" @click="c.isSuspended = true" v-if="!c.isSuspended">Suspend</el-button> | ||||
| 					<el-button size="mini" type="success" @click="c.isSuspended = false" v-else>Resume</el-button> | ||||
| 					<el-button size="mini" type="danger" @click="c.close">Disconnect</el-button> | ||||
| 				</div> | ||||
|  | ||||
| 					<el-table | ||||
| 						:data="c.inout" | ||||
| 						style="width: 100%" | ||||
| 						:default-sort="{prop: 'at', order: 'descending'}" | ||||
| 					> | ||||
| 						<el-table-column type="expand"> | ||||
| 							<template slot-scope="props"> | ||||
| 								<pre>{{ props.row.data }}</pre> | ||||
| 							</template> | ||||
| 						</el-table-column> | ||||
|  | ||||
| 						<el-table-column | ||||
| 							label="Date" | ||||
| 							prop="at" | ||||
| 							sortable | ||||
| 						> | ||||
| 							<template slot-scope="scope"> | ||||
| 								<b style="margin-right: 8px">{{ scope.row.at.getTime() }}</b> | ||||
| 								<span>(<mk-time :time="scope.row.at"/>)</span> | ||||
| 							</template> | ||||
| 						</el-table-column> | ||||
|  | ||||
| 						<el-table-column | ||||
| 							label="Type" | ||||
| 						> | ||||
| 							<template slot-scope="scope"> | ||||
| 								<span>{{ getMessageType(scope.row.data) }}</span> | ||||
| 							</template> | ||||
| 						</el-table-column> | ||||
|  | ||||
| 						<el-table-column | ||||
| 							label="Incoming / Outgoing" | ||||
| 							prop="type" | ||||
| 						/> | ||||
| 					</el-table> | ||||
| 				</el-tab-pane> | ||||
| 			</el-tabs> | ||||
| 		</el-tab-pane> | ||||
|  | ||||
| 		<el-tab-pane label="Windows"> | ||||
| 			<el-table | ||||
| 				:data="Array.from(os.windows.windows)" | ||||
| 				style="width: 100%" | ||||
| 			> | ||||
| 				<el-table-column | ||||
| 					label="Name" | ||||
| 				> | ||||
| 					<template slot-scope="scope"> | ||||
| 						<b>{{ scope.row.name || '(unknown)' }}</b> | ||||
| 					</template> | ||||
| 				</el-table-column> | ||||
|  | ||||
| 				<el-table-column | ||||
| 					label="Operations" | ||||
| 				> | ||||
| 					<template slot-scope="scope"> | ||||
| 						<el-button size="mini" type="danger" @click="scope.row.close">Close</el-button> | ||||
| 					</template> | ||||
| 				</el-table-column> | ||||
| 			</el-table> | ||||
| 		</el-tab-pane> | ||||
| 	</el-tabs> | ||||
| </mk-window> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	mounted() { | ||||
| 		(this as any).os.windows.on('added', this.onWindowsChanged); | ||||
| 		(this as any).os.windows.on('removed', this.onWindowsChanged); | ||||
| 	}, | ||||
| 	beforeDestroy() { | ||||
| 		(this as any).os.windows.off('added', this.onWindowsChanged); | ||||
| 		(this as any).os.windows.off('removed', this.onWindowsChanged); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		getMessageType(data): string { | ||||
| 			return data.type ? data.type : '-'; | ||||
| 		}, | ||||
| 		onWindowsChanged() { | ||||
| 			this.$forceUpdate(); | ||||
| 		}, | ||||
| 		send(c) { | ||||
| 			(this as any).apis.input({ | ||||
| 				title: 'Send a JSON message', | ||||
| 				allowEmpty: false | ||||
| 			}).then(json => { | ||||
| 				c.send(JSON.parse(json)); | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" module> | ||||
| .header | ||||
| 	> [data-fa] | ||||
| 		margin-right 4px | ||||
|  | ||||
| .content | ||||
| 	height 100% | ||||
| 	overflow auto | ||||
|  | ||||
| </style> | ||||
|  | ||||
| <style> | ||||
| .el-tabs__header { | ||||
| 	margin-bottom: 0 !important; | ||||
| } | ||||
|  | ||||
| .el-tabs__item { | ||||
| 	padding: 0 20px !important; | ||||
| } | ||||
| </style> | ||||
| @@ -2,8 +2,8 @@ | ||||
| <div class="mk-timeline"> | ||||
| 	<header> | ||||
| 		<span :data-active="src == 'home'" @click="src = 'home'">%fa:home% %i18n:@home%</span> | ||||
| 		<span :data-active="src == 'local'" @click="src = 'local'">%fa:R comments% %i18n:@local%</span> | ||||
| 		<span :data-active="src == 'hybrid'" @click="src = 'hybrid'">%fa:share-alt% %i18n:@hybrid%</span> | ||||
| 		<span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline">%fa:R comments% %i18n:@local%</span> | ||||
| 		<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline">%fa:share-alt% %i18n:@hybrid%</span> | ||||
| 		<span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span> | ||||
| 		<span :data-active="src == 'list'" @click="src = 'list'" v-if="list">%fa:list% {{ list.title }}</span> | ||||
| 		<button @click="chooseList" title="%i18n:@list%">%fa:list%</button> | ||||
| @@ -29,7 +29,8 @@ export default Vue.extend({ | ||||
| 	data() { | ||||
| 		return { | ||||
| 			src: 'home', | ||||
| 			list: null | ||||
| 			list: null, | ||||
| 			enableLocalTimeline: false | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| @@ -44,6 +45,10 @@ export default Vue.extend({ | ||||
| 	}, | ||||
|  | ||||
| 	created() { | ||||
| 		(this as any).os.getMeta().then(meta => { | ||||
| 			this.enableLocalTimeline = !meta.disableLocalTimeline; | ||||
| 		}); | ||||
|  | ||||
| 		if (this.$store.state.device.tl) { | ||||
| 			this.src = this.$store.state.device.tl.src; | ||||
| 			if (this.src == 'list') { | ||||
|   | ||||
| @@ -27,7 +27,7 @@ export default Vue.extend({ | ||||
| 					translateY: -64, | ||||
| 					duration: 500, | ||||
| 					easing: 'easeInElastic', | ||||
| 					complete: () => this.$destroy() | ||||
| 					complete: () => this.destroyDom() | ||||
| 				}); | ||||
| 			}, 6000); | ||||
| 		}); | ||||
|   | ||||
| @@ -75,7 +75,7 @@ export default Vue.extend({ | ||||
| 				'margin-top': '-8px', | ||||
| 				duration: 200, | ||||
| 				easing: 'easeOutQuad', | ||||
| 				complete: () => this.$destroy() | ||||
| 				complete: () => this.destroyDom() | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -1,17 +1,16 @@ | ||||
| <template> | ||||
| <div class="root item"> | ||||
| 	<mk-avatar class="avatar" :user="user"/> | ||||
| 	<div class="main"> | ||||
| 		<header> | ||||
| 			<router-link class="name" :to="user | userPage" v-user-preview="user.id">{{ user | userName }}</router-link> | ||||
| 			<span class="username">@{{ user | acct }}</span> | ||||
| 		</header> | ||||
| 		<div class="body"> | ||||
| 			<p class="followed" v-if="user.isFollowed">%i18n:@followed%</p> | ||||
| 			<div class="description">{{ user.description }}</div> | ||||
| <div class="zvdbznxvfixtmujpsigoccczftvpiwqh"> | ||||
| 	<div class="banner" :style="bannerStyle"></div> | ||||
| 	<mk-avatar class="avatar" :user="user" :disable-preview="true"/> | ||||
| 	<div class="body"> | ||||
| 		<router-link :to="user | userPage" class="name">{{ user | userName }}</router-link> | ||||
| 		<span class="username">@{{ user | acct }}</span> | ||||
| 		<div class="description"> | ||||
| 			<misskey-flavored-markdown v-if="user.description" :text="user.description" :i="$store.state.i"/> | ||||
| 		</div> | ||||
| 		<p class="followed" v-if="user.isFollowed">%i18n:@followed%</p> | ||||
| 		<mk-follow-button :user="user" :size="'big'"/> | ||||
| 	</div> | ||||
| 	<mk-follow-button :user="user"/> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| @@ -19,76 +18,69 @@ | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	props: ['user'] | ||||
| 	props: ['user'], | ||||
|  | ||||
| 	computed: { | ||||
| 		bannerStyle(): any { | ||||
| 			if (this.user.bannerUrl == null) return {}; | ||||
| 			return { | ||||
| 				backgroundColor: this.user.bannerColor && this.user.bannerColor.length == 3 ? `rgb(${ this.user.bannerColor.join(',') })` : null, | ||||
| 				backgroundImage: `url(${ this.user.bannerUrl })` | ||||
| 			}; | ||||
| 		} | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| .root.item | ||||
| 	padding 16px | ||||
| 	font-size 16px | ||||
| .zvdbznxvfixtmujpsigoccczftvpiwqh | ||||
| 	$bg = #fff | ||||
|  | ||||
| 	&:after | ||||
| 		content "" | ||||
| 		display block | ||||
| 		clear both | ||||
| 	margin 16px auto | ||||
| 	max-width calc(100% - 32px) | ||||
| 	font-size 16px | ||||
| 	text-align center | ||||
| 	background $bg | ||||
| 	box-shadow 0 2px 4px rgba(0, 0, 0, 0.1) | ||||
|  | ||||
| 	> .banner | ||||
| 		height 100px | ||||
| 		background-color #f9f4f4 | ||||
| 		background-position center | ||||
| 		background-size cover | ||||
|  | ||||
| 	> .avatar | ||||
| 		display block | ||||
| 		float left | ||||
| 		margin 0 16px 0 0 | ||||
| 		width 58px | ||||
| 		height 58px | ||||
| 		border-radius 8px | ||||
| 		margin -40px auto 0 auto | ||||
| 		width 80px | ||||
| 		height 80px | ||||
| 		border-radius 100% | ||||
| 		border solid 4px $bg | ||||
|  | ||||
| 	> .main | ||||
| 		float left | ||||
| 		width calc(100% - 74px) | ||||
| 	> .body | ||||
| 		padding 4px 32px 32px 32px | ||||
|  | ||||
| 		> header | ||||
| 			margin-bottom 2px | ||||
| 		@media (max-width 400px) | ||||
| 			padding 4px 16px 16px 16px | ||||
|  | ||||
| 			> .name | ||||
| 				display inline | ||||
| 				margin 0 | ||||
| 				padding 0 | ||||
| 				color #777 | ||||
| 				font-size 1em | ||||
| 				font-weight 700 | ||||
| 				text-align left | ||||
| 				text-decoration none | ||||
| 		> .name | ||||
| 			font-size 20px | ||||
| 			font-weight bold | ||||
|  | ||||
| 				&:hover | ||||
| 					text-decoration underline | ||||
| 		> .username | ||||
| 			display block | ||||
| 			opacity 0.7 | ||||
|  | ||||
| 			> .username | ||||
| 				text-align left | ||||
| 				margin 0 0 0 8px | ||||
| 				color #ccc | ||||
| 		> .description | ||||
| 			margin 16px 0 | ||||
|  | ||||
| 		> .body | ||||
| 			> .followed | ||||
| 				display inline-block | ||||
| 				margin 0 0 4px 0 | ||||
| 				padding 2px 8px | ||||
| 				vertical-align top | ||||
| 				font-size 10px | ||||
| 				color #71afc7 | ||||
| 				background #eefaff | ||||
| 				border-radius 4px | ||||
|  | ||||
| 			> .description | ||||
| 				cursor default | ||||
| 				display block | ||||
| 				margin 0 | ||||
| 				padding 0 | ||||
| 				overflow-wrap break-word | ||||
| 				font-size 1.1em | ||||
| 				color #717171 | ||||
|  | ||||
| 	> .mk-follow-button | ||||
| 		position absolute | ||||
| 		top 16px | ||||
| 		right 16px | ||||
| 		> .followed | ||||
| 			margin 0 0 16px 0 | ||||
| 			padding 0 | ||||
| 			line-height 24px | ||||
| 			font-size 0.8em | ||||
| 			color #71afc7 | ||||
| 			background #eefaff | ||||
| 			border-radius 4px | ||||
|  | ||||
| </style> | ||||
|   | ||||
| @@ -33,7 +33,7 @@ export default Vue.extend({ | ||||
| 	props: ['fetch', 'count', 'youKnowCount'], | ||||
| 	data() { | ||||
| 		return { | ||||
| 			limit: 30, | ||||
| 			limit: 20, | ||||
| 			mode: 'all', | ||||
| 			fetching: true, | ||||
| 			moreFetching: false, | ||||
| @@ -73,10 +73,14 @@ export default Vue.extend({ | ||||
|  | ||||
| .mk-users-list | ||||
| 	height 100% | ||||
| 	background #fff | ||||
| 	overflow auto | ||||
| 	background #eee | ||||
|  | ||||
| 	> nav | ||||
| 		z-index 1 | ||||
| 		z-index 10 | ||||
| 		position sticky | ||||
| 		top 0 | ||||
| 		background #fff | ||||
| 		box-shadow 0 1px 0 rgba(#000, 0.1) | ||||
|  | ||||
| 		> div | ||||
| @@ -114,16 +118,14 @@ export default Vue.extend({ | ||||
| 					background #eee | ||||
| 					border-radius 20px | ||||
|  | ||||
| 	> .users | ||||
| 		height calc(100% - 54px) | ||||
| 		overflow auto | ||||
| 	> button | ||||
| 		display block | ||||
| 		width calc(100% - 32px) | ||||
| 		margin 16px | ||||
| 		padding 16px | ||||
|  | ||||
| 		> * | ||||
| 			border-bottom solid 1px rgba(#000, 0.05) | ||||
|  | ||||
| 			> * | ||||
| 				max-width 600px | ||||
| 				margin 0 auto | ||||
| 		&:hover | ||||
| 			background rgba(#000, 0.1) | ||||
|  | ||||
| 	> .no | ||||
| 		margin 0 | ||||
|   | ||||
| @@ -106,7 +106,7 @@ export default Vue.extend({ | ||||
|  | ||||
| 	mounted() { | ||||
| 		if (this.preventMount) { | ||||
| 			this.$destroy(); | ||||
| 			this.destroyDom(); | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| @@ -190,7 +190,7 @@ export default Vue.extend({ | ||||
| 			}); | ||||
|  | ||||
| 			setTimeout(() => { | ||||
| 				this.$destroy(); | ||||
| 				this.destroyDom(); | ||||
| 				this.$emit('closed'); | ||||
| 			}, 300); | ||||
| 		}, | ||||
|   | ||||
| @@ -1,22 +1,34 @@ | ||||
| <template> | ||||
| <div class="obdskegsannmntldydackcpzezagxqfy mk-admin-card"> | ||||
| 	<header>%i18n:@dashboard%</header> | ||||
|  | ||||
| 	<div v-if="stats" class="stats"> | ||||
| 		<div><b>%fa:user% {{ stats.originalUsersCount | number }}</b><span>%i18n:@original-users%</span></div> | ||||
| 		<div><span>%fa:user% {{ stats.usersCount | number }}</span><span>%i18n:@all-users%</span></div> | ||||
| 		<div><b>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</b><span>%i18n:@original-notes%</span></div> | ||||
| 		<div><span>%fa:pencil-alt% {{ stats.notesCount | number }}</span><span>%i18n:@all-notes%</span></div> | ||||
| 	</div> | ||||
|  | ||||
| 	<div class="cpu-memory"> | ||||
| 		<x-cpu-memory :connection="connection"/> | ||||
| 	</div> | ||||
| 	<div> | ||||
| 		<label> | ||||
| 			<input type="checkbox" v-model="disableRegistration" @change="updateMeta"> | ||||
| 			<span>disableRegistration</span> | ||||
| 		</label> | ||||
| 		<button class="ui" @click="invite">%i18n:@invite%</button> | ||||
| 		<p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p> | ||||
|  | ||||
| 	<div class="form"> | ||||
| 		<div> | ||||
| 			<label> | ||||
| 				<input type="checkbox" v-model="disableRegistration" @change="updateMeta"> | ||||
| 				<span>%i18n:@disableRegistration%</span> | ||||
| 			</label> | ||||
| 			<button class="ui" @click="invite">%i18n:@invite%</button> | ||||
| 			<p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p> | ||||
| 		</div> | ||||
|  | ||||
| 		<div> | ||||
| 			<label> | ||||
| 				<input type="checkbox" v-model="disableLocalTimeline" @change="updateMeta"> | ||||
| 				<span>%i18n:@disableLocalTimeline%</span> | ||||
| 			</label> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
| @@ -33,6 +45,7 @@ export default Vue.extend({ | ||||
| 		return { | ||||
| 			stats: null, | ||||
| 			disableRegistration: false, | ||||
| 			disableLocalTimeline: false, | ||||
| 			inviteCode: null, | ||||
| 			connection: null, | ||||
| 			connectionId: null | ||||
| @@ -44,6 +57,7 @@ export default Vue.extend({ | ||||
|  | ||||
| 		(this as any).os.getMeta().then(meta => { | ||||
| 			this.disableRegistration = meta.disableRegistration; | ||||
| 			this.disableLocalTimeline = meta.disableLocalTimeline; | ||||
| 		}); | ||||
|  | ||||
| 		(this as any).api('stats').then(stats => { | ||||
| @@ -61,7 +75,8 @@ export default Vue.extend({ | ||||
| 		}, | ||||
| 		updateMeta() { | ||||
| 			(this as any).api('admin/update-meta', { | ||||
| 				disableRegistration: this.disableRegistration | ||||
| 				disableRegistration: this.disableRegistration, | ||||
| 				disableLocalTimeline: this.disableLocalTimeline | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| @@ -97,4 +112,8 @@ export default Vue.extend({ | ||||
| 		border solid 1px #eee | ||||
| 		border-radius: 8px | ||||
|  | ||||
| 	> .form | ||||
| 		> div | ||||
| 			border-bottom solid 1px #eee | ||||
|  | ||||
| </style> | ||||
|   | ||||
							
								
								
									
										41
									
								
								src/client/app/desktop/views/pages/admin/admin.hashtags.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/client/app/desktop/views/pages/admin/admin.hashtags.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| <template> | ||||
| <div class="jdnqwkzlnxcfftthoybjxrebyolvoucw mk-admin-card"> | ||||
| 	<header>%i18n:@hided-tags%</header> | ||||
| 	<textarea v-model="hidedTags"></textarea> | ||||
| 	<button class="ui" @click="save">%i18n:@save%</button> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from "vue"; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	data() { | ||||
| 		return { | ||||
| 			hidedTags: '', | ||||
| 		}; | ||||
| 	}, | ||||
| 	created() { | ||||
| 		(this as any).os.getMeta().then(meta => { | ||||
| 			this.hidedTags = meta.hidedTags.join('\n'); | ||||
| 		}); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		save() { | ||||
| 			(this as any).api('admin/update-meta', { | ||||
| 				hidedTags: this.hidedTags.split('\n') | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| @import '~const.styl' | ||||
|  | ||||
| .jdnqwkzlnxcfftthoybjxrebyolvoucw | ||||
| 	textarea | ||||
| 		width 100% | ||||
| 		min-height 300px | ||||
|  | ||||
| </style> | ||||
| @@ -5,6 +5,8 @@ | ||||
| 			<li @click="nav('dashboard')" :class="{ active: page == 'dashboard' }">%fa:chalkboard .fw%%i18n:@dashboard%</li> | ||||
| 			<li @click="nav('users')" :class="{ active: page == 'users' }">%fa:users .fw%%i18n:@users%</li> | ||||
| 			<li @click="nav('announcements')" :class="{ active: page == 'announcements' }">%fa:broadcast-tower .fw%%i18n:@announcements%</li> | ||||
| 			<li @click="nav('hashtags')" :class="{ active: page == 'hashtags' }">%fa:hashtag .fw%%i18n:@hashtags%</li> | ||||
|  | ||||
| 			<!-- <li @click="nav('drive')" :class="{ active: page == 'drive' }">%fa:cloud .fw%%i18n:@drive%</li> --> | ||||
| 			<!-- <li @click="nav('update')" :class="{ active: page == 'update' }">%i18n:@update%</li> --> | ||||
| 		</ul> | ||||
| @@ -17,6 +19,9 @@ | ||||
| 		<div v-show="page == 'announcements'"> | ||||
| 			<x-announcements/> | ||||
| 		</div> | ||||
| 		<div v-show="page == 'hashtags'"> | ||||
| 			<x-hashtags/> | ||||
| 		</div> | ||||
| 		<div v-if="page == 'users'"> | ||||
| 			<x-suspend-user/> | ||||
| 			<x-unsuspend-user/> | ||||
| @@ -33,6 +38,7 @@ | ||||
| import Vue from "vue"; | ||||
| import XDashboard from "./admin.dashboard.vue"; | ||||
| import XAnnouncements from "./admin.announcements.vue"; | ||||
| import XHashtags from "./admin.hashtags.vue"; | ||||
| import XSuspendUser from "./admin.suspend-user.vue"; | ||||
| import XUnsuspendUser from "./admin.unsuspend-user.vue"; | ||||
| import XVerifyUser from "./admin.verify-user.vue"; | ||||
| @@ -43,6 +49,7 @@ export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XDashboard, | ||||
| 		XAnnouncements, | ||||
| 		XHashtags, | ||||
| 		XSuspendUser, | ||||
| 		XUnsuspendUser, | ||||
| 		XVerifyUser, | ||||
|   | ||||
| @@ -28,6 +28,7 @@ | ||||
| import Vue from 'vue'; | ||||
| import Menu from '../../../../common/views/components/menu.vue'; | ||||
| import contextmenu from '../../../api/contextmenu'; | ||||
| import { countIf } from '../../../../../../prelude/array'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	props: { | ||||
| @@ -117,7 +118,7 @@ export default Vue.extend({ | ||||
| 		toggleActive() { | ||||
| 			if (!this.isStacked) return; | ||||
| 			const vms = this.$store.state.settings.deck.layout.find(ids => ids.indexOf(this.column.id) != -1).map(id => this.getColumnVm(id)); | ||||
| 			if (this.active && vms.filter(vm => vm.$el.classList.contains('active')).length == 1) return; | ||||
| 			if (this.active && countIf(vm => vm.$el.classList.contains('active'), vms) == 1) return; | ||||
| 			this.active = !this.active; | ||||
| 		}, | ||||
|  | ||||
|   | ||||
| @@ -68,7 +68,7 @@ export default Vue.extend({ | ||||
| 				(this as any).api('notes/user-list-timeline', { | ||||
| 					listId: this.list.id, | ||||
| 					limit: fetchLimit + 1, | ||||
| 					mediaOnly: this.mediaOnly, | ||||
| 					withFiles: this.mediaOnly, | ||||
| 					includeMyRenotes: this.$store.state.settings.showMyRenotes, | ||||
| 					includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, | ||||
| 					includeLocalRenotes: this.$store.state.settings.showLocalRenotes | ||||
| @@ -90,7 +90,7 @@ export default Vue.extend({ | ||||
| 				listId: this.list.id, | ||||
| 				limit: fetchLimit + 1, | ||||
| 				untilId: (this.$refs.timeline as any).tail().id, | ||||
| 				mediaOnly: this.mediaOnly, | ||||
| 				withFiles: this.mediaOnly, | ||||
| 				includeMyRenotes: this.$store.state.settings.showMyRenotes, | ||||
| 				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, | ||||
| 				includeLocalRenotes: this.$store.state.settings.showLocalRenotes | ||||
| @@ -109,7 +109,7 @@ export default Vue.extend({ | ||||
| 			return promise; | ||||
| 		}, | ||||
| 		onNote(note) { | ||||
| 			if (this.mediaOnly && note.media.length == 0) return; | ||||
| 			if (this.mediaOnly && note.files.length == 0) return; | ||||
|  | ||||
| 			// Prepend a note | ||||
| 			(this.$refs.timeline as any).prepend(note); | ||||
|   | ||||
| @@ -18,7 +18,7 @@ | ||||
| 			<div class="body"> | ||||
| 				<p v-if="p.cw != null" class="cw"> | ||||
| 					<span class="text" v-if="p.cw != ''">{{ p.cw }}</span> | ||||
| 					<span class="toggle" @click="showContent = !showContent">{{ showContent ? '%i18n:@less%' : '%i18n:@more%' }}</span> | ||||
| 					<mk-cw-button v-model="showContent"/> | ||||
| 				</p> | ||||
| 				<div class="content" v-show="p.cw == null || showContent"> | ||||
| 					<div class="text"> | ||||
| @@ -28,8 +28,8 @@ | ||||
| 						<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/> | ||||
| 						<a class="rp" v-if="p.renote != null">RP:</a> | ||||
| 					</div> | ||||
| 					<div class="media" v-if="p.media.length > 0"> | ||||
| 						<mk-media-list :media-list="p.media"/> | ||||
| 					<div class="files" v-if="p.files.length > 0"> | ||||
| 						<mk-media-list :media-list="p.files"/> | ||||
| 					</div> | ||||
| 					<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/> | ||||
| 					<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> | ||||
| @@ -54,11 +54,11 @@ | ||||
| 	</article> | ||||
| </div> | ||||
| <div v-else class="srwrkujossgfuhrbnvqkybtzxpblgchi"> | ||||
| 	<div v-if="note.media.length > 0"> | ||||
| 		<mk-media-list :media-list="note.media"/> | ||||
| 	<div v-if="note.files.length > 0"> | ||||
| 		<mk-media-list :media-list="note.files"/> | ||||
| 	</div> | ||||
| 	<div v-if="note.renote && note.renote.media.length > 0"> | ||||
| 		<mk-media-list :media-list="note.renote.media"/> | ||||
| 	<div v-if="note.renote && note.renote.files.length > 0"> | ||||
| 		<mk-media-list :media-list="note.renote.files"/> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
| @@ -100,7 +100,7 @@ export default Vue.extend({ | ||||
| 		isRenote(): boolean { | ||||
| 			return (this.note.renote && | ||||
| 				this.note.text == null && | ||||
| 				this.note.mediaIds.length == 0 && | ||||
| 				this.note.fileIds.length == 0 && | ||||
| 				this.note.poll == null); | ||||
| 		}, | ||||
|  | ||||
| @@ -371,7 +371,7 @@ root(isDark) | ||||
| 					.mk-url-preview | ||||
| 						margin-top 8px | ||||
|  | ||||
| 					> .media | ||||
| 					> .files | ||||
| 						> img | ||||
| 							display block | ||||
| 							max-width 100% | ||||
| @@ -394,7 +394,7 @@ root(isDark) | ||||
| 					> .renote | ||||
| 						margin 8px 0 | ||||
|  | ||||
| 						> .mk-note-preview | ||||
| 						> * | ||||
| 							padding 16px | ||||
| 							border dashed 1px isDark ? #4e945e : #c0dac6 | ||||
| 							border-radius 8px | ||||
|   | ||||
| @@ -127,7 +127,7 @@ export default Vue.extend({ | ||||
| 		prepend(note, silent = false) { | ||||
| 			//#region 弾く | ||||
| 			const isMyNote = note.userId == this.$store.state.i.id; | ||||
| 			const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null; | ||||
| 			const isPureRenote = note.renoteId != null && note.text == null && note.fileIds.length == 0 && note.poll == null; | ||||
|  | ||||
| 			if (this.$store.state.settings.showMyRenotes === false) { | ||||
| 				if (isMyNote && isPureRenote) { | ||||
|   | ||||
| @@ -96,7 +96,7 @@ export default Vue.extend({ | ||||
| 			(this.$refs.timeline as any).init(() => new Promise((res, rej) => { | ||||
| 				(this as any).api(this.endpoint, { | ||||
| 					limit: fetchLimit + 1, | ||||
| 					mediaOnly: this.mediaOnly, | ||||
| 					withFiles: this.mediaOnly, | ||||
| 					includeMyRenotes: this.$store.state.settings.showMyRenotes, | ||||
| 					includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, | ||||
| 					includeLocalRenotes: this.$store.state.settings.showLocalRenotes | ||||
| @@ -117,7 +117,7 @@ export default Vue.extend({ | ||||
|  | ||||
| 			const promise = (this as any).api(this.endpoint, { | ||||
| 				limit: fetchLimit + 1, | ||||
| 				mediaOnly: this.mediaOnly, | ||||
| 				withFiles: this.mediaOnly, | ||||
| 				untilId: (this.$refs.timeline as any).tail().id, | ||||
| 				includeMyRenotes: this.$store.state.settings.showMyRenotes, | ||||
| 				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, | ||||
| @@ -138,7 +138,7 @@ export default Vue.extend({ | ||||
| 		}, | ||||
|  | ||||
| 		onNote(note) { | ||||
| 			if (this.mediaOnly && note.media.length == 0) return; | ||||
| 			if (this.mediaOnly && note.files.length == 0) return; | ||||
|  | ||||
| 			// Prepend a note | ||||
| 			(this.$refs.timeline as any).prepend(note); | ||||
|   | ||||
| @@ -85,6 +85,7 @@ export default Vue.extend({ | ||||
| 	}, | ||||
|  | ||||
| 	mounted() { | ||||
| 		document.title = (this as any).os.instanceName; | ||||
| 		document.documentElement.style.overflow = 'hidden'; | ||||
| 	}, | ||||
|  | ||||
|   | ||||
| @@ -6,7 +6,7 @@ | ||||
| 		<div class="title"> | ||||
| 			<p class="name">{{ user | userName }}</p> | ||||
| 			<div> | ||||
| 				<span class="username"><mk-acct :user="user"/></span> | ||||
| 				<span class="username"><mk-acct :user="user" :detail="true" /></span> | ||||
| 				<span v-if="user.isBot" title="%i18n:@is-bot%">%fa:robot%</span> | ||||
| 				<span class="location" v-if="user.host === null && user.profile.location">%fa:map-marker% {{ user.profile.location }}</span> | ||||
| 				<span class="birthday" v-if="user.host === null && user.profile.birthday">%fa:birthday-cake% {{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ age }}歳)</span> | ||||
|   | ||||
| @@ -24,12 +24,12 @@ export default Vue.extend({ | ||||
| 	mounted() { | ||||
| 		(this as any).api('users/notes', { | ||||
| 			userId: this.user.id, | ||||
| 			withMedia: true, | ||||
| 			withFiles: true, | ||||
| 			limit: 9 | ||||
| 		}).then(notes => { | ||||
| 			notes.forEach(note => { | ||||
| 				note.media.forEach(media => { | ||||
| 					if (this.images.length < 9) this.images.push(media); | ||||
| 				note.files.forEach(file => { | ||||
| 					if (this.images.length < 9) this.images.push(file); | ||||
| 				}); | ||||
| 			}); | ||||
| 			this.fetching = false; | ||||
|   | ||||
| @@ -66,7 +66,7 @@ export default Vue.extend({ | ||||
| 					limit: fetchLimit + 1, | ||||
| 					untilDate: this.date ? this.date.getTime() : undefined, | ||||
| 					includeReplies: this.mode == 'with-replies', | ||||
| 					withMedia: this.mode == 'with-media' | ||||
| 					withFiles: this.mode == 'with-media' | ||||
| 				}).then(notes => { | ||||
| 					if (notes.length == fetchLimit + 1) { | ||||
| 						notes.pop(); | ||||
| @@ -86,7 +86,7 @@ export default Vue.extend({ | ||||
| 				userId: this.user.id, | ||||
| 				limit: fetchLimit + 1, | ||||
| 				includeReplies: this.mode == 'with-replies', | ||||
| 				withMedia: this.mode == 'with-media', | ||||
| 				withFiles: this.mode == 'with-media', | ||||
| 				untilId: (this.$refs.timeline as any).tail().id | ||||
| 			}); | ||||
|  | ||||
|   | ||||
| @@ -7,45 +7,130 @@ | ||||
|  | ||||
| 	<mk-forkit class="forkit"/> | ||||
|  | ||||
| 	<div class="body"> | ||||
| 		<div class="main block"> | ||||
| 			<h1 v-if="name != 'Misskey'">{{ name }}</h1> | ||||
| 			<h1 v-else><img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" :alt="name"></h1> | ||||
| 	<main> | ||||
| 		<div class="body"> | ||||
| 			<div class="main block"> | ||||
| 				<div> | ||||
| 					<h1 v-if="name != 'Misskey'">{{ name }}</h1> | ||||
| 					<h1 v-else><img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" :alt="name"></h1> | ||||
|  | ||||
| 			<div class="info"> | ||||
| 				<span><b>{{ host }}</b> - <span v-html="'%i18n:@powered-by-misskey%'"></span></span> | ||||
| 				<span class="stats" v-if="stats"> | ||||
| 					<span>%fa:user% {{ stats.originalUsersCount | number }}</span> | ||||
| 					<span>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</span> | ||||
| 				</span> | ||||
| 					<div class="info"> | ||||
| 						<span><b>{{ host }}</b> - <span v-html="'%i18n:@powered-by-misskey%'"></span></span> | ||||
| 						<span class="stats" v-if="stats"> | ||||
| 							<span>%fa:user% {{ stats.originalUsersCount | number }}</span> | ||||
| 							<span>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</span> | ||||
| 						</span> | ||||
| 					</div> | ||||
|  | ||||
| 					<div class="desc"> | ||||
| 						<span class="desc" v-html="description || '%i18n:common.about%'"></span> | ||||
| 						<a class="about" @click="about">%i18n:@about%</a> | ||||
| 					</div> | ||||
|  | ||||
| 					<p class="sign"> | ||||
| 						<span class="signup" @click="signup">%i18n:@signup%</span> | ||||
| 						<span class="divider">|</span> | ||||
| 						<span class="signin" @click="signin">%i18n:@signin%</span> | ||||
| 					</p> | ||||
|  | ||||
| 					<img src="/assets/ai.png" alt="" title="藍" class="char"> | ||||
| 				</div> | ||||
| 			</div> | ||||
|  | ||||
| 			<p class="desc" v-html="description || '%i18n:common.about%'"></p> | ||||
| 			<div class="announcements block"> | ||||
| 				<header>%fa:broadcast-tower% %i18n:@announcements%</header> | ||||
| 				<div v-if="announcements && announcements.length > 0"> | ||||
| 					<div v-for="announcement in announcements"> | ||||
| 						<h1 v-html="announcement.title"></h1> | ||||
| 						<div v-html="announcement.text"></div> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
|  | ||||
| 			<p class="sign"> | ||||
| 				<span class="signup" @click="signup">%i18n:@signup%</span> | ||||
| 				<span class="divider">|</span> | ||||
| 				<span class="signin" @click="signin">%i18n:@signin%</span> | ||||
| 			</p> | ||||
| 		</div> | ||||
| 			<div class="photos block"> | ||||
| 				<header>%fa:images% %i18n:@photos%</header> | ||||
| 				<div> | ||||
| 					<div v-for="photo in photos" :style="`background-image: url(${photo.thumbnailUrl})`"></div> | ||||
| 				</div> | ||||
| 			</div> | ||||
|  | ||||
| 		<div class="broadcasts block"> | ||||
| 			<div v-for="broadcast in broadcasts"> | ||||
| 				<h1 v-html="broadcast.title"></h1> | ||||
| 				<div v-html="broadcast.text"></div> | ||||
| 			<div class="tag-cloud block"> | ||||
| 				<div> | ||||
| 					<mk-tag-cloud/> | ||||
| 				</div> | ||||
| 			</div> | ||||
|  | ||||
| 			<div class="nav block"> | ||||
| 				<div> | ||||
| 					<mk-nav class="nav"/> | ||||
| 				</div> | ||||
| 			</div> | ||||
|  | ||||
| 			<div class="side"> | ||||
| 				<div class="trends block"> | ||||
| 					<div> | ||||
| 						<mk-trends/> | ||||
| 					</div> | ||||
| 				</div> | ||||
|  | ||||
| 				<div class="tl block"> | ||||
| 					<header>%fa:comment-alt R% %i18n:@timeline%</header> | ||||
| 					<div> | ||||
| 						<mk-welcome-timeline class="tl" :max="20"/> | ||||
| 					</div> | ||||
| 				</div> | ||||
|  | ||||
| 				<div class="info block"> | ||||
| 					<header>%fa:info-circle% %i18n:@info%</header> | ||||
| 					<div> | ||||
| 						<div v-if="meta" class="body"> | ||||
| 							<p>Version: <b>{{ meta.version }}</b></p> | ||||
| 							<p>Maintainer: <b><a :href="meta.maintainer.url" target="_blank">{{ meta.maintainer.name }}</a></b></p> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</main> | ||||
|  | ||||
| 		<div class="nav block"> | ||||
| 			<mk-nav class="nav"/> | ||||
| 		</div> | ||||
|  | ||||
| 		<div class="side"> | ||||
| 			<mk-trends class="trends block"/> | ||||
|  | ||||
| 			<mk-welcome-timeline class="tl block" :max="20"/> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<modal name="about" :class="$store.state.device.darkmode ? ['about', 'modal-dark'] : ['about', 'modal-light']" width="800px" height="auto" scrollable> | ||||
| 		<article class="fpdezooorhntlzyeszemrsqdlgbysvxq"> | ||||
| 			<h1>%i18n:common.intro.title%</h1> | ||||
| 			<p v-html="'%i18n:common.intro.about%'"></p> | ||||
| 			<section> | ||||
| 				<h2>%i18n:common.intro.features%</h2> | ||||
| 				<section> | ||||
| 					<div class="body"> | ||||
| 						<h3>%i18n:common.intro.rich-contents%</h3> | ||||
| 						<p v-html="'%i18n:common.intro.rich-contents-desc%'"></p> | ||||
| 					</div> | ||||
| 					<div class="image"><img src="/assets/about/post.png" alt=""></div> | ||||
| 				</section> | ||||
| 				<section> | ||||
| 					<div class="body"> | ||||
| 						<h3>%i18n:common.intro.reaction%</h3> | ||||
| 						<p v-html="'%i18n:common.intro.reaction-desc%'"></p> | ||||
| 					</div> | ||||
| 					<div class="image"><img src="/assets/about/reaction.png" alt=""></div> | ||||
| 				</section> | ||||
| 				<section> | ||||
| 					<div class="body"> | ||||
| 						<h3>%i18n:common.intro.ui%</h3> | ||||
| 						<p v-html="'%i18n:common.intro.ui-desc%'"></p> | ||||
| 					</div> | ||||
| 					<div class="image"><img src="/assets/about/ui.png" alt=""></div> | ||||
| 				</section> | ||||
| 				<section> | ||||
| 					<div class="body"> | ||||
| 						<h3>%i18n:common.intro.drive%</h3> | ||||
| 						<p v-html="'%i18n:common.intro.drive-desc%'"></p> | ||||
| 					</div> | ||||
| 					<div class="image"><img src="/assets/about/drive.png" alt=""></div> | ||||
| 				</section> | ||||
| 			</section> | ||||
| 			<p v-html="'%i18n:common.intro.outro%'"></p> | ||||
| 		</article> | ||||
| 	</modal> | ||||
|  | ||||
| 	<modal name="signup" :class="$store.state.device.darkmode ? 'modal-dark' : 'modal-light'" width="450px" height="auto" scrollable> | ||||
| 		<header class="formHeader">%i18n:@signup%</header> | ||||
| @@ -62,37 +147,62 @@ | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import { host, copyright } from '../../../config'; | ||||
| import { concat } from '../../../../../prelude/array'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	data() { | ||||
| 		return { | ||||
| 			meta: null, | ||||
| 			stats: null, | ||||
| 			copyright, | ||||
| 			host, | ||||
| 			name: 'Misskey', | ||||
| 			description: '', | ||||
| 			broadcasts: [] | ||||
| 			announcements: [], | ||||
| 			photos: [] | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| 	created() { | ||||
| 		(this as any).os.getMeta().then(meta => { | ||||
| 			this.meta = meta; | ||||
| 			this.name = meta.name; | ||||
| 			this.description = meta.description; | ||||
| 			this.broadcasts = meta.broadcasts; | ||||
| 			this.announcements = meta.broadcasts; | ||||
| 		}); | ||||
|  | ||||
| 		(this as any).api('stats').then(stats => { | ||||
| 			this.stats = stats; | ||||
| 		}); | ||||
|  | ||||
| 		const image = [ | ||||
| 			'image/jpeg', | ||||
| 			'image/png', | ||||
| 			'image/gif' | ||||
| 		]; | ||||
|  | ||||
| 		(this as any).api('notes/local-timeline', { | ||||
| 			fileType: image, | ||||
| 			limit: 6 | ||||
| 		}).then((notes: any[]) => { | ||||
| 			const files = concat(notes.map((n: any): any[] => n.files)); | ||||
| 			this.photos = files.filter(f => image.includes(f.type)).slice(0, 6); | ||||
| 		}); | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		about() { | ||||
| 			this.$modal.show('about'); | ||||
| 		}, | ||||
|  | ||||
| 		signup() { | ||||
| 			this.$modal.show('signup'); | ||||
| 		}, | ||||
|  | ||||
| 		signin() { | ||||
| 			this.$modal.show('signin'); | ||||
| 		}, | ||||
|  | ||||
| 		dark() { | ||||
| 			this.$store.commit('device/set', { | ||||
| 				key: 'darkmode', | ||||
| @@ -137,6 +247,54 @@ export default Vue.extend({ | ||||
| 		margin 0 48px | ||||
| 		font-size 1.5em | ||||
|  | ||||
| .v--modal-overlay.about | ||||
| 	.v--modal-box.v--modal | ||||
| 		margin 32px 0 | ||||
|  | ||||
| .fpdezooorhntlzyeszemrsqdlgbysvxq | ||||
| 	padding 64px | ||||
|  | ||||
| 	> p:last-child | ||||
| 		margin-bottom 0 | ||||
|  | ||||
| 	> h1 | ||||
| 		margin-top 0 | ||||
|  | ||||
| 	> section | ||||
| 		> h2 | ||||
| 			border-bottom 1px solid isDark ? rgba(#000, 0.2) : rgba(#000, 0.05) | ||||
|  | ||||
| 		> section | ||||
| 			display grid | ||||
| 			grid-template-rows 1fr | ||||
| 			grid-template-columns 180px 1fr | ||||
| 			gap 32px | ||||
| 			margin-bottom 32px | ||||
| 			padding-bottom 32px | ||||
| 			border-bottom 1px solid isDark ? rgba(#000, 0.2) : rgba(#000, 0.05) | ||||
|  | ||||
| 			&:nth-child(odd) | ||||
| 				grid-template-columns 1fr 180px | ||||
|  | ||||
| 				> .body | ||||
| 					grid-column 1 | ||||
|  | ||||
| 				> .image | ||||
| 					grid-column 2 | ||||
|  | ||||
| 			> .body | ||||
| 				grid-row 1 | ||||
| 				grid-column 2 | ||||
|  | ||||
| 			> .image | ||||
| 				grid-row 1 | ||||
| 				grid-column 1 | ||||
|  | ||||
| 				> img | ||||
| 					display block | ||||
| 					width 100% | ||||
| 					height 100% | ||||
| 					object-fit cover | ||||
| </style> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| @@ -164,116 +322,176 @@ root(isDark) | ||||
| 		font-size 18px | ||||
| 		color isDark ? #fff : #444 | ||||
|  | ||||
| 	> .body | ||||
| 		display grid | ||||
| 		grid-template-rows 0.5fr 0.5fr 64px | ||||
| 		grid-template-columns 1fr 350px | ||||
| 		gap 16px | ||||
| 		width 100% | ||||
| 		max-width 1200px | ||||
| 		height 100vh | ||||
| 		min-height 800px | ||||
| 	> main | ||||
| 		margin 0 auto | ||||
| 		padding 64px | ||||
| 		width 100% | ||||
| 		max-width 1200px | ||||
|  | ||||
| 		.block | ||||
| 			color isDark ? #fff : #444 | ||||
| 			background isDark ? #313543 : #fff | ||||
| 			background isDark ? #282C37 : #fff | ||||
| 			box-shadow 0 3px 8px rgba(0, 0, 0, 0.2) | ||||
| 			//border-radius 8px | ||||
| 			overflow auto | ||||
|  | ||||
| 		> .main | ||||
| 			grid-row 1 | ||||
| 			grid-column 1 | ||||
| 			padding 32px | ||||
| 			border-top solid 5px $theme-color | ||||
| 			> header | ||||
| 				z-index 1 | ||||
| 				padding 0 16px | ||||
| 				line-height 48px | ||||
| 				background isDark ? #313543 : #fff | ||||
|  | ||||
| 			> h1 | ||||
| 				margin 0 | ||||
| 				if !isDark | ||||
| 					box-shadow 0 1px 0px rgba(0, 0, 0, 0.1) | ||||
|  | ||||
| 				> img | ||||
| 					margin -8px 0 0 -16px | ||||
| 					max-width 280px | ||||
|  | ||||
| 			> .info | ||||
| 				margin 0 auto 16px auto | ||||
| 				width $width | ||||
| 				font-size 14px | ||||
|  | ||||
| 				> .stats | ||||
| 					margin-left 16px | ||||
| 					padding-left 16px | ||||
| 					border-left solid 1px isDark ? #fff : #444 | ||||
|  | ||||
| 					> * | ||||
| 						margin-right 16px | ||||
|  | ||||
| 			> .sign | ||||
| 				font-size 120% | ||||
|  | ||||
| 				> .divider | ||||
| 					margin 0 16px | ||||
|  | ||||
| 				> .signin | ||||
| 				> .signup | ||||
| 					cursor pointer | ||||
|  | ||||
| 					&:hover | ||||
| 						color $theme-color | ||||
|  | ||||
| 			> .hashtags | ||||
| 				margin 16px auto | ||||
| 				width $width | ||||
| 				font-size 14px | ||||
| 				background rgba(#000, 0.3) | ||||
| 				border-radius 8px | ||||
|  | ||||
| 				> * | ||||
| 					display inline-block | ||||
| 					margin 14px | ||||
|  | ||||
| 		> .broadcasts | ||||
| 			grid-row 2 | ||||
| 			grid-column 1 | ||||
| 			padding 32px | ||||
| 				& + div | ||||
| 					max-height calc(100% - 48px) | ||||
|  | ||||
| 			> div | ||||
| 				padding 0 0 16px 0 | ||||
| 				margin 0 0 16px 0 | ||||
| 				border-bottom 1px solid isDark ? rgba(#000, 0.2) : rgba(#000, 0.05) | ||||
|  | ||||
| 				> h1 | ||||
| 					margin 0 | ||||
| 					font-size 1.5em | ||||
|  | ||||
| 		> .nav | ||||
| 			display flex | ||||
| 			justify-content center | ||||
| 			align-items center | ||||
| 			grid-row 3 | ||||
| 			grid-column 1 | ||||
| 			font-size 14px | ||||
|  | ||||
| 		> .side | ||||
| 			display grid | ||||
| 			grid-row 1 / 4 | ||||
| 			grid-column 2 | ||||
| 			grid-template-rows 1fr 350px | ||||
| 			grid-template-columns 1fr | ||||
| 			gap 16px | ||||
|  | ||||
| 			> .tl | ||||
| 				grid-row 1 | ||||
| 				grid-column 1 | ||||
| 				text-align left | ||||
| 				max-height 100% | ||||
| 				overflow auto | ||||
|  | ||||
| 			> .trends | ||||
| 		> .body | ||||
| 			display grid | ||||
| 			grid-template-rows 390px 1fr 256px 64px | ||||
| 			grid-template-columns 1fr 1fr 350px | ||||
| 			gap 16px | ||||
| 			height 1150px | ||||
|  | ||||
| 			> .main | ||||
| 				grid-row 1 | ||||
| 				grid-column 1 / 3 | ||||
| 				border-top solid 5px $theme-color | ||||
|  | ||||
| 				> div | ||||
| 					padding 32px | ||||
| 					min-height 100% | ||||
|  | ||||
| 					> h1 | ||||
| 						margin 0 | ||||
|  | ||||
| 						> img | ||||
| 							margin -8px 0 0 -16px | ||||
| 							max-width 280px | ||||
|  | ||||
| 					> .info | ||||
| 						margin 0 auto 16px auto | ||||
| 						width $width | ||||
| 						font-size 14px | ||||
|  | ||||
| 						> .stats | ||||
| 							margin-left 16px | ||||
| 							padding-left 16px | ||||
| 							border-left solid 1px isDark ? #fff : #444 | ||||
|  | ||||
| 							> * | ||||
| 								margin-right 16px | ||||
|  | ||||
| 					> .desc | ||||
| 						max-width calc(100% - 150px) | ||||
|  | ||||
| 					> .sign | ||||
| 						font-size 120% | ||||
| 						margin-bottom 0 | ||||
|  | ||||
| 						> .divider | ||||
| 							margin 0 16px | ||||
|  | ||||
| 						> .signin | ||||
| 						> .signup | ||||
| 							cursor pointer | ||||
|  | ||||
| 							&:hover | ||||
| 								color $theme-color | ||||
|  | ||||
| 					> .char | ||||
| 						display block | ||||
| 						position absolute | ||||
| 						right 16px | ||||
| 						bottom 0 | ||||
| 						height 320px | ||||
| 						opacity 0.7 | ||||
|  | ||||
| 					> *:not(.char) | ||||
| 						z-index 1 | ||||
|  | ||||
| 			> .announcements | ||||
| 				grid-row 2 | ||||
| 				grid-column 1 | ||||
| 				padding 8px | ||||
|  | ||||
| 				> div | ||||
| 					padding 32px | ||||
|  | ||||
| 					> div | ||||
| 						padding 0 0 16px 0 | ||||
| 						margin 0 0 16px 0 | ||||
| 						border-bottom 1px solid isDark ? rgba(#000, 0.2) : rgba(#000, 0.05) | ||||
|  | ||||
| 						> h1 | ||||
| 							margin 0 | ||||
| 							font-size 1.25em | ||||
|  | ||||
| 			> .photos | ||||
| 				grid-row 2 | ||||
| 				grid-column 2 | ||||
|  | ||||
| 				> div | ||||
| 					display grid | ||||
| 					grid-template-rows 1fr 1fr 1fr | ||||
| 					grid-template-columns 1fr 1fr | ||||
| 					gap 8px | ||||
| 					height 100% | ||||
| 					padding 16px | ||||
|  | ||||
| 					> div | ||||
| 						//border-radius 4px | ||||
| 						background-position center center | ||||
| 						background-size cover | ||||
|  | ||||
| 			> .tag-cloud | ||||
| 				grid-row 3 | ||||
| 				grid-column 1 / 3 | ||||
|  | ||||
| 				> div | ||||
| 					height 256px | ||||
| 					padding 32px | ||||
|  | ||||
| 			> .nav | ||||
| 				display flex | ||||
| 				justify-content center | ||||
| 				align-items center | ||||
| 				grid-row 4 | ||||
| 				grid-column 1 / 3 | ||||
| 				font-size 14px | ||||
|  | ||||
| 			> .side | ||||
| 				display grid | ||||
| 				grid-row 1 / 5 | ||||
| 				grid-column 3 | ||||
| 				grid-template-rows 1fr 350px | ||||
| 				grid-template-columns 1fr | ||||
| 				gap 16px | ||||
|  | ||||
| 				> .tl | ||||
| 					grid-row 1 | ||||
| 					grid-column 1 | ||||
| 					overflow auto | ||||
|  | ||||
| 				> .trends | ||||
| 					grid-row 2 | ||||
| 					grid-column 1 | ||||
| 					padding 8px | ||||
|  | ||||
| 				> .info | ||||
| 					grid-row 3 | ||||
| 					grid-column 1 | ||||
|  | ||||
| 					> div | ||||
| 						padding 16px | ||||
|  | ||||
| 						> .body | ||||
| 							> p | ||||
| 								display block | ||||
| 								margin 0 | ||||
|  | ||||
| .mk-welcome[data-darkmode] | ||||
| 	root(true) | ||||
|   | ||||
| @@ -49,7 +49,7 @@ export default define({ | ||||
| 				offset: this.offset, | ||||
| 				renote: false, | ||||
| 				reply: false, | ||||
| 				media: false, | ||||
| 				file: false, | ||||
| 				poll: false | ||||
| 			}).then(notes => { | ||||
| 				const note = notes ? notes[0] : null; | ||||
|   | ||||
| @@ -7,9 +7,6 @@ import Vuex from 'vuex'; | ||||
| import VueRouter from 'vue-router'; | ||||
| import * as TreeView from 'vue-json-tree-view'; | ||||
| import VAnimateCss from 'v-animate-css'; | ||||
| import Element from 'element-ui'; | ||||
| import ElementLocaleEn from 'element-ui/lib/locale/lang/en'; | ||||
| import ElementLocaleJa from 'element-ui/lib/locale/lang/ja'; | ||||
| import VModal from 'vue-js-modal'; | ||||
|  | ||||
| import App from './app.vue'; | ||||
| @@ -17,18 +14,10 @@ import checkForUpdate from './common/scripts/check-for-update'; | ||||
| import MiOS, { API } from './mios'; | ||||
| import { version, codename, lang } from './config'; | ||||
|  | ||||
| let elementLocale; | ||||
| switch (lang) { | ||||
| 	case 'ja-JP': elementLocale = ElementLocaleJa; break; | ||||
| 	case 'en-US': elementLocale = ElementLocaleEn; break; | ||||
| 	default: elementLocale = ElementLocaleEn; break; | ||||
| } | ||||
|  | ||||
| Vue.use(Vuex); | ||||
| Vue.use(VueRouter); | ||||
| Vue.use(TreeView); | ||||
| Vue.use(VAnimateCss); | ||||
| Vue.use(Element, { locale: elementLocale }); | ||||
| Vue.use(VModal); | ||||
|  | ||||
| // Register global directives | ||||
| @@ -42,9 +31,13 @@ require('./common/views/widgets'); | ||||
| require('./common/views/filters'); | ||||
|  | ||||
| Vue.mixin({ | ||||
| 	destroyed(this: any) { | ||||
| 		if (this.$el.parentNode) { | ||||
| 			this.$el.parentNode.removeChild(this.$el); | ||||
| 	methods: { | ||||
| 		destroyDom() { | ||||
| 			this.$destroy(); | ||||
|  | ||||
| 			if (this.$el.parentNode) { | ||||
| 				this.$el.parentNode.removeChild(this.$el); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
|   | ||||
| @@ -17,6 +17,7 @@ import Err from './common/views/components/connect-failed.vue'; | ||||
| import { LocalTimelineStreamManager } from './common/scripts/streaming/local-timeline'; | ||||
| import { HybridTimelineStreamManager } from './common/scripts/streaming/hybrid-timeline'; | ||||
| import { GlobalTimelineStreamManager } from './common/scripts/streaming/global-timeline'; | ||||
| import { erase } from '../../prelude/array'; | ||||
|  | ||||
| //#region api requests | ||||
| let spinner = null; | ||||
| @@ -537,7 +538,7 @@ export default class MiOS extends EventEmitter { | ||||
| 	} | ||||
|  | ||||
| 	public unregisterStreamConnection(connection: Connection) { | ||||
| 		this.connections = this.connections.filter(c => c != connection); | ||||
| 		this.connections = erase(connection, this.connections); | ||||
| 	} | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -6,7 +6,6 @@ import VueRouter from 'vue-router'; | ||||
|  | ||||
| // Style | ||||
| import './style.styl'; | ||||
| import '../../element.scss'; | ||||
|  | ||||
| import init from '../init'; | ||||
|  | ||||
|   | ||||
| @@ -78,7 +78,7 @@ export default Vue.extend({ | ||||
| 				scale: 0.8, | ||||
| 				duration: 300, | ||||
| 				easing: [ 0.5, -0.5, 1, 0.5 ], | ||||
| 				complete: () => this.$destroy() | ||||
| 				complete: () => this.destroyDom() | ||||
| 			}); | ||||
| 		}, | ||||
| 		onBgClick() { | ||||
|   | ||||
| @@ -31,15 +31,15 @@ export default Vue.extend({ | ||||
| 		}, | ||||
| 		onSelected(file) { | ||||
| 			this.$emit('selected', file); | ||||
| 			this.$destroy(); | ||||
| 			this.destroyDom(); | ||||
| 		}, | ||||
| 		cancel() { | ||||
| 			this.$emit('canceled'); | ||||
| 			this.$destroy(); | ||||
| 			this.destroyDom(); | ||||
| 		}, | ||||
| 		ok() { | ||||
| 			this.$emit('selected', this.files); | ||||
| 			this.$destroy(); | ||||
| 			this.destroyDom(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
|   | ||||
| @@ -19,11 +19,11 @@ export default Vue.extend({ | ||||
| 	methods: { | ||||
| 		cancel() { | ||||
| 			this.$emit('canceled'); | ||||
| 			this.$destroy(); | ||||
| 			this.destroyDom(); | ||||
| 		}, | ||||
| 		ok() { | ||||
| 			this.$emit('selected', (this.$refs.browser as any).folder); | ||||
| 			this.$destroy(); | ||||
| 			this.destroyDom(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
|   | ||||
| @@ -67,7 +67,7 @@ | ||||
| import Vue from 'vue'; | ||||
| import * as EXIF from 'exif-js'; | ||||
| import * as hljs from 'highlight.js'; | ||||
| import gcd from '../../../common/scripts/gcd'; | ||||
| import { gcd } from '../../../../../prelude/math'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	props: ['file'], | ||||
|   | ||||
| @@ -47,7 +47,7 @@ export default Vue.extend({ | ||||
| 			this.fetch(); | ||||
| 		}, | ||||
| 		close() { | ||||
| 			this.$destroy(); | ||||
| 			this.destroyDom(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| <template> | ||||
| <div class="qjewsnkgzzxlxtzncydssfbgjibiehcy" v-if="image.isSensitive && hide" @click="hide = false"> | ||||
| <div class="qjewsnkgzzxlxtzncydssfbgjibiehcy" v-if="image.isSensitive && hide && !$store.state.device.alwaysShowNsfw" @click="hide = false"> | ||||
| 	<div> | ||||
| 		<b>%fa:exclamation-triangle% %i18n:@sensitive%</b> | ||||
| 		<span>%i18n:@click-to-show%</span> | ||||
|   | ||||
| @@ -35,20 +35,26 @@ | ||||
| 			</div> | ||||
| 		</header> | ||||
| 		<div class="body"> | ||||
| 			<div class="text"> | ||||
| 				<span v-if="p.isHidden" style="opacity: 0.5">(%i18n:@private%)</span> | ||||
| 				<span v-if="p.deletedAt" style="opacity: 0.5">(%i18n:@deleted%)</span> | ||||
| 				<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/> | ||||
| 			</div> | ||||
| 			<div class="media" v-if="p.media.length > 0"> | ||||
| 				<mk-media-list :media-list="p.media" :raw="true"/> | ||||
| 			</div> | ||||
| 			<mk-poll v-if="p.poll" :note="p"/> | ||||
| 			<mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/> | ||||
| 			<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> | ||||
| 			<div class="map" v-if="p.geo" ref="map"></div> | ||||
| 			<div class="renote" v-if="p.renote"> | ||||
| 				<mk-note-preview :note="p.renote"/> | ||||
| 			<p v-if="p.cw != null" class="cw"> | ||||
| 				<span class="text" v-if="p.cw != ''">{{ p.cw }}</span> | ||||
| 				<mk-cw-button v-model="showContent"/> | ||||
| 			</p> | ||||
| 			<div class="content" v-show="p.cw == null || showContent"> | ||||
| 				<div class="text"> | ||||
| 					<span v-if="p.isHidden" style="opacity: 0.5">(%i18n:@private%)</span> | ||||
| 					<span v-if="p.deletedAt" style="opacity: 0.5">(%i18n:@deleted%)</span> | ||||
| 					<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/> | ||||
| 				</div> | ||||
| 				<div class="files" v-if="p.files.length > 0"> | ||||
| 					<mk-media-list :media-list="p.files" :raw="true"/> | ||||
| 				</div> | ||||
| 				<mk-poll v-if="p.poll" :note="p"/> | ||||
| 				<mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/> | ||||
| 				<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> | ||||
| 				<div class="map" v-if="p.geo" ref="map"></div> | ||||
| 				<div class="renote" v-if="p.renote"> | ||||
| 					<mk-note-preview :note="p.renote"/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<router-link class="time" :to="p | notePage"> | ||||
| @@ -85,6 +91,7 @@ import parse from '../../../../../mfm/parse'; | ||||
| import MkNoteMenu from '../../../common/views/components/note-menu.vue'; | ||||
| import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; | ||||
| import XSub from './note.sub.vue'; | ||||
| import { sum } from '../../../../../prelude/array'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| @@ -103,6 +110,7 @@ export default Vue.extend({ | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			showContent: false, | ||||
| 			conversation: [], | ||||
| 			conversationFetching: false, | ||||
| 			replies: [] | ||||
| @@ -113,7 +121,7 @@ export default Vue.extend({ | ||||
| 		isRenote(): boolean { | ||||
| 			return (this.note.renote && | ||||
| 				this.note.text == null && | ||||
| 				this.note.mediaIds.length == 0 && | ||||
| 				this.note.fileIds.length == 0 && | ||||
| 				this.note.poll == null); | ||||
| 		}, | ||||
|  | ||||
| @@ -123,9 +131,7 @@ export default Vue.extend({ | ||||
|  | ||||
| 		reactionsCount(): number { | ||||
| 			return this.p.reactionCounts | ||||
| 				? Object.keys(this.p.reactionCounts) | ||||
| 					.map(key => this.p.reactionCounts[key]) | ||||
| 					.reduce((a, b) => a + b) | ||||
| 				? sum(Object.values(this.p.reactionCounts)) | ||||
| 				: 0; | ||||
| 		}, | ||||
|  | ||||
| @@ -335,44 +341,57 @@ root(isDark) | ||||
| 		> .body | ||||
| 			padding 8px 0 | ||||
|  | ||||
| 			> .text | ||||
| 			> .cw | ||||
| 				cursor default | ||||
| 				display block | ||||
| 				margin 0 | ||||
| 				padding 0 | ||||
| 				overflow-wrap break-word | ||||
| 				font-size 16px | ||||
| 				color isDark ? #fff : #717171 | ||||
|  | ||||
| 				@media (min-width 500px) | ||||
| 					font-size 24px | ||||
| 				> .text | ||||
| 					margin-right 8px | ||||
|  | ||||
| 			> .renote | ||||
| 				margin 8px 0 | ||||
| 			> .content | ||||
|  | ||||
| 				> .mk-note-preview | ||||
| 					padding 16px | ||||
| 					border dashed 1px #c0dac6 | ||||
| 					border-radius 8px | ||||
|  | ||||
| 			> .location | ||||
| 				margin 4px 0 | ||||
| 				font-size 12px | ||||
| 				color #ccc | ||||
|  | ||||
| 			> .map | ||||
| 				width 100% | ||||
| 				height 200px | ||||
|  | ||||
| 				&:empty | ||||
| 					display none | ||||
|  | ||||
| 			> .mk-url-preview | ||||
| 				margin-top 8px | ||||
|  | ||||
| 			> .media | ||||
| 				> img | ||||
| 				> .text | ||||
| 					display block | ||||
| 					max-width 100% | ||||
| 					margin 0 | ||||
| 					padding 0 | ||||
| 					overflow-wrap break-word | ||||
| 					font-size 16px | ||||
| 					color isDark ? #fff : #717171 | ||||
|  | ||||
| 					@media (min-width 500px) | ||||
| 						font-size 24px | ||||
|  | ||||
| 				> .renote | ||||
| 					margin 8px 0 | ||||
|  | ||||
| 					> * | ||||
| 						padding 16px | ||||
| 						border dashed 1px #c0dac6 | ||||
| 						border-radius 8px | ||||
|  | ||||
| 				> .location | ||||
| 					margin 4px 0 | ||||
| 					font-size 12px | ||||
| 					color #ccc | ||||
|  | ||||
| 				> .map | ||||
| 					width 100% | ||||
| 					height 200px | ||||
|  | ||||
| 					&:empty | ||||
| 						display none | ||||
|  | ||||
| 				> .mk-url-preview | ||||
| 					margin-top 8px | ||||
|  | ||||
| 				> .files | ||||
| 					> img | ||||
| 						display block | ||||
| 						max-width 100% | ||||
|  | ||||
| 		> .time | ||||
| 			font-size 16px | ||||
|   | ||||
| @@ -1,10 +1,16 @@ | ||||
| <template> | ||||
| <div class="mk-note-preview" :class="{ smart: $store.state.device.postStyle == 'smart' }"> | ||||
| <div class="yohlumlkhizgfkvvscwfcrcggkotpvry" :class="{ smart: $store.state.device.postStyle == 'smart' }"> | ||||
| 	<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle != 'smart'"/> | ||||
| 	<div class="main"> | ||||
| 		<mk-note-header class="header" :note="note" :mini="true"/> | ||||
| 		<div class="body"> | ||||
| 			<mk-sub-note-content class="text" :note="note"/> | ||||
| 			<p v-if="note.cw != null" class="cw"> | ||||
| 				<span class="text" v-if="note.cw != ''">{{ note.cw }}</span> | ||||
| 				<mk-cw-button v-model="showContent"/> | ||||
| 			</p> | ||||
| 			<div class="content" v-show="note.cw == null || showContent"> | ||||
| 				<mk-sub-note-content class="text" :note="note"/> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
| @@ -14,7 +20,18 @@ | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	props: ['note'] | ||||
| 	props: { | ||||
| 		note: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			showContent: false | ||||
| 		}; | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| @@ -65,16 +82,28 @@ root(isDark) | ||||
|  | ||||
| 		> .body | ||||
|  | ||||
| 			> .text | ||||
| 			> .cw | ||||
| 				cursor default | ||||
| 				display block | ||||
| 				margin 0 | ||||
| 				padding 0 | ||||
| 				color isDark ? #959ba7 : #717171 | ||||
| 				overflow-wrap break-word | ||||
| 				color isDark ? #fff : #717171 | ||||
|  | ||||
| .mk-note-preview[data-darkmode] | ||||
| 				> .text | ||||
| 					margin-right 8px | ||||
|  | ||||
| 			> .content | ||||
| 				> .text | ||||
| 					cursor default | ||||
| 					margin 0 | ||||
| 					padding 0 | ||||
| 					color isDark ? #959ba7 : #717171 | ||||
|  | ||||
| .yohlumlkhizgfkvvscwfcrcggkotpvry[data-darkmode] | ||||
| 	root(true) | ||||
|  | ||||
| .mk-note-preview:not([data-darkmode]) | ||||
| .yohlumlkhizgfkvvscwfcrcggkotpvry:not([data-darkmode]) | ||||
| 	root(false) | ||||
|  | ||||
| </style> | ||||
|   | ||||
| @@ -1,10 +1,16 @@ | ||||
| <template> | ||||
| <div class="sub" :class="{ smart: $store.state.device.postStyle == 'smart' }"> | ||||
| <div class="zlrxdaqttccpwhpaagdmkawtzklsccam" :class="{ smart: $store.state.device.postStyle == 'smart' }"> | ||||
| 	<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle != 'smart'"/> | ||||
| 	<div class="main"> | ||||
| 		<mk-note-header class="header" :note="note" :mini="true"/> | ||||
| 		<div class="body"> | ||||
| 			<mk-sub-note-content class="text" :note="note"/> | ||||
| 			<p v-if="note.cw != null" class="cw"> | ||||
| 				<span class="text" v-if="note.cw != ''">{{ note.cw }}</span> | ||||
| 				<mk-cw-button v-model="showContent"/> | ||||
| 			</p> | ||||
| 			<div class="content" v-show="note.cw == null || showContent"> | ||||
| 				<mk-sub-note-content class="text" :note="note"/> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
| @@ -24,6 +30,12 @@ export default Vue.extend({ | ||||
| 			type: Boolean, | ||||
| 			default: true | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			showContent: false | ||||
| 		}; | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| @@ -77,20 +89,31 @@ root(isDark) | ||||
| 			margin-bottom 2px | ||||
|  | ||||
| 		> .body | ||||
|  | ||||
| 			> .text | ||||
| 			> .cw | ||||
| 				cursor default | ||||
| 				display block | ||||
| 				margin 0 | ||||
| 				padding 0 | ||||
| 				color isDark ? #959ba7 : #717171 | ||||
| 				overflow-wrap break-word | ||||
| 				color isDark ? #fff : #717171 | ||||
|  | ||||
| 				pre | ||||
| 					max-height 120px | ||||
| 					font-size 80% | ||||
| 				> .text | ||||
| 					margin-right 8px | ||||
|  | ||||
| .sub[data-darkmode] | ||||
| 			> .content | ||||
| 				> .text | ||||
| 					margin 0 | ||||
| 					padding 0 | ||||
| 					color isDark ? #959ba7 : #717171 | ||||
|  | ||||
| 					pre | ||||
| 						max-height 120px | ||||
| 						font-size 80% | ||||
|  | ||||
| .zlrxdaqttccpwhpaagdmkawtzklsccam[data-darkmode] | ||||
| 	root(true) | ||||
|  | ||||
| .sub:not([data-darkmode]) | ||||
| .zlrxdaqttccpwhpaagdmkawtzklsccam:not([data-darkmode]) | ||||
| 	root(false) | ||||
|  | ||||
| </style> | ||||
|   | ||||
| @@ -18,7 +18,7 @@ | ||||
| 			<div class="body"> | ||||
| 				<p v-if="p.cw != null" class="cw"> | ||||
| 					<span class="text" v-if="p.cw != ''">{{ p.cw }}</span> | ||||
| 					<span class="toggle" @click="showContent = !showContent">{{ showContent ? '%i18n:@less%' : '%i18n:@more%' }}</span> | ||||
| 					<mk-cw-button v-model="showContent"/> | ||||
| 				</p> | ||||
| 				<div class="content" v-show="p.cw == null || showContent"> | ||||
| 					<div class="text"> | ||||
| @@ -28,16 +28,14 @@ | ||||
| 						<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i" :class="$style.text"/> | ||||
| 						<a class="rp" v-if="p.renote != null">RP:</a> | ||||
| 					</div> | ||||
| 					<div class="media" v-if="p.media.length > 0"> | ||||
| 						<mk-media-list :media-list="p.media"/> | ||||
| 					<div class="files" v-if="p.files.length > 0"> | ||||
| 						<mk-media-list :media-list="p.files"/> | ||||
| 					</div> | ||||
| 					<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/> | ||||
| 					<mk-url-preview v-for="url in urls" :url="url" :key="url"/> | ||||
| 					<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> | ||||
| 					<div class="map" v-if="p.geo" ref="map"></div> | ||||
| 					<div class="renote" v-if="p.renote"> | ||||
| 						<mk-note-preview :note="p.renote"/> | ||||
| 					</div> | ||||
| 					<div class="renote" v-if="p.renote"><mk-note-preview :note="p.renote"/></div> | ||||
| 				</div> | ||||
| 				<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span> | ||||
| 			</div> | ||||
| @@ -70,6 +68,7 @@ import parse from '../../../../../mfm/parse'; | ||||
| import MkNoteMenu from '../../../common/views/components/note-menu.vue'; | ||||
| import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; | ||||
| import XSub from './note.sub.vue'; | ||||
| import { sum } from '../../../../../prelude/array'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| @@ -90,7 +89,7 @@ export default Vue.extend({ | ||||
| 		isRenote(): boolean { | ||||
| 			return (this.note.renote && | ||||
| 				this.note.text == null && | ||||
| 				this.note.mediaIds.length == 0 && | ||||
| 				this.note.fileIds.length == 0 && | ||||
| 				this.note.poll == null); | ||||
| 		}, | ||||
|  | ||||
| @@ -100,9 +99,7 @@ export default Vue.extend({ | ||||
|  | ||||
| 		reactionsCount(): number { | ||||
| 			return this.p.reactionCounts | ||||
| 				? Object.keys(this.p.reactionCounts) | ||||
| 					.map(key => this.p.reactionCounts[key]) | ||||
| 					.reduce((a, b) => a + b) | ||||
| 				? sum(Object.values(this.p.reactionCounts)) | ||||
| 				: 0; | ||||
| 		}, | ||||
|  | ||||
| @@ -353,19 +350,6 @@ root(isDark) | ||||
| 					> .text | ||||
| 						margin-right 8px | ||||
|  | ||||
| 					> .toggle | ||||
| 						display inline-block | ||||
| 						padding 4px 8px | ||||
| 						font-size 0.7em | ||||
| 						color isDark ? #393f4f : #fff | ||||
| 						background isDark ? #687390 : #b1b9c1 | ||||
| 						border-radius 2px | ||||
| 						cursor pointer | ||||
| 						user-select none | ||||
|  | ||||
| 						&:hover | ||||
| 							background isDark ? #707b97 : #bbc4ce | ||||
|  | ||||
| 				> .content | ||||
|  | ||||
| 					> .text | ||||
| @@ -414,7 +398,7 @@ root(isDark) | ||||
| 					.mk-url-preview | ||||
| 						margin-top 8px | ||||
|  | ||||
| 					> .media | ||||
| 					> .files | ||||
| 						> img | ||||
| 							display block | ||||
| 							max-width 100% | ||||
| @@ -437,7 +421,7 @@ root(isDark) | ||||
| 					> .renote | ||||
| 						margin 8px 0 | ||||
|  | ||||
| 						> .mk-note-preview | ||||
| 						> * | ||||
| 							padding 16px | ||||
| 							border dashed 1px isDark ? #4e945e : #c0dac6 | ||||
| 							border-radius 8px | ||||
|   | ||||
| @@ -14,8 +14,7 @@ | ||||
| 	</div> | ||||
|  | ||||
| 	<!-- トランジションを有効にするとなぜかメモリリークする --> | ||||
| 	<!-- <transition-group name="mk-notes" class="transition"> --> | ||||
| 	<div class="transition"> | ||||
| 	<transition-group name="mk-notes" class="transition" tag="div"> | ||||
| 		<template v-for="(note, i) in _notes"> | ||||
| 			<mk-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/> | ||||
| 			<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date"> | ||||
| @@ -23,8 +22,7 @@ | ||||
| 				<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span> | ||||
| 			</p> | ||||
| 		</template> | ||||
| 	</div> | ||||
| 	<!-- </transition-group> --> | ||||
| 	</transition-group> | ||||
|  | ||||
| 	<footer v-if="more"> | ||||
| 		<button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> | ||||
| @@ -125,7 +123,7 @@ export default Vue.extend({ | ||||
| 		prepend(note, silent = false) { | ||||
| 			//#region 弾く | ||||
| 			const isMyNote = note.userId == this.$store.state.i.id; | ||||
| 			const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null; | ||||
| 			const isPureRenote = note.renoteId != null && note.text == null && note.fileIds.length == 0 && note.poll == null; | ||||
|  | ||||
| 			if (this.$store.state.settings.showMyRenotes === false) { | ||||
| 				if (isMyNote && isPureRenote) { | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| <template> | ||||
| <div class="mk-notify"> | ||||
| <div class="mk-notify" :class="pos"> | ||||
| 	<div> | ||||
| 		<mk-notification-preview :notification="notification"/> | ||||
| 	</div> | ||||
| @@ -12,11 +12,16 @@ import * as anime from 'animejs'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	props: ['notification'], | ||||
| 	computed: { | ||||
| 		pos() { | ||||
| 			return this.$store.state.device.mobileNotificationPosition; | ||||
| 		} | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		this.$nextTick(() => { | ||||
| 			anime({ | ||||
| 				targets: this.$el, | ||||
| 				bottom: '0px', | ||||
| 				[this.pos]: '0px', | ||||
| 				duration: 500, | ||||
| 				easing: 'easeOutQuad' | ||||
| 			}); | ||||
| @@ -24,10 +29,10 @@ export default Vue.extend({ | ||||
| 			setTimeout(() => { | ||||
| 				anime({ | ||||
| 					targets: this.$el, | ||||
| 					bottom: `-${this.$el.offsetHeight}px`, | ||||
| 					[this.pos]: `-${this.$el.offsetHeight}px`, | ||||
| 					duration: 500, | ||||
| 					easing: 'easeOutQuad', | ||||
| 					complete: () => this.$destroy() | ||||
| 					complete: () => this.destroyDom() | ||||
| 				}); | ||||
| 			}, 6000); | ||||
| 		}); | ||||
| @@ -40,8 +45,7 @@ export default Vue.extend({ | ||||
| 	$height = 78px | ||||
|  | ||||
| 	position fixed | ||||
| 	z-index 1024 | ||||
| 	bottom -($height) | ||||
| 	z-index 10000 | ||||
| 	left 0 | ||||
| 	right 0 | ||||
| 	width 100% | ||||
| @@ -52,6 +56,12 @@ export default Vue.extend({ | ||||
| 	pointer-events none | ||||
| 	font-size 80% | ||||
|  | ||||
| 	&.bottom | ||||
| 		bottom -($height) | ||||
|  | ||||
| 	&.top | ||||
| 		top -($height) | ||||
|  | ||||
| 	> div | ||||
| 		height 100% | ||||
| 		-webkit-backdrop-filter blur(2px) | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| <template> | ||||
| <div class="ulveipglmagnxfgvitaxyszerjwiqmwl"> | ||||
| 	<div class="bg" ref="bg" @click="onBgClick"></div> | ||||
| 	<div class="main" ref="main" @click.self="onBgClick"> | ||||
| 	<div class="bg" ref="bg"></div> | ||||
| 	<div class="main" ref="main"> | ||||
| 		<mk-post-form ref="form" | ||||
| 			:reply="reply" | ||||
| 			:renote="renote" | ||||
| @@ -79,15 +79,10 @@ export default Vue.extend({ | ||||
| 				translateY: 16, | ||||
| 				duration: 300, | ||||
| 				easing: 'easeOutQuad', | ||||
| 				complete: () => this.$destroy() | ||||
| 				complete: () => this.destroyDom() | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		onBgClick() { | ||||
| 			this.$emit('cancel'); | ||||
| 			this.close(); | ||||
| 		}, | ||||
|  | ||||
| 		onPosted() { | ||||
| 			this.$emit('posted'); | ||||
| 			this.close(); | ||||
|   | ||||
| @@ -4,14 +4,14 @@ | ||||
| 		<header> | ||||
| 			<button class="cancel" @click="cancel">%fa:times%</button> | ||||
| 			<div> | ||||
| 				<span class="text-count" :class="{ over: text.length > 1000 }">{{ 1000 - text.length }}</span> | ||||
| 				<span class="text-count" :class="{ over: trimmedLength(text) > 1000 }">{{ 1000 - trimmedLength(text) }}</span> | ||||
| 				<span class="geo" v-if="geo">%fa:map-marker-alt%</span> | ||||
| 				<button class="submit" :disabled="!canPost" @click="post">{{ submitText }}</button> | ||||
| 			</div> | ||||
| 		</header> | ||||
| 		<div class="form"> | ||||
| 			<mk-note-preview v-if="reply" :note="reply"/> | ||||
| 			<mk-note-preview v-if="renote" :note="renote"/> | ||||
| 			<mk-note-preview class="preview" v-if="reply" :note="reply"/> | ||||
| 			<mk-note-preview class="preview" v-if="renote" :note="renote"/> | ||||
| 			<div v-if="visibility == 'specified'" class="visibleUsers"> | ||||
| 				<span v-for="u in visibleUsers">{{ u | userName }}<a @click="removeVisibleUser(u)">[x]</a></span> | ||||
| 				<a @click="addVisibleUser">+%i18n:@add-visible-user%</a> | ||||
| @@ -59,6 +59,9 @@ import MkVisibilityChooser from '../../../common/views/components/visibility-cho | ||||
| import getFace from '../../../common/scripts/get-face'; | ||||
| import parse from '../../../../../mfm/parse'; | ||||
| import { host } from '../../../config'; | ||||
| import { erase } from '../../../../../prelude/array'; | ||||
| import { length } from 'stringz'; | ||||
| import parseAcct from '../../../../../misc/acct/parse'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| @@ -94,7 +97,7 @@ export default Vue.extend({ | ||||
| 			files: [], | ||||
| 			poll: false, | ||||
| 			geo: null, | ||||
| 			visibility: this.$store.state.device.visibility || 'public', | ||||
| 			visibility: this.$store.state.settings.rememberNoteVisibility ? (this.$store.state.device.visibility || this.$store.state.settings.defaultNoteVisibility) : this.$store.state.settings.defaultNoteVisibility, | ||||
| 			visibleUsers: [], | ||||
| 			useCw: false, | ||||
| 			cw: null, | ||||
| @@ -178,6 +181,10 @@ export default Vue.extend({ | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		trimmedLength(text: string) { | ||||
| 			return length(text.trim()); | ||||
| 		}, | ||||
|  | ||||
| 		addTag(tag: string) { | ||||
| 			insertTextAtCursor(this.$refs.text, ` #${tag} `); | ||||
| 		}, | ||||
| @@ -200,12 +207,12 @@ export default Vue.extend({ | ||||
|  | ||||
| 		attachMedia(driveFile) { | ||||
| 			this.files.push(driveFile); | ||||
| 			this.$emit('change-attached-media', this.files); | ||||
| 			this.$emit('change-attached-files', this.files); | ||||
| 		}, | ||||
|  | ||||
| 		detachMedia(file) { | ||||
| 			this.files = this.files.filter(x => x.id != file.id); | ||||
| 			this.$emit('change-attached-media', this.files); | ||||
| 			this.$emit('change-attached-files', this.files); | ||||
| 		}, | ||||
|  | ||||
| 		onChangeFile() { | ||||
| @@ -252,24 +259,23 @@ export default Vue.extend({ | ||||
| 		addVisibleUser() { | ||||
| 			(this as any).apis.input({ | ||||
| 				title: '%i18n:@username-prompt%' | ||||
| 			}).then(username => { | ||||
| 				(this as any).api('users/show', { | ||||
| 					username | ||||
| 				}).then(user => { | ||||
| 			}).then(acct => { | ||||
| 				if (acct.startsWith('@')) acct = acct.substr(1); | ||||
| 				(this as any).api('users/show', parseAcct(acct)).then(user => { | ||||
| 					this.visibleUsers.push(user); | ||||
| 				}); | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		removeVisibleUser(user) { | ||||
| 			this.visibleUsers = this.visibleUsers.filter(u => u != user); | ||||
| 			this.visibleUsers = erase(user, this.visibleUsers); | ||||
| 		}, | ||||
|  | ||||
| 		clear() { | ||||
| 			this.text = ''; | ||||
| 			this.files = []; | ||||
| 			this.poll = false; | ||||
| 			this.$emit('change-attached-media'); | ||||
| 			this.$emit('change-attached-files'); | ||||
| 		}, | ||||
|  | ||||
| 		post() { | ||||
| @@ -277,7 +283,7 @@ export default Vue.extend({ | ||||
| 			const viaMobile = this.$store.state.settings.disableViaMobile !== true; | ||||
| 			(this as any).api('notes/create', { | ||||
| 				text: this.text == '' ? undefined : this.text, | ||||
| 				mediaIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined, | ||||
| 				fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined, | ||||
| 				replyId: this.reply ? this.reply.id : undefined, | ||||
| 				renoteId: this.renote ? this.renote.id : undefined, | ||||
| 				poll: this.poll ? (this.$refs.poll as any).get() : undefined, | ||||
| @@ -302,7 +308,7 @@ export default Vue.extend({ | ||||
| 			if (this.text && this.text != '') { | ||||
| 				const hashtags = parse(this.text).filter(x => x.type == 'hashtag').map(x => x.hashtag); | ||||
| 				const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[]; | ||||
| 				localStorage.setItem('hashtags', JSON.stringify(hashtags.concat(history).reduce((a, c) => a.includes(c) ? a : [...a, c], []))); | ||||
| 				localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history)))); | ||||
| 			} | ||||
| 		}, | ||||
|  | ||||
| @@ -381,7 +387,7 @@ root(isDark) | ||||
| 			max-width 500px | ||||
| 			margin 0 auto | ||||
|  | ||||
| 			> .mk-note-preview | ||||
| 			> .preview | ||||
| 				padding 16px | ||||
|  | ||||
| 			> .visibleUsers | ||||
|   | ||||
| @@ -7,9 +7,9 @@ | ||||
| 		<misskey-flavored-markdown v-if="note.text" :text="note.text" :i="$store.state.i"/> | ||||
| 		<a class="rp" v-if="note.renoteId">RP: ...</a> | ||||
| 	</div> | ||||
| 	<details v-if="note.media.length > 0"> | ||||
| 		<summary>({{ '%i18n:@media-count%'.replace('{}', note.media.length) }})</summary> | ||||
| 		<mk-media-list :media-list="note.media"/> | ||||
| 	<details v-if="note.files.length > 0"> | ||||
| 		<summary>({{ '%i18n:@media-count%'.replace('{}', note.files.length) }})</summary> | ||||
| 		<mk-media-list :media-list="note.files"/> | ||||
| 	</details> | ||||
| 	<details v-if="note.poll"> | ||||
| 		<summary>%i18n:@poll%</summary> | ||||
|   | ||||
| @@ -34,6 +34,12 @@ | ||||
| 					<li @click="dark"><p><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template><span>%i18n:@darkmode%</span></p></li> | ||||
| 				</ul> | ||||
| 			</div> | ||||
| 			<div class="announcements" v-if="announcements && announcements.length > 0"> | ||||
| 				<article v-for="announcement in announcements"> | ||||
| 					<span v-html="announcement.title" class="title"></span> | ||||
| 					<div v-html="announcement.text"></div> | ||||
| 				</article> | ||||
| 			</div> | ||||
| 			<a :href="aboutUrl"><p class="about">%i18n:@about%</p></a> | ||||
| 		</div> | ||||
| 	</transition> | ||||
| @@ -46,23 +52,32 @@ import { lang } from '../../../config'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	props: ['isOpen'], | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			hasGameInvitation: false, | ||||
| 			connection: null, | ||||
| 			connectionId: null, | ||||
| 			aboutUrl: `/docs/${lang}/about` | ||||
| 			aboutUrl: `/docs/${lang}/about`, | ||||
| 			announcements: [] | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| 	computed: { | ||||
| 		hasUnreadNotification(): boolean { | ||||
| 			return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadNotification; | ||||
| 		}, | ||||
|  | ||||
| 		hasUnreadMessagingMessage(): boolean { | ||||
| 			return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadMessagingMessage; | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	mounted() { | ||||
| 		(this as any).os.getMeta().then(meta => { | ||||
| 			this.announcements = meta.broadcasts; | ||||
| 		}); | ||||
|  | ||||
| 		if (this.$store.getters.isSignedIn) { | ||||
| 			this.connection = (this as any).os.stream.getConnection(); | ||||
| 			this.connectionId = (this as any).os.stream.use(); | ||||
| @@ -71,6 +86,7 @@ export default Vue.extend({ | ||||
| 			this.connection.on('reversi_no_invites', this.onReversiNoInvites); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	beforeDestroy() { | ||||
| 		if (this.$store.getters.isSignedIn) { | ||||
| 			this.connection.off('reversi_invited', this.onReversiInvited); | ||||
| @@ -78,18 +94,22 @@ export default Vue.extend({ | ||||
| 			(this as any).os.stream.dispose(this.connectionId); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		search() { | ||||
| 			const query = window.prompt('%i18n:@search%'); | ||||
| 			if (query == null || query == '') return; | ||||
| 			this.$router.push(`/search?q=${encodeURIComponent(query)}`); | ||||
| 		}, | ||||
|  | ||||
| 		onReversiInvited() { | ||||
| 			this.hasGameInvitation = true; | ||||
| 		}, | ||||
|  | ||||
| 		onReversiNoInvites() { | ||||
| 			this.hasGameInvitation = false; | ||||
| 		}, | ||||
|  | ||||
| 		dark() { | ||||
| 			this.$store.commit('device/set', { | ||||
| 				key: 'darkmode', | ||||
| @@ -204,6 +224,17 @@ root(isDark) | ||||
| 					color $color | ||||
| 					opacity 0.5 | ||||
|  | ||||
| 	.announcements | ||||
| 		> article | ||||
| 			background isDark ? rgba(30, 129, 216, 0.2) : rgba(155, 196, 232, 0.2) | ||||
| 			color isDark ? #fff : #3f4967 | ||||
| 			padding 16px | ||||
| 			margin 8px 0 | ||||
| 			font-size 12px | ||||
|  | ||||
| 			> .title | ||||
| 				font-weight bold | ||||
|  | ||||
| 	.about | ||||
| 		margin 0 0 8px 0 | ||||
| 		padding 1em 0 | ||||
|   | ||||
| @@ -41,7 +41,7 @@ export default Vue.extend({ | ||||
| 			(this.$refs.timeline as any).init(() => new Promise((res, rej) => { | ||||
| 				(this as any).api('users/notes', { | ||||
| 					userId: this.user.id, | ||||
| 					withMedia: this.withMedia, | ||||
| 					withFiles: this.withMedia, | ||||
| 					limit: fetchLimit + 1 | ||||
| 				}).then(notes => { | ||||
| 					if (notes.length == fetchLimit + 1) { | ||||
| @@ -62,7 +62,7 @@ export default Vue.extend({ | ||||
|  | ||||
| 			const promise = (this as any).api('users/notes', { | ||||
| 				userId: this.user.id, | ||||
| 				withMedia: this.withMedia, | ||||
| 				withFiles: this.withMedia, | ||||
| 				limit: fetchLimit + 1, | ||||
| 				untilId: (this.$refs.timeline as any).tail().id | ||||
| 			}); | ||||
|   | ||||
| @@ -24,8 +24,8 @@ | ||||
| 			<div class="body"> | ||||
| 				<div> | ||||
| 					<span :data-active="src == 'home'" @click="src = 'home'">%fa:home% %i18n:@home%</span> | ||||
| 					<span :data-active="src == 'local'" @click="src = 'local'">%fa:R comments% %i18n:@local%</span> | ||||
| 					<span :data-active="src == 'hybrid'" @click="src = 'hybrid'">%fa:share-alt% %i18n:@hybrid%</span> | ||||
| 					<span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline">%fa:R comments% %i18n:@local%</span> | ||||
| 					<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline">%fa:share-alt% %i18n:@hybrid%</span> | ||||
| 					<span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span> | ||||
| 					<template v-if="lists"> | ||||
| 						<span v-for="l in lists" :data-active="src == 'list' && list == l" @click="src = 'list'; list = l" :key="l.id">%fa:list% {{ l.title }}</span> | ||||
| @@ -60,7 +60,8 @@ export default Vue.extend({ | ||||
| 			src: 'home', | ||||
| 			list: null, | ||||
| 			lists: null, | ||||
| 			showNav: false | ||||
| 			showNav: false, | ||||
| 			enableLocalTimeline: false | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| @@ -85,6 +86,10 @@ export default Vue.extend({ | ||||
| 	}, | ||||
|  | ||||
| 	created() { | ||||
| 		(this as any).os.getMeta().then(meta => { | ||||
| 			this.enableLocalTimeline = !meta.disableLocalTimeline; | ||||
| 		}); | ||||
|  | ||||
| 		if (this.$store.state.device.tl) { | ||||
| 			this.src = this.$store.state.device.tl.src; | ||||
| 			if (this.src == 'list') { | ||||
|   | ||||
| @@ -10,80 +10,119 @@ | ||||
| 			<ui-card> | ||||
| 				<div slot="title">%fa:palette% %i18n:@design%</div> | ||||
|  | ||||
| 				<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> | ||||
| 				<ui-switch v-model="$store.state.settings.iLikeSushi" @change="onChangeILikeSushi">%i18n:common.i-like-sushi%</ui-switch> | ||||
| 				<ui-switch v-model="$store.state.settings.disableAnimatedMfm" @change="onChangeDisableAnimatedMfm">%i18n:common.disable-animated-mfm%</ui-switch> | ||||
| 				<ui-switch v-model="$store.state.settings.games.reversi.showBoardLabels" @change="onChangeReversiBoardLabels">%i18n:common.show-reversi-board-labels%</ui-switch> | ||||
| 				<ui-switch v-model="$store.state.settings.games.reversi.useContrastStones" @change="onChangeUseContrastReversiStones">%i18n:common.use-contrast-reversi-stones%</ui-switch> | ||||
| 				<section> | ||||
| 					<ui-switch v-model="darkmode">%i18n:@dark-mode%</ui-switch> | ||||
| 					<ui-switch v-model="circleIcons">%i18n:@circle-icons%</ui-switch> | ||||
| 					<ui-switch v-model="contrastedAcct">%i18n:@contrasted-acct%</ui-switch> | ||||
| 					<ui-switch v-model="showFullAcct">%i18n:common.show-full-acct%</ui-switch> | ||||
| 					<ui-switch v-model="iLikeSushi">%i18n:common.i-like-sushi%</ui-switch> | ||||
| 					<ui-switch v-model="disableAnimatedMfm">%i18n:common.disable-animated-mfm%</ui-switch> | ||||
| 					<ui-switch v-model="alwaysShowNsfw">%i18n:common.always-show-nsfw% (%i18n:common.this-setting-is-this-device-only%)</ui-switch> | ||||
| 					<ui-switch v-model="games_reversi_showBoardLabels">%i18n:common.show-reversi-board-labels%</ui-switch> | ||||
| 					<ui-switch v-model="games_reversi_useContrastStones">%i18n:common.use-contrast-reversi-stones%</ui-switch> | ||||
| 				</section> | ||||
|  | ||||
| 				<div> | ||||
| 					<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> | ||||
| 					<ui-switch v-model="$store.state.settings.showLocalRenotes" @change="onChangeShowLocalRenotes">%i18n:@show-local-renotes%</ui-switch> | ||||
| 				</div> | ||||
| 				<section> | ||||
| 					<header>%i18n:@timeline%</header> | ||||
| 					<div> | ||||
| 						<ui-switch v-model="showReplyTarget">%i18n:@show-reply-target%</ui-switch> | ||||
| 						<ui-switch v-model="showMyRenotes">%i18n:@show-my-renotes%</ui-switch> | ||||
| 						<ui-switch v-model="showRenotedMyNotes">%i18n:@show-renoted-my-notes%</ui-switch> | ||||
| 						<ui-switch v-model="showLocalRenotes">%i18n:@show-local-renotes%</ui-switch> | ||||
| 					</div> | ||||
| 				</section> | ||||
|  | ||||
| 				<div> | ||||
| 					<div>%i18n:@post-style%</div> | ||||
| 				<section> | ||||
| 					<header>%i18n:@post-style%</header> | ||||
| 					<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> | ||||
| 				</section> | ||||
|  | ||||
| 				<section> | ||||
| 					<header>%i18n:@notification-position%</header> | ||||
| 					<ui-radio v-model="mobileNotificationPosition" value="bottom">%i18n:@notification-position-bottom%</ui-radio> | ||||
| 					<ui-radio v-model="mobileNotificationPosition" value="top">%i18n:@notification-position-top%</ui-radio> | ||||
| 				</section> | ||||
| 			</ui-card> | ||||
|  | ||||
| 			<ui-card> | ||||
| 				<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> | ||||
|  | ||||
| 				<section> | ||||
| 					<ui-switch v-model="fetchOnScroll">%i18n:@fetch-on-scroll%</ui-switch> | ||||
| 					<ui-switch v-model="disableViaMobile">%i18n:@disable-via-mobile%</ui-switch> | ||||
| 					<ui-switch v-model="loadRawImages">%i18n:@load-raw-images%</ui-switch> | ||||
| 					<ui-switch v-model="loadRemoteMedia">%i18n:@load-remote-media%</ui-switch> | ||||
| 					<ui-switch v-model="lightmode">%i18n:@i-am-under-limited-internet%</ui-switch> | ||||
| 				</section> | ||||
|  | ||||
| 				<section> | ||||
| 					<header>%i18n:@note-visibility%</header> | ||||
| 					<ui-switch v-model="rememberNoteVisibility">%i18n:@remember-note-visibility%</ui-switch> | ||||
| 					<section> | ||||
| 						<header>%i18n:@default-note-visibility%</header> | ||||
| 						<ui-select v-model="defaultNoteVisibility"> | ||||
| 							<option value="public">%i18n:common.note-visibility.public%</option> | ||||
| 							<option value="home">%i18n:common.note-visibility.home%</option> | ||||
| 							<option value="followers">%i18n:common.note-visibility.followers%</option> | ||||
| 							<option value="specified">%i18n:common.note-visibility.specified%</option> | ||||
| 							<option value="private">%i18n:common.note-visibility.private%</option> | ||||
| 						</ui-select> | ||||
| 					</section> | ||||
| 				</section> | ||||
| 			</ui-card> | ||||
|  | ||||
| 			<ui-card> | ||||
| 				<div slot="title">%fa:volume-up% %i18n:@sound%</div> | ||||
|  | ||||
| 				<ui-switch v-model="enableSounds">%i18n:@enable-sounds%</ui-switch> | ||||
| 				<section> | ||||
| 					<ui-switch v-model="enableSounds">%i18n:@enable-sounds%</ui-switch> | ||||
| 				</section> | ||||
| 			</ui-card> | ||||
|  | ||||
| 			<ui-card> | ||||
| 				<div slot="title">%fa:language% %i18n:@lang%</div> | ||||
|  | ||||
| 				<ui-select v-model="lang" placeholder="%i18n:@auto%"> | ||||
| 					<optgroup label="%i18n:@recommended%"> | ||||
| 						<option value="">%i18n:@auto%</option> | ||||
| 					</optgroup> | ||||
| 				<section class="fit-top"> | ||||
| 					<ui-select v-model="lang" placeholder="%i18n:@auto%"> | ||||
| 						<optgroup label="%i18n:@recommended%"> | ||||
| 							<option value="">%i18n:@auto%</option> | ||||
| 						</optgroup> | ||||
|  | ||||
| 					<optgroup label="%i18n:@specify-language%"> | ||||
| 						<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option> | ||||
| 					</optgroup> | ||||
| 				</ui-select> | ||||
| 				<span>%fa:info-circle% %i18n:@lang-tip%</span> | ||||
| 						<optgroup label="%i18n:@specify-language%"> | ||||
| 							<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option> | ||||
| 						</optgroup> | ||||
| 					</ui-select> | ||||
| 					<span>%fa:info-circle% %i18n:@lang-tip%</span> | ||||
| 				</section> | ||||
| 			</ui-card> | ||||
|  | ||||
| 			<ui-card> | ||||
| 				<div slot="title">%fa:B twitter% %i18n:@twitter%</div> | ||||
|  | ||||
| 				<p class="account" v-if="$store.state.i.twitter"><a :href="`https://twitter.com/${$store.state.i.twitter.screenName}`" target="_blank">@{{ $store.state.i.twitter.screenName }}</a></p> | ||||
| 				<p> | ||||
| 					<a :href="`${apiUrl}/connect/twitter`" target="_blank">{{ $store.state.i.twitter ? '%i18n:@twitter-reconnect%' : '%i18n:@twitter-connect%' }}</a> | ||||
| 					<span v-if="$store.state.i.twitter"> or </span> | ||||
| 					<a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="$store.state.i.twitter">%i18n:@twitter-disconnect%</a> | ||||
| 				</p> | ||||
| 				<section> | ||||
| 					<p class="account" v-if="$store.state.i.twitter"><a :href="`https://twitter.com/${$store.state.i.twitter.screenName}`" target="_blank">@{{ $store.state.i.twitter.screenName }}</a></p> | ||||
| 					<p> | ||||
| 						<a :href="`${apiUrl}/connect/twitter`" target="_blank">{{ $store.state.i.twitter ? '%i18n:@twitter-reconnect%' : '%i18n:@twitter-connect%' }}</a> | ||||
| 						<span v-if="$store.state.i.twitter"> or </span> | ||||
| 						<a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="$store.state.i.twitter">%i18n:@twitter-disconnect%</a> | ||||
| 					</p> | ||||
| 				</section> | ||||
| 			</ui-card> | ||||
|  | ||||
| 			<ui-card> | ||||
| 				<div slot="title">%fa:sync-alt% %i18n:@update%</div> | ||||
|  | ||||
| 				<div>%i18n:@version% <i>{{ version }}</i></div> | ||||
| 				<template v-if="latestVersion !== undefined"> | ||||
| 					<div>%i18n:@latest-version% <i>{{ latestVersion ? latestVersion : version }}</i></div> | ||||
| 				</template> | ||||
| 				<ui-button @click="checkForUpdate" :disabled="checkingForUpdate"> | ||||
| 					<template v-if="checkingForUpdate">%i18n:@update-checking%<mk-ellipsis/></template> | ||||
| 					<template v-else>%i18n:@check-for-updates%</template> | ||||
| 				</ui-button> | ||||
| 				<section> | ||||
| 					<div>%i18n:@version% <i>{{ version }}</i></div> | ||||
| 					<template v-if="latestVersion !== undefined"> | ||||
| 						<div>%i18n:@latest-version% <i>{{ latestVersion ? latestVersion : version }}</i></div> | ||||
| 					</template> | ||||
| 					<ui-button @click="checkForUpdate" :disabled="checkingForUpdate"> | ||||
| 						<template v-if="checkingForUpdate">%i18n:@update-checking%<mk-ellipsis/></template> | ||||
| 						<template v-else>%i18n:@check-for-updates%</template> | ||||
| 					</ui-button> | ||||
| 				</section> | ||||
| 			</ui-card> | ||||
| 		</div> | ||||
|  | ||||
| @@ -129,11 +168,21 @@ export default Vue.extend({ | ||||
| 			set(value) { this.$store.commit('device/set', { key: 'darkmode', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		alwaysShowNsfw: { | ||||
| 			get() { return this.$store.state.device.alwaysShowNsfw; }, | ||||
| 			set(value) { this.$store.commit('device/set', { key: 'alwaysShowNsfw', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		postStyle: { | ||||
| 			get() { return this.$store.state.device.postStyle; }, | ||||
| 			set(value) { this.$store.commit('device/set', { key: 'postStyle', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		mobileNotificationPosition: { | ||||
| 			get() { return this.$store.state.device.mobileNotificationPosition; }, | ||||
| 			set(value) { this.$store.commit('device/set', { key: 'mobileNotificationPosition', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		lightmode: { | ||||
| 			get() { return this.$store.state.device.lightmode; }, | ||||
| 			set(value) { this.$store.commit('device/set', { key: 'lightmode', value }); } | ||||
| @@ -153,6 +202,86 @@ export default Vue.extend({ | ||||
| 			get() { return this.$store.state.device.enableSounds; }, | ||||
| 			set(value) { this.$store.commit('device/set', { key: 'enableSounds', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		fetchOnScroll: { | ||||
| 			get() { return this.$store.state.settings.fetchOnScroll; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'fetchOnScroll', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		rememberNoteVisibility: { | ||||
| 			get() { return this.$store.state.settings.rememberNoteVisibility; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'rememberNoteVisibility', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		disableViaMobile: { | ||||
| 			get() { return this.$store.state.settings.disableViaMobile; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'disableViaMobile', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		loadRemoteMedia: { | ||||
| 			get() { return this.$store.state.settings.loadRemoteMedia; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'loadRemoteMedia', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		circleIcons: { | ||||
| 			get() { return this.$store.state.settings.circleIcons; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'circleIcons', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		contrastedAcct: { | ||||
| 			get() { return this.$store.state.settings.contrastedAcct; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'contrastedAcct', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		showFullAcct: { | ||||
| 			get() { return this.$store.state.settings.showFullAcct; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'showFullAcct', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		iLikeSushi: { | ||||
| 			get() { return this.$store.state.settings.iLikeSushi; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'iLikeSushi', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		games_reversi_showBoardLabels: { | ||||
| 			get() { return this.$store.state.settings.games.reversi.showBoardLabels; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'games.reversi.showBoardLabels', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		games_reversi_useContrastStones: { | ||||
| 			get() { return this.$store.state.settings.games.reversi.useContrastStones; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'games.reversi.useContrastStones', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		disableAnimatedMfm: { | ||||
| 			get() { return this.$store.state.settings.disableAnimatedMfm; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'disableAnimatedMfm', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		showReplyTarget: { | ||||
| 			get() { return this.$store.state.settings.showReplyTarget; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'showReplyTarget', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		showMyRenotes: { | ||||
| 			get() { return this.$store.state.settings.showMyRenotes; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'showMyRenotes', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		showRenotedMyNotes: { | ||||
| 			get() { return this.$store.state.settings.showRenotedMyNotes; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'showRenotedMyNotes', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		showLocalRenotes: { | ||||
| 			get() { return this.$store.state.settings.showLocalRenotes; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'showLocalRenotes', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		defaultNoteVisibility: { | ||||
| 			get() { return this.$store.state.settings.defaultNoteVisibility; }, | ||||
| 			set(value) { this.$store.dispatch('settings/set', { key: 'defaultNoteVisibility', value }); } | ||||
| 		}, | ||||
| 	}, | ||||
|  | ||||
| 	mounted() { | ||||
| @@ -164,90 +293,6 @@ export default Vue.extend({ | ||||
| 			(this as any).os.signout(); | ||||
| 		}, | ||||
|  | ||||
| 		onChangeFetchOnScroll(v) { | ||||
| 			this.$store.dispatch('settings/set', { | ||||
| 				key: 'fetchOnScroll', | ||||
| 				value: v | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		onChangeDisableViaMobile(v) { | ||||
| 			this.$store.dispatch('settings/set', { | ||||
| 				key: 'disableViaMobile', | ||||
| 				value: v | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		onChangeLoadRemoteMedia(v) { | ||||
| 			this.$store.dispatch('settings/set', { | ||||
| 				key: 'loadRemoteMedia', | ||||
| 				value: v | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		onChangeCircleIcons(v) { | ||||
| 			this.$store.dispatch('settings/set', { | ||||
| 				key: 'circleIcons', | ||||
| 				value: v | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		onChangeILikeSushi(v) { | ||||
| 			this.$store.dispatch('settings/set', { | ||||
| 				key: 'iLikeSushi', | ||||
| 				value: v | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		onChangeReversiBoardLabels(v) { | ||||
| 			this.$store.dispatch('settings/set', { | ||||
| 				key: 'games.reversi.showBoardLabels', | ||||
| 				value: v | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		onChangeUseContrastReversiStones(v) { | ||||
| 			this.$store.dispatch('settings/set', { | ||||
| 				key: 'games.reversi.useContrastStones', | ||||
| 				value: v | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		onChangeDisableAnimatedMfm(v) { | ||||
| 			this.$store.dispatch('settings/set', { | ||||
| 				key: 'disableAnimatedMfm', | ||||
| 				value: v | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		onChangeShowReplyTarget(v) { | ||||
| 			this.$store.dispatch('settings/set', { | ||||
| 				key: 'showReplyTarget', | ||||
| 				value: v | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		onChangeShowMyRenotes(v) { | ||||
| 			this.$store.dispatch('settings/set', { | ||||
| 				key: 'showMyRenotes', | ||||
| 				value: v | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		onChangeShowRenotedMyNotes(v) { | ||||
| 			this.$store.dispatch('settings/set', { | ||||
| 				key: 'showRenotedMyNotes', | ||||
| 				value: v | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		onChangeShowLocalRenotes(v) { | ||||
| 			this.$store.dispatch('settings/set', { | ||||
| 				key: 'showLocalRenotes', | ||||
| 				value: v | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		checkForUpdate() { | ||||
| 			this.checkingForUpdate = true; | ||||
| 			checkForUpdate((this as any).os, true, true).then(newer => { | ||||
| @@ -273,7 +318,7 @@ export default Vue.extend({ | ||||
| <style lang="stylus" scoped> | ||||
| root(isDark) | ||||
| 	margin 0 auto | ||||
| 	max-width 500px | ||||
| 	max-width 600px | ||||
| 	width 100% | ||||
|  | ||||
| 	> .signin-as | ||||
|   | ||||
| @@ -2,47 +2,64 @@ | ||||
| <ui-card> | ||||
| 	<div slot="title">%fa:user% %i18n:@title%</div> | ||||
|  | ||||
| 	<ui-form :disabled="saving"> | ||||
| 		<ui-input v-model="name" :max="30"> | ||||
| 			<span>%i18n:@name%</span> | ||||
| 		</ui-input> | ||||
| 	<section class="fit-top"> | ||||
| 		<ui-form :disabled="saving"> | ||||
| 			<ui-input v-model="name" :max="30"> | ||||
| 				<span>%i18n:@name%</span> | ||||
| 			</ui-input> | ||||
|  | ||||
| 		<ui-input v-model="username" readonly> | ||||
| 			<span>%i18n:@account%</span> | ||||
| 			<span slot="prefix">@</span> | ||||
| 			<span slot="suffix">@{{ host }}</span> | ||||
| 		</ui-input> | ||||
| 			<ui-input v-model="username" readonly> | ||||
| 				<span>%i18n:@account%</span> | ||||
| 				<span slot="prefix">@</span> | ||||
| 				<span slot="suffix">@{{ host }}</span> | ||||
| 			</ui-input> | ||||
|  | ||||
| 		<ui-input v-model="location"> | ||||
| 			<span>%i18n:@location%</span> | ||||
| 			<span slot="prefix">%fa:map-marker-alt%</span> | ||||
| 		</ui-input> | ||||
| 			<ui-input v-model="location"> | ||||
| 				<span>%i18n:@location%</span> | ||||
| 				<span slot="prefix">%fa:map-marker-alt%</span> | ||||
| 			</ui-input> | ||||
|  | ||||
| 		<ui-input v-model="birthday" type="date"> | ||||
| 			<span>%i18n:@birthday%</span> | ||||
| 			<span slot="prefix">%fa:birthday-cake%</span> | ||||
| 		</ui-input> | ||||
| 			<ui-input v-model="birthday" type="date"> | ||||
| 				<span>%i18n:@birthday%</span> | ||||
| 				<span slot="prefix">%fa:birthday-cake%</span> | ||||
| 			</ui-input> | ||||
|  | ||||
| 		<ui-textarea v-model="description" :max="500"> | ||||
| 			<span>%i18n:@description%</span> | ||||
| 		</ui-textarea> | ||||
| 			<ui-textarea v-model="description" :max="500"> | ||||
| 				<span>%i18n:@description%</span> | ||||
| 			</ui-textarea> | ||||
|  | ||||
| 		<ui-input type="file" @change="onAvatarChange"> | ||||
| 			<span>%i18n:@avatar%</span> | ||||
| 			<span slot="icon">%fa:image%</span> | ||||
| 			<span slot="text" v-if="avatarUploading">%i18n:@uploading%<mk-ellipsis/></span> | ||||
| 		</ui-input> | ||||
| 			<ui-input type="file" @change="onAvatarChange"> | ||||
| 				<span>%i18n:@avatar%</span> | ||||
| 				<span slot="icon">%fa:image%</span> | ||||
| 				<span slot="text" v-if="avatarUploading">%i18n:@uploading%<mk-ellipsis/></span> | ||||
| 			</ui-input> | ||||
|  | ||||
| 		<ui-input type="file" @change="onBannerChange"> | ||||
| 			<span>%i18n:@banner%</span> | ||||
| 			<span slot="icon">%fa:image%</span> | ||||
| 			<span slot="text" v-if="bannerUploading">%i18n:@uploading%<mk-ellipsis/></span> | ||||
| 		</ui-input> | ||||
| 			<ui-input type="file" @change="onBannerChange"> | ||||
| 				<span>%i18n:@banner%</span> | ||||
| 				<span slot="icon">%fa:image%</span> | ||||
| 				<span slot="text" v-if="bannerUploading">%i18n:@uploading%<mk-ellipsis/></span> | ||||
| 			</ui-input> | ||||
|  | ||||
| 		<ui-switch v-model="isCat">%i18n:@is-cat%</ui-switch> | ||||
| 			<ui-button @click="save(true)">%i18n:@save%</ui-button> | ||||
| 		</ui-form> | ||||
| 	</section> | ||||
|  | ||||
| 		<ui-button @click="save">%i18n:@save%</ui-button> | ||||
| 	</ui-form> | ||||
| 	<section> | ||||
| 		<header>%i18n:@advanced%</header> | ||||
|  | ||||
| 		<div> | ||||
| 			<ui-switch v-model="isCat" @change="save(false)">%i18n:@is-cat%</ui-switch> | ||||
| 			<ui-switch v-model="alwaysMarkNsfw">%i18n:common.always-mark-nsfw%</ui-switch> | ||||
| 		</div> | ||||
| 	</section> | ||||
|  | ||||
| 	<section> | ||||
| 		<header>%i18n:@privacy%</header> | ||||
|  | ||||
| 		<div> | ||||
| 			<ui-switch v-model="isLocked" @change="save(false)">%i18n:@is-locked%</ui-switch> | ||||
| 		</div> | ||||
| 	</section> | ||||
| </ui-card> | ||||
| </template> | ||||
|  | ||||
| @@ -62,12 +79,20 @@ export default Vue.extend({ | ||||
| 			avatarId: null, | ||||
| 			bannerId: null, | ||||
| 			isCat: false, | ||||
| 			isLocked: false, | ||||
| 			saving: false, | ||||
| 			avatarUploading: false, | ||||
| 			bannerUploading: false | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| 	computed: { | ||||
| 		alwaysMarkNsfw: { | ||||
| 			get() { return this.$store.state.i.settings.alwaysMarkNsfw; }, | ||||
| 			set(value) { (this as any).api('i/update', { alwaysMarkNsfw: value }); } | ||||
| 		}, | ||||
| 	}, | ||||
|  | ||||
| 	created() { | ||||
| 		this.name = this.$store.state.i.name || ''; | ||||
| 		this.username = this.$store.state.i.username; | ||||
| @@ -77,6 +102,7 @@ export default Vue.extend({ | ||||
| 		this.avatarId = this.$store.state.i.avatarId; | ||||
| 		this.bannerId = this.$store.state.i.bannerId; | ||||
| 		this.isCat = this.$store.state.i.isCat; | ||||
| 		this.isLocked = this.$store.state.i.isLocked; | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| @@ -124,7 +150,7 @@ export default Vue.extend({ | ||||
| 				}); | ||||
| 		}, | ||||
|  | ||||
| 		save() { | ||||
| 		save(notify) { | ||||
| 			this.saving = true; | ||||
|  | ||||
| 			(this as any).api('i/update', { | ||||
| @@ -134,7 +160,8 @@ export default Vue.extend({ | ||||
| 				birthday: this.birthday || null, | ||||
| 				avatarId: this.avatarId, | ||||
| 				bannerId: this.bannerId, | ||||
| 				isCat: this.isCat | ||||
| 				isCat: this.isCat, | ||||
| 				isLocked: this.isLocked | ||||
| 			}).then(i => { | ||||
| 				this.saving = false; | ||||
| 				this.$store.state.i.avatarId = i.avatarId; | ||||
| @@ -142,7 +169,9 @@ export default Vue.extend({ | ||||
| 				this.$store.state.i.bannerId = i.bannerId; | ||||
| 				this.$store.state.i.bannerUrl = i.bannerUrl; | ||||
|  | ||||
| 				alert('%i18n:@saved%'); | ||||
| 				if (notify) { | ||||
| 					alert('%i18n:@saved%'); | ||||
| 				} | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -16,7 +16,7 @@ | ||||
| 				</div> | ||||
| 				<div class="title"> | ||||
| 					<h1>{{ user | userName }}</h1> | ||||
| 					<span class="username"><mk-acct :user="user"/></span> | ||||
| 					<span class="username"><mk-acct :user="user" :detail="true" /></span> | ||||
| 					<span class="followed" v-if="user.isFollowed">%i18n:@follows-you%</span> | ||||
| 				</div> | ||||
| 				<div class="description"> | ||||
|   | ||||
| @@ -26,7 +26,7 @@ export default Vue.extend({ | ||||
| 	mounted() { | ||||
| 		(this as any).api('users/notes', { | ||||
| 			userId: this.user.id, | ||||
| 			withMedia: true, | ||||
| 			withFiles: true, | ||||
| 			limit: 6 | ||||
| 		}).then(notes => { | ||||
| 			notes.forEach(note => { | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| <template> | ||||
| <div class="welcome"> | ||||
| <div class="wgwfgvvimdjvhjfwxropcwksnzftjqes"> | ||||
| 	<div> | ||||
| 		<img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" :alt="name"> | ||||
| 		<p class="host">{{ host }}</p> | ||||
| @@ -15,12 +15,53 @@ | ||||
| 			<mk-welcome-timeline/> | ||||
| 		</div> | ||||
| 		<div class="hashtags"> | ||||
| 			<router-link v-for="tag in tags" :key="tag" :to="`/tags/${ tag }`" :title="tag">#{{ tag }}</router-link> | ||||
| 			<mk-tag-cloud/> | ||||
| 		</div> | ||||
| 		<div class="photos"> | ||||
| 			<div v-for="photo in photos" :style="`background-image: url(${photo.thumbnailUrl})`"></div> | ||||
| 		</div> | ||||
| 		<div class="stats" v-if="stats"> | ||||
| 			<span>%fa:user% {{ stats.originalUsersCount | number }}</span> | ||||
| 			<span>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</span> | ||||
| 		</div> | ||||
| 		<div class="announcements" v-if="announcements && announcements.length > 0"> | ||||
| 			<article v-for="announcement in announcements"> | ||||
| 				<span class="title" v-html="announcement.title"></span> | ||||
| 				<div v-html="announcement.text"></div> | ||||
| 			</article> | ||||
| 		</div> | ||||
| 		<article class="about-misskey"> | ||||
| 			<h1>%i18n:common.intro.title%</h1> | ||||
| 			<p v-html="'%i18n:common.intro.about%'"></p> | ||||
| 			<section> | ||||
| 				<h2>%i18n:common.intro.features%</h2> | ||||
| 				<section> | ||||
| 					<h3>%i18n:common.intro.rich-contents%</h3> | ||||
| 					<div class="image"><img src="/assets/about/post.png" alt=""></div> | ||||
| 					<p v-html="'%i18n:common.intro.rich-contents-desc%'"></p> | ||||
| 				</section> | ||||
| 				<section> | ||||
| 					<h3>%i18n:common.intro.reaction%</h3> | ||||
| 					<div class="image"><img src="/assets/about/reaction.png" alt=""></div> | ||||
| 					<p v-html="'%i18n:common.intro.reaction-desc%'"></p> | ||||
| 				</section> | ||||
| 				<section> | ||||
| 					<h3>%i18n:common.intro.ui%</h3> | ||||
| 					<div class="image"><img src="/assets/about/ui.png" alt=""></div> | ||||
| 					<p v-html="'%i18n:common.intro.ui-desc%'"></p> | ||||
| 				</section> | ||||
| 				<section> | ||||
| 					<h3>%i18n:common.intro.drive%</h3> | ||||
| 					<div class="image"><img src="/assets/about/drive.png" alt=""></div> | ||||
| 					<p v-html="'%i18n:common.intro.drive-desc%'"></p> | ||||
| 				</section> | ||||
| 			</section> | ||||
| 			<p v-html="'%i18n:common.intro.outro%'"></p> | ||||
| 		</article> | ||||
| 		<div class="info" v-if="meta"> | ||||
| 			<p>Version: <b>{{ meta.version }}</b></p> | ||||
| 			<p>Maintainer: <b><a :href="meta.maintainer.url" target="_blank">{{ meta.maintainer.name }}</a></b></p> | ||||
| 		</div> | ||||
| 		<footer> | ||||
| 			<small>{{ copyright }}</small> | ||||
| 		</footer> | ||||
| @@ -30,39 +71,53 @@ | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import { apiUrl, copyright, host } from '../../../config'; | ||||
| import { copyright, host } from '../../../config'; | ||||
| import { concat } from '../../../../../prelude/array'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	data() { | ||||
| 		return { | ||||
| 			apiUrl, | ||||
| 			meta: null, | ||||
| 			copyright, | ||||
| 			stats: null, | ||||
| 			host, | ||||
| 			name: 'Misskey', | ||||
| 			description: '', | ||||
| 			tags: [] | ||||
| 			photos: [], | ||||
| 			announcements: [] | ||||
| 		}; | ||||
| 	}, | ||||
| 	created() { | ||||
| 		(this as any).os.getMeta().then(meta => { | ||||
| 			this.meta = meta; | ||||
| 			this.name = meta.name; | ||||
| 			this.description = meta.description; | ||||
| 			this.announcements = meta.broadcasts; | ||||
| 		}); | ||||
|  | ||||
| 		(this as any).api('stats').then(stats => { | ||||
| 			this.stats = stats; | ||||
| 		}); | ||||
|  | ||||
| 		(this as any).api('hashtags/trend').then(stats => { | ||||
| 			this.tags = stats.map(x => x.tag); | ||||
| 		const image = [ | ||||
| 			'image/jpeg', | ||||
| 			'image/png', | ||||
| 			'image/gif' | ||||
| 		]; | ||||
|  | ||||
| 		(this as any).api('notes/local-timeline', { | ||||
| 			fileType: image, | ||||
| 			limit: 6 | ||||
| 		}).then((notes: any[]) => { | ||||
| 			const files = concat(notes.map((n: any): any[] => n.files)); | ||||
| 			this.photos = files.filter(f => image.includes(f.type)).slice(0, 6); | ||||
| 		}); | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| .welcome | ||||
| root(isDark) | ||||
| 	text-align center | ||||
| 	//background #fff | ||||
|  | ||||
| @@ -138,12 +193,21 @@ export default Vue.extend({ | ||||
| 				-webkit-overflow-scrolling touch | ||||
|  | ||||
| 		> .hashtags | ||||
| 			padding 16px 0 | ||||
| 			border solid 2px #ddd | ||||
| 			border-radius 8px | ||||
| 			padding 0 8px | ||||
| 			height 200px | ||||
|  | ||||
| 			> * | ||||
| 				margin 0 16px | ||||
| 		> .photos | ||||
| 			display grid | ||||
| 			grid-template-rows 1fr 1fr 1fr | ||||
| 			grid-template-columns 1fr 1fr | ||||
| 			gap 8px | ||||
| 			height 300px | ||||
| 			margin-top 16px | ||||
|  | ||||
| 			> div | ||||
| 				border-radius 4px | ||||
| 				background-position center center | ||||
| 				background-size cover | ||||
|  | ||||
| 		> .stats | ||||
| 			margin 16px 0 | ||||
| @@ -156,6 +220,68 @@ export default Vue.extend({ | ||||
| 			> * | ||||
| 				margin 0 8px | ||||
|  | ||||
| 		> .announcements | ||||
| 			margin 16px 0 | ||||
|  | ||||
| 			> article | ||||
| 				background isDark ? rgba(30, 129, 216, 0.2) : rgba(155, 196, 232, 0.2) | ||||
| 				border-radius 6px | ||||
| 				color isDark ? #fff : #3f4967 | ||||
| 				padding 16px | ||||
| 				margin 8px 0 | ||||
| 				font-size 12px | ||||
|  | ||||
| 				> .title | ||||
| 					font-weight bold | ||||
|  | ||||
| 		> .about-misskey | ||||
| 			margin 16px 0 | ||||
| 			padding 32px | ||||
| 			font-size 14px | ||||
| 			background #fff | ||||
| 			border-radius 6px | ||||
| 			overflow hidden | ||||
| 			color #3a3e46 | ||||
|  | ||||
| 			> h1 | ||||
| 				margin 0 | ||||
|  | ||||
| 				& + p | ||||
| 					margin-top 8px | ||||
|  | ||||
| 			> p:last-child | ||||
| 				margin-bottom 0 | ||||
|  | ||||
| 			> section | ||||
| 				> h2 | ||||
| 					border-bottom 1px solid isDark ? rgba(#000, 0.2) : rgba(#000, 0.05) | ||||
|  | ||||
| 				> section | ||||
| 					margin-bottom 16px | ||||
| 					padding-bottom 16px | ||||
| 					border-bottom 1px solid isDark ? rgba(#000, 0.2) : rgba(#000, 0.05) | ||||
|  | ||||
| 					> h3 | ||||
| 						margin-bottom 8px | ||||
|  | ||||
| 					> p | ||||
| 						margin-bottom 0 | ||||
|  | ||||
| 					> .image | ||||
| 						> img | ||||
| 							display block | ||||
| 							width 100% | ||||
| 							height 120px | ||||
| 							object-fit cover | ||||
|  | ||||
| 		> .info | ||||
| 			padding 16px 0 | ||||
| 			border solid 2px #ddd | ||||
| 			border-radius 8px | ||||
|  | ||||
| 			> * | ||||
| 				margin 0 16px | ||||
|  | ||||
| 		> footer | ||||
| 			text-align center | ||||
| 			color #444 | ||||
| @@ -165,4 +291,10 @@ export default Vue.extend({ | ||||
| 				margin 16px 0 0 0 | ||||
| 				opacity 0.7 | ||||
|  | ||||
| .wgwfgvvimdjvhjfwxropcwksnzftjqes[data-darkmode] | ||||
| 	root(true) | ||||
|  | ||||
| .wgwfgvvimdjvhjfwxropcwksnzftjqes:not([data-darkmode]) | ||||
| 	root(false) | ||||
|  | ||||
| </style> | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import * as nestedProperty from 'nested-property'; | ||||
|  | ||||
| import MiOS from './mios'; | ||||
| import { hostname } from './config'; | ||||
| import { erase } from '../../prelude/array'; | ||||
|  | ||||
| const defaultSettings = { | ||||
| 	home: null, | ||||
| @@ -15,6 +16,8 @@ const defaultSettings = { | ||||
| 	suggestRecentHashtags: true, | ||||
| 	showClockOnHeader: true, | ||||
| 	circleIcons: true, | ||||
| 	contrastedAcct: true, | ||||
| 	showFullAcct: false, | ||||
| 	gradientWindowHeader: false, | ||||
| 	showReplyTarget: true, | ||||
| 	showMyRenotes: true, | ||||
| @@ -24,6 +27,8 @@ const defaultSettings = { | ||||
| 	disableViaMobile: false, | ||||
| 	memo: null, | ||||
| 	iLikeSushi: false, | ||||
| 	rememberNoteVisibility: false, | ||||
| 	defaultNoteVisibility: 'public', | ||||
| 	games: { | ||||
| 		reversi: { | ||||
| 			showBoardLabels: false, | ||||
| @@ -43,7 +48,9 @@ const defaultDeviceSettings = { | ||||
| 	debug: false, | ||||
| 	lightmode: false, | ||||
| 	loadRawImages: false, | ||||
| 	postStyle: 'standard' | ||||
| 	alwaysShowNsfw: false, | ||||
| 	postStyle: 'standard', | ||||
| 	mobileNotificationPosition: 'bottom' | ||||
| }; | ||||
|  | ||||
| export default (os: MiOS) => new Vuex.Store({ | ||||
| @@ -194,7 +201,7 @@ export default (os: MiOS) => new Vuex.Store({ | ||||
|  | ||||
| 				removeDeckColumn(state, id) { | ||||
| 					state.deck.columns = state.deck.columns.filter(c => c.id != id); | ||||
| 					state.deck.layout = state.deck.layout.map(ids => ids.filter(x => x != id)); | ||||
| 					state.deck.layout = state.deck.layout.map(ids => erase(id, ids)); | ||||
| 					state.deck.layout = state.deck.layout.filter(ids => ids.length > 0); | ||||
| 				}, | ||||
|  | ||||
| @@ -265,7 +272,7 @@ export default (os: MiOS) => new Vuex.Store({ | ||||
|  | ||||
| 				stackLeftDeckColumn(state, id) { | ||||
| 					const i = state.deck.layout.findIndex(ids => ids.indexOf(id) != -1); | ||||
| 					state.deck.layout = state.deck.layout.map(ids => ids.filter(x => x != id)); | ||||
| 					state.deck.layout = state.deck.layout.map(ids => erase(id, ids)); | ||||
| 					const left = state.deck.layout[i - 1]; | ||||
| 					if (left) state.deck.layout[i - 1].push(id); | ||||
| 					state.deck.layout = state.deck.layout.filter(ids => ids.length > 0); | ||||
| @@ -273,7 +280,7 @@ export default (os: MiOS) => new Vuex.Store({ | ||||
|  | ||||
| 				popRightDeckColumn(state, id) { | ||||
| 					const i = state.deck.layout.findIndex(ids => ids.indexOf(id) != -1); | ||||
| 					state.deck.layout = state.deck.layout.map(ids => ids.filter(x => x != id)); | ||||
| 					state.deck.layout = state.deck.layout.map(ids => erase(id, ids)); | ||||
| 					state.deck.layout.splice(i + 1, 0, [id]); | ||||
| 					state.deck.layout = state.deck.layout.filter(ids => ids.length > 0); | ||||
| 				}, | ||||
|   | ||||
| @@ -3,6 +3,7 @@ | ||||
|  */ | ||||
|  | ||||
| import composeNotification from './common/scripts/compose-notification'; | ||||
| import { erase } from '../../prelude/array'; | ||||
|  | ||||
| // キャッシュするリソース | ||||
| const cachee = [ | ||||
| @@ -24,8 +25,7 @@ self.addEventListener('activate', ev => { | ||||
| 	// Clean up old caches | ||||
| 	ev.waitUntil( | ||||
| 		caches.keys().then(keys => Promise.all( | ||||
| 			keys | ||||
| 				.filter(key => key != _VERSION_) | ||||
| 			erase(_VERSION_, keys) | ||||
| 				.map(key => caches.delete(key)) | ||||
| 		)) | ||||
| 	); | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	 tamaina
					tamaina