Migrate to Vue3 (#6587)
* Update reaction.vue
* fix  bug
* wip
* wip
* wjio
* wip
* Revert "wip"
This reverts commit e427f2160a.
* wip
* wip
* wip
* Update init.ts
* Update drive-window.vue
* wip
* wip
* Use PascalCase for components
* Use PascalCase for components
* update dep
* wip
* wip
* wip
* Update init.ts
* wip
* Update paging.ts
* Update test.vue
* watch deep
* wip
* lint
* wip
* wip
* wip
* wip
* wiop
* wip
* Update webpack.config.ts
* alllow null poll
* wip
* wip
* wip
* wiop
* UI redesign & refactor (#6714)
* wip
* wip
* wip
* wip
* wip
* Update drive.vue
* Update word-mute.vue
* wip
* wip
* wip
* clean up
* wip
* Update default.vue
* wip
* Update notes.vue
* Update mfm.ts
* Update index.home.vue
* Update post-form.vue
* Update post-form-attaches.vue
* wip
* Update post-form.vue
* Update sidebar.vue
* wip
* wip
* Update index.vue
* wip
* Update default.vue
* Update index.vue
* Update index.vue
* wip
* Update post-form-attaches.vue
* Update note.vue
* wip
* clean up
* Update notes.vue
* wip
* wip
* Update ja-JP.yml
* wip
* wip
* Update index.vue
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* Update default.vue
* wip
* Update _dark.json5
* wip
* wip
* wip
* clean up
* wip
* wip
* Update index.vue
* Update test.vue
* wip
* wip
* fix
* wip
* wip
* wip
* wip
* clena yop
* wip
* wip
* Update store.ts
* Update messaging-room.vue
* Update default.widgets.vue
* fix
* wip
* wip
* Update modal.vue
* wip
* Update os.ts
* Update os.ts
* Update deck.vue
* Update init.ts
* wip
* Update ja-JP.yml
* v-sizeは単にwindowのresizeを監視するだけで良いかもしれない
* Update modal.vue
* wip
* Update tooltip.ts
* wip
* wip
* wip
* wip
* wip
* Update image-viewer.vue
* wip
* wip
* Update style.scss
* Update style.scss
* Update visitor.vue
* wip
* Update init.ts
* Update init.ts
* wip
* wip
* Update visitor.vue
* Update visitor.vue
* Update visitor.vue
* Update visitor.vue
* wip
* wip
* Update modal.vue
* Update header.vue
* Update menu.vue
* Update about.vue
* Update about-misskey.vue
* wip
* wip
* Update visitor.vue
* Update tooltip.ts
* wip
* Update drive.vue
* wip
* Update style.scss
* Update header.vue
* wip
* wip
* Update users.user.vue
* Update announcements.vue
* wip
* wip
* wip
* Update emojis.vue
* wip
* Update emojis.vue
* Update style.scss
* Update users.vue
* wip
* Update style.scss
* wip
* Update welcome.entrance.vue
* Update radio.vue
* Update size.ts
* Update emoji-edit-dialog.vue
* wip
* Update emojis.vue
* wip
* Update emojis.vue
* Update emojis.vue
* Update emojis.vue
* wip
* wip
* wip
* wip
* Update file-dialog.vue
* wip
* wip
* Update token-generate-window.vue
* Update notification-setting-window.vue
* wip
* wip
* Update _error_.vue
* Update ja-JP.yml
* wip
* wip
* Update store.ts
* Update emojis.vue
* Update emojis.vue
* Update emojis.vue
* Update announcements.vue
* Update store.ts
* wip
* Update page-editor.vue
* wip
* wip
* Update modal.vue
* wip
* Update select-file.ts
* Update timeline.vue
* Update emojis.vue
* Update os.ts
* wip
* Update user-select.vue
* Update mfm.ts
* Update get-file-info.ts
* Update drive.vue
* Update init.ts
* Update mfm.ts
* wip
* wip
* Update window.vue
* Update note.vue
* wip
* wip
* Update user-info.vue
* wip
* wip
* wip
* wip
* wip
* Update header.vue
* Update header.vue
* wip
* Update explore.vue
* wip
* wip
* wip
* Update webpack.config.ts
* wip
* wip
* wip
* wip
* wip
* wip
* Update autocomplete.ts
* wip
* wip
* wip
* Update toast.vue
* wip
* Update post-form-dialog.vue
* wip
* wip
* wip
* wip
* wip
* Update users.vue
* wip
* Update explore.vue
* wip
* wip
* wip
* Update package.json
* wip
* Update icon-dialog.vue
* wip
* wip
* Update user-preview.ts
* wip
* wip
* wip
* wip
* wip
* Update instance.vue
* Update user-name.vue
* Update federation.vue
* Update instance.vue
* wip
* wip
* Update tag.vue
* wip
* wip
* wip
* wip
* wip
* Update instance.vue
* wip
* Update os.ts
* Update os.ts
* wip
* wip
* wip
* Update router.ts
* wip
* Update init.ts
* Update note.vue
* Update messages.vue
* wip
* wip
* wip
* wip
* wip
* google
* wip
* wip
* wip
* wip
* Update theme-editor.vue
* wip
* wip
* Update room.vue
* Update channel-editor.vue
* wip
* Update window.vue
* Update window.vue
* wip
* Update window.vue
* Update window.vue
* wip
* Update menu.vue
* wip
* wip
* wip
* wip
* Update messaging-room.vue
* wip
* Update post-form.vue
* Update default.widgets.vue
* Update window.vue
* wip
			
			
This commit is contained in:
		
							
								
								
									
										55
									
								
								src/client/pages/_error_.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/client/pages/_error_.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,55 @@
 | 
			
		||||
<template>
 | 
			
		||||
<transition :name="$store.state.device.animation ? 'zoom' : ''" appear>
 | 
			
		||||
	<div class="_section">
 | 
			
		||||
		<div class="mjndxjch _content">
 | 
			
		||||
			<img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/>
 | 
			
		||||
			<p><Fa :icon="faExclamationTriangle"/> {{ $t('pageLoadError') }}</p>
 | 
			
		||||
			<p>{{ $t('pageLoadErrorDescription') }}</p>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</transition>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkButton,
 | 
			
		||||
	},
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('error'),
 | 
			
		||||
					icon: faExclamationTriangle
 | 
			
		||||
				}]
 | 
			
		||||
			},
 | 
			
		||||
			faExclamationTriangle
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.mjndxjch {
 | 
			
		||||
	text-align: center;
 | 
			
		||||
 | 
			
		||||
	> p {
 | 
			
		||||
		margin: 0 0 8px 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .button {
 | 
			
		||||
		margin: 0 auto;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> img {
 | 
			
		||||
		vertical-align: bottom;
 | 
			
		||||
		height: 128px;
 | 
			
		||||
		margin-bottom: 16px;
 | 
			
		||||
		border-radius: 16px;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										10
									
								
								src/client/pages/_loading_.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/client/pages/_loading_.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
<template>
 | 
			
		||||
<MkLoading/>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({});
 | 
			
		||||
</script>
 | 
			
		||||
@@ -1,83 +1,97 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="znqjceqz">
 | 
			
		||||
	<portal to="title">{{ $t('aboutMisskey') }}</portal>
 | 
			
		||||
 | 
			
		||||
	<section class="_card">
 | 
			
		||||
		<div class="_title">{{ $t('aboutMisskey') }}</div>
 | 
			
		||||
	<section class="_section">
 | 
			
		||||
		<div class="_content" style="text-align: center;">
 | 
			
		||||
			<img src="/assets/icons/512.png" alt="" style="display: block; width: 100px; margin: 0 auto; border-radius: 16px;"/>
 | 
			
		||||
			<div style="margin-top: 0.75em;">Misskey</div>
 | 
			
		||||
			<div style="opacity: 0.5;">v{{ version }}</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
	<section class="_section">
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<div style="margin-bottom: 1em;">{{ $t('aboutMisskeyText') }}</div>
 | 
			
		||||
			<div>🛠️ {{ $t('misskeyMembers') }}</div>
 | 
			
		||||
			<ul class="members">
 | 
			
		||||
				<li><mk-link url="https://github.com/syuilo" class="at">@syuilo</mk-link></li>
 | 
			
		||||
				<li><mk-link url="https://github.com/AyaMorisawa" class="at">@AyaMorisawa</mk-link></li>
 | 
			
		||||
				<li><mk-link url="https://github.com/mei23" class="at">@mei23</mk-link></li>
 | 
			
		||||
				<li><mk-link url="https://github.com/acid-chicken" class="at">@acid-chicken</mk-link></li>
 | 
			
		||||
				<li><mk-link url="https://github.com/tamaina" class="at">@tamaina</mk-link></li>
 | 
			
		||||
				<li><mk-link url="https://github.com/rinsuki" class="at">@rinsuki</mk-link></li>
 | 
			
		||||
				<li><mk-link url="https://github.com/Xeltica" class="at">@Xeltica</mk-link></li>
 | 
			
		||||
				<li><mk-link url="https://github.com/u1-liquid" class="at">@u1-liquid</mk-link></li>
 | 
			
		||||
			</ul>
 | 
			
		||||
			<div style="margin-top: 1em;">📦 {{ $t('misskeySource') }}</div>
 | 
			
		||||
			<mk-url url="https://github.com/syuilo/misskey"/>
 | 
			
		||||
			<div style="margin-top: 1em;">🌏 {{ $t('misskeyTranslation') }}</div>
 | 
			
		||||
			<mk-url url="https://crowdin.com/project/misskey"/>
 | 
			
		||||
			<div style="margin-top: 1em;">💴 {{ $t('misskeyDonate') }}</div>
 | 
			
		||||
			<mk-url url="https://www.patreon.com/syuilo"/>
 | 
			
		||||
			<div style="text-align: center;">{{ $t('aboutMisskeyText') }}</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<span><mfm text="<motion>❤</motion>"/> {{ $t('patrons') }}</span>
 | 
			
		||||
			<ul>
 | 
			
		||||
				<li>Gargron</li>
 | 
			
		||||
				<li>Satsuki Yanagi</li>
 | 
			
		||||
				<li>noellabo</li>
 | 
			
		||||
				<li>naga_rus</li>
 | 
			
		||||
				<li>Melilot</li>
 | 
			
		||||
				<li>AureoleArk</li>
 | 
			
		||||
				<li>Peter G.</li>
 | 
			
		||||
				<li>motcha</li>
 | 
			
		||||
				<li>Atsuko Tominaga</li>
 | 
			
		||||
				<li>dansup</li>
 | 
			
		||||
				<li>Nokotaro Takeda</li>
 | 
			
		||||
				<li>YUKIMOCHI</li>
 | 
			
		||||
				<li>nanami kan</li>
 | 
			
		||||
				<li>Hekovic</li>
 | 
			
		||||
				<li>wara</li>
 | 
			
		||||
				<li>Takashi Shibuya</li>
 | 
			
		||||
				<li>Noizeman</li>
 | 
			
		||||
				<li>mydarkstar</li>
 | 
			
		||||
				<li>nenohi</li>
 | 
			
		||||
				<li>Eduardo Quiros</li>
 | 
			
		||||
	</section>
 | 
			
		||||
	<section class="_section">
 | 
			
		||||
		<div class="_content" style="text-align: center;">
 | 
			
		||||
			<div>📦 {{ $t('misskeySource') }}</div>
 | 
			
		||||
			<MkUrl url="https://github.com/syuilo/misskey"/>
 | 
			
		||||
			<div style="margin-top: 1em;">🌏 {{ $t('misskeyTranslation') }}</div>
 | 
			
		||||
			<MkUrl url="https://crowdin.com/project/misskey"/>
 | 
			
		||||
			<div style="margin-top: 1em;">💴 {{ $t('misskeyDonate') }}</div>
 | 
			
		||||
			<MkUrl url="https://www.patreon.com/syuilo"/>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
	<section class="_section">
 | 
			
		||||
		<div class="_content" style="text-align: center;">
 | 
			
		||||
			<div>🛠️ {{ $t('misskeyMembers') }}</div>
 | 
			
		||||
			<ul class="members" style="list-style: none; padding: 0; margin: 1em 0 0 0;">
 | 
			
		||||
				<li><MkLink url="https://github.com/syuilo" class="at">@syuilo</MkLink></li>
 | 
			
		||||
				<li><MkLink url="https://github.com/AyaMorisawa" class="at">@AyaMorisawa</MkLink></li>
 | 
			
		||||
				<li><MkLink url="https://github.com/mei23" class="at">@mei23</MkLink></li>
 | 
			
		||||
				<li><MkLink url="https://github.com/acid-chicken" class="at">@acid-chicken</MkLink></li>
 | 
			
		||||
				<li><MkLink url="https://github.com/tamaina" class="at">@tamaina</MkLink></li>
 | 
			
		||||
				<li><MkLink url="https://github.com/rinsuki" class="at">@rinsuki</MkLink></li>
 | 
			
		||||
				<li><MkLink url="https://github.com/Xeltica" class="at">@Xeltica</MkLink></li>
 | 
			
		||||
				<li><MkLink url="https://github.com/u1-liquid" class="at">@u1-liquid</MkLink></li>
 | 
			
		||||
			</ul>
 | 
			
		||||
			<span>{{ $t('morePatrons') }}</span>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
	<section class="_section">
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<div class="_card">
 | 
			
		||||
				<div class="_title"><Mfm text="<motion>❤</motion>"/> {{ $t('patrons') }}</div>
 | 
			
		||||
				<div class="_content">
 | 
			
		||||
					<ul style="margin: 0;">
 | 
			
		||||
						<li>Gargron</li>
 | 
			
		||||
						<li>Satsuki Yanagi</li>
 | 
			
		||||
						<li>noellabo</li>
 | 
			
		||||
						<li>naga_rus</li>
 | 
			
		||||
						<li>Melilot</li>
 | 
			
		||||
						<li>AureoleArk</li>
 | 
			
		||||
						<li>Peter G.</li>
 | 
			
		||||
						<li>motcha</li>
 | 
			
		||||
						<li>Atsuko Tominaga</li>
 | 
			
		||||
						<li>dansup</li>
 | 
			
		||||
						<li>Nokotaro Takeda</li>
 | 
			
		||||
						<li>YUKIMOCHI</li>
 | 
			
		||||
						<li>nanami kan</li>
 | 
			
		||||
						<li>Hekovic</li>
 | 
			
		||||
						<li>wara</li>
 | 
			
		||||
						<li>Takashi Shibuya</li>
 | 
			
		||||
						<li>Noizeman</li>
 | 
			
		||||
						<li>mydarkstar</li>
 | 
			
		||||
						<li>nenohi</li>
 | 
			
		||||
						<li>Eduardo Quiros</li>
 | 
			
		||||
					</ul>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div class="_footer">{{ $t('morePatrons') }}</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { version } from '../config';
 | 
			
		||||
import MkLink from '../components/link.vue';
 | 
			
		||||
import { version } from '@/config';
 | 
			
		||||
import MkLink from '@/components/link.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkLink
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: this.$t('aboutMisskey') as string
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('aboutMisskey'),
 | 
			
		||||
					icon: null
 | 
			
		||||
				}]
 | 
			
		||||
			},
 | 
			
		||||
			version,
 | 
			
		||||
			faInfoCircle
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,7 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="mmnnbwxb">
 | 
			
		||||
	<portal to="icon"><fa :icon="faInfoCircle"/></portal>
 | 
			
		||||
	<portal to="title">{{ $t('about') }}</portal>
 | 
			
		||||
 | 
			
		||||
	<section class="_card info" v-if="meta">
 | 
			
		||||
		<div class="_title"><fa :icon="faInfoCircle"/> {{ $t('instanceInfo') }}</div>
 | 
			
		||||
	<section class="_section info" v-if="meta">
 | 
			
		||||
		<div class="_title"><Fa :icon="faInfoCircle"/> {{ $t('instanceInfo') }}</div>
 | 
			
		||||
		<div class="_content" v-if="meta.description">
 | 
			
		||||
			<div v-html="meta.description"></div>
 | 
			
		||||
		</div>
 | 
			
		||||
@@ -17,29 +14,34 @@
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<mk-instance-stats style="margin-top: var(--margin);"/>
 | 
			
		||||
	<div class="_section">
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<MkInstanceStats/>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { version } from '../config';
 | 
			
		||||
import MkInstanceStats from '../components/instance-stats.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: this.$t('instance') as string
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
import { version } from '@/config';
 | 
			
		||||
import MkInstanceStats from '@/components/instance-stats.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkInstanceStats
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('about'),
 | 
			
		||||
					icon: faInfoCircle
 | 
			
		||||
				}]
 | 
			
		||||
			},
 | 
			
		||||
			version,
 | 
			
		||||
			serverInfo: null,
 | 
			
		||||
			faInfoCircle
 | 
			
		||||
 
 | 
			
		||||
@@ -1,36 +1,28 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
	<portal to="icon"><fa :icon="faBroadcastTower"/></portal>
 | 
			
		||||
	<portal to="title">{{ $t('announcements') }}</portal>
 | 
			
		||||
 | 
			
		||||
	<mk-pagination :pagination="pagination" #default="{items}" class="ruryvtyk" ref="list">
 | 
			
		||||
<div class="_section">
 | 
			
		||||
	<MkPagination :pagination="pagination" #default="{items}" class="ruryvtyk _content" ref="list">
 | 
			
		||||
		<section class="_card announcement" v-for="(announcement, i) in items" :key="announcement.id">
 | 
			
		||||
			<div class="_title"><span v-if="$store.getters.isSignedIn && !announcement.isRead">🆕 </span>{{ announcement.title }}</div>
 | 
			
		||||
			<div class="_content">
 | 
			
		||||
				<mfm :text="announcement.text"/>
 | 
			
		||||
				<Mfm :text="announcement.text"/>
 | 
			
		||||
				<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="_footer" v-if="$store.getters.isSignedIn && !announcement.isRead">
 | 
			
		||||
				<mk-button @click="read(items, announcement, i)" primary><fa :icon="faCheck"/> {{ $t('gotIt') }}</mk-button>
 | 
			
		||||
				<MkButton @click="read(items, announcement, i)" primary><Fa :icon="faCheck"/> {{ $t('gotIt') }}</MkButton>
 | 
			
		||||
			</div>
 | 
			
		||||
		</section>
 | 
			
		||||
	</mk-pagination>
 | 
			
		||||
	</MkPagination>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faCheck, faBroadcastTower } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import MkPagination from '../components/ui/pagination.vue';
 | 
			
		||||
import MkButton from '../components/ui/button.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: this.$t('announcements') as string
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
import MkPagination from '@/components/ui/pagination.vue';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkPagination,
 | 
			
		||||
		MkButton
 | 
			
		||||
@@ -38,22 +30,28 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('announcements'),
 | 
			
		||||
					icon: faBroadcastTower
 | 
			
		||||
				}]
 | 
			
		||||
			},
 | 
			
		||||
			pagination: {
 | 
			
		||||
				endpoint: 'announcements',
 | 
			
		||||
				limit: 10,
 | 
			
		||||
			},
 | 
			
		||||
			faCheck, faBroadcastTower
 | 
			
		||||
			faCheck,
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		// TODO: これは実質的に親コンポーネントから子コンポーネントのプロパティを変更してるのでなんとかしたい
 | 
			
		||||
		read(items, announcement, i) {
 | 
			
		||||
			Vue.set(items, i, {
 | 
			
		||||
			items[i] = {
 | 
			
		||||
				...announcement,
 | 
			
		||||
				isRead: true,
 | 
			
		||||
			});
 | 
			
		||||
			this.$root.api('i/read-announcement', { announcementId: announcement.id });
 | 
			
		||||
			};
 | 
			
		||||
			os.api('i/read-announcement', { announcementId: announcement.id });
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
	<portal to="icon"><fa :icon="faPlug"/></portal>
 | 
			
		||||
	<portal to="title">{{ $t('installedApps') }}</portal>
 | 
			
		||||
 | 
			
		||||
	<mk-pagination :pagination="pagination" class="bfomjevm" ref="list">
 | 
			
		||||
	<MkPagination :pagination="pagination" class="bfomjevm" ref="list">
 | 
			
		||||
		<template #empty>
 | 
			
		||||
			<div class="_fullinfo">
 | 
			
		||||
				<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
 | 
			
		||||
@@ -18,14 +15,14 @@
 | 
			
		||||
					<div class="description">{{ token.description }}</div>
 | 
			
		||||
					<div class="_keyValue">
 | 
			
		||||
						<div>{{ $t('installedDate') }}:</div>
 | 
			
		||||
						<div><mk-time :time="token.createdAt"/></div>
 | 
			
		||||
						<div><MkTime :time="token.createdAt"/></div>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="_keyValue">
 | 
			
		||||
						<div>{{ $t('lastUsedDate') }}:</div>
 | 
			
		||||
						<div><mk-time :time="token.lastUsedAt"/></div>
 | 
			
		||||
						<div><MkTime :time="token.lastUsedAt"/></div>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="actions">
 | 
			
		||||
						<button class="_button" @click="revoke(token)"><fa :icon="faTrashAlt"/></button>
 | 
			
		||||
						<button class="_button" @click="revoke(token)"><Fa :icon="faTrashAlt"/></button>
 | 
			
		||||
					</div>
 | 
			
		||||
					<details>
 | 
			
		||||
						<summary>{{ $t('details') }}</summary>
 | 
			
		||||
@@ -36,28 +33,29 @@
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</template>
 | 
			
		||||
	</mk-pagination>
 | 
			
		||||
	</MkPagination>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faTrashAlt, faPlug } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import MkPagination from '../components/ui/pagination.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: this.$t('installedApps') as string
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
import MkPagination from '@/components/ui/pagination.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkPagination
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('installedApps'),
 | 
			
		||||
					icon: faPlug,
 | 
			
		||||
				}],
 | 
			
		||||
			},
 | 
			
		||||
			pagination: {
 | 
			
		||||
				endpoint: 'i/apps',
 | 
			
		||||
				limit: 100,
 | 
			
		||||
@@ -71,7 +69,7 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		revoke(token) {
 | 
			
		||||
			this.$root.api('i/revoke-token', { tokenId: token.id }).then(() => {
 | 
			
		||||
			os.api('i/revoke-token', { tokenId: token.id }).then(() => {
 | 
			
		||||
				this.$refs.list.reload();
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
<section class="_card">
 | 
			
		||||
<section class="_section">
 | 
			
		||||
	<div class="_title">{{ $t('_auth.shareAccess', { name: app.name }) }}</div>
 | 
			
		||||
	<div class="_content">
 | 
			
		||||
		<h2>{{ app.name }}</h2>
 | 
			
		||||
@@ -9,23 +9,22 @@
 | 
			
		||||
	<div class="_content">
 | 
			
		||||
		<h2>{{ $t('_auth.permissionAsk') }}</h2>
 | 
			
		||||
		<ul>
 | 
			
		||||
			<template v-for="p in app.permission">
 | 
			
		||||
				<li :key="p">{{ $t(`_permissions.${p}`) }}</li>
 | 
			
		||||
			</template>
 | 
			
		||||
			<li v-for="p in app.permission" :key="p">{{ $t(`_permissions.${p}`) }}</li>
 | 
			
		||||
		</ul>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="_footer">
 | 
			
		||||
		<mk-button @click="cancel" inline>{{ $t('cancel') }}</mk-button>
 | 
			
		||||
		<mk-button @click="accept" inline primary>{{ $t('accept') }}</mk-button>
 | 
			
		||||
		<MkButton @click="cancel" inline>{{ $t('cancel') }}</MkButton>
 | 
			
		||||
		<MkButton @click="accept" inline primary>{{ $t('accept') }}</MkButton>
 | 
			
		||||
	</div>
 | 
			
		||||
</section>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import MkButton from '../components/ui/button.vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkButton
 | 
			
		||||
	},
 | 
			
		||||
@@ -42,7 +41,7 @@ export default Vue.extend({
 | 
			
		||||
	},
 | 
			
		||||
	methods: {
 | 
			
		||||
		cancel() {
 | 
			
		||||
			this.$root.api('auth/deny', {
 | 
			
		||||
			os.api('auth/deny', {
 | 
			
		||||
				token: this.session.token
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				this.$emit('denied');
 | 
			
		||||
@@ -50,7 +49,7 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		accept() {
 | 
			
		||||
			this.$root.api('auth/accept', {
 | 
			
		||||
			os.api('auth/accept', {
 | 
			
		||||
				token: this.session.token
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				this.$emit('accepted');
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="_panel" v-if="$store.getters.isSignedIn && fetching">
 | 
			
		||||
	<mk-loading/>
 | 
			
		||||
<div class="" v-if="$store.getters.isSignedIn && fetching">
 | 
			
		||||
	<MkLoading/>
 | 
			
		||||
</div>
 | 
			
		||||
<div v-else-if="$store.getters.isSignedIn">
 | 
			
		||||
	<x-form
 | 
			
		||||
	<XForm
 | 
			
		||||
		class="form"
 | 
			
		||||
		ref="form"
 | 
			
		||||
		v-if="state == 'waiting'"
 | 
			
		||||
@@ -11,29 +11,30 @@
 | 
			
		||||
		@denied="state = 'denied'"
 | 
			
		||||
		@accepted="accepted"
 | 
			
		||||
	/>
 | 
			
		||||
	<div class="denied _panel" v-if="state == 'denied'">
 | 
			
		||||
	<div class="denied" v-if="state == 'denied'">
 | 
			
		||||
		<h1>{{ $t('_auth.denied') }}</h1>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="accepted _panel" v-if="state == 'accepted'">
 | 
			
		||||
	<div class="accepted" v-if="state == 'accepted'">
 | 
			
		||||
		<h1>{{ session.app.isAuthorized ? this.$t('already-authorized') : this.$t('allowed') }}</h1>
 | 
			
		||||
		<p v-if="session.app.callbackUrl">{{ $t('_auth.callback') }}<mk-ellipsis/></p>
 | 
			
		||||
		<p v-if="session.app.callbackUrl">{{ $t('_auth.callback') }}<MkEllipsis/></p>
 | 
			
		||||
		<p v-if="!session.app.callbackUrl">{{ $t('_auth.pleaseGoBack') }}</p>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="error _panel" v-if="state == 'fetch-session-error'">
 | 
			
		||||
		<p>{{ $t('error') }}</p>
 | 
			
		||||
	<div class="error" v-if="state == 'fetch-session-error'">
 | 
			
		||||
		<p>{{ $t('somethingHappened') }}</p>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
<div class="signin" v-else>
 | 
			
		||||
	<mk-signin @login="onLogin"/>
 | 
			
		||||
	<MkSignin @login="onLogin"/>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import XForm from './auth.form.vue';
 | 
			
		||||
import MkSignin from '../components/signin.vue';
 | 
			
		||||
import MkSignin from '@/components/signin.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XForm,
 | 
			
		||||
		MkSignin,
 | 
			
		||||
@@ -54,7 +55,7 @@ export default Vue.extend({
 | 
			
		||||
		if (!this.$store.getters.isSignedIn) return;
 | 
			
		||||
 | 
			
		||||
		// Fetch session
 | 
			
		||||
		this.$root.api('auth/session/show', {
 | 
			
		||||
		os.api('auth/session/show', {
 | 
			
		||||
			token: this.token
 | 
			
		||||
		}).then(session => {
 | 
			
		||||
			this.session = session;
 | 
			
		||||
@@ -62,7 +63,7 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
			// 既に連携していた場合
 | 
			
		||||
			if (this.session.app.isAuthorized) {
 | 
			
		||||
				this.$root.api('auth/accept', {
 | 
			
		||||
				os.api('auth/accept', {
 | 
			
		||||
					token: this.session.token
 | 
			
		||||
				}).then(() => {
 | 
			
		||||
					this.accepted();
 | 
			
		||||
 
 | 
			
		||||
@@ -1,39 +1,37 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
	<portal to="icon"><fa :icon="faSatelliteDish"/></portal>
 | 
			
		||||
	<portal to="title">{{ channelId ? $t('_channel.edit') : $t('_channel.create') }}</portal>
 | 
			
		||||
 | 
			
		||||
	<div class="_card">
 | 
			
		||||
	<div class="_section">
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-input v-model="name">{{ $t('name') }}</mk-input>
 | 
			
		||||
			<MkInput v-model:value="name">{{ $t('name') }}</MkInput>
 | 
			
		||||
 | 
			
		||||
			<mk-textarea v-model="description">{{ $t('description') }}</mk-textarea>
 | 
			
		||||
			<MkTextarea v-model:value="description">{{ $t('description') }}</MkTextarea>
 | 
			
		||||
 | 
			
		||||
			<div class="banner">
 | 
			
		||||
				<mk-button v-if="bannerId == null" @click="setBannerImage"><fa :icon="faPlus"/> {{ $t('_channel.setBanner') }}</mk-button>
 | 
			
		||||
				<MkButton v-if="bannerId == null" @click="setBannerImage"><Fa :icon="faPlus"/> {{ $t('_channel.setBanner') }}</MkButton>
 | 
			
		||||
				<div v-else-if="bannerUrl">
 | 
			
		||||
					<img :src="bannerUrl" style="width: 100%;"/>
 | 
			
		||||
					<mk-button @click="removeBannerImage()"><fa :icon="faTrashAlt"/> {{ $t('_channel.removeBanner') }}</mk-button>
 | 
			
		||||
					<MkButton @click="removeBannerImage()"><Fa :icon="faTrashAlt"/> {{ $t('_channel.removeBanner') }}</MkButton>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_footer">
 | 
			
		||||
			<mk-button @click="save()" primary><fa :icon="faSave"/> {{ channelId ? $t('save') : $t('create') }}</mk-button>
 | 
			
		||||
			<MkButton @click="save()" primary><Fa :icon="faSave"/> {{ channelId ? $t('save') : $t('create') }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { computed, defineComponent } from 'vue';
 | 
			
		||||
import { faPlus, faSatelliteDish } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faSave, faTrashAlt } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import MkTextarea from '../components/ui/textarea.vue';
 | 
			
		||||
import MkButton from '../components/ui/button.vue';
 | 
			
		||||
import MkInput from '../components/ui/input.vue';
 | 
			
		||||
import { selectFile } from '../scripts/select-file';
 | 
			
		||||
import MkTextarea from '@/components/ui/textarea.vue';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import MkInput from '@/components/ui/input.vue';
 | 
			
		||||
import { selectFile } from '@/scripts/select-file';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkTextarea, MkButton, MkInput,
 | 
			
		||||
	},
 | 
			
		||||
@@ -47,6 +45,17 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: computed(() => this.channelId ? {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('_channel.edit'),
 | 
			
		||||
					icon: faSatelliteDish,
 | 
			
		||||
				}],
 | 
			
		||||
			} : {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('_channel.create'),
 | 
			
		||||
					icon: faSatelliteDish,
 | 
			
		||||
				}],
 | 
			
		||||
			}),
 | 
			
		||||
			channel: null,
 | 
			
		||||
			name: null,
 | 
			
		||||
			description: null,
 | 
			
		||||
@@ -61,7 +70,7 @@ export default Vue.extend({
 | 
			
		||||
			if (this.bannerId == null) {
 | 
			
		||||
				this.bannerUrl = null;
 | 
			
		||||
			} else {
 | 
			
		||||
				this.bannerUrl = (await this.$root.api('drive/files/show', {
 | 
			
		||||
				this.bannerUrl = (await os.api('drive/files/show', {
 | 
			
		||||
					fileId: this.bannerId,
 | 
			
		||||
				})).url;
 | 
			
		||||
			}
 | 
			
		||||
@@ -70,7 +79,7 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
	async created() {
 | 
			
		||||
		if (this.channelId) {
 | 
			
		||||
			this.channel = await this.$root.api('channels/show', {
 | 
			
		||||
			this.channel = await os.api('channels/show', {
 | 
			
		||||
				channelId: this.channelId,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
@@ -91,27 +100,21 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
			if (this.channelId) {
 | 
			
		||||
				params.channelId = this.channelId;
 | 
			
		||||
				this.$root.api('channels/update', params)
 | 
			
		||||
				os.api('channels/update', params)
 | 
			
		||||
				.then(channel => {
 | 
			
		||||
					this.$root.dialog({
 | 
			
		||||
						type: 'success',
 | 
			
		||||
						iconOnly: true, autoClose: true
 | 
			
		||||
					});
 | 
			
		||||
					os.success();
 | 
			
		||||
				});
 | 
			
		||||
			} else {
 | 
			
		||||
				this.$root.api('channels/create', params)
 | 
			
		||||
				os.api('channels/create', params)
 | 
			
		||||
				.then(channel => {
 | 
			
		||||
					this.$root.dialog({
 | 
			
		||||
						type: 'success',
 | 
			
		||||
						iconOnly: true, autoClose: true
 | 
			
		||||
					});
 | 
			
		||||
					os.success();
 | 
			
		||||
					this.$router.push(`/channels/${channel.id}`);
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		setBannerImage(e) {
 | 
			
		||||
			selectFile(this, e.currentTarget || e.target, null, false).then(file => {
 | 
			
		||||
			selectFile(e.currentTarget || e.target, null, false).then(file => {
 | 
			
		||||
				this.bannerId = file.id;
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 
 | 
			
		||||
@@ -1,50 +1,42 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div v-if="channel">
 | 
			
		||||
	<portal to="icon"><fa :icon="faSatelliteDish"/></portal>
 | 
			
		||||
	<portal to="title">{{ channel.name }}</portal>
 | 
			
		||||
 | 
			
		||||
	<div class="wpgynlbz _panel _vMargin" :class="{ hide: !showBanner }">
 | 
			
		||||
		<x-channel-follow-button :channel="channel" :full="true" class="subscribe"/>
 | 
			
		||||
		<XChannelFollow-button :channel="channel" :full="true" class="subscribe"/>
 | 
			
		||||
		<button class="_button toggle" @click="() => showBanner = !showBanner">
 | 
			
		||||
			<template v-if="showBanner"><fa :icon="faAngleUp"/></template>
 | 
			
		||||
			<template v-else><fa :icon="faAngleDown"/></template>
 | 
			
		||||
			<template v-if="showBanner"><Fa :icon="faAngleUp"/></template>
 | 
			
		||||
			<template v-else><Fa :icon="faAngleDown"/></template>
 | 
			
		||||
		</button>
 | 
			
		||||
		<div class="hideOverlay" v-if="!showBanner">
 | 
			
		||||
		</div>
 | 
			
		||||
		<div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : null }" class="banner">
 | 
			
		||||
			<div class="status">
 | 
			
		||||
				<div><fa :icon="faUsers" fixed-width/><i18n path="_channel.usersCount" tag="span" style="margin-left: 4px;"><b place="n">{{ channel.usersCount }}</b></i18n></div>
 | 
			
		||||
				<div><fa :icon="faPencilAlt" fixed-width/><i18n path="_channel.notesCount" tag="span" style="margin-left: 4px;"><b place="n">{{ channel.notesCount }}</b></i18n></div>
 | 
			
		||||
				<div><Fa :icon="faUsers" fixed-width/><i18n path="_channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></i18n></div>
 | 
			
		||||
				<div><Fa :icon="faPencilAlt" fixed-width/><i18n path="_channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></i18n></div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="fade"></div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="description" v-if="channel.description">
 | 
			
		||||
			<mfm :text="channel.description" :is-note="false" :i="$store.state.i"/>
 | 
			
		||||
			<Mfm :text="channel.description" :is-note="false" :i="$store.state.i"/>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<x-post-form :channel="channel" class="post-form _panel _vMargin" fixed/>
 | 
			
		||||
	<XPostForm :channel="channel" class="post-form _panel _vMargin" fixed/>
 | 
			
		||||
 | 
			
		||||
	<x-timeline class="_vMargin" src="channel" :channel="channelId" @before="before" @after="after"/>
 | 
			
		||||
	<XTimeline class="_vMargin" src="channel" :channel="channelId" @before="before" @after="after"/>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { computed, defineComponent } from 'vue';
 | 
			
		||||
import { faSatelliteDish, faUsers, faPencilAlt, faAngleUp, faAngleDown } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import {  } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import MkContainer from '../components/ui/container.vue';
 | 
			
		||||
import XPostForm from '../components/post-form.vue';
 | 
			
		||||
import XTimeline from '../components/timeline.vue';
 | 
			
		||||
import XChannelFollowButton from '../components/channel-follow-button.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: this.$t('channel') as string
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
import MkContainer from '@/components/ui/container.vue';
 | 
			
		||||
import XPostForm from '@/components/post-form.vue';
 | 
			
		||||
import XTimeline from '@/components/timeline.vue';
 | 
			
		||||
import XChannelFollowButton from '@/components/channel-follow-button.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkContainer,
 | 
			
		||||
		XPostForm,
 | 
			
		||||
@@ -61,6 +53,12 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: computed(() => this.channel ? {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.channel.name,
 | 
			
		||||
					icon: faSatelliteDish,
 | 
			
		||||
				}],
 | 
			
		||||
			} : null),
 | 
			
		||||
			channel: null,
 | 
			
		||||
			showBanner: true,
 | 
			
		||||
			pagination: {
 | 
			
		||||
@@ -77,7 +75,7 @@ export default Vue.extend({
 | 
			
		||||
	watch: {
 | 
			
		||||
		channelId: {
 | 
			
		||||
			async handler() {
 | 
			
		||||
				this.channel = await this.$root.api('channels/show', {
 | 
			
		||||
				this.channel = await os.api('channels/show', {
 | 
			
		||||
					channelId: this.channelId,
 | 
			
		||||
				});
 | 
			
		||||
			},
 | 
			
		||||
 
 | 
			
		||||
@@ -1,46 +1,53 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
	<portal to="icon"><fa :icon="faSatelliteDish"/></portal>
 | 
			
		||||
	<portal to="title">{{ $t('channel') }}</portal>
 | 
			
		||||
 | 
			
		||||
	<mk-tab v-model="tab" :items="[{ label: $t('_channel.featured'), value: 'featured', icon: faFireAlt }, { label: $t('_channel.following'), value: 'following', icon: faHeart }, { label: $t('_channel.owned'), value: 'owned', icon: faEdit }]"/>
 | 
			
		||||
 | 
			
		||||
	<div class="grwlizim featured" v-if="tab === 'featured'">
 | 
			
		||||
		<mk-pagination :pagination="featuredPagination" #default="{items}">
 | 
			
		||||
			<mk-channel-preview v-for="channel in items" class="uveselbe" :channel="channel" :key="channel.id"/>
 | 
			
		||||
		</mk-pagination>
 | 
			
		||||
	<div class="_section" style="padding: 0;">
 | 
			
		||||
		<MkTab class="_content" v-model:value="tab" :items="[{ label: $t('_channel.featured'), value: 'featured', icon: faFireAlt }, { label: $t('_channel.following'), value: 'following', icon: faHeart }, { label: $t('_channel.owned'), value: 'owned', icon: faEdit }]"/>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<div class="grwlizim following" v-if="tab === 'following'">
 | 
			
		||||
		<mk-pagination :pagination="followingPagination" #default="{items}">
 | 
			
		||||
			<mk-channel-preview v-for="channel in items" class="uveselbe" :channel="channel" :key="channel.id"/>
 | 
			
		||||
		</mk-pagination>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="_section">
 | 
			
		||||
		<div class="_content grwlizim featured" v-if="tab === 'featured'">
 | 
			
		||||
			<MkPagination :pagination="featuredPagination" #default="{items}">
 | 
			
		||||
				<MkChannelPreview v-for="channel in items" class="uveselbe" :channel="channel" :key="channel.id"/>
 | 
			
		||||
			</MkPagination>
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
	<div class="grwlizim owned" v-if="tab === 'owned'">
 | 
			
		||||
		<mk-button class="new" @click="create()"><fa :icon="faPlus"/></mk-button>
 | 
			
		||||
		<mk-pagination :pagination="ownedPagination" #default="{items}">
 | 
			
		||||
			<mk-channel-preview v-for="channel in items" class="uveselbe" :channel="channel" :key="channel.id"/>
 | 
			
		||||
		</mk-pagination>
 | 
			
		||||
		<div class="_content grwlizim following" v-if="tab === 'following'">
 | 
			
		||||
			<MkPagination :pagination="followingPagination" #default="{items}">
 | 
			
		||||
				<MkChannelPreview v-for="channel in items" class="uveselbe" :channel="channel" :key="channel.id"/>
 | 
			
		||||
			</MkPagination>
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
		<div class="_content grwlizim owned" v-if="tab === 'owned'">
 | 
			
		||||
			<MkButton class="new" @click="create()"><Fa :icon="faPlus"/></MkButton>
 | 
			
		||||
			<MkPagination :pagination="ownedPagination" #default="{items}">
 | 
			
		||||
				<MkChannelPreview v-for="channel in items" class="uveselbe" :channel="channel" :key="channel.id"/>
 | 
			
		||||
			</MkPagination>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faSatelliteDish, faPlus, faEdit, faFireAlt } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faHeart } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import MkChannelPreview from '../components/channel-preview.vue';
 | 
			
		||||
import MkPagination from '../components/ui/pagination.vue';
 | 
			
		||||
import MkButton from '../components/ui/button.vue';
 | 
			
		||||
import MkTab from '../components/tab.vue';
 | 
			
		||||
import MkChannelPreview from '@/components/channel-preview.vue';
 | 
			
		||||
import MkPagination from '@/components/ui/pagination.vue';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import MkTab from '@/components/tab.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkChannelPreview, MkPagination, MkButton, MkTab
 | 
			
		||||
	},
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('channel'),
 | 
			
		||||
					icon: faSatelliteDish
 | 
			
		||||
				}]
 | 
			
		||||
			},
 | 
			
		||||
			tab: 'featured',
 | 
			
		||||
			featuredPagination: {
 | 
			
		||||
				endpoint: 'channels/featured',
 | 
			
		||||
 
 | 
			
		||||
@@ -1,26 +1,24 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
	<portal to="icon"><fa :icon="faFileAlt"/></portal>
 | 
			
		||||
	<portal to="title">{{ title }}</portal>
 | 
			
		||||
	<main class="_card">
 | 
			
		||||
		<div class="_title"><fa :icon="faFileAlt"/> {{ title }}</div>
 | 
			
		||||
	<main class="_section">
 | 
			
		||||
		<div class="_title"><Fa :icon="faFileAlt"/> {{ title }}</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<div v-html="body" class="qyqbqfal"></div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_footer">
 | 
			
		||||
			<mk-link :url="`https://github.com/syuilo/misskey/blob/master/src/docs/${doc}.ja-JP.md`" class="at">{{ $t('docSource') }}</mk-link>
 | 
			
		||||
			<MkLink :url="`https://github.com/syuilo/misskey/blob/master/src/docs/${doc}.ja-JP.md`" class="at">{{ $t('docSource') }}</MkLink>
 | 
			
		||||
		</div>
 | 
			
		||||
	</main>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faFileAlt } from '@fortawesome/free-solid-svg-icons'
 | 
			
		||||
import MarkdownIt from 'markdown-it';
 | 
			
		||||
import MarkdownItAnchor from 'markdown-it-anchor';
 | 
			
		||||
import { url, lang } from '../config';
 | 
			
		||||
import MkLink from '../components/link.vue';
 | 
			
		||||
import { url, lang } from '@/config';
 | 
			
		||||
import MkLink from '@/components/link.vue';
 | 
			
		||||
 | 
			
		||||
const markdown = MarkdownIt({
 | 
			
		||||
	html: true
 | 
			
		||||
@@ -30,13 +28,7 @@ markdown.use(MarkdownItAnchor, {
 | 
			
		||||
	slugify: (s) => encodeURIComponent(String(s).trim().replace(/\s+/g, '-'))
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: this.title,
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkLink
 | 
			
		||||
	},
 | 
			
		||||
@@ -48,6 +40,21 @@ export default Vue.extend({
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.title,
 | 
			
		||||
					icon: faFileAlt
 | 
			
		||||
				}],
 | 
			
		||||
			},
 | 
			
		||||
			faFileAlt,
 | 
			
		||||
			title: '',
 | 
			
		||||
			body: '',
 | 
			
		||||
			markdown: '',
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	watch: {
 | 
			
		||||
		doc: {
 | 
			
		||||
			handler() {
 | 
			
		||||
@@ -57,15 +64,6 @@ export default Vue.extend({
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			faFileAlt,
 | 
			
		||||
			title: '',
 | 
			
		||||
			body: '',
 | 
			
		||||
			markdown: '',
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		fetchDoc() {
 | 
			
		||||
			fetch(`${url}/assets/docs/${this.doc}.${lang}.md`).then(res => res.text()).then(md => {
 | 
			
		||||
@@ -120,11 +118,11 @@ export default Vue.extend({
 | 
			
		||||
		margin-bottom: 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	::v-deep a {
 | 
			
		||||
	::v-deep(a) {
 | 
			
		||||
		color: var(--link);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	::v-deep blockquote {
 | 
			
		||||
	::v-deep(blockquote) {
 | 
			
		||||
		display: block;
 | 
			
		||||
		margin: 8px;
 | 
			
		||||
		padding: 6px 0 6px 12px;
 | 
			
		||||
@@ -137,19 +135,19 @@ export default Vue.extend({
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	::v-deep h2 {
 | 
			
		||||
	::v-deep(h2) {
 | 
			
		||||
		font-size: 1.25em;
 | 
			
		||||
		padding: 0 0 0.5em 0;
 | 
			
		||||
		border-bottom: solid 1px var(--divider);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	::v-deep table {
 | 
			
		||||
	::v-deep(table) {
 | 
			
		||||
		width: 100%;
 | 
			
		||||
		max-width: 100%;
 | 
			
		||||
		overflow: auto;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	::v-deep kbd.group {
 | 
			
		||||
	::v-deep(kbd.group) {
 | 
			
		||||
		display: inline-block;
 | 
			
		||||
		padding: 2px;
 | 
			
		||||
		border: 1px solid var(--divider);
 | 
			
		||||
@@ -157,7 +155,7 @@ export default Vue.extend({
 | 
			
		||||
		box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	::v-deep kbd.key {
 | 
			
		||||
	::v-deep(kbd.key) {
 | 
			
		||||
		display: inline-block;
 | 
			
		||||
		padding: 6px 8px;
 | 
			
		||||
		border: solid 1px var(--divider);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
	<portal to="icon"><fa :icon="faQuestionCircle"/></portal>
 | 
			
		||||
	<portal to="title">{{ $t('help') }}</portal>
 | 
			
		||||
	<main class="_card">
 | 
			
		||||
	<main class="_section">
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<ul>
 | 
			
		||||
				<li v-for="doc in docs" :key="doc.path">
 | 
			
		||||
@@ -15,19 +13,19 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faQuestionCircle } from '@fortawesome/free-solid-svg-icons'
 | 
			
		||||
import { url, lang } from '../config';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: this.$t('help') as string,
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
import { url, lang } from '@/config';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('help'),
 | 
			
		||||
					icon: faQuestionCircle
 | 
			
		||||
				}],
 | 
			
		||||
			},
 | 
			
		||||
			docs: [],
 | 
			
		||||
			faQuestionCircle
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,87 +1,40 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="full">
 | 
			
		||||
	<portal to="header">
 | 
			
		||||
		<button @click="menu" class="_button _jmoebdiw_">
 | 
			
		||||
			<fa :icon="faCloud" style="margin-right: 8px;"/>
 | 
			
		||||
			<span v-if="folder">{{ $t('drive') }} ({{ folder.name }})</span>
 | 
			
		||||
			<span v-else>{{ $t('drive') }}</span>
 | 
			
		||||
			<fa :icon="menuOpened ? faAngleUp : faAngleDown" style="margin-left: 8px;"/>
 | 
			
		||||
		</button>
 | 
			
		||||
	</portal>
 | 
			
		||||
	<x-drive ref="drive" @cd="x => folder = x"/>
 | 
			
		||||
<div>
 | 
			
		||||
	<XDrive ref="drive" @cd="x => folder = x"/>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { faCloud, faAngleDown, faAngleUp, faFolderPlus, faUpload, faLink, faICursor, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import XDrive from '../components/drive.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: this.$t('drive') as string
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
import { computed, defineComponent } from 'vue';
 | 
			
		||||
import { faCloud, faEllipsisH } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import XDrive from '@/components/drive.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XDrive
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			menuOpened: false,
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: computed(() => this.folder ? this.folder.name : this.$t('drive')),
 | 
			
		||||
					icon: faCloud,
 | 
			
		||||
				}],
 | 
			
		||||
				action: {
 | 
			
		||||
					icon: faEllipsisH,
 | 
			
		||||
					handler: this.menu
 | 
			
		||||
				}
 | 
			
		||||
			},
 | 
			
		||||
			folder: null,
 | 
			
		||||
			faCloud, faAngleDown, faAngleUp
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		menu(ev) {
 | 
			
		||||
			this.menuOpened = true;
 | 
			
		||||
			this.$root.menu({
 | 
			
		||||
				items: [{
 | 
			
		||||
					text: this.$t('addFile'),
 | 
			
		||||
					type: 'label'
 | 
			
		||||
				}, {
 | 
			
		||||
					text: this.$t('upload'),
 | 
			
		||||
					icon: faUpload,
 | 
			
		||||
					action: () => { this.$refs.drive.selectLocalFile(); }
 | 
			
		||||
				}, {
 | 
			
		||||
					text: this.$t('fromUrl'),
 | 
			
		||||
					icon: faLink,
 | 
			
		||||
					action: () => { this.$refs.drive.urlUpload(); }
 | 
			
		||||
				}, null, {
 | 
			
		||||
					text: this.folder ? this.folder.name : this.$t('drive'),
 | 
			
		||||
					type: 'label'
 | 
			
		||||
				}, this.folder ? {
 | 
			
		||||
					text: this.$t('renameFolder'),
 | 
			
		||||
					icon: faICursor,
 | 
			
		||||
					action: () => { this.$refs.drive.renameFolder(this.folder); }
 | 
			
		||||
				} : undefined, this.folder ? {
 | 
			
		||||
					text: this.$t('deleteFolder'),
 | 
			
		||||
					icon: faTrashAlt,
 | 
			
		||||
					action: () => { this.$refs.drive.deleteFolder(this.folder); }
 | 
			
		||||
				} : undefined, {
 | 
			
		||||
					text: this.$t('createFolder'),
 | 
			
		||||
					icon: faFolderPlus,
 | 
			
		||||
					action: () => { this.$refs.drive.createFolder(); }
 | 
			
		||||
				}],
 | 
			
		||||
				fixed: true,
 | 
			
		||||
				noCenter: true,
 | 
			
		||||
				source: ev.currentTarget || ev.target
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				this.menuOpened = false;
 | 
			
		||||
			});
 | 
			
		||||
			os.modalMenu(this.$refs.drive.getMenu(), ev.currentTarget || ev.target);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
._jmoebdiw_ {
 | 
			
		||||
	height: 100%;
 | 
			
		||||
	padding: 0 16px;
 | 
			
		||||
	font-weight: bold;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,75 +1,86 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
	<portal to="icon"><fa :icon="faHashtag"/></portal>
 | 
			
		||||
	<portal to="title">{{ $t('explore') }}</portal>
 | 
			
		||||
	<div class="_section">
 | 
			
		||||
		<MkInput v-model:value="query" :debounce="true" type="search"><template #icon><Fa :icon="faSearch"/></template><span>{{ $t('searchUser') }}</span></MkInput>
 | 
			
		||||
 | 
			
		||||
	<div class="localfedi7 _panel" v-if="meta && stats && tag == null" :style="{ backgroundImage: meta.bannerUrl ? `url(${meta.bannerUrl})` : null }">
 | 
			
		||||
		<header><span>{{ $t('explore', { host: meta.name || 'Misskey' }) }}</span></header>
 | 
			
		||||
		<div><span>{{ $t('exploreUsersCount', { count: num(stats.originalUsersCount) }) }}</span></div>
 | 
			
		||||
	</div>
 | 
			
		||||
		<XUserList v-if="query" class="_vMargin" :pagination="searchPagination" ref="search"/>
 | 
			
		||||
 | 
			
		||||
	<template v-if="tag == null">
 | 
			
		||||
		<x-user-list :pagination="pinnedUsers" :expanded="false">
 | 
			
		||||
			<fa :icon="faBookmark" fixed-width/>{{ $t('pinnedUsers') }}
 | 
			
		||||
		</x-user-list>
 | 
			
		||||
		<x-user-list :pagination="popularUsers" :expanded="false">
 | 
			
		||||
			<fa :icon="faChartLine" fixed-width/>{{ $t('popularUsers') }}
 | 
			
		||||
		</x-user-list>
 | 
			
		||||
		<x-user-list :pagination="recentlyUpdatedUsers" :expanded="false">
 | 
			
		||||
			<fa :icon="faCommentAlt" fixed-width/>{{ $t('recentlyUpdatedUsers') }}
 | 
			
		||||
		</x-user-list>
 | 
			
		||||
		<x-user-list :pagination="recentlyRegisteredUsers" :expanded="false">
 | 
			
		||||
			<fa :icon="faPlus" fixed-width/>{{ $t('recentlyRegisteredUsers') }}
 | 
			
		||||
		</x-user-list>
 | 
			
		||||
	</template>
 | 
			
		||||
 | 
			
		||||
	<div class="localfedi7 _panel" v-if="tag == null" :style="{ backgroundImage: `url(/assets/fedi.jpg)`, marginTop: 'var(--margin)' }">
 | 
			
		||||
		<header><span>{{ $t('exploreFediverse') }}</span></header>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<mk-container :body-togglable="true" :expanded="false" ref="tags">
 | 
			
		||||
		<template #header><fa :icon="faHashtag" fixed-width/>{{ $t('popularTags') }}</template>
 | 
			
		||||
 | 
			
		||||
		<div class="vxjfqztj">
 | 
			
		||||
			<router-link v-for="tag in tagsLocal" :to="`/explore/tags/${tag.tag}`" :key="'local:' + tag.tag" class="local">{{ tag.tag }}</router-link>
 | 
			
		||||
			<router-link v-for="tag in tagsRemote" :to="`/explore/tags/${tag.tag}`" :key="'remote:' + tag.tag">{{ tag.tag }}</router-link>
 | 
			
		||||
		<div class="localfedi7 _panel _vMargin" v-if="meta && stats && tag == null" :style="{ backgroundImage: meta.bannerUrl ? `url(${meta.bannerUrl})` : null }">
 | 
			
		||||
			<header><span>{{ $t('explore', { host: meta.name || 'Misskey' }) }}</span></header>
 | 
			
		||||
			<div><span>{{ $t('exploreUsersCount', { count: num(stats.originalUsersCount) }) }}</span></div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</mk-container>
 | 
			
		||||
 | 
			
		||||
	<x-user-list v-if="tag != null" :pagination="tagUsers" :key="`${tag}`">
 | 
			
		||||
		<fa :icon="faHashtag" fixed-width/>{{ tag }}
 | 
			
		||||
	</x-user-list>
 | 
			
		||||
	<template v-if="tag == null">
 | 
			
		||||
		<x-user-list :pagination="popularUsersF" :expanded="false">
 | 
			
		||||
			<fa :icon="faChartLine" fixed-width/>{{ $t('popularUsers') }}
 | 
			
		||||
		</x-user-list>
 | 
			
		||||
		<x-user-list :pagination="recentlyUpdatedUsersF" :expanded="false">
 | 
			
		||||
			<fa :icon="faCommentAlt" fixed-width/>{{ $t('recentlyUpdatedUsers') }}
 | 
			
		||||
		</x-user-list>
 | 
			
		||||
		<x-user-list :pagination="recentlyRegisteredUsersF" :expanded="false">
 | 
			
		||||
			<fa :icon="faRocket" fixed-width/>{{ $t('recentlyDiscoveredUsers') }}
 | 
			
		||||
		</x-user-list>
 | 
			
		||||
	</template>
 | 
			
		||||
		<template v-if="tag == null">
 | 
			
		||||
			<MkFolder class="_vMargin" persist-key="explore-pinned-users">
 | 
			
		||||
				<template #header><Fa :icon="faBookmark" fixed-width style="margin-right: 0.5em;"/>{{ $t('pinnedUsers') }}</template>
 | 
			
		||||
				<XUserList :pagination="pinnedUsers"/>
 | 
			
		||||
			</MkFolder>
 | 
			
		||||
			<MkFolder class="_vMargin" persist-key="explore-popular-users">
 | 
			
		||||
				<template #header><Fa :icon="faChartLine" fixed-width style="margin-right: 0.5em;"/>{{ $t('popularUsers') }}</template>
 | 
			
		||||
				<XUserList :pagination="popularUsers"/>
 | 
			
		||||
			</MkFolder>
 | 
			
		||||
			<MkFolder class="_vMargin" persist-key="explore-recently-updated-users">
 | 
			
		||||
				<template #header><Fa :icon="faCommentAlt" fixed-width style="margin-right: 0.5em;"/>{{ $t('recentlyUpdatedUsers') }}</template>
 | 
			
		||||
				<XUserList :pagination="recentlyUpdatedUsers"/>
 | 
			
		||||
			</MkFolder>
 | 
			
		||||
			<MkFolder class="_vMargin" persist-key="explore-recently-registered-users">
 | 
			
		||||
				<template #header><Fa :icon="faPlus" fixed-width style="margin-right: 0.5em;"/>{{ $t('recentlyRegisteredUsers') }}</template>
 | 
			
		||||
				<XUserList :pagination="recentlyRegisteredUsers"/>
 | 
			
		||||
			</MkFolder>
 | 
			
		||||
		</template>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="_section">
 | 
			
		||||
		<div class="localfedi7 _panel _vMargin" v-if="tag == null" :style="{ backgroundImage: `url(/assets/fedi.jpg)` }">
 | 
			
		||||
			<header><span>{{ $t('exploreFediverse') }}</span></header>
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
		<MkFolder :body-togglable="true" :expanded="false" ref="tags" class="_vMargin">
 | 
			
		||||
			<template #header><Fa :icon="faHashtag" fixed-width style="margin-right: 0.5em;"/>{{ $t('popularTags') }}</template>
 | 
			
		||||
 | 
			
		||||
			<div class="vxjfqztj">
 | 
			
		||||
				<router-link v-for="tag in tagsLocal" :to="`/explore/tags/${tag.tag}`" :key="'local:' + tag.tag" class="local">{{ tag.tag }}</router-link>
 | 
			
		||||
				<router-link v-for="tag in tagsRemote" :to="`/explore/tags/${tag.tag}`" :key="'remote:' + tag.tag">{{ tag.tag }}</router-link>
 | 
			
		||||
			</div>
 | 
			
		||||
		</MkFolder>
 | 
			
		||||
 | 
			
		||||
		<MkFolder v-if="tag != null" :key="`${tag}`" class="_vMargin">
 | 
			
		||||
			<template #header><Fa :icon="faHashtag" fixed-width style="margin-right: 0.5em;"/>{{ tag }}</template>
 | 
			
		||||
			<XUserList :pagination="tagUsers"/>
 | 
			
		||||
		</MkFolder>
 | 
			
		||||
 | 
			
		||||
		<template v-if="tag == null">
 | 
			
		||||
			<MkFolder class="_vMargin">
 | 
			
		||||
				<template #header><Fa :icon="faChartLine" fixed-width style="margin-right: 0.5em;"/>{{ $t('popularUsers') }}</template>
 | 
			
		||||
				<XUserList :pagination="popularUsersF"/>
 | 
			
		||||
			</MkFolder>
 | 
			
		||||
			<MkFolder class="_vMargin">
 | 
			
		||||
				<template #header><Fa :icon="faCommentAlt" fixed-width style="margin-right: 0.5em;"/>{{ $t('recentlyUpdatedUsers') }}</template>
 | 
			
		||||
				<XUserList :pagination="recentlyUpdatedUsersF"/>
 | 
			
		||||
			</MkFolder>
 | 
			
		||||
			<MkFolder class="_vMargin">
 | 
			
		||||
				<template #header><Fa :icon="faRocket" fixed-width style="margin-right: 0.5em;"/>{{ $t('recentlyDiscoveredUsers') }}</template>
 | 
			
		||||
				<XUserList :pagination="recentlyRegisteredUsersF"/>
 | 
			
		||||
			</MkFolder>
 | 
			
		||||
		</template>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { faChartLine, faPlus, faHashtag, faRocket } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { computed, defineComponent } from 'vue';
 | 
			
		||||
import { faChartLine, faPlus, faHashtag, faRocket, faSearch } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faBookmark, faCommentAlt } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import XUserList from '../components/user-list.vue';
 | 
			
		||||
import MkContainer from '../components/ui/container.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: this.$t('explore') as string
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
import XUserList from '@/components/user-list.vue';
 | 
			
		||||
import MkFolder from '@/components/ui/folder.vue';
 | 
			
		||||
import MkInput from '@/components/ui/input.vue';
 | 
			
		||||
import number from '@/filters/number';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XUserList,
 | 
			
		||||
		MkContainer,
 | 
			
		||||
		MkFolder,
 | 
			
		||||
		MkInput,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	props: {
 | 
			
		||||
@@ -81,6 +92,12 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('explore'),
 | 
			
		||||
					icon: faHashtag
 | 
			
		||||
				}],
 | 
			
		||||
			},
 | 
			
		||||
			pinnedUsers: { endpoint: 'pinned-users' },
 | 
			
		||||
			popularUsers: { endpoint: 'users', limit: 10, noPaging: true, params: {
 | 
			
		||||
				state: 'alive',
 | 
			
		||||
@@ -109,11 +126,19 @@ export default Vue.extend({
 | 
			
		||||
				origin: 'combined',
 | 
			
		||||
				sort: '+createdAt',
 | 
			
		||||
			} },
 | 
			
		||||
			searchPagination: {
 | 
			
		||||
				endpoint: 'users/search',
 | 
			
		||||
				limit: 10,
 | 
			
		||||
				params: computed(() => (this.query && this.query !== '') ? {
 | 
			
		||||
					query: this.query
 | 
			
		||||
				} : null)
 | 
			
		||||
			},
 | 
			
		||||
			tagsLocal: [],
 | 
			
		||||
			tagsRemote: [],
 | 
			
		||||
			stats: null,
 | 
			
		||||
			num: Vue.filter('number'),
 | 
			
		||||
			faBookmark, faChartLine, faCommentAlt, faPlus, faHashtag, faRocket
 | 
			
		||||
			query: null,
 | 
			
		||||
			num: number,
 | 
			
		||||
			faBookmark, faChartLine, faCommentAlt, faPlus, faHashtag, faRocket, faSearch,
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
@@ -137,25 +162,25 @@ export default Vue.extend({
 | 
			
		||||
	watch: {
 | 
			
		||||
		tag() {
 | 
			
		||||
			if (this.$refs.tags) this.$refs.tags.toggleContent(this.tag == null);
 | 
			
		||||
		}
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		this.$root.api('hashtags/list', {
 | 
			
		||||
		os.api('hashtags/list', {
 | 
			
		||||
			sort: '+attachedLocalUsers',
 | 
			
		||||
			attachedToLocalUserOnly: true,
 | 
			
		||||
			limit: 30
 | 
			
		||||
		}).then(tags => {
 | 
			
		||||
			this.tagsLocal = tags;
 | 
			
		||||
		});
 | 
			
		||||
		this.$root.api('hashtags/list', {
 | 
			
		||||
		os.api('hashtags/list', {
 | 
			
		||||
			sort: '+attachedRemoteUsers',
 | 
			
		||||
			attachedToRemoteUserOnly: true,
 | 
			
		||||
			limit: 30
 | 
			
		||||
		}).then(tags => {
 | 
			
		||||
			this.tagsRemote = tags;
 | 
			
		||||
		});
 | 
			
		||||
		this.$root.api('stats').then(stats => {
 | 
			
		||||
		os.api('stats').then(stats => {
 | 
			
		||||
			this.stats = stats;
 | 
			
		||||
		});
 | 
			
		||||
	},
 | 
			
		||||
@@ -195,8 +220,6 @@ export default Vue.extend({
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.vxjfqztj {
 | 
			
		||||
	padding: 16px;
 | 
			
		||||
 | 
			
		||||
	> * {
 | 
			
		||||
		margin-right: 16px;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,37 +1,35 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
	<portal to="icon"><fa :icon="faStar"/></portal>
 | 
			
		||||
	<portal to="title">{{ $t('favorites') }}</portal>
 | 
			
		||||
	<x-notes :pagination="pagination" :detail="true" :prop="'note'" @before="before()" @after="after()"/>
 | 
			
		||||
<div class="_section">
 | 
			
		||||
	<XNotes class="_content" :pagination="pagination" :detail="true" :prop="'note'" @before="before()" @after="after()"/>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faStar } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import Progress from '../scripts/loading';
 | 
			
		||||
import XNotes from '../components/notes.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: this.$t('favorites') as string
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
import Progress from '@/scripts/loading';
 | 
			
		||||
import XNotes from '@/components/notes.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XNotes
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('favorites'),
 | 
			
		||||
					icon: faStar
 | 
			
		||||
				}]
 | 
			
		||||
			},
 | 
			
		||||
			pagination: {
 | 
			
		||||
				endpoint: 'i/favorites',
 | 
			
		||||
				limit: 10,
 | 
			
		||||
				params: () => ({
 | 
			
		||||
				})
 | 
			
		||||
			},
 | 
			
		||||
			faStar
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,30 +1,28 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
	<portal to="icon"><fa :icon="faFireAlt"/></portal>
 | 
			
		||||
	<portal to="title">{{ $t('featured') }}</portal>
 | 
			
		||||
	<x-notes ref="notes" :pagination="pagination" @before="before" @after="after"/>
 | 
			
		||||
<div class="_section">
 | 
			
		||||
	<XNotes class="_content" ref="notes" :pagination="pagination" @before="before" @after="after"/>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faFireAlt } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import Progress from '../scripts/loading';
 | 
			
		||||
import XNotes from '../components/notes.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: this.$t('featured') as string
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
import Progress from '@/scripts/loading';
 | 
			
		||||
import XNotes from '@/components/notes.vue';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XNotes
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('featured'),
 | 
			
		||||
					icon: faFireAlt
 | 
			
		||||
				}],
 | 
			
		||||
			},
 | 
			
		||||
			pagination: {
 | 
			
		||||
				endpoint: 'notes/featured',
 | 
			
		||||
				limit: 10,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
	<portal to="icon"><fa :icon="faUserClock"/></portal>
 | 
			
		||||
	<portal to="title">{{ $t('followRequests') }}</portal>
 | 
			
		||||
 | 
			
		||||
	<mk-pagination :pagination="pagination" class="mk-follow-requests" ref="list">
 | 
			
		||||
	<MkPagination :pagination="pagination" class="mk-follow-requests" ref="list">
 | 
			
		||||
		<template #empty>
 | 
			
		||||
			<div class="_fullinfo">
 | 
			
		||||
				<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
 | 
			
		||||
@@ -12,44 +9,46 @@
 | 
			
		||||
		</template>
 | 
			
		||||
		<template #default="{items}">
 | 
			
		||||
			<div class="user _panel" v-for="req in items" :key="req.id">
 | 
			
		||||
				<mk-avatar class="avatar" :user="req.follower"/>
 | 
			
		||||
				<MkAvatar class="avatar" :user="req.follower"/>
 | 
			
		||||
				<div class="body">
 | 
			
		||||
					<div class="name">
 | 
			
		||||
						<router-link class="name" :to="req.follower | userPage" v-user-preview="req.follower.id"><mk-user-name :user="req.follower"/></router-link>
 | 
			
		||||
						<p class="acct">@{{ req.follower | acct }}</p>
 | 
			
		||||
						<router-link class="name" :to="userPage(req.follower)" v-user-preview="req.follower.id"><MkUserName :user="req.follower"/></router-link>
 | 
			
		||||
						<p class="acct">@{{ acct(req.follower) }}</p>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="description" v-if="req.follower.description" :title="req.follower.description">
 | 
			
		||||
						<mfm :text="req.follower.description" :is-note="false" :author="req.follower" :i="$store.state.i" :custom-emojis="req.follower.emojis" :plain="true" :nowrap="true"/>
 | 
			
		||||
						<Mfm :text="req.follower.description" :is-note="false" :author="req.follower" :i="$store.state.i" :custom-emojis="req.follower.emojis" :plain="true" :nowrap="true"/>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="actions">
 | 
			
		||||
						<button class="_button" @click="accept(req.follower)"><fa :icon="faCheck"/></button>
 | 
			
		||||
						<button class="_button" @click="reject(req.follower)"><fa :icon="faTimes"/></button>
 | 
			
		||||
						<button class="_button" @click="accept(req.follower)"><Fa :icon="faCheck"/></button>
 | 
			
		||||
						<button class="_button" @click="reject(req.follower)"><Fa :icon="faTimes"/></button>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</template>
 | 
			
		||||
	</mk-pagination>
 | 
			
		||||
	</MkPagination>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faUserClock, faCheck, faTimes } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import MkPagination from '../components/ui/pagination.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: this.$t('followRequests') as string
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
import MkPagination from '@/components/ui/pagination.vue';
 | 
			
		||||
import { userPage, acct } from '../filters/user';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkPagination
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('followRequests'),
 | 
			
		||||
					icon: faUserClock,
 | 
			
		||||
				}],
 | 
			
		||||
			},
 | 
			
		||||
			pagination: {
 | 
			
		||||
				endpoint: 'following/requests/list',
 | 
			
		||||
				limit: 10,
 | 
			
		||||
@@ -60,15 +59,17 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		accept(user) {
 | 
			
		||||
			this.$root.api('following/requests/accept', { userId: user.id }).then(() => {
 | 
			
		||||
			os.api('following/requests/accept', { userId: user.id }).then(() => {
 | 
			
		||||
				this.$refs.list.reload();
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
		reject(user) {
 | 
			
		||||
			this.$root.api('following/requests/reject', { userId: user.id }).then(() => {
 | 
			
		||||
			os.api('following/requests/reject', { userId: user.id }).then(() => {
 | 
			
		||||
				this.$refs.list.reload();
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
		},
 | 
			
		||||
		userPage,
 | 
			
		||||
		acct
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -4,14 +4,15 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	created() {
 | 
			
		||||
		const acct = new URL(location.href).searchParams.get('acct');
 | 
			
		||||
		if (acct == null) return;
 | 
			
		||||
 | 
			
		||||
		const dialog = this.$root.dialog({
 | 
			
		||||
		const dialog = os.dialog({
 | 
			
		||||
			type: 'waiting',
 | 
			
		||||
			text: this.$t('fetchingAsApObject') + '...',
 | 
			
		||||
			showOkButton: false,
 | 
			
		||||
@@ -20,13 +21,13 @@ export default Vue.extend({
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		if (acct.startsWith('https://')) {
 | 
			
		||||
			this.$root.api('ap/show', {
 | 
			
		||||
			os.api('ap/show', {
 | 
			
		||||
				uri: acct
 | 
			
		||||
			}).then(res => {
 | 
			
		||||
				if (res.type == 'User') {
 | 
			
		||||
					this.follow(res.object);
 | 
			
		||||
				} else {
 | 
			
		||||
					this.$root.dialog({
 | 
			
		||||
					os.dialog({
 | 
			
		||||
						type: 'error',
 | 
			
		||||
						text: 'Not a user'
 | 
			
		||||
					}).then(() => {
 | 
			
		||||
@@ -34,7 +35,7 @@ export default Vue.extend({
 | 
			
		||||
					});
 | 
			
		||||
				}
 | 
			
		||||
			}).catch(e => {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
				os.dialog({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: e
 | 
			
		||||
				}).then(() => {
 | 
			
		||||
@@ -44,10 +45,10 @@ export default Vue.extend({
 | 
			
		||||
				dialog.close();
 | 
			
		||||
			});
 | 
			
		||||
		} else {
 | 
			
		||||
			this.$root.api('users/show', parseAcct(acct)).then(user => {
 | 
			
		||||
			os.api('users/show', parseAcct(acct)).then(user => {
 | 
			
		||||
				this.follow(user);
 | 
			
		||||
			}).catch(e => {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
				os.dialog({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: e
 | 
			
		||||
				}).then(() => {
 | 
			
		||||
@@ -61,7 +62,7 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		async follow(user) {
 | 
			
		||||
			const { canceled } = await this.$root.dialog({
 | 
			
		||||
			const { canceled } = await os.dialog({
 | 
			
		||||
				type: 'question',
 | 
			
		||||
				text: this.$t('followConfirm', { name: user.name || user.username }),
 | 
			
		||||
				showCancelButton: true
 | 
			
		||||
@@ -72,17 +73,14 @@ export default Vue.extend({
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
			
 | 
			
		||||
			this.$root.api('following/create', {
 | 
			
		||||
			os.api('following/create', {
 | 
			
		||||
				userId: user.id
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
					type: 'success',
 | 
			
		||||
					iconOnly: true, autoClose: true
 | 
			
		||||
				}).then(() => {
 | 
			
		||||
				os.success().then(() => {
 | 
			
		||||
					window.close();
 | 
			
		||||
				});
 | 
			
		||||
			}).catch(e => {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
				os.dialog({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: e
 | 
			
		||||
				}).then(() => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,31 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
<component :is="$store.getters.isSignedIn ? 'home' : 'welcome'" :show-title="showTitle"></component>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import Home from './index.home.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	name: 'index',
 | 
			
		||||
 | 
			
		||||
	components: {
 | 
			
		||||
		Home,
 | 
			
		||||
		Welcome: () => import('./index.welcome.vue').then(m => m.default),
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			showTitle: true,
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	activated() {
 | 
			
		||||
		this.showTitle = true;
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	deactivated() {
 | 
			
		||||
		this.showTitle = false;
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
@@ -1,95 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="rsqzvsbo">
 | 
			
		||||
	<div class="_panel about" v-if="meta">
 | 
			
		||||
		<div class="banner" :style="{ backgroundImage: `url(${ meta.bannerUrl })` }"></div>
 | 
			
		||||
		<div class="body">
 | 
			
		||||
			<h1 class="name" v-html="meta.name || host"></h1>
 | 
			
		||||
			<div class="desc" v-html="meta.description || $t('introMisskey')"></div>
 | 
			
		||||
			<mk-button @click="signup()" style="display: inline-block; margin-right: 16px;" primary>{{ $t('signup') }}</mk-button>
 | 
			
		||||
			<mk-button @click="signin()" style="display: inline-block;">{{ $t('login') }}</mk-button>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
	<x-notes :pagination="featuredPagination"/>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { toUnicode } from 'punycode';
 | 
			
		||||
import XSigninDialog from '../components/signin-dialog.vue';
 | 
			
		||||
import XSignupDialog from '../components/signup-dialog.vue';
 | 
			
		||||
import MkButton from '../components/ui/button.vue';
 | 
			
		||||
import XNotes from '../components/notes.vue';
 | 
			
		||||
import { host } from '../config';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkButton,
 | 
			
		||||
		XNotes,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			featuredPagination: {
 | 
			
		||||
				endpoint: 'notes/featured',
 | 
			
		||||
				limit: 10,
 | 
			
		||||
				noPaging: true,
 | 
			
		||||
			},
 | 
			
		||||
			host: toUnicode(host),
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	computed: {
 | 
			
		||||
		meta() {
 | 
			
		||||
			return this.$store.state.instance.meta;
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		this.$root.api('stats').then(stats => {
 | 
			
		||||
			this.stats = stats;
 | 
			
		||||
		});
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		signin() {
 | 
			
		||||
			this.$root.new(XSigninDialog, {
 | 
			
		||||
				autoSet: true
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		signup() {
 | 
			
		||||
			this.$root.new(XSignupDialog, {
 | 
			
		||||
				autoSet: true
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.rsqzvsbo {
 | 
			
		||||
	> .about {
 | 
			
		||||
		overflow: hidden;
 | 
			
		||||
		margin-bottom: var(--margin);
 | 
			
		||||
 | 
			
		||||
		> .banner {
 | 
			
		||||
			height: 170px;
 | 
			
		||||
			background-size: cover;
 | 
			
		||||
			background-position: center center;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> .body {
 | 
			
		||||
			padding: 32px;
 | 
			
		||||
 | 
			
		||||
			@media (max-width: 500px) {
 | 
			
		||||
				padding: 16px;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> .name {
 | 
			
		||||
				margin: 0 0 0.5em 0;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,33 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div v-if="meta" class="mk-welcome">
 | 
			
		||||
	<portal to="title">{{ instanceName }}</portal>
 | 
			
		||||
	<x-setup v-if="meta.requireSetup"/>
 | 
			
		||||
	<x-entrance v-else/>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import XSetup from './index.welcome.setup.vue';
 | 
			
		||||
import XEntrance from './index.welcome.entrance.vue';
 | 
			
		||||
import { instanceName } from '../config';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	components: {
 | 
			
		||||
		XSetup,
 | 
			
		||||
		XEntrance,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			instanceName: instanceName || 'Misskey',
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	computed: {
 | 
			
		||||
		meta() {
 | 
			
		||||
			return this.$store.state.instance.meta;
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
@@ -1,44 +1,41 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="ztgjmzrw">
 | 
			
		||||
	<portal to="icon"><fa :icon="faBroadcastTower"/></portal>
 | 
			
		||||
	<portal to="title">{{ $t('announcements') }}</portal>
 | 
			
		||||
	<mk-button @click="add()" primary style="margin: 0 auto 16px auto;"><fa :icon="faPlus"/> {{ $t('add') }}</mk-button>
 | 
			
		||||
	<section class="_card announcements">
 | 
			
		||||
		<div class="_content announcement" v-for="announcement in announcements">
 | 
			
		||||
			<mk-input v-model="announcement.title">
 | 
			
		||||
				<span>{{ $t('title') }}</span>
 | 
			
		||||
			</mk-input>
 | 
			
		||||
			<mk-textarea v-model="announcement.text">
 | 
			
		||||
				<span>{{ $t('text') }}</span>
 | 
			
		||||
			</mk-textarea>
 | 
			
		||||
			<mk-input v-model="announcement.imageUrl">
 | 
			
		||||
				<span>{{ $t('imageUrl') }}</span>
 | 
			
		||||
			</mk-input>
 | 
			
		||||
			<p v-if="announcement.reads">{{ $t('nUsersRead', { n: announcement.reads }) }}</p>
 | 
			
		||||
			<div class="buttons">
 | 
			
		||||
				<mk-button class="button" inline @click="save(announcement)" primary><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
 | 
			
		||||
				<mk-button class="button" inline @click="remove(announcement)"><fa :icon="faTrashAlt"/> {{ $t('remove') }}</mk-button>
 | 
			
		||||
			</div>
 | 
			
		||||
	<div class="_section">
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<MkButton @click="add()" primary style="margin: 0 auto 16px auto;"><Fa :icon="faPlus"/> {{ $t('add') }}</MkButton>
 | 
			
		||||
			<section class="_card _vMargin announcements" v-for="announcement in announcements">
 | 
			
		||||
				<div class="_content announcement">
 | 
			
		||||
					<MkInput v-model:value="announcement.title">
 | 
			
		||||
						<span>{{ $t('title') }}</span>
 | 
			
		||||
					</MkInput>
 | 
			
		||||
					<MkTextarea v-model:value="announcement.text">
 | 
			
		||||
						<span>{{ $t('text') }}</span>
 | 
			
		||||
					</MkTextarea>
 | 
			
		||||
					<MkInput v-model:value="announcement.imageUrl">
 | 
			
		||||
						<span>{{ $t('imageUrl') }}</span>
 | 
			
		||||
					</MkInput>
 | 
			
		||||
					<p v-if="announcement.reads">{{ $t('nUsersRead', { n: announcement.reads }) }}</p>
 | 
			
		||||
					<div class="buttons">
 | 
			
		||||
						<MkButton class="button" inline @click="save(announcement)" primary><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
 | 
			
		||||
						<MkButton class="button" inline @click="remove(announcement)"><Fa :icon="faTrashAlt"/> {{ $t('remove') }}</MkButton>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</section>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faBroadcastTower, faPlus } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faSave, faTrashAlt } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import MkButton from '../../components/ui/button.vue';
 | 
			
		||||
import MkInput from '../../components/ui/input.vue';
 | 
			
		||||
import MkTextarea from '../../components/ui/textarea.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: this.$t('announcements') as string
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import MkInput from '@/components/ui/input.vue';
 | 
			
		||||
import MkTextarea from '@/components/ui/textarea.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkButton,
 | 
			
		||||
		MkInput,
 | 
			
		||||
@@ -47,13 +44,19 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('announcements'),
 | 
			
		||||
					icon: faBroadcastTower
 | 
			
		||||
				}]
 | 
			
		||||
			},
 | 
			
		||||
			announcements: [],
 | 
			
		||||
			faBroadcastTower, faSave, faTrashAlt, faPlus
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		this.$root.api('admin/announcements/list').then(announcements => {
 | 
			
		||||
		os.api('admin/announcements/list').then(announcements => {
 | 
			
		||||
			this.announcements = announcements;
 | 
			
		||||
		});
 | 
			
		||||
	},
 | 
			
		||||
@@ -69,38 +72,38 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		remove(announcement) {
 | 
			
		||||
			this.$root.dialog({
 | 
			
		||||
			os.dialog({
 | 
			
		||||
				type: 'warning',
 | 
			
		||||
				text: this.$t('removeAreYouSure', { x: announcement.title }),
 | 
			
		||||
				showCancelButton: true
 | 
			
		||||
			}).then(({ canceled }) => {
 | 
			
		||||
				if (canceled) return;
 | 
			
		||||
				this.announcements = this.announcements.filter(x => x != announcement);
 | 
			
		||||
				this.$root.api('admin/announcements/delete', announcement);
 | 
			
		||||
				os.api('admin/announcements/delete', announcement);
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		save(announcement) {
 | 
			
		||||
			if (announcement.id == null) {
 | 
			
		||||
				this.$root.api('admin/announcements/create', announcement).then(() => {
 | 
			
		||||
					this.$root.dialog({
 | 
			
		||||
				os.api('admin/announcements/create', announcement).then(() => {
 | 
			
		||||
					os.dialog({
 | 
			
		||||
						type: 'success',
 | 
			
		||||
						text: this.$t('saved')
 | 
			
		||||
					});
 | 
			
		||||
				}).catch(e => {
 | 
			
		||||
					this.$root.dialog({
 | 
			
		||||
					os.dialog({
 | 
			
		||||
						type: 'error',
 | 
			
		||||
						text: e
 | 
			
		||||
					});
 | 
			
		||||
				});
 | 
			
		||||
			} else {
 | 
			
		||||
				this.$root.api('admin/announcements/update', announcement).then(() => {
 | 
			
		||||
					this.$root.dialog({
 | 
			
		||||
				os.api('admin/announcements/update', announcement).then(() => {
 | 
			
		||||
					os.dialog({
 | 
			
		||||
						type: 'success',
 | 
			
		||||
						text: this.$t('saved')
 | 
			
		||||
					});
 | 
			
		||||
				}).catch(e => {
 | 
			
		||||
					this.$root.dialog({
 | 
			
		||||
					os.dialog({
 | 
			
		||||
						type: 'error',
 | 
			
		||||
						text: e
 | 
			
		||||
					});
 | 
			
		||||
@@ -110,17 +113,3 @@ export default Vue.extend({
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.ztgjmzrw {
 | 
			
		||||
	> .announcements {
 | 
			
		||||
		> .announcement {
 | 
			
		||||
			> .buttons {
 | 
			
		||||
				> .button:first-child {
 | 
			
		||||
					margin-right: 8px;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										116
									
								
								src/client/pages/instance/emoji-edit-dialog.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								src/client/pages/instance/emoji-edit-dialog.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,116 @@
 | 
			
		||||
<template>
 | 
			
		||||
<XModalWindow ref="dialog"
 | 
			
		||||
	:width="370"
 | 
			
		||||
	:with-ok-button="true"
 | 
			
		||||
	@close="$refs.dialog.close()"
 | 
			
		||||
	@closed="$emit('closed')"
 | 
			
		||||
	@ok="ok()"
 | 
			
		||||
>
 | 
			
		||||
	<template #header>:{{ emoji.name }}:</template>
 | 
			
		||||
 | 
			
		||||
	<div class="yigymqpb _section">
 | 
			
		||||
		<img :src="emoji.url" class="img"/>
 | 
			
		||||
		<MkInput v-model:value="name"><span>{{ $t('name') }}</span></MkInput>
 | 
			
		||||
		<MkInput v-model:value="category" :datalist="categories"><span>{{ $t('category') }}</span></MkInput>
 | 
			
		||||
		<MkInput v-model:value="aliases">
 | 
			
		||||
			<span>{{ $t('tags') }}</span>
 | 
			
		||||
			<template #desc>{{ $t('setMultipleBySeparatingWithSpace') }}</template>
 | 
			
		||||
		</MkInput>
 | 
			
		||||
		<MkButton danger @click="del()"><Fa :icon="faTrashAlt"/> {{ $t('delete') }}</MkButton>
 | 
			
		||||
	</div>
 | 
			
		||||
</XModalWindow>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import XModalWindow from '@/components/ui/modal-window.vue';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import MkInput from '@/components/ui/input.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { unique } from '../../../prelude/array';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XModalWindow,
 | 
			
		||||
		MkButton,
 | 
			
		||||
		MkInput,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	props: {
 | 
			
		||||
		emoji: {
 | 
			
		||||
			required: true,
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	emits: ['done', 'closed'],
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			name: this.emoji.name,
 | 
			
		||||
			category: this.emoji.category,
 | 
			
		||||
			aliases: this.emoji.aliases?.join(' '),
 | 
			
		||||
			categories: [],
 | 
			
		||||
			faTrashAlt,
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		os.api('meta', { detail: false }).then(({ emojis }) => {
 | 
			
		||||
			this.categories = unique(emojis.map((x: any) => x.category || '').filter((x: string) => x !== ''));
 | 
			
		||||
		});
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		ok() {
 | 
			
		||||
			this.update();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async update() {
 | 
			
		||||
			await os.apiWithDialog('admin/emoji/update', {
 | 
			
		||||
				id: this.emoji.id,
 | 
			
		||||
				name: this.name,
 | 
			
		||||
				category: this.category,
 | 
			
		||||
				aliases: this.aliases.split(' '),
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			this.$emit('done', {
 | 
			
		||||
				updated: {
 | 
			
		||||
					name: this.name,
 | 
			
		||||
					category: this.category,
 | 
			
		||||
					aliases: this.aliases.split(' '),
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
			this.$refs.dialog.close();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async del() {
 | 
			
		||||
			const { canceled } = await os.dialog({
 | 
			
		||||
				type: 'warning',
 | 
			
		||||
				text: this.$t('removeAreYouSure', { x: this.emoji.name }),
 | 
			
		||||
				showCancelButton: true
 | 
			
		||||
			});
 | 
			
		||||
			if (canceled) return;
 | 
			
		||||
 | 
			
		||||
			os.api('admin/emoji/remove', {
 | 
			
		||||
				id: this.emoji.id
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				this.$emit('done', {
 | 
			
		||||
					deleted: true
 | 
			
		||||
				});
 | 
			
		||||
				this.$refs.dialog.close();
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.yigymqpb {
 | 
			
		||||
	> .img {
 | 
			
		||||
		display: block;
 | 
			
		||||
		height: 64px;
 | 
			
		||||
		margin: 0 auto;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,80 +1,67 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="mk-instance-emojis">
 | 
			
		||||
	<portal to="icon"><fa :icon="faLaugh"/></portal>
 | 
			
		||||
	<portal to="title">{{ $t('customEmojis') }}</portal>
 | 
			
		||||
	<div class="_section" style="padding: 0;">
 | 
			
		||||
		<MkTab v-model:value="tab" :items="[{ label: $t('local'), value: 'local' }, { label: $t('remote'), value: 'remote' }]"/>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<section class="_card _vMargin local">
 | 
			
		||||
		<div class="_title"><fa :icon="faLaugh"/> {{ $t('customEmojis') }}</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-pagination :pagination="pagination" class="emojis" ref="emojis">
 | 
			
		||||
	<div class="_section">
 | 
			
		||||
		<div class="_content local" v-if="tab === 'local'">
 | 
			
		||||
			<MkButton primary @click="add" style="margin: 0 auto var(--margin) auto;"><Fa :icon="faPlus"/> {{ $t('addEmoji') }}</MkButton>
 | 
			
		||||
			<MkInput v-model:value="query" :debounce="true" type="search"><template #icon><Fa :icon="faSearch"/></template><span>{{ $t('search') }}</span></MkInput>
 | 
			
		||||
			<MkPagination :pagination="pagination" ref="emojis">
 | 
			
		||||
				<template #empty><span>{{ $t('noCustomEmojis') }}</span></template>
 | 
			
		||||
				<template #default="{items}">
 | 
			
		||||
					<div class="emoji" v-for="(emoji, i) in items" :key="emoji.id" @click="selected = emoji" :class="{ selected: selected && (selected.id === emoji.id) }">
 | 
			
		||||
						<img :src="emoji.url" class="img" :alt="emoji.name"/>
 | 
			
		||||
						<div class="body">
 | 
			
		||||
							<span class="name">{{ emoji.name }}</span>
 | 
			
		||||
							<span class="info">
 | 
			
		||||
								<b class="category">{{ emoji.category }}</b>
 | 
			
		||||
								<span class="aliases">{{ emoji.aliases.join(' ') }}</span>
 | 
			
		||||
							</span>
 | 
			
		||||
					<div class="emojis">
 | 
			
		||||
						<button class="emoji _panel _button" v-for="emoji in items" :key="emoji.id" @click="edit(emoji)">
 | 
			
		||||
							<img :src="emoji.url" class="img" :alt="emoji.name"/>
 | 
			
		||||
							<div class="body">
 | 
			
		||||
								<span class="name">{{ emoji.name }}</span>
 | 
			
		||||
								<span class="info">
 | 
			
		||||
									<span class="category">{{ emoji.category }}</span>
 | 
			
		||||
								</span>
 | 
			
		||||
							</div>
 | 
			
		||||
						</button>
 | 
			
		||||
					</div>
 | 
			
		||||
				</template>
 | 
			
		||||
			</MkPagination>
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
		<div class="_content remote" v-else-if="tab === 'remote'">
 | 
			
		||||
			<MkInput v-model:value="queryRemote" :debounce="true" type="search"><template #icon><Fa :icon="faSearch"/></template><span>{{ $t('search') }}</span></MkInput>
 | 
			
		||||
			<MkInput v-model:value="host" :debounce="true"><span>{{ $t('host') }}</span></MkInput>
 | 
			
		||||
			<MkPagination :pagination="remotePagination" ref="remoteEmojis">
 | 
			
		||||
				<template #empty><span>{{ $t('noCustomEmojis') }}</span></template>
 | 
			
		||||
				<template #default="{items}">
 | 
			
		||||
					<div class="emojis">
 | 
			
		||||
						<div class="emoji _panel _button" v-for="emoji in items" :key="emoji.id" @click="remoteMenu(emoji, $event)">
 | 
			
		||||
							<img :src="emoji.url" class="img" :alt="emoji.name"/>
 | 
			
		||||
							<div class="body">
 | 
			
		||||
								<span class="name">{{ emoji.name }}</span>
 | 
			
		||||
								<span class="info">{{ emoji.host }}</span>
 | 
			
		||||
							</div>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</template>
 | 
			
		||||
			</mk-pagination>
 | 
			
		||||
			</MkPagination>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_content" v-if="selected">
 | 
			
		||||
			<mk-input v-model="name"><span>{{ $t('name') }}</span></mk-input>
 | 
			
		||||
			<mk-input v-model="category" :datalist="categories"><span>{{ $t('category') }}</span></mk-input>
 | 
			
		||||
			<mk-input v-model="aliases"><span>{{ $t('tags') }}</span></mk-input>
 | 
			
		||||
			<mk-button inline primary @click="update"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
 | 
			
		||||
			<mk-button inline :disabled="selected == null" @click="del()"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</mk-button>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_footer">
 | 
			
		||||
			<mk-button inline primary @click="add"><fa :icon="faPlus"/> {{ $t('addEmoji') }}</mk-button>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
	<section class="_card _vMargin remote">
 | 
			
		||||
		<div class="_title"><fa :icon="faLaugh"/> {{ $t('customEmojisOfRemote') }}</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-input v-model="host" :debounce="true"><span>{{ $t('host') }}</span></mk-input>
 | 
			
		||||
			<mk-pagination :pagination="remotePagination" class="emojis" ref="remoteEmojis">
 | 
			
		||||
				<template #empty><span>{{ $t('noCustomEmojis') }}</span></template>
 | 
			
		||||
				<template #default="{items}">
 | 
			
		||||
					<div class="emoji" v-for="(emoji, i) in items" :key="emoji.id" @click="selectedRemote = emoji" :class="{ selected: selectedRemote && (selectedRemote.id === emoji.id) }">
 | 
			
		||||
						<img :src="emoji.url" class="img" :alt="emoji.name"/>
 | 
			
		||||
						<div class="body">
 | 
			
		||||
							<span class="name">{{ emoji.name }}</span>
 | 
			
		||||
							<span class="info">{{ emoji.host }}</span>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</template>
 | 
			
		||||
			</mk-pagination>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_footer">
 | 
			
		||||
			<mk-button inline primary :disabled="selectedRemote == null" @click="im()"><fa :icon="faPlus"/> {{ $t('import') }}</mk-button>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { faPlus, faSave } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { computed, defineComponent } from 'vue';
 | 
			
		||||
import { faPlus, faSave, faSearch } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faTrashAlt, faLaugh } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import MkButton from '../../components/ui/button.vue';
 | 
			
		||||
import MkInput from '../../components/ui/input.vue';
 | 
			
		||||
import MkPagination from '../../components/ui/pagination.vue';
 | 
			
		||||
import { selectFile } from '../../scripts/select-file';
 | 
			
		||||
import { unique } from '../../../prelude/array';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: `${this.$t('customEmojis')} | ${this.$t('instance')}`
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import MkInput from '@/components/ui/input.vue';
 | 
			
		||||
import MkPagination from '@/components/ui/pagination.vue';
 | 
			
		||||
import MkTab from '@/components/tab.vue';
 | 
			
		||||
import { selectFile } from '@/scripts/select-file';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkTab,
 | 
			
		||||
		MkButton,
 | 
			
		||||
		MkInput,
 | 
			
		||||
		MkPagination,
 | 
			
		||||
@@ -82,54 +69,44 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			selected: null,
 | 
			
		||||
			selectedRemote: null,
 | 
			
		||||
			name: null,
 | 
			
		||||
			category: null,
 | 
			
		||||
			aliases: null,
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('customEmojis'),
 | 
			
		||||
					icon: faLaugh
 | 
			
		||||
				}],
 | 
			
		||||
				action: {
 | 
			
		||||
					icon: faPlus,
 | 
			
		||||
					handler: this.add
 | 
			
		||||
				}
 | 
			
		||||
			},
 | 
			
		||||
			tab: 'local',
 | 
			
		||||
			query: null,
 | 
			
		||||
			queryRemote: null,
 | 
			
		||||
			host: '',
 | 
			
		||||
			pagination: {
 | 
			
		||||
				endpoint: 'admin/emoji/list',
 | 
			
		||||
				limit: 10,
 | 
			
		||||
				limit: 15,
 | 
			
		||||
				params: computed(() => ({
 | 
			
		||||
					query: (this.query && this.query !== '') ? this.query : null
 | 
			
		||||
				}))
 | 
			
		||||
			},
 | 
			
		||||
			remotePagination: {
 | 
			
		||||
				endpoint: 'admin/emoji/list-remote',
 | 
			
		||||
				limit: 10,
 | 
			
		||||
				params: () => ({
 | 
			
		||||
					host: this.host ? this.host : null
 | 
			
		||||
				})
 | 
			
		||||
				limit: 15,
 | 
			
		||||
				params: computed(() => ({
 | 
			
		||||
					query: (this.queryRemote && this.queryRemote !== '') ? this.queryRemote : null,
 | 
			
		||||
					host: (this.host && this.host !== '') ? this.host : null
 | 
			
		||||
				}))
 | 
			
		||||
			},
 | 
			
		||||
			faTrashAlt, faPlus, faLaugh, faSave
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	computed: {
 | 
			
		||||
		categories() {
 | 
			
		||||
			if (this.$store.state.instance.meta) {
 | 
			
		||||
				return unique(this.$store.state.instance.meta.emojis.map((x: any) => x.category || '').filter((x: string) => x !== ''));
 | 
			
		||||
			} else {
 | 
			
		||||
				return [];
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	watch: {
 | 
			
		||||
		host() {
 | 
			
		||||
			this.$refs.remoteEmojis.reload();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		selected() {
 | 
			
		||||
			this.name = this.selected ? this.selected.name : null;
 | 
			
		||||
			this.category = this.selected ? this.selected.category : null;
 | 
			
		||||
			this.aliases = this.selected ? this.selected.aliases.join(' ') : null;
 | 
			
		||||
			faTrashAlt, faPlus, faLaugh, faSave, faSearch,
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		async add(e) {
 | 
			
		||||
			const files = await selectFile(this, e.currentTarget || e.target, null, true);
 | 
			
		||||
			const files = await selectFile(e.currentTarget || e.target, null, true);
 | 
			
		||||
 | 
			
		||||
			const dialog = this.$root.dialog({
 | 
			
		||||
			const dialog = os.dialog({
 | 
			
		||||
				type: 'waiting',
 | 
			
		||||
				text: this.$t('doing') + '...',
 | 
			
		||||
				showOkButton: false,
 | 
			
		||||
@@ -137,133 +114,112 @@ export default Vue.extend({
 | 
			
		||||
				cancelableByBgClick: false
 | 
			
		||||
			});
 | 
			
		||||
			
 | 
			
		||||
			Promise.all(files.map(file => this.$root.api('admin/emoji/add', {
 | 
			
		||||
			Promise.all(files.map(file => os.api('admin/emoji/add', {
 | 
			
		||||
				fileId: file.id,
 | 
			
		||||
			})))
 | 
			
		||||
			.then(() => {
 | 
			
		||||
				this.$refs.emojis.reload();
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
					type: 'success',
 | 
			
		||||
					iconOnly: true, autoClose: true
 | 
			
		||||
				});
 | 
			
		||||
				os.success();
 | 
			
		||||
			})
 | 
			
		||||
			.finally(() => {
 | 
			
		||||
				dialog.close();
 | 
			
		||||
				dialog.cancel();
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async update() {
 | 
			
		||||
			await this.$root.api('admin/emoji/update', {
 | 
			
		||||
				id: this.selected.id,
 | 
			
		||||
				name: this.name,
 | 
			
		||||
				category: this.category,
 | 
			
		||||
				aliases: this.aliases.split(' '),
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			this.$root.dialog({
 | 
			
		||||
				type: 'success',
 | 
			
		||||
				iconOnly: true, autoClose: true
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			this.$refs.emojis.reload();
 | 
			
		||||
		async edit(emoji) {
 | 
			
		||||
			os.popup(await import('./emoji-edit-dialog.vue'), {
 | 
			
		||||
				emoji: emoji
 | 
			
		||||
			}, {
 | 
			
		||||
				done: result => {
 | 
			
		||||
					if (result.updated) {
 | 
			
		||||
						this.$refs.emojis.replaceItem(item => item.id === emoji.id, {
 | 
			
		||||
							...emoji,
 | 
			
		||||
							...result.updated
 | 
			
		||||
						});
 | 
			
		||||
					} else if (result.deleted) {
 | 
			
		||||
						this.$refs.emojis.removeItem(item => item.id === emoji.id);
 | 
			
		||||
					}
 | 
			
		||||
				},
 | 
			
		||||
			}, 'closed');
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async del() {
 | 
			
		||||
			const { canceled } = await this.$root.dialog({
 | 
			
		||||
				type: 'warning',
 | 
			
		||||
				text: this.$t('removeAreYouSure', { x: this.selected.name }),
 | 
			
		||||
				showCancelButton: true
 | 
			
		||||
			});
 | 
			
		||||
			if (canceled) return;
 | 
			
		||||
 | 
			
		||||
			this.$root.api('admin/emoji/remove', {
 | 
			
		||||
				id: this.selected.id
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				this.$refs.emojis.reload();
 | 
			
		||||
		im(emoji) {
 | 
			
		||||
			os.apiWithDialog('admin/emoji/copy', {
 | 
			
		||||
				emojiId: emoji.id,
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		im() {
 | 
			
		||||
			this.$root.api('admin/emoji/copy', {
 | 
			
		||||
				emojiId: this.selectedRemote.id,
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				this.$refs.emojis.reload();
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
					type: 'success',
 | 
			
		||||
					iconOnly: true, autoClose: true
 | 
			
		||||
				});
 | 
			
		||||
			}).catch(e => {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: e
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
		remoteMenu(emoji, ev) {
 | 
			
		||||
			os.modalMenu([{
 | 
			
		||||
				type: 'label',
 | 
			
		||||
				text: ':' + emoji.name + ':',
 | 
			
		||||
			}, {
 | 
			
		||||
				text: this.$t('import'),
 | 
			
		||||
				icon: faPlus,
 | 
			
		||||
				action: () => { this.im(emoji) }
 | 
			
		||||
			}], ev.currentTarget || ev.target);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.mk-instance-emojis {
 | 
			
		||||
	> .local {
 | 
			
		||||
		> ._content {
 | 
			
		||||
			max-height: 300px;
 | 
			
		||||
			overflow: auto;
 | 
			
		||||
			
 | 
			
		||||
			> .emojis {
 | 
			
		||||
	> ._section {
 | 
			
		||||
		> .local {
 | 
			
		||||
			.emojis {
 | 
			
		||||
				display: grid;
 | 
			
		||||
				grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
 | 
			
		||||
				grid-gap: var(--margin);
 | 
			
		||||
		
 | 
			
		||||
				> .emoji {
 | 
			
		||||
					display: flex;
 | 
			
		||||
					align-items: center;
 | 
			
		||||
					padding: 12px;
 | 
			
		||||
					text-align: left;
 | 
			
		||||
 | 
			
		||||
					&.selected {
 | 
			
		||||
						background: var(--accent);
 | 
			
		||||
						box-shadow: 0 0 0 8px var(--accent);
 | 
			
		||||
						color: #fff;
 | 
			
		||||
					&:hover {
 | 
			
		||||
						color: var(--accent);
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					> .img {
 | 
			
		||||
						width: 50px;
 | 
			
		||||
						height: 50px;
 | 
			
		||||
						width: 42px;
 | 
			
		||||
						height: 42px;
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					> .body {
 | 
			
		||||
						padding: 8px;
 | 
			
		||||
						padding: 0 0 0 8px;
 | 
			
		||||
						white-space: nowrap;
 | 
			
		||||
						overflow: hidden;
 | 
			
		||||
 | 
			
		||||
						> .name {
 | 
			
		||||
							display: block;
 | 
			
		||||
							text-overflow: ellipsis;
 | 
			
		||||
							overflow: hidden;
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						> .info {
 | 
			
		||||
							opacity: 0.5;
 | 
			
		||||
 | 
			
		||||
							> .category {
 | 
			
		||||
								margin-right: 16px;
 | 
			
		||||
							}
 | 
			
		||||
 | 
			
		||||
							> .aliases {
 | 
			
		||||
								font-style: oblique;
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .remote {
 | 
			
		||||
		> ._content {
 | 
			
		||||
			max-height: 300px;
 | 
			
		||||
			overflow: auto;
 | 
			
		||||
			
 | 
			
		||||
			> .emojis {
 | 
			
		||||
		> .remote {
 | 
			
		||||
			.emojis {
 | 
			
		||||
				display: grid;
 | 
			
		||||
				grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
 | 
			
		||||
				grid-gap: var(--margin);
 | 
			
		||||
 | 
			
		||||
				> .emoji {
 | 
			
		||||
					display: flex;
 | 
			
		||||
					align-items: center;
 | 
			
		||||
					padding: 12px;
 | 
			
		||||
					text-align: left;
 | 
			
		||||
 | 
			
		||||
					&.selected {
 | 
			
		||||
						background: var(--accent);
 | 
			
		||||
						box-shadow: 0 0 0 8px var(--accent);
 | 
			
		||||
						color: #fff;
 | 
			
		||||
					&:hover {
 | 
			
		||||
						color: var(--accent);
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					> .img {
 | 
			
		||||
@@ -272,14 +228,21 @@ export default Vue.extend({
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					> .body {
 | 
			
		||||
						padding: 0 8px;
 | 
			
		||||
						padding: 0 0 0 8px;
 | 
			
		||||
						white-space: nowrap;
 | 
			
		||||
						overflow: hidden;
 | 
			
		||||
 | 
			
		||||
						> .name {
 | 
			
		||||
							display: block;
 | 
			
		||||
							text-overflow: ellipsis;
 | 
			
		||||
							overflow: hidden;
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						> .info {
 | 
			
		||||
							opacity: 0.5;
 | 
			
		||||
							display: block;
 | 
			
		||||
							text-overflow: ellipsis;
 | 
			
		||||
							overflow: hidden;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,10 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="mk-federation">
 | 
			
		||||
	<portal to="icon"><fa :icon="faGlobe"/></portal>
 | 
			
		||||
	<portal to="title">{{ $t('federation') }}</portal>
 | 
			
		||||
 | 
			
		||||
	<section class="_card instances">
 | 
			
		||||
<div>
 | 
			
		||||
	<div class="_section">
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-input v-model="host" :debounce="true"><span>{{ $t('host') }}</span></mk-input>
 | 
			
		||||
			<MkInput v-model:value="host" :debounce="true"><span>{{ $t('host') }}</span></MkInput>
 | 
			
		||||
			<div class="inputs" style="display: flex;">
 | 
			
		||||
				<mk-select v-model="state" style="margin: 0; flex: 1;">
 | 
			
		||||
				<MkSelect v-model:value="state" style="margin: 0; flex: 1;">
 | 
			
		||||
					<template #label>{{ $t('state') }}</template>
 | 
			
		||||
					<option value="all">{{ $t('all') }}</option>
 | 
			
		||||
					<option value="federating">{{ $t('federating') }}</option>
 | 
			
		||||
@@ -16,8 +13,8 @@
 | 
			
		||||
					<option value="suspended">{{ $t('suspended') }}</option>
 | 
			
		||||
					<option value="blocked">{{ $t('blocked') }}</option>
 | 
			
		||||
					<option value="notResponding">{{ $t('notResponding') }}</option>
 | 
			
		||||
				</mk-select>
 | 
			
		||||
				<mk-select v-model="sort" style="margin: 0; flex: 1;">
 | 
			
		||||
				</MkSelect>
 | 
			
		||||
				<MkSelect v-model:value="sort" style="margin: 0; flex: 1;">
 | 
			
		||||
					<template #label>{{ $t('sort') }}</template>
 | 
			
		||||
					<option value="+pubSub">{{ $t('pubSub') }} ({{ $t('descendingOrder') }})</option>
 | 
			
		||||
					<option value="-pubSub">{{ $t('pubSub') }} ({{ $t('ascendingOrder') }})</option>
 | 
			
		||||
@@ -37,44 +34,41 @@
 | 
			
		||||
					<option value="-driveUsage">{{ $t('driveUsage') }} ({{ $t('ascendingOrder') }})</option>
 | 
			
		||||
					<option value="+driveFiles">{{ $t('driveFiles') }} ({{ $t('descendingOrder') }})</option>
 | 
			
		||||
					<option value="-driveFiles">{{ $t('driveFiles') }} ({{ $t('ascendingOrder') }})</option>
 | 
			
		||||
				</mk-select>
 | 
			
		||||
				</MkSelect>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="_section">
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-pagination :pagination="pagination" #default="{items}" class="instances" ref="instances" :key="host + state">
 | 
			
		||||
				<div class="instance" v-for="instance in items" :key="instance.id" @click="info(instance)">
 | 
			
		||||
					<div class="host"><fa :icon="faCircle" class="indicator" :class="getStatus(instance)"/><b>{{ instance.host }}</b></div>
 | 
			
		||||
			<MkPagination :pagination="pagination" #default="{items}" ref="instances" :key="host + state">
 | 
			
		||||
				<div class="ppgwaixt _panel" v-for="instance in items" :key="instance.id" @click="info(instance)">
 | 
			
		||||
					<div class="host"><Fa :icon="faCircle" class="indicator" :class="getStatus(instance)"/><b>{{ instance.host }}</b></div>
 | 
			
		||||
					<div class="status">
 | 
			
		||||
						<span class="sub" v-if="instance.followersCount > 0"><fa :icon="faCaretDown" class="icon"/>Sub</span>
 | 
			
		||||
						<span class="sub" v-else><fa :icon="faCaretDown" class="icon"/>-</span>
 | 
			
		||||
						<span class="pub" v-if="instance.followingCount > 0"><fa :icon="faCaretUp" class="icon"/>Pub</span>
 | 
			
		||||
						<span class="pub" v-else><fa :icon="faCaretUp" class="icon"/>-</span>
 | 
			
		||||
						<span class="lastCommunicatedAt"><fa :icon="faExchangeAlt" class="icon"/><mk-time :time="instance.lastCommunicatedAt"/></span>
 | 
			
		||||
						<span class="latestStatus"><fa :icon="faTrafficLight" class="icon"/>{{ instance.latestStatus || '-' }}</span>
 | 
			
		||||
						<span class="sub" v-if="instance.followersCount > 0"><Fa :icon="faCaretDown" class="icon"/>Sub</span>
 | 
			
		||||
						<span class="sub" v-else><Fa :icon="faCaretDown" class="icon"/>-</span>
 | 
			
		||||
						<span class="pub" v-if="instance.followingCount > 0"><Fa :icon="faCaretUp" class="icon"/>Pub</span>
 | 
			
		||||
						<span class="pub" v-else><Fa :icon="faCaretUp" class="icon"/>-</span>
 | 
			
		||||
						<span class="lastCommunicatedAt"><Fa :icon="faExchangeAlt" class="icon"/><MkTime :time="instance.lastCommunicatedAt"/></span>
 | 
			
		||||
						<span class="latestStatus"><Fa :icon="faTrafficLight" class="icon"/>{{ instance.latestStatus || '-' }}</span>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</mk-pagination>
 | 
			
		||||
			</MkPagination>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faGlobe, faCircle, faExchangeAlt, faCaretDown, faCaretUp, faTrafficLight } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import MkButton from '../../components/ui/button.vue';
 | 
			
		||||
import MkInput from '../../components/ui/input.vue';
 | 
			
		||||
import MkSelect from '../../components/ui/select.vue';
 | 
			
		||||
import MkPagination from '../../components/ui/pagination.vue';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import MkInput from '@/components/ui/input.vue';
 | 
			
		||||
import MkSelect from '@/components/ui/select.vue';
 | 
			
		||||
import MkPagination from '@/components/ui/pagination.vue';
 | 
			
		||||
import MkInstanceInfo from './instance.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: this.$t('federation') as string
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkButton,
 | 
			
		||||
		MkInput,
 | 
			
		||||
@@ -84,6 +78,12 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('federation'),
 | 
			
		||||
					icon: faGlobe
 | 
			
		||||
				}],
 | 
			
		||||
			},
 | 
			
		||||
			host: '',
 | 
			
		||||
			state: 'federating',
 | 
			
		||||
			sort: '+pubSub',
 | 
			
		||||
@@ -125,60 +125,57 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		info(instance) {
 | 
			
		||||
			this.$root.new(MkInstanceInfo, {
 | 
			
		||||
			os.popup(MkInstanceInfo, {
 | 
			
		||||
				instance: instance
 | 
			
		||||
			});
 | 
			
		||||
			}, {}, 'closed');
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.mk-federation {
 | 
			
		||||
	> .instances {
 | 
			
		||||
		> ._content {
 | 
			
		||||
			> .instances {
 | 
			
		||||
				> .instance {
 | 
			
		||||
					cursor: pointer;
 | 
			
		||||
.ppgwaixt {
 | 
			
		||||
	cursor: pointer;
 | 
			
		||||
	padding: 16px;
 | 
			
		||||
 | 
			
		||||
					> .host {
 | 
			
		||||
						> .indicator {
 | 
			
		||||
							font-size: 70%;
 | 
			
		||||
							vertical-align: baseline;
 | 
			
		||||
							margin-right: 4px;
 | 
			
		||||
	&:hover {
 | 
			
		||||
		color: var(--accent);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
							&.green {
 | 
			
		||||
								color: #49c5ba;
 | 
			
		||||
							}
 | 
			
		||||
	> .host {
 | 
			
		||||
		> .indicator {
 | 
			
		||||
			font-size: 70%;
 | 
			
		||||
			vertical-align: baseline;
 | 
			
		||||
			margin-right: 4px;
 | 
			
		||||
 | 
			
		||||
							&.yellow {
 | 
			
		||||
								color: #c5a549;
 | 
			
		||||
							}
 | 
			
		||||
			&.green {
 | 
			
		||||
				color: #49c5ba;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
							&.red {
 | 
			
		||||
								color: #c54949;
 | 
			
		||||
							}
 | 
			
		||||
			&.yellow {
 | 
			
		||||
				color: #c5a549;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
							&.off {
 | 
			
		||||
								color: rgba(0, 0, 0, 0.5);
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
			&.red {
 | 
			
		||||
				color: #c54949;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
					> .status {
 | 
			
		||||
						display: flex;
 | 
			
		||||
						align-items: center;
 | 
			
		||||
						font-size: 90%;
 | 
			
		||||
			&.off {
 | 
			
		||||
				color: rgba(0, 0, 0, 0.5);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
						> span {
 | 
			
		||||
							flex: 1;
 | 
			
		||||
							
 | 
			
		||||
							> .icon {
 | 
			
		||||
								margin-right: 6px;
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
	> .status {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		align-items: center;
 | 
			
		||||
		font-size: 90%;
 | 
			
		||||
 | 
			
		||||
		> span {
 | 
			
		||||
			flex: 1;
 | 
			
		||||
			
 | 
			
		||||
			> .icon {
 | 
			
		||||
				margin-right: 6px;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										136
									
								
								src/client/pages/instance/file-dialog.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								src/client/pages/instance/file-dialog.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,136 @@
 | 
			
		||||
<template>
 | 
			
		||||
<XModalWindow ref="dialog"
 | 
			
		||||
	:width="370"
 | 
			
		||||
	@close="$refs.dialog.close()"
 | 
			
		||||
	@closed="$emit('closed')"
 | 
			
		||||
>
 | 
			
		||||
	<template #header v-if="file">{{ file.name }}</template>
 | 
			
		||||
	<div class="cxqhhsmd" v-if="file">
 | 
			
		||||
		<div class="_section">
 | 
			
		||||
			<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
 | 
			
		||||
			<div class="info">
 | 
			
		||||
				<span style="margin-right: 1em;">{{ file.type }}</span>
 | 
			
		||||
				<span>{{ bytes(file.size) }}</span>
 | 
			
		||||
				<MkTime :time="file.createdAt" mode="detail" style="display: block;"/>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_section">
 | 
			
		||||
			<div class="_content">
 | 
			
		||||
				<MkSwitch @update:value="toggleIsSensitive" v-model:value="isSensitive">NSFW</MkSwitch>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_section">
 | 
			
		||||
			<div class="_content">
 | 
			
		||||
				<MkButton full @click="showUser"><Fa :icon="faExternalLinkSquareAlt"/> {{ $t('user') }}</MkButton>
 | 
			
		||||
				<MkButton full danger @click="del"><Fa :icon="faTrashAlt"/> {{ $t('delete') }}</MkButton>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_section" v-if="info">
 | 
			
		||||
			<details class="_content rawdata">
 | 
			
		||||
				<pre><code>{{ JSON.stringify(info, null, 2) }}</code></pre>
 | 
			
		||||
			</details>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</XModalWindow>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { computed, defineComponent } from 'vue';
 | 
			
		||||
import { faTimes, faBookmark, faKey, faSync, faMicrophoneSlash, faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faSnowflake, faTrashAlt, faBookmark as farBookmark  } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import MkSwitch from '@/components/ui/switch.vue';
 | 
			
		||||
import XModalWindow from '@/components/ui/modal-window.vue';
 | 
			
		||||
import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue';
 | 
			
		||||
import Progress from '@/scripts/loading';
 | 
			
		||||
import bytes from '@/filters/bytes';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkButton,
 | 
			
		||||
		MkSwitch,
 | 
			
		||||
		XModalWindow,
 | 
			
		||||
		MkDriveFileThumbnail,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	props: {
 | 
			
		||||
		fileId: {
 | 
			
		||||
			required: true,
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	emits: ['closed'],
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			file: null,
 | 
			
		||||
			info: null,
 | 
			
		||||
			isSensitive: false,
 | 
			
		||||
			faTimes, faBookmark, farBookmark, faKey, faSync, faMicrophoneSlash, faSnowflake, faTrashAlt, faExternalLinkSquareAlt
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		this.fetch();
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		async fetch() {
 | 
			
		||||
			Progress.start();
 | 
			
		||||
			this.file = await os.api('drive/files/show', { fileId: this.fileId });
 | 
			
		||||
			this.info = await os.api('admin/drive/show-file', { fileId: this.fileId });
 | 
			
		||||
			this.isSensitive = this.file.isSensitive;
 | 
			
		||||
			Progress.done();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async showUser() {
 | 
			
		||||
			os.popup(await import('./user-dialog.vue'), {
 | 
			
		||||
				userId: this.file.userId
 | 
			
		||||
			}, {}, 'closed');
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async del() {
 | 
			
		||||
			const { canceled } = await os.dialog({
 | 
			
		||||
				type: 'warning',
 | 
			
		||||
				text: this.$t('removeAreYouSure', { x: this.file.name }),
 | 
			
		||||
				showCancelButton: true
 | 
			
		||||
			});
 | 
			
		||||
			if (canceled) return;
 | 
			
		||||
 | 
			
		||||
			os.api('drive/files/delete', {
 | 
			
		||||
				fileId: this.file.id
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				this.$refs.files.removeItem(x => x.id === this.file.id);
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async toggleIsSensitive(v) {
 | 
			
		||||
			await os.api('drive/files/update', { fileId: this.fileId, isSensitive: v });
 | 
			
		||||
			this.isSensitive = v;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		bytes
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.cxqhhsmd {
 | 
			
		||||
	> ._section {
 | 
			
		||||
		> .thumbnail {
 | 
			
		||||
			height: 150px;
 | 
			
		||||
			max-width: 100%;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> .info {
 | 
			
		||||
			text-align: center;
 | 
			
		||||
			margin-top: 8px;
 | 
			
		||||
		}
 | 
			
		||||
		
 | 
			
		||||
		> .rawdata {
 | 
			
		||||
			overflow: auto;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,54 +1,190 @@
 | 
			
		||||
<template>
 | 
			
		||||
<section class="_card">
 | 
			
		||||
	<div class="_title"><fa :icon="faCloud"/> {{ $t('files') }}</div>
 | 
			
		||||
	<div class="_content">
 | 
			
		||||
		<mk-button primary @click="clear()"><fa :icon="faTrashAlt"/> {{ $t('clearCachedFiles') }}</mk-button>
 | 
			
		||||
<div class="xrmjdkdw">
 | 
			
		||||
	<div class="_section">
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<MkButton primary @click="clear()"><Fa :icon="faTrashAlt"/> {{ $t('clearCachedFiles') }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</section>
 | 
			
		||||
 | 
			
		||||
	<div class="_section lookup">
 | 
			
		||||
		<div class="_title"><Fa :icon="faSearch"/> {{ $t('lookup') }}</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<MkInput class="target" v-model:value="q" type="text" @enter="find()">
 | 
			
		||||
				<span>{{ $t('fileIdOrUrl') }}</span>
 | 
			
		||||
			</MkInput>
 | 
			
		||||
			<MkButton @click="find()" primary><Fa :icon="faSearch"/> {{ $t('lookup') }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<div class="_section">
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<div class="inputs" style="display: flex;">
 | 
			
		||||
				<MkSelect v-model:value="origin" style="margin: 0; flex: 1;">
 | 
			
		||||
					<template #label>{{ $t('instance') }}</template>
 | 
			
		||||
					<option value="combined">{{ $t('all') }}</option>
 | 
			
		||||
					<option value="local">{{ $t('local') }}</option>
 | 
			
		||||
					<option value="remote">{{ $t('remote') }}</option>
 | 
			
		||||
				</MkSelect>
 | 
			
		||||
				<MkInput v-model:value="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="pagination.params().origin === 'local'">
 | 
			
		||||
					<span>{{ $t('host') }}</span>
 | 
			
		||||
				</MkInput>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="inputs" style="display: flex; padding-top: 1.2em;">
 | 
			
		||||
				<MkInput v-model:value="type" :debounce="true" type="search" style="margin: 0; flex: 1;">
 | 
			
		||||
					<span>{{ $t('type') }}</span>
 | 
			
		||||
				</MkInput>
 | 
			
		||||
			</div>
 | 
			
		||||
			<MkPagination :pagination="pagination" #default="{items}" class="urempief" ref="files" :auto-margin="false">
 | 
			
		||||
				<button class="file _panel _button _vMargin" v-for="file in items" :key="file.id" @click="show(file, $event)">
 | 
			
		||||
					<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
 | 
			
		||||
					<div class="body">
 | 
			
		||||
						<div>
 | 
			
		||||
							<small style="opacity: 0.7;">{{ file.name }}</small>
 | 
			
		||||
						</div>
 | 
			
		||||
						<div>
 | 
			
		||||
							<MkAcct :user="file.user"/>
 | 
			
		||||
						</div>
 | 
			
		||||
						<div>
 | 
			
		||||
							<span style="margin-right: 1em;">{{ file.type }}</span>
 | 
			
		||||
							<span>{{ bytes(file.size) }}</span>
 | 
			
		||||
						</div>
 | 
			
		||||
						<div>
 | 
			
		||||
							<span>{{ $t('registeredDate') }}: <MkTime :time="file.createdAt" mode="detail"/></span>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</button>
 | 
			
		||||
			</MkPagination>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { faCloud } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faCloud, faSearch } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import MkButton from '../../components/ui/button.vue';
 | 
			
		||||
import MkPagination from '../../components/ui/pagination.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: `${this.$t('files')} | ${this.$t('instance')}`
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import MkInput from '@/components/ui/input.vue';
 | 
			
		||||
import MkSelect from '@/components/ui/select.vue';
 | 
			
		||||
import MkPagination from '@/components/ui/pagination.vue';
 | 
			
		||||
import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue';
 | 
			
		||||
import bytes from '@/filters/bytes';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkButton,
 | 
			
		||||
		MkInput,
 | 
			
		||||
		MkSelect,
 | 
			
		||||
		MkPagination,
 | 
			
		||||
		MkDriveFileThumbnail,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			faTrashAlt, faCloud
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('files'),
 | 
			
		||||
					icon: faCloud
 | 
			
		||||
				}],
 | 
			
		||||
			},
 | 
			
		||||
			q: null,
 | 
			
		||||
			origin: 'local',
 | 
			
		||||
			type: null,
 | 
			
		||||
			searchHost: '',
 | 
			
		||||
			pagination: {
 | 
			
		||||
				endpoint: 'admin/drive/files',
 | 
			
		||||
				limit: 10,
 | 
			
		||||
				params: () => ({
 | 
			
		||||
					type: (this.type && this.type !== '') ? this.type : null,
 | 
			
		||||
					origin: this.origin,
 | 
			
		||||
					hostname: (this.hostname && this.hostname !== '') ? this.hostname : null,
 | 
			
		||||
				}),
 | 
			
		||||
			},
 | 
			
		||||
			faTrashAlt, faCloud, faSearch,
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	watch: {
 | 
			
		||||
		type() {
 | 
			
		||||
			this.$refs.files.reload();
 | 
			
		||||
		},
 | 
			
		||||
		origin() {
 | 
			
		||||
			this.$refs.files.reload();
 | 
			
		||||
		},
 | 
			
		||||
		searchHost() {
 | 
			
		||||
			this.$refs.files.reload();
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		clear() {
 | 
			
		||||
			this.$root.dialog({
 | 
			
		||||
			os.dialog({
 | 
			
		||||
				type: 'warning',
 | 
			
		||||
				text: this.$t('clearCachedFilesConfirm'),
 | 
			
		||||
				showCancelButton: true
 | 
			
		||||
			}).then(({ canceled }) => {
 | 
			
		||||
				if (canceled) return;
 | 
			
		||||
 | 
			
		||||
				this.$root.api('admin/drive/clean-remote-files', {}).then(() => {
 | 
			
		||||
					this.$root.dialog({
 | 
			
		||||
						type: 'success',
 | 
			
		||||
						iconOnly: true, autoClose: true
 | 
			
		||||
					});
 | 
			
		||||
				});
 | 
			
		||||
				os.apiWithDialog('admin/drive/clean-remote-files', {});
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async show(file, ev) {
 | 
			
		||||
			os.popup(await import('./file-dialog.vue'), {
 | 
			
		||||
				fileId: file.id
 | 
			
		||||
			}, {}, 'closed');
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		find() {
 | 
			
		||||
			os.api('admin/drive/show-file', this.q.startsWith('http://') || this.q.startsWith('https://') ? { url: this.q.trim() } : { fileId: this.q.trim() }).then(file => {
 | 
			
		||||
				this.show(file);
 | 
			
		||||
			}).catch(e => {
 | 
			
		||||
				if (e.code === 'NO_SUCH_FILE') {
 | 
			
		||||
					os.dialog({
 | 
			
		||||
						type: 'error',
 | 
			
		||||
						text: this.$t('notFound')
 | 
			
		||||
					});
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		bytes
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.xrmjdkdw {
 | 
			
		||||
	.urempief {
 | 
			
		||||
		margin-top: var(--margin);
 | 
			
		||||
 | 
			
		||||
		> .file {
 | 
			
		||||
			display: flex;
 | 
			
		||||
			width: 100%;
 | 
			
		||||
			box-sizing: border-box;
 | 
			
		||||
			text-align: left;
 | 
			
		||||
			align-items: center;
 | 
			
		||||
 | 
			
		||||
			&:hover {
 | 
			
		||||
				color: var(--accent);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> .thumbnail {
 | 
			
		||||
				width: 128px;
 | 
			
		||||
				height: 128px;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> .body {
 | 
			
		||||
				margin-left: 0.3em;
 | 
			
		||||
				padding: 8px;
 | 
			
		||||
				flex: 1;
 | 
			
		||||
 | 
			
		||||
				@media (max-width: 500px) {
 | 
			
		||||
					font-size: 14px;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										576
									
								
								src/client/pages/instance/index.metrics.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										576
									
								
								src/client/pages/instance/index.metrics.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,576 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
	<MkFolder>
 | 
			
		||||
		<template #header><Fa :icon="faHeartbeat"/> {{ $t('metrics') }}</template>
 | 
			
		||||
		<div class="_section" style="padding: 0 var(--margin);">
 | 
			
		||||
			<div class="_content">
 | 
			
		||||
				<MkContainer :body-togglable="false" class="_vMargin">
 | 
			
		||||
					<template #header><Fa :icon="faMicrochip"/>{{ $t('cpuAndMemory') }}</template>
 | 
			
		||||
					<!--
 | 
			
		||||
					<template #func>
 | 
			
		||||
						<button class="_button" @click="resume" :disabled="!paused"><Fa :icon="faPlay"/></button>
 | 
			
		||||
						<button class="_button" @click="pause" :disabled="paused"><Fa :icon="faPause"/></button>
 | 
			
		||||
					</template>
 | 
			
		||||
					-->
 | 
			
		||||
 | 
			
		||||
					<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
 | 
			
		||||
						<canvas :ref="cpumem"></canvas>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="_content" v-if="serverInfo">
 | 
			
		||||
						<div class="_table">
 | 
			
		||||
							<div class="_row">
 | 
			
		||||
								<div class="_cell"><div class="_label">MEM total</div>{{ bytes(serverInfo.mem.total) }}</div>
 | 
			
		||||
								<div class="_cell"><div class="_label">MEM used</div>{{ bytes(memUsage) }} ({{ (memUsage / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
 | 
			
		||||
								<div class="_cell"><div class="_label">MEM free</div>{{ bytes(serverInfo.mem.total - memUsage) }} ({{ ((serverInfo.mem.total - memUsage) / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
 | 
			
		||||
							</div>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</MkContainer>
 | 
			
		||||
 | 
			
		||||
				<MkContainer :body-togglable="false" class="_vMargin">
 | 
			
		||||
					<template #header><Fa :icon="faHdd"/> {{ $t('disk') }}</template>
 | 
			
		||||
					<!--
 | 
			
		||||
					<template #func>
 | 
			
		||||
						<button class="_button" @click="resume" :disabled="!paused"><Fa :icon="faPlay"/></button>
 | 
			
		||||
						<button class="_button" @click="pause" :disabled="paused"><Fa :icon="faPause"/></button>
 | 
			
		||||
					</template>
 | 
			
		||||
					-->
 | 
			
		||||
 | 
			
		||||
					<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
 | 
			
		||||
						<canvas :ref="disk"></canvas>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="_content" v-if="serverInfo">
 | 
			
		||||
						<div class="_table">
 | 
			
		||||
							<div class="_row">
 | 
			
		||||
								<div class="_cell"><div class="_label">Disk total</div>{{ bytes(serverInfo.fs.total) }}</div>
 | 
			
		||||
								<div class="_cell"><div class="_label">Disk used</div>{{ bytes(serverInfo.fs.used) }} ({{ (serverInfo.fs.used / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
 | 
			
		||||
								<div class="_cell"><div class="_label">Disk free</div>{{ bytes(serverInfo.fs.total - serverInfo.fs.used) }} ({{ ((serverInfo.fs.total - serverInfo.fs.used) / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
 | 
			
		||||
							</div>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</MkContainer>
 | 
			
		||||
 | 
			
		||||
				<MkContainer :body-togglable="false" class="_vMargin">
 | 
			
		||||
					<template #header><Fa :icon="faExchangeAlt"/> {{ $t('network') }}</template>
 | 
			
		||||
					<!--
 | 
			
		||||
					<template #func>
 | 
			
		||||
						<button class="_button" @click="resume" :disabled="!paused"><Fa :icon="faPlay"/></button>
 | 
			
		||||
						<button class="_button" @click="pause" :disabled="paused"><Fa :icon="faPause"/></button>
 | 
			
		||||
					</template>
 | 
			
		||||
					-->
 | 
			
		||||
 | 
			
		||||
					<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
 | 
			
		||||
						<canvas :ref="net"></canvas>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="_content" v-if="serverInfo">
 | 
			
		||||
						<div class="_table">
 | 
			
		||||
							<div class="_row">
 | 
			
		||||
								<div class="_cell"><div class="_label">Interface</div>{{ serverInfo.net.interface }}</div>
 | 
			
		||||
							</div>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</MkContainer>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</MkFolder>
 | 
			
		||||
 | 
			
		||||
	<MkFolder>
 | 
			
		||||
		<template #header><Fa :icon="faClipboardList"/> {{ $t('jobQueue') }}</template>
 | 
			
		||||
 | 
			
		||||
		<div class="vkyrmkwb" :style="{ gridTemplateRows: queueHeight }">
 | 
			
		||||
			<MkContainer :body-togglable="false" :scrollable="true" :resize-base-el="() => $el">
 | 
			
		||||
				<template #header><Fa :icon="faExclamationTriangle"/> {{ $t('delayed') }}</template>
 | 
			
		||||
 | 
			
		||||
				<div class="_content">
 | 
			
		||||
					<div class="_keyValue" v-for="job in jobs" :key="job[0]">
 | 
			
		||||
						<button class="_button" @click="showInstanceInfo(job[0])">{{ job[0] }}</button>
 | 
			
		||||
						<div style="text-align: right;">{{ number(job[1]) }} jobs</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</MkContainer>
 | 
			
		||||
			<XQueue :connection="queueConnection" domain="inbox" ref="queue" class="queue">
 | 
			
		||||
				<template #title><Fa :icon="faExchangeAlt"/> In</template>
 | 
			
		||||
			</XQueue>
 | 
			
		||||
			<XQueue :connection="queueConnection" domain="deliver" class="queue">
 | 
			
		||||
				<template #title><Fa :icon="faExchangeAlt"/> Out</template>
 | 
			
		||||
			</XQueue>
 | 
			
		||||
		</div>
 | 
			
		||||
	</MkFolder>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, markRaw } from 'vue';
 | 
			
		||||
import { faPlay, faPause, faDatabase, faServer, faExchangeAlt, faMicrochip, faHdd, faStream, faTrashAlt, faInfoCircle, faExclamationTriangle, faTachometerAlt, faHeartbeat, faClipboardList } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import Chart from 'chart.js';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import MkSelect from '@/components/ui/select.vue';
 | 
			
		||||
import MkInput from '@/components/ui/input.vue';
 | 
			
		||||
import MkContainer from '@/components/ui/container.vue';
 | 
			
		||||
import MkFolder from '@/components/ui/folder.vue';
 | 
			
		||||
import MkwFederation from '../../widgets/federation.vue';
 | 
			
		||||
import { version, url } from '@/config';
 | 
			
		||||
import bytes from '../../filters/bytes';
 | 
			
		||||
import number from '../../filters/number';
 | 
			
		||||
import MkInstanceInfo from './instance.vue';
 | 
			
		||||
 | 
			
		||||
const alpha = (hex, a) => {
 | 
			
		||||
	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
 | 
			
		||||
	const r = parseInt(result[1], 16);
 | 
			
		||||
	const g = parseInt(result[2], 16);
 | 
			
		||||
	const b = parseInt(result[3], 16);
 | 
			
		||||
	return `rgba(${r}, ${g}, ${b}, ${a})`;
 | 
			
		||||
};
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkButton,
 | 
			
		||||
		MkSelect,
 | 
			
		||||
		MkInput,
 | 
			
		||||
		MkContainer,
 | 
			
		||||
		MkFolder,
 | 
			
		||||
		MkwFederation,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			version,
 | 
			
		||||
			url,
 | 
			
		||||
			stats: null,
 | 
			
		||||
			serverInfo: null,
 | 
			
		||||
			connection: null,
 | 
			
		||||
			queueConnection: os.stream.useSharedConnection('queueStats'),
 | 
			
		||||
			memUsage: 0,
 | 
			
		||||
			chartCpuMem: null,
 | 
			
		||||
			chartNet: null,
 | 
			
		||||
			jobs: [],
 | 
			
		||||
			logs: [],
 | 
			
		||||
			logLevel: 'all',
 | 
			
		||||
			logDomain: '',
 | 
			
		||||
			modLogs: [],
 | 
			
		||||
			dbInfo: null,
 | 
			
		||||
			overviewHeight: '1fr',
 | 
			
		||||
			queueHeight: '1fr',
 | 
			
		||||
			paused: false,
 | 
			
		||||
			faPlay, faPause, faDatabase, faServer, faExchangeAlt, faMicrochip, faHdd, faStream, faTrashAlt, faInfoCircle, faExclamationTriangle, faTachometerAlt, faHeartbeat, faClipboardList,
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	computed: {
 | 
			
		||||
		gridColor() {
 | 
			
		||||
			// TODO: var(--panel)の色が暗いか明るいかで判定する
 | 
			
		||||
			return this.$store.state.device.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	mounted() {
 | 
			
		||||
		this.fetchJobs();
 | 
			
		||||
 | 
			
		||||
		Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
 | 
			
		||||
 | 
			
		||||
		os.api('admin/server-info', {}).then(res => {
 | 
			
		||||
			this.serverInfo = res;
 | 
			
		||||
 | 
			
		||||
			this.connection = os.stream.useSharedConnection('serverStats');
 | 
			
		||||
			this.connection.on('stats', this.onStats);
 | 
			
		||||
			this.connection.on('statsLog', this.onStatsLog);
 | 
			
		||||
			this.connection.send('requestLog', {
 | 
			
		||||
				id: Math.random().toString().substr(2, 8),
 | 
			
		||||
				length: 150
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			this.$nextTick(() => {
 | 
			
		||||
				this.queueConnection.send('requestLog', {
 | 
			
		||||
					id: Math.random().toString().substr(2, 8),
 | 
			
		||||
					length: 200
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	beforeUnmount() {
 | 
			
		||||
		this.connection.off('stats', this.onStats);
 | 
			
		||||
		this.connection.off('statsLog', this.onStatsLog);
 | 
			
		||||
		this.connection.dispose();
 | 
			
		||||
		this.queueConnection.dispose();
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		cpumem(el) {
 | 
			
		||||
			if (this.chartCpuMem != null) return;
 | 
			
		||||
			this.chartCpuMem = markRaw(new Chart(el, {
 | 
			
		||||
				type: 'line',
 | 
			
		||||
				data: {
 | 
			
		||||
					labels: [],
 | 
			
		||||
					datasets: [{
 | 
			
		||||
						label: 'CPU',
 | 
			
		||||
						pointRadius: 0,
 | 
			
		||||
						lineTension: 0,
 | 
			
		||||
						borderWidth: 2,
 | 
			
		||||
						borderColor: '#86b300',
 | 
			
		||||
						backgroundColor: alpha('#86b300', 0.1),
 | 
			
		||||
						data: []
 | 
			
		||||
					}, {
 | 
			
		||||
						label: 'MEM (active)',
 | 
			
		||||
						pointRadius: 0,
 | 
			
		||||
						lineTension: 0,
 | 
			
		||||
						borderWidth: 2,
 | 
			
		||||
						borderColor: '#935dbf',
 | 
			
		||||
						backgroundColor: alpha('#935dbf', 0.02),
 | 
			
		||||
						data: []
 | 
			
		||||
					}, {
 | 
			
		||||
						label: 'MEM (used)',
 | 
			
		||||
						pointRadius: 0,
 | 
			
		||||
						lineTension: 0,
 | 
			
		||||
						borderWidth: 2,
 | 
			
		||||
						borderColor: '#935dbf',
 | 
			
		||||
						borderDash: [5, 5],
 | 
			
		||||
						fill: false,
 | 
			
		||||
						data: []
 | 
			
		||||
					}]
 | 
			
		||||
				},
 | 
			
		||||
				options: {
 | 
			
		||||
					aspectRatio: 3,
 | 
			
		||||
					layout: {
 | 
			
		||||
						padding: {
 | 
			
		||||
							left: 0,
 | 
			
		||||
							right: 0,
 | 
			
		||||
							top: 8,
 | 
			
		||||
							bottom: 0
 | 
			
		||||
						}
 | 
			
		||||
					},
 | 
			
		||||
					legend: {
 | 
			
		||||
						position: 'bottom',
 | 
			
		||||
						labels: {
 | 
			
		||||
							boxWidth: 16,
 | 
			
		||||
						}
 | 
			
		||||
					},
 | 
			
		||||
					scales: {
 | 
			
		||||
						xAxes: [{
 | 
			
		||||
							gridLines: {
 | 
			
		||||
								display: false,
 | 
			
		||||
								color: this.gridColor,
 | 
			
		||||
								zeroLineColor: this.gridColor,
 | 
			
		||||
							},
 | 
			
		||||
							ticks: {
 | 
			
		||||
								display: false,
 | 
			
		||||
							}
 | 
			
		||||
						}],
 | 
			
		||||
						yAxes: [{
 | 
			
		||||
							position: 'right',
 | 
			
		||||
							gridLines: {
 | 
			
		||||
								display: true,
 | 
			
		||||
								color: this.gridColor,
 | 
			
		||||
								zeroLineColor: this.gridColor,
 | 
			
		||||
							},
 | 
			
		||||
							ticks: {
 | 
			
		||||
								display: false,
 | 
			
		||||
								max: 100
 | 
			
		||||
							}
 | 
			
		||||
						}]
 | 
			
		||||
					},
 | 
			
		||||
					tooltips: {
 | 
			
		||||
						intersect: false,
 | 
			
		||||
						mode: 'index',
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}));
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		net(el) {
 | 
			
		||||
			if (this.chartNet != null) return;
 | 
			
		||||
			this.chartNet = markRaw(new Chart(el, {
 | 
			
		||||
				type: 'line',
 | 
			
		||||
				data: {
 | 
			
		||||
					labels: [],
 | 
			
		||||
					datasets: [{
 | 
			
		||||
						label: 'In',
 | 
			
		||||
						pointRadius: 0,
 | 
			
		||||
						lineTension: 0,
 | 
			
		||||
						borderWidth: 2,
 | 
			
		||||
						borderColor: '#94a029',
 | 
			
		||||
						backgroundColor: alpha('#94a029', 0.1),
 | 
			
		||||
						data: []
 | 
			
		||||
					}, {
 | 
			
		||||
						label: 'Out',
 | 
			
		||||
						pointRadius: 0,
 | 
			
		||||
						lineTension: 0,
 | 
			
		||||
						borderWidth: 2,
 | 
			
		||||
						borderColor: '#ff9156',
 | 
			
		||||
						backgroundColor: alpha('#ff9156', 0.1),
 | 
			
		||||
						data: []
 | 
			
		||||
					}]
 | 
			
		||||
				},
 | 
			
		||||
				options: {
 | 
			
		||||
					aspectRatio: 3,
 | 
			
		||||
					layout: {
 | 
			
		||||
						padding: {
 | 
			
		||||
							left: 0,
 | 
			
		||||
							right: 0,
 | 
			
		||||
							top: 8,
 | 
			
		||||
							bottom: 0
 | 
			
		||||
						}
 | 
			
		||||
					},
 | 
			
		||||
					legend: {
 | 
			
		||||
						position: 'bottom',
 | 
			
		||||
						labels: {
 | 
			
		||||
							boxWidth: 16,
 | 
			
		||||
						}
 | 
			
		||||
					},
 | 
			
		||||
					scales: {
 | 
			
		||||
						xAxes: [{
 | 
			
		||||
							gridLines: {
 | 
			
		||||
								display: false,
 | 
			
		||||
								color: this.gridColor,
 | 
			
		||||
								zeroLineColor: this.gridColor,
 | 
			
		||||
							},
 | 
			
		||||
							ticks: {
 | 
			
		||||
								display: false
 | 
			
		||||
							}
 | 
			
		||||
						}],
 | 
			
		||||
						yAxes: [{
 | 
			
		||||
							position: 'right',
 | 
			
		||||
							gridLines: {
 | 
			
		||||
								display: true,
 | 
			
		||||
								color: this.gridColor,
 | 
			
		||||
								zeroLineColor: this.gridColor,
 | 
			
		||||
							},
 | 
			
		||||
							ticks: {
 | 
			
		||||
								display: false,
 | 
			
		||||
							}
 | 
			
		||||
						}]
 | 
			
		||||
					},
 | 
			
		||||
					tooltips: {
 | 
			
		||||
						intersect: false,
 | 
			
		||||
						mode: 'index',
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}));
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		disk(el) {
 | 
			
		||||
			if (this.chartDisk != null) return;
 | 
			
		||||
			this.chartDisk = markRaw(new Chart(el, {
 | 
			
		||||
				type: 'line',
 | 
			
		||||
				data: {
 | 
			
		||||
					labels: [],
 | 
			
		||||
					datasets: [{
 | 
			
		||||
						label: 'Read',
 | 
			
		||||
						pointRadius: 0,
 | 
			
		||||
						lineTension: 0,
 | 
			
		||||
						borderWidth: 2,
 | 
			
		||||
						borderColor: '#94a029',
 | 
			
		||||
						backgroundColor: alpha('#94a029', 0.1),
 | 
			
		||||
						data: []
 | 
			
		||||
					}, {
 | 
			
		||||
						label: 'Write',
 | 
			
		||||
						pointRadius: 0,
 | 
			
		||||
						lineTension: 0,
 | 
			
		||||
						borderWidth: 2,
 | 
			
		||||
						borderColor: '#ff9156',
 | 
			
		||||
						backgroundColor: alpha('#ff9156', 0.1),
 | 
			
		||||
						data: []
 | 
			
		||||
					}]
 | 
			
		||||
				},
 | 
			
		||||
				options: {
 | 
			
		||||
					aspectRatio: 3,
 | 
			
		||||
					layout: {
 | 
			
		||||
						padding: {
 | 
			
		||||
							left: 0,
 | 
			
		||||
							right: 0,
 | 
			
		||||
							top: 8,
 | 
			
		||||
							bottom: 0
 | 
			
		||||
						}
 | 
			
		||||
					},
 | 
			
		||||
					legend: {
 | 
			
		||||
						position: 'bottom',
 | 
			
		||||
						labels: {
 | 
			
		||||
							boxWidth: 16,
 | 
			
		||||
						}
 | 
			
		||||
					},
 | 
			
		||||
					scales: {
 | 
			
		||||
						xAxes: [{
 | 
			
		||||
							gridLines: {
 | 
			
		||||
								display: false,
 | 
			
		||||
								color: this.gridColor,
 | 
			
		||||
								zeroLineColor: this.gridColor,
 | 
			
		||||
							},
 | 
			
		||||
							ticks: {
 | 
			
		||||
								display: false
 | 
			
		||||
							}
 | 
			
		||||
						}],
 | 
			
		||||
						yAxes: [{
 | 
			
		||||
							position: 'right',
 | 
			
		||||
							gridLines: {
 | 
			
		||||
								display: true,
 | 
			
		||||
								color: this.gridColor,
 | 
			
		||||
								zeroLineColor: this.gridColor,
 | 
			
		||||
							},
 | 
			
		||||
							ticks: {
 | 
			
		||||
								display: false,
 | 
			
		||||
							}
 | 
			
		||||
						}]
 | 
			
		||||
					},
 | 
			
		||||
					tooltips: {
 | 
			
		||||
						intersect: false,
 | 
			
		||||
						mode: 'index',
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}));
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async showInstanceInfo(q) {
 | 
			
		||||
			let instance = q;
 | 
			
		||||
			if (typeof q === 'string') {
 | 
			
		||||
				instance = await os.api('federation/show-instance', {
 | 
			
		||||
					host: q
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
			os.popup(MkInstanceInfo, {
 | 
			
		||||
				instance: instance
 | 
			
		||||
			}, {}, 'closed');
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		fetchJobs() {
 | 
			
		||||
			os.api('admin/queue/deliver-delayed', {}).then(jobs => {
 | 
			
		||||
				this.jobs = jobs;
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onStats(stats) {
 | 
			
		||||
			if (this.paused) return;
 | 
			
		||||
 | 
			
		||||
			const cpu = (stats.cpu * 100).toFixed(0);
 | 
			
		||||
			const memActive = (stats.mem.active / this.serverInfo.mem.total * 100).toFixed(0);
 | 
			
		||||
			const memUsed = (stats.mem.used / this.serverInfo.mem.total * 100).toFixed(0);
 | 
			
		||||
			this.memUsage = stats.mem.active;
 | 
			
		||||
 | 
			
		||||
			this.chartCpuMem.data.labels.push('');
 | 
			
		||||
			this.chartCpuMem.data.datasets[0].data.push(cpu);
 | 
			
		||||
			this.chartCpuMem.data.datasets[1].data.push(memActive);
 | 
			
		||||
			this.chartCpuMem.data.datasets[2].data.push(memUsed);
 | 
			
		||||
			this.chartNet.data.labels.push('');
 | 
			
		||||
			this.chartNet.data.datasets[0].data.push(stats.net.rx);
 | 
			
		||||
			this.chartNet.data.datasets[1].data.push(stats.net.tx);
 | 
			
		||||
			this.chartDisk.data.labels.push('');
 | 
			
		||||
			this.chartDisk.data.datasets[0].data.push(stats.fs.r);
 | 
			
		||||
			this.chartDisk.data.datasets[1].data.push(stats.fs.w);
 | 
			
		||||
			if (this.chartCpuMem.data.datasets[0].data.length > 150) {
 | 
			
		||||
				this.chartCpuMem.data.labels.shift();
 | 
			
		||||
				this.chartCpuMem.data.datasets[0].data.shift();
 | 
			
		||||
				this.chartCpuMem.data.datasets[1].data.shift();
 | 
			
		||||
				this.chartCpuMem.data.datasets[2].data.shift();
 | 
			
		||||
				this.chartNet.data.labels.shift();
 | 
			
		||||
				this.chartNet.data.datasets[0].data.shift();
 | 
			
		||||
				this.chartNet.data.datasets[1].data.shift();
 | 
			
		||||
				this.chartDisk.data.labels.shift();
 | 
			
		||||
				this.chartDisk.data.datasets[0].data.shift();
 | 
			
		||||
				this.chartDisk.data.datasets[1].data.shift();
 | 
			
		||||
			}
 | 
			
		||||
			this.chartCpuMem.update();
 | 
			
		||||
			this.chartNet.update();
 | 
			
		||||
			this.chartDisk.update();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onStatsLog(statsLog) {
 | 
			
		||||
			for (const stats of [...statsLog].reverse()) {
 | 
			
		||||
				this.onStats(stats);
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		bytes,
 | 
			
		||||
 | 
			
		||||
		number,
 | 
			
		||||
 | 
			
		||||
		pause() {
 | 
			
		||||
			this.paused = true;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		resume() {
 | 
			
		||||
			this.paused = false;
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.xhexznfu {
 | 
			
		||||
	&.min-width_1000px {
 | 
			
		||||
		.sboqnrfi {
 | 
			
		||||
			display: grid;
 | 
			
		||||
			grid-template-columns: 3.2fr 1fr;
 | 
			
		||||
			grid-template-rows: 1fr;
 | 
			
		||||
			gap: 16px 16px;
 | 
			
		||||
 | 
			
		||||
			> .stats {
 | 
			
		||||
				height: min-content;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> .column {
 | 
			
		||||
				display: flex;
 | 
			
		||||
				flex-direction: column;
 | 
			
		||||
 | 
			
		||||
				> .info {
 | 
			
		||||
					flex-shrink: 0;
 | 
			
		||||
					flex-grow: 0;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				> .db {
 | 
			
		||||
					flex: 1;
 | 
			
		||||
					flex-grow: 0;
 | 
			
		||||
					height: 100%;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				> .fed {
 | 
			
		||||
					flex: 1;
 | 
			
		||||
					flex-grow: 0;
 | 
			
		||||
					height: 100%;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				> *:not(:last-child) {
 | 
			
		||||
					margin-bottom: var(--margin);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.segusily {
 | 
			
		||||
			display: grid;
 | 
			
		||||
			grid-template-columns: 1fr 1fr 1fr;
 | 
			
		||||
			grid-template-rows: 1fr;
 | 
			
		||||
			gap: 16px 16px;
 | 
			
		||||
			padding: 0 16px;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.vkyrmkwb {
 | 
			
		||||
			display: grid;
 | 
			
		||||
			grid-template-columns: 0.5fr 1fr 1fr;
 | 
			
		||||
			grid-template-rows: 1fr;
 | 
			
		||||
			gap: 16px 16px;
 | 
			
		||||
			margin-bottom: var(--margin);
 | 
			
		||||
 | 
			
		||||
			> .queue {
 | 
			
		||||
				height: min-content;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> * {
 | 
			
		||||
				margin-bottom: 0;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.uwuemslx {
 | 
			
		||||
			display: grid;
 | 
			
		||||
			grid-template-columns: 2fr 3fr;
 | 
			
		||||
			grid-template-rows: 1fr;
 | 
			
		||||
			gap: 16px 16px;
 | 
			
		||||
			height: 400px;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.vkyrmkwb {
 | 
			
		||||
		> * {
 | 
			
		||||
			margin-bottom: var(--margin);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,198 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
<mk-container :body-togglable="false">
 | 
			
		||||
	<template #header><slot name="title"></slot></template>
 | 
			
		||||
	<template #func><button class="_button" @click="resume" :disabled="!paused"><fa :icon="faPlay"/></button><button class="_button" @click="pause" :disabled="paused"><fa :icon="faPause"/></button></template>
 | 
			
		||||
 | 
			
		||||
	<div class="_content _table">
 | 
			
		||||
		<div class="_row">
 | 
			
		||||
			<div class="_cell"><div class="_label">Process</div>{{ activeSincePrevTick | number }}</div>
 | 
			
		||||
			<div class="_cell"><div class="_label">Active</div>{{ active | number }}</div>
 | 
			
		||||
			<div class="_cell"><div class="_label">Waiting</div>{{ waiting | number }}</div>
 | 
			
		||||
			<div class="_cell"><div class="_label">Delayed</div>{{ delayed | number }}</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="_content" style="margin-bottom: -8px;">
 | 
			
		||||
		<canvas ref="chart"></canvas>
 | 
			
		||||
	</div>
 | 
			
		||||
</mk-container>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import Chart from 'chart.js';
 | 
			
		||||
import { faPlay, faPause } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import MkContainer from '../../components/ui/container.vue';
 | 
			
		||||
 | 
			
		||||
const alpha = (hex, a) => {
 | 
			
		||||
	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
 | 
			
		||||
	const r = parseInt(result[1], 16);
 | 
			
		||||
	const g = parseInt(result[2], 16);
 | 
			
		||||
	const b = parseInt(result[3], 16);
 | 
			
		||||
	return `rgba(${r}, ${g}, ${b}, ${a})`;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkContainer,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	props: {
 | 
			
		||||
		domain: {
 | 
			
		||||
			required: true
 | 
			
		||||
		},
 | 
			
		||||
		connection: {
 | 
			
		||||
			required: true
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			chart: null,
 | 
			
		||||
			activeSincePrevTick: 0,
 | 
			
		||||
			active: 0,
 | 
			
		||||
			waiting: 0,
 | 
			
		||||
			delayed: 0,
 | 
			
		||||
			paused: false,
 | 
			
		||||
			faPlay, faPause
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	mounted() {
 | 
			
		||||
		// TODO: var(--panel)の色が暗いか明るいかで判定する
 | 
			
		||||
		const gridColor = this.$store.state.device.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
 | 
			
		||||
 | 
			
		||||
		Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
 | 
			
		||||
 | 
			
		||||
		this.chart = new Chart(this.$refs.chart, {
 | 
			
		||||
			type: 'bar',
 | 
			
		||||
			data: {
 | 
			
		||||
				labels: [],
 | 
			
		||||
				datasets: [{
 | 
			
		||||
					label: 'Process',
 | 
			
		||||
					pointRadius: 0,
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					borderWidth: 0,
 | 
			
		||||
					backgroundColor: '#8BC34A',
 | 
			
		||||
					data: []
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Active',
 | 
			
		||||
					pointRadius: 0,
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					borderWidth: 0,
 | 
			
		||||
					backgroundColor: '#03A9F4',
 | 
			
		||||
					data: []
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Waiting',
 | 
			
		||||
					pointRadius: 0,
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					borderWidth: 0,
 | 
			
		||||
					backgroundColor: '#FFC107',
 | 
			
		||||
					data: []
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Delayed',
 | 
			
		||||
					order: -1,
 | 
			
		||||
					type: 'line',
 | 
			
		||||
					pointRadius: 0,
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					borderColor: '#F44336',
 | 
			
		||||
					borderDash: [5, 5],
 | 
			
		||||
					fill: false,
 | 
			
		||||
					data: []
 | 
			
		||||
				}]
 | 
			
		||||
			},
 | 
			
		||||
			options: {
 | 
			
		||||
				aspectRatio: 3,
 | 
			
		||||
				layout: {
 | 
			
		||||
					padding: {
 | 
			
		||||
						left: 0,
 | 
			
		||||
						right: 0,
 | 
			
		||||
						top: 8,
 | 
			
		||||
						bottom: 0
 | 
			
		||||
					}
 | 
			
		||||
				},
 | 
			
		||||
				legend: {
 | 
			
		||||
					position: 'bottom',
 | 
			
		||||
					labels: {
 | 
			
		||||
						boxWidth: 16,
 | 
			
		||||
					}
 | 
			
		||||
				},
 | 
			
		||||
				scales: {
 | 
			
		||||
					xAxes: [{
 | 
			
		||||
						stacked: true,
 | 
			
		||||
						gridLines: {
 | 
			
		||||
							display: false,
 | 
			
		||||
							color: gridColor,
 | 
			
		||||
							zeroLineColor: gridColor,
 | 
			
		||||
						},
 | 
			
		||||
						ticks: {
 | 
			
		||||
							display: false
 | 
			
		||||
						}
 | 
			
		||||
					}],
 | 
			
		||||
					yAxes: [{
 | 
			
		||||
						stacked: true,
 | 
			
		||||
						position: 'right',
 | 
			
		||||
						gridLines: {
 | 
			
		||||
							display: true,
 | 
			
		||||
							color: gridColor,
 | 
			
		||||
							zeroLineColor: gridColor,
 | 
			
		||||
						},
 | 
			
		||||
						ticks: {
 | 
			
		||||
							display: false,
 | 
			
		||||
						}
 | 
			
		||||
					}]
 | 
			
		||||
				},
 | 
			
		||||
				tooltips: {
 | 
			
		||||
					intersect: false,
 | 
			
		||||
					mode: 'index',
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		this.connection.on('stats', this.onStats);
 | 
			
		||||
		this.connection.on('statsLog', this.onStatsLog);
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	beforeDestroy() {
 | 
			
		||||
		this.connection.off('stats', this.onStats);
 | 
			
		||||
		this.connection.off('statsLog', this.onStatsLog);
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		onStats(stats) {
 | 
			
		||||
			if (this.paused) return;
 | 
			
		||||
			this.activeSincePrevTick = stats[this.domain].activeSincePrevTick;
 | 
			
		||||
			this.active = stats[this.domain].active;
 | 
			
		||||
			this.waiting = stats[this.domain].waiting;
 | 
			
		||||
			this.delayed = stats[this.domain].delayed;
 | 
			
		||||
			this.chart.data.labels.push('');
 | 
			
		||||
			this.chart.data.datasets[0].data.push(stats[this.domain].activeSincePrevTick);
 | 
			
		||||
			this.chart.data.datasets[1].data.push(stats[this.domain].active);
 | 
			
		||||
			this.chart.data.datasets[2].data.push(stats[this.domain].waiting);
 | 
			
		||||
			this.chart.data.datasets[3].data.push(stats[this.domain].delayed);
 | 
			
		||||
			if (this.chart.data.datasets[0].data.length > 100) {
 | 
			
		||||
				this.chart.data.labels.shift();
 | 
			
		||||
				this.chart.data.datasets[0].data.shift();
 | 
			
		||||
				this.chart.data.datasets[1].data.shift();
 | 
			
		||||
				this.chart.data.datasets[2].data.shift();
 | 
			
		||||
				this.chart.data.datasets[3].data.shift();
 | 
			
		||||
			}
 | 
			
		||||
			this.chart.update();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onStatsLog(statsLog) {
 | 
			
		||||
			for (const stats of [...statsLog].reverse()) {
 | 
			
		||||
				this.onStats(stats);
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		pause() {
 | 
			
		||||
			this.paused = true;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		resume() {
 | 
			
		||||
			this.paused = false;
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
@@ -1,219 +1,77 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div v-if="meta" class="xhexznfu" v-size="{ min: [1600] }">
 | 
			
		||||
	<portal to="icon"><fa :icon="faServer"/></portal>
 | 
			
		||||
	<portal to="title">{{ $t('instance') }}</portal>
 | 
			
		||||
 | 
			
		||||
	<mk-folder>
 | 
			
		||||
		<template #header><fa :icon="faTachometerAlt"/> {{ $t('overview') }}</template>
 | 
			
		||||
<div v-if="meta" v-show="page === 'index'" class="xhexznfu _section">
 | 
			
		||||
	<MkFolder>
 | 
			
		||||
		<template #header><Fa :icon="faTachometerAlt"/> {{ $t('overview') }}</template>
 | 
			
		||||
 | 
			
		||||
		<div class="sboqnrfi" :style="{ gridTemplateRows: overviewHeight }">
 | 
			
		||||
			<mk-instance-stats :chart-limit="300" :detailed="true" class="stats" ref="stats"/>
 | 
			
		||||
			<MkInstanceStats :chart-limit="300" :detailed="true" class="_vMargin" ref="stats"/>
 | 
			
		||||
 | 
			
		||||
			<div class="column">
 | 
			
		||||
				<mk-container :body-togglable="true" :resize-base-el="() => $el" class="info">
 | 
			
		||||
					<template #header><fa :icon="faInfoCircle"/>{{ $t('instanceInfo') }}</template>
 | 
			
		||||
 | 
			
		||||
					<div class="_content">
 | 
			
		||||
						<div class="_keyValue"><b>Misskey</b><span>v{{ version }}</span></div>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="_content" v-if="serverInfo">
 | 
			
		||||
						<div class="_keyValue"><b>Node.js</b><span>{{ serverInfo.node }}</span></div>
 | 
			
		||||
						<div class="_keyValue"><b>PostgreSQL</b><span>v{{ serverInfo.psql }}</span></div>
 | 
			
		||||
						<div class="_keyValue"><b>Redis</b><span>v{{ serverInfo.redis }}</span></div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</mk-container>
 | 
			
		||||
				
 | 
			
		||||
				<mk-container :body-togglable="true" :scrollable="true" :resize-base-el="() => $el" class="db">
 | 
			
		||||
					<template #header><fa :icon="faDatabase"/>{{ $t('database') }}</template>
 | 
			
		||||
 | 
			
		||||
					<div class="_content" v-if="dbInfo">
 | 
			
		||||
						<table style="border-collapse: collapse; width: 100%;">
 | 
			
		||||
							<tr style="opacity: 0.7;">
 | 
			
		||||
								<th style="text-align: left; padding: 0 8px 8px 0;">Table</th>
 | 
			
		||||
								<th style="text-align: left; padding: 0 8px 8px 0;">Records</th>
 | 
			
		||||
								<th style="text-align: left; padding: 0 0 8px 0;">Size</th>
 | 
			
		||||
							</tr>
 | 
			
		||||
							<tr v-for="table in dbInfo" :key="table[0]">
 | 
			
		||||
								<th style="text-align: left; padding: 0 8px 0 0; word-break: break-all;">{{ table[0] }}</th>
 | 
			
		||||
								<td style="padding: 0 8px 0 0;">{{ table[1].count | number }}</td>
 | 
			
		||||
								<td style="padding: 0; opacity: 0.7;">{{ table[1].size | bytes }}</td>
 | 
			
		||||
							</tr>
 | 
			
		||||
						</table>
 | 
			
		||||
					</div>
 | 
			
		||||
				</mk-container>
 | 
			
		||||
 | 
			
		||||
				<mkw-federation class="fed" :body-togglable="true" :scrollable="true"/>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</mk-folder>
 | 
			
		||||
 | 
			
		||||
	<mk-folder style="margin: var(--margin) 0;">
 | 
			
		||||
		<template #header><fa :icon="faHeartbeat"/> {{ $t('metrics') }}</template>
 | 
			
		||||
 | 
			
		||||
		<div class="segusily">
 | 
			
		||||
			<mk-container :body-togglable="false" :resize-base-el="() => $el">
 | 
			
		||||
				<template #header><fa :icon="faMicrochip"/>{{ $t('cpuAndMemory') }}</template>
 | 
			
		||||
				<template #func><button class="_button" @click="resume" :disabled="!paused"><fa :icon="faPlay"/></button><button class="_button" @click="pause" :disabled="paused"><fa :icon="faPause"/></button></template>
 | 
			
		||||
 | 
			
		||||
				<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
 | 
			
		||||
					<canvas ref="cpumem"></canvas>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div class="_content" v-if="serverInfo">
 | 
			
		||||
					<div class="_table">
 | 
			
		||||
						<!--
 | 
			
		||||
						<div class="_row">
 | 
			
		||||
							<div class="_cell"><div class="_label">CPU</div>{{ serverInfo.cpu.model }}</div>
 | 
			
		||||
						</div>
 | 
			
		||||
						-->
 | 
			
		||||
						<div class="_row">
 | 
			
		||||
							<div class="_cell"><div class="_label">MEM total</div>{{ serverInfo.mem.total | bytes }}</div>
 | 
			
		||||
							<div class="_cell"><div class="_label">MEM used</div>{{ memUsage | bytes }} ({{ (memUsage / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
 | 
			
		||||
							<div class="_cell"><div class="_label">MEM free</div>{{ serverInfo.mem.total - memUsage | bytes }} ({{ ((serverInfo.mem.total - memUsage) / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</mk-container>
 | 
			
		||||
 | 
			
		||||
			<mk-container :body-togglable="false" :resize-base-el="() => $el">
 | 
			
		||||
				<template #header><fa :icon="faHdd"/> {{ $t('disk') }}</template>
 | 
			
		||||
				<template #func><button class="_button" @click="resume" :disabled="!paused"><fa :icon="faPlay"/></button><button class="_button" @click="pause" :disabled="paused"><fa :icon="faPause"/></button></template>
 | 
			
		||||
 | 
			
		||||
				<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
 | 
			
		||||
					<canvas ref="disk"></canvas>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div class="_content" v-if="serverInfo">
 | 
			
		||||
					<div class="_table">
 | 
			
		||||
						<div class="_row">
 | 
			
		||||
							<div class="_cell"><div class="_label">Disk total</div>{{ serverInfo.fs.total | bytes }}</div>
 | 
			
		||||
							<div class="_cell"><div class="_label">Disk used</div>{{ serverInfo.fs.used | bytes }} ({{ (serverInfo.fs.used / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
 | 
			
		||||
							<div class="_cell"><div class="_label">Disk free</div>{{ serverInfo.fs.total - serverInfo.fs.used | bytes }} ({{ ((serverInfo.fs.total - serverInfo.fs.used) / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</mk-container>
 | 
			
		||||
 | 
			
		||||
			<mk-container :body-togglable="false" :resize-base-el="() => $el">
 | 
			
		||||
				<template #header><fa :icon="faExchangeAlt"/> {{ $t('network') }}</template>
 | 
			
		||||
				<template #func><button class="_button" @click="resume" :disabled="!paused"><fa :icon="faPlay"/></button><button class="_button" @click="pause" :disabled="paused"><fa :icon="faPause"/></button></template>
 | 
			
		||||
 | 
			
		||||
				<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
 | 
			
		||||
					<canvas ref="net"></canvas>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div class="_content" v-if="serverInfo">
 | 
			
		||||
					<div class="_table">
 | 
			
		||||
						<div class="_row">
 | 
			
		||||
							<div class="_cell"><div class="_label">Interface</div>{{ serverInfo.net.interface }}</div>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</mk-container>
 | 
			
		||||
		</div>
 | 
			
		||||
	</mk-folder>
 | 
			
		||||
 | 
			
		||||
	<mk-folder>
 | 
			
		||||
		<template #header><fa :icon="faClipboardList"/> {{ $t('jobQueue') }}</template>
 | 
			
		||||
 | 
			
		||||
		<div class="vkyrmkwb" :style="{ gridTemplateRows: queueHeight }">
 | 
			
		||||
			<mk-container :body-togglable="false" :scrollable="true" :resize-base-el="() => $el">
 | 
			
		||||
				<template #header><fa :icon="faExclamationTriangle"/> {{ $t('delayed') }}</template>
 | 
			
		||||
			<MkContainer :body-togglable="true" class="_vMargin">
 | 
			
		||||
				<template #header><Fa :icon="faInfoCircle"/>{{ $t('instanceInfo') }}</template>
 | 
			
		||||
 | 
			
		||||
				<div class="_content">
 | 
			
		||||
					<div class="_keyValue" v-for="job in jobs" :key="job[0]">
 | 
			
		||||
						<button class="_button" @click="showInstanceInfo(job[0])">{{ job[0] }}</button>
 | 
			
		||||
						<div style="text-align: right;">{{ job[1] | number }} jobs</div>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="_keyValue"><b>Misskey</b><span>v{{ version }}</span></div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</mk-container>
 | 
			
		||||
			<x-queue :connection="queueConnection" domain="inbox" ref="queue" class="queue">
 | 
			
		||||
				<template #title><fa :icon="faExchangeAlt"/> In</template>
 | 
			
		||||
			</x-queue>
 | 
			
		||||
			<x-queue :connection="queueConnection" domain="deliver" class="queue">
 | 
			
		||||
				<template #title><fa :icon="faExchangeAlt"/> Out</template>
 | 
			
		||||
			</x-queue>
 | 
			
		||||
				<div class="_content" v-if="serverInfo">
 | 
			
		||||
					<div class="_keyValue"><b>Node.js</b><span>{{ serverInfo.node }}</span></div>
 | 
			
		||||
					<div class="_keyValue"><b>PostgreSQL</b><span>v{{ serverInfo.psql }}</span></div>
 | 
			
		||||
					<div class="_keyValue"><b>Redis</b><span>v{{ serverInfo.redis }}</span></div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</MkContainer>
 | 
			
		||||
			
 | 
			
		||||
			<MkContainer :body-togglable="true" :scrollable="true" class="_vMargin" style="height: 300px;">
 | 
			
		||||
				<template #header><Fa :icon="faDatabase"/>{{ $t('database') }}</template>
 | 
			
		||||
 | 
			
		||||
				<div class="_content" v-if="dbInfo">
 | 
			
		||||
					<table style="border-collapse: collapse; width: 100%;">
 | 
			
		||||
						<tr style="opacity: 0.7;">
 | 
			
		||||
							<th style="text-align: left; padding: 0 8px 8px 0;">Table</th>
 | 
			
		||||
							<th style="text-align: left; padding: 0 8px 8px 0;">Records</th>
 | 
			
		||||
							<th style="text-align: left; padding: 0 0 8px 0;">Size</th>
 | 
			
		||||
						</tr>
 | 
			
		||||
						<tr v-for="table in dbInfo" :key="table[0]">
 | 
			
		||||
							<th style="text-align: left; padding: 0 8px 0 0; word-break: break-all;">{{ table[0] }}</th>
 | 
			
		||||
							<td style="padding: 0 8px 0 0;">{{ number(table[1].count) }}</td>
 | 
			
		||||
							<td style="padding: 0; opacity: 0.7;">{{ bytes(table[1].size) }}</td>
 | 
			
		||||
						</tr>
 | 
			
		||||
					</table>
 | 
			
		||||
				</div>
 | 
			
		||||
			</MkContainer>
 | 
			
		||||
		</div>
 | 
			
		||||
	</mk-folder>
 | 
			
		||||
	</MkFolder>
 | 
			
		||||
</div>
 | 
			
		||||
<div v-if="page === 'logs'" class="_section">
 | 
			
		||||
	<MkFolder>
 | 
			
		||||
		<template #header><Fa :icon="faStream"/> {{ $t('logs') }}</template>
 | 
			
		||||
 | 
			
		||||
	<mk-folder>
 | 
			
		||||
		<template #header><fa :icon="faStream"/> {{ $t('logs') }}</template>
 | 
			
		||||
 | 
			
		||||
		<div class="uwuemslx">
 | 
			
		||||
			<mk-container :body-togglable="false" :resize-base-el="() => $el">
 | 
			
		||||
				<template #header><fa :icon="faInfoCircle"/>{{ $t('') }}</template>
 | 
			
		||||
 | 
			
		||||
				<div class="_content">
 | 
			
		||||
					<div class="_keyValue" v-for="log in modLogs">
 | 
			
		||||
						<b>{{ log.type }}</b><span>by {{ log.user.username }}</span><mk-time :time="log.createdAt" style="opacity: 0.7;"/>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</mk-container>
 | 
			
		||||
 | 
			
		||||
			<section class="_card logs">
 | 
			
		||||
				<div class="_title"><fa :icon="faStream"/> {{ $t('serverLogs') }}</div>
 | 
			
		||||
				<div class="_content">
 | 
			
		||||
					<div class="_inputs">
 | 
			
		||||
						<mk-input v-model="logDomain" :debounce="true">
 | 
			
		||||
							<span>{{ $t('domain') }}</span>
 | 
			
		||||
						</mk-input>
 | 
			
		||||
						<mk-select v-model="logLevel">
 | 
			
		||||
							<template #label>{{ $t('level') }}</template>
 | 
			
		||||
							<option value="all">{{ $t('levels.all') }}</option>
 | 
			
		||||
							<option value="info">{{ $t('levels.info') }}</option>
 | 
			
		||||
							<option value="success">{{ $t('levels.success') }}</option>
 | 
			
		||||
							<option value="warning">{{ $t('levels.warning') }}</option>
 | 
			
		||||
							<option value="error">{{ $t('levels.error') }}</option>
 | 
			
		||||
							<option value="debug">{{ $t('levels.debug') }}</option>
 | 
			
		||||
						</mk-select>
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					<div class="logs">
 | 
			
		||||
						<code v-for="log in logs" :key="log.id" :class="log.level">
 | 
			
		||||
							<details>
 | 
			
		||||
								<summary><mk-time :time="log.createdAt"/> [{{ log.domain.join('.') }}] {{ log.message }}</summary>
 | 
			
		||||
								<vue-json-pretty v-if="log.data" :data="log.data"></vue-json-pretty>
 | 
			
		||||
							</details>
 | 
			
		||||
						</code>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div class="_footer">
 | 
			
		||||
					<mk-button @click="deleteAllLogs()" primary><fa :icon="faTrashAlt"/> {{ $t('deleteAll') }}</mk-button>
 | 
			
		||||
				</div>
 | 
			
		||||
			</section>
 | 
			
		||||
		<div class="_keyValue" v-for="log in modLogs">
 | 
			
		||||
			<b>{{ log.type }}</b><span>by {{ log.user.username }}</span><MkTime :time="log.createdAt" style="opacity: 0.7;"/>
 | 
			
		||||
		</div>
 | 
			
		||||
	</mk-folder>
 | 
			
		||||
	</MkFolder>
 | 
			
		||||
</div>
 | 
			
		||||
<div v-if="page === 'metrics'">
 | 
			
		||||
	<XMetrics/>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { computed, defineComponent, markRaw } from 'vue';
 | 
			
		||||
import { faPlay, faPause, faDatabase, faServer, faExchangeAlt, faMicrochip, faHdd, faStream, faTrashAlt, faInfoCircle, faExclamationTriangle, faTachometerAlt, faHeartbeat, faClipboardList } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import Chart from 'chart.js';
 | 
			
		||||
import VueJsonPretty from 'vue-json-pretty';
 | 
			
		||||
import MkInstanceStats from '../../components/instance-stats.vue';
 | 
			
		||||
import MkButton from '../../components/ui/button.vue';
 | 
			
		||||
import MkSelect from '../../components/ui/select.vue';
 | 
			
		||||
import MkInput from '../../components/ui/input.vue';
 | 
			
		||||
import MkContainer from '../../components/ui/container.vue';
 | 
			
		||||
import MkFolder from '../../components/ui/folder.vue';
 | 
			
		||||
import MkwFederation from '../../widgets/federation.vue';
 | 
			
		||||
import { version, url } from '../../config';
 | 
			
		||||
import XQueue from './index.queue-chart.vue';
 | 
			
		||||
import MkInstanceStats from '@/components/instance-stats.vue';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import MkSelect from '@/components/ui/select.vue';
 | 
			
		||||
import MkInput from '@/components/ui/input.vue';
 | 
			
		||||
import MkContainer from '@/components/ui/container.vue';
 | 
			
		||||
import MkFolder from '@/components/ui/folder.vue';
 | 
			
		||||
import { version, url } from '@/config';
 | 
			
		||||
import bytes from '../../filters/bytes';
 | 
			
		||||
import number from '../../filters/number';
 | 
			
		||||
import MkInstanceInfo from './instance.vue';
 | 
			
		||||
import XMetrics from './index.metrics.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
const alpha = (hex, a) => {
 | 
			
		||||
	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
 | 
			
		||||
	const r = parseInt(result[1], 16);
 | 
			
		||||
	const g = parseInt(result[2], 16);
 | 
			
		||||
	const b = parseInt(result[3], 16);
 | 
			
		||||
	return `rgba(${r}, ${g}, ${b}, ${a})`;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: this.$t('instance') as string
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkInstanceStats,
 | 
			
		||||
		MkButton,
 | 
			
		||||
@@ -221,31 +79,43 @@ export default Vue.extend({
 | 
			
		||||
		MkInput,
 | 
			
		||||
		MkContainer,
 | 
			
		||||
		MkFolder,
 | 
			
		||||
		MkwFederation,
 | 
			
		||||
		XQueue,
 | 
			
		||||
		XMetrics,
 | 
			
		||||
		VueJsonPretty,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					id: 'index',
 | 
			
		||||
					title: null,
 | 
			
		||||
					tooltip: this.$t('instance'),
 | 
			
		||||
					icon: faServer,
 | 
			
		||||
					onClick: () => { this.page = 'index'; },
 | 
			
		||||
					selected: computed(() => this.page === 'index')
 | 
			
		||||
				}, {
 | 
			
		||||
					id: 'metrics',
 | 
			
		||||
					title: null,
 | 
			
		||||
					tooltip: this.$t('metrics'),
 | 
			
		||||
					icon: faHeartbeat,
 | 
			
		||||
					onClick: () => { this.page = 'metrics'; },
 | 
			
		||||
					selected: computed(() => this.page === 'metrics')
 | 
			
		||||
				}, {
 | 
			
		||||
					id: 'logs',
 | 
			
		||||
					title: null,
 | 
			
		||||
					tooltip: this.$t('logs'),
 | 
			
		||||
					icon: faStream,
 | 
			
		||||
					onClick: () => { this.page = 'logs'; },
 | 
			
		||||
					selected: computed(() => this.page === 'logs')
 | 
			
		||||
				}]
 | 
			
		||||
			},
 | 
			
		||||
			page: 'index',
 | 
			
		||||
			version,
 | 
			
		||||
			url,
 | 
			
		||||
			stats: null,
 | 
			
		||||
			serverInfo: null,
 | 
			
		||||
			connection: null,
 | 
			
		||||
			queueConnection: this.$root.stream.useSharedConnection('queueStats'),
 | 
			
		||||
			memUsage: 0,
 | 
			
		||||
			chartCpuMem: null,
 | 
			
		||||
			chartNet: null,
 | 
			
		||||
			jobs: [],
 | 
			
		||||
			logs: [],
 | 
			
		||||
			logLevel: 'all',
 | 
			
		||||
			logDomain: '',
 | 
			
		||||
			modLogs: [],
 | 
			
		||||
			dbInfo: null,
 | 
			
		||||
			overviewHeight: '1fr',
 | 
			
		||||
			queueHeight: '1fr',
 | 
			
		||||
			paused: false,
 | 
			
		||||
			faPlay, faPause, faDatabase, faServer, faExchangeAlt, faMicrochip, faHdd, faStream, faTrashAlt, faInfoCircle, faExclamationTriangle, faTachometerAlt, faHeartbeat, faClipboardList,
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
@@ -256,509 +126,47 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	watch: {
 | 
			
		||||
		logLevel() {
 | 
			
		||||
			this.logs = [];
 | 
			
		||||
			this.fetchLogs();
 | 
			
		||||
		},
 | 
			
		||||
		logDomain() {
 | 
			
		||||
			this.logs = [];
 | 
			
		||||
			this.fetchLogs();
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		this.$store.commit('setFullView', true);
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	mounted() {
 | 
			
		||||
		this.fetchLogs();
 | 
			
		||||
		this.fetchJobs();
 | 
			
		||||
		this.fetchModLogs();
 | 
			
		||||
 | 
			
		||||
		// TODO: var(--panel)の色が暗いか明るいかで判定する
 | 
			
		||||
		const gridColor = this.$store.state.device.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
 | 
			
		||||
	
 | 
			
		||||
		Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
 | 
			
		||||
 | 
			
		||||
		this.chartCpuMem = new Chart(this.$refs.cpumem, {
 | 
			
		||||
			type: 'line',
 | 
			
		||||
			data: {
 | 
			
		||||
				labels: [],
 | 
			
		||||
				datasets: [{
 | 
			
		||||
					label: 'CPU',
 | 
			
		||||
					pointRadius: 0,
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					borderColor: '#86b300',
 | 
			
		||||
					backgroundColor: alpha('#86b300', 0.1),
 | 
			
		||||
					data: []
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'MEM (active)',
 | 
			
		||||
					pointRadius: 0,
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					borderColor: '#935dbf',
 | 
			
		||||
					backgroundColor: alpha('#935dbf', 0.02),
 | 
			
		||||
					data: []
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'MEM (used)',
 | 
			
		||||
					pointRadius: 0,
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					borderColor: '#935dbf',
 | 
			
		||||
					borderDash: [5, 5],
 | 
			
		||||
					fill: false,
 | 
			
		||||
					data: []
 | 
			
		||||
				}]
 | 
			
		||||
			},
 | 
			
		||||
			options: {
 | 
			
		||||
				aspectRatio: 3,
 | 
			
		||||
				layout: {
 | 
			
		||||
					padding: {
 | 
			
		||||
						left: 0,
 | 
			
		||||
						right: 0,
 | 
			
		||||
						top: 8,
 | 
			
		||||
						bottom: 0
 | 
			
		||||
					}
 | 
			
		||||
				},
 | 
			
		||||
				legend: {
 | 
			
		||||
					position: 'bottom',
 | 
			
		||||
					labels: {
 | 
			
		||||
						boxWidth: 16,
 | 
			
		||||
					}
 | 
			
		||||
				},
 | 
			
		||||
				scales: {
 | 
			
		||||
					xAxes: [{
 | 
			
		||||
						gridLines: {
 | 
			
		||||
							display: false,
 | 
			
		||||
							color: gridColor,
 | 
			
		||||
							zeroLineColor: gridColor,
 | 
			
		||||
						},
 | 
			
		||||
						ticks: {
 | 
			
		||||
							display: false,
 | 
			
		||||
						}
 | 
			
		||||
					}],
 | 
			
		||||
					yAxes: [{
 | 
			
		||||
						position: 'right',
 | 
			
		||||
						gridLines: {
 | 
			
		||||
							display: true,
 | 
			
		||||
							color: gridColor,
 | 
			
		||||
							zeroLineColor: gridColor,
 | 
			
		||||
						},
 | 
			
		||||
						ticks: {
 | 
			
		||||
							display: false,
 | 
			
		||||
							max: 100
 | 
			
		||||
						}
 | 
			
		||||
					}]
 | 
			
		||||
				},
 | 
			
		||||
				tooltips: {
 | 
			
		||||
					intersect: false,
 | 
			
		||||
					mode: 'index',
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		this.chartNet = new Chart(this.$refs.net, {
 | 
			
		||||
			type: 'line',
 | 
			
		||||
			data: {
 | 
			
		||||
				labels: [],
 | 
			
		||||
				datasets: [{
 | 
			
		||||
					label: 'In',
 | 
			
		||||
					pointRadius: 0,
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					borderColor: '#94a029',
 | 
			
		||||
					backgroundColor: alpha('#94a029', 0.1),
 | 
			
		||||
					data: []
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Out',
 | 
			
		||||
					pointRadius: 0,
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					borderColor: '#ff9156',
 | 
			
		||||
					backgroundColor: alpha('#ff9156', 0.1),
 | 
			
		||||
					data: []
 | 
			
		||||
				}]
 | 
			
		||||
			},
 | 
			
		||||
			options: {
 | 
			
		||||
				aspectRatio: 3,
 | 
			
		||||
				layout: {
 | 
			
		||||
					padding: {
 | 
			
		||||
						left: 0,
 | 
			
		||||
						right: 0,
 | 
			
		||||
						top: 8,
 | 
			
		||||
						bottom: 0
 | 
			
		||||
					}
 | 
			
		||||
				},
 | 
			
		||||
				legend: {
 | 
			
		||||
					position: 'bottom',
 | 
			
		||||
					labels: {
 | 
			
		||||
						boxWidth: 16,
 | 
			
		||||
					}
 | 
			
		||||
				},
 | 
			
		||||
				scales: {
 | 
			
		||||
					xAxes: [{
 | 
			
		||||
						gridLines: {
 | 
			
		||||
							display: false,
 | 
			
		||||
							color: gridColor,
 | 
			
		||||
							zeroLineColor: gridColor,
 | 
			
		||||
						},
 | 
			
		||||
						ticks: {
 | 
			
		||||
							display: false
 | 
			
		||||
						}
 | 
			
		||||
					}],
 | 
			
		||||
					yAxes: [{
 | 
			
		||||
						position: 'right',
 | 
			
		||||
						gridLines: {
 | 
			
		||||
							display: true,
 | 
			
		||||
							color: gridColor,
 | 
			
		||||
							zeroLineColor: gridColor,
 | 
			
		||||
						},
 | 
			
		||||
						ticks: {
 | 
			
		||||
							display: false,
 | 
			
		||||
						}
 | 
			
		||||
					}]
 | 
			
		||||
				},
 | 
			
		||||
				tooltips: {
 | 
			
		||||
					intersect: false,
 | 
			
		||||
					mode: 'index',
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		this.chartDisk = new Chart(this.$refs.disk, {
 | 
			
		||||
			type: 'line',
 | 
			
		||||
			data: {
 | 
			
		||||
				labels: [],
 | 
			
		||||
				datasets: [{
 | 
			
		||||
					label: 'Read',
 | 
			
		||||
					pointRadius: 0,
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					borderColor: '#94a029',
 | 
			
		||||
					backgroundColor: alpha('#94a029', 0.1),
 | 
			
		||||
					data: []
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Write',
 | 
			
		||||
					pointRadius: 0,
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					borderColor: '#ff9156',
 | 
			
		||||
					backgroundColor: alpha('#ff9156', 0.1),
 | 
			
		||||
					data: []
 | 
			
		||||
				}]
 | 
			
		||||
			},
 | 
			
		||||
			options: {
 | 
			
		||||
				aspectRatio: 3,
 | 
			
		||||
				layout: {
 | 
			
		||||
					padding: {
 | 
			
		||||
						left: 0,
 | 
			
		||||
						right: 0,
 | 
			
		||||
						top: 8,
 | 
			
		||||
						bottom: 0
 | 
			
		||||
					}
 | 
			
		||||
				},
 | 
			
		||||
				legend: {
 | 
			
		||||
					position: 'bottom',
 | 
			
		||||
					labels: {
 | 
			
		||||
						boxWidth: 16,
 | 
			
		||||
					}
 | 
			
		||||
				},
 | 
			
		||||
				scales: {
 | 
			
		||||
					xAxes: [{
 | 
			
		||||
						gridLines: {
 | 
			
		||||
							display: false,
 | 
			
		||||
							color: gridColor,
 | 
			
		||||
							zeroLineColor: gridColor,
 | 
			
		||||
						},
 | 
			
		||||
						ticks: {
 | 
			
		||||
							display: false
 | 
			
		||||
						}
 | 
			
		||||
					}],
 | 
			
		||||
					yAxes: [{
 | 
			
		||||
						position: 'right',
 | 
			
		||||
						gridLines: {
 | 
			
		||||
							display: true,
 | 
			
		||||
							color: gridColor,
 | 
			
		||||
							zeroLineColor: gridColor,
 | 
			
		||||
						},
 | 
			
		||||
						ticks: {
 | 
			
		||||
							display: false,
 | 
			
		||||
						}
 | 
			
		||||
					}]
 | 
			
		||||
				},
 | 
			
		||||
				tooltips: {
 | 
			
		||||
					intersect: false,
 | 
			
		||||
					mode: 'index',
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		this.$root.api('admin/server-info', {}).then(res => {
 | 
			
		||||
		os.api('admin/server-info', {}).then(res => {
 | 
			
		||||
			this.serverInfo = res;
 | 
			
		||||
 | 
			
		||||
			this.connection = this.$root.stream.useSharedConnection('serverStats');
 | 
			
		||||
			this.connection.on('stats', this.onStats);
 | 
			
		||||
			this.connection.on('statsLog', this.onStatsLog);
 | 
			
		||||
			this.connection.send('requestLog', {
 | 
			
		||||
				id: Math.random().toString().substr(2, 8),
 | 
			
		||||
				length: 150
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			this.$nextTick(() => {
 | 
			
		||||
				this.queueConnection.send('requestLog', {
 | 
			
		||||
					id: Math.random().toString().substr(2, 8),
 | 
			
		||||
					length: 200
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		this.$root.api('admin/get-table-stats', {}).then(res => {
 | 
			
		||||
		os.api('admin/get-table-stats', {}).then(res => {
 | 
			
		||||
			this.dbInfo = Object.entries(res).sort((a, b) => b[1].size - a[1].size);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		this.$nextTick(() => {
 | 
			
		||||
			new ResizeObserver((entries, observer) => {
 | 
			
		||||
				if (this.$refs.stats && this.$refs.stats.$el) {
 | 
			
		||||
					this.overviewHeight = this.$refs.stats.$el.offsetHeight + 'px';
 | 
			
		||||
				}
 | 
			
		||||
			}).observe(this.$refs.stats.$el);
 | 
			
		||||
 | 
			
		||||
			new ResizeObserver((entries, observer) => {
 | 
			
		||||
				if (this.$refs.queue && this.$refs.queue.$el) {
 | 
			
		||||
					this.queueHeight = this.$refs.queue.$el.offsetHeight + 'px';
 | 
			
		||||
				}
 | 
			
		||||
			}).observe(this.$refs.queue.$el);
 | 
			
		||||
		});
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	beforeDestroy() {
 | 
			
		||||
		this.connection.off('stats', this.onStats);
 | 
			
		||||
		this.connection.off('statsLog', this.onStatsLog);
 | 
			
		||||
		this.connection.dispose();
 | 
			
		||||
		this.queueConnection.dispose();
 | 
			
		||||
		this.$store.commit('setFullView', false);
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		async showInstanceInfo(q) {
 | 
			
		||||
			let instance = q;
 | 
			
		||||
			if (typeof q === 'string') {
 | 
			
		||||
				instance = await this.$root.api('federation/show-instance', {
 | 
			
		||||
				instance = await os.api('federation/show-instance', {
 | 
			
		||||
					host: q
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
			this.$root.new(MkInstanceInfo, {
 | 
			
		||||
			os.popup(MkInstanceInfo, {
 | 
			
		||||
				instance: instance
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		fetchLogs() {
 | 
			
		||||
			this.$root.api('admin/logs', {
 | 
			
		||||
				level: this.logLevel === 'all' ? null : this.logLevel,
 | 
			
		||||
				domain: this.logDomain === '' ? null : this.logDomain,
 | 
			
		||||
				limit: 30
 | 
			
		||||
			}).then(logs => {
 | 
			
		||||
				this.logs = logs.reverse();
 | 
			
		||||
			});
 | 
			
		||||
			}, {}, 'closed');
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		fetchJobs() {
 | 
			
		||||
			this.$root.api('admin/queue/deliver-delayed', {}).then(jobs => {
 | 
			
		||||
			os.api('admin/queue/deliver-delayed', {}).then(jobs => {
 | 
			
		||||
				this.jobs = jobs;
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		fetchModLogs() {
 | 
			
		||||
			this.$root.api('admin/show-moderation-logs', {}).then(logs => {
 | 
			
		||||
			os.api('admin/show-moderation-logs', {}).then(logs => {
 | 
			
		||||
				this.modLogs = logs;
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		deleteAllLogs() {
 | 
			
		||||
			this.$root.api('admin/delete-logs').then(() => {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
					type: 'success',
 | 
			
		||||
					iconOnly: true, autoClose: true
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
		bytes,
 | 
			
		||||
 | 
			
		||||
		onStats(stats) {
 | 
			
		||||
			if (this.paused) return;
 | 
			
		||||
 | 
			
		||||
			const cpu = (stats.cpu * 100).toFixed(0);
 | 
			
		||||
			const memActive = (stats.mem.active / this.serverInfo.mem.total * 100).toFixed(0);
 | 
			
		||||
			const memUsed = (stats.mem.used / this.serverInfo.mem.total * 100).toFixed(0);
 | 
			
		||||
			this.memUsage = stats.mem.active;
 | 
			
		||||
 | 
			
		||||
			this.chartCpuMem.data.labels.push('');
 | 
			
		||||
			this.chartCpuMem.data.datasets[0].data.push(cpu);
 | 
			
		||||
			this.chartCpuMem.data.datasets[1].data.push(memActive);
 | 
			
		||||
			this.chartCpuMem.data.datasets[2].data.push(memUsed);
 | 
			
		||||
			this.chartNet.data.labels.push('');
 | 
			
		||||
			this.chartNet.data.datasets[0].data.push(stats.net.rx);
 | 
			
		||||
			this.chartNet.data.datasets[1].data.push(stats.net.tx);
 | 
			
		||||
			this.chartDisk.data.labels.push('');
 | 
			
		||||
			this.chartDisk.data.datasets[0].data.push(stats.fs.r);
 | 
			
		||||
			this.chartDisk.data.datasets[1].data.push(stats.fs.w);
 | 
			
		||||
			if (this.chartCpuMem.data.datasets[0].data.length > 150) {
 | 
			
		||||
				this.chartCpuMem.data.labels.shift();
 | 
			
		||||
				this.chartCpuMem.data.datasets[0].data.shift();
 | 
			
		||||
				this.chartCpuMem.data.datasets[1].data.shift();
 | 
			
		||||
				this.chartCpuMem.data.datasets[2].data.shift();
 | 
			
		||||
				this.chartNet.data.labels.shift();
 | 
			
		||||
				this.chartNet.data.datasets[0].data.shift();
 | 
			
		||||
				this.chartNet.data.datasets[1].data.shift();
 | 
			
		||||
				this.chartDisk.data.labels.shift();
 | 
			
		||||
				this.chartDisk.data.datasets[0].data.shift();
 | 
			
		||||
				this.chartDisk.data.datasets[1].data.shift();
 | 
			
		||||
			}
 | 
			
		||||
			this.chartCpuMem.update();
 | 
			
		||||
			this.chartNet.update();
 | 
			
		||||
			this.chartDisk.update();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onStatsLog(statsLog) {
 | 
			
		||||
			for (const stats of [...statsLog].reverse()) {
 | 
			
		||||
				this.onStats(stats);
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		pause() {
 | 
			
		||||
			this.paused = true;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		resume() {
 | 
			
		||||
			this.paused = false;
 | 
			
		||||
		},
 | 
			
		||||
		number,
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.xhexznfu {
 | 
			
		||||
	&.min-width_1600px {
 | 
			
		||||
		.sboqnrfi {
 | 
			
		||||
			display: grid;
 | 
			
		||||
			grid-template-columns: 3.2fr 1fr;
 | 
			
		||||
			grid-template-rows: 1fr;
 | 
			
		||||
			gap: 16px 16px;
 | 
			
		||||
 | 
			
		||||
			> .stats {
 | 
			
		||||
				height: min-content;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> .column {
 | 
			
		||||
				display: flex;
 | 
			
		||||
				flex-direction: column;
 | 
			
		||||
 | 
			
		||||
				> .info {
 | 
			
		||||
					flex-shrink: 0;
 | 
			
		||||
					flex-grow: 0;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				> .db {
 | 
			
		||||
					flex: 1;
 | 
			
		||||
					flex-grow: 0;
 | 
			
		||||
					height: 100%;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				> .fed {
 | 
			
		||||
					flex: 1;
 | 
			
		||||
					flex-grow: 0;
 | 
			
		||||
					height: 100%;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				> *:not(:last-child) {
 | 
			
		||||
					margin-bottom: var(--margin);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.segusily {
 | 
			
		||||
			display: grid;
 | 
			
		||||
			grid-template-columns: 1fr 1fr 1fr;
 | 
			
		||||
			grid-template-rows: 1fr;
 | 
			
		||||
			gap: 16px 16px;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.vkyrmkwb {
 | 
			
		||||
			display: grid;
 | 
			
		||||
			grid-template-columns: 0.5fr 1fr 1fr;
 | 
			
		||||
			grid-template-rows: 1fr;
 | 
			
		||||
			gap: 16px 16px;
 | 
			
		||||
			margin-bottom: var(--margin);
 | 
			
		||||
 | 
			
		||||
			> .queue {
 | 
			
		||||
				height: min-content;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> * {
 | 
			
		||||
				margin-bottom: 0;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.uwuemslx {
 | 
			
		||||
			display: grid;
 | 
			
		||||
			grid-template-columns: 2fr 3fr;
 | 
			
		||||
			grid-template-rows: 1fr;
 | 
			
		||||
			gap: 16px 16px;
 | 
			
		||||
			height: 400px;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.vkyrmkwb {
 | 
			
		||||
		> * {
 | 
			
		||||
			margin-bottom: var(--margin);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .stats {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		justify-content: space-between;
 | 
			
		||||
		flex-wrap: wrap;
 | 
			
		||||
		margin: calc(0px - var(--margin) / 2);
 | 
			
		||||
		margin-bottom: calc(var(--margin) / 2);
 | 
			
		||||
 | 
			
		||||
		> div {
 | 
			
		||||
			flex: 1 0 213px;
 | 
			
		||||
			margin: calc(var(--margin) / 2);
 | 
			
		||||
			box-sizing: border-box;
 | 
			
		||||
			padding: 16px;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .logs {
 | 
			
		||||
		> ._content {
 | 
			
		||||
			> .logs {
 | 
			
		||||
				padding: 8px;
 | 
			
		||||
				background: #000;
 | 
			
		||||
				color: #fff;
 | 
			
		||||
				font-size: 0.9em;
 | 
			
		||||
 | 
			
		||||
				> code {
 | 
			
		||||
					display: block;
 | 
			
		||||
 | 
			
		||||
					&.error {
 | 
			
		||||
						color: #f00;
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					&.warning {
 | 
			
		||||
						color: #ff0;
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					&.success {
 | 
			
		||||
						color: #0f0;
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					&.debug {
 | 
			
		||||
						opacity: 0.7;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,13 @@
 | 
			
		||||
<template>
 | 
			
		||||
<x-window @closed="() => { $emit('closed'); destroyDom(); }" :no-padding="true" :width="520" :height="500">
 | 
			
		||||
<XModalWindow ref="dialog"
 | 
			
		||||
	:width="520"
 | 
			
		||||
	:height="500"
 | 
			
		||||
	@close="$refs.dialog.close()"
 | 
			
		||||
	@closed="$emit('closed')"
 | 
			
		||||
>
 | 
			
		||||
	<template #header>{{ instance.host }}</template>
 | 
			
		||||
	<div class="mk-instance-info">
 | 
			
		||||
		<div class="_table">
 | 
			
		||||
		<div class="_table section">
 | 
			
		||||
			<div class="_row">
 | 
			
		||||
				<div class="_cell">
 | 
			
		||||
					<div class="_label">{{ $t('software') }}</div>
 | 
			
		||||
@@ -14,47 +19,47 @@
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_table data">
 | 
			
		||||
		<div class="_table data section">
 | 
			
		||||
			<div class="_row">
 | 
			
		||||
				<div class="_cell">
 | 
			
		||||
					<div class="_label">{{ $t('registeredAt') }}</div>
 | 
			
		||||
					<div class="_data">{{ new Date(instance.caughtAt).toLocaleString() }} (<mk-time :time="instance.caughtAt"/>)</div>
 | 
			
		||||
					<div class="_data">{{ new Date(instance.caughtAt).toLocaleString() }} (<MkTime :time="instance.caughtAt"/>)</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="_row">
 | 
			
		||||
				<div class="_cell">
 | 
			
		||||
					<div class="_label">{{ $t('following') }}</div>
 | 
			
		||||
					<button class="_data _textButton" @click="showFollowing()">{{ instance.followingCount | number }}</button>
 | 
			
		||||
					<button class="_data _textButton" @click="showFollowing()">{{ number(instance.followingCount) }}</button>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div class="_cell">
 | 
			
		||||
					<div class="_label">{{ $t('followers') }}</div>
 | 
			
		||||
					<button class="_data _textButton" @click="showFollowers()">{{ instance.followersCount | number }}</button>
 | 
			
		||||
					<button class="_data _textButton" @click="showFollowers()">{{ number(instance.followersCount) }}</button>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="_row">
 | 
			
		||||
				<div class="_cell">
 | 
			
		||||
					<div class="_label">{{ $t('users') }}</div>
 | 
			
		||||
					<button class="_data _textButton" @click="showUsers()">{{ instance.usersCount | number }}</button>
 | 
			
		||||
					<button class="_data _textButton" @click="showUsers()">{{ number(instance.usersCount) }}</button>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div class="_cell">
 | 
			
		||||
					<div class="_label">{{ $t('notes') }}</div>
 | 
			
		||||
					<div class="_data">{{ instance.notesCount | number }}</div>
 | 
			
		||||
					<div class="_data">{{ number(instance.notesCount) }}</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="_row">
 | 
			
		||||
				<div class="_cell">
 | 
			
		||||
					<div class="_label">{{ $t('files') }}</div>
 | 
			
		||||
					<div class="_data">{{ instance.driveFiles | number }}</div>
 | 
			
		||||
					<div class="_data">{{ number(instance.driveFiles) }}</div>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div class="_cell">
 | 
			
		||||
					<div class="_label">{{ $t('storageUsage') }}</div>
 | 
			
		||||
					<div class="_data">{{ instance.driveUsage | bytes }}</div>
 | 
			
		||||
					<div class="_data">{{ bytes(instance.driveUsage) }}</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="_row">
 | 
			
		||||
				<div class="_cell">
 | 
			
		||||
					<div class="_label">{{ $t('latestRequestSentAt') }}</div>
 | 
			
		||||
					<div class="_data"><mk-time v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></div>
 | 
			
		||||
					<div class="_data"><MkTime v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></div>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div class="_cell">
 | 
			
		||||
					<div class="_label">{{ $t('latestStatus') }}</div>
 | 
			
		||||
@@ -64,7 +69,7 @@
 | 
			
		||||
			<div class="_row">
 | 
			
		||||
				<div class="_cell">
 | 
			
		||||
					<div class="_label">{{ $t('latestRequestReceivedAt') }}</div>
 | 
			
		||||
					<div class="_data"><mk-time v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></div>
 | 
			
		||||
					<div class="_data"><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
@@ -72,7 +77,7 @@
 | 
			
		||||
			<div class="header">
 | 
			
		||||
				<span class="label">{{ $t('charts') }}</span>
 | 
			
		||||
				<div class="selects">
 | 
			
		||||
					<mk-select v-model="chartSrc" style="margin: 0; flex: 1;">
 | 
			
		||||
					<MkSelect v-model:value="chartSrc" style="margin: 0; flex: 1;">
 | 
			
		||||
						<option value="requests">{{ $t('_instanceCharts.requests') }}</option>
 | 
			
		||||
						<option value="users">{{ $t('_instanceCharts.users') }}</option>
 | 
			
		||||
						<option value="users-total">{{ $t('_instanceCharts.usersTotal') }}</option>
 | 
			
		||||
@@ -84,49 +89,52 @@
 | 
			
		||||
						<option value="drive-usage-total">{{ $t('_instanceCharts.cacheSizeTotal') }}</option>
 | 
			
		||||
						<option value="drive-files">{{ $t('_instanceCharts.files') }}</option>
 | 
			
		||||
						<option value="drive-files-total">{{ $t('_instanceCharts.filesTotal') }}</option>
 | 
			
		||||
					</mk-select>
 | 
			
		||||
					<mk-select v-model="chartSpan" style="margin: 0;">
 | 
			
		||||
					</MkSelect>
 | 
			
		||||
					<MkSelect v-model:value="chartSpan" style="margin: 0;">
 | 
			
		||||
						<option value="hour">{{ $t('perHour') }}</option>
 | 
			
		||||
						<option value="day">{{ $t('perDay') }}</option>
 | 
			
		||||
					</mk-select>
 | 
			
		||||
					</MkSelect>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="chart">
 | 
			
		||||
				<canvas ref="chart"></canvas>
 | 
			
		||||
				<canvas :ref="setChart"></canvas>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="operations">
 | 
			
		||||
		<div class="operations section">
 | 
			
		||||
			<span class="label">{{ $t('operations') }}</span>
 | 
			
		||||
			<mk-switch v-model="isSuspended" class="switch">{{ $t('stopActivityDelivery') }}</mk-switch>
 | 
			
		||||
			<mk-switch :value="isBlocked" class="switch" @change="changeBlock">{{ $t('blockThisInstance') }}</mk-switch>
 | 
			
		||||
			<MkSwitch v-model:value="isSuspended" class="switch">{{ $t('stopActivityDelivery') }}</MkSwitch>
 | 
			
		||||
			<MkSwitch :value="isBlocked" class="switch" @update:value="changeBlock">{{ $t('blockThisInstance') }}</MkSwitch>
 | 
			
		||||
			<details>
 | 
			
		||||
				<summary>{{ $t('deleteAllFiles') }}</summary>
 | 
			
		||||
				<mk-button @click="deleteAllFiles()" style="margin: 0.5em 0 0.5em 0;"><fa :icon="faTrashAlt"/> {{ $t('deleteAllFiles') }}</mk-button>
 | 
			
		||||
				<MkButton @click="deleteAllFiles()" style="margin: 0.5em 0 0.5em 0;"><Fa :icon="faTrashAlt"/> {{ $t('deleteAllFiles') }}</MkButton>
 | 
			
		||||
			</details>
 | 
			
		||||
			<details>
 | 
			
		||||
				<summary>{{ $t('removeAllFollowing') }}</summary>
 | 
			
		||||
				<mk-button @click="removeAllFollowing()" style="margin: 0.5em 0 0.5em 0;"><fa :icon="faMinusCircle"/> {{ $t('removeAllFollowing') }}</mk-button>
 | 
			
		||||
				<mk-info warn>{{ $t('removeAllFollowingDescription', { host: instance.host }) }}</mk-info>
 | 
			
		||||
				<MkButton @click="removeAllFollowing()" style="margin: 0.5em 0 0.5em 0;"><Fa :icon="faMinusCircle"/> {{ $t('removeAllFollowing') }}</MkButton>
 | 
			
		||||
				<MkInfo warn>{{ $t('removeAllFollowingDescription', { host: instance.host }) }}</MkInfo>
 | 
			
		||||
			</details>
 | 
			
		||||
		</div>
 | 
			
		||||
		<details class="metadata">
 | 
			
		||||
		<details class="metadata section">
 | 
			
		||||
			<summary class="label">{{ $t('metadata') }}</summary>
 | 
			
		||||
			<pre><code>{{ JSON.stringify(instance, null, 2) }}</code></pre>
 | 
			
		||||
		</details>
 | 
			
		||||
	</div>
 | 
			
		||||
</x-window>
 | 
			
		||||
</XModalWindow>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import Chart from 'chart.js';
 | 
			
		||||
import { faTimes, faCrosshairs, faCloudDownloadAlt, faCloudUploadAlt, faUsers, faPencilAlt, faFileImage, faDatabase, faTrafficLight, faLongArrowAltUp, faLongArrowAltDown, faMinusCircle, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import XWindow from '../../components/window.vue';
 | 
			
		||||
import MkUsersDialog from '../../components/users-dialog.vue';
 | 
			
		||||
import MkSelect from '../../components/ui/select.vue';
 | 
			
		||||
import MkButton from '../../components/ui/button.vue';
 | 
			
		||||
import MkSwitch from '../../components/ui/switch.vue';
 | 
			
		||||
import MkInfo from '../../components/ui/info.vue';
 | 
			
		||||
import XModalWindow from '@/components/ui/modal-window.vue';
 | 
			
		||||
import MkUsersDialog from '@/components/users-dialog.vue';
 | 
			
		||||
import MkSelect from '@/components/ui/select.vue';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import MkSwitch from '@/components/ui/switch.vue';
 | 
			
		||||
import MkInfo from '@/components/ui/info.vue';
 | 
			
		||||
import bytes from '../../filters/bytes';
 | 
			
		||||
import number from '../../filters/number';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
const chartLimit = 90;
 | 
			
		||||
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
 | 
			
		||||
@@ -139,9 +147,9 @@ const alpha = hex => {
 | 
			
		||||
	return `rgba(${r}, ${g}, ${b}, 0.1)`;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XWindow,
 | 
			
		||||
		XModalWindow,
 | 
			
		||||
		MkSelect,
 | 
			
		||||
		MkButton,
 | 
			
		||||
		MkSwitch,
 | 
			
		||||
@@ -155,10 +163,13 @@ export default Vue.extend({
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	emits: ['closed'],
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			isSuspended: this.instance.isSuspended,
 | 
			
		||||
			now: null,
 | 
			
		||||
			canvas: null,
 | 
			
		||||
			chart: null,
 | 
			
		||||
			chartInstance: null,
 | 
			
		||||
			chartSrc: 'requests',
 | 
			
		||||
@@ -199,13 +210,13 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		isBlocked() {
 | 
			
		||||
			return this.meta && this.meta.blockedHosts.includes(this.instance.host);
 | 
			
		||||
			return this.meta && this.meta.blockedHosts && this.meta.blockedHosts.includes(this.instance.host);
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	watch: {
 | 
			
		||||
		isSuspended() {
 | 
			
		||||
			this.$root.api('admin/federation/update-instance', {
 | 
			
		||||
			os.api('admin/federation/update-instance', {
 | 
			
		||||
				host: this.instance.host,
 | 
			
		||||
				isSuspended: this.isSuspended
 | 
			
		||||
			});
 | 
			
		||||
@@ -220,12 +231,12 @@ export default Vue.extend({
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	async created() {	
 | 
			
		||||
	async created() {
 | 
			
		||||
		this.now = new Date();
 | 
			
		||||
 | 
			
		||||
		const [perHour, perDay] = await Promise.all([
 | 
			
		||||
			this.$root.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'hour' }),
 | 
			
		||||
			this.$root.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'day' }),
 | 
			
		||||
			os.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'hour' }),
 | 
			
		||||
			os.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'day' }),
 | 
			
		||||
		]);
 | 
			
		||||
 | 
			
		||||
		const chart = {
 | 
			
		||||
@@ -239,8 +250,12 @@ export default Vue.extend({
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		setChart(el) {
 | 
			
		||||
			this.canvas = el;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		changeBlock(e) {
 | 
			
		||||
			this.$root.api('admin/update-meta', {
 | 
			
		||||
			os.api('admin/update-meta', {
 | 
			
		||||
				blockedHosts: this.isBlocked ? this.meta.blockedHosts.concat([this.instance.host]) : this.meta.blockedHosts.filter(x => x !== this.instance.host)
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
@@ -250,24 +265,14 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		removeAllFollowing() {
 | 
			
		||||
			this.$root.api('admin/federation/remove-all-following', {
 | 
			
		||||
			os.apiWithDialog('admin/federation/remove-all-following', {
 | 
			
		||||
				host: this.instance.host
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
					type: 'success',
 | 
			
		||||
					iconOnly: true, autoClose: true
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		deleteAllFiles() {
 | 
			
		||||
			this.$root.api('admin/federation/delete-all-files', {
 | 
			
		||||
			os.apiWithDialog('admin/federation/delete-all-files', {
 | 
			
		||||
				host: this.instance.host
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
					type: 'success',
 | 
			
		||||
					iconOnly: true, autoClose: true
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
@@ -277,7 +282,7 @@ export default Vue.extend({
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
 | 
			
		||||
			this.chartInstance = new Chart(this.$refs.chart, {
 | 
			
		||||
			this.chartInstance = new Chart(this.canvas, {
 | 
			
		||||
				type: 'line',
 | 
			
		||||
				data: {
 | 
			
		||||
					labels: new Array(chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(),
 | 
			
		||||
@@ -436,7 +441,7 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		showFollowing() {
 | 
			
		||||
			this.$root.new(MkUsersDialog, {
 | 
			
		||||
			os.modal(MkUsersDialog, {
 | 
			
		||||
				title: this.$t('instanceFollowing'),
 | 
			
		||||
				pagination: {
 | 
			
		||||
					endpoint: 'federation/following',
 | 
			
		||||
@@ -450,7 +455,7 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		showFollowers() {
 | 
			
		||||
			this.$root.new(MkUsersDialog, {
 | 
			
		||||
			os.modal(MkUsersDialog, {
 | 
			
		||||
				title: this.$t('instanceFollowers'),
 | 
			
		||||
				pagination: {
 | 
			
		||||
					endpoint: 'federation/followers',
 | 
			
		||||
@@ -464,7 +469,7 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		showUsers() {
 | 
			
		||||
			this.$root.new(MkUsersDialog, {
 | 
			
		||||
			os.modal(MkUsersDialog, {
 | 
			
		||||
				title: this.$t('instanceUsers'),
 | 
			
		||||
				pagination: {
 | 
			
		||||
					endpoint: 'federation/users',
 | 
			
		||||
@@ -474,7 +479,11 @@ export default Vue.extend({
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		bytes,
 | 
			
		||||
 | 
			
		||||
		number
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
@@ -483,34 +492,21 @@ export default Vue.extend({
 | 
			
		||||
.mk-instance-info {
 | 
			
		||||
	overflow: auto;
 | 
			
		||||
 | 
			
		||||
	> ._table {
 | 
			
		||||
		padding: 0 32px;
 | 
			
		||||
	> .section {
 | 
			
		||||
		padding: 16px 32px;
 | 
			
		||||
 | 
			
		||||
		@media (max-width: 500px) {
 | 
			
		||||
			padding: 0 16px;
 | 
			
		||||
			padding: 8px 16px;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .data {
 | 
			
		||||
		margin-top: 16px;
 | 
			
		||||
		padding-top: 16px;
 | 
			
		||||
		border-top: solid 1px var(--divider);
 | 
			
		||||
 | 
			
		||||
		@media (max-width: 500px) {
 | 
			
		||||
			margin-top: 8px;
 | 
			
		||||
			padding-top: 8px;
 | 
			
		||||
		&:not(:first-child) {
 | 
			
		||||
			border-top: solid 1px var(--divider);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .chart {
 | 
			
		||||
		margin-top: 16px;
 | 
			
		||||
		padding-top: 16px;
 | 
			
		||||
		border-top: solid 1px var(--divider);
 | 
			
		||||
 | 
			
		||||
		@media (max-width: 500px) {
 | 
			
		||||
			margin-top: 8px;
 | 
			
		||||
			padding-top: 8px;
 | 
			
		||||
		}
 | 
			
		||||
		padding: 16px 0 12px 0;
 | 
			
		||||
 | 
			
		||||
		> .header {
 | 
			
		||||
			padding: 0 32px;
 | 
			
		||||
@@ -539,15 +535,6 @@ export default Vue.extend({
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .operations {
 | 
			
		||||
		padding: 16px 32px 16px 32px;
 | 
			
		||||
		margin-top: 8px;
 | 
			
		||||
		border-top: solid 1px var(--divider);
 | 
			
		||||
 | 
			
		||||
		@media (max-width: 500px) {
 | 
			
		||||
			padding: 8px 16px 8px 16px;
 | 
			
		||||
			margin-top: 0;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> .label {
 | 
			
		||||
			font-size: 80%;
 | 
			
		||||
			opacity: 0.7;
 | 
			
		||||
@@ -559,13 +546,6 @@ export default Vue.extend({
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .metadata {
 | 
			
		||||
		padding: 16px 32px 16px 32px;
 | 
			
		||||
		border-top: solid 1px var(--divider);
 | 
			
		||||
 | 
			
		||||
		@media (max-width: 500px) {
 | 
			
		||||
			padding: 8px 16px 8px 16px;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> .label {
 | 
			
		||||
			font-size: 80%;
 | 
			
		||||
			opacity: 0.7;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										95
									
								
								src/client/pages/instance/logs.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								src/client/pages/instance/logs.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,95 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="_section">
 | 
			
		||||
	<div class="_inputs">
 | 
			
		||||
		<MkInput v-model:value="logDomain" :debounce="true">
 | 
			
		||||
			<span>{{ $t('domain') }}</span>
 | 
			
		||||
		</MkInput>
 | 
			
		||||
		<MkSelect v-model:value="logLevel">
 | 
			
		||||
			<template #label>{{ $t('level') }}</template>
 | 
			
		||||
			<option value="all">{{ $t('levels.all') }}</option>
 | 
			
		||||
			<option value="info">{{ $t('levels.info') }}</option>
 | 
			
		||||
			<option value="success">{{ $t('levels.success') }}</option>
 | 
			
		||||
			<option value="warning">{{ $t('levels.warning') }}</option>
 | 
			
		||||
			<option value="error">{{ $t('levels.error') }}</option>
 | 
			
		||||
			<option value="debug">{{ $t('levels.debug') }}</option>
 | 
			
		||||
		</MkSelect>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<div class="logs">
 | 
			
		||||
		<code v-for="log in logs" :key="log.id" :class="log.level">
 | 
			
		||||
			<details>
 | 
			
		||||
				<summary><MkTime :time="log.createdAt"/> [{{ log.domain.join('.') }}] {{ log.message }}</summary>
 | 
			
		||||
				<!--<vue-json-pretty v-if="log.data" :data="log.data"></vue-json-pretty>-->
 | 
			
		||||
			</details>
 | 
			
		||||
		</code>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<MkButton @click="deleteAllLogs()" primary><Fa :icon="faTrashAlt"/> {{ $t('deleteAll') }}</MkButton>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faStream } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import MkInput from '@/components/ui/input.vue';
 | 
			
		||||
import MkSelect from '@/components/ui/select.vue';
 | 
			
		||||
import MkTextarea from '@/components/ui/textarea.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkButton,
 | 
			
		||||
		MkInput,
 | 
			
		||||
		MkSelect,
 | 
			
		||||
		MkTextarea,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('serverLogs'),
 | 
			
		||||
					icon: faStream
 | 
			
		||||
				}]
 | 
			
		||||
			},
 | 
			
		||||
			logs: [],
 | 
			
		||||
			logLevel: 'all',
 | 
			
		||||
			logDomain: '',
 | 
			
		||||
			faTrashAlt,
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	watch: {
 | 
			
		||||
		logLevel() {
 | 
			
		||||
			this.logs = [];
 | 
			
		||||
			this.fetchLogs();
 | 
			
		||||
		},
 | 
			
		||||
		logDomain() {
 | 
			
		||||
			this.logs = [];
 | 
			
		||||
			this.fetchLogs();
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		this.fetchLogs();
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		fetchLogs() {
 | 
			
		||||
			os.api('admin/logs', {
 | 
			
		||||
				level: this.logLevel === 'all' ? null : this.logLevel,
 | 
			
		||||
				domain: this.logDomain === '' ? null : this.logDomain,
 | 
			
		||||
				limit: 30
 | 
			
		||||
			}).then(logs => {
 | 
			
		||||
				this.logs = logs.reverse();
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		deleteAllLogs() {
 | 
			
		||||
			os.apiWithDialog('admin/delete-logs');
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
@@ -1,12 +1,12 @@
 | 
			
		||||
<template>
 | 
			
		||||
<section class="_card">
 | 
			
		||||
<section class="_section">
 | 
			
		||||
	<div class="_title"><slot name="title"></slot></div>
 | 
			
		||||
	<div class="_content _table">
 | 
			
		||||
		<div class="_row">
 | 
			
		||||
			<div class="_cell"><div class="_label">Process</div>{{ activeSincePrevTick | number }}</div>
 | 
			
		||||
			<div class="_cell"><div class="_label">Active</div>{{ active | number }}</div>
 | 
			
		||||
			<div class="_cell"><div class="_label">Waiting</div>{{ waiting | number }}</div>
 | 
			
		||||
			<div class="_cell"><div class="_label">Delayed</div>{{ delayed | number }}</div>
 | 
			
		||||
			<div class="_cell"><div class="_label">Process</div>{{ number(activeSincePrevTick) }}</div>
 | 
			
		||||
			<div class="_cell"><div class="_label">Active</div>{{ number(active) }}</div>
 | 
			
		||||
			<div class="_cell"><div class="_label">Waiting</div>{{ number(waiting) }}</div>
 | 
			
		||||
			<div class="_cell"><div class="_label">Delayed</div>{{ number(delayed) }}</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="_content" style="margin-bottom: -8px;">
 | 
			
		||||
@@ -16,7 +16,7 @@
 | 
			
		||||
		<div v-if="jobs.length > 0">
 | 
			
		||||
			<div v-for="job in jobs" :key="job[0]">
 | 
			
		||||
				<span>{{ job[0] }}</span>
 | 
			
		||||
				<span style="margin-left: 8px; opacity: 0.7;">({{ job[1] | number }} jobs)</span>
 | 
			
		||||
				<span style="margin-left: 8px; opacity: 0.7;">({{ number(job[1]) }} jobs)</span>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<span v-else style="opacity: 0.5;">{{ $t('noJobs') }}</span>
 | 
			
		||||
@@ -25,8 +25,9 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import Chart from 'chart.js';
 | 
			
		||||
import number from '../../filters/number';
 | 
			
		||||
 | 
			
		||||
const alpha = (hex, a) => {
 | 
			
		||||
	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
 | 
			
		||||
@@ -35,8 +36,9 @@ const alpha = (hex, a) => {
 | 
			
		||||
	const b = parseInt(result[3], 16);
 | 
			
		||||
	return `rgba(${r}, ${g}, ${b}, ${a})`;
 | 
			
		||||
};
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	props: {
 | 
			
		||||
		domain: {
 | 
			
		||||
			required: true
 | 
			
		||||
@@ -154,7 +156,7 @@ export default Vue.extend({
 | 
			
		||||
		this.connection.on('statsLog', this.onStatsLog);
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	beforeDestroy() {
 | 
			
		||||
	beforeUnmount() {
 | 
			
		||||
		this.connection.off('stats', this.onStats);
 | 
			
		||||
		this.connection.off('statsLog', this.onStatsLog);
 | 
			
		||||
	},
 | 
			
		||||
@@ -187,10 +189,12 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		fetchJobs() {
 | 
			
		||||
			this.$root.api(this.domain === 'inbox' ? 'admin/queue/inbox-delayed' : this.domain === 'deliver' ? 'admin/queue/deliver-delayed' : null, {}).then(jobs => {
 | 
			
		||||
			os.api(this.domain === 'inbox' ? 'admin/queue/inbox-delayed' : this.domain === 'deliver' ? 'admin/queue/deliver-delayed' : null, {}).then(jobs => {
 | 
			
		||||
				this.jobs = jobs;
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		number
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,36 +1,28 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
	<portal to="icon"><fa :icon="faExchangeAlt"/></portal>
 | 
			
		||||
	<portal to="title">{{ $t('jobQueue') }}</portal>
 | 
			
		||||
 | 
			
		||||
	<x-queue :connection="connection" domain="inbox">
 | 
			
		||||
		<template #title><fa :icon="faExchangeAlt"/> In</template>
 | 
			
		||||
	</x-queue>
 | 
			
		||||
	<x-queue :connection="connection" domain="deliver">
 | 
			
		||||
		<template #title><fa :icon="faExchangeAlt"/> Out</template>
 | 
			
		||||
	</x-queue>
 | 
			
		||||
	<section class="_card">
 | 
			
		||||
	<XQueue :connection="connection" domain="inbox">
 | 
			
		||||
		<template #title><Fa :icon="faExchangeAlt"/> In</template>
 | 
			
		||||
	</XQueue>
 | 
			
		||||
	<XQueue :connection="connection" domain="deliver">
 | 
			
		||||
		<template #title><Fa :icon="faExchangeAlt"/> Out</template>
 | 
			
		||||
	</XQueue>
 | 
			
		||||
	<section class="_section">
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-button @click="clear()"><fa :icon="faTrashAlt"/> {{ $t('clearQueue') }}</mk-button>
 | 
			
		||||
			<MkButton @click="clear()"><Fa :icon="faTrashAlt"/> {{ $t('clearQueue') }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faExchangeAlt } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import MkButton from '../../components/ui/button.vue';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import XQueue from './queue.chart.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: `${this.$t('jobQueue')} | ${this.$t('instance')}`
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkButton,
 | 
			
		||||
		XQueue,
 | 
			
		||||
@@ -38,7 +30,13 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			connection: this.$root.stream.useSharedConnection('queueStats'),
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('jobQueue'),
 | 
			
		||||
					icon: faExchangeAlt,
 | 
			
		||||
				}],
 | 
			
		||||
			},
 | 
			
		||||
			connection: os.stream.useSharedConnection('queueStats'),
 | 
			
		||||
			faExchangeAlt, faTrashAlt
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
@@ -52,13 +50,13 @@ export default Vue.extend({
 | 
			
		||||
		});
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	beforeDestroy() {
 | 
			
		||||
	beforeUnmount() {
 | 
			
		||||
		this.connection.dispose();
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		clear() {
 | 
			
		||||
			this.$root.dialog({
 | 
			
		||||
			os.dialog({
 | 
			
		||||
				type: 'warning',
 | 
			
		||||
				title: this.$t('clearQueueConfirmTitle'),
 | 
			
		||||
				text: this.$t('clearQueueConfirmText'),
 | 
			
		||||
@@ -66,12 +64,7 @@ export default Vue.extend({
 | 
			
		||||
			}).then(({ canceled }) => {
 | 
			
		||||
				if (canceled) return;
 | 
			
		||||
 | 
			
		||||
				this.$root.api('admin/queue/clear', {}).then(() => {
 | 
			
		||||
					this.$root.dialog({
 | 
			
		||||
						type: 'success',
 | 
			
		||||
						iconOnly: true, autoClose: true
 | 
			
		||||
					});
 | 
			
		||||
				});
 | 
			
		||||
				os.apiWithDialog('admin/queue/clear', {});
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,43 +1,35 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="relaycxt">
 | 
			
		||||
	<portal to="icon"><fa :icon="faProjectDiagram"/></portal>
 | 
			
		||||
	<portal to="title">{{ $t('relays') }}</portal>
 | 
			
		||||
 | 
			
		||||
	<section class="_card _vMargin add">
 | 
			
		||||
		<div class="_title"><fa :icon="faPlus"/> {{ $t('addRelay') }}</div>
 | 
			
		||||
	<section class="_section add">
 | 
			
		||||
		<div class="_title"><Fa :icon="faPlus"/> {{ $t('addRelay') }}</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-input v-model="inbox">
 | 
			
		||||
			<MkInput v-model:value="inbox">
 | 
			
		||||
				<span>{{ $t('inboxUrl') }}</span>
 | 
			
		||||
			</mk-input>
 | 
			
		||||
			<mk-button @click="add(inbox)" primary><fa :icon="faPlus"/> {{ $t('add') }}</mk-button>
 | 
			
		||||
			</MkInput>
 | 
			
		||||
			<MkButton @click="add(inbox)" primary><Fa :icon="faPlus"/> {{ $t('add') }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<section class="_card _vMargin relays">
 | 
			
		||||
		<div class="_title"><fa :icon="faProjectDiagram"/> {{ $t('addedRelays') }}</div>
 | 
			
		||||
	<section class="_section relays">
 | 
			
		||||
		<div class="_title"><Fa :icon="faProjectDiagram"/> {{ $t('addedRelays') }}</div>
 | 
			
		||||
		<div class="_content relay" v-for="relay in relays" :key="relay.inbox">
 | 
			
		||||
			<div>{{ relay.inbox }}</div>
 | 
			
		||||
			<div>{{ $t(`_relayStatus.${relay.status}`) }}</div>
 | 
			
		||||
			<mk-button class="button" inline @click="remove(relay.inbox)"><fa :icon="faTrashAlt"/> {{ $t('remove') }}</mk-button>
 | 
			
		||||
			<MkButton class="button" inline @click="remove(relay.inbox)"><Fa :icon="faTrashAlt"/> {{ $t('remove') }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faPlus, faProjectDiagram } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faSave, faTrashAlt } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import MkButton from '../../components/ui/button.vue';
 | 
			
		||||
import MkInput from '../../components/ui/input.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: this.$t('relays') as string
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import MkInput from '@/components/ui/input.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkButton,
 | 
			
		||||
		MkInput,
 | 
			
		||||
@@ -45,6 +37,12 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('relays'),
 | 
			
		||||
					icon: faProjectDiagram,
 | 
			
		||||
				}],
 | 
			
		||||
			},
 | 
			
		||||
			relays: [],
 | 
			
		||||
			inbox: '',
 | 
			
		||||
			faPlus, faProjectDiagram, faSave, faTrashAlt
 | 
			
		||||
@@ -57,12 +55,12 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		add(inbox: string) {
 | 
			
		||||
			this.$root.api('admin/relays/add', {
 | 
			
		||||
			os.api('admin/relays/add', {
 | 
			
		||||
				inbox
 | 
			
		||||
			}).then((relay: any) => {
 | 
			
		||||
				this.refresh();
 | 
			
		||||
			}).catch((e: any) => {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
				os.dialog({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: e.message || e
 | 
			
		||||
				});
 | 
			
		||||
@@ -70,12 +68,12 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		remove(inbox: string) {
 | 
			
		||||
			this.$root.api('admin/relays/remove', {
 | 
			
		||||
			os.api('admin/relays/remove', {
 | 
			
		||||
				inbox
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				this.refresh();
 | 
			
		||||
			}).catch((e: any) => {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
				os.dialog({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: e.message || e
 | 
			
		||||
				});
 | 
			
		||||
@@ -83,7 +81,7 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		refresh() {
 | 
			
		||||
			this.$root.api('admin/relays/list').then((relays: any) => {
 | 
			
		||||
			os.api('admin/relays/list').then((relays: any) => {
 | 
			
		||||
				this.relays = relays;
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,53 +1,50 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div v-if="meta">
 | 
			
		||||
	<portal to="icon"><fa :icon="faCog"/></portal>
 | 
			
		||||
	<portal to="title">{{ $t('settings') }}</portal>
 | 
			
		||||
 | 
			
		||||
	<section class="_card _vMargin info">
 | 
			
		||||
		<div class="_title"><fa :icon="faInfoCircle"/> {{ $t('basicInfo') }}</div>
 | 
			
		||||
	<section class="_section info">
 | 
			
		||||
		<div class="_title"><Fa :icon="faInfoCircle"/> {{ $t('basicInfo') }}</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-input v-model="name">{{ $t('instanceName') }}</mk-input>
 | 
			
		||||
			<mk-textarea v-model="description">{{ $t('instanceDescription') }}</mk-textarea>
 | 
			
		||||
			<mk-input v-model="iconUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('iconUrl') }}</mk-input>
 | 
			
		||||
			<mk-input v-model="bannerUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('bannerUrl') }}</mk-input>
 | 
			
		||||
			<mk-input v-model="tosUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('tosUrl') }}</mk-input>
 | 
			
		||||
			<mk-input v-model="maintainerName">{{ $t('maintainerName') }}</mk-input>
 | 
			
		||||
			<mk-input v-model="maintainerEmail" type="email"><template #icon><fa :icon="faEnvelope"/></template>{{ $t('maintainerEmail') }}</mk-input>
 | 
			
		||||
			<MkInput v-model:value="name">{{ $t('instanceName') }}</MkInput>
 | 
			
		||||
			<MkTextarea v-model:value="description">{{ $t('instanceDescription') }}</MkTextarea>
 | 
			
		||||
			<MkInput v-model:value="iconUrl"><template #icon><Fa :icon="faLink"/></template>{{ $t('iconUrl') }}</MkInput>
 | 
			
		||||
			<MkInput v-model:value="bannerUrl"><template #icon><Fa :icon="faLink"/></template>{{ $t('bannerUrl') }}</MkInput>
 | 
			
		||||
			<MkInput v-model:value="tosUrl"><template #icon><Fa :icon="faLink"/></template>{{ $t('tosUrl') }}</MkInput>
 | 
			
		||||
			<MkInput v-model:value="maintainerName">{{ $t('maintainerName') }}</MkInput>
 | 
			
		||||
			<MkInput v-model:value="maintainerEmail" type="email"><template #icon><Fa :icon="faEnvelope"/></template>{{ $t('maintainerEmail') }}</MkInput>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_footer">
 | 
			
		||||
			<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
 | 
			
		||||
			<MkButton primary @click="save(true)"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<section class="_card _vMargin info">
 | 
			
		||||
	<section class="_section info">
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-input v-model="maxNoteTextLength" type="number" :save="() => save()" style="margin:0;"><template #icon><fa :icon="faPencilAlt"/></template>{{ $t('maxNoteTextLength') }}</mk-input>
 | 
			
		||||
			<MkInput v-model:value="maxNoteTextLength" type="number" :save="() => save()" style="margin:0;"><template #icon><Fa :icon="faPencilAlt"/></template>{{ $t('maxNoteTextLength') }}</MkInput>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-switch v-model="enableLocalTimeline" @change="save()">{{ $t('enableLocalTimeline') }}</mk-switch>
 | 
			
		||||
			<mk-switch v-model="enableGlobalTimeline" @change="save()">{{ $t('enableGlobalTimeline') }}</mk-switch>
 | 
			
		||||
			<mk-info>{{ $t('disablingTimelinesInfo') }}</mk-info>
 | 
			
		||||
			<MkSwitch v-model:value="enableLocalTimeline" @update:value="save()">{{ $t('enableLocalTimeline') }}</MkSwitch>
 | 
			
		||||
			<MkSwitch v-model:value="enableGlobalTimeline" @update:value="save()">{{ $t('enableGlobalTimeline') }}</MkSwitch>
 | 
			
		||||
			<MkInfo>{{ $t('disablingTimelinesInfo') }}</MkInfo>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-switch v-model="useStarForReactionFallback" @change="save()">{{ $t('useStarForReactionFallback') }}</mk-switch>
 | 
			
		||||
			<MkSwitch v-model:value="useStarForReactionFallback" @update:value="save()">{{ $t('useStarForReactionFallback') }}</MkSwitch>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<section class="_card _vMargin info">
 | 
			
		||||
		<div class="_title"><fa :icon="faUser"/> {{ $t('registration') }}</div>
 | 
			
		||||
	<section class="_section info">
 | 
			
		||||
		<div class="_title"><Fa :icon="faUser"/> {{ $t('registration') }}</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-switch v-model="enableRegistration" @change="save()">{{ $t('enableRegistration') }}</mk-switch>
 | 
			
		||||
			<mk-button v-if="!enableRegistration" @click="invite">{{ $t('invite') }}</mk-button>
 | 
			
		||||
			<MkSwitch v-model:value="enableRegistration" @update:value="save()">{{ $t('enableRegistration') }}</MkSwitch>
 | 
			
		||||
			<MkButton v-if="!enableRegistration" @click="invite">{{ $t('invite') }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<section class="_card _vMargin">
 | 
			
		||||
		<div class="_title"><fa :icon="faShieldAlt"/> {{ $t('hcaptcha') }}</div>
 | 
			
		||||
	<section class="_section">
 | 
			
		||||
		<div class="_title"><Fa :icon="faShieldAlt"/> {{ $t('hcaptcha') }}</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-switch v-model="enableHcaptcha" ref="enableHcaptcha">{{ $t('enableHcaptcha') }}</mk-switch>
 | 
			
		||||
			<MkSwitch v-model:value="enableHcaptcha">{{ $t('enableHcaptcha') }}</MkSwitch>
 | 
			
		||||
			<template v-if="enableHcaptcha">
 | 
			
		||||
				<mk-input v-model="hcaptchaSiteKey" :disabled="!enableHcaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('hcaptchaSiteKey') }}</mk-input>
 | 
			
		||||
				<mk-input v-model="hcaptchaSecretKey" :disabled="!enableHcaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('hcaptchaSecretKey') }}</mk-input>
 | 
			
		||||
				<MkInput v-model:value="hcaptchaSiteKey" :disabled="!enableHcaptcha"><template #icon><Fa :icon="faKey"/></template>{{ $t('hcaptchaSiteKey') }}</MkInput>
 | 
			
		||||
				<MkInput v-model:value="hcaptchaSecretKey" :disabled="!enableHcaptcha"><template #icon><Fa :icon="faKey"/></template>{{ $t('hcaptchaSecretKey') }}</MkInput>
 | 
			
		||||
			</template>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_content" v-if="enableHcaptcha">
 | 
			
		||||
@@ -55,17 +52,17 @@
 | 
			
		||||
			<captcha v-if="enableHcaptcha" provider="hcaptcha" :sitekey="hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_footer">
 | 
			
		||||
			<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
 | 
			
		||||
			<MkButton primary @click="save(true)"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<section class="_card _vMargin">
 | 
			
		||||
		<div class="_title"><fa :icon="faShieldAlt"/> {{ $t('recaptcha') }}</div>
 | 
			
		||||
	<section class="_section">
 | 
			
		||||
		<div class="_title"><Fa :icon="faShieldAlt"/> {{ $t('recaptcha') }}</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-switch v-model="enableRecaptcha" ref="enableRecaptcha">{{ $t('enableRecaptcha') }}</mk-switch>
 | 
			
		||||
			<MkSwitch v-model:value="enableRecaptcha" ref="enableRecaptcha">{{ $t('enableRecaptcha') }}</MkSwitch>
 | 
			
		||||
			<template v-if="enableRecaptcha">
 | 
			
		||||
				<mk-input v-model="recaptchaSiteKey" :disabled="!enableRecaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('recaptchaSiteKey') }}</mk-input>
 | 
			
		||||
				<mk-input v-model="recaptchaSecretKey" :disabled="!enableRecaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('recaptchaSecretKey') }}</mk-input>
 | 
			
		||||
				<MkInput v-model:value="recaptchaSiteKey" :disabled="!enableRecaptcha"><template #icon><Fa :icon="faKey"/></template>{{ $t('recaptchaSiteKey') }}</MkInput>
 | 
			
		||||
				<MkInput v-model:value="recaptchaSecretKey" :disabled="!enableRecaptcha"><template #icon><Fa :icon="faKey"/></template>{{ $t('recaptchaSecretKey') }}</MkInput>
 | 
			
		||||
			</template>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_content" v-if="enableRecaptcha && recaptchaSiteKey">
 | 
			
		||||
@@ -73,198 +70,198 @@
 | 
			
		||||
			<captcha v-if="enableRecaptcha" provider="grecaptcha" :sitekey="recaptchaSiteKey"/>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_footer">
 | 
			
		||||
			<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
 | 
			
		||||
			<MkButton primary @click="save(true)"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<section class="_card _vMargin">
 | 
			
		||||
		<div class="_title"><fa :icon="faEnvelope" /> {{ $t('emailConfig') }}</div>
 | 
			
		||||
	<section class="_section">
 | 
			
		||||
		<div class="_title"><Fa :icon="faEnvelope" /> {{ $t('emailConfig') }}</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-switch v-model="enableEmail" @change="save()">{{ $t('enableEmail') }}<template #desc>{{ $t('emailConfigInfo') }}</template></mk-switch>
 | 
			
		||||
			<mk-input v-model="email" type="email" :disabled="!enableEmail">{{ $t('email') }}</mk-input>
 | 
			
		||||
			<MkSwitch v-model:value="enableEmail" @update:value="save()">{{ $t('enableEmail') }}<template #desc>{{ $t('emailConfigInfo') }}</template></MkSwitch>
 | 
			
		||||
			<MkInput v-model:value="email" type="email" :disabled="!enableEmail">{{ $t('email') }}</MkInput>
 | 
			
		||||
			<div><b>{{ $t('smtpConfig') }}</b></div>
 | 
			
		||||
			<div class="_inputs">
 | 
			
		||||
				<mk-input v-model="smtpHost" :disabled="!enableEmail">{{ $t('smtpHost') }}</mk-input>
 | 
			
		||||
				<mk-input v-model="smtpPort" type="number" :disabled="!enableEmail">{{ $t('smtpPort') }}</mk-input>
 | 
			
		||||
				<MkInput v-model:value="smtpHost" :disabled="!enableEmail">{{ $t('smtpHost') }}</MkInput>
 | 
			
		||||
				<MkInput v-model:value="smtpPort" type="number" :disabled="!enableEmail">{{ $t('smtpPort') }}</MkInput>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="_inputs">
 | 
			
		||||
				<mk-input v-model="smtpUser" :disabled="!enableEmail">{{ $t('smtpUser') }}</mk-input>
 | 
			
		||||
				<mk-input v-model="smtpPass" type="password" :disabled="!enableEmail">{{ $t('smtpPass') }}</mk-input>
 | 
			
		||||
				<MkInput v-model:value="smtpUser" :disabled="!enableEmail">{{ $t('smtpUser') }}</MkInput>
 | 
			
		||||
				<MkInput v-model:value="smtpPass" type="password" :disabled="!enableEmail">{{ $t('smtpPass') }}</MkInput>
 | 
			
		||||
			</div>
 | 
			
		||||
			<mk-info>{{ $t('emptyToDisableSmtpAuth') }}</mk-info>
 | 
			
		||||
			<mk-switch v-model="smtpSecure" :disabled="!enableEmail">{{ $t('smtpSecure') }}<template #desc>{{ $t('smtpSecureInfo') }}</template></mk-switch>
 | 
			
		||||
			<MkInfo>{{ $t('emptyToDisableSmtpAuth') }}</MkInfo>
 | 
			
		||||
			<MkSwitch v-model:value="smtpSecure" :disabled="!enableEmail">{{ $t('smtpSecure') }}<template #desc>{{ $t('smtpSecureInfo') }}</template></MkSwitch>
 | 
			
		||||
			<div>
 | 
			
		||||
				<mk-button :disabled="!enableEmail" inline @click="testEmail()">{{ $t('testEmail') }}</mk-button>
 | 
			
		||||
				<mk-button :disabled="!enableEmail" primary inline @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
 | 
			
		||||
				<MkButton :disabled="!enableEmail" inline @click="testEmail()">{{ $t('testEmail') }}</MkButton>
 | 
			
		||||
				<MkButton :disabled="!enableEmail" primary inline @click="save(true)"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<section class="_card _vMargin">
 | 
			
		||||
		<div class="_title"><fa :icon="faBolt"/> {{ $t('serviceworker') }}</div>
 | 
			
		||||
	<section class="_section">
 | 
			
		||||
		<div class="_title"><Fa :icon="faBolt"/> {{ $t('serviceworker') }}</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-switch v-model="enableServiceWorker">{{ $t('enableServiceworker') }}<template #desc>{{ $t('serviceworkerInfo') }}</template></mk-switch>
 | 
			
		||||
			<MkSwitch v-model:value="enableServiceWorker">{{ $t('enableServiceworker') }}<template #desc>{{ $t('serviceworkerInfo') }}</template></MkSwitch>
 | 
			
		||||
			<template v-if="enableServiceWorker">
 | 
			
		||||
				<div class="_inputs">
 | 
			
		||||
					<mk-input v-model="swPublicKey" :disabled="!enableServiceWorker"><template #icon><fa :icon="faKey"/></template>Public key</mk-input>
 | 
			
		||||
					<mk-input v-model="swPrivateKey" :disabled="!enableServiceWorker"><template #icon><fa :icon="faKey"/></template>Private key</mk-input>
 | 
			
		||||
					<MkInput v-model:value="swPublicKey" :disabled="!enableServiceWorker"><template #icon><Fa :icon="faKey"/></template>Public key</MkInput>
 | 
			
		||||
					<MkInput v-model:value="swPrivateKey" :disabled="!enableServiceWorker"><template #icon><Fa :icon="faKey"/></template>Private key</MkInput>
 | 
			
		||||
				</div>
 | 
			
		||||
			</template>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_footer">
 | 
			
		||||
			<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
 | 
			
		||||
			<MkButton primary @click="save(true)"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<section class="_card _vMargin">
 | 
			
		||||
		<div class="_title"><fa :icon="faThumbtack"/> {{ $t('pinnedUsers') }}</div>
 | 
			
		||||
	<section class="_section">
 | 
			
		||||
		<div class="_title"><Fa :icon="faThumbtack"/> {{ $t('pinnedUsers') }}</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-textarea v-model="pinnedUsers">
 | 
			
		||||
			<MkTextarea v-model:value="pinnedUsers">
 | 
			
		||||
				<template #desc>{{ $t('pinnedUsersDescription') }} <button class="_textButton" @click="addPinUser">{{ $t('addUser') }}</button></template>
 | 
			
		||||
			</mk-textarea>
 | 
			
		||||
			</MkTextarea>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_footer">
 | 
			
		||||
			<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
 | 
			
		||||
			<MkButton primary @click="save(true)"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<section class="_card _vMargin">
 | 
			
		||||
		<div class="_title"><fa :icon="faCloud"/> {{ $t('files') }}</div>
 | 
			
		||||
	<section class="_section">
 | 
			
		||||
		<div class="_title"><Fa :icon="faCloud"/> {{ $t('files') }}</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-switch v-model="cacheRemoteFiles">{{ $t('cacheRemoteFiles') }}<template #desc>{{ $t('cacheRemoteFilesDescription') }}</template></mk-switch>
 | 
			
		||||
			<mk-switch v-model="proxyRemoteFiles">{{ $t('proxyRemoteFiles') }}<template #desc>{{ $t('proxyRemoteFilesDescription') }}</template></mk-switch>
 | 
			
		||||
			<mk-input v-model="localDriveCapacityMb" type="number">{{ $t('driveCapacityPerLocalAccount') }}<template #suffix>MB</template><template #desc>{{ $t('inMb') }}</template></mk-input>
 | 
			
		||||
			<mk-input v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles" style="margin-bottom: 0;">{{ $t('driveCapacityPerRemoteAccount') }}<template #suffix>MB</template><template #desc>{{ $t('inMb') }}</template></mk-input>
 | 
			
		||||
			<MkSwitch v-model:value="cacheRemoteFiles">{{ $t('cacheRemoteFiles') }}<template #desc>{{ $t('cacheRemoteFilesDescription') }}</template></MkSwitch>
 | 
			
		||||
			<MkSwitch v-model:value="proxyRemoteFiles">{{ $t('proxyRemoteFiles') }}<template #desc>{{ $t('proxyRemoteFilesDescription') }}</template></MkSwitch>
 | 
			
		||||
			<MkInput v-model:value="localDriveCapacityMb" type="number">{{ $t('driveCapacityPerLocalAccount') }}<template #suffix>MB</template><template #desc>{{ $t('inMb') }}</template></MkInput>
 | 
			
		||||
			<MkInput v-model:value="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles" style="margin-bottom: 0;">{{ $t('driveCapacityPerRemoteAccount') }}<template #suffix>MB</template><template #desc>{{ $t('inMb') }}</template></MkInput>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_footer">
 | 
			
		||||
			<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
 | 
			
		||||
			<MkButton primary @click="save(true)"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<section class="_card _vMargin">
 | 
			
		||||
		<div class="_title"><fa :icon="faCloud"/> {{ $t('objectStorage') }}</div>
 | 
			
		||||
	<section class="_section">
 | 
			
		||||
		<div class="_title"><Fa :icon="faCloud"/> {{ $t('objectStorage') }}</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-switch v-model="useObjectStorage">{{ $t('useObjectStorage') }}</mk-switch>
 | 
			
		||||
			<MkSwitch v-model:value="useObjectStorage">{{ $t('useObjectStorage') }}</MkSwitch>
 | 
			
		||||
			<template v-if="useObjectStorage">
 | 
			
		||||
				<mk-input v-model="objectStorageBaseUrl" :disabled="!useObjectStorage">{{ $t('objectStorageBaseUrl') }}<template #desc>{{ $t('objectStorageBaseUrlDesc') }}</template></mk-input>
 | 
			
		||||
				<MkInput v-model:value="objectStorageBaseUrl" :disabled="!useObjectStorage">{{ $t('objectStorageBaseUrl') }}<template #desc>{{ $t('objectStorageBaseUrlDesc') }}</template></MkInput>
 | 
			
		||||
				<div class="_inputs">
 | 
			
		||||
					<mk-input v-model="objectStorageBucket" :disabled="!useObjectStorage">{{ $t('objectStorageBucket') }}<template #desc>{{ $t('objectStorageBucketDesc') }}</template></mk-input>
 | 
			
		||||
					<mk-input v-model="objectStoragePrefix" :disabled="!useObjectStorage">{{ $t('objectStoragePrefix') }}<template #desc>{{ $t('objectStoragePrefixDesc') }}</template></mk-input>
 | 
			
		||||
					<MkInput v-model:value="objectStorageBucket" :disabled="!useObjectStorage">{{ $t('objectStorageBucket') }}<template #desc>{{ $t('objectStorageBucketDesc') }}</template></MkInput>
 | 
			
		||||
					<MkInput v-model:value="objectStoragePrefix" :disabled="!useObjectStorage">{{ $t('objectStoragePrefix') }}<template #desc>{{ $t('objectStoragePrefixDesc') }}</template></MkInput>
 | 
			
		||||
				</div>
 | 
			
		||||
				<mk-input v-model="objectStorageEndpoint" :disabled="!useObjectStorage">{{ $t('objectStorageEndpoint') }}<template #desc>{{ $t('objectStorageEndpointDesc') }}</template></mk-input>
 | 
			
		||||
				<MkInput v-model:value="objectStorageEndpoint" :disabled="!useObjectStorage">{{ $t('objectStorageEndpoint') }}<template #desc>{{ $t('objectStorageEndpointDesc') }}</template></MkInput>
 | 
			
		||||
				<div class="_inputs">
 | 
			
		||||
					<mk-input v-model="objectStorageRegion" :disabled="!useObjectStorage">{{ $t('objectStorageRegion') }}<template #desc>{{ $t('objectStorageRegionDesc') }}</template></mk-input>
 | 
			
		||||
					<MkInput v-model:value="objectStorageRegion" :disabled="!useObjectStorage">{{ $t('objectStorageRegion') }}<template #desc>{{ $t('objectStorageRegionDesc') }}</template></MkInput>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div class="_inputs">
 | 
			
		||||
					<mk-input v-model="objectStorageAccessKey" :disabled="!useObjectStorage"><template #icon><fa :icon="faKey"/></template>Access key</mk-input>
 | 
			
		||||
					<mk-input v-model="objectStorageSecretKey" :disabled="!useObjectStorage"><template #icon><fa :icon="faKey"/></template>Secret key</mk-input>
 | 
			
		||||
					<MkInput v-model:value="objectStorageAccessKey" :disabled="!useObjectStorage"><template #icon><Fa :icon="faKey"/></template>Access key</MkInput>
 | 
			
		||||
					<MkInput v-model:value="objectStorageSecretKey" :disabled="!useObjectStorage"><template #icon><Fa :icon="faKey"/></template>Secret key</MkInput>
 | 
			
		||||
				</div>
 | 
			
		||||
				<mk-switch v-model="objectStorageUseSSL" :disabled="!useObjectStorage">{{ $t('objectStorageUseSSL') }}<template #desc>{{ $t('objectStorageUseSSLDesc') }}</template></mk-switch>
 | 
			
		||||
				<mk-switch v-model="objectStorageUseProxy" :disabled="!useObjectStorage">{{ $t('objectStorageUseProxy') }}<template #desc>{{ $t('objectStorageUseProxyDesc') }}</template></mk-switch>
 | 
			
		||||
				<mk-switch v-model="objectStorageSetPublicRead" :disabled="!useObjectStorage">{{ $t('objectStorageSetPublicRead') }}</mk-switch>
 | 
			
		||||
				<MkSwitch v-model:value="objectStorageUseSSL" :disabled="!useObjectStorage">{{ $t('objectStorageUseSSL') }}<template #desc>{{ $t('objectStorageUseSSLDesc') }}</template></MkSwitch>
 | 
			
		||||
				<MkSwitch v-model:value="objectStorageUseProxy" :disabled="!useObjectStorage">{{ $t('objectStorageUseProxy') }}<template #desc>{{ $t('objectStorageUseProxyDesc') }}</template></MkSwitch>
 | 
			
		||||
				<MkSwitch v-model:value="objectStorageSetPublicRead" :disabled="!useObjectStorage">{{ $t('objectStorageSetPublicRead') }}</MkSwitch>
 | 
			
		||||
			</template>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_footer">
 | 
			
		||||
			<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
 | 
			
		||||
			<MkButton primary @click="save(true)"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<section class="_card _vMargin">
 | 
			
		||||
		<div class="_title"><fa :icon="faGhost"/> {{ $t('proxyAccount') }}</div>
 | 
			
		||||
	<section class="_section">
 | 
			
		||||
		<div class="_title"><Fa :icon="faGhost"/> {{ $t('proxyAccount') }}</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-input :value="proxyAccount ? proxyAccount.username : null" style="margin: 0;" disabled><template #prefix>@</template>{{ $t('proxyAccount') }}<template #desc>{{ $t('proxyAccountDescription') }}</template></mk-input>
 | 
			
		||||
			<mk-button primary @click="chooseProxyAccount">{{ $t('chooseProxyAccount') }}</mk-button>
 | 
			
		||||
			<MkInput :value="proxyAccount ? proxyAccount.username : null" style="margin: 0;" disabled><template #prefix>@</template>{{ $t('proxyAccount') }}<template #desc>{{ $t('proxyAccountDescription') }}</template></MkInput>
 | 
			
		||||
			<MkButton primary @click="chooseProxyAccount">{{ $t('chooseProxyAccount') }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<section class="_card _vMargin">
 | 
			
		||||
		<div class="_title"><fa :icon="faBan"/> {{ $t('blockedInstances') }}</div>
 | 
			
		||||
	<section class="_section">
 | 
			
		||||
		<div class="_title"><Fa :icon="faBan"/> {{ $t('blockedInstances') }}</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-textarea v-model="blockedHosts">
 | 
			
		||||
			<MkTextarea v-model:value="blockedHosts">
 | 
			
		||||
				<template #desc>{{ $t('blockedInstancesDescription') }}</template>
 | 
			
		||||
			</mk-textarea>
 | 
			
		||||
			</MkTextarea>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_footer">
 | 
			
		||||
			<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
 | 
			
		||||
			<MkButton primary @click="save(true)"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<section class="_card _vMargin">
 | 
			
		||||
		<div class="_title"><fa :icon="faShareAlt"/> {{ $t('integration') }}</div>
 | 
			
		||||
	<section class="_section">
 | 
			
		||||
		<div class="_title"><Fa :icon="faShareAlt"/> {{ $t('integration') }}</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<header><fa :icon="faTwitter"/> Twitter</header>
 | 
			
		||||
			<mk-switch v-model="enableTwitterIntegration">{{ $t('enable') }}</mk-switch>
 | 
			
		||||
			<header><Fa :icon="faTwitter"/> Twitter</header>
 | 
			
		||||
			<MkSwitch v-model:value="enableTwitterIntegration">{{ $t('enable') }}</MkSwitch>
 | 
			
		||||
			<template v-if="enableTwitterIntegration">
 | 
			
		||||
				<mk-info>Callback URL: {{ `${url}/api/tw/cb` }}</mk-info>
 | 
			
		||||
				<mk-input v-model="twitterConsumerKey" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>Consumer Key</mk-input>
 | 
			
		||||
				<mk-input v-model="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>Consumer Secret</mk-input>
 | 
			
		||||
				<MkInfo>Callback URL: {{ `${url}/api/tw/cb` }}</MkInfo>
 | 
			
		||||
				<MkInput v-model:value="twitterConsumerKey" :disabled="!enableTwitterIntegration"><template #icon><Fa :icon="faKey"/></template>Consumer Key</MkInput>
 | 
			
		||||
				<MkInput v-model:value="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><template #icon><Fa :icon="faKey"/></template>Consumer Secret</MkInput>
 | 
			
		||||
			</template>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<header><fa :icon="faGithub"/> GitHub</header>
 | 
			
		||||
			<mk-switch v-model="enableGithubIntegration">{{ $t('enable') }}</mk-switch>
 | 
			
		||||
			<header><Fa :icon="faGithub"/> GitHub</header>
 | 
			
		||||
			<MkSwitch v-model:value="enableGithubIntegration">{{ $t('enable') }}</MkSwitch>
 | 
			
		||||
			<template v-if="enableGithubIntegration">
 | 
			
		||||
				<mk-info>Callback URL: {{ `${url}/api/gh/cb` }}</mk-info>
 | 
			
		||||
				<mk-input v-model="githubClientId" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>Client ID</mk-input>
 | 
			
		||||
				<mk-input v-model="githubClientSecret" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>Client Secret</mk-input>
 | 
			
		||||
				<MkInfo>Callback URL: {{ `${url}/api/gh/cb` }}</MkInfo>
 | 
			
		||||
				<MkInput v-model:value="githubClientId" :disabled="!enableGithubIntegration"><template #icon><Fa :icon="faKey"/></template>Client ID</MkInput>
 | 
			
		||||
				<MkInput v-model:value="githubClientSecret" :disabled="!enableGithubIntegration"><template #icon><Fa :icon="faKey"/></template>Client Secret</MkInput>
 | 
			
		||||
			</template>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<header><fa :icon="faDiscord"/> Discord</header>
 | 
			
		||||
			<mk-switch v-model="enableDiscordIntegration">{{ $t('enable') }}</mk-switch>
 | 
			
		||||
			<header><Fa :icon="faDiscord"/> Discord</header>
 | 
			
		||||
			<MkSwitch v-model:value="enableDiscordIntegration">{{ $t('enable') }}</MkSwitch>
 | 
			
		||||
			<template v-if="enableDiscordIntegration">
 | 
			
		||||
				<mk-info>Callback URL: {{ `${url}/api/dc/cb` }}</mk-info>
 | 
			
		||||
				<mk-input v-model="discordClientId" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>Client ID</mk-input>
 | 
			
		||||
				<mk-input v-model="discordClientSecret" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>Client Secret</mk-input>
 | 
			
		||||
				<MkInfo>Callback URL: {{ `${url}/api/dc/cb` }}</MkInfo>
 | 
			
		||||
				<MkInput v-model:value="discordClientId" :disabled="!enableDiscordIntegration"><template #icon><Fa :icon="faKey"/></template>Client ID</MkInput>
 | 
			
		||||
				<MkInput v-model:value="discordClientSecret" :disabled="!enableDiscordIntegration"><template #icon><Fa :icon="faKey"/></template>Client Secret</MkInput>
 | 
			
		||||
			</template>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_footer">
 | 
			
		||||
			<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
 | 
			
		||||
			<MkButton primary @click="save(true)"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<section class="_card _vMargin">
 | 
			
		||||
		<div class="_title"><fa :icon="faArchway" /> Summaly Proxy</div>
 | 
			
		||||
	<section class="_section">
 | 
			
		||||
		<div class="_title"><Fa :icon="faArchway" /> Summaly Proxy</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-input v-model="summalyProxy">URL</mk-input>
 | 
			
		||||
			<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
 | 
			
		||||
			<MkInput v-model:value="summalyProxy">URL</MkInput>
 | 
			
		||||
			<MkButton primary @click="save(true)"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent, defineAsyncComponent } from 'vue';
 | 
			
		||||
import { faPencilAlt, faShareAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faThumbtack, faUser, faShieldAlt, faKey, faBolt, faArchway } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faTrashAlt, faEnvelope } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import { faTwitter, faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons';
 | 
			
		||||
import MkButton from '../../components/ui/button.vue';
 | 
			
		||||
import MkInput from '../../components/ui/input.vue';
 | 
			
		||||
import MkTextarea from '../../components/ui/textarea.vue';
 | 
			
		||||
import MkSwitch from '../../components/ui/switch.vue';
 | 
			
		||||
import MkInfo from '../../components/ui/info.vue';
 | 
			
		||||
import MkUserSelect from '../../components/user-select.vue';
 | 
			
		||||
import { url } from '../../config';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import MkInput from '@/components/ui/input.vue';
 | 
			
		||||
import MkTextarea from '@/components/ui/textarea.vue';
 | 
			
		||||
import MkSwitch from '@/components/ui/switch.vue';
 | 
			
		||||
import MkInfo from '@/components/ui/info.vue';
 | 
			
		||||
import { url } from '@/config';
 | 
			
		||||
import getAcct from '../../../misc/acct/render';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: this.$t('instance') as string
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkButton,
 | 
			
		||||
		MkInput,
 | 
			
		||||
		MkTextarea,
 | 
			
		||||
		MkSwitch,
 | 
			
		||||
		MkInfo,
 | 
			
		||||
		Captcha: () => import('../../components/captcha.vue').then(x => x.default),
 | 
			
		||||
		Captcha: defineAsyncComponent(() => import('@/components/captcha.vue')),
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('instance'),
 | 
			
		||||
					icon: faCog,
 | 
			
		||||
				}],
 | 
			
		||||
			},
 | 
			
		||||
			url,
 | 
			
		||||
			proxyAccount: null,
 | 
			
		||||
			proxyAccountId: null,
 | 
			
		||||
@@ -394,16 +391,16 @@ export default Vue.extend({
 | 
			
		||||
		this.summalyProxy = this.meta.summalyProxy;
 | 
			
		||||
 | 
			
		||||
		if (this.proxyAccountId) {
 | 
			
		||||
			this.$root.api('users/show', { userId: this.proxyAccountId }).then(proxyAccount => {
 | 
			
		||||
			os.api('users/show', { userId: this.proxyAccountId }).then(proxyAccount => {
 | 
			
		||||
				this.proxyAccount = proxyAccount;
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	mounted() {
 | 
			
		||||
		this.$refs.enableHcaptcha.$on('change', () => {
 | 
			
		||||
		this.$watch('enableHcaptcha', () => {
 | 
			
		||||
			if (this.enableHcaptcha && this.enableRecaptcha) {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
				os.dialog({
 | 
			
		||||
					type: 'question', // warning だと間違って cancel するかもしれない
 | 
			
		||||
					showCancelButton: true,
 | 
			
		||||
					title: this.$t('settingGuide'),
 | 
			
		||||
@@ -418,9 +415,9 @@ export default Vue.extend({
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		this.$refs.enableRecaptcha.$on('change', () => {
 | 
			
		||||
		this.$watch('enableRecaptcha', () => {
 | 
			
		||||
			if (this.enableRecaptcha && this.enableHcaptcha) {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
				os.dialog({
 | 
			
		||||
					type: 'question', // warning だと間違って cancel するかもしれない
 | 
			
		||||
					showCancelButton: true,
 | 
			
		||||
					title: this.$t('settingGuide'),
 | 
			
		||||
@@ -438,13 +435,13 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		invite() {
 | 
			
		||||
			this.$root.api('admin/invite').then(x => {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
			os.api('admin/invite').then(x => {
 | 
			
		||||
				os.dialog({
 | 
			
		||||
					type: 'info',
 | 
			
		||||
					text: x.code
 | 
			
		||||
				});
 | 
			
		||||
			}).catch(e => {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
				os.dialog({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: e
 | 
			
		||||
				});
 | 
			
		||||
@@ -452,7 +449,7 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		addPinUser() {
 | 
			
		||||
			this.$root.new(MkUserSelect, {}).$once('selected', user => {
 | 
			
		||||
			os.selectUser().then(user => {
 | 
			
		||||
				this.pinnedUsers = this.pinnedUsers.trim();
 | 
			
		||||
				this.pinnedUsers += '\n@' + getAcct(user);
 | 
			
		||||
				this.pinnedUsers = this.pinnedUsers.trim();
 | 
			
		||||
@@ -460,7 +457,7 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		chooseProxyAccount() {
 | 
			
		||||
			this.$root.new(MkUserSelect, {}).$once('selected', user => {
 | 
			
		||||
			os.selectUser().then(user => {
 | 
			
		||||
				this.proxyAccount = user;
 | 
			
		||||
				this.proxyAccountId = user.id;
 | 
			
		||||
				this.save(true);
 | 
			
		||||
@@ -468,17 +465,17 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async testEmail() {
 | 
			
		||||
			this.$root.api('admin/send-email', {
 | 
			
		||||
			os.api('admin/send-email', {
 | 
			
		||||
				to: this.maintainerEmail,
 | 
			
		||||
				subject: 'Test email',
 | 
			
		||||
				text: 'Yo'
 | 
			
		||||
			}).then(x => {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
				os.dialog({
 | 
			
		||||
					type: 'success',
 | 
			
		||||
					splash: true
 | 
			
		||||
				});
 | 
			
		||||
			}).catch(e => {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
				os.dialog({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: e
 | 
			
		||||
				});
 | 
			
		||||
@@ -486,7 +483,7 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		save(withDialog = false) {
 | 
			
		||||
			this.$root.api('admin/update-meta', {
 | 
			
		||||
			os.api('admin/update-meta', {
 | 
			
		||||
				name: this.name,
 | 
			
		||||
				description: this.description,
 | 
			
		||||
				tosUrl: this.tosUrl,
 | 
			
		||||
@@ -547,13 +544,10 @@ export default Vue.extend({
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				this.$store.dispatch('instance/fetch');
 | 
			
		||||
				if (withDialog) {
 | 
			
		||||
					this.$root.dialog({
 | 
			
		||||
						type: 'success',
 | 
			
		||||
						iconOnly: true, autoClose: true
 | 
			
		||||
					});
 | 
			
		||||
					os.success();
 | 
			
		||||
				}
 | 
			
		||||
			}).catch(e => {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
				os.dialog({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: e
 | 
			
		||||
				});
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										233
									
								
								src/client/pages/instance/user-dialog.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										233
									
								
								src/client/pages/instance/user-dialog.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,233 @@
 | 
			
		||||
<template>
 | 
			
		||||
<XModalWindow ref="dialog"
 | 
			
		||||
	:width="370"
 | 
			
		||||
	@close="$refs.dialog.close()"
 | 
			
		||||
	@closed="$emit('closed')"
 | 
			
		||||
>
 | 
			
		||||
	<template #header v-if="user"><MkUserName class="name" :user="user"/></template>
 | 
			
		||||
	<div class="vrcsvlkm" v-if="user && info">
 | 
			
		||||
		<div class="_section">
 | 
			
		||||
			<div class="banner" :style="bannerStyle">
 | 
			
		||||
				<MkAvatar class="avatar" :user="user"/>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_section">
 | 
			
		||||
			<div class="title">
 | 
			
		||||
				<span class="acct">@{{ acct(user) }}</span>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="status">
 | 
			
		||||
				<span class="staff" v-if="user.isAdmin"><Fa :icon="faBookmark"/></span>
 | 
			
		||||
				<span class="staff" v-if="user.isModerator"><Fa :icon="farBookmark"/></span>
 | 
			
		||||
				<span class="punished" v-if="user.isSilenced"><Fa :icon="faMicrophoneSlash"/></span>
 | 
			
		||||
				<span class="punished" v-if="user.isSuspended"><Fa :icon="faSnowflake"/></span>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_section">
 | 
			
		||||
			<div class="_content">
 | 
			
		||||
				<MkSwitch v-if="user.host == null && $store.state.i.isAdmin && (this.moderator || !user.isAdmin)" @update:value="toggleModerator" v-model:value="moderator">{{ $t('moderator') }}</MkSwitch>
 | 
			
		||||
				<MkSwitch @update:value="toggleSilence" v-model:value="silenced">{{ $t('silence') }}</MkSwitch>
 | 
			
		||||
				<MkSwitch @update:value="toggleSuspend" v-model:value="suspended">{{ $t('suspend') }}</MkSwitch>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_section">
 | 
			
		||||
			<div class="_content">
 | 
			
		||||
				<MkButton full @click="openProfile"><Fa :icon="faExternalLinkSquareAlt"/> {{ $t('profile') }}</MkButton>
 | 
			
		||||
				<MkButton full v-if="user.host != null" @click="updateRemoteUser"><Fa :icon="faSync"/> {{ $t('updateRemoteUser') }}</MkButton>
 | 
			
		||||
				<MkButton full @click="resetPassword"><Fa :icon="faKey"/> {{ $t('resetPassword') }}</MkButton>
 | 
			
		||||
				<MkButton full @click="deleteAllFiles" danger><Fa :icon="faTrashAlt"/> {{ $t('deleteAllFiles') }}</MkButton>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_section">
 | 
			
		||||
			<details class="_content rawdata">
 | 
			
		||||
				<pre><code>{{ JSON.stringify(info, null, 2) }}</code></pre>
 | 
			
		||||
			</details>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</XModalWindow>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { computed, defineComponent } from 'vue';
 | 
			
		||||
import { faTimes, faBookmark, faKey, faSync, faMicrophoneSlash, faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faSnowflake, faTrashAlt, faBookmark as farBookmark  } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import MkSwitch from '@/components/ui/switch.vue';
 | 
			
		||||
import XModalWindow from '@/components/ui/modal-window.vue';
 | 
			
		||||
import Progress from '@/scripts/loading';
 | 
			
		||||
import { acct, userPage } from '../../filters/user';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkButton,
 | 
			
		||||
		MkSwitch,
 | 
			
		||||
		XModalWindow,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	props: {
 | 
			
		||||
		userId: {
 | 
			
		||||
			required: true,
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	emits: ['closed'],
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			user: null,
 | 
			
		||||
			info: null,
 | 
			
		||||
			moderator: false,
 | 
			
		||||
			silenced: false,
 | 
			
		||||
			suspended: false,
 | 
			
		||||
			faTimes, faBookmark, farBookmark, faKey, faSync, faMicrophoneSlash, faSnowflake, faTrashAlt, faExternalLinkSquareAlt
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	computed: {
 | 
			
		||||
		bannerStyle(): any {
 | 
			
		||||
			if (this.user.bannerUrl == null) return {};
 | 
			
		||||
			return {
 | 
			
		||||
				backgroundImage: `url(${ this.user.bannerUrl })`
 | 
			
		||||
			};
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		this.fetch();
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		async fetch() {
 | 
			
		||||
			Progress.start();
 | 
			
		||||
			this.user = await os.api('users/show', { userId: this.userId });
 | 
			
		||||
			this.info = await os.api('admin/show-user', { userId: this.userId });
 | 
			
		||||
			this.moderator = this.info.isModerator;
 | 
			
		||||
			this.silenced = this.info.isSilenced;
 | 
			
		||||
			this.suspended = this.info.isSuspended;
 | 
			
		||||
			Progress.done();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		/** 処理対象ユーザーの情報を更新する */
 | 
			
		||||
		async refreshUser() {
 | 
			
		||||
			this.user = await os.api('users/show', { userId: this.user.id });
 | 
			
		||||
			this.info = await os.api('admin/show-user', { userId: this.user.id });
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		openProfile() {
 | 
			
		||||
			window.open(userPage(this.user, null, true), '_blank');
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async updateRemoteUser() {
 | 
			
		||||
			await os.api('admin/update-remote-user', { userId: this.user.id }).then(res => {
 | 
			
		||||
				os.success();
 | 
			
		||||
			});
 | 
			
		||||
			await this.refreshUser();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async resetPassword() {
 | 
			
		||||
			os.apiWithDialog('admin/reset-password', {
 | 
			
		||||
				userId: this.user.id,
 | 
			
		||||
			}, undefined, ({ password }) => {
 | 
			
		||||
				os.dialog({
 | 
			
		||||
					type: 'success',
 | 
			
		||||
					text: this.$t('newPasswordIs', { password })
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async toggleSilence(v) {
 | 
			
		||||
			const confirm = await os.dialog({
 | 
			
		||||
				type: 'warning',
 | 
			
		||||
				showCancelButton: true,
 | 
			
		||||
				text: v ? this.$t('silenceConfirm') : this.$t('unsilenceConfirm'),
 | 
			
		||||
			});
 | 
			
		||||
			if (confirm.canceled) {
 | 
			
		||||
				this.silenced = !v;
 | 
			
		||||
			} else {
 | 
			
		||||
				await os.api(v ? 'admin/silence-user' : 'admin/unsilence-user', { userId: this.user.id });
 | 
			
		||||
				await this.refreshUser();
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async toggleSuspend(v) {
 | 
			
		||||
			const confirm = await os.dialog({
 | 
			
		||||
				type: 'warning',
 | 
			
		||||
				showCancelButton: true,
 | 
			
		||||
				text: v ? this.$t('suspendConfirm') : this.$t('unsuspendConfirm'),
 | 
			
		||||
			});
 | 
			
		||||
			if (confirm.canceled) {
 | 
			
		||||
				this.suspended = !v;
 | 
			
		||||
			} else {
 | 
			
		||||
				await os.api(v ? 'admin/suspend-user' : 'admin/unsuspend-user', { userId: this.user.id });
 | 
			
		||||
				await this.refreshUser();
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async toggleModerator(v) {
 | 
			
		||||
			await os.api(v ? 'admin/moderators/add' : 'admin/moderators/remove', { userId: this.user.id });
 | 
			
		||||
			await this.refreshUser();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async deleteAllFiles() {
 | 
			
		||||
			const confirm = await os.dialog({
 | 
			
		||||
				type: 'warning',
 | 
			
		||||
				showCancelButton: true,
 | 
			
		||||
				text: this.$t('deleteAllFilesConfirm'),
 | 
			
		||||
			});
 | 
			
		||||
			if (confirm.canceled) return;
 | 
			
		||||
			const process = async () => {
 | 
			
		||||
				await os.api('admin/delete-all-files-of-a-user', { userId: this.user.id });
 | 
			
		||||
				os.success();
 | 
			
		||||
			};
 | 
			
		||||
			await process().catch(e => {
 | 
			
		||||
				os.dialog({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: e.toString()
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
			await this.refreshUser();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		acct
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.vrcsvlkm {
 | 
			
		||||
	> ._section {
 | 
			
		||||
		> .banner {
 | 
			
		||||
			position: relative;
 | 
			
		||||
			height: 100px;
 | 
			
		||||
			background-color: #4c5e6d;
 | 
			
		||||
			background-size: cover;
 | 
			
		||||
			background-position: center;
 | 
			
		||||
			border-radius: 8px;
 | 
			
		||||
 | 
			
		||||
			> .avatar {
 | 
			
		||||
				position: absolute;
 | 
			
		||||
				top: 60px;
 | 
			
		||||
				width: 64px;
 | 
			
		||||
				height: 64px;
 | 
			
		||||
				left: 0;
 | 
			
		||||
				right: 0;
 | 
			
		||||
				margin: 0 auto;
 | 
			
		||||
				border: solid 4px var(--panel);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> .title {
 | 
			
		||||
			text-align: center;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> .status {
 | 
			
		||||
			text-align: center;
 | 
			
		||||
			margin-top: 8px;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> .rawdata {
 | 
			
		||||
			overflow: auto;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,206 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="vrcsvlkm" v-if="user && info">
 | 
			
		||||
	<portal to="title" v-if="user"><mk-user-name :user="user" :nowrap="false" class="name"/></portal>
 | 
			
		||||
	<portal to="avatar" v-if="user"><mk-avatar class="avatar" :user="user" :disable-preview="true"/></portal>
 | 
			
		||||
 | 
			
		||||
	<section class="_card">
 | 
			
		||||
		<div class="_title">
 | 
			
		||||
			<mk-avatar class="avatar" :user="user"/>
 | 
			
		||||
			<mk-user-name class="name" :user="user"/>
 | 
			
		||||
			<span class="acct">@{{ user | acct }}</span>
 | 
			
		||||
			<span class="staff" v-if="user.isAdmin"><fa :icon="faBookmark"/></span>
 | 
			
		||||
			<span class="staff" v-if="user.isModerator"><fa :icon="farBookmark"/></span>
 | 
			
		||||
			<span class="punished" v-if="user.isSilenced"><fa :icon="faMicrophoneSlash"/></span>
 | 
			
		||||
			<span class="punished" v-if="user.isSuspended"><fa :icon="faSnowflake"/></span>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_content actions">
 | 
			
		||||
			<div style="flex: 1; padding-left: 1em;">
 | 
			
		||||
				<mk-switch v-if="user.host == null && $store.state.i.isAdmin && (this.moderator || !user.isAdmin)" @change="toggleModerator()" v-model="moderator">{{ $t('moderator') }}</mk-switch>
 | 
			
		||||
				<mk-switch @change="toggleSilence()" v-model="silenced">{{ $t('silence') }}</mk-switch>
 | 
			
		||||
				<mk-switch @change="toggleSuspend()" v-model="suspended">{{ $t('suspend') }}</mk-switch>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div style="flex: 1; padding-left: 1em;">
 | 
			
		||||
				<mk-button @click="openProfile"><fa :icon="faExternalLinkSquareAlt"/> {{ $t('profile')}}</mk-button>
 | 
			
		||||
				<mk-button v-if="user.host != null" @click="updateRemoteUser"><fa :icon="faSync"/> {{ $t('updateRemoteUser') }}</mk-button>
 | 
			
		||||
				<mk-button @click="resetPassword"><fa :icon="faKey"/> {{ $t('resetPassword') }}</mk-button>
 | 
			
		||||
				<mk-button @click="deleteAllFiles"><fa :icon="faTrashAlt"/> {{ $t('deleteAllFiles') }}</mk-button>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_content rawdata">
 | 
			
		||||
			<pre><code>{{ JSON.stringify(info, null, 2) }}</code></pre>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { faTimes, faBookmark, faKey, faSync, faMicrophoneSlash, faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faSnowflake, faTrashAlt, faBookmark as farBookmark  } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import MkButton from '../../components/ui/button.vue';
 | 
			
		||||
import MkSwitch from '../../components/ui/switch.vue';
 | 
			
		||||
import Progress from '../../scripts/loading';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkButton,
 | 
			
		||||
		MkSwitch,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			user: null,
 | 
			
		||||
			info: null,
 | 
			
		||||
			moderator: false,
 | 
			
		||||
			silenced: false,
 | 
			
		||||
			suspended: false,
 | 
			
		||||
			faTimes, faBookmark, farBookmark, faKey, faSync, faMicrophoneSlash, faSnowflake, faTrashAlt, faExternalLinkSquareAlt
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	watch: {
 | 
			
		||||
		$route: 'fetch'
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		this.fetch();
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		async fetch() {
 | 
			
		||||
			Progress.start();
 | 
			
		||||
			this.user = await this.$root.api('users/show', { userId: this.$route.params.user });
 | 
			
		||||
			this.info = await this.$root.api('admin/show-user', { userId: this.$route.params.user });
 | 
			
		||||
			this.moderator = this.info.isModerator;
 | 
			
		||||
			this.silenced = this.info.isSilenced;
 | 
			
		||||
			this.suspended = this.info.isSuspended;
 | 
			
		||||
			Progress.done();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		/** 処理対象ユーザーの情報を更新する */
 | 
			
		||||
		async refreshUser() {
 | 
			
		||||
			this.user = await this.$root.api('users/show', { userId: this.user.id });
 | 
			
		||||
			this.info = await this.$root.api('admin/show-user', { userId: this.user.id });
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		openProfile() {
 | 
			
		||||
			window.open(Vue.filter('userPage')(this.user, null, true), '_blank');
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async updateRemoteUser() {
 | 
			
		||||
			await this.$root.api('admin/update-remote-user', { userId: this.user.id }).then(res => {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
					type: 'success',
 | 
			
		||||
					iconOnly: true, autoClose: true
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
			await this.refreshUser();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async resetPassword() {
 | 
			
		||||
			const dialog = this.$root.dialog({
 | 
			
		||||
				type: 'waiting',
 | 
			
		||||
				iconOnly: true
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			this.$root.api('admin/reset-password', {
 | 
			
		||||
				userId: this.user.id,
 | 
			
		||||
			}).then(({ password }) => {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
					type: 'success',
 | 
			
		||||
					text: this.$t('newPasswordIs', { password })
 | 
			
		||||
				});
 | 
			
		||||
			}).catch(e => {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: e
 | 
			
		||||
				});
 | 
			
		||||
			}).finally(() => {
 | 
			
		||||
				dialog.close();
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async toggleSilence() {
 | 
			
		||||
			const confirm = await this.$root.dialog({
 | 
			
		||||
				type: 'warning',
 | 
			
		||||
				showCancelButton: true,
 | 
			
		||||
				text: this.silenced ? this.$t('silenceConfirm') : this.$t('unsilenceConfirm'),
 | 
			
		||||
			});
 | 
			
		||||
			if (confirm.canceled) {
 | 
			
		||||
				this.silenced = !this.silenced;
 | 
			
		||||
			} else {
 | 
			
		||||
				await this.$root.api(this.silenced ? 'admin/silence-user' : 'admin/unsilence-user', { userId: this.user.id });
 | 
			
		||||
				await this.refreshUser();
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async toggleSuspend() {
 | 
			
		||||
			const confirm = await this.$root.dialog({
 | 
			
		||||
				type: 'warning',
 | 
			
		||||
				showCancelButton: true,
 | 
			
		||||
				text: this.suspended ? this.$t('suspendConfirm') : this.$t('unsuspendConfirm'),
 | 
			
		||||
			});
 | 
			
		||||
			if (confirm.canceled) {
 | 
			
		||||
				this.suspended = !this.suspended;
 | 
			
		||||
			} else {
 | 
			
		||||
				await this.$root.api(this.suspended ? 'admin/suspend-user' : 'admin/unsuspend-user', { userId: this.user.id });
 | 
			
		||||
				await this.refreshUser();
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async toggleModerator() {
 | 
			
		||||
			await this.$root.api(this.moderator ? 'admin/moderators/add' : 'admin/moderators/remove', { userId: this.user.id });
 | 
			
		||||
			await this.refreshUser();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async deleteAllFiles() {
 | 
			
		||||
			const confirm = await this.$root.dialog({
 | 
			
		||||
				type: 'warning',
 | 
			
		||||
				showCancelButton: true,
 | 
			
		||||
				text: this.$t('deleteAllFilesConfirm'),
 | 
			
		||||
			});
 | 
			
		||||
			if (confirm.canceled) return;
 | 
			
		||||
			const process = async () => {
 | 
			
		||||
				await this.$root.api('admin/delete-all-files-of-a-user', { userId: this.user.id });
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
					type: 'success',
 | 
			
		||||
					iconOnly: true, autoClose: true
 | 
			
		||||
				});
 | 
			
		||||
			};
 | 
			
		||||
			await process().catch(e => {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: e.toString()
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
			await this.refreshUser();
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.vrcsvlkm {
 | 
			
		||||
	display: flex;
 | 
			
		||||
	flex-direction: column;
 | 
			
		||||
 | 
			
		||||
	> ._card {
 | 
			
		||||
		> .actions {
 | 
			
		||||
			display: flex;
 | 
			
		||||
			box-sizing: border-box;
 | 
			
		||||
			text-align: left;
 | 
			
		||||
			align-items: center;
 | 
			
		||||
			margin-top: 16px;
 | 
			
		||||
			margin-bottom: 16px;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> .rawdata {
 | 
			
		||||
			> pre > code {
 | 
			
		||||
				display: block;
 | 
			
		||||
				width: 100%;
 | 
			
		||||
				height: 100%;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,33 +1,33 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="mk-instance-users">
 | 
			
		||||
	<portal to="icon"><fa :icon="faUsers"/></portal>
 | 
			
		||||
	<portal to="title">{{ $t('users') }}</portal>
 | 
			
		||||
 | 
			
		||||
	<section class="_card _vMargin lookup">
 | 
			
		||||
		<div class="_title"><fa :icon="faSearch"/> {{ $t('lookup') }}</div>
 | 
			
		||||
	<div class="_section">
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-input class="target" v-model="target" type="text" @enter="showUser()">
 | 
			
		||||
				<span>{{ $t('usernameOrUserId') }}</span>
 | 
			
		||||
			</mk-input>
 | 
			
		||||
			<mk-button @click="showUser()" primary><fa :icon="faSearch"/> {{ $t('lookup') }}</mk-button>
 | 
			
		||||
			<MkButton inline primary @click="addUser()"><Fa :icon="faPlus"/> {{ $t('addUser') }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_footer">
 | 
			
		||||
			<mk-button inline primary @click="searchUser()"><fa :icon="faSearch"/> {{ $t('search') }}</mk-button>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<section class="_card _vMargin users">
 | 
			
		||||
		<div class="_title"><fa :icon="faUsers"/> {{ $t('users') }}</div>
 | 
			
		||||
	<div class="_section lookup">
 | 
			
		||||
		<div class="_title"><Fa :icon="faSearch"/> {{ $t('lookup') }}</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<MkInput class="target" v-model:value="target" type="text" @enter="showUser()">
 | 
			
		||||
				<span>{{ $t('usernameOrUserId') }}</span>
 | 
			
		||||
			</MkInput>
 | 
			
		||||
			<MkButton @click="showUser()" primary><Fa :icon="faSearch"/> {{ $t('lookup') }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<div class="_section users">
 | 
			
		||||
		<div class="_title"><Fa :icon="faUsers"/> {{ $t('users') }}</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<div class="inputs" style="display: flex;">
 | 
			
		||||
				<mk-select v-model="sort" style="margin: 0; flex: 1;">
 | 
			
		||||
				<MkSelect v-model:value="sort" style="margin: 0; flex: 1;">
 | 
			
		||||
					<template #label>{{ $t('sort') }}</template>
 | 
			
		||||
					<option value="-createdAt">{{ $t('registeredDate') }} ({{ $t('ascendingOrder') }})</option>
 | 
			
		||||
					<option value="+createdAt">{{ $t('registeredDate') }} ({{ $t('descendingOrder') }})</option>
 | 
			
		||||
					<option value="-updatedAt">{{ $t('lastUsed') }} ({{ $t('ascendingOrder') }})</option>
 | 
			
		||||
					<option value="+updatedAt">{{ $t('lastUsed') }} ({{ $t('descendingOrder') }})</option>
 | 
			
		||||
				</mk-select>
 | 
			
		||||
				<mk-select v-model="state" style="margin: 0; flex: 1;">
 | 
			
		||||
				</MkSelect>
 | 
			
		||||
				<MkSelect v-model:value="state" style="margin: 0; flex: 1;">
 | 
			
		||||
					<template #label>{{ $t('state') }}</template>
 | 
			
		||||
					<option value="all">{{ $t('all') }}</option>
 | 
			
		||||
					<option value="available">{{ $t('normal') }}</option>
 | 
			
		||||
@@ -35,71 +35,62 @@
 | 
			
		||||
					<option value="moderator">{{ $t('moderator') }}</option>
 | 
			
		||||
					<option value="silenced">{{ $t('silence') }}</option>
 | 
			
		||||
					<option value="suspended">{{ $t('suspend') }}</option>
 | 
			
		||||
				</mk-select>
 | 
			
		||||
				<mk-select v-model="origin" style="margin: 0; flex: 1;">
 | 
			
		||||
				</MkSelect>
 | 
			
		||||
				<MkSelect v-model:value="origin" style="margin: 0; flex: 1;">
 | 
			
		||||
					<template #label>{{ $t('instance') }}</template>
 | 
			
		||||
					<option value="combined">{{ $t('all') }}</option>
 | 
			
		||||
					<option value="local">{{ $t('local') }}</option>
 | 
			
		||||
					<option value="remote">{{ $t('remote') }}</option>
 | 
			
		||||
				</mk-select>
 | 
			
		||||
				</MkSelect>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="inputs" style="display: flex; padding-top: 1.2em;">
 | 
			
		||||
				<mk-input v-model="searchUsername" style="margin: 0; flex: 1;" type="text" spellcheck="false" @input="$refs.users.reload()">
 | 
			
		||||
				<MkInput v-model:value="searchUsername" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:value="$refs.users.reload()">
 | 
			
		||||
					<span>{{ $t('username') }}</span>
 | 
			
		||||
				</mk-input>
 | 
			
		||||
				<mk-input v-model="searchHost" style="margin: 0; flex: 1;" type="text" spellcheck="false" @input="$refs.users.reload()" :disabled="pagination.params().origin === 'local'">
 | 
			
		||||
				</MkInput>
 | 
			
		||||
				<MkInput v-model:value="searchHost" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:value="$refs.users.reload()" :disabled="pagination.params().origin === 'local'">
 | 
			
		||||
					<span>{{ $t('host') }}</span>
 | 
			
		||||
				</mk-input>
 | 
			
		||||
				</MkInput>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_content _list">
 | 
			
		||||
			<mk-pagination :pagination="pagination" #default="{items}" class="users" ref="users" :auto-margin="false">
 | 
			
		||||
				<button class="user _button _listItem" v-for="(user, i) in items" :key="user.id" @click="show(user)">
 | 
			
		||||
					<mk-avatar class="avatar" :user="user" :disable-link="true"/>
 | 
			
		||||
 | 
			
		||||
			<MkPagination :pagination="pagination" #default="{items}" class="users" ref="users" :auto-margin="false">
 | 
			
		||||
				<button class="user _panel _button _vMargin" v-for="user in items" :key="user.id" @click="show(user)">
 | 
			
		||||
					<MkAvatar class="avatar" :user="user" :disable-link="true"/>
 | 
			
		||||
					<div class="body">
 | 
			
		||||
						<header>
 | 
			
		||||
							<mk-user-name class="name" :user="user"/>
 | 
			
		||||
							<span class="acct">@{{ user | acct }}</span>
 | 
			
		||||
							<span class="staff" v-if="user.isAdmin"><fa :icon="faBookmark"/></span>
 | 
			
		||||
							<span class="staff" v-if="user.isModerator"><fa :icon="farBookmark"/></span>
 | 
			
		||||
							<span class="punished" v-if="user.isSilenced"><fa :icon="faMicrophoneSlash"/></span>
 | 
			
		||||
							<span class="punished" v-if="user.isSuspended"><fa :icon="faSnowflake"/></span>
 | 
			
		||||
							<MkUserName class="name" :user="user"/>
 | 
			
		||||
							<span class="acct">@{{ acct(user) }}</span>
 | 
			
		||||
							<span class="staff" v-if="user.isAdmin"><Fa :icon="faBookmark"/></span>
 | 
			
		||||
							<span class="staff" v-if="user.isModerator"><Fa :icon="farBookmark"/></span>
 | 
			
		||||
							<span class="punished" v-if="user.isSilenced"><Fa :icon="faMicrophoneSlash"/></span>
 | 
			
		||||
							<span class="punished" v-if="user.isSuspended"><Fa :icon="faSnowflake"/></span>
 | 
			
		||||
						</header>
 | 
			
		||||
						<div>
 | 
			
		||||
							<span>{{ $t('lastUsed') }}: <mk-time :time="user.updatedAt" mode="detail"/></span>
 | 
			
		||||
							<span>{{ $t('lastUsed') }}: <MkTime v-if="user.updatedAt" :time="user.updatedAt" mode="detail"/></span>
 | 
			
		||||
						</div>
 | 
			
		||||
						<div>
 | 
			
		||||
							<span>{{ $t('registeredDate') }}: <mk-time :time="user.createdAt" mode="detail"/></span>
 | 
			
		||||
							<span>{{ $t('registeredDate') }}: <MkTime :time="user.createdAt" mode="detail"/></span>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</button>
 | 
			
		||||
			</mk-pagination>
 | 
			
		||||
			</MkPagination>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_footer">
 | 
			
		||||
			<mk-button inline primary @click="addUser()"><fa :icon="faPlus"/> {{ $t('addUser') }}</mk-button>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faPlus, faUsers, faSearch, faBookmark, faMicrophoneSlash } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faSnowflake, faBookmark as farBookmark } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import parseAcct from '../../../misc/acct/parse';
 | 
			
		||||
import MkButton from '../../components/ui/button.vue';
 | 
			
		||||
import MkInput from '../../components/ui/input.vue';
 | 
			
		||||
import MkSelect from '../../components/ui/select.vue';
 | 
			
		||||
import MkPagination from '../../components/ui/pagination.vue';
 | 
			
		||||
import MkUserSelect from '../../components/user-select.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: `${this.$t('users')} | ${this.$t('instance')}`
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import MkInput from '@/components/ui/input.vue';
 | 
			
		||||
import MkSelect from '@/components/ui/select.vue';
 | 
			
		||||
import MkPagination from '@/components/ui/pagination.vue';
 | 
			
		||||
import { acct } from '../../filters/user';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkButton,
 | 
			
		||||
		MkInput,
 | 
			
		||||
@@ -109,6 +100,16 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('users'),
 | 
			
		||||
					icon: faUsers
 | 
			
		||||
				}],
 | 
			
		||||
				action: {
 | 
			
		||||
					icon: faSearch,
 | 
			
		||||
					handler: this.searchUser
 | 
			
		||||
				}
 | 
			
		||||
			},
 | 
			
		||||
			target: '',
 | 
			
		||||
			sort: '+createdAt',
 | 
			
		||||
			state: 'all',
 | 
			
		||||
@@ -147,12 +148,12 @@ export default Vue.extend({
 | 
			
		||||
		/** テキストエリアのユーザーを解決する */
 | 
			
		||||
		fetchUser() {
 | 
			
		||||
			return new Promise((res) => {
 | 
			
		||||
				const usernamePromise = this.$root.api('users/show', parseAcct(this.target));
 | 
			
		||||
				const idPromise = this.$root.api('users/show', { userId: this.target });
 | 
			
		||||
				const usernamePromise = os.api('users/show', parseAcct(this.target));
 | 
			
		||||
				const idPromise = os.api('users/show', { userId: this.target });
 | 
			
		||||
				let _notFound = false;
 | 
			
		||||
				const notFound = () => {
 | 
			
		||||
					if (_notFound) {
 | 
			
		||||
						this.$root.dialog({
 | 
			
		||||
						os.dialog({
 | 
			
		||||
							type: 'error',
 | 
			
		||||
							text: this.$t('noSuchUser')
 | 
			
		||||
						});
 | 
			
		||||
@@ -179,51 +180,39 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		searchUser() {
 | 
			
		||||
			this.$root.new(MkUserSelect, {}).$once('selected', user => {
 | 
			
		||||
			os.selectUser().then(user => {
 | 
			
		||||
				this.show(user);
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async addUser() {
 | 
			
		||||
			const { canceled: canceled1, result: username } = await this.$root.dialog({
 | 
			
		||||
			const { canceled: canceled1, result: username } = await os.dialog({
 | 
			
		||||
				title: this.$t('username'),
 | 
			
		||||
				input: true
 | 
			
		||||
			});
 | 
			
		||||
			if (canceled1) return;
 | 
			
		||||
 | 
			
		||||
			const { canceled: canceled2, result: password } = await this.$root.dialog({
 | 
			
		||||
			const { canceled: canceled2, result: password } = await os.dialog({
 | 
			
		||||
				title: this.$t('password'),
 | 
			
		||||
				input: { type: 'password' }
 | 
			
		||||
			});
 | 
			
		||||
			if (canceled2) return;
 | 
			
		||||
 | 
			
		||||
			const dialog = this.$root.dialog({
 | 
			
		||||
				type: 'waiting',
 | 
			
		||||
				iconOnly: true
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			this.$root.api('admin/accounts/create', {
 | 
			
		||||
			os.apiWithDialog('admin/accounts/create', {
 | 
			
		||||
				username: username,
 | 
			
		||||
				password: password,
 | 
			
		||||
			}).then(res => {
 | 
			
		||||
				this.$refs.users.reload();
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
					type: 'success',
 | 
			
		||||
					iconOnly: true, autoClose: true
 | 
			
		||||
				});
 | 
			
		||||
			}).catch(e => {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: e.id
 | 
			
		||||
				});
 | 
			
		||||
			}).finally(() => {
 | 
			
		||||
				dialog.close();
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async show(user) {
 | 
			
		||||
			this.$router.push('./users/' + user.id);
 | 
			
		||||
		}
 | 
			
		||||
			os.popup(await import('./user-dialog.vue'), {
 | 
			
		||||
				userId: user.id
 | 
			
		||||
			}, {}, 'closed');
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		acct
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
@@ -232,28 +221,32 @@ export default Vue.extend({
 | 
			
		||||
.mk-instance-users {
 | 
			
		||||
	> .users {
 | 
			
		||||
		> ._content {
 | 
			
		||||
			max-height: 300px;
 | 
			
		||||
			overflow: auto;
 | 
			
		||||
			
 | 
			
		||||
			> .users {
 | 
			
		||||
				margin-top: var(--margin);
 | 
			
		||||
 | 
			
		||||
				> .user {
 | 
			
		||||
					display: flex;
 | 
			
		||||
					width: 100%;
 | 
			
		||||
					box-sizing: border-box;
 | 
			
		||||
					text-align: left;
 | 
			
		||||
					align-items: center;
 | 
			
		||||
					padding: 16px;
 | 
			
		||||
 | 
			
		||||
					&:hover {
 | 
			
		||||
						color: var(--accent);
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					> .avatar {
 | 
			
		||||
						width: 64px;
 | 
			
		||||
						height: 64px;
 | 
			
		||||
						width: 60px;
 | 
			
		||||
						height: 60px;
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					> .body {
 | 
			
		||||
						margin-left: 0.3em;
 | 
			
		||||
						padding: 8px;
 | 
			
		||||
						padding: 0 8px;
 | 
			
		||||
						flex: 1;
 | 
			
		||||
 | 
			
		||||
						@media (max-width 500px) {
 | 
			
		||||
						@media (max-width: 500px) {
 | 
			
		||||
							font-size: 14px;
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,30 +1,28 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
	<portal to="icon"><fa :icon="faAt"/></portal>
 | 
			
		||||
	<portal to="title">{{ $t('mentions') }}</portal>
 | 
			
		||||
	<x-notes :pagination="pagination" @before="before()" @after="after()"/>
 | 
			
		||||
<div class="_section">
 | 
			
		||||
	<XNotes class="_content" :pagination="pagination" @before="before()" @after="after()"/>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faAt } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import Progress from '../scripts/loading';
 | 
			
		||||
import XNotes from '../components/notes.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: this.$t('mentions') as string
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
import Progress from '@/scripts/loading';
 | 
			
		||||
import XNotes from '@/components/notes.vue';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XNotes
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('mentions'),
 | 
			
		||||
					icon: faAt
 | 
			
		||||
				}],
 | 
			
		||||
			},
 | 
			
		||||
			pagination: {
 | 
			
		||||
				endpoint: 'notes/mentions',
 | 
			
		||||
				limit: 10,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,30 +1,28 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
	<portal to="icon"><fa :icon="faEnvelope"/></portal>
 | 
			
		||||
	<portal to="title">{{ $t('directNotes') }}</portal>
 | 
			
		||||
	<x-notes :pagination="pagination" @before="before()" @after="after()"/>
 | 
			
		||||
	<XNotes :pagination="pagination" @before="before()" @after="after()"/>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faEnvelope } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import Progress from '../scripts/loading';
 | 
			
		||||
import XNotes from '../components/notes.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: this.$t('directNotes') as string
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
import Progress from '@/scripts/loading';
 | 
			
		||||
import XNotes from '@/components/notes.vue';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XNotes
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('directNotes'),
 | 
			
		||||
					icon: faEnvelope
 | 
			
		||||
				}],
 | 
			
		||||
			},
 | 
			
		||||
			pagination: {
 | 
			
		||||
				endpoint: 'notes/mentions',
 | 
			
		||||
				limit: 10,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,58 +1,66 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="mk-messaging" v-size="{ max: [400] }">
 | 
			
		||||
	<portal to="icon"><fa :icon="faComments"/></portal>
 | 
			
		||||
	<portal to="title">{{ $t('messaging') }}</portal>
 | 
			
		||||
<div class="_section">
 | 
			
		||||
	<div class="mk-messaging _content" v-size="{ max: [400] }">
 | 
			
		||||
		<MkButton @click="start" primary class="start"><Fa :icon="faPlus"/> {{ $t('startMessaging') }}</MkButton>
 | 
			
		||||
 | 
			
		||||
	<mk-button @click="start" primary class="start"><fa :icon="faPlus"/> {{ $t('startMessaging') }}</mk-button>
 | 
			
		||||
 | 
			
		||||
	<div class="history" v-if="messages.length > 0">
 | 
			
		||||
		<router-link v-for="(message, i) in messages"
 | 
			
		||||
			class="message _panel"
 | 
			
		||||
			:to="message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`"
 | 
			
		||||
			:data-is-me="isMe(message)"
 | 
			
		||||
			:data-is-read="message.groupId ? message.reads.includes($store.state.i.id) : message.isRead"
 | 
			
		||||
			:data-index="i"
 | 
			
		||||
			:key="message.id"
 | 
			
		||||
		>
 | 
			
		||||
			<div>
 | 
			
		||||
				<mk-avatar class="avatar" :user="message.groupId ? message.user : isMe(message) ? message.recipient : message.user"/>
 | 
			
		||||
				<header v-if="message.groupId">
 | 
			
		||||
					<span class="name">{{ message.group.name }}</span>
 | 
			
		||||
					<mk-time :time="message.createdAt"/>
 | 
			
		||||
				</header>
 | 
			
		||||
				<header v-else>
 | 
			
		||||
					<span class="name"><mk-user-name :user="isMe(message) ? message.recipient : message.user"/></span>
 | 
			
		||||
					<span class="username">@{{ isMe(message) ? message.recipient : message.user | acct }}</span>
 | 
			
		||||
					<mk-time :time="message.createdAt"/>
 | 
			
		||||
				</header>
 | 
			
		||||
				<div class="body">
 | 
			
		||||
					<p class="text"><span class="me" v-if="isMe(message)">{{ $t('you') }}:</span>{{ message.text }}</p>
 | 
			
		||||
		<div class="history" v-if="messages.length > 0">
 | 
			
		||||
			<router-link v-for="(message, i) in messages"
 | 
			
		||||
				class="message _panel"
 | 
			
		||||
				:class="{ isMe: isMe(message), isRead: message.groupId ? message.reads.includes($store.state.i.id) : message.isRead }"
 | 
			
		||||
				:to="message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`"
 | 
			
		||||
				:data-index="i"
 | 
			
		||||
				:key="message.id"
 | 
			
		||||
				@click.prevent="go(message)"
 | 
			
		||||
			>
 | 
			
		||||
				<div>
 | 
			
		||||
					<MkAvatar class="avatar" :user="message.groupId ? message.user : isMe(message) ? message.recipient : message.user"/>
 | 
			
		||||
					<header v-if="message.groupId">
 | 
			
		||||
						<span class="name">{{ message.group.name }}</span>
 | 
			
		||||
						<MkTime :time="message.createdAt"/>
 | 
			
		||||
					</header>
 | 
			
		||||
					<header v-else>
 | 
			
		||||
						<span class="name"><MkUserName :user="isMe(message) ? message.recipient : message.user"/></span>
 | 
			
		||||
						<span class="username">@{{ acct(isMe(message) ? message.recipient : message.user) }}</span>
 | 
			
		||||
						<MkTime :time="message.createdAt"/>
 | 
			
		||||
					</header>
 | 
			
		||||
					<div class="body">
 | 
			
		||||
						<p class="text"><span class="me" v-if="isMe(message)">{{ $t('you') }}:</span>{{ message.text }}</p>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</router-link>
 | 
			
		||||
			</router-link>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_fullinfo" v-if="!fetching && messages.length == 0">
 | 
			
		||||
			<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
 | 
			
		||||
			<div>{{ $t('noHistory') }}</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<MkLoading v-if="fetching"/>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="_fullinfo" v-if="!fetching && messages.length == 0">
 | 
			
		||||
		<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
 | 
			
		||||
		<div>{{ $t('noHistory') }}</div>
 | 
			
		||||
	</div>
 | 
			
		||||
	<mk-loading v-if="fetching"/>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineAsyncComponent, defineComponent } from 'vue';
 | 
			
		||||
import { faUser, faUsers, faComments, faPlus } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import getAcct from '../../../misc/acct/render';
 | 
			
		||||
import MkButton from '../../components/ui/button.vue';
 | 
			
		||||
import MkUserSelect from '../../components/user-select.vue';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import { acct } from '../../filters/user';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkButton
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	inject: ['navHook'],
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('messaging'),
 | 
			
		||||
					icon: faComments
 | 
			
		||||
				}]
 | 
			
		||||
			},
 | 
			
		||||
			fetching: true,
 | 
			
		||||
			moreFetching: false,
 | 
			
		||||
			messages: [],
 | 
			
		||||
@@ -62,13 +70,13 @@ export default Vue.extend({
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	mounted() {
 | 
			
		||||
		this.connection = this.$root.stream.useSharedConnection('messagingIndex');
 | 
			
		||||
		this.connection = os.stream.useSharedConnection('messagingIndex');
 | 
			
		||||
 | 
			
		||||
		this.connection.on('message', this.onMessage);
 | 
			
		||||
		this.connection.on('read', this.onRead);
 | 
			
		||||
 | 
			
		||||
		this.$root.api('messaging/history', { group: false }).then(userMessages => {
 | 
			
		||||
			this.$root.api('messaging/history', { group: true }).then(groupMessages => {
 | 
			
		||||
		os.api('messaging/history', { group: false }).then(userMessages => {
 | 
			
		||||
			os.api('messaging/history', { group: true }).then(groupMessages => {
 | 
			
		||||
				const messages = userMessages.concat(groupMessages);
 | 
			
		||||
				messages.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
 | 
			
		||||
				this.messages = messages;
 | 
			
		||||
@@ -77,11 +85,23 @@ export default Vue.extend({
 | 
			
		||||
		});
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	beforeDestroy() {
 | 
			
		||||
	beforeUnmount() {
 | 
			
		||||
		this.connection.dispose();
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		go(message) {
 | 
			
		||||
			const url = message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(this.isMe(message) ? message.recipient : message.user)}`;
 | 
			
		||||
			if (this.navHook) {
 | 
			
		||||
				this.navHook(url, defineAsyncComponent(() => import('@/pages/messaging/messaging-room.vue')), {
 | 
			
		||||
					userAcct: message.groupId ? null : getAcct(this.isMe(message) ? message.recipient : message.user),
 | 
			
		||||
					groupId: message.groupId
 | 
			
		||||
				});
 | 
			
		||||
			} else {
 | 
			
		||||
				this.$router.push(url);
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		getAcct,
 | 
			
		||||
 | 
			
		||||
		isMe(message) {
 | 
			
		||||
@@ -115,39 +135,35 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		start(ev) {
 | 
			
		||||
			this.$root.menu({
 | 
			
		||||
				items: [{
 | 
			
		||||
					text: this.$t('messagingWithUser'),
 | 
			
		||||
					icon: faUser,
 | 
			
		||||
					action: () => { this.startUser() }
 | 
			
		||||
				}, {
 | 
			
		||||
					text: this.$t('messagingWithGroup'),
 | 
			
		||||
					icon: faUsers,
 | 
			
		||||
					action: () => { this.startGroup() }
 | 
			
		||||
				}],
 | 
			
		||||
				noCenter: true,
 | 
			
		||||
				source: ev.currentTarget || ev.target,
 | 
			
		||||
			});
 | 
			
		||||
			os.modalMenu([{
 | 
			
		||||
				text: this.$t('messagingWithUser'),
 | 
			
		||||
				icon: faUser,
 | 
			
		||||
				action: () => { this.startUser() }
 | 
			
		||||
			}, {
 | 
			
		||||
				text: this.$t('messagingWithGroup'),
 | 
			
		||||
				icon: faUsers,
 | 
			
		||||
				action: () => { this.startGroup() }
 | 
			
		||||
			}], ev.currentTarget || ev.target);
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async startUser() {
 | 
			
		||||
			this.$root.new(MkUserSelect, {}).$once('selected', user => {
 | 
			
		||||
			os.selectUser().then(user => {
 | 
			
		||||
				this.$router.push(`/my/messaging/${getAcct(user)}`);
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async startGroup() {
 | 
			
		||||
			const groups1 = await this.$root.api('users/groups/owned');
 | 
			
		||||
			const groups2 = await this.$root.api('users/groups/joined');
 | 
			
		||||
			const groups1 = await os.api('users/groups/owned');
 | 
			
		||||
			const groups2 = await os.api('users/groups/joined');
 | 
			
		||||
			if (groups1.length === 0 && groups2.length === 0) {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
				os.dialog({
 | 
			
		||||
					type: 'warning',
 | 
			
		||||
					title: this.$t('youHaveNoGroups'),
 | 
			
		||||
					text: this.$t('joinOrCreateGroup'),
 | 
			
		||||
				});
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
			const { canceled, result: group } = await this.$root.dialog({
 | 
			
		||||
			const { canceled, result: group } = await os.dialog({
 | 
			
		||||
				type: null,
 | 
			
		||||
				title: this.$t('group'),
 | 
			
		||||
				select: {
 | 
			
		||||
@@ -159,7 +175,9 @@ export default Vue.extend({
 | 
			
		||||
			});
 | 
			
		||||
			if (canceled) return;
 | 
			
		||||
			this.$router.push(`/my/messaging/group/${group.id}`);
 | 
			
		||||
		}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		acct
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
@@ -191,12 +209,12 @@ export default Vue.extend({
 | 
			
		||||
			&:active {
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&[data-is-read],
 | 
			
		||||
			&[data-is-me] {
 | 
			
		||||
			&.isRead,
 | 
			
		||||
			&.isMe {
 | 
			
		||||
				opacity: 0.8;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&:not([data-is-me]):not([data-is-read]) {
 | 
			
		||||
			&:not(.isMe):not(.isRead) {
 | 
			
		||||
				> div {
 | 
			
		||||
					background-image: url("/assets/unread.svg");
 | 
			
		||||
					background-repeat: no-repeat;
 | 
			
		||||
@@ -283,7 +301,7 @@ export default Vue.extend({
 | 
			
		||||
	&.max-width_400px {
 | 
			
		||||
		> .history {
 | 
			
		||||
			> .message {
 | 
			
		||||
				&:not([data-is-me]):not([data-is-read]) {
 | 
			
		||||
				&:not(.isMe):not(.isRead) {
 | 
			
		||||
					> div {
 | 
			
		||||
						background-image: none;
 | 
			
		||||
						border-left: solid 4px #3aa2dc;
 | 
			
		||||
 
 | 
			
		||||
@@ -9,31 +9,28 @@
 | 
			
		||||
		@keypress="onKeypress"
 | 
			
		||||
		@paste="onPaste"
 | 
			
		||||
		:placeholder="$t('inputMessageHere')"
 | 
			
		||||
		v-autocomplete="{ model: 'text' }"
 | 
			
		||||
	></textarea>
 | 
			
		||||
	<div class="file" @click="file = null" v-if="file">{{ file.name }}</div>
 | 
			
		||||
	<x-uploader ref="uploader" @uploaded="onUploaded"/>
 | 
			
		||||
	<button class="send _button" @click="send" :disabled="!canSend || sending" :title="$t('send')">
 | 
			
		||||
		<template v-if="!sending"><fa :icon="faPaperPlane"/></template><template v-if="sending"><fa icon="spinner .spin"/></template>
 | 
			
		||||
		<template v-if="!sending"><Fa :icon="faPaperPlane"/></template><template v-if="sending"><Fa icon="spinner .spin"/></template>
 | 
			
		||||
	</button>
 | 
			
		||||
	<button class="_button" @click="chooseFile"><fa :icon="faPhotoVideo"/></button>
 | 
			
		||||
	<button class="_button" @click="insertEmoji"><fa :icon="faLaughSquint"/></button>
 | 
			
		||||
	<button class="_button" @click="chooseFile"><Fa :icon="faPhotoVideo"/></button>
 | 
			
		||||
	<button class="_button" @click="insertEmoji"><Fa :icon="faLaughSquint"/></button>
 | 
			
		||||
	<input ref="file" type="file" @change="onChangeFile"/>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent, defineAsyncComponent } from 'vue';
 | 
			
		||||
import { faPaperPlane, faPhotoVideo, faLaughSquint } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import insertTextAtCursor from 'insert-text-at-cursor';
 | 
			
		||||
import * as autosize from 'autosize';
 | 
			
		||||
import { formatTimeString } from '../../../misc/format-time-string';
 | 
			
		||||
import { selectFile } from '../../scripts/select-file';
 | 
			
		||||
import { selectFile } from '@/scripts/select-file';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { Autocomplete } from '@/scripts/autocomplete';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	components: {
 | 
			
		||||
		XUploader: () => import('../../components/uploader.vue').then(m => m.default),
 | 
			
		||||
	},
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	props: {
 | 
			
		||||
		user: {
 | 
			
		||||
			type: Object,
 | 
			
		||||
@@ -69,15 +66,14 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
		file() {
 | 
			
		||||
			this.saveDraft();
 | 
			
		||||
 | 
			
		||||
			if (this.room.isBottom()) {
 | 
			
		||||
				this.room.scrollToBottom();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	mounted() {
 | 
			
		||||
		autosize(this.$refs.text);
 | 
			
		||||
 | 
			
		||||
		// TODO: detach when unmount
 | 
			
		||||
		new Autocomplete(this.$refs.text, this, { model: 'text' });
 | 
			
		||||
 | 
			
		||||
		// 書きかけの投稿を復元
 | 
			
		||||
		const draft = JSON.parse(localStorage.getItem('message_drafts') || '{}')[this.draftKey];
 | 
			
		||||
		if (draft) {
 | 
			
		||||
@@ -97,7 +93,7 @@ export default Vue.extend({
 | 
			
		||||
					const ext = lio >= 0 ? file.name.slice(lio) : '';
 | 
			
		||||
					const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.settings.pastedFileName).replace(/{{number}}/g, '1')}${ext}`;
 | 
			
		||||
					const name = this.$store.state.settings.pasteDialog
 | 
			
		||||
						? await this.$root.dialog({
 | 
			
		||||
						? await os.dialog({
 | 
			
		||||
							title: this.$t('enterFileName'),
 | 
			
		||||
							input: {
 | 
			
		||||
								default: formatted
 | 
			
		||||
@@ -109,7 +105,7 @@ export default Vue.extend({
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				if (items[0].kind == 'file') {
 | 
			
		||||
					this.$root.dialog({
 | 
			
		||||
					os.dialog({
 | 
			
		||||
						type: 'error',
 | 
			
		||||
						text: this.$t('onlyOneFileCanBeAttached')
 | 
			
		||||
					});
 | 
			
		||||
@@ -119,7 +115,7 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
		onDragover(e) {
 | 
			
		||||
			const isFile = e.dataTransfer.items[0].kind == 'file';
 | 
			
		||||
			const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file';
 | 
			
		||||
			const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
 | 
			
		||||
			if (isFile || isDriveFile) {
 | 
			
		||||
				e.preventDefault();
 | 
			
		||||
				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
 | 
			
		||||
@@ -134,7 +130,7 @@ export default Vue.extend({
 | 
			
		||||
				return;
 | 
			
		||||
			} else if (e.dataTransfer.files.length > 1) {
 | 
			
		||||
				e.preventDefault();
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
				os.dialog({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: this.$t('onlyOneFileCanBeAttached')
 | 
			
		||||
				});
 | 
			
		||||
@@ -142,7 +138,7 @@ export default Vue.extend({
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			//#region ドライブのファイル
 | 
			
		||||
			const driveFile = e.dataTransfer.getData('mk_drive_file');
 | 
			
		||||
			const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
 | 
			
		||||
			if (driveFile != null && driveFile != '') {
 | 
			
		||||
				this.file = JSON.parse(driveFile);
 | 
			
		||||
				e.preventDefault();
 | 
			
		||||
@@ -157,7 +153,7 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		chooseFile(e) {
 | 
			
		||||
			selectFile(this, e.currentTarget || e.target, this.$t('selectFile'), false).then(file => {
 | 
			
		||||
			selectFile(e.currentTarget || e.target, this.$t('selectFile'), false).then(file => {
 | 
			
		||||
				this.file = file;
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
@@ -167,16 +163,14 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		upload(file: File, name?: string) {
 | 
			
		||||
			(this.$refs.uploader as any).upload(file, this.$store.state.settings.uploadFolder, name);
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onUploaded(file) {
 | 
			
		||||
			this.file = file;
 | 
			
		||||
			os.upload(file, this.$store.state.settings.uploadFolder, name).then(res => {
 | 
			
		||||
				this.file = res;
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		send() {
 | 
			
		||||
			this.sending = true;
 | 
			
		||||
			this.$root.api('messaging/messages/create', {
 | 
			
		||||
			os.api('messaging/messages/create', {
 | 
			
		||||
				userId: this.user ? this.user.id : undefined,
 | 
			
		||||
				groupId: this.group ? this.group.id : undefined,
 | 
			
		||||
				text: this.text ? this.text : undefined,
 | 
			
		||||
@@ -219,11 +213,8 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async insertEmoji(ev) {
 | 
			
		||||
			const vm = this.$root.new(await import('../../components/emoji-picker.vue').then(m => m.default), {
 | 
			
		||||
				source: ev.currentTarget || ev.target
 | 
			
		||||
			}).$once('chosen', emoji => {
 | 
			
		||||
			os.pickEmoji(ev.currentTarget || ev.target).then(emoji => {
 | 
			
		||||
				insertTextAtCursor(this.$refs.text, emoji);
 | 
			
		||||
				vm.close();
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,13 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="thvuemwp" :data-is-me="isMe">
 | 
			
		||||
	<mk-avatar class="avatar" :user="message.user"/>
 | 
			
		||||
<div class="thvuemwp" :class="{ isMe }">
 | 
			
		||||
	<MkAvatar class="avatar" :user="message.user"/>
 | 
			
		||||
	<div class="content">
 | 
			
		||||
		<div class="balloon" :data-no-text="message.text == null">
 | 
			
		||||
		<div class="balloon" :class="{ noText: message.text == null }" >
 | 
			
		||||
			<button class="delete-button" v-if="isMe" :title="$t('delete')" @click="del">
 | 
			
		||||
				<img src="/assets/remove.png" alt="Delete"/>
 | 
			
		||||
			</button>
 | 
			
		||||
			<div class="content" v-if="!message.isDeleted">
 | 
			
		||||
				<mfm class="text" v-if="message.text" ref="text" :text="message.text" :i="$store.state.i"/>
 | 
			
		||||
				<Mfm class="text" v-if="message.text" ref="text" :text="message.text" :i="$store.state.i"/>
 | 
			
		||||
				<div class="file" v-if="message.file">
 | 
			
		||||
					<a :href="message.file.url" rel="noopener" target="_blank" :title="message.file.name">
 | 
			
		||||
						<img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"/>
 | 
			
		||||
@@ -20,7 +20,7 @@
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div></div>
 | 
			
		||||
		<mk-url-preview v-for="url in urls" :url="url" :key="url" style="margin: 8px 0;"/>
 | 
			
		||||
		<MkUrlPreview v-for="url in urls" :url="url" :key="url" style="margin: 8px 0;"/>
 | 
			
		||||
		<footer>
 | 
			
		||||
			<template v-if="isGroup">
 | 
			
		||||
				<span class="read" v-if="message.reads.length > 0">{{ $t('messageRead') }} {{ message.reads.length }}</span>
 | 
			
		||||
@@ -28,20 +28,21 @@
 | 
			
		||||
			<template v-else>
 | 
			
		||||
				<span class="read" v-if="isMe && message.isRead">{{ $t('messageRead') }}</span>
 | 
			
		||||
			</template>
 | 
			
		||||
			<mk-time :time="message.createdAt"/>
 | 
			
		||||
			<template v-if="message.is_edited"><fa icon="pencil-alt"/></template>
 | 
			
		||||
			<MkTime :time="message.createdAt"/>
 | 
			
		||||
			<template v-if="message.is_edited"><Fa icon="pencil-alt"/></template>
 | 
			
		||||
		</footer>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { parse } from '../../../mfm/parse';
 | 
			
		||||
import { unique } from '../../../prelude/array';
 | 
			
		||||
import MkUrlPreview from '../../components/url-preview.vue';
 | 
			
		||||
import MkUrlPreview from '@/components/url-preview.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkUrlPreview
 | 
			
		||||
	},
 | 
			
		||||
@@ -70,7 +71,7 @@ export default Vue.extend({
 | 
			
		||||
	},
 | 
			
		||||
	methods: {
 | 
			
		||||
		del() {
 | 
			
		||||
			this.$root.api('messaging/messages/delete', {
 | 
			
		||||
			os.api('messaging/messages/delete', {
 | 
			
		||||
				messageId: this.message.id
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
@@ -240,7 +241,7 @@ export default Vue.extend({
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&:not([data-is-me]) {
 | 
			
		||||
	&:not(.isMe) {
 | 
			
		||||
		padding-left: var(--margin);
 | 
			
		||||
 | 
			
		||||
		> .content {
 | 
			
		||||
@@ -251,11 +252,11 @@ export default Vue.extend({
 | 
			
		||||
				$color: var(--messageBg);
 | 
			
		||||
				background: $color;
 | 
			
		||||
 | 
			
		||||
				&[data-no-text] {
 | 
			
		||||
				&.noText {
 | 
			
		||||
					background: transparent;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				&:not([data-no-text]):before {
 | 
			
		||||
				&:not(.noText):before {
 | 
			
		||||
					left: -14px;
 | 
			
		||||
					border-top: solid 8px transparent;
 | 
			
		||||
					border-right: solid 8px $color;
 | 
			
		||||
@@ -276,7 +277,7 @@ export default Vue.extend({
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&[data-is-me] {
 | 
			
		||||
	&.isMe {
 | 
			
		||||
		flex-direction: row-reverse;
 | 
			
		||||
		padding-right: var(--margin);
 | 
			
		||||
 | 
			
		||||
@@ -289,11 +290,11 @@ export default Vue.extend({
 | 
			
		||||
				background: $me-balloon-color;
 | 
			
		||||
				text-align: left;
 | 
			
		||||
 | 
			
		||||
				&[data-no-text] {
 | 
			
		||||
				&.noText {
 | 
			
		||||
					background: transparent;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				&:not([data-no-text]):before {
 | 
			
		||||
				&:not(.noText):before {
 | 
			
		||||
					right: -14px;
 | 
			
		||||
					left: auto;
 | 
			
		||||
					border-top: solid 8px transparent;
 | 
			
		||||
@@ -309,7 +310,7 @@ export default Vue.extend({
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					> .text {
 | 
			
		||||
						&, ::v-deep * {
 | 
			
		||||
						&, ::v-deep(*) {
 | 
			
		||||
							color: #fff !important;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
@@ -325,11 +326,5 @@ export default Vue.extend({
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&[data-is-deleted] {
 | 
			
		||||
		> .balloon {
 | 
			
		||||
			opacity: 0.5;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,57 +1,85 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="mk-messaging-room"
 | 
			
		||||
<div class="_section"
 | 
			
		||||
	@dragover.prevent.stop="onDragover"
 | 
			
		||||
	@drop.prevent.stop="onDrop"
 | 
			
		||||
>
 | 
			
		||||
	<template v-if="!fetching && user">
 | 
			
		||||
		<portal to="title"><mk-user-name :user="user" :nowrap="false" class="name"/></portal>
 | 
			
		||||
		<portal to="avatar"><mk-avatar class="avatar" :user="user" :disable-preview="true"/></portal>
 | 
			
		||||
	</template>
 | 
			
		||||
	<template v-if="!fetching && group">
 | 
			
		||||
		<portal to="icon"><fa :icon="faUsers"/></portal>
 | 
			
		||||
		<portal to="title">{{ group.name }}</portal>
 | 
			
		||||
	</template>
 | 
			
		||||
 | 
			
		||||
	<div class="body">
 | 
			
		||||
		<mk-loading v-if="fetching"/>
 | 
			
		||||
		<p class="empty" v-if="!fetching && messages.length == 0"><fa :icon="faInfoCircle"/>{{ $t('noMessagesYet') }}</p>
 | 
			
		||||
		<p class="no-history" v-if="!fetching && messages.length > 0 && !existMoreMessages"><fa :icon="faFlag"/>{{ $t('noMoreHistory') }}</p>
 | 
			
		||||
		<button class="more _button" ref="loadMore" :class="{ fetching: fetchingMoreMessages }" v-show="existMoreMessages" @click="fetchMoreMessages" :disabled="fetchingMoreMessages">
 | 
			
		||||
			<template v-if="fetchingMoreMessages"><fa icon="spinner" pulse fixed-width/></template>{{ fetchingMoreMessages ? $t('loading') : $t('loadMore') }}
 | 
			
		||||
		</button>
 | 
			
		||||
		<x-list class="messages" :items="messages" v-slot="{ item: message }" direction="up" reversed>
 | 
			
		||||
			<x-message :message="message" :is-group="group != null" :key="message.id"/>
 | 
			
		||||
		</x-list>
 | 
			
		||||
	<div class="_content mk-messaging-room">
 | 
			
		||||
		<div class="body">
 | 
			
		||||
			<MkLoading v-if="fetching"/>
 | 
			
		||||
			<p class="empty" v-if="!fetching && messages.length == 0"><Fa :icon="faInfoCircle"/>{{ $t('noMessagesYet') }}</p>
 | 
			
		||||
			<p class="no-history" v-if="!fetching && messages.length > 0 && !existMoreMessages"><Fa :icon="faFlag"/>{{ $t('noMoreHistory') }}</p>
 | 
			
		||||
			<button class="more _button" ref="loadMore" :class="{ fetching: fetchingMoreMessages }" v-show="existMoreMessages" @click="fetchMoreMessages" :disabled="fetchingMoreMessages">
 | 
			
		||||
				<template v-if="fetchingMoreMessages"><Fa icon="spinner" pulse fixed-width/></template>{{ fetchingMoreMessages ? $t('loading') : $t('loadMore') }}
 | 
			
		||||
			</button>
 | 
			
		||||
			<XList class="messages" :items="messages" v-slot="{ item: message }" direction="up" reversed>
 | 
			
		||||
				<XMessage :message="message" :is-group="group != null" :key="message.id"/>
 | 
			
		||||
			</XList>
 | 
			
		||||
		</div>
 | 
			
		||||
		<footer>
 | 
			
		||||
			<transition name="fade">
 | 
			
		||||
				<div class="new-message" v-show="showIndicator">
 | 
			
		||||
					<button class="_buttonPrimary" @click="onIndicatorClick"><i><Fa :icon="faArrowCircleDown"/></i>{{ $t('newMessageExists') }}</button>
 | 
			
		||||
				</div>
 | 
			
		||||
			</transition>
 | 
			
		||||
			<XForm v-if="!fetching" :user="user" :group="group" ref="form"/>
 | 
			
		||||
		</footer>
 | 
			
		||||
	</div>
 | 
			
		||||
	<footer>
 | 
			
		||||
		<transition name="fade">
 | 
			
		||||
			<div class="new-message" v-show="showIndicator">
 | 
			
		||||
				<button class="_buttonPrimary" @click="onIndicatorClick"><i><fa :icon="faArrowCircleDown"/></i>{{ $t('newMessageExists') }}</button>
 | 
			
		||||
			</div>
 | 
			
		||||
		</transition>
 | 
			
		||||
		<x-form v-if="!fetching" :user="user" :group="group" ref="form"/>
 | 
			
		||||
	</footer>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { faArrowCircleDown, faFlag, faUsers, faInfoCircle } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import XList from '../../components/date-separated-list.vue';
 | 
			
		||||
import { computed, defineComponent } from 'vue';
 | 
			
		||||
import { faArrowCircleDown, faFlag, faUsers, faInfoCircle, faEllipsisH, faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faWindowMaximize } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import XList from '@/components/date-separated-list.vue';
 | 
			
		||||
import XMessage from './messaging-room.message.vue';
 | 
			
		||||
import XForm from './messaging-room.form.vue';
 | 
			
		||||
import parseAcct from '../../../misc/acct/parse';
 | 
			
		||||
import { isBottom, onScrollBottom } from '../../scripts/scroll';
 | 
			
		||||
import { isBottom, onScrollBottom, scroll } from '@/scripts/scroll';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { popout } from '@/scripts/popout';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
const Component = defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XMessage,
 | 
			
		||||
		XForm,
 | 
			
		||||
		XList,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	inject: ['inWindow'],
 | 
			
		||||
 | 
			
		||||
	props: {
 | 
			
		||||
		userAcct: {
 | 
			
		||||
			type: String,
 | 
			
		||||
			required: false,
 | 
			
		||||
		},
 | 
			
		||||
		groupId: {
 | 
			
		||||
			type: String,
 | 
			
		||||
			required: false,
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: computed(() => !this.fetching ? this.user ? {
 | 
			
		||||
				header: [{
 | 
			
		||||
					userName: this.user,
 | 
			
		||||
					avatar: this.user,
 | 
			
		||||
				}],
 | 
			
		||||
				action: {
 | 
			
		||||
					icon: faEllipsisH,
 | 
			
		||||
					handler: this.menu,
 | 
			
		||||
				},
 | 
			
		||||
			} : {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.group.name,
 | 
			
		||||
					icon: faUsers
 | 
			
		||||
				}],
 | 
			
		||||
				action: {
 | 
			
		||||
					icon: faEllipsisH,
 | 
			
		||||
					handler: this.menu,
 | 
			
		||||
				},
 | 
			
		||||
			} : null),
 | 
			
		||||
			fetching: true,
 | 
			
		||||
			user: null,
 | 
			
		||||
			group: null,
 | 
			
		||||
@@ -68,7 +96,7 @@ export default Vue.extend({
 | 
			
		||||
					&& this.existMoreMessages
 | 
			
		||||
					&& this.fetchMoreMessages()
 | 
			
		||||
			),
 | 
			
		||||
			faArrowCircleDown, faFlag, faUsers, faInfoCircle
 | 
			
		||||
			faArrowCircleDown, faFlag, faInfoCircle
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
@@ -79,7 +107,8 @@ export default Vue.extend({
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	watch: {
 | 
			
		||||
		$route: 'fetch'
 | 
			
		||||
		userAcct: 'fetch',
 | 
			
		||||
		groupId: 'fetch',
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	mounted() {
 | 
			
		||||
@@ -89,7 +118,7 @@ export default Vue.extend({
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	beforeDestroy() {
 | 
			
		||||
	beforeUnmount() {
 | 
			
		||||
		this.connection.dispose();
 | 
			
		||||
 | 
			
		||||
		document.removeEventListener('visibilitychange', this.onVisibilitychange);
 | 
			
		||||
@@ -100,15 +129,15 @@ export default Vue.extend({
 | 
			
		||||
	methods: {
 | 
			
		||||
		async fetch() {
 | 
			
		||||
			this.fetching = true;
 | 
			
		||||
			if (this.$route.params.user) {
 | 
			
		||||
				const user = await this.$root.api('users/show', parseAcct(this.$route.params.user));
 | 
			
		||||
			if (this.userAcct) {
 | 
			
		||||
				const user = await os.api('users/show', parseAcct(this.userAcct));
 | 
			
		||||
				this.user = user;
 | 
			
		||||
			} else {
 | 
			
		||||
				const group = await this.$root.api('users/groups/show', { groupId: this.$route.params.group });
 | 
			
		||||
				const group = await os.api('users/groups/show', { groupId: this.groupId });
 | 
			
		||||
				this.group = group;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			this.connection = this.$root.stream.connectToChannel('messaging', {
 | 
			
		||||
			this.connection = os.stream.connectToChannel('messaging', {
 | 
			
		||||
				otherparty: this.user ? this.user.id : undefined,
 | 
			
		||||
				group: this.group ? this.group.id : undefined,
 | 
			
		||||
			});
 | 
			
		||||
@@ -131,7 +160,7 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
		onDragover(e) {
 | 
			
		||||
			const isFile = e.dataTransfer.items[0].kind == 'file';
 | 
			
		||||
			const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file';
 | 
			
		||||
			const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
 | 
			
		||||
 | 
			
		||||
			if (isFile || isDriveFile) {
 | 
			
		||||
				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
 | 
			
		||||
@@ -146,7 +175,7 @@ export default Vue.extend({
 | 
			
		||||
				this.form.upload(e.dataTransfer.files[0]);
 | 
			
		||||
				return;
 | 
			
		||||
			} else if (e.dataTransfer.files.length > 1) {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
				os.dialog({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: this.$t('onlyOneFileCanBeAttached')
 | 
			
		||||
				});
 | 
			
		||||
@@ -154,7 +183,7 @@ export default Vue.extend({
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			//#region ドライブのファイル
 | 
			
		||||
			const driveFile = e.dataTransfer.getData('mk_drive_file');
 | 
			
		||||
			const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
 | 
			
		||||
			if (driveFile != null && driveFile != '') {
 | 
			
		||||
				const file = JSON.parse(driveFile);
 | 
			
		||||
				this.form.file = file;
 | 
			
		||||
@@ -166,7 +195,7 @@ export default Vue.extend({
 | 
			
		||||
			return new Promise((resolve, reject) => {
 | 
			
		||||
				const max = this.existMoreMessages ? 20 : 10;
 | 
			
		||||
 | 
			
		||||
				this.$root.api('messaging/messages', {
 | 
			
		||||
				os.api('messaging/messages', {
 | 
			
		||||
					userId: this.user ? this.user.id : undefined,
 | 
			
		||||
					groupId: this.group ? this.group.id : undefined,
 | 
			
		||||
					limit: max + 1,
 | 
			
		||||
@@ -193,7 +222,7 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onMessage(message) {
 | 
			
		||||
			this.$root.sound('chat');
 | 
			
		||||
			os.sound('chat');
 | 
			
		||||
 | 
			
		||||
			const _isBottom = isBottom(this.$el, 64);
 | 
			
		||||
 | 
			
		||||
@@ -248,7 +277,7 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		scrollToBottom() {
 | 
			
		||||
			window.scroll(0, document.body.offsetHeight);
 | 
			
		||||
			scroll(this.$el, this.$el.offsetHeight);
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onIndicatorClick() {
 | 
			
		||||
@@ -279,17 +308,36 @@ export default Vue.extend({
 | 
			
		||||
					});
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		menu(ev) {
 | 
			
		||||
			const url = this.groupId ? `/my/messaging/group/${this.groupId}` : `/my/messaging/${this.userAcct}`;
 | 
			
		||||
 | 
			
		||||
			os.modalMenu([this.inWindow ? undefined : {
 | 
			
		||||
				text: this.$t('openInWindow'),
 | 
			
		||||
				icon: faWindowMaximize,
 | 
			
		||||
				action: () => {
 | 
			
		||||
					os.pageWindow(url, Component, this.$props);
 | 
			
		||||
					this.$router.back();
 | 
			
		||||
				},
 | 
			
		||||
			}, this.inWindow ? undefined : {
 | 
			
		||||
				text: this.$t('popout'),
 | 
			
		||||
				icon: faExternalLinkAlt,
 | 
			
		||||
				action: () => {
 | 
			
		||||
					popout(url);
 | 
			
		||||
					this.$router.back();
 | 
			
		||||
				},
 | 
			
		||||
			}], ev.currentTarget || ev.target);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default Component;
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.mk-messaging-room {
 | 
			
		||||
 | 
			
		||||
	> .body {
 | 
			
		||||
		width: 100%;
 | 
			
		||||
 | 
			
		||||
		> .empty {
 | 
			
		||||
			width: 100%;
 | 
			
		||||
			margin: 0;
 | 
			
		||||
@@ -344,7 +392,7 @@ export default Vue.extend({
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> .messages {
 | 
			
		||||
			> ::v-deep * {
 | 
			
		||||
			> ::v-deep(*) {
 | 
			
		||||
				margin-bottom: 16px;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
@@ -384,7 +432,7 @@ export default Vue.extend({
 | 
			
		||||
	transition: opacity 0.1s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fade-enter, .fade-leave-to {
 | 
			
		||||
.fade-enter-from, .fade-leave-to {
 | 
			
		||||
	transition: opacity 0.5s;
 | 
			
		||||
	opacity: 0;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,49 +1,48 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div v-if="$store.getters.isSignedIn">
 | 
			
		||||
	<div class="waiting _card _vMargin" v-if="state == 'waiting'">
 | 
			
		||||
	<div class="waiting _section" v-if="state == 'waiting'">
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-loading/>
 | 
			
		||||
			<MkLoading/>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="denied _card _vMargin" v-if="state == 'denied'">
 | 
			
		||||
	<div class="denied _section" v-if="state == 'denied'">
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<p>{{ $t('_auth.denied') }}</p>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="accepted _card _vMargin" v-else-if="state == 'accepted'">
 | 
			
		||||
	<div class="accepted _section" v-else-if="state == 'accepted'">
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<p v-if="callback">{{ $t('_auth.callback') }}<mk-ellipsis/></p>
 | 
			
		||||
			<p v-if="callback">{{ $t('_auth.callback') }}<MkEllipsis/></p>
 | 
			
		||||
			<p v-else>{{ $t('_auth.pleaseGoBack') }}</p>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="_card _vMargin" v-else>
 | 
			
		||||
	<div class="_section" v-else>
 | 
			
		||||
		<div class="_title" v-if="name">{{ $t('_auth.shareAccess', { name: name }) }}</div>
 | 
			
		||||
		<div class="_title" v-else>{{ $t('_auth.shareAccessAsk') }}</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<p>{{ $t('_auth.permissionAsk') }}</p>
 | 
			
		||||
			<ul>
 | 
			
		||||
				<template v-for="p in permission">
 | 
			
		||||
					<li :key="p">{{ $t(`_permissions.${p}`) }}</li>
 | 
			
		||||
				</template>
 | 
			
		||||
				<li v-for="p in permission" :key="p">{{ $t(`_permissions.${p}`) }}</li>
 | 
			
		||||
			</ul>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_footer">
 | 
			
		||||
			<mk-button @click="deny" inline>{{ $t('cancel') }}</mk-button>
 | 
			
		||||
			<mk-button @click="accept" inline primary>{{ $t('accept') }}</mk-button>
 | 
			
		||||
			<MkButton @click="deny" inline>{{ $t('cancel') }}</MkButton>
 | 
			
		||||
			<MkButton @click="accept" inline primary>{{ $t('accept') }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
<div class="signin" v-else>
 | 
			
		||||
	<mk-signin @login="onLogin"/>
 | 
			
		||||
	<MkSignin @login="onLogin"/>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import MkSignin from '../components/signin.vue';
 | 
			
		||||
import MkButton from '../components/ui/button.vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import MkSignin from '@/components/signin.vue';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkSignin,
 | 
			
		||||
		MkButton,
 | 
			
		||||
@@ -73,7 +72,7 @@ export default Vue.extend({
 | 
			
		||||
	methods: {
 | 
			
		||||
		async accept() {
 | 
			
		||||
			this.state = 'waiting';
 | 
			
		||||
			await this.$root.api('miauth/gen-token', {
 | 
			
		||||
			await os.api('miauth/gen-token', {
 | 
			
		||||
				session: this.session,
 | 
			
		||||
				name: this.name,
 | 
			
		||||
				iconUrl: this.icon,
 | 
			
		||||
 
 | 
			
		||||
@@ -2,61 +2,61 @@
 | 
			
		||||
<div class="shaynizk _card">
 | 
			
		||||
	<div class="_title" v-if="antenna.name">{{ antenna.name }}</div>
 | 
			
		||||
	<div class="_content body">
 | 
			
		||||
		<mk-input v-model="name">
 | 
			
		||||
		<MkInput v-model:value="name">
 | 
			
		||||
			<span>{{ $t('name') }}</span>
 | 
			
		||||
		</mk-input>
 | 
			
		||||
		<mk-select v-model="src">
 | 
			
		||||
		</MkInput>
 | 
			
		||||
		<MkSelect v-model:value="src">
 | 
			
		||||
			<template #label>{{ $t('antennaSource') }}</template>
 | 
			
		||||
			<option value="all">{{ $t('_antennaSources.all') }}</option>
 | 
			
		||||
			<option value="home">{{ $t('_antennaSources.homeTimeline') }}</option>
 | 
			
		||||
			<option value="users">{{ $t('_antennaSources.users') }}</option>
 | 
			
		||||
			<option value="list">{{ $t('_antennaSources.userList') }}</option>
 | 
			
		||||
			<option value="group">{{ $t('_antennaSources.userGroup') }}</option>
 | 
			
		||||
		</mk-select>
 | 
			
		||||
		<mk-select v-model="userListId" v-if="src === 'list'">
 | 
			
		||||
		</MkSelect>
 | 
			
		||||
		<MkSelect v-model:value="userListId" v-if="src === 'list'">
 | 
			
		||||
			<template #label>{{ $t('userList') }}</template>
 | 
			
		||||
			<option v-for="list in userLists" :value="list.id" :key="list.id">{{ list.name }}</option>
 | 
			
		||||
		</mk-select>
 | 
			
		||||
		<mk-select v-model="userGroupId" v-else-if="src === 'group'">
 | 
			
		||||
		</MkSelect>
 | 
			
		||||
		<MkSelect v-model:value="userGroupId" v-else-if="src === 'group'">
 | 
			
		||||
			<template #label>{{ $t('userGroup') }}</template>
 | 
			
		||||
			<option v-for="group in userGroups" :value="group.id" :key="group.id">{{ group.name }}</option>
 | 
			
		||||
		</mk-select>
 | 
			
		||||
		<mk-textarea v-model="users" v-else-if="src === 'users'">
 | 
			
		||||
		</MkSelect>
 | 
			
		||||
		<MkTextarea v-model:value="users" v-else-if="src === 'users'">
 | 
			
		||||
			<span>{{ $t('users') }}</span>
 | 
			
		||||
			<template #desc>{{ $t('antennaUsersDescription') }} <button class="_textButton" @click="addUser">{{ $t('addUser') }}</button></template>
 | 
			
		||||
		</mk-textarea>
 | 
			
		||||
		<mk-switch v-model="withReplies">{{ $t('withReplies') }}</mk-switch>
 | 
			
		||||
		<mk-textarea v-model="keywords">
 | 
			
		||||
		</MkTextarea>
 | 
			
		||||
		<MkSwitch v-model:value="withReplies">{{ $t('withReplies') }}</MkSwitch>
 | 
			
		||||
		<MkTextarea v-model:value="keywords">
 | 
			
		||||
			<span>{{ $t('antennaKeywords') }}</span>
 | 
			
		||||
			<template #desc>{{ $t('antennaKeywordsDescription') }}</template>
 | 
			
		||||
		</mk-textarea>
 | 
			
		||||
		<mk-textarea v-model="excludeKeywords">
 | 
			
		||||
		</MkTextarea>
 | 
			
		||||
		<MkTextarea v-model:value="excludeKeywords">
 | 
			
		||||
			<span>{{ $t('antennaExcludeKeywords') }}</span>
 | 
			
		||||
			<template #desc>{{ $t('antennaKeywordsDescription') }}</template>
 | 
			
		||||
		</mk-textarea>
 | 
			
		||||
		<mk-switch v-model="caseSensitive">{{ $t('caseSensitive') }}</mk-switch>
 | 
			
		||||
		<mk-switch v-model="withFile">{{ $t('withFileAntenna') }}</mk-switch>
 | 
			
		||||
		<mk-switch v-model="notify">{{ $t('notifyAntenna') }}</mk-switch>
 | 
			
		||||
		</MkTextarea>
 | 
			
		||||
		<MkSwitch v-model:value="caseSensitive">{{ $t('caseSensitive') }}</MkSwitch>
 | 
			
		||||
		<MkSwitch v-model:value="withFile">{{ $t('withFileAntenna') }}</MkSwitch>
 | 
			
		||||
		<MkSwitch v-model:value="notify">{{ $t('notifyAntenna') }}</MkSwitch>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="_footer">
 | 
			
		||||
		<mk-button inline @click="saveAntenna()" primary><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
 | 
			
		||||
		<mk-button inline @click="deleteAntenna()" v-if="antenna.id != null"><fa :icon="faTrash"/> {{ $t('delete') }}</mk-button>
 | 
			
		||||
		<MkButton inline @click="saveAntenna()" primary><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
 | 
			
		||||
		<MkButton inline @click="deleteAntenna()" v-if="antenna.id != null"><Fa :icon="faTrash"/> {{ $t('delete') }}</MkButton>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faSave, faTrash } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import MkButton from '../../components/ui/button.vue';
 | 
			
		||||
import MkInput from '../../components/ui/input.vue';
 | 
			
		||||
import MkTextarea from '../../components/ui/textarea.vue';
 | 
			
		||||
import MkSelect from '../../components/ui/select.vue';
 | 
			
		||||
import MkSwitch from '../../components/ui/switch.vue';
 | 
			
		||||
import MkUserSelect from '../../components/user-select.vue';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import MkInput from '@/components/ui/input.vue';
 | 
			
		||||
import MkTextarea from '@/components/ui/textarea.vue';
 | 
			
		||||
import MkSelect from '@/components/ui/select.vue';
 | 
			
		||||
import MkSwitch from '@/components/ui/switch.vue';
 | 
			
		||||
import getAcct from '../../../misc/acct/render';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkButton, MkInput, MkTextarea, MkSelect, MkSwitch
 | 
			
		||||
	},
 | 
			
		||||
@@ -90,12 +90,12 @@ export default Vue.extend({
 | 
			
		||||
	watch: {
 | 
			
		||||
		async src() {
 | 
			
		||||
			if (this.src === 'list' && this.userLists === null) {
 | 
			
		||||
				this.userLists = await this.$root.api('users/lists/list');
 | 
			
		||||
				this.userLists = await os.api('users/lists/list');
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (this.src === 'group' && this.userGroups === null) {
 | 
			
		||||
				const groups1 = await this.$root.api('users/groups/owned');
 | 
			
		||||
				const groups2 = await this.$root.api('users/groups/joined');
 | 
			
		||||
				const groups1 = await os.api('users/groups/owned');
 | 
			
		||||
				const groups2 = await os.api('users/groups/joined');
 | 
			
		||||
 | 
			
		||||
				this.userGroups = [...groups1, ...groups2];
 | 
			
		||||
			}
 | 
			
		||||
@@ -119,7 +119,7 @@ export default Vue.extend({
 | 
			
		||||
	methods: {
 | 
			
		||||
		async saveAntenna() {
 | 
			
		||||
			if (this.antenna.id == null) {
 | 
			
		||||
				await this.$root.api('antennas/create', {
 | 
			
		||||
				await os.api('antennas/create', {
 | 
			
		||||
					name: this.name,
 | 
			
		||||
					src: this.src,
 | 
			
		||||
					userListId: this.userListId,
 | 
			
		||||
@@ -134,7 +134,7 @@ export default Vue.extend({
 | 
			
		||||
				});
 | 
			
		||||
				this.$emit('created');
 | 
			
		||||
			} else {
 | 
			
		||||
				await this.$root.api('antennas/update', {
 | 
			
		||||
				await os.api('antennas/update', {
 | 
			
		||||
					antennaId: this.antenna.id,
 | 
			
		||||
					name: this.name,
 | 
			
		||||
					src: this.src,
 | 
			
		||||
@@ -150,33 +150,27 @@ export default Vue.extend({
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			this.$root.dialog({
 | 
			
		||||
				type: 'success',
 | 
			
		||||
				iconOnly: true, autoClose: true
 | 
			
		||||
			});
 | 
			
		||||
			os.success();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async deleteAntenna() {
 | 
			
		||||
			const { canceled } = await this.$root.dialog({
 | 
			
		||||
			const { canceled } = await os.dialog({
 | 
			
		||||
				type: 'warning',
 | 
			
		||||
				text: this.$t('removeAreYouSure', { x: this.antenna.name }),
 | 
			
		||||
				showCancelButton: true
 | 
			
		||||
			});
 | 
			
		||||
			if (canceled) return;
 | 
			
		||||
 | 
			
		||||
			await this.$root.api('antennas/delete', {
 | 
			
		||||
			await os.api('antennas/delete', {
 | 
			
		||||
				antennaId: this.antenna.id,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			this.$root.dialog({
 | 
			
		||||
				type: 'success',
 | 
			
		||||
				iconOnly: true, autoClose: true
 | 
			
		||||
			});
 | 
			
		||||
			os.success();
 | 
			
		||||
			this.$emit('deleted');
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		addUser() {
 | 
			
		||||
			this.$root.new(MkUserSelect, {}).$once('selected', user => {
 | 
			
		||||
			os.selectUser().then(user => {
 | 
			
		||||
				this.users = this.users.trim();
 | 
			
		||||
				this.users += '\n@' + getAcct(user);
 | 
			
		||||
				this.users = this.users.trim();
 | 
			
		||||
 
 | 
			
		||||
@@ -1,32 +1,25 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="ieepwinx">
 | 
			
		||||
	<portal to="icon"><fa :icon="faSatellite"/></portal>
 | 
			
		||||
	<portal to="title">{{ $t('manageAntennas') }}</portal>
 | 
			
		||||
<div class="ieepwinx _section">
 | 
			
		||||
	<MkButton @click="create" primary class="add"><Fa :icon="faPlus"/> {{ $t('add') }}</MkButton>
 | 
			
		||||
 | 
			
		||||
	<mk-button @click="create" primary class="add"><fa :icon="faPlus"/> {{ $t('add') }}</mk-button>
 | 
			
		||||
	<div class="_content">
 | 
			
		||||
		<XAntenna v-if="draft" :antenna="draft" @created="onAntennaCreated" style="margin-bottom: var(--margin);"/>
 | 
			
		||||
 | 
			
		||||
	<x-antenna v-if="draft" :antenna="draft" @created="onAntennaCreated" style="margin-bottom: var(--margin);"/>
 | 
			
		||||
 | 
			
		||||
	<mk-pagination :pagination="pagination" #default="{items}" class="antennas" ref="list">
 | 
			
		||||
		<x-antenna v-for="(antenna, i) in items" :key="antenna.id" :antenna="antenna" @created="onAntennaDeleted"/>
 | 
			
		||||
	</mk-pagination>
 | 
			
		||||
		<MkPagination :pagination="pagination" #default="{items}" class="antennas" ref="list">
 | 
			
		||||
			<XAntenna v-for="(antenna, i) in items" :key="antenna.id" :antenna="antenna" @created="onAntennaDeleted"/>
 | 
			
		||||
		</MkPagination>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faSatellite, faPlus } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import MkPagination from '../../components/ui/pagination.vue';
 | 
			
		||||
import MkButton from '../../components/ui/button.vue';
 | 
			
		||||
import MkPagination from '@/components/ui/pagination.vue';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import XAntenna from './index.antenna.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: this.$t('manageAntennas') as string,
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkPagination,
 | 
			
		||||
		MkButton,
 | 
			
		||||
@@ -35,6 +28,16 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('manageAntennas'),
 | 
			
		||||
					icon: faSatellite
 | 
			
		||||
				}],
 | 
			
		||||
				action: {
 | 
			
		||||
					icon: faPlus,
 | 
			
		||||
					handler: this.create
 | 
			
		||||
				}
 | 
			
		||||
			},
 | 
			
		||||
			pagination: {
 | 
			
		||||
				endpoint: 'antennas/list',
 | 
			
		||||
				limit: 10,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,63 +1,58 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="mk-group-page">
 | 
			
		||||
	<portal to="icon"><fa :icon="faUsers"/></portal>
 | 
			
		||||
	<portal to="title">{{ group.name }}</portal>
 | 
			
		||||
 | 
			
		||||
	<transition name="zoom" mode="out-in">
 | 
			
		||||
		<div v-if="group" class="_card _vMargin">
 | 
			
		||||
		<div v-if="group" class="_section">
 | 
			
		||||
			<div class="_content">
 | 
			
		||||
				<mk-button inline @click="renameGroup()">{{ $t('rename') }}</mk-button>
 | 
			
		||||
				<mk-button inline @click="transfer()">{{ $t('transfer') }}</mk-button>
 | 
			
		||||
				<mk-button inline @click="deleteGroup()">{{ $t('delete') }}</mk-button>
 | 
			
		||||
				<MkButton inline @click="invite()">{{ $t('invite') }}</MkButton>
 | 
			
		||||
				<MkButton inline @click="renameGroup()">{{ $t('rename') }}</MkButton>
 | 
			
		||||
				<MkButton inline @click="transfer()">{{ $t('transfer') }}</MkButton>
 | 
			
		||||
				<MkButton inline @click="deleteGroup()">{{ $t('delete') }}</MkButton>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</transition>
 | 
			
		||||
 | 
			
		||||
	<transition name="zoom" mode="out-in">
 | 
			
		||||
		<div v-if="group" class="_card members _vMargin">
 | 
			
		||||
		<div v-if="group" class="_section members _vMargin">
 | 
			
		||||
			<div class="_title">{{ $t('members') }}</div>
 | 
			
		||||
			<div class="_content">
 | 
			
		||||
				<div class="users">
 | 
			
		||||
					<div class="user" v-for="user in users" :key="user.id">
 | 
			
		||||
						<mk-avatar :user="user" class="avatar"/>
 | 
			
		||||
					<div class="user _panel" v-for="user in users" :key="user.id">
 | 
			
		||||
						<MkAvatar :user="user" class="avatar"/>
 | 
			
		||||
						<div class="body">
 | 
			
		||||
							<mk-user-name :user="user" class="name"/>
 | 
			
		||||
							<mk-acct :user="user" class="acct"/>
 | 
			
		||||
							<MkUserName :user="user" class="name"/>
 | 
			
		||||
							<MkAcct :user="user" class="acct"/>
 | 
			
		||||
						</div>
 | 
			
		||||
						<div class="action">
 | 
			
		||||
							<button class="_button" @click="removeUser(user)"><fa :icon="faTimes"/></button>
 | 
			
		||||
							<button class="_button" @click="removeUser(user)"><Fa :icon="faTimes"/></button>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="_footer">
 | 
			
		||||
				<mk-button inline @click="invite()">{{ $t('invite') }}</mk-button>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</transition>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { computed, defineComponent } from 'vue';
 | 
			
		||||
import { faTimes, faUsers } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import Progress from '../../scripts/loading';
 | 
			
		||||
import MkButton from '../../components/ui/button.vue';
 | 
			
		||||
import MkUserSelect from '../../components/user-select.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: this.group ? `${this.group.name} | ${this.$t('manageGroups')}` : this.$t('manageGroups')
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
import Progress from '@/scripts/loading';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkButton
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: computed(() => this.group ? {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.group.name,
 | 
			
		||||
					icon: faUsers,
 | 
			
		||||
				}],
 | 
			
		||||
			} : null),
 | 
			
		||||
			group: null,
 | 
			
		||||
			users: [],
 | 
			
		||||
			faTimes, faUsers
 | 
			
		||||
@@ -75,11 +70,11 @@ export default Vue.extend({
 | 
			
		||||
	methods: {
 | 
			
		||||
		fetch() {
 | 
			
		||||
			Progress.start();
 | 
			
		||||
			this.$root.api('users/groups/show', {
 | 
			
		||||
			os.api('users/groups/show', {
 | 
			
		||||
				groupId: this.$route.params.group
 | 
			
		||||
			}).then(group => {
 | 
			
		||||
				this.group = group;
 | 
			
		||||
				this.$root.api('users/show', {
 | 
			
		||||
				os.api('users/show', {
 | 
			
		||||
					userIds: this.group.userIds
 | 
			
		||||
				}).then(users => {
 | 
			
		||||
					this.users = users;
 | 
			
		||||
@@ -89,26 +84,16 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		invite() {
 | 
			
		||||
			this.$root.new(MkUserSelect, {}).$once('selected', user => {
 | 
			
		||||
				this.$root.api('users/groups/invite', {
 | 
			
		||||
			os.selectUser().then(user => {
 | 
			
		||||
				os.apiWithDialog('users/groups/invite', {
 | 
			
		||||
					groupId: this.group.id,
 | 
			
		||||
					userId: user.id
 | 
			
		||||
				}).then(() => {
 | 
			
		||||
					this.$root.dialog({
 | 
			
		||||
						type: 'success',
 | 
			
		||||
						iconOnly: true, autoClose: true
 | 
			
		||||
					});
 | 
			
		||||
				}).catch(e => {
 | 
			
		||||
					this.$root.dialog({
 | 
			
		||||
						type: 'error',
 | 
			
		||||
						text: e
 | 
			
		||||
					});
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		removeUser(user) {
 | 
			
		||||
			this.$root.api('users/groups/pull', {
 | 
			
		||||
			os.api('users/groups/pull', {
 | 
			
		||||
				groupId: this.group.id,
 | 
			
		||||
				userId: user.id
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
@@ -117,7 +102,7 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async renameGroup() {
 | 
			
		||||
			const { canceled, result: name } = await this.$root.dialog({
 | 
			
		||||
			const { canceled, result: name } = await os.dialog({
 | 
			
		||||
				title: this.$t('groupName'),
 | 
			
		||||
				input: {
 | 
			
		||||
					default: this.group.name
 | 
			
		||||
@@ -125,7 +110,7 @@ export default Vue.extend({
 | 
			
		||||
			});
 | 
			
		||||
			if (canceled) return;
 | 
			
		||||
 | 
			
		||||
			await this.$root.api('users/groups/update', {
 | 
			
		||||
			await os.api('users/groups/update', {
 | 
			
		||||
				groupId: this.group.id,
 | 
			
		||||
				name: name
 | 
			
		||||
			});
 | 
			
		||||
@@ -134,39 +119,25 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		transfer() {
 | 
			
		||||
			this.$root.new(MkUserSelect, {}).$once('selected', user => {
 | 
			
		||||
				this.$root.api('users/groups/transfer', {
 | 
			
		||||
			os.selectUser().then(user => {
 | 
			
		||||
				os.apiWithDialog('users/groups/transfer', {
 | 
			
		||||
					groupId: this.group.id,
 | 
			
		||||
					userId: user.id
 | 
			
		||||
				}).then(() => {
 | 
			
		||||
					this.$root.dialog({
 | 
			
		||||
						type: 'success',
 | 
			
		||||
						iconOnly: true, autoClose: true
 | 
			
		||||
					});
 | 
			
		||||
				}).catch(e => {
 | 
			
		||||
					this.$root.dialog({
 | 
			
		||||
						type: 'error',
 | 
			
		||||
						text: e
 | 
			
		||||
					});
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async deleteGroup() {
 | 
			
		||||
			const { canceled } = await this.$root.dialog({
 | 
			
		||||
			const { canceled } = await os.dialog({
 | 
			
		||||
				type: 'warning',
 | 
			
		||||
				text: this.$t('removeAreYouSure', { x: this.group.name }),
 | 
			
		||||
				showCancelButton: true
 | 
			
		||||
			});
 | 
			
		||||
			if (canceled) return;
 | 
			
		||||
 | 
			
		||||
			await this.$root.api('users/groups/delete', {
 | 
			
		||||
			await os.apiWithDialog('users/groups/delete', {
 | 
			
		||||
				groupId: this.group.id
 | 
			
		||||
			});
 | 
			
		||||
			this.$root.dialog({
 | 
			
		||||
				type: 'success',
 | 
			
		||||
				iconOnly: true, autoClose: true
 | 
			
		||||
			});
 | 
			
		||||
			this.$router.push('/my/groups');
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@@ -177,13 +148,11 @@ export default Vue.extend({
 | 
			
		||||
.mk-group-page {
 | 
			
		||||
	> .members {
 | 
			
		||||
		> ._content {
 | 
			
		||||
			max-height: 400px;
 | 
			
		||||
			overflow: auto;
 | 
			
		||||
 | 
			
		||||
			> .users {
 | 
			
		||||
				> .user {
 | 
			
		||||
					display: flex;
 | 
			
		||||
					align-items: center;
 | 
			
		||||
					padding: 16px;
 | 
			
		||||
 | 
			
		||||
					> .avatar {
 | 
			
		||||
						width: 50px;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,70 +1,74 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="">
 | 
			
		||||
	<portal to="icon"><fa :icon="faUsers"/></portal>
 | 
			
		||||
	<portal to="title">{{ $t('groups') }}</portal>
 | 
			
		||||
	<div class="_section" style="padding: 0;">
 | 
			
		||||
		<MkTab v-model:value="tab" :items="[{ label: $t('ownedGroups'), value: 'owned' }, { label: $t('joinedGroups'), value: 'joined' }, { label: $t('invites'), icon: faEnvelopeOpenText, value: 'invites' }]"/>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<mk-button @click="create" primary style="margin: 0 auto var(--margin) auto;"><fa :icon="faPlus"/> {{ $t('createGroup') }}</mk-button>
 | 
			
		||||
	<div class="_section">
 | 
			
		||||
		<div class="_content" v-if="tab === 'owned'">
 | 
			
		||||
			<MkButton @click="create" primary style="margin: 0 auto var(--margin) auto;"><Fa :icon="faPlus"/> {{ $t('createGroup') }}</MkButton>
 | 
			
		||||
 | 
			
		||||
	<mk-container :body-togglable="true">
 | 
			
		||||
		<template #header><fa :icon="faUsers"/> {{ $t('ownedGroups') }}</template>
 | 
			
		||||
		<mk-pagination :pagination="ownedPagination" #default="{items}" ref="owned">
 | 
			
		||||
			<div class="_card" v-for="group in items" :key="group.id">
 | 
			
		||||
				<div class="_title"><router-link :to="`/my/groups/${ group.id }`" class="_link">{{ group.name }}</router-link></div>
 | 
			
		||||
				<div class="_content"><mk-avatars :user-ids="group.userIds"/></div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</mk-pagination>
 | 
			
		||||
	</mk-container>
 | 
			
		||||
 | 
			
		||||
	<mk-container :body-togglable="true">
 | 
			
		||||
		<template #header><fa :icon="faEnvelopeOpenText"/> {{ $t('invites') }}</template>
 | 
			
		||||
		<mk-pagination :pagination="invitationPagination" #default="{items}" ref="invitations">
 | 
			
		||||
			<div class="_card" v-for="invitation in items" :key="invitation.id">
 | 
			
		||||
				<div class="_title">{{ invitation.group.name }}</div>
 | 
			
		||||
				<div class="_content"><mk-avatars :user-ids="invitation.group.userIds"/></div>
 | 
			
		||||
				<div class="_footer">
 | 
			
		||||
					<mk-button @click="acceptInvite(invitation)" primary inline><fa :icon="faCheck"/> {{ $t('accept') }}</mk-button>
 | 
			
		||||
					<mk-button @click="rejectInvite(invitation)" primary inline><fa :icon="faBan"/> {{ $t('reject') }}</mk-button>
 | 
			
		||||
			<MkPagination :pagination="ownedPagination" #default="{items}" ref="owned">
 | 
			
		||||
				<div class="_card" v-for="group in items" :key="group.id">
 | 
			
		||||
					<div class="_title"><router-link :to="`/my/groups/${ group.id }`" class="_link">{{ group.name }}</router-link></div>
 | 
			
		||||
					<div class="_content"><MkAvatars :user-ids="group.userIds"/></div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</mk-pagination>
 | 
			
		||||
	</mk-container>
 | 
			
		||||
			</MkPagination>
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
	<mk-container :body-togglable="true">
 | 
			
		||||
		<template #header><fa :icon="faUsers"/> {{ $t('joinedGroups') }}</template>
 | 
			
		||||
		<mk-pagination :pagination="joinedPagination" #default="{items}" ref="joined">
 | 
			
		||||
			<div class="_card" v-for="group in items" :key="group.id">
 | 
			
		||||
				<div class="_title">{{ group.name }}</div>
 | 
			
		||||
				<div class="_content"><mk-avatars :user-ids="group.userIds"/></div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</mk-pagination>
 | 
			
		||||
	</mk-container>
 | 
			
		||||
		<div class="_content" v-else-if="tab === 'joined'">
 | 
			
		||||
			<MkPagination :pagination="joinedPagination" #default="{items}" ref="joined">
 | 
			
		||||
				<div class="_card" v-for="group in items" :key="group.id">
 | 
			
		||||
					<div class="_title">{{ group.name }}</div>
 | 
			
		||||
					<div class="_content"><MkAvatars :user-ids="group.userIds"/></div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</MkPagination>
 | 
			
		||||
		</div>
 | 
			
		||||
	
 | 
			
		||||
		<div class="_content" v-else-if="tab === 'invites'">
 | 
			
		||||
			<MkPagination :pagination="invitationPagination" #default="{items}" ref="invitations">
 | 
			
		||||
				<div class="_card" v-for="invitation in items" :key="invitation.id">
 | 
			
		||||
					<div class="_title">{{ invitation.group.name }}</div>
 | 
			
		||||
					<div class="_content"><MkAvatars :user-ids="invitation.group.userIds"/></div>
 | 
			
		||||
					<div class="_footer">
 | 
			
		||||
						<MkButton @click="acceptInvite(invitation)" primary inline><Fa :icon="faCheck"/> {{ $t('accept') }}</MkButton>
 | 
			
		||||
						<MkButton @click="rejectInvite(invitation)" primary inline><Fa :icon="faBan"/> {{ $t('reject') }}</MkButton>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</MkPagination>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faUsers, faPlus, faEnvelopeOpenText } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import MkPagination from '../../components/ui/pagination.vue';
 | 
			
		||||
import MkButton from '../../components/ui/button.vue';
 | 
			
		||||
import MkContainer from '../../components/ui/container.vue';
 | 
			
		||||
import MkAvatars from '../../components/avatars.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: this.$t('groups') as string,
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
import MkPagination from '@/components/ui/pagination.vue';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import MkContainer from '@/components/ui/container.vue';
 | 
			
		||||
import MkAvatars from '@/components/avatars.vue';
 | 
			
		||||
import MkTab from '@/components/tab.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkPagination,
 | 
			
		||||
		MkButton,
 | 
			
		||||
		MkContainer,
 | 
			
		||||
		MkTab,
 | 
			
		||||
		MkAvatars,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('groups'),
 | 
			
		||||
					icon: faUsers
 | 
			
		||||
				}],
 | 
			
		||||
			},
 | 
			
		||||
			tab: 'owned',
 | 
			
		||||
			ownedPagination: {
 | 
			
		||||
				endpoint: 'users/groups/owned',
 | 
			
		||||
				limit: 10,
 | 
			
		||||
@@ -83,32 +87,26 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		async create() {
 | 
			
		||||
			const { canceled, result: name } = await this.$root.dialog({
 | 
			
		||||
			const { canceled, result: name } = await os.dialog({
 | 
			
		||||
				title: this.$t('groupName'),
 | 
			
		||||
				input: true
 | 
			
		||||
			});
 | 
			
		||||
			if (canceled) return;
 | 
			
		||||
			await this.$root.api('users/groups/create', { name: name });
 | 
			
		||||
			await os.api('users/groups/create', { name: name });
 | 
			
		||||
			this.$refs.owned.reload();
 | 
			
		||||
			this.$root.dialog({
 | 
			
		||||
				type: 'success',
 | 
			
		||||
				iconOnly: true, autoClose: true
 | 
			
		||||
			});
 | 
			
		||||
			os.success();
 | 
			
		||||
		},
 | 
			
		||||
		acceptInvite(invitation) {
 | 
			
		||||
			this.$root.api('users/groups/invitations/accept', {
 | 
			
		||||
			os.api('users/groups/invitations/accept', {
 | 
			
		||||
				invitationId: invitation.id
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
					type: 'success',
 | 
			
		||||
					iconOnly: true, autoClose: true
 | 
			
		||||
				});
 | 
			
		||||
				os.success();
 | 
			
		||||
				this.$refs.invitations.reload();
 | 
			
		||||
				this.$refs.joined.reload();
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
		rejectInvite(invitation) {
 | 
			
		||||
			this.$root.api('users/groups/invitations/reject', {
 | 
			
		||||
			os.api('users/groups/invitations/reject', {
 | 
			
		||||
				invitationId: invitation.id
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				this.$refs.invitations.reload();
 | 
			
		||||
 
 | 
			
		||||
@@ -1,31 +1,23 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="qkcjvfiv">
 | 
			
		||||
	<portal to="icon"><fa :icon="faListUl"/></portal>
 | 
			
		||||
	<portal to="title">{{ $t('manageLists') }}</portal>
 | 
			
		||||
<div class="qkcjvfiv _section">
 | 
			
		||||
	<MkButton @click="create" primary class="add"><Fa :icon="faPlus"/> {{ $t('createList') }}</MkButton>
 | 
			
		||||
 | 
			
		||||
	<mk-button @click="create" primary class="add"><fa :icon="faPlus"/> {{ $t('createList') }}</mk-button>
 | 
			
		||||
 | 
			
		||||
	<mk-pagination :pagination="pagination" #default="{items}" class="lists" ref="list">
 | 
			
		||||
	<MkPagination :pagination="pagination" #default="{items}" class="lists _content" ref="list">
 | 
			
		||||
		<div class="list _panel" v-for="(list, i) in items" :key="list.id">
 | 
			
		||||
			<router-link :to="`/my/lists/${ list.id }`">{{ list.name }}</router-link>
 | 
			
		||||
		</div>
 | 
			
		||||
	</mk-pagination>
 | 
			
		||||
	</MkPagination>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faListUl, faPlus } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import MkPagination from '../../components/ui/pagination.vue';
 | 
			
		||||
import MkButton from '../../components/ui/button.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: this.$t('manageLists') as string,
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
import MkPagination from '@/components/ui/pagination.vue';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkPagination,
 | 
			
		||||
		MkButton,
 | 
			
		||||
@@ -33,6 +25,16 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('manageLists'),
 | 
			
		||||
					icon: faListUl
 | 
			
		||||
				}],
 | 
			
		||||
				action: {
 | 
			
		||||
					icon: faPlus,
 | 
			
		||||
					handler: this.create
 | 
			
		||||
				}
 | 
			
		||||
			},
 | 
			
		||||
			pagination: {
 | 
			
		||||
				endpoint: 'users/lists/list',
 | 
			
		||||
				limit: 10,
 | 
			
		||||
@@ -43,17 +45,14 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		async create() {
 | 
			
		||||
			const { canceled, result: name } = await this.$root.dialog({
 | 
			
		||||
			const { canceled, result: name } = await os.dialog({
 | 
			
		||||
				title: this.$t('enterListName'),
 | 
			
		||||
				input: true
 | 
			
		||||
			});
 | 
			
		||||
			if (canceled) return;
 | 
			
		||||
			await this.$root.api('users/lists/create', { name: name });
 | 
			
		||||
			await os.api('users/lists/create', { name: name });
 | 
			
		||||
			this.$refs.list.reload();
 | 
			
		||||
			this.$root.dialog({
 | 
			
		||||
				type: 'success',
 | 
			
		||||
				iconOnly: true, autoClose: true
 | 
			
		||||
			});
 | 
			
		||||
			os.success();
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,62 +1,57 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="mk-list-page">
 | 
			
		||||
	<portal to="icon"><fa :icon="faListUl"/></portal>
 | 
			
		||||
	<portal to="title">{{ list.name }}</portal>
 | 
			
		||||
 | 
			
		||||
	<transition name="zoom" mode="out-in">
 | 
			
		||||
		<div v-if="list" class="_card _vMargin">
 | 
			
		||||
		<div v-if="list" class="_section">
 | 
			
		||||
			<div class="_content">
 | 
			
		||||
				<mk-button inline @click="renameList()">{{ $t('rename') }}</mk-button>
 | 
			
		||||
				<mk-button inline @click="deleteList()">{{ $t('delete') }}</mk-button>
 | 
			
		||||
				<MkButton inline @click="addUser()">{{ $t('addUser') }}</MkButton>
 | 
			
		||||
				<MkButton inline @click="renameList()">{{ $t('rename') }}</MkButton>
 | 
			
		||||
				<MkButton inline @click="deleteList()">{{ $t('delete') }}</MkButton>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</transition>
 | 
			
		||||
 | 
			
		||||
	<transition name="zoom" mode="out-in">
 | 
			
		||||
		<div v-if="list" class="_card members _vMargin">
 | 
			
		||||
		<div v-if="list" class="_section members _vMargin">
 | 
			
		||||
			<div class="_title">{{ $t('members') }}</div>
 | 
			
		||||
			<div class="_content">
 | 
			
		||||
				<div class="users">
 | 
			
		||||
					<div class="user" v-for="user in users" :key="user.id">
 | 
			
		||||
						<mk-avatar :user="user" class="avatar"/>
 | 
			
		||||
					<div class="user _panel" v-for="user in users" :key="user.id">
 | 
			
		||||
						<MkAvatar :user="user" class="avatar"/>
 | 
			
		||||
						<div class="body">
 | 
			
		||||
							<mk-user-name :user="user" class="name"/>
 | 
			
		||||
							<mk-acct :user="user" class="acct"/>
 | 
			
		||||
							<MkUserName :user="user" class="name"/>
 | 
			
		||||
							<MkAcct :user="user" class="acct"/>
 | 
			
		||||
						</div>
 | 
			
		||||
						<div class="action">
 | 
			
		||||
							<button class="_button" @click="removeUser(user)"><fa :icon="faTimes"/></button>
 | 
			
		||||
							<button class="_button" @click="removeUser(user)"><Fa :icon="faTimes"/></button>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="_footer">
 | 
			
		||||
				<mk-button inline @click="addUser()">{{ $t('addUser') }}</mk-button>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</transition>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { computed, defineComponent } from 'vue';
 | 
			
		||||
import { faTimes, faListUl } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import Progress from '../../scripts/loading';
 | 
			
		||||
import MkButton from '../../components/ui/button.vue';
 | 
			
		||||
import MkUserSelect from '../../components/user-select.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: this.list ? `${this.list.name} | ${this.$t('manageLists')}` : this.$t('manageLists')
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
import Progress from '@/scripts/loading';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkButton
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: computed(() => this.list ? {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.list.name,
 | 
			
		||||
					icon: faListUl,
 | 
			
		||||
				}],
 | 
			
		||||
			} : null),
 | 
			
		||||
			list: null,
 | 
			
		||||
			users: [],
 | 
			
		||||
			faTimes, faListUl
 | 
			
		||||
@@ -74,11 +69,11 @@ export default Vue.extend({
 | 
			
		||||
	methods: {
 | 
			
		||||
		fetch() {
 | 
			
		||||
			Progress.start();
 | 
			
		||||
			this.$root.api('users/lists/show', {
 | 
			
		||||
			os.api('users/lists/show', {
 | 
			
		||||
				listId: this.$route.params.list
 | 
			
		||||
			}).then(list => {
 | 
			
		||||
				this.list = list;
 | 
			
		||||
				this.$root.api('users/show', {
 | 
			
		||||
				os.api('users/show', {
 | 
			
		||||
					userIds: this.list.userIds
 | 
			
		||||
				}).then(users => {
 | 
			
		||||
					this.users = users;
 | 
			
		||||
@@ -88,27 +83,18 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		addUser() {
 | 
			
		||||
			this.$root.new(MkUserSelect, {}).$once('selected', user => {
 | 
			
		||||
				this.$root.api('users/lists/push', {
 | 
			
		||||
			os.selectUser().then(user => {
 | 
			
		||||
				os.apiWithDialog('users/lists/push', {
 | 
			
		||||
					listId: this.list.id,
 | 
			
		||||
					userId: user.id
 | 
			
		||||
				}).then(() => {
 | 
			
		||||
					this.users.push(user);
 | 
			
		||||
					this.$root.dialog({
 | 
			
		||||
						type: 'success',
 | 
			
		||||
						iconOnly: true, autoClose: true
 | 
			
		||||
					});
 | 
			
		||||
				}).catch(e => {
 | 
			
		||||
					this.$root.dialog({
 | 
			
		||||
						type: 'error',
 | 
			
		||||
						text: e
 | 
			
		||||
					});
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		removeUser(user) {
 | 
			
		||||
			this.$root.api('users/lists/pull', {
 | 
			
		||||
			os.api('users/lists/pull', {
 | 
			
		||||
				listId: this.list.id,
 | 
			
		||||
				userId: user.id
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
@@ -117,7 +103,7 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async renameList() {
 | 
			
		||||
			const { canceled, result: name } = await this.$root.dialog({
 | 
			
		||||
			const { canceled, result: name } = await os.dialog({
 | 
			
		||||
				title: this.$t('enterListName'),
 | 
			
		||||
				input: {
 | 
			
		||||
					default: this.list.name
 | 
			
		||||
@@ -125,7 +111,7 @@ export default Vue.extend({
 | 
			
		||||
			});
 | 
			
		||||
			if (canceled) return;
 | 
			
		||||
 | 
			
		||||
			await this.$root.api('users/lists/update', {
 | 
			
		||||
			await os.api('users/lists/update', {
 | 
			
		||||
				listId: this.list.id,
 | 
			
		||||
				name: name
 | 
			
		||||
			});
 | 
			
		||||
@@ -134,20 +120,17 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async deleteList() {
 | 
			
		||||
			const { canceled } = await this.$root.dialog({
 | 
			
		||||
			const { canceled } = await os.dialog({
 | 
			
		||||
				type: 'warning',
 | 
			
		||||
				text: this.$t('removeAreYouSure', { x: this.list.name }),
 | 
			
		||||
				showCancelButton: true
 | 
			
		||||
			});
 | 
			
		||||
			if (canceled) return;
 | 
			
		||||
 | 
			
		||||
			await this.$root.api('users/lists/delete', {
 | 
			
		||||
			await os.api('users/lists/delete', {
 | 
			
		||||
				listId: this.list.id
 | 
			
		||||
			});
 | 
			
		||||
			this.$root.dialog({
 | 
			
		||||
				type: 'success',
 | 
			
		||||
				iconOnly: true, autoClose: true
 | 
			
		||||
			});
 | 
			
		||||
			os.success();
 | 
			
		||||
			this.$router.push('/my/lists');
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@@ -158,13 +141,11 @@ export default Vue.extend({
 | 
			
		||||
.mk-list-page {
 | 
			
		||||
	> .members {
 | 
			
		||||
		> ._content {
 | 
			
		||||
			max-height: 400px;
 | 
			
		||||
			overflow: auto;
 | 
			
		||||
 | 
			
		||||
			> .users {
 | 
			
		||||
				> .user {
 | 
			
		||||
					display: flex;
 | 
			
		||||
					align-items: center;
 | 
			
		||||
					padding: 16px;
 | 
			
		||||
 | 
			
		||||
					> .avatar {
 | 
			
		||||
						width: 50px;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,58 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
<section class="_card">
 | 
			
		||||
	<div class="_title"><fa :icon="faKey"/> API</div>
 | 
			
		||||
	<div class="_content">
 | 
			
		||||
		<mk-button @click="generateToken">{{ $t('generateAccessToken') }}</mk-button>
 | 
			
		||||
		<mk-button @click="regenerateToken"><fa :icon="faSyncAlt"/> {{ $t('regenerate') }}</mk-button>
 | 
			
		||||
	</div>
 | 
			
		||||
</section>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { faKey, faSyncAlt } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import MkButton from '../../components/ui/button.vue';
 | 
			
		||||
import MkInput from '../../components/ui/input.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkButton, MkInput
 | 
			
		||||
	},
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			faKey, faSyncAlt
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
	methods: {
 | 
			
		||||
		async generateToken() {
 | 
			
		||||
			this.$root.new(await import('../../components/token-generate-window.vue').then(m => m.default), {
 | 
			
		||||
			}).$on('ok', async ({ name, permissions }) => {
 | 
			
		||||
				const { token } = await this.$root.api('miauth/gen-token', {
 | 
			
		||||
					session: null,
 | 
			
		||||
					name: name,
 | 
			
		||||
					permission: permissions,
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
					type: 'success',
 | 
			
		||||
					title: this.$t('token'),
 | 
			
		||||
					text: token
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
		regenerateToken() {
 | 
			
		||||
			this.$root.dialog({
 | 
			
		||||
				title: this.$t('password'),
 | 
			
		||||
				input: {
 | 
			
		||||
					type: 'password'
 | 
			
		||||
				}
 | 
			
		||||
			}).then(({ canceled, result: password }) => {
 | 
			
		||||
				if (canceled) return;
 | 
			
		||||
				this.$root.api('i/regenerate_token', {
 | 
			
		||||
					password: password
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
@@ -1,137 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
	<portal to="icon"><fa :icon="faCog"/></portal>
 | 
			
		||||
	<portal to="title">{{ $t('accountSettings') }}</portal>
 | 
			
		||||
 | 
			
		||||
	<x-profile-setting class="_vMargin"/>
 | 
			
		||||
	<x-privacy-setting class="_vMargin"/>
 | 
			
		||||
	<x-reaction-setting class="_vMargin"/>
 | 
			
		||||
 | 
			
		||||
	<section class="_card _vMargin">
 | 
			
		||||
		<div class="_title"><fa :icon="faCog"/> {{ $t('general') }}</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-switch v-model="$store.state.i.autoWatch" @change="onChangeAutoWatch">
 | 
			
		||||
				{{ $t('autoNoteWatch') }}<template #desc>{{ $t('autoNoteWatchDescription') }}</template>
 | 
			
		||||
			</mk-switch>
 | 
			
		||||
			<mk-switch v-model="$store.state.i.injectFeaturedNote" @change="onChangeInjectFeaturedNote">
 | 
			
		||||
				{{ $t('showFeaturedNotesInTimeline') }}
 | 
			
		||||
			</mk-switch>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-button @click="readAllNotifications">{{ $t('markAsReadAllNotifications') }}</mk-button>
 | 
			
		||||
			<mk-button @click="readAllUnreadNotes">{{ $t('markAsReadAllUnreadNotes') }}</mk-button>
 | 
			
		||||
			<mk-button @click="readAllMessagingMessages">{{ $t('markAsReadAllTalkMessages') }}</mk-button>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-button @click="configure">{{ $t('notificationSetting') }}</mk-button>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<x-import-export class="_vMargin"/>
 | 
			
		||||
	<x-drive class="_vMargin"/>
 | 
			
		||||
	<x-mute-block class="_vMargin"/>
 | 
			
		||||
	<x-word-mute class="_vMargin"/>
 | 
			
		||||
	<x-security class="_vMargin"/>
 | 
			
		||||
	<x-2fa class="_vMargin"/>
 | 
			
		||||
	<x-integration class="_vMargin"/>
 | 
			
		||||
	<x-api class="_vMargin"/>
 | 
			
		||||
 | 
			
		||||
	<router-link class="_panel _buttonPrimary" to="/my/apps" style="margin: var(--margin) auto;">{{ $t('installedApps') }}</router-link>
 | 
			
		||||
 | 
			
		||||
	<button class="_panel _buttonPrimary" @click="$root.signout()" style="margin: var(--margin) auto;">{{ $t('logout') }}</button>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { faCog } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import XProfileSetting from './profile.vue';
 | 
			
		||||
import XPrivacySetting from './privacy.vue';
 | 
			
		||||
import XImportExport from './import-export.vue';
 | 
			
		||||
import XDrive from './drive.vue';
 | 
			
		||||
import XReactionSetting from './reaction.vue';
 | 
			
		||||
import XMuteBlock from './mute-block.vue';
 | 
			
		||||
import XWordMute from './word-mute.vue';
 | 
			
		||||
import XSecurity from './security.vue';
 | 
			
		||||
import X2fa from './2fa.vue';
 | 
			
		||||
import XIntegration from './integration.vue';
 | 
			
		||||
import XApi from './api.vue';
 | 
			
		||||
import MkButton from '../../components/ui/button.vue';
 | 
			
		||||
import MkSwitch from '../../components/ui/switch.vue';
 | 
			
		||||
import { notificationTypes } from '../../../types';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: this.$t('settings') as string
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	components: {
 | 
			
		||||
		XProfileSetting,
 | 
			
		||||
		XPrivacySetting,
 | 
			
		||||
		XImportExport,
 | 
			
		||||
		XDrive,
 | 
			
		||||
		XReactionSetting,
 | 
			
		||||
		XMuteBlock,
 | 
			
		||||
		XWordMute,
 | 
			
		||||
		XSecurity,
 | 
			
		||||
		X2fa,
 | 
			
		||||
		XIntegration,
 | 
			
		||||
		XApi,
 | 
			
		||||
		MkButton,
 | 
			
		||||
		MkSwitch,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			faCog
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		onChangeAutoWatch(v) {
 | 
			
		||||
			this.$root.api('i/update', {
 | 
			
		||||
				autoWatch: v
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onChangeInjectFeaturedNote(v) {
 | 
			
		||||
			this.$root.api('i/update', {
 | 
			
		||||
				injectFeaturedNote: v
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		readAllUnreadNotes() {
 | 
			
		||||
			this.$root.api('i/read-all-unread-notes');
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		readAllMessagingMessages() {
 | 
			
		||||
			this.$root.api('i/read-all-messaging-messages');
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		readAllNotifications() {
 | 
			
		||||
			this.$root.api('notifications/mark-all-as-read');
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async configure() {
 | 
			
		||||
			const includingTypes = notificationTypes.filter(x => !this.$store.state.i.mutingNotificationTypes.includes(x));
 | 
			
		||||
			this.$root.new(await import('../../components/notification-setting-window.vue').then(m => m.default), {
 | 
			
		||||
				includingTypes,
 | 
			
		||||
				showGlobalToggle: false,
 | 
			
		||||
			}).$on('ok', async ({ includingTypes: value }: any) => {
 | 
			
		||||
				await this.$root.api('i/update', {
 | 
			
		||||
					mutingNotificationTypes: notificationTypes.filter(x => !value.includes(x)),
 | 
			
		||||
				}).then(i => {
 | 
			
		||||
					this.$store.state.i.mutingNotificationTypes = i.mutingNotificationTypes;
 | 
			
		||||
				}).catch(err => {
 | 
			
		||||
					this.$root.dialog({
 | 
			
		||||
						type: 'error',
 | 
			
		||||
						text: err.message
 | 
			
		||||
					});
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
@@ -1,73 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
<section class="rrfwjxfl _card">
 | 
			
		||||
	<div class="_title"><fa :icon="faBan"/> {{ $t('muteAndBlock') }}</div>
 | 
			
		||||
	<div class="_content">
 | 
			
		||||
		<span>{{ $t('mutedUsers') }}</span>
 | 
			
		||||
		<mk-pagination :pagination="mutingPagination" class="muting">
 | 
			
		||||
			<template #empty><span>{{ $t('noUsers') }}</span></template>
 | 
			
		||||
			<template #default="{items}">
 | 
			
		||||
				<div class="user" v-for="(mute, i) in items" :key="mute.id">
 | 
			
		||||
					<router-link class="name" :to="mute.mutee | userPage">
 | 
			
		||||
						<mk-acct :user="mute.mutee"/>
 | 
			
		||||
					</router-link>
 | 
			
		||||
				</div>
 | 
			
		||||
			</template>
 | 
			
		||||
		</mk-pagination>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="_content">
 | 
			
		||||
		<span>{{ $t('blockedUsers') }}</span>
 | 
			
		||||
		<mk-pagination :pagination="blockingPagination" class="blocking">
 | 
			
		||||
			<template #empty><span>{{ $t('noUsers') }}</span></template>
 | 
			
		||||
			<template #default="{items}">
 | 
			
		||||
				<div class="user" v-for="(block, i) in items" :key="block.id">
 | 
			
		||||
					<router-link class="name" :to="block.blockee | userPage">
 | 
			
		||||
						<mk-acct :user="block.blockee"/>
 | 
			
		||||
					</router-link>
 | 
			
		||||
				</div>
 | 
			
		||||
			</template>
 | 
			
		||||
		</mk-pagination>
 | 
			
		||||
	</div>
 | 
			
		||||
</section>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { faBan } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import MkPagination from '../../components/ui/pagination.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkPagination,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			mutingPagination: {
 | 
			
		||||
				endpoint: 'mute/list',
 | 
			
		||||
				limit: 10,
 | 
			
		||||
			},
 | 
			
		||||
			blockingPagination: {
 | 
			
		||||
				endpoint: 'blocking/list',
 | 
			
		||||
				limit: 10,
 | 
			
		||||
			},
 | 
			
		||||
			faBan
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.rrfwjxfl {
 | 
			
		||||
	> ._content {
 | 
			
		||||
		max-height: 350px;
 | 
			
		||||
		overflow: auto;
 | 
			
		||||
 | 
			
		||||
		> .muting,
 | 
			
		||||
		> .blocking {
 | 
			
		||||
			> .empty {
 | 
			
		||||
				opacity: 0.5 !important;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,73 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
<section class="_card">
 | 
			
		||||
	<div class="_title"><fa :icon="faLock"/> {{ $t('privacy') }}</div>
 | 
			
		||||
	<div class="_content">
 | 
			
		||||
		<mk-switch v-model="isLocked" @change="save()">{{ $t('makeFollowManuallyApprove') }}</mk-switch>
 | 
			
		||||
		<mk-switch v-model="autoAcceptFollowed" v-if="isLocked" @change="save()">{{ $t('autoAcceptFollowed') }}</mk-switch>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="_content">
 | 
			
		||||
		<mk-switch v-model="rememberNoteVisibility" @change="save()">{{ $t('rememberNoteVisibility') }}</mk-switch>
 | 
			
		||||
		<mk-select v-model="defaultNoteVisibility" style="margin-bottom: 8px;" v-if="!rememberNoteVisibility">
 | 
			
		||||
			<template #label>{{ $t('defaultNoteVisibility') }}</template>
 | 
			
		||||
			<option value="public">{{ $t('_visibility.public') }}</option>
 | 
			
		||||
			<option value="home">{{ $t('_visibility.home') }}</option>
 | 
			
		||||
			<option value="followers">{{ $t('_visibility.followers') }}</option>
 | 
			
		||||
			<option value="specified">{{ $t('_visibility.specified') }}</option>
 | 
			
		||||
		</mk-select>
 | 
			
		||||
		<mk-switch v-model="defaultNoteLocalOnly" v-if="!rememberNoteVisibility">{{ $t('_visibility.localOnly') }}</mk-switch>
 | 
			
		||||
	</div>
 | 
			
		||||
</section>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { faLock } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import MkSelect from '../../components/ui/select.vue';
 | 
			
		||||
import MkSwitch from '../../components/ui/switch.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkSelect,
 | 
			
		||||
		MkSwitch,
 | 
			
		||||
	},
 | 
			
		||||
	
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			isLocked: false,
 | 
			
		||||
			autoAcceptFollowed: false,
 | 
			
		||||
			faLock
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	computed: {
 | 
			
		||||
		defaultNoteVisibility: {
 | 
			
		||||
			get() { return this.$store.state.settings.defaultNoteVisibility; },
 | 
			
		||||
			set(value) { this.$store.dispatch('settings/set', { key: 'defaultNoteVisibility', value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		defaultNoteLocalOnly: {
 | 
			
		||||
			get() { return this.$store.state.settings.defaultNoteLocalOnly; },
 | 
			
		||||
			set(value) { this.$store.dispatch('settings/set', { key: 'defaultNoteLocalOnly', value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		rememberNoteVisibility: {
 | 
			
		||||
			get() { return this.$store.state.settings.rememberNoteVisibility; },
 | 
			
		||||
			set(value) { this.$store.dispatch('settings/set', { key: 'rememberNoteVisibility', value }); }
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		this.isLocked = this.$store.state.i.isLocked;
 | 
			
		||||
		this.autoAcceptFollowed = this.$store.state.i.autoAcceptFollowed;
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		save() {
 | 
			
		||||
			this.$root.api('i/update', {
 | 
			
		||||
				isLocked: !!this.isLocked,
 | 
			
		||||
				autoAcceptFollowed: !!this.autoAcceptFollowed,
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
@@ -1,84 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
<section class="_card">
 | 
			
		||||
	<div class="_title"><fa :icon="faLaugh"/> {{ $t('reaction') }}</div>
 | 
			
		||||
	<div class="_content">
 | 
			
		||||
		<mk-input v-model="reactions" style="font-family: 'Segoe UI Emoji', 'Noto Color Emoji', Roboto, HelveticaNeue, Arial, sans-serif">
 | 
			
		||||
			{{ $t('reaction') }}<template #desc>{{ $t('reactionSettingDescription') }} <button class="_textButton" @click="chooseEmoji">{{ $t('chooseEmoji') }}</button></template>
 | 
			
		||||
		</mk-input>
 | 
			
		||||
		<mk-button inline @click="setDefault"><fa :icon="faUndo"/> {{ $t('default') }}</mk-button>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="_footer">
 | 
			
		||||
		<mk-button @click="save()" primary inline :disabled="!changed"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
 | 
			
		||||
		<mk-button inline @click="preview"><fa :icon="faEye"/> {{ $t('preview') }}</mk-button>
 | 
			
		||||
	</div>
 | 
			
		||||
</section>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { faLaugh, faSave, faEye } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import { faUndo } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import MkInput from '../../components/ui/input.vue';
 | 
			
		||||
import MkButton from '../../components/ui/button.vue';
 | 
			
		||||
import MkReactionPicker from '../../components/reaction-picker.vue';
 | 
			
		||||
import { emojiRegexWithCustom } from '../../../misc/emoji-regex';
 | 
			
		||||
import { defaultSettings } from '../../store';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkInput,
 | 
			
		||||
		MkButton,
 | 
			
		||||
	},
 | 
			
		||||
	
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			reactions: this.$store.state.settings.reactions.join(''),
 | 
			
		||||
			changed: false,
 | 
			
		||||
			faLaugh, faSave, faEye, faUndo
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	computed: {
 | 
			
		||||
		splited(): any {
 | 
			
		||||
			return this.reactions.match(emojiRegexWithCustom);
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	watch: {
 | 
			
		||||
		reactions() {
 | 
			
		||||
			this.changed = true;
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		save() {
 | 
			
		||||
			this.$store.dispatch('settings/set', { key: 'reactions', value: this.splited });
 | 
			
		||||
			this.changed = false;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		preview(ev) {
 | 
			
		||||
			const picker = this.$root.new(MkReactionPicker, {
 | 
			
		||||
				source: ev.currentTarget || ev.target,
 | 
			
		||||
				reactions: this.splited,
 | 
			
		||||
				showFocus: false,
 | 
			
		||||
			});
 | 
			
		||||
			picker.$once('chosen', reaction => {
 | 
			
		||||
				picker.close();
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		setDefault() {
 | 
			
		||||
			this.reactions = defaultSettings.reactions.join('');
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async chooseEmoji(ev) {
 | 
			
		||||
			const vm = this.$root.new(await import('../../components/emoji-picker.vue').then(m => m.default), {
 | 
			
		||||
				source: ev.currentTarget || ev.target
 | 
			
		||||
			}).$once('chosen', emoji => {
 | 
			
		||||
				this.reactions += emoji;
 | 
			
		||||
				vm.close();
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
@@ -1,84 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
<section class="_card">
 | 
			
		||||
	<div class="_title"><fa :icon="faLock"/> {{ $t('password') }}</div>
 | 
			
		||||
	<div class="_content">
 | 
			
		||||
		<mk-button primary @click="change()">{{ $t('changePassword') }}</mk-button>
 | 
			
		||||
	</div>
 | 
			
		||||
</section>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { faLock } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import MkButton from '../../components/ui/button.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkButton,
 | 
			
		||||
	},
 | 
			
		||||
	
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			faLock
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		async change() {
 | 
			
		||||
			const { canceled: canceled1, result: currentPassword } = await this.$root.dialog({
 | 
			
		||||
				title: this.$t('currentPassword'),
 | 
			
		||||
				input: {
 | 
			
		||||
					type: 'password'
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
			if (canceled1) return;
 | 
			
		||||
 | 
			
		||||
			const { canceled: canceled2, result: newPassword } = await this.$root.dialog({
 | 
			
		||||
				title: this.$t('newPassword'),
 | 
			
		||||
				input: {
 | 
			
		||||
					type: 'password'
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
			if (canceled2) return;
 | 
			
		||||
 | 
			
		||||
			const { canceled: canceled3, result: newPassword2 } = await this.$root.dialog({
 | 
			
		||||
				title: this.$t('newPasswordRetype'),
 | 
			
		||||
				input: {
 | 
			
		||||
					type: 'password'
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
			if (canceled3) return;
 | 
			
		||||
 | 
			
		||||
			if (newPassword !== newPassword2) {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: this.$t('retypedNotMatch')
 | 
			
		||||
				});
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const dialog = this.$root.dialog({
 | 
			
		||||
				type: 'waiting',
 | 
			
		||||
				iconOnly: true
 | 
			
		||||
			});
 | 
			
		||||
			
 | 
			
		||||
			this.$root.api('i/change-password', {
 | 
			
		||||
				currentPassword,
 | 
			
		||||
				newPassword
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
					type: 'success',
 | 
			
		||||
					iconOnly: true, autoClose: true
 | 
			
		||||
				});
 | 
			
		||||
			}).catch(e => {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: e
 | 
			
		||||
				});
 | 
			
		||||
			}).finally(() => {
 | 
			
		||||
				dialog.close();
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
@@ -1,81 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
<section class="_card">
 | 
			
		||||
	<div class="_title"><fa :icon="faCommentSlash"/> {{ $t('wordMute') }}</div>
 | 
			
		||||
	<div class="_content _noPad">
 | 
			
		||||
		<mk-tab v-model="tab" :items="[{ label: $t('_wordMute.soft'), value: 'soft' }, { label: $t('_wordMute.hard'), value: 'hard' }]"/>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="_content" v-show="tab === 'soft'">
 | 
			
		||||
		<mk-info>{{ $t('_wordMute.softDescription') }}</mk-info>
 | 
			
		||||
		<mk-textarea v-model="softMutedWords">
 | 
			
		||||
			<span>{{ $t('_wordMute.muteWords') }}</span>
 | 
			
		||||
			<template #desc>{{ $t('_wordMute.muteWordsDescription') }}<br>{{ $t('_wordMute.muteWordsDescription2') }}</template>
 | 
			
		||||
		</mk-textarea>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="_content" v-show="tab === 'hard'">
 | 
			
		||||
		<mk-info>{{ $t('_wordMute.hardDescription') }}</mk-info>
 | 
			
		||||
		<mk-textarea v-model="hardMutedWords" style="margin-bottom: 16px;">
 | 
			
		||||
			<span>{{ $t('_wordMute.muteWords') }}</span>
 | 
			
		||||
			<template #desc>{{ $t('_wordMute.muteWordsDescription') }}<br>{{ $t('_wordMute.muteWordsDescription2') }}</template>
 | 
			
		||||
		</mk-textarea>
 | 
			
		||||
		<div v-if="hardWordMutedNotesCount != null" class="_caption">{{ $t('_wordMute.mutedNotes') }}: {{ hardWordMutedNotesCount | number }}</div>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="_footer">
 | 
			
		||||
		<mk-button @click="save()" primary inline :disabled="!changed"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
 | 
			
		||||
	</div>
 | 
			
		||||
</section>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { faCommentSlash, faSave } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import MkButton from '../../components/ui/button.vue';
 | 
			
		||||
import MkTextarea from '../../components/ui/textarea.vue';
 | 
			
		||||
import MkTab from '../../components/tab.vue';
 | 
			
		||||
import MkInfo from '../../components/ui/info.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkButton,
 | 
			
		||||
		MkTextarea,
 | 
			
		||||
		MkTab,
 | 
			
		||||
		MkInfo,
 | 
			
		||||
	},
 | 
			
		||||
	
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			tab: 'soft',
 | 
			
		||||
			softMutedWords: '',
 | 
			
		||||
			hardMutedWords: '',
 | 
			
		||||
			hardWordMutedNotesCount: null,
 | 
			
		||||
			changed: false,
 | 
			
		||||
			faCommentSlash, faSave,
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	watch: {
 | 
			
		||||
		softMutedWords() {
 | 
			
		||||
			this.changed = true;
 | 
			
		||||
		},
 | 
			
		||||
		hardMutedWords() {
 | 
			
		||||
			this.changed = true;
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	async created() {
 | 
			
		||||
		this.softMutedWords = this.$store.state.settings.mutedWords.map(x => x.join(' ')).join('\n');
 | 
			
		||||
		this.hardMutedWords = this.$store.state.i.mutedWords.map(x => x.join(' ')).join('\n');
 | 
			
		||||
 | 
			
		||||
		this.hardWordMutedNotesCount = (await this.$root.api('i/get-word-muted-notes-count', {})).count;
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		async save() {
 | 
			
		||||
			this.$store.dispatch('settings/set', { key: 'mutedWords', value: this.softMutedWords.trim().split('\n').map(x => x.trim().split(' ')) });
 | 
			
		||||
			await this.$root.api('i/update', {
 | 
			
		||||
				mutedWords: this.hardMutedWords.trim().split('\n').map(x => x.trim().split(' ')),
 | 
			
		||||
			});
 | 
			
		||||
			this.changed = false;
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
@@ -1,8 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="ipledcug">
 | 
			
		||||
	<portal to="icon"><fa :icon="faExclamationTriangle"/></portal>
 | 
			
		||||
	<portal to="title">{{ $t('notFound') }}</portal>
 | 
			
		||||
 | 
			
		||||
	<div class="_fullinfo">
 | 
			
		||||
		<img src="https://xn--931a.moe/assets/not-found.jpg" class="_ghost"/>
 | 
			
		||||
		<div>{{ $t('notFoundDescription') }}</div>
 | 
			
		||||
@@ -11,19 +8,19 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: this.$t('notFound') as string
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			faExclamationTriangle
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('notFound'),
 | 
			
		||||
					icon: faExclamationTriangle
 | 
			
		||||
				}]
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,53 +1,55 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="mk-note-page">
 | 
			
		||||
	<portal to="avatar" v-if="note"><mk-avatar class="avatar" :user="note.user" :disable-preview="true"/></portal>
 | 
			
		||||
	<portal to="title" v-if="note">
 | 
			
		||||
		<mfm 
 | 
			
		||||
			:text="$t('noteOf', { user: note.user.name || note.user.username })"
 | 
			
		||||
			:plain="true" :nowrap="true" :custom-emojis="note.user.emojis" :is-note="false"
 | 
			
		||||
		/>
 | 
			
		||||
	</portal>
 | 
			
		||||
<div class="fcuexfpr">
 | 
			
		||||
	<div v-if="note" class="note">
 | 
			
		||||
		<div class="_section">
 | 
			
		||||
			<XNotes v-if="showNext" class="_content" :pagination="next"/>
 | 
			
		||||
			<MkButton v-else-if="hasNext" class="load _content" @click="showNext = true"><Fa :icon="faChevronUp"/></MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
	<div v-if="note">
 | 
			
		||||
		<button class="_panel _button" v-if="hasNext && !showNext" @click="showNext = true" style="margin: 0 auto var(--margin) auto;"><fa :icon="faChevronUp"/></button>
 | 
			
		||||
		<x-notes v-if="showNext" ref="next" :pagination="next"/>
 | 
			
		||||
		<hr v-if="showNext"/>
 | 
			
		||||
		<div class="_section">
 | 
			
		||||
			<div class="_content">
 | 
			
		||||
				<MkRemoteCaution v-if="note.user.host != null" :href="note.url || note.uri" style="margin-bottom: var(--margin)"/>
 | 
			
		||||
				<XNote v-model:note="note" :key="note.id" :detail="true"/>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
		<mk-remote-caution v-if="note.user.host != null" :href="note.url || note.uri" style="margin-bottom: var(--margin)"/>
 | 
			
		||||
		<x-note v-model="note" :key="note.id" :detail="true"/>
 | 
			
		||||
 | 
			
		||||
		<button class="_panel _button" v-if="hasPrev && !showPrev" @click="showPrev = true" style="margin: var(--margin) auto 0 auto;"><fa :icon="faChevronDown"/></button>
 | 
			
		||||
		<hr v-if="showPrev"/>
 | 
			
		||||
		<x-notes v-if="showPrev" ref="prev" :pagination="prev" style="margin-top: var(--margin);"/>
 | 
			
		||||
		<div class="_section">
 | 
			
		||||
			<XNotes v-if="showPrev" class="_content" :pagination="prev"/>
 | 
			
		||||
			<MkButton v-else-if="hasPrev" class="load _content" @click="showPrev = true"><Fa :icon="faChevronDown"/></MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<div v-if="error">
 | 
			
		||||
		<mk-error @retry="fetch()"/>
 | 
			
		||||
		<MkError @retry="fetch()"/>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { computed, defineComponent } from 'vue';
 | 
			
		||||
import { faChevronUp, faChevronDown } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import Progress from '../scripts/loading';
 | 
			
		||||
import XNote from '../components/note.vue';
 | 
			
		||||
import XNotes from '../components/notes.vue';
 | 
			
		||||
import MkRemoteCaution from '../components/remote-caution.vue';
 | 
			
		||||
import Progress from '@/scripts/loading';
 | 
			
		||||
import XNote from '@/components/note.vue';
 | 
			
		||||
import XNotes from '@/components/notes.vue';
 | 
			
		||||
import MkRemoteCaution from '@/components/remote-caution.vue';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: this.$t('note') as string
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XNote,
 | 
			
		||||
		XNotes,
 | 
			
		||||
		MkRemoteCaution,
 | 
			
		||||
		MkButton,
 | 
			
		||||
	},
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: computed(() => this.note ? {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('note'),
 | 
			
		||||
					avatar: this.note.user,
 | 
			
		||||
				}],
 | 
			
		||||
			} : null),
 | 
			
		||||
			note: null,
 | 
			
		||||
			hasPrev: false,
 | 
			
		||||
			hasNext: false,
 | 
			
		||||
@@ -83,16 +85,16 @@ export default Vue.extend({
 | 
			
		||||
	methods: {
 | 
			
		||||
		fetch() {
 | 
			
		||||
			Progress.start();
 | 
			
		||||
			this.$root.api('notes/show', {
 | 
			
		||||
			os.api('notes/show', {
 | 
			
		||||
				noteId: this.$route.params.note
 | 
			
		||||
			}).then(note => {
 | 
			
		||||
				Promise.all([
 | 
			
		||||
					this.$root.api('users/notes', {
 | 
			
		||||
					os.api('users/notes', {
 | 
			
		||||
						userId: note.userId,
 | 
			
		||||
						untilId: note.id,
 | 
			
		||||
						limit: 1,
 | 
			
		||||
					}),
 | 
			
		||||
					this.$root.api('users/notes', {
 | 
			
		||||
					os.api('users/notes', {
 | 
			
		||||
						userId: note.userId,
 | 
			
		||||
						sinceId: note.id,
 | 
			
		||||
						limit: 1,
 | 
			
		||||
@@ -111,3 +113,16 @@ export default Vue.extend({
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.fcuexfpr {
 | 
			
		||||
	> .note {
 | 
			
		||||
		> ._section {
 | 
			
		||||
			> .load {
 | 
			
		||||
				min-width: 0;
 | 
			
		||||
				border-radius: 999px;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,31 +1,31 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
	<portal to="icon"><fa :icon="faBell"/></portal>
 | 
			
		||||
	<portal to="title">{{ $t('notifications') }}</portal>
 | 
			
		||||
	<x-notifications @before="before" @after="after" page/>
 | 
			
		||||
	<div class="_section">
 | 
			
		||||
		<XNotifications class="_content" @before="before" @after="after" page/>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faBell } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import Progress from '../scripts/loading';
 | 
			
		||||
import XNotifications from '../components/notifications.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: this.$t('notifications') as string
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
import Progress from '@/scripts/loading';
 | 
			
		||||
import XNotifications from '@/components/notifications.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XNotifications
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			faBell
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('notifications'),
 | 
			
		||||
					icon: faBell
 | 
			
		||||
				}]
 | 
			
		||||
			},
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,24 +1,24 @@
 | 
			
		||||
<template>
 | 
			
		||||
<x-container @remove="() => $emit('remove')" :draggable="true">
 | 
			
		||||
	<template #header><fa :icon="faBolt"/> {{ $t('_pages.blocks.button') }}</template>
 | 
			
		||||
<XContainer @remove="() => $emit('remove')" :draggable="true">
 | 
			
		||||
	<template #header><Fa :icon="faBolt"/> {{ $t('_pages.blocks.button') }}</template>
 | 
			
		||||
 | 
			
		||||
	<section class="xfhsjczc">
 | 
			
		||||
		<mk-input v-model="value.text"><span>{{ $t('_pages.blocks._button.text') }}</span></mk-input>
 | 
			
		||||
		<mk-switch v-model="value.primary"><span>{{ $t('_pages.blocks._button.colored') }}</span></mk-switch>
 | 
			
		||||
		<mk-select v-model="value.action">
 | 
			
		||||
		<MkInput v-model:value="value.text"><span>{{ $t('_pages.blocks._button.text') }}</span></MkInput>
 | 
			
		||||
		<MkSwitch v-model:value="value.primary"><span>{{ $t('_pages.blocks._button.colored') }}</span></MkSwitch>
 | 
			
		||||
		<MkSelect v-model:value="value.action">
 | 
			
		||||
			<template #label>{{ $t('_pages.blocks._button.action') }}</template>
 | 
			
		||||
			<option value="dialog">{{ $t('_pages.blocks._button._action.dialog') }}</option>
 | 
			
		||||
			<option value="resetRandom">{{ $t('_pages.blocks._button._action.resetRandom') }}</option>
 | 
			
		||||
			<option value="pushEvent">{{ $t('_pages.blocks._button._action.pushEvent') }}</option>
 | 
			
		||||
			<option value="callAiScript">{{ $t('_pages.blocks._button._action.callAiScript') }}</option>
 | 
			
		||||
		</mk-select>
 | 
			
		||||
		</MkSelect>
 | 
			
		||||
		<template v-if="value.action === 'dialog'">
 | 
			
		||||
			<mk-input v-model="value.content"><span>{{ $t('_pages.blocks._button._action._dialog.content') }}</span></mk-input>
 | 
			
		||||
			<MkInput v-model:value="value.content"><span>{{ $t('_pages.blocks._button._action._dialog.content') }}</span></MkInput>
 | 
			
		||||
		</template>
 | 
			
		||||
		<template v-else-if="value.action === 'pushEvent'">
 | 
			
		||||
			<mk-input v-model="value.event"><span>{{ $t('_pages.blocks._button._action._pushEvent.event') }}</span></mk-input>
 | 
			
		||||
			<mk-input v-model="value.message"><span>{{ $t('_pages.blocks._button._action._pushEvent.message') }}</span></mk-input>
 | 
			
		||||
			<mk-select v-model="value.var">
 | 
			
		||||
			<MkInput v-model:value="value.event"><span>{{ $t('_pages.blocks._button._action._pushEvent.event') }}</span></MkInput>
 | 
			
		||||
			<MkInput v-model:value="value.message"><span>{{ $t('_pages.blocks._button._action._pushEvent.message') }}</span></MkInput>
 | 
			
		||||
			<MkSelect v-model:value="value.var">
 | 
			
		||||
				<template #label>{{ $t('_pages.blocks._button._action._pushEvent.variable') }}</template>
 | 
			
		||||
				<option :value="null">{{ $t('_pages.blocks._button._action._pushEvent.no-variable') }}</option>
 | 
			
		||||
				<option v-for="v in hpml.getVarsByType()" :value="v.name">{{ v.name }}</option>
 | 
			
		||||
@@ -28,24 +28,25 @@
 | 
			
		||||
				<optgroup :label="$t('_pages.script.enviromentVariables')">
 | 
			
		||||
					<option v-for="v in hpml.getEnvVarsByType()" :value="v">{{ v }}</option>
 | 
			
		||||
				</optgroup>
 | 
			
		||||
			</mk-select>
 | 
			
		||||
			</MkSelect>
 | 
			
		||||
		</template>
 | 
			
		||||
		<template v-else-if="value.action === 'callAiScript'">
 | 
			
		||||
			<mk-input v-model="value.fn"><span>{{ $t('_pages.blocks._button._action._callAiScript.functionName') }}</span></mk-input>
 | 
			
		||||
			<MkInput v-model:value="value.fn"><span>{{ $t('_pages.blocks._button._action._callAiScript.functionName') }}</span></MkInput>
 | 
			
		||||
		</template>
 | 
			
		||||
	</section>
 | 
			
		||||
</x-container>
 | 
			
		||||
</XContainer>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faBolt } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import XContainer from '../page-editor.container.vue';
 | 
			
		||||
import MkSelect from '../../../components/ui/select.vue';
 | 
			
		||||
import MkInput from '../../../components/ui/input.vue';
 | 
			
		||||
import MkSwitch from '../../../components/ui/switch.vue';
 | 
			
		||||
import MkSelect from '@/components/ui/select.vue';
 | 
			
		||||
import MkInput from '@/components/ui/input.vue';
 | 
			
		||||
import MkSwitch from '@/components/ui/switch.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XContainer, MkSelect, MkInput, MkSwitch
 | 
			
		||||
	},
 | 
			
		||||
@@ -66,14 +67,14 @@ export default Vue.extend({
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		if (this.value.text == null) Vue.set(this.value, 'text', '');
 | 
			
		||||
		if (this.value.action == null) Vue.set(this.value, 'action', 'dialog');
 | 
			
		||||
		if (this.value.content == null) Vue.set(this.value, 'content', null);
 | 
			
		||||
		if (this.value.event == null) Vue.set(this.value, 'event', null);
 | 
			
		||||
		if (this.value.message == null) Vue.set(this.value, 'message', null);
 | 
			
		||||
		if (this.value.primary == null) Vue.set(this.value, 'primary', false);
 | 
			
		||||
		if (this.value.var == null) Vue.set(this.value, 'var', null);
 | 
			
		||||
		if (this.value.fn == null) Vue.set(this.value, 'fn', null);
 | 
			
		||||
		if (this.value.text == null) this.value.text = '';
 | 
			
		||||
		if (this.value.action == null) this.value.action = 'dialog';
 | 
			
		||||
		if (this.value.content == null) this.value.content = null;
 | 
			
		||||
		if (this.value.event == null) this.value.event = null;
 | 
			
		||||
		if (this.value.message == null) this.value.message = null;
 | 
			
		||||
		if (this.value.primary == null) this.value.primary = false;
 | 
			
		||||
		if (this.value.var == null) this.value.var = null;
 | 
			
		||||
		if (this.value.fn == null) this.value.fn = null;
 | 
			
		||||
	},
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,22 +1,23 @@
 | 
			
		||||
<template>
 | 
			
		||||
<x-container @remove="() => $emit('remove')" :draggable="true">
 | 
			
		||||
	<template #header><fa :icon="faPaintBrush"/> {{ $t('_pages.blocks.canvas') }}</template>
 | 
			
		||||
<XContainer @remove="() => $emit('remove')" :draggable="true">
 | 
			
		||||
	<template #header><Fa :icon="faPaintBrush"/> {{ $t('_pages.blocks.canvas') }}</template>
 | 
			
		||||
 | 
			
		||||
	<section style="padding: 0 16px 0 16px;">
 | 
			
		||||
		<mk-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('_pages.blocks._canvas.id') }}</span></mk-input>
 | 
			
		||||
		<mk-input v-model="value.width" type="number"><span>{{ $t('_pages.blocks._canvas.width') }}</span><template #suffix>px</template></mk-input>
 | 
			
		||||
		<mk-input v-model="value.height" type="number"><span>{{ $t('_pages.blocks._canvas.height') }}</span><template #suffix>px</template></mk-input>
 | 
			
		||||
		<MkInput v-model:value="value.name"><template #prefix><Fa :icon="faMagic"/></template><span>{{ $t('_pages.blocks._canvas.id') }}</span></MkInput>
 | 
			
		||||
		<MkInput v-model:value="value.width" type="number"><span>{{ $t('_pages.blocks._canvas.width') }}</span><template #suffix>px</template></MkInput>
 | 
			
		||||
		<MkInput v-model:value="value.height" type="number"><span>{{ $t('_pages.blocks._canvas.height') }}</span><template #suffix>px</template></MkInput>
 | 
			
		||||
	</section>
 | 
			
		||||
</x-container>
 | 
			
		||||
</XContainer>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faPaintBrush, faMagic } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import XContainer from '../page-editor.container.vue';
 | 
			
		||||
import MkInput from '../../../components/ui/input.vue';
 | 
			
		||||
import MkInput from '@/components/ui/input.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XContainer, MkInput
 | 
			
		||||
	},
 | 
			
		||||
@@ -34,9 +35,9 @@ export default Vue.extend({
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		if (this.value.name == null) Vue.set(this.value, 'name', '');
 | 
			
		||||
		if (this.value.width == null) Vue.set(this.value, 'width', 300);
 | 
			
		||||
		if (this.value.height == null) Vue.set(this.value, 'height', 200);
 | 
			
		||||
		if (this.value.name == null) this.value.name = '';
 | 
			
		||||
		if (this.value.width == null) this.value.width = 300;
 | 
			
		||||
		if (this.value.height == null) this.value.height = 200;
 | 
			
		||||
	},
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,22 +1,23 @@
 | 
			
		||||
<template>
 | 
			
		||||
<x-container @remove="() => $emit('remove')" :draggable="true">
 | 
			
		||||
	<template #header><fa :icon="faBolt"/> {{ $t('_pages.blocks.counter') }}</template>
 | 
			
		||||
<XContainer @remove="() => $emit('remove')" :draggable="true">
 | 
			
		||||
	<template #header><Fa :icon="faBolt"/> {{ $t('_pages.blocks.counter') }}</template>
 | 
			
		||||
 | 
			
		||||
	<section style="padding: 0 16px 0 16px;">
 | 
			
		||||
		<mk-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('_pages.blocks._counter.name') }}</span></mk-input>
 | 
			
		||||
		<mk-input v-model="value.text"><span>{{ $t('_pages.blocks._counter.text') }}</span></mk-input>
 | 
			
		||||
		<mk-input v-model="value.inc" type="number"><span>{{ $t('_pages.blocks._counter.inc') }}</span></mk-input>
 | 
			
		||||
		<MkInput v-model:value="value.name"><template #prefix><Fa :icon="faMagic"/></template><span>{{ $t('_pages.blocks._counter.name') }}</span></MkInput>
 | 
			
		||||
		<MkInput v-model:value="value.text"><span>{{ $t('_pages.blocks._counter.text') }}</span></MkInput>
 | 
			
		||||
		<MkInput v-model:value="value.inc" type="number"><span>{{ $t('_pages.blocks._counter.inc') }}</span></MkInput>
 | 
			
		||||
	</section>
 | 
			
		||||
</x-container>
 | 
			
		||||
</XContainer>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import XContainer from '../page-editor.container.vue';
 | 
			
		||||
import MkInput from '../../../components/ui/input.vue';
 | 
			
		||||
import MkInput from '@/components/ui/input.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XContainer, MkInput
 | 
			
		||||
	},
 | 
			
		||||
@@ -34,7 +35,7 @@ export default Vue.extend({
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		if (this.value.name == null) Vue.set(this.value, 'name', '');
 | 
			
		||||
		if (this.value.name == null) this.value.name = '';
 | 
			
		||||
	},
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,14 @@
 | 
			
		||||
<template>
 | 
			
		||||
<x-container @remove="() => $emit('remove')" :draggable="true">
 | 
			
		||||
	<template #header><fa :icon="faQuestion"/> {{ $t('_pages.blocks.if') }}</template>
 | 
			
		||||
<XContainer @remove="() => $emit('remove')" :draggable="true">
 | 
			
		||||
	<template #header><Fa :icon="faQuestion"/> {{ $t('_pages.blocks.if') }}</template>
 | 
			
		||||
	<template #func>
 | 
			
		||||
		<button @click="add()" class="_button">
 | 
			
		||||
			<fa :icon="faPlus"/>
 | 
			
		||||
			<Fa :icon="faPlus"/>
 | 
			
		||||
		</button>
 | 
			
		||||
	</template>
 | 
			
		||||
 | 
			
		||||
	<section class="romcojzs">
 | 
			
		||||
		<mk-select v-model="value.var">
 | 
			
		||||
		<MkSelect v-model:value="value.var">
 | 
			
		||||
			<template #label>{{ $t('_pages.blocks._if.variable') }}</template>
 | 
			
		||||
			<option v-for="v in hpml.getVarsByType('boolean')" :value="v.name">{{ v.name }}</option>
 | 
			
		||||
			<optgroup :label="$t('_pages.script.pageVariables')">
 | 
			
		||||
@@ -17,21 +17,22 @@
 | 
			
		||||
			<optgroup :label="$t('_pages.script.enviromentVariables')">
 | 
			
		||||
				<option v-for="v in hpml.getEnvVarsByType('boolean')" :value="v">{{ v }}</option>
 | 
			
		||||
			</optgroup>
 | 
			
		||||
		</mk-select>
 | 
			
		||||
		</MkSelect>
 | 
			
		||||
 | 
			
		||||
		<x-blocks class="children" v-model="value.children" :hpml="hpml"/>
 | 
			
		||||
		<XBlocks class="children" v-model:value="value.children" :hpml="hpml"/>
 | 
			
		||||
	</section>
 | 
			
		||||
</x-container>
 | 
			
		||||
</XContainer>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { v4 as uuid } from 'uuid';
 | 
			
		||||
import { faPlus, faQuestion } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import XContainer from '../page-editor.container.vue';
 | 
			
		||||
import MkSelect from '../../../components/ui/select.vue';
 | 
			
		||||
import MkSelect from '@/components/ui/select.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XContainer, MkSelect
 | 
			
		||||
	},
 | 
			
		||||
@@ -58,13 +59,13 @@ export default Vue.extend({
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		if (this.value.children == null) Vue.set(this.value, 'children', []);
 | 
			
		||||
		if (this.value.var === undefined) Vue.set(this.value, 'var', null);
 | 
			
		||||
		if (this.value.children == null) this.value.children = [];
 | 
			
		||||
		if (this.value.var === undefined) this.value.var = null;
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		async add() {
 | 
			
		||||
			const { canceled, result: type } = await this.$root.dialog({
 | 
			
		||||
			const { canceled, result: type } = await os.dialog({
 | 
			
		||||
				type: null,
 | 
			
		||||
				title: this.$t('_pages.chooseBlock'),
 | 
			
		||||
				select: {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,29 +1,29 @@
 | 
			
		||||
<template>
 | 
			
		||||
<x-container @remove="() => $emit('remove')" :draggable="true">
 | 
			
		||||
	<template #header><fa :icon="faImage"/> {{ $t('_pages.blocks.image') }}</template>
 | 
			
		||||
<XContainer @remove="() => $emit('remove')" :draggable="true">
 | 
			
		||||
	<template #header><Fa :icon="faImage"/> {{ $t('_pages.blocks.image') }}</template>
 | 
			
		||||
	<template #func>
 | 
			
		||||
		<button @click="choose()">
 | 
			
		||||
			<fa :icon="faFolderOpen"/>
 | 
			
		||||
			<Fa :icon="faFolderOpen"/>
 | 
			
		||||
		</button>
 | 
			
		||||
	</template>
 | 
			
		||||
 | 
			
		||||
	<section class="oyyftmcf">
 | 
			
		||||
		<mk-file-thumbnail class="preview" v-if="file" :file="file" fit="contain" @click="choose()"/>
 | 
			
		||||
		<MkDriveFileThumbnail class="preview" v-if="file" :file="file" fit="contain" @click="choose()"/>
 | 
			
		||||
	</section>
 | 
			
		||||
</x-container>
 | 
			
		||||
</XContainer>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faImage, faFolderOpen } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import XContainer from '../page-editor.container.vue';
 | 
			
		||||
import MkFileThumbnail from '../../../components/drive-file-thumbnail.vue';
 | 
			
		||||
import { selectDriveFile } from '../../../scripts/select-drive-file';
 | 
			
		||||
import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XContainer, MkFileThumbnail
 | 
			
		||||
		XContainer, MkDriveFileThumbnail
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	props: {
 | 
			
		||||
@@ -40,14 +40,14 @@ export default Vue.extend({
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		if (this.value.fileId === undefined) Vue.set(this.value, 'fileId', null);
 | 
			
		||||
		if (this.value.fileId === undefined) this.value.fileId = null;
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	mounted() {
 | 
			
		||||
		if (this.value.fileId == null) {
 | 
			
		||||
			this.choose();
 | 
			
		||||
		} else {
 | 
			
		||||
			this.$root.api('drive/files/show', {
 | 
			
		||||
			os.api('drive/files/show', {
 | 
			
		||||
				fileId: this.value.fileId
 | 
			
		||||
			}).then(file => {
 | 
			
		||||
				this.file = file;
 | 
			
		||||
@@ -57,7 +57,7 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		async choose() {
 | 
			
		||||
			selectDriveFile(this.$root, false).then(file => {
 | 
			
		||||
			os.selectDriveFile(false).then(file => {
 | 
			
		||||
				this.file = file;
 | 
			
		||||
				this.value.fileId = file.id;
 | 
			
		||||
			});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,22 +1,23 @@
 | 
			
		||||
<template>
 | 
			
		||||
<x-container @remove="() => $emit('remove')" :draggable="true">
 | 
			
		||||
	<template #header><fa :icon="faBolt"/> {{ $t('_pages.blocks.numberInput') }}</template>
 | 
			
		||||
<XContainer @remove="() => $emit('remove')" :draggable="true">
 | 
			
		||||
	<template #header><Fa :icon="faBolt"/> {{ $t('_pages.blocks.numberInput') }}</template>
 | 
			
		||||
 | 
			
		||||
	<section style="padding: 0 16px 0 16px;">
 | 
			
		||||
		<mk-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('_pages.blocks._numberInput.name') }}</span></mk-input>
 | 
			
		||||
		<mk-input v-model="value.text"><span>{{ $t('_pages.blocks._numberInput.text') }}</span></mk-input>
 | 
			
		||||
		<mk-input v-model="value.default" type="number"><span>{{ $t('_pages.blocks._numberInput.default') }}</span></mk-input>
 | 
			
		||||
		<MkInput v-model:value="value.name"><template #prefix><Fa :icon="faMagic"/></template><span>{{ $t('_pages.blocks._numberInput.name') }}</span></MkInput>
 | 
			
		||||
		<MkInput v-model:value="value.text"><span>{{ $t('_pages.blocks._numberInput.text') }}</span></MkInput>
 | 
			
		||||
		<MkInput v-model:value="value.default" type="number"><span>{{ $t('_pages.blocks._numberInput.default') }}</span></MkInput>
 | 
			
		||||
	</section>
 | 
			
		||||
</x-container>
 | 
			
		||||
</XContainer>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import XContainer from '../page-editor.container.vue';
 | 
			
		||||
import MkInput from '../../../components/ui/input.vue';
 | 
			
		||||
import MkInput from '@/components/ui/input.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XContainer, MkInput
 | 
			
		||||
	},
 | 
			
		||||
@@ -34,7 +35,7 @@ export default Vue.extend({
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		if (this.value.name == null) Vue.set(this.value, 'name', '');
 | 
			
		||||
		if (this.value.name == null) this.value.name = '';
 | 
			
		||||
	},
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,24 +1,25 @@
 | 
			
		||||
<template>
 | 
			
		||||
<x-container @remove="() => $emit('remove')" :draggable="true">
 | 
			
		||||
	<template #header><fa :icon="faPaperPlane"/> {{ $t('_pages.blocks.post') }}</template>
 | 
			
		||||
<XContainer @remove="() => $emit('remove')" :draggable="true">
 | 
			
		||||
	<template #header><Fa :icon="faPaperPlane"/> {{ $t('_pages.blocks.post') }}</template>
 | 
			
		||||
 | 
			
		||||
	<section style="padding: 16px;">
 | 
			
		||||
		<mk-textarea v-model="value.text">{{ $t('_pages.blocks._post.text') }}</mk-textarea>
 | 
			
		||||
		<mk-switch v-model="value.attachCanvasImage"><span>{{ $t('_pages.blocks._post.attachCanvasImage') }}</span></mk-switch>
 | 
			
		||||
		<mk-input v-if="value.attachCanvasImage" v-model="value.canvasId"><span>{{ $t('_pages.blocks._post.canvasId') }}</span></mk-input>
 | 
			
		||||
		<MkTextarea v-model:value="value.text">{{ $t('_pages.blocks._post.text') }}</MkTextarea>
 | 
			
		||||
		<MkSwitch v-model:value="value.attachCanvasImage"><span>{{ $t('_pages.blocks._post.attachCanvasImage') }}</span></MkSwitch>
 | 
			
		||||
		<MkInput v-if="value.attachCanvasImage" v-model:value="value.canvasId"><span>{{ $t('_pages.blocks._post.canvasId') }}</span></MkInput>
 | 
			
		||||
	</section>
 | 
			
		||||
</x-container>
 | 
			
		||||
</XContainer>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faPaperPlane } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import XContainer from '../page-editor.container.vue';
 | 
			
		||||
import MkTextarea from '../../../components/ui/textarea.vue';
 | 
			
		||||
import MkInput from '../../../components/ui/input.vue';
 | 
			
		||||
import MkSwitch from '../../../components/ui/switch.vue';
 | 
			
		||||
import MkTextarea from '@/components/ui/textarea.vue';
 | 
			
		||||
import MkInput from '@/components/ui/input.vue';
 | 
			
		||||
import MkSwitch from '@/components/ui/switch.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XContainer, MkTextarea, MkInput, MkSwitch
 | 
			
		||||
	},
 | 
			
		||||
@@ -36,9 +37,9 @@ export default Vue.extend({
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		if (this.value.text == null) Vue.set(this.value, 'text', '');
 | 
			
		||||
		if (this.value.attachCanvasImage == null) Vue.set(this.value, 'attachCanvasImage', false);
 | 
			
		||||
		if (this.value.canvasId == null) Vue.set(this.value, 'canvasId', '');
 | 
			
		||||
		if (this.value.text == null) this.value.text = '';
 | 
			
		||||
		if (this.value.attachCanvasImage == null) this.value.attachCanvasImage = false;
 | 
			
		||||
		if (this.value.canvasId == null) this.value.canvasId = '';
 | 
			
		||||
	},
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,24 +1,25 @@
 | 
			
		||||
<template>
 | 
			
		||||
<x-container @remove="() => $emit('remove')" :draggable="true">
 | 
			
		||||
	<template #header><fa :icon="faBolt"/> {{ $t('_pages.blocks.radioButton') }}</template>
 | 
			
		||||
<XContainer @remove="() => $emit('remove')" :draggable="true">
 | 
			
		||||
	<template #header><Fa :icon="faBolt"/> {{ $t('_pages.blocks.radioButton') }}</template>
 | 
			
		||||
 | 
			
		||||
	<section style="padding: 0 16px 16px 16px;">
 | 
			
		||||
		<mk-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('_pages.blocks._radioButton.name') }}</span></mk-input>
 | 
			
		||||
		<mk-input v-model="value.title"><span>{{ $t('_pages.blocks._radioButton.title') }}</span></mk-input>
 | 
			
		||||
		<mk-textarea v-model="values"><span>{{ $t('_pages.blocks._radioButton.values') }}</span></mk-textarea>
 | 
			
		||||
		<mk-input v-model="value.default"><span>{{ $t('_pages.blocks._radioButton.default') }}</span></mk-input>
 | 
			
		||||
		<MkInput v-model:value="value.name"><template #prefix><Fa :icon="faMagic"/></template><span>{{ $t('_pages.blocks._radioButton.name') }}</span></MkInput>
 | 
			
		||||
		<MkInput v-model:value="value.title"><span>{{ $t('_pages.blocks._radioButton.title') }}</span></MkInput>
 | 
			
		||||
		<MkTextarea v-model:value="values"><span>{{ $t('_pages.blocks._radioButton.values') }}</span></MkTextarea>
 | 
			
		||||
		<MkInput v-model:value="value.default"><span>{{ $t('_pages.blocks._radioButton.default') }}</span></MkInput>
 | 
			
		||||
	</section>
 | 
			
		||||
</x-container>
 | 
			
		||||
</XContainer>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import XContainer from '../page-editor.container.vue';
 | 
			
		||||
import MkTextarea from '../../../components/ui/textarea.vue';
 | 
			
		||||
import MkInput from '../../../components/ui/input.vue';
 | 
			
		||||
import MkTextarea from '@/components/ui/textarea.vue';
 | 
			
		||||
import MkInput from '@/components/ui/input.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XContainer, MkTextarea, MkInput
 | 
			
		||||
	},
 | 
			
		||||
@@ -34,14 +35,17 @@ export default Vue.extend({
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
	watch: {
 | 
			
		||||
		values() {
 | 
			
		||||
			Vue.set(this.value, 'values', this.values.split('\n'));
 | 
			
		||||
		values: {
 | 
			
		||||
			handler() {
 | 
			
		||||
				this.value.values = this.values.split('\n');
 | 
			
		||||
			},
 | 
			
		||||
			deep: true
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	created() {
 | 
			
		||||
		if (this.value.name == null) Vue.set(this.value, 'name', '');
 | 
			
		||||
		if (this.value.title == null) Vue.set(this.value, 'title', '');
 | 
			
		||||
		if (this.value.values == null) Vue.set(this.value, 'values', []);
 | 
			
		||||
		if (this.value.name == null) this.value.name = '';
 | 
			
		||||
		if (this.value.title == null) this.value.title = '';
 | 
			
		||||
		if (this.value.values == null) this.value.values = [];
 | 
			
		||||
		this.values = this.value.values.join('\n');
 | 
			
		||||
	},
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,29 +1,30 @@
 | 
			
		||||
<template>
 | 
			
		||||
<x-container @remove="() => $emit('remove')" :draggable="true">
 | 
			
		||||
	<template #header><fa :icon="faStickyNote"/> {{ value.title }}</template>
 | 
			
		||||
<XContainer @remove="() => $emit('remove')" :draggable="true">
 | 
			
		||||
	<template #header><Fa :icon="faStickyNote"/> {{ value.title }}</template>
 | 
			
		||||
	<template #func>
 | 
			
		||||
		<button @click="rename()" class="_button">
 | 
			
		||||
			<fa :icon="faPencilAlt"/>
 | 
			
		||||
			<Fa :icon="faPencilAlt"/>
 | 
			
		||||
		</button>
 | 
			
		||||
		<button @click="add()" class="_button">
 | 
			
		||||
			<fa :icon="faPlus"/>
 | 
			
		||||
			<Fa :icon="faPlus"/>
 | 
			
		||||
		</button>
 | 
			
		||||
	</template>
 | 
			
		||||
 | 
			
		||||
	<section class="ilrvjyvi">
 | 
			
		||||
		<x-blocks class="children" v-model="value.children" :hpml="hpml"/>
 | 
			
		||||
		<XBlocks class="children" v-model:value="value.children" :hpml="hpml"/>
 | 
			
		||||
	</section>
 | 
			
		||||
</x-container>
 | 
			
		||||
</XContainer>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { v4 as uuid } from 'uuid';
 | 
			
		||||
import { faPlus, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faStickyNote } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import XContainer from '../page-editor.container.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XContainer
 | 
			
		||||
	},
 | 
			
		||||
@@ -50,8 +51,8 @@ export default Vue.extend({
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		if (this.value.title == null) Vue.set(this.value, 'title', null);
 | 
			
		||||
		if (this.value.children == null) Vue.set(this.value, 'children', []);
 | 
			
		||||
		if (this.value.title == null) this.value.title = null;
 | 
			
		||||
		if (this.value.children == null) this.value.children = [];
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	mounted() {
 | 
			
		||||
@@ -62,7 +63,7 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		async rename() {
 | 
			
		||||
			const { canceled, result: title } = await this.$root.dialog({
 | 
			
		||||
			const { canceled, result: title } = await os.dialog({
 | 
			
		||||
				title: 'Enter title',
 | 
			
		||||
				input: {
 | 
			
		||||
					type: 'text',
 | 
			
		||||
@@ -75,7 +76,7 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async add() {
 | 
			
		||||
			const { canceled, result: type } = await this.$root.dialog({
 | 
			
		||||
			const { canceled, result: type } = await os.dialog({
 | 
			
		||||
				type: null,
 | 
			
		||||
				title: this.$t('_pages.chooseBlock'),
 | 
			
		||||
				select: {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,23 +1,24 @@
 | 
			
		||||
<template>
 | 
			
		||||
<x-container @remove="() => $emit('remove')" :draggable="true">
 | 
			
		||||
	<template #header><fa :icon="faBolt"/> {{ $t('_pages.blocks.switch') }}</template>
 | 
			
		||||
<XContainer @remove="() => $emit('remove')" :draggable="true">
 | 
			
		||||
	<template #header><Fa :icon="faBolt"/> {{ $t('_pages.blocks.switch') }}</template>
 | 
			
		||||
 | 
			
		||||
	<section class="kjuadyyj">
 | 
			
		||||
		<mk-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('_pages.blocks._switch.name') }}</span></mk-input>
 | 
			
		||||
		<mk-input v-model="value.text"><span>{{ $t('_pages.blocks._switch.text') }}</span></mk-input>
 | 
			
		||||
		<mk-switch v-model="value.default"><span>{{ $t('_pages.blocks._switch.default') }}</span></mk-switch>
 | 
			
		||||
		<MkInput v-model:value="value.name"><template #prefix><Fa :icon="faMagic"/></template><span>{{ $t('_pages.blocks._switch.name') }}</span></MkInput>
 | 
			
		||||
		<MkInput v-model:value="value.text"><span>{{ $t('_pages.blocks._switch.text') }}</span></MkInput>
 | 
			
		||||
		<MkSwitch v-model:value="value.default"><span>{{ $t('_pages.blocks._switch.default') }}</span></MkSwitch>
 | 
			
		||||
	</section>
 | 
			
		||||
</x-container>
 | 
			
		||||
</XContainer>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import XContainer from '../page-editor.container.vue';
 | 
			
		||||
import MkSwitch from '../../../components/ui/switch.vue';
 | 
			
		||||
import MkInput from '../../../components/ui/input.vue';
 | 
			
		||||
import MkSwitch from '@/components/ui/switch.vue';
 | 
			
		||||
import MkInput from '@/components/ui/input.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XContainer, MkSwitch, MkInput
 | 
			
		||||
	},
 | 
			
		||||
@@ -35,7 +36,7 @@ export default Vue.extend({
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		if (this.value.name == null) Vue.set(this.value, 'name', '');
 | 
			
		||||
		if (this.value.name == null) this.value.name = '';
 | 
			
		||||
	},
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,22 +1,23 @@
 | 
			
		||||
<template>
 | 
			
		||||
<x-container @remove="() => $emit('remove')" :draggable="true">
 | 
			
		||||
	<template #header><fa :icon="faBolt"/> {{ $t('_pages.blocks.textInput') }}</template>
 | 
			
		||||
<XContainer @remove="() => $emit('remove')" :draggable="true">
 | 
			
		||||
	<template #header><Fa :icon="faBolt"/> {{ $t('_pages.blocks.textInput') }}</template>
 | 
			
		||||
 | 
			
		||||
	<section style="padding: 0 16px 0 16px;">
 | 
			
		||||
		<mk-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('_pages.blocks._textInput.name') }}</span></mk-input>
 | 
			
		||||
		<mk-input v-model="value.text"><span>{{ $t('_pages.blocks._textInput.text') }}</span></mk-input>
 | 
			
		||||
		<mk-input v-model="value.default" type="text"><span>{{ $t('_pages.blocks._textInput.default') }}</span></mk-input>
 | 
			
		||||
		<MkInput v-model:value="value.name"><template #prefix><Fa :icon="faMagic"/></template><span>{{ $t('_pages.blocks._textInput.name') }}</span></MkInput>
 | 
			
		||||
		<MkInput v-model:value="value.text"><span>{{ $t('_pages.blocks._textInput.text') }}</span></MkInput>
 | 
			
		||||
		<MkInput v-model:value="value.default" type="text"><span>{{ $t('_pages.blocks._textInput.default') }}</span></MkInput>
 | 
			
		||||
	</section>
 | 
			
		||||
</x-container>
 | 
			
		||||
</XContainer>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import XContainer from '../page-editor.container.vue';
 | 
			
		||||
import MkInput from '../../../components/ui/input.vue';
 | 
			
		||||
import MkInput from '@/components/ui/input.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XContainer, MkInput
 | 
			
		||||
	},
 | 
			
		||||
@@ -34,7 +35,7 @@ export default Vue.extend({
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		if (this.value.name == null) Vue.set(this.value, 'name', '');
 | 
			
		||||
		if (this.value.name == null) this.value.name = '';
 | 
			
		||||
	},
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,20 @@
 | 
			
		||||
<template>
 | 
			
		||||
<x-container @remove="() => $emit('remove')" :draggable="true">
 | 
			
		||||
	<template #header><fa :icon="faAlignLeft"/> {{ $t('_pages.blocks.text') }}</template>
 | 
			
		||||
<XContainer @remove="() => $emit('remove')" :draggable="true">
 | 
			
		||||
	<template #header><Fa :icon="faAlignLeft"/> {{ $t('_pages.blocks.text') }}</template>
 | 
			
		||||
 | 
			
		||||
	<section class="vckmsadr">
 | 
			
		||||
		<textarea v-model="value.text"></textarea>
 | 
			
		||||
	</section>
 | 
			
		||||
</x-container>
 | 
			
		||||
</XContainer>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faAlignLeft } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import XContainer from '../page-editor.container.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XContainer
 | 
			
		||||
	},
 | 
			
		||||
@@ -31,7 +32,7 @@ export default Vue.extend({
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		if (this.value.text == null) Vue.set(this.value, 'text', '');
 | 
			
		||||
		if (this.value.text == null) this.value.text = '';
 | 
			
		||||
	},
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,23 +1,24 @@
 | 
			
		||||
<template>
 | 
			
		||||
<x-container @remove="() => $emit('remove')" :draggable="true">
 | 
			
		||||
	<template #header><fa :icon="faBolt"/> {{ $t('_pages.blocks.textareaInput') }}</template>
 | 
			
		||||
<XContainer @remove="() => $emit('remove')" :draggable="true">
 | 
			
		||||
	<template #header><Fa :icon="faBolt"/> {{ $t('_pages.blocks.textareaInput') }}</template>
 | 
			
		||||
 | 
			
		||||
	<section style="padding: 0 16px 16px 16px;">
 | 
			
		||||
		<mk-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('_pages.blocks._textareaInput.name') }}</span></mk-input>
 | 
			
		||||
		<mk-input v-model="value.text"><span>{{ $t('_pages.blocks._textareaInput.text') }}</span></mk-input>
 | 
			
		||||
		<mk-textarea v-model="value.default"><span>{{ $t('_pages.blocks._textareaInput.default') }}</span></mk-textarea>
 | 
			
		||||
		<MkInput v-model:value="value.name"><template #prefix><Fa :icon="faMagic"/></template><span>{{ $t('_pages.blocks._textareaInput.name') }}</span></MkInput>
 | 
			
		||||
		<MkInput v-model:value="value.text"><span>{{ $t('_pages.blocks._textareaInput.text') }}</span></MkInput>
 | 
			
		||||
		<MkTextarea v-model:value="value.default"><span>{{ $t('_pages.blocks._textareaInput.default') }}</span></MkTextarea>
 | 
			
		||||
	</section>
 | 
			
		||||
</x-container>
 | 
			
		||||
</XContainer>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import XContainer from '../page-editor.container.vue';
 | 
			
		||||
import MkTextarea from '../../../components/ui/textarea.vue';
 | 
			
		||||
import MkInput from '../../../components/ui/input.vue';
 | 
			
		||||
import MkTextarea from '@/components/ui/textarea.vue';
 | 
			
		||||
import MkInput from '@/components/ui/input.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XContainer, MkTextarea, MkInput
 | 
			
		||||
	},
 | 
			
		||||
@@ -35,7 +36,7 @@ export default Vue.extend({
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		if (this.value.name == null) Vue.set(this.value, 'name', '');
 | 
			
		||||
		if (this.value.name == null) this.value.name = '';
 | 
			
		||||
	},
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,20 @@
 | 
			
		||||
<template>
 | 
			
		||||
<x-container @remove="() => $emit('remove')" :draggable="true">
 | 
			
		||||
	<template #header><fa :icon="faAlignLeft"/> {{ $t('_pages.blocks.textarea') }}</template>
 | 
			
		||||
<XContainer @remove="() => $emit('remove')" :draggable="true">
 | 
			
		||||
	<template #header><Fa :icon="faAlignLeft"/> {{ $t('_pages.blocks.textarea') }}</template>
 | 
			
		||||
 | 
			
		||||
	<section class="ihymsbbe">
 | 
			
		||||
		<textarea v-model="value.text"></textarea>
 | 
			
		||||
	</section>
 | 
			
		||||
</x-container>
 | 
			
		||||
</XContainer>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faAlignLeft } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import XContainer from '../page-editor.container.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XContainer
 | 
			
		||||
	},
 | 
			
		||||
@@ -31,7 +32,7 @@ export default Vue.extend({
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		if (this.value.text == null) Vue.set(this.value, 'text', '');
 | 
			
		||||
		if (this.value.text == null) this.value.text = '';
 | 
			
		||||
	},
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,11 @@
 | 
			
		||||
<template>
 | 
			
		||||
<x-draggable tag="div" :list="blocks" handle=".drag-handle" :group="{ name: 'blocks' }" animation="150" swap-threshold="0.5">
 | 
			
		||||
	<component v-for="block in blocks" :is="'x-' + block.type" :value="block" @input="updateItem" @remove="() => removeItem(block)" :key="block.id" :hpml="hpml"/>
 | 
			
		||||
</x-draggable>
 | 
			
		||||
<XDraggable tag="div" :list="blocks" handle=".drag-handle" :group="{ name: 'blocks' }" animation="150" swap-threshold="0.5">
 | 
			
		||||
	<component v-for="block in blocks" :is="'x-' + block.type" :value="block" @update:value="updateItem" @remove="() => removeItem(block)" :key="block.id" :hpml="hpml"/>
 | 
			
		||||
</XDraggable>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import * as XDraggable from 'vuedraggable';
 | 
			
		||||
import { defineComponent, defineAsyncComponent } from 'vue';
 | 
			
		||||
import XSection from './els/page-editor.el.section.vue';
 | 
			
		||||
import XText from './els/page-editor.el.text.vue';
 | 
			
		||||
import XTextarea from './els/page-editor.el.textarea.vue';
 | 
			
		||||
@@ -21,10 +20,12 @@ import XPost from './els/page-editor.el.post.vue';
 | 
			
		||||
import XCounter from './els/page-editor.el.counter.vue';
 | 
			
		||||
import XRadioButton from './els/page-editor.el.radio-button.vue';
 | 
			
		||||
import XCanvas from './els/page-editor.el.canvas.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XDraggable, XSection, XText, XImage, XButton, XTextarea, XTextInput, XTextareaInput, XNumberInput, XSwitch, XIf, XPost, XCounter, XRadioButton, XCanvas
 | 
			
		||||
		XDraggable: defineAsyncComponent(() => import('vue-draggable-next').then(x => x.VueDraggableNext)),
 | 
			
		||||
		XSection, XText, XImage, XButton, XTextarea, XTextInput, XTextareaInput, XNumberInput, XSwitch, XIf, XPost, XCounter, XRadioButton, XCanvas
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	props: {
 | 
			
		||||
@@ -51,7 +52,7 @@ export default Vue.extend({
 | 
			
		||||
				v,
 | 
			
		||||
				...this.blocks.slice(i + 1)
 | 
			
		||||
			];
 | 
			
		||||
			this.$emit('input', newValue);
 | 
			
		||||
			this.$emit('update:value', newValue);
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		removeItem(el) {
 | 
			
		||||
@@ -60,7 +61,7 @@ export default Vue.extend({
 | 
			
		||||
				...this.blocks.slice(0, i),
 | 
			
		||||
				...this.blocks.slice(i + 1)
 | 
			
		||||
			];
 | 
			
		||||
			this.$emit('input', newValue);
 | 
			
		||||
			this.$emit('update:value', newValue);
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -5,14 +5,14 @@
 | 
			
		||||
		<div class="buttons">
 | 
			
		||||
			<slot name="func"></slot>
 | 
			
		||||
			<button v-if="removable" @click="remove()" class="_button">
 | 
			
		||||
				<fa :icon="faTrashAlt"/>
 | 
			
		||||
				<Fa :icon="faTrashAlt"/>
 | 
			
		||||
			</button>
 | 
			
		||||
			<button v-if="draggable" class="drag-handle _button">
 | 
			
		||||
				<fa :icon="faBars"/>
 | 
			
		||||
				<Fa :icon="faBars"/>
 | 
			
		||||
			</button>
 | 
			
		||||
			<button @click="toggleContent(!showBody)" class="_button">
 | 
			
		||||
				<template v-if="showBody"><fa :icon="faAngleUp"/></template>
 | 
			
		||||
				<template v-else><fa :icon="faAngleDown"/></template>
 | 
			
		||||
				<template v-if="showBody"><Fa :icon="faAngleUp"/></template>
 | 
			
		||||
				<template v-else><Fa :icon="faAngleDown"/></template>
 | 
			
		||||
			</button>
 | 
			
		||||
		</div>
 | 
			
		||||
	</header>
 | 
			
		||||
@@ -25,11 +25,12 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faBars, faAngleUp, faAngleDown } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	props: {
 | 
			
		||||
		expanded: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
@@ -147,7 +148,7 @@ export default Vue.extend({
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .body {
 | 
			
		||||
		::v-deep .juejbjww, ::v-deep .eiipwacr {
 | 
			
		||||
		::v-deep(.juejbjww), ::v-deep(.eiipwacr) {
 | 
			
		||||
			&:not(.inline):first-child {
 | 
			
		||||
				margin-top: 28px;
 | 
			
		||||
			}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
<template>
 | 
			
		||||
<x-container :removable="removable" @remove="() => $emit('remove')" :error="error" :warn="warn" :draggable="draggable">
 | 
			
		||||
	<template #header><fa v-if="icon" :icon="icon"/> <template v-if="title">{{ title }} <span class="turmquns" v-if="typeText">({{ typeText }})</span></template><template v-else-if="typeText">{{ typeText }}</template></template>
 | 
			
		||||
<XContainer :removable="removable" @remove="() => $emit('remove')" :error="error" :warn="warn" :draggable="draggable">
 | 
			
		||||
	<template #header><Fa v-if="icon" :icon="icon"/> <template v-if="title">{{ title }} <span class="turmquns" v-if="typeText">({{ typeText }})</span></template><template v-else-if="typeText">{{ typeText }}</template></template>
 | 
			
		||||
	<template #func>
 | 
			
		||||
		<button @click="changeType()" class="_button">
 | 
			
		||||
			<fa :icon="faPencilAlt"/>
 | 
			
		||||
			<Fa :icon="faPencilAlt"/>
 | 
			
		||||
		</button>
 | 
			
		||||
	</template>
 | 
			
		||||
 | 
			
		||||
@@ -40,30 +40,31 @@
 | 
			
		||||
		<input v-model="value.value"/>
 | 
			
		||||
	</section>
 | 
			
		||||
	<section v-else-if="value.type === 'fn'" class="" style="padding:0 16px 16px 16px;">
 | 
			
		||||
		<mk-textarea v-model="slots">
 | 
			
		||||
		<MkTextarea v-model:value="slots">
 | 
			
		||||
			<span>{{ $t('_pages.script.blocks._fn.slots') }}</span>
 | 
			
		||||
			<template #desc>{{ $t('_pages.script.blocks._fn.slots-info') }}</template>
 | 
			
		||||
		</mk-textarea>
 | 
			
		||||
		<x-v v-if="value.value.expression" v-model="value.value.expression" :title="$t(`_pages.script.blocks._fn.arg1`)" :get-expected-type="() => null" :hpml="hpml" :fn-slots="value.value.slots" :name="name"/>
 | 
			
		||||
		</MkTextarea>
 | 
			
		||||
		<XV v-if="value.value.expression" v-model:value="value.value.expression" :title="$t(`_pages.script.blocks._fn.arg1`)" :get-expected-type="() => null" :hpml="hpml" :fn-slots="value.value.slots" :name="name"/>
 | 
			
		||||
	</section>
 | 
			
		||||
	<section v-else-if="value.type.startsWith('fn:')" class="" style="padding:16px;">
 | 
			
		||||
		<x-v v-for="(x, i) in value.args" v-model="value.args[i]" :title="hpml.getVarByName(value.type.split(':')[1]).value.slots[i].name" :get-expected-type="() => null" :hpml="hpml" :name="name" :key="i"/>
 | 
			
		||||
		<XV v-for="(x, i) in value.args" v-model:value="value.args[i]" :title="hpml.getVarByName(value.type.split(':')[1]).value.slots[i].name" :get-expected-type="() => null" :hpml="hpml" :name="name" :key="i"/>
 | 
			
		||||
	</section>
 | 
			
		||||
	<section v-else class="" style="padding:16px;">
 | 
			
		||||
		<x-v v-for="(x, i) in value.args" v-model="value.args[i]" :title="$t(`_pages.script.blocks._${value.type}.arg${i + 1}`)" :get-expected-type="() => _getExpectedType(i)" :hpml="hpml" :name="name" :fn-slots="fnSlots" :key="i"/>
 | 
			
		||||
		<XV v-for="(x, i) in value.args" v-model:value="value.args[i]" :title="$t(`_pages.script.blocks._${value.type}.arg${i + 1}`)" :get-expected-type="() => _getExpectedType(i)" :hpml="hpml" :name="name" :fn-slots="fnSlots" :key="i"/>
 | 
			
		||||
	</section>
 | 
			
		||||
</x-container>
 | 
			
		||||
</XContainer>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faPencilAlt, faPlug } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { v4 as uuid } from 'uuid';
 | 
			
		||||
import XContainer from './page-editor.container.vue';
 | 
			
		||||
import MkTextarea from '../../components/ui/textarea.vue';
 | 
			
		||||
import { isLiteralBlock, funcDefs, blockDefs } from '../../scripts/hpml/index';
 | 
			
		||||
import MkTextarea from '@/components/ui/textarea.vue';
 | 
			
		||||
import { isLiteralBlock, funcDefs, blockDefs } from '@/scripts/hpml/index';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XContainer, MkTextarea
 | 
			
		||||
	},
 | 
			
		||||
@@ -123,11 +124,14 @@ export default Vue.extend({
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	watch: {
 | 
			
		||||
		slots() {
 | 
			
		||||
			this.value.value.slots = this.slots.split('\n').map(x => ({
 | 
			
		||||
				name: x,
 | 
			
		||||
				type: null
 | 
			
		||||
			}));
 | 
			
		||||
		slots: {
 | 
			
		||||
			handler() {
 | 
			
		||||
				this.value.value.slots = this.slots.split('\n').map(x => ({
 | 
			
		||||
					name: x,
 | 
			
		||||
					type: null
 | 
			
		||||
				}));
 | 
			
		||||
			},
 | 
			
		||||
			deep: true
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
@@ -136,18 +140,19 @@ export default Vue.extend({
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		if (this.value.value == null) Vue.set(this.value, 'value', null);
 | 
			
		||||
		if (this.value.value == null) this.value.value = null;
 | 
			
		||||
 | 
			
		||||
		if (this.value.value && this.value.value.slots) this.slots = this.value.value.slots.map(x => x.name).join('\n');
 | 
			
		||||
 | 
			
		||||
		this.$watch('value.type', (t) => {
 | 
			
		||||
		this.$watch(() => this.value.type, (t) => {
 | 
			
		||||
			this.warn = null;
 | 
			
		||||
 | 
			
		||||
			if (this.value.type === 'fn') {
 | 
			
		||||
				const id = uuid();
 | 
			
		||||
				this.value.value = {};
 | 
			
		||||
				Vue.set(this.value.value, 'slots', []);
 | 
			
		||||
				Vue.set(this.value.value, 'expression', { id, type: null });
 | 
			
		||||
				this.value.value = {
 | 
			
		||||
					slots: [],
 | 
			
		||||
					expression: { id, type: null }
 | 
			
		||||
				};
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
@@ -160,7 +165,7 @@ export default Vue.extend({
 | 
			
		||||
					const id = uuid();
 | 
			
		||||
					empties.push({ id, type: null });
 | 
			
		||||
				}
 | 
			
		||||
				Vue.set(this.value, 'args', empties);
 | 
			
		||||
				this.value.args = empties;
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
@@ -171,7 +176,7 @@ export default Vue.extend({
 | 
			
		||||
				const id = uuid();
 | 
			
		||||
				empties.push({ id, type: null });
 | 
			
		||||
			}
 | 
			
		||||
			Vue.set(this.value, 'args', empties);
 | 
			
		||||
			this.value.args = empties;
 | 
			
		||||
 | 
			
		||||
			for (let i = 0; i < funcDefs[this.value.type].in.length; i++) {
 | 
			
		||||
				const inType = funcDefs[this.value.type].in[i];
 | 
			
		||||
@@ -182,7 +187,7 @@ export default Vue.extend({
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		this.$watch('value.args', (args) => {
 | 
			
		||||
		this.$watch(() => this.value.args, (args) => {
 | 
			
		||||
			if (args == null) {
 | 
			
		||||
				this.warn = null;
 | 
			
		||||
				return;
 | 
			
		||||
@@ -199,7 +204,7 @@ export default Vue.extend({
 | 
			
		||||
			deep: true
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		this.$watch('hpml.variables', () => {
 | 
			
		||||
		this.$watch(() => this.hpml.variables, () => {
 | 
			
		||||
			if (this.type != null && this.value) {
 | 
			
		||||
				this.error = this.hpml.typeCheck(this.value);
 | 
			
		||||
			}
 | 
			
		||||
@@ -210,7 +215,7 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		async changeType() {
 | 
			
		||||
			const { canceled, result: type } = await this.$root.dialog({
 | 
			
		||||
			const { canceled, result: type } = await os.dialog({
 | 
			
		||||
				type: null,
 | 
			
		||||
				title: this.$t('_pages.selectType'),
 | 
			
		||||
				select: {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,117 +1,118 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
	<div class="gwbmwxkm _panel">
 | 
			
		||||
		<header>
 | 
			
		||||
			<div class="title"><fa :icon="faStickyNote"/> {{ readonly ? $t('_pages.readPage') : pageId ? $t('_pages.editPage') : $t('_pages.newPage') }}</div>
 | 
			
		||||
			<div class="buttons">
 | 
			
		||||
				<button class="_button" @click="del()" v-if="!readonly"><fa :icon="faTrashAlt"/></button>
 | 
			
		||||
				<button class="_button" @click="() => showOptions = !showOptions"><fa :icon="faCog"/></button>
 | 
			
		||||
				<button class="_button" @click="save()" v-if="!readonly"><fa :icon="faSave"/></button>
 | 
			
		||||
			</div>
 | 
			
		||||
		</header>
 | 
			
		||||
 | 
			
		||||
		<section>
 | 
			
		||||
			<router-link class="view" v-if="pageId" :to="`/@${ author.username }/pages/${ currentName }`"><fa :icon="faExternalLinkSquareAlt"/> {{ $t('_pages.viewPage') }}</router-link>
 | 
			
		||||
 | 
			
		||||
			<mk-input v-model="title">
 | 
			
		||||
				<span>{{ $t('_pages.title') }}</span>
 | 
			
		||||
			</mk-input>
 | 
			
		||||
 | 
			
		||||
			<template v-if="showOptions">
 | 
			
		||||
				<mk-input v-model="summary">
 | 
			
		||||
					<span>{{ $t('_pages.summary') }}</span>
 | 
			
		||||
				</mk-input>
 | 
			
		||||
 | 
			
		||||
				<mk-input v-model="name">
 | 
			
		||||
					<template #prefix>{{ url }}/@{{ author.username }}/pages/</template>
 | 
			
		||||
					<span>{{ $t('_pages.url') }}</span>
 | 
			
		||||
				</mk-input>
 | 
			
		||||
 | 
			
		||||
				<mk-switch v-model="alignCenter">{{ $t('_pages.alignCenter') }}</mk-switch>
 | 
			
		||||
 | 
			
		||||
				<mk-select v-model="font">
 | 
			
		||||
					<template #label>{{ $t('_pages.font') }}</template>
 | 
			
		||||
					<option value="serif">{{ $t('_pages.fontSerif') }}</option>
 | 
			
		||||
					<option value="sans-serif">{{ $t('_pages.fontSansSerif') }}</option>
 | 
			
		||||
				</mk-select>
 | 
			
		||||
 | 
			
		||||
				<mk-switch v-model="hideTitleWhenPinned">{{ $t('_pages.hideTitleWhenPinned') }}</mk-switch>
 | 
			
		||||
 | 
			
		||||
				<div class="eyeCatch">
 | 
			
		||||
					<mk-button v-if="eyeCatchingImageId == null && !readonly" @click="setEyeCatchingImage()"><fa :icon="faPlus"/> {{ $t('_pages.eyeCatchingImageSet') }}</mk-button>
 | 
			
		||||
					<div v-else-if="eyeCatchingImage">
 | 
			
		||||
						<img :src="eyeCatchingImage.url" :alt="eyeCatchingImage.name"/>
 | 
			
		||||
						<mk-button @click="removeEyeCatchingImage()" v-if="!readonly"><fa :icon="faTrashAlt"/> {{ $t('_pages.eyeCatchingImageRemove') }}</mk-button>
 | 
			
		||||
					</div>
 | 
			
		||||
<div class="_section">
 | 
			
		||||
	<div class="_content">
 | 
			
		||||
		<div class="gwbmwxkm _panel _vMargin">
 | 
			
		||||
			<header>
 | 
			
		||||
				<div class="title"><Fa :icon="faStickyNote"/> {{ readonly ? $t('_pages.readPage') : pageId ? $t('_pages.editPage') : $t('_pages.newPage') }}</div>
 | 
			
		||||
				<div class="buttons">
 | 
			
		||||
					<button class="_button" @click="del()" v-if="!readonly"><Fa :icon="faTrashAlt"/></button>
 | 
			
		||||
					<button class="_button" @click="() => showOptions = !showOptions"><Fa :icon="faCog"/></button>
 | 
			
		||||
					<button class="_button" @click="save()" v-if="!readonly"><Fa :icon="faSave"/></button>
 | 
			
		||||
				</div>
 | 
			
		||||
			</template>
 | 
			
		||||
			</header>
 | 
			
		||||
 | 
			
		||||
			<x-blocks class="content" v-model="content" :hpml="hpml"/>
 | 
			
		||||
			<section>
 | 
			
		||||
				<router-link class="view" v-if="pageId" :to="`/@${ author.username }/pages/${ currentName }`"><Fa :icon="faExternalLinkSquareAlt"/> {{ $t('_pages.viewPage') }}</router-link>
 | 
			
		||||
 | 
			
		||||
			<mk-button @click="add()" v-if="!readonly"><fa :icon="faPlus"/></mk-button>
 | 
			
		||||
		</section>
 | 
			
		||||
				<MkInput v-model:value="title">
 | 
			
		||||
					<span>{{ $t('_pages.title') }}</span>
 | 
			
		||||
				</MkInput>
 | 
			
		||||
 | 
			
		||||
				<template v-if="showOptions">
 | 
			
		||||
					<MkInput v-model:value="summary">
 | 
			
		||||
						<span>{{ $t('_pages.summary') }}</span>
 | 
			
		||||
					</MkInput>
 | 
			
		||||
 | 
			
		||||
					<MkInput v-model:value="name">
 | 
			
		||||
						<template #prefix>{{ url }}/@{{ author.username }}/pages/</template>
 | 
			
		||||
						<span>{{ $t('_pages.url') }}</span>
 | 
			
		||||
					</MkInput>
 | 
			
		||||
 | 
			
		||||
					<MkSwitch v-model:value="alignCenter">{{ $t('_pages.alignCenter') }}</MkSwitch>
 | 
			
		||||
 | 
			
		||||
					<MkSelect v-model:value="font">
 | 
			
		||||
						<template #label>{{ $t('_pages.font') }}</template>
 | 
			
		||||
						<option value="serif">{{ $t('_pages.fontSerif') }}</option>
 | 
			
		||||
						<option value="sans-serif">{{ $t('_pages.fontSansSerif') }}</option>
 | 
			
		||||
					</MkSelect>
 | 
			
		||||
 | 
			
		||||
					<MkSwitch v-model:value="hideTitleWhenPinned">{{ $t('_pages.hideTitleWhenPinned') }}</MkSwitch>
 | 
			
		||||
 | 
			
		||||
					<div class="eyeCatch">
 | 
			
		||||
						<MkButton v-if="eyeCatchingImageId == null && !readonly" @click="setEyeCatchingImage()"><Fa :icon="faPlus"/> {{ $t('_pages.eyeCatchingImageSet') }}</MkButton>
 | 
			
		||||
						<div v-else-if="eyeCatchingImage">
 | 
			
		||||
							<img :src="eyeCatchingImage.url" :alt="eyeCatchingImage.name"/>
 | 
			
		||||
							<MkButton @click="removeEyeCatchingImage()" v-if="!readonly"><Fa :icon="faTrashAlt"/> {{ $t('_pages.eyeCatchingImageRemove') }}</MkButton>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</template>
 | 
			
		||||
 | 
			
		||||
				<XBlocks class="content" v-model:value="content" :hpml="hpml"/>
 | 
			
		||||
 | 
			
		||||
				<MkButton @click="add()" v-if="!readonly"><Fa :icon="faPlus"/></MkButton>
 | 
			
		||||
			</section>
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
		<MkContainer :body-togglable="true" class="_vMargin">
 | 
			
		||||
			<template #header><Fa :icon="faMagic"/> {{ $t('_pages.variables') }}</template>
 | 
			
		||||
			<div class="qmuvgica">
 | 
			
		||||
				<XDraggable tag="div" class="variables" v-show="variables.length > 0" :list="variables" handle=".drag-handle" :group="{ name: 'variables' }" animation="150" swap-threshold="0.5">
 | 
			
		||||
					<XVariable v-for="variable in variables"
 | 
			
		||||
						:value="variable"
 | 
			
		||||
						:removable="true"
 | 
			
		||||
						@update:value="v => updateVariable(v)"
 | 
			
		||||
						@remove="() => removeVariable(variable)"
 | 
			
		||||
						:key="variable.name"
 | 
			
		||||
						:hpml="hpml"
 | 
			
		||||
						:name="variable.name"
 | 
			
		||||
						:title="variable.name"
 | 
			
		||||
						:draggable="true"
 | 
			
		||||
					/>
 | 
			
		||||
				</XDraggable>
 | 
			
		||||
 | 
			
		||||
				<MkButton @click="addVariable()" class="add" v-if="!readonly"><Fa :icon="faPlus"/></MkButton>
 | 
			
		||||
			</div>
 | 
			
		||||
		</MkContainer>
 | 
			
		||||
 | 
			
		||||
		<MkContainer :body-togglable="true" :expanded="true" class="_vMargin">
 | 
			
		||||
			<template #header><Fa :icon="faCode"/> {{ $t('script') }}</template>
 | 
			
		||||
			<div>
 | 
			
		||||
				<MkTextarea class="_code" v-model:value="script"/>
 | 
			
		||||
			</div>
 | 
			
		||||
		</MkContainer>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<mk-container :body-togglable="true">
 | 
			
		||||
		<template #header><fa :icon="faMagic"/> {{ $t('_pages.variables') }}</template>
 | 
			
		||||
		<div class="qmuvgica">
 | 
			
		||||
			<x-draggable tag="div" class="variables" v-show="variables.length > 0" :list="variables" handle=".drag-handle" :group="{ name: 'variables' }" animation="150" swap-threshold="0.5">
 | 
			
		||||
				<x-variable v-for="variable in variables"
 | 
			
		||||
					:value="variable"
 | 
			
		||||
					:removable="true"
 | 
			
		||||
					@input="v => updateVariable(v)"
 | 
			
		||||
					@remove="() => removeVariable(variable)"
 | 
			
		||||
					:key="variable.name"
 | 
			
		||||
					:hpml="hpml"
 | 
			
		||||
					:name="variable.name"
 | 
			
		||||
					:title="variable.name"
 | 
			
		||||
					:draggable="true"
 | 
			
		||||
				/>
 | 
			
		||||
			</x-draggable>
 | 
			
		||||
 | 
			
		||||
			<mk-button @click="addVariable()" class="add" v-if="!readonly"><fa :icon="faPlus"/></mk-button>
 | 
			
		||||
		</div>
 | 
			
		||||
	</mk-container>
 | 
			
		||||
 | 
			
		||||
	<mk-container :body-togglable="true" :expanded="true">
 | 
			
		||||
		<template #header><fa :icon="faCode"/> {{ $t('script') }}</template>
 | 
			
		||||
		<div>
 | 
			
		||||
			<prism-editor class="_code" v-model="script" :highlight="highlighter" :line-numbers="false"/>
 | 
			
		||||
		</div>
 | 
			
		||||
	</mk-container>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import * as XDraggable from 'vuedraggable';
 | 
			
		||||
import { defineComponent, defineAsyncComponent } from 'vue';
 | 
			
		||||
import 'prismjs';
 | 
			
		||||
import { highlight, languages } from 'prismjs/components/prism-core';
 | 
			
		||||
import 'prismjs/components/prism-clike';
 | 
			
		||||
import 'prismjs/components/prism-javascript';
 | 
			
		||||
import 'prismjs/themes/prism-okaidia.css';
 | 
			
		||||
import { PrismEditor } from 'vue-prism-editor';
 | 
			
		||||
import 'vue-prism-editor/dist/prismeditor.min.css';
 | 
			
		||||
import { faICursor, faPlus, faMagic, faCog, faCode, faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faSave, faStickyNote, faTrashAlt } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import { v4 as uuid } from 'uuid';
 | 
			
		||||
import XVariable from './page-editor.script-block.vue';
 | 
			
		||||
import XBlocks from './page-editor.blocks.vue';
 | 
			
		||||
import MkTextarea from '../../components/ui/textarea.vue';
 | 
			
		||||
import MkContainer from '../../components/ui/container.vue';
 | 
			
		||||
import MkButton from '../../components/ui/button.vue';
 | 
			
		||||
import MkSelect from '../../components/ui/select.vue';
 | 
			
		||||
import MkSwitch from '../../components/ui/switch.vue';
 | 
			
		||||
import MkInput from '../../components/ui/input.vue';
 | 
			
		||||
import { blockDefs } from '../../scripts/hpml/index';
 | 
			
		||||
import { HpmlTypeChecker } from '../../scripts/hpml/type-checker';
 | 
			
		||||
import { url } from '../../config';
 | 
			
		||||
import { collectPageVars } from '../../scripts/collect-page-vars';
 | 
			
		||||
import { selectDriveFile } from '../../scripts/select-drive-file';
 | 
			
		||||
import MkTextarea from '@/components/ui/textarea.vue';
 | 
			
		||||
import MkContainer from '@/components/ui/container.vue';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import MkSelect from '@/components/ui/select.vue';
 | 
			
		||||
import MkSwitch from '@/components/ui/switch.vue';
 | 
			
		||||
import MkInput from '@/components/ui/input.vue';
 | 
			
		||||
import { blockDefs } from '@/scripts/hpml/index';
 | 
			
		||||
import { HpmlTypeChecker } from '@/scripts/hpml/type-checker';
 | 
			
		||||
import { url } from '@/config';
 | 
			
		||||
import { collectPageVars } from '@/scripts/collect-page-vars';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XDraggable, XVariable, XBlocks, MkTextarea, MkContainer, MkButton, MkSelect, MkSwitch, MkInput, PrismEditor
 | 
			
		||||
		XDraggable: defineAsyncComponent(() => import('vue-draggable-next').then(x => x.VueDraggableNext)),
 | 
			
		||||
		XVariable, XBlocks, MkTextarea, MkContainer, MkButton, MkSelect, MkSwitch, MkInput,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	props: {
 | 
			
		||||
@@ -159,7 +160,7 @@ export default Vue.extend({
 | 
			
		||||
			if (this.eyeCatchingImageId == null) {
 | 
			
		||||
				this.eyeCatchingImage = null;
 | 
			
		||||
			} else {
 | 
			
		||||
				this.eyeCatchingImage = await this.$root.api('drive/files/show', {
 | 
			
		||||
				this.eyeCatchingImage = await os.api('drive/files/show', {
 | 
			
		||||
					fileId: this.eyeCatchingImageId,
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
@@ -178,11 +179,11 @@ export default Vue.extend({
 | 
			
		||||
		}, { deep: true });
 | 
			
		||||
 | 
			
		||||
		if (this.initPageId) {
 | 
			
		||||
			this.page = await this.$root.api('pages/show', {
 | 
			
		||||
			this.page = await os.api('pages/show', {
 | 
			
		||||
				pageId: this.initPageId,
 | 
			
		||||
			});
 | 
			
		||||
		} else if (this.initPageName && this.initUser) {
 | 
			
		||||
			this.page = await this.$root.api('pages/show', {
 | 
			
		||||
			this.page = await os.api('pages/show', {
 | 
			
		||||
				name: this.initPageName,
 | 
			
		||||
				username: this.initUser,
 | 
			
		||||
			});
 | 
			
		||||
@@ -239,14 +240,14 @@ export default Vue.extend({
 | 
			
		||||
			const onError = err => {
 | 
			
		||||
				if (err.id == '3d81ceae-475f-4600-b2a8-2bc116157532') {
 | 
			
		||||
					if (err.info.param == 'name') {
 | 
			
		||||
						this.$root.dialog({
 | 
			
		||||
						os.dialog({
 | 
			
		||||
							type: 'error',
 | 
			
		||||
							title: this.$t('_pages.invalidNameTitle'),
 | 
			
		||||
							text: this.$t('_pages.invalidNameText')
 | 
			
		||||
						});
 | 
			
		||||
					}
 | 
			
		||||
				} else if (err.code == 'NAME_ALREADY_EXISTS') {
 | 
			
		||||
					this.$root.dialog({
 | 
			
		||||
					os.dialog({
 | 
			
		||||
						type: 'error',
 | 
			
		||||
						text: this.$t('_pages.nameAlreadyExists')
 | 
			
		||||
					});
 | 
			
		||||
@@ -255,20 +256,20 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
			if (this.pageId) {
 | 
			
		||||
				options.pageId = this.pageId;
 | 
			
		||||
				this.$root.api('pages/update', options)
 | 
			
		||||
				os.api('pages/update', options)
 | 
			
		||||
				.then(page => {
 | 
			
		||||
					this.currentName = this.name.trim();
 | 
			
		||||
					this.$root.dialog({
 | 
			
		||||
					os.dialog({
 | 
			
		||||
						type: 'success',
 | 
			
		||||
						text: this.$t('_pages.updated')
 | 
			
		||||
					});
 | 
			
		||||
				}).catch(onError);
 | 
			
		||||
			} else {
 | 
			
		||||
				this.$root.api('pages/create', options)
 | 
			
		||||
				os.api('pages/create', options)
 | 
			
		||||
				.then(page => {
 | 
			
		||||
					this.pageId = page.id;
 | 
			
		||||
					this.currentName = this.name.trim();
 | 
			
		||||
					this.$root.dialog({
 | 
			
		||||
					os.dialog({
 | 
			
		||||
						type: 'success',
 | 
			
		||||
						text: this.$t('_pages.created')
 | 
			
		||||
					});
 | 
			
		||||
@@ -278,16 +279,16 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		del() {
 | 
			
		||||
			this.$root.dialog({
 | 
			
		||||
			os.dialog({
 | 
			
		||||
				type: 'warning',
 | 
			
		||||
				text: this.$t('removeAreYouSure', { x: this.title.trim() }),
 | 
			
		||||
				showCancelButton: true
 | 
			
		||||
			}).then(({ canceled }) => {
 | 
			
		||||
				if (canceled) return;
 | 
			
		||||
				this.$root.api('pages/delete', {
 | 
			
		||||
				os.api('pages/delete', {
 | 
			
		||||
					pageId: this.pageId,
 | 
			
		||||
				}).then(() => {
 | 
			
		||||
					this.$root.dialog({
 | 
			
		||||
					os.dialog({
 | 
			
		||||
						type: 'success',
 | 
			
		||||
						text: this.$t('_pages.deleted')
 | 
			
		||||
					});
 | 
			
		||||
@@ -297,7 +298,7 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async add() {
 | 
			
		||||
			const { canceled, result: type } = await this.$root.dialog({
 | 
			
		||||
			const { canceled, result: type } = await os.dialog({
 | 
			
		||||
				type: null,
 | 
			
		||||
				title: this.$t('_pages.chooseBlock'),
 | 
			
		||||
				select: {
 | 
			
		||||
@@ -312,7 +313,7 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async addVariable() {
 | 
			
		||||
			let { canceled, result: name } = await this.$root.dialog({
 | 
			
		||||
			let { canceled, result: name } = await os.dialog({
 | 
			
		||||
				title: this.$t('_pages.enterVariableName'),
 | 
			
		||||
				input: {
 | 
			
		||||
					type: 'text',
 | 
			
		||||
@@ -324,7 +325,7 @@ export default Vue.extend({
 | 
			
		||||
			name = name.trim();
 | 
			
		||||
 | 
			
		||||
			if (this.hpml.isUsedName(name)) {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
				os.dialog({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: this.$t('_pages.variableNameIsAlreadyUsed')
 | 
			
		||||
				});
 | 
			
		||||
@@ -413,7 +414,7 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		setEyeCatchingImage() {
 | 
			
		||||
			selectDriveFile(this.$root, false).then(file => {
 | 
			
		||||
			os.selectDriveFile(false).then(file => {
 | 
			
		||||
				this.eyeCatchingImageId = file.id;
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
@@ -431,7 +432,7 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.gwbmwxkm {
 | 
			
		||||
	margin-bottom: var(--margin);
 | 
			
		||||
	position: relative;
 | 
			
		||||
 | 
			
		||||
	> header {
 | 
			
		||||
		> .title {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,41 +1,44 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="xcukqgmh">
 | 
			
		||||
	<portal to="avatar" v-if="page"><mk-avatar class="avatar" :user="page.user" :disable-preview="true"/></portal>
 | 
			
		||||
	<portal to="title" v-if="page">{{ page.title || page.name }}</portal>
 | 
			
		||||
 | 
			
		||||
	<div class="_card" v-if="page" :key="page.id">
 | 
			
		||||
		<div class="_title">{{ page.title }}</div>
 | 
			
		||||
		<div class="banner">
 | 
			
		||||
			<img :src="page.eyeCatchingImage.url" v-if="page.eyeCatchingImageId"/>
 | 
			
		||||
		</div>
 | 
			
		||||
<div class="xcukqgmh" v-if="page" :key="page.id">
 | 
			
		||||
	<div class="_section main">
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<x-page :page="page"/>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_footer">
 | 
			
		||||
			<small>@{{ page.user.username }}</small>
 | 
			
		||||
			<template v-if="$store.getters.isSignedIn && $store.state.i.id === page.userId">
 | 
			
		||||
				<router-link :to="`/my/pages/edit/${page.id}`">{{ $t('_pages.editThisPage') }}</router-link>
 | 
			
		||||
				<a v-if="$store.state.i.pinnedPageId === page.id" @click="pin(false)">{{ $t('unpin') }}</a>
 | 
			
		||||
				<a v-else @click="pin(true)">{{ $t('pin') }}</a>
 | 
			
		||||
			</template>
 | 
			
		||||
			<router-link :to="`./${page.name}/view-source`">{{ $t('_pages.viewSource') }}</router-link>
 | 
			
		||||
			<div class="like">
 | 
			
		||||
				<button class="_button" @click="unlike()" v-if="page.isLiked" :title="$t('_pages.unlike')"><fa :icon="faHeartS"/></button>
 | 
			
		||||
				<button class="_button" @click="like()" v-else :title="$t('_pages.like')"><fa :icon="faHeartR"/></button>
 | 
			
		||||
				<span class="count" v-if="page.likedCount > 0">{{ page.likedCount }}</span>
 | 
			
		||||
			<div class="banner">
 | 
			
		||||
				<img :src="page.eyeCatchingImage.url" v-if="page.eyeCatchingImageId"/>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div>
 | 
			
		||||
				<XPage :page="page"/>
 | 
			
		||||
				<small style="display: block; opacity: 0.7; margin-top: 1em;">@{{ page.user.username }}</small>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="_section like">
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<button class="_button" @click="unlike()" v-if="page.isLiked" :title="$t('_pages.unlike')"><Fa :icon="faHeartS"/></button>
 | 
			
		||||
			<button class="_button" @click="like()" v-else :title="$t('_pages.like')"><Fa :icon="faHeartR"/></button>
 | 
			
		||||
			<span class="count" v-if="page.likedCount > 0">{{ page.likedCount }}</span>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="_section links">
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<router-link :to="`./${page.name}/view-source`" class="link">{{ $t('_pages.viewSource') }}</router-link>
 | 
			
		||||
			<template v-if="$store.getters.isSignedIn && $store.state.i.id === page.userId">
 | 
			
		||||
				<router-link :to="`/my/pages/edit/${page.id}`" class="link">{{ $t('_pages.editThisPage') }}</router-link>
 | 
			
		||||
				<button v-if="$store.state.i.pinnedPageId === page.id" @click="pin(false)" class="link _textButton">{{ $t('unpin') }}</button>
 | 
			
		||||
				<button v-else @click="pin(true)" class="link _textButton">{{ $t('pin') }}</button>
 | 
			
		||||
			</template>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { computed, defineComponent } from 'vue';
 | 
			
		||||
import { faHeart as faHeartS } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faHeart as faHeartR } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import XPage from '../components/page/page.vue';
 | 
			
		||||
import XPage from '@/components/page/page.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XPage
 | 
			
		||||
	},
 | 
			
		||||
@@ -53,6 +56,12 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: computed(() => this.page ? {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: computed(() => this.page.title || this.page.name),
 | 
			
		||||
					avatar: this.page.user,
 | 
			
		||||
				}],
 | 
			
		||||
			} : null),
 | 
			
		||||
			page: null,
 | 
			
		||||
			faHeartS, faHeartR
 | 
			
		||||
		};
 | 
			
		||||
@@ -76,7 +85,7 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		fetch() {
 | 
			
		||||
			this.$root.api('pages/show', {
 | 
			
		||||
			os.api('pages/show', {
 | 
			
		||||
				name: this.pageName,
 | 
			
		||||
				username: this.username,
 | 
			
		||||
			}).then(page => {
 | 
			
		||||
@@ -85,7 +94,7 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		like() {
 | 
			
		||||
			this.$root.api('pages/like', {
 | 
			
		||||
			os.api('pages/like', {
 | 
			
		||||
				pageId: this.page.id,
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				this.page.isLiked = true;
 | 
			
		||||
@@ -94,7 +103,7 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		unlike() {
 | 
			
		||||
			this.$root.api('pages/unlike', {
 | 
			
		||||
			os.api('pages/unlike', {
 | 
			
		||||
				pageId: this.page.id,
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				this.page.isLiked = false;
 | 
			
		||||
@@ -103,13 +112,8 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		pin(pin) {
 | 
			
		||||
			this.$root.api('i/update', {
 | 
			
		||||
			os.apiWithDialog('i/update', {
 | 
			
		||||
				pinnedPageId: pin ? this.page.id : null,
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
					type: 'success',
 | 
			
		||||
					iconOnly: true, autoClose: true
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@@ -118,19 +122,23 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.xcukqgmh {
 | 
			
		||||
	> ._card {
 | 
			
		||||
		> .banner {
 | 
			
		||||
			> img {
 | 
			
		||||
				display: block;
 | 
			
		||||
				width: 100%;
 | 
			
		||||
				height: 120px;
 | 
			
		||||
				object-fit: cover;
 | 
			
		||||
	> .main {
 | 
			
		||||
		> ._content {
 | 
			
		||||
			> .banner {
 | 
			
		||||
				> img {
 | 
			
		||||
					display: block;
 | 
			
		||||
					width: 100%;
 | 
			
		||||
					height: 120px;
 | 
			
		||||
					object-fit: cover;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
		> ._footer {
 | 
			
		||||
			> * {
 | 
			
		||||
				margin: 0 0.5em;
 | 
			
		||||
	> .links {
 | 
			
		||||
		> ._content {
 | 
			
		||||
			> .link {
 | 
			
		||||
				margin-right: 0.75em;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,40 +1,47 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
	<portal to="icon"><fa :icon="faStickyNote"/></portal>
 | 
			
		||||
	<portal to="title">{{ $t('pages') }}</portal>
 | 
			
		||||
 | 
			
		||||
	<mk-tab v-model="tab" :items="[{ label: $t('_pages.my'), value: 'my', icon: faEdit }, { label: $t('_pages.liked'), value: 'liked', icon: faHeart }]"/>
 | 
			
		||||
	<MkTab v-model:value="tab" :items="[{ label: $t('_pages.my'), value: 'my', icon: faEdit }, { label: $t('_pages.liked'), value: 'liked', icon: faHeart }]"/>
 | 
			
		||||
 | 
			
		||||
	<div class="rknalgpo my" v-if="tab === 'my'">
 | 
			
		||||
		<mk-button class="new" @click="create()"><fa :icon="faPlus"/></mk-button>
 | 
			
		||||
		<mk-pagination :pagination="myPagesPagination" #default="{items}">
 | 
			
		||||
			<mk-page-preview v-for="page in items" class="ckltabjg" :page="page" :key="page.id"/>
 | 
			
		||||
		</mk-pagination>
 | 
			
		||||
		<MkButton class="new" @click="create()"><Fa :icon="faPlus"/></MkButton>
 | 
			
		||||
		<MkPagination :pagination="myPagesPagination" #default="{items}">
 | 
			
		||||
			<MkPagePreview v-for="page in items" class="ckltabjg" :page="page" :key="page.id"/>
 | 
			
		||||
		</MkPagination>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<div class="rknalgpo" v-if="tab === 'liked'">
 | 
			
		||||
		<mk-pagination :pagination="likedPagesPagination" #default="{items}">
 | 
			
		||||
			<mk-page-preview v-for="like in items" class="ckltabjg" :page="like.page" :key="like.page.id"/>
 | 
			
		||||
		</mk-pagination>
 | 
			
		||||
		<MkPagination :pagination="likedPagesPagination" #default="{items}">
 | 
			
		||||
			<MkPagePreview v-for="like in items" class="ckltabjg" :page="like.page" :key="like.page.id"/>
 | 
			
		||||
		</MkPagination>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faPlus, faEdit } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faStickyNote, faHeart } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import MkPagePreview from '../components/page-preview.vue';
 | 
			
		||||
import MkPagination from '../components/ui/pagination.vue';
 | 
			
		||||
import MkButton from '../components/ui/button.vue';
 | 
			
		||||
import MkTab from '../components/tab.vue';
 | 
			
		||||
import MkPagePreview from '@/components/page-preview.vue';
 | 
			
		||||
import MkPagination from '@/components/ui/pagination.vue';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import MkTab from '@/components/tab.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkPagePreview, MkPagination, MkButton, MkTab
 | 
			
		||||
	},
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('pages'),
 | 
			
		||||
					icon: faStickyNote
 | 
			
		||||
				}],
 | 
			
		||||
				action: {
 | 
			
		||||
					icon: faPlus,
 | 
			
		||||
					handler: this.create
 | 
			
		||||
				}
 | 
			
		||||
			},
 | 
			
		||||
			tab: 'my',
 | 
			
		||||
			myPagesPagination: {
 | 
			
		||||
				endpoint: 'i/pages',
 | 
			
		||||
 
 | 
			
		||||
@@ -1,360 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
	<portal to="icon"><fa :icon="faCog"/></portal>
 | 
			
		||||
	<portal to="title">{{ $t('clinetSettings') }}</portal>
 | 
			
		||||
 | 
			
		||||
	<router-link v-if="$store.getters.isSignedIn" class="_panel _buttonPrimary" to="/my/settings" style="margin-bottom: var(--margin);">{{ $t('accountSettings') }}</router-link>
 | 
			
		||||
 | 
			
		||||
	<x-theme class="_vMargin"/>
 | 
			
		||||
 | 
			
		||||
	<x-sidebar class="_vMargin"/>
 | 
			
		||||
 | 
			
		||||
	<x-plugins class="_vMargin"/>
 | 
			
		||||
 | 
			
		||||
	<section class="_card _vMargin">
 | 
			
		||||
		<div class="_title"><fa :icon="faMusic"/> {{ $t('sounds') }}</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-range v-model="sfxVolume" :min="0" :max="1" :step="0.1">
 | 
			
		||||
				<fa slot="icon" :icon="volumeIcon"/>
 | 
			
		||||
				<span slot="title">{{ $t('volume') }}</span>
 | 
			
		||||
			</mk-range>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-select v-model="sfxNote">
 | 
			
		||||
				<template #label>{{ $t('_sfx.note') }}</template>
 | 
			
		||||
				<option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
 | 
			
		||||
				<template #text><button class="_textButton" @click="listen(sfxNote)" v-if="sfxNote"><fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
 | 
			
		||||
			</mk-select>
 | 
			
		||||
			<mk-select v-model="sfxNoteMy">
 | 
			
		||||
				<template #label>{{ $t('_sfx.noteMy') }}</template>
 | 
			
		||||
				<option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
 | 
			
		||||
				<template #text><button class="_textButton" @click="listen(sfxNoteMy)" v-if="sfxNoteMy"><fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
 | 
			
		||||
			</mk-select>
 | 
			
		||||
			<mk-select v-model="sfxNotification">
 | 
			
		||||
				<template #label>{{ $t('_sfx.notification') }}</template>
 | 
			
		||||
				<option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
 | 
			
		||||
				<template #text><button class="_textButton" @click="listen(sfxNotification)" v-if="sfxNotification"><fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
 | 
			
		||||
			</mk-select>
 | 
			
		||||
			<mk-select v-model="sfxChat">
 | 
			
		||||
				<template #label>{{ $t('_sfx.chat') }}</template>
 | 
			
		||||
				<option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
 | 
			
		||||
				<template #text><button class="_textButton" @click="listen(sfxChat)" v-if="sfxChat"><fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
 | 
			
		||||
			</mk-select>
 | 
			
		||||
			<mk-select v-model="sfxChatBg">
 | 
			
		||||
				<template #label>{{ $t('_sfx.chatBg') }}</template>
 | 
			
		||||
				<option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
 | 
			
		||||
				<template #text><button class="_textButton" @click="listen(sfxChatBg)" v-if="sfxChatBg"><fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
 | 
			
		||||
			</mk-select>
 | 
			
		||||
			<mk-select v-model="sfxAntenna">
 | 
			
		||||
				<template #label>{{ $t('_sfx.antenna') }}</template>
 | 
			
		||||
				<option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
 | 
			
		||||
				<template #text><button class="_textButton" @click="listen(sfxAntenna)" v-if="sfxAntenna"><fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
 | 
			
		||||
			</mk-select>
 | 
			
		||||
			<mk-select v-model="sfxChannel">
 | 
			
		||||
				<template #label>{{ $t('_sfx.channel') }}</template>
 | 
			
		||||
				<option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
 | 
			
		||||
				<template #text><button class="_textButton" @click="listen(sfxChannel)" v-if="sfxChannel"><fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
 | 
			
		||||
			</mk-select>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<section class="_card _vMargin">
 | 
			
		||||
		<div class="_title"><fa :icon="faColumns"/> {{ $t('deck') }}</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-switch v-model="deckAlwaysShowMainColumn">
 | 
			
		||||
				{{ $t('_deck.alwaysShowMainColumn') }}
 | 
			
		||||
			</mk-switch>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<div>{{ $t('_deck.columnAlign') }}</div>
 | 
			
		||||
			<mk-radio v-model="deckColumnAlign" value="left">{{ $t('left') }}</mk-radio>
 | 
			
		||||
			<mk-radio v-model="deckColumnAlign" value="center">{{ $t('center') }}</mk-radio>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<section class="_card _vMargin">
 | 
			
		||||
		<div class="_title"><fa :icon="faCog"/> {{ $t('appearance') }}</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-switch v-model="disableAnimatedMfm">{{ $t('disableAnimatedMfm') }}</mk-switch>
 | 
			
		||||
			<mk-switch v-model="reduceAnimation">{{ $t('reduceUiAnimation') }}</mk-switch>
 | 
			
		||||
			<mk-switch v-model="useBlurEffectForModal">{{ $t('useBlurEffectForModal') }}</mk-switch>
 | 
			
		||||
			<mk-switch v-model="useOsNativeEmojis">
 | 
			
		||||
				{{ $t('useOsNativeEmojis') }}
 | 
			
		||||
				<template #desc><mfm text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></template>
 | 
			
		||||
			</mk-switch>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<div>{{ $t('fontSize') }}</div>
 | 
			
		||||
			<mk-radio v-model="fontSize" value="small"><span style="font-size: 14px;">Aa</span></mk-radio>
 | 
			
		||||
			<mk-radio v-model="fontSize" :value="null"><span style="font-size: 16px;">Aa</span></mk-radio>
 | 
			
		||||
			<mk-radio v-model="fontSize" value="large"><span style="font-size: 18px;">Aa</span></mk-radio>
 | 
			
		||||
			<mk-radio v-model="fontSize" value="veryLarge"><span style="font-size: 20px;">Aa</span></mk-radio>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<section class="_card _vMargin">
 | 
			
		||||
		<div class="_title"><fa :icon="faCog"/> {{ $t('general') }}</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<div>{{ $t('whenServerDisconnected') }}</div>
 | 
			
		||||
			<mk-radio v-model="serverDisconnectedBehavior" value="reload">{{ $t('_serverDisconnectedBehavior.reload') }}</mk-radio>
 | 
			
		||||
			<mk-radio v-model="serverDisconnectedBehavior" value="dialog">{{ $t('_serverDisconnectedBehavior.dialog') }}</mk-radio>
 | 
			
		||||
			<mk-radio v-model="serverDisconnectedBehavior" value="quiet">{{ $t('_serverDisconnectedBehavior.quiet') }}</mk-radio>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-switch v-model="imageNewTab">{{ $t('openImageInNewTab') }}</mk-switch>
 | 
			
		||||
			<mk-switch v-model="showFixedPostForm">{{ $t('showFixedPostForm') }}</mk-switch>
 | 
			
		||||
			<mk-switch v-model="enableInfiniteScroll">{{ $t('enableInfiniteScroll') }}</mk-switch>
 | 
			
		||||
			<mk-switch v-model="fixedWidgetsPosition">{{ $t('fixedWidgetsPosition') }}</mk-switch>
 | 
			
		||||
			<mk-switch v-model="disablePagesScript">{{ $t('disablePagesScript') }}</mk-switch>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-select v-model="lang">
 | 
			
		||||
				<template #label>{{ $t('uiLanguage') }}</template>
 | 
			
		||||
 | 
			
		||||
				<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
 | 
			
		||||
			</mk-select>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<mk-button @click="cacheClear()" primary style="margin: var(--margin) auto;">{{ $t('cacheClear') }}</mk-button>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { faImage, faCog, faMusic, faPlay, faVolumeUp, faVolumeMute, faColumns } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import MkButton from '../../components/ui/button.vue';
 | 
			
		||||
import MkSwitch from '../../components/ui/switch.vue';
 | 
			
		||||
import MkSelect from '../../components/ui/select.vue';
 | 
			
		||||
import MkRadio from '../../components/ui/radio.vue';
 | 
			
		||||
import MkRange from '../../components/ui/range.vue';
 | 
			
		||||
import XTheme from './theme.vue';
 | 
			
		||||
import XSidebar from './sidebar.vue';
 | 
			
		||||
import XPlugins from './plugins.vue';
 | 
			
		||||
import { langs } from '../../config';
 | 
			
		||||
import { clientDb, set } from '../../db';
 | 
			
		||||
 | 
			
		||||
const sounds = [
 | 
			
		||||
	null,
 | 
			
		||||
	'syuilo/up',
 | 
			
		||||
	'syuilo/down',
 | 
			
		||||
	'syuilo/pope1',
 | 
			
		||||
	'syuilo/pope2',
 | 
			
		||||
	'syuilo/waon',
 | 
			
		||||
	'syuilo/popo',
 | 
			
		||||
	'syuilo/triple',
 | 
			
		||||
	'syuilo/poi1',
 | 
			
		||||
	'syuilo/poi2',
 | 
			
		||||
	'syuilo/pirori',
 | 
			
		||||
	'syuilo/pirori-wet',
 | 
			
		||||
	'syuilo/pirori-square-wet',
 | 
			
		||||
	'syuilo/square-pico',
 | 
			
		||||
	'syuilo/reverved',
 | 
			
		||||
	'syuilo/ryukyu',
 | 
			
		||||
	'aisha/1',
 | 
			
		||||
	'aisha/2',
 | 
			
		||||
	'aisha/3',
 | 
			
		||||
	'noizenecio/kick_gaba',
 | 
			
		||||
	'noizenecio/kick_gaba2',
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: this.$t('settings') as string
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	components: {
 | 
			
		||||
		XTheme,
 | 
			
		||||
		XSidebar,
 | 
			
		||||
		XPlugins,
 | 
			
		||||
		MkButton,
 | 
			
		||||
		MkSwitch,
 | 
			
		||||
		MkSelect,
 | 
			
		||||
		MkRadio,
 | 
			
		||||
		MkRange,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			langs,
 | 
			
		||||
			lang: localStorage.getItem('lang'),
 | 
			
		||||
			fontSize: localStorage.getItem('fontSize'),
 | 
			
		||||
			sounds,
 | 
			
		||||
			faImage, faCog, faMusic, faPlay, faVolumeUp, faVolumeMute, faColumns
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	computed: {
 | 
			
		||||
		serverDisconnectedBehavior: {
 | 
			
		||||
			get() { return this.$store.state.device.serverDisconnectedBehavior; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'serverDisconnectedBehavior', value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		reduceAnimation: {
 | 
			
		||||
			get() { return !this.$store.state.device.animation; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'animation', value: !value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		useBlurEffectForModal: {
 | 
			
		||||
			get() { return this.$store.state.device.useBlurEffectForModal; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'useBlurEffectForModal', value: value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		disableAnimatedMfm: {
 | 
			
		||||
			get() { return !this.$store.state.device.animatedMfm; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'animatedMfm', value: !value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		useOsNativeEmojis: {
 | 
			
		||||
			get() { return this.$store.state.device.useOsNativeEmojis; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'useOsNativeEmojis', value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		imageNewTab: {
 | 
			
		||||
			get() { return this.$store.state.device.imageNewTab; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'imageNewTab', value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		disablePagesScript: {
 | 
			
		||||
			get() { return this.$store.state.device.disablePagesScript; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'disablePagesScript', value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		showFixedPostForm: {
 | 
			
		||||
			get() { return this.$store.state.device.showFixedPostForm; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'showFixedPostForm', value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		enableInfiniteScroll: {
 | 
			
		||||
			get() { return this.$store.state.device.enableInfiniteScroll; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'enableInfiniteScroll', value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		fixedWidgetsPosition: {
 | 
			
		||||
			get() { return this.$store.state.device.fixedWidgetsPosition; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'fixedWidgetsPosition', value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		deckAlwaysShowMainColumn: {
 | 
			
		||||
			get() { return this.$store.state.device.deckAlwaysShowMainColumn; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'deckAlwaysShowMainColumn', value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		deckColumnAlign: {
 | 
			
		||||
			get() { return this.$store.state.device.deckColumnAlign; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'deckColumnAlign', value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		sfxVolume: {
 | 
			
		||||
			get() { return this.$store.state.device.sfxVolume; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'sfxVolume', value: parseFloat(value, 10) }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		sfxNote: {
 | 
			
		||||
			get() { return this.$store.state.device.sfxNote; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'sfxNote', value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		sfxNoteMy: {
 | 
			
		||||
			get() { return this.$store.state.device.sfxNoteMy; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'sfxNoteMy', value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		sfxNotification: {
 | 
			
		||||
			get() { return this.$store.state.device.sfxNotification; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'sfxNotification', value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		sfxChat: {
 | 
			
		||||
			get() { return this.$store.state.device.sfxChat; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'sfxChat', value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		sfxChatBg: {
 | 
			
		||||
			get() { return this.$store.state.device.sfxChatBg; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'sfxChatBg', value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		sfxAntenna: {
 | 
			
		||||
			get() { return this.$store.state.device.sfxAntenna; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'sfxAntenna', value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		sfxChannel: {
 | 
			
		||||
			get() { return this.$store.state.device.sfxChannel; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'sfxChannel', value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		volumeIcon: {
 | 
			
		||||
			get() {
 | 
			
		||||
				return this.sfxVolume === 0 ? faVolumeMute : faVolumeUp;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	watch: {
 | 
			
		||||
		lang() {
 | 
			
		||||
			const dialog = this.$root.dialog({
 | 
			
		||||
				type: 'waiting',
 | 
			
		||||
				iconOnly: true
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			localStorage.setItem('lang', this.lang);
 | 
			
		||||
 | 
			
		||||
			return set('_version_', `changeLang-${(new Date()).toJSON()}`, clientDb.i18n)
 | 
			
		||||
				.then(() => location.reload())
 | 
			
		||||
				.catch(() => {
 | 
			
		||||
					dialog.close();
 | 
			
		||||
					this.$root.dialog({
 | 
			
		||||
						type: 'error',
 | 
			
		||||
						iconOnly: true,
 | 
			
		||||
						autoClose: true
 | 
			
		||||
					});
 | 
			
		||||
				});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		fontSize() {
 | 
			
		||||
			if (this.fontSize == null) {
 | 
			
		||||
				localStorage.removeItem('fontSize');
 | 
			
		||||
			} else {
 | 
			
		||||
				localStorage.setItem('fontSize', this.fontSize);
 | 
			
		||||
			}
 | 
			
		||||
			location.reload();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		fixedWidgetsPosition() {
 | 
			
		||||
			location.reload()
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		enableInfiniteScroll() {
 | 
			
		||||
			location.reload()
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		listen(sound) {
 | 
			
		||||
			const audio = new Audio(`/assets/sounds/${sound}.mp3`);
 | 
			
		||||
			audio.volume = this.$store.state.device.sfxVolume;
 | 
			
		||||
			audio.play();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		cacheClear() {
 | 
			
		||||
			// Clear cache (service worker)
 | 
			
		||||
			try {
 | 
			
		||||
				navigator.serviceWorker.controller.postMessage('clear');
 | 
			
		||||
 | 
			
		||||
				navigator.serviceWorker.getRegistrations().then(registrations => {
 | 
			
		||||
					for (const registration of registrations) registration.unregister();
 | 
			
		||||
				});
 | 
			
		||||
			} catch (e) {
 | 
			
		||||
				console.error(e);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Force reload
 | 
			
		||||
			location.reload(true);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
@@ -1,95 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
<section class="_card">
 | 
			
		||||
	<div class="_title"><fa :icon="faListUl"/> {{ $t('sidebar') }}</div>
 | 
			
		||||
	<div class="_content">
 | 
			
		||||
		<mk-textarea v-model="items" tall>
 | 
			
		||||
			<span>{{ $t('sidebar') }}</span>
 | 
			
		||||
			<template #desc><button class="_textButton" @click="addItem">{{ $t('addItem') }}</button></template>
 | 
			
		||||
		</mk-textarea>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="_content">
 | 
			
		||||
		<div>{{ $t('display') }}</div>
 | 
			
		||||
		<mk-radio v-model="sidebarDisplay" value="full">{{ $t('_sidebar.full') }}</mk-radio>
 | 
			
		||||
		<mk-radio v-model="sidebarDisplay" value="icon">{{ $t('_sidebar.icon') }}</mk-radio>
 | 
			
		||||
		<!-- <mk-radio v-model="sidebarDisplay" value="hide" disabled>{{ $t('_sidebar.hide') }}</mk-radio>--> <!-- TODO: サイドバーを完全に隠せるようにすると、別途ハンバーガーボタンのようなものをUIに表示する必要があり面倒 -->
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="_footer">
 | 
			
		||||
		<mk-button inline @click="save()" primary><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
 | 
			
		||||
		<mk-button inline @click="reset()"><fa :icon="faRedo"/> {{ $t('default') }}</mk-button>
 | 
			
		||||
	</div>
 | 
			
		||||
</section>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { faListUl, faSave, faRedo } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import MkButton from '../../components/ui/button.vue';
 | 
			
		||||
import MkTextarea from '../../components/ui/textarea.vue';
 | 
			
		||||
import MkRadio from '../../components/ui/radio.vue';
 | 
			
		||||
import { defaultDeviceUserSettings } from '../../store';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkButton,
 | 
			
		||||
		MkTextarea,
 | 
			
		||||
		MkRadio,
 | 
			
		||||
	},
 | 
			
		||||
	
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			menuDef: this.$store.getters.nav({}),
 | 
			
		||||
			items: '',
 | 
			
		||||
			faListUl, faSave, faRedo
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	computed: {
 | 
			
		||||
		splited(): string[] {
 | 
			
		||||
			return this.items.trim().split('\n').filter(x => x.trim() !== '');
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		sidebarDisplay: {
 | 
			
		||||
			get() { return this.$store.state.device.sidebarDisplay; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'sidebarDisplay', value }); }
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		this.items = this.$store.state.deviceUser.menu.join('\n');
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		async addItem() {
 | 
			
		||||
			const menu = Object.keys(this.menuDef).filter(k => !this.$store.state.deviceUser.menu.includes(k));
 | 
			
		||||
			const { canceled, result: item } = await this.$root.dialog({
 | 
			
		||||
				type: null,
 | 
			
		||||
				title: this.$t('addItem'),
 | 
			
		||||
				select: {
 | 
			
		||||
					items: [...menu.map(k => ({
 | 
			
		||||
						value: k, text: this.$t(this.menuDef[k].title)
 | 
			
		||||
					})), ...[{
 | 
			
		||||
						value: '-', text: this.$t('divider')
 | 
			
		||||
					}]]
 | 
			
		||||
				},
 | 
			
		||||
				showCancelButton: true
 | 
			
		||||
			});
 | 
			
		||||
			if (canceled) return;
 | 
			
		||||
			this.items = [...this.splited, item].join('\n');
 | 
			
		||||
			this.save();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		save() {
 | 
			
		||||
			this.$store.commit('deviceUser/setMenu', this.splited);
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		reset() {
 | 
			
		||||
			this.$store.commit('deviceUser/setMenu', defaultDeviceUserSettings.menu);
 | 
			
		||||
			this.items = this.$store.state.deviceUser.menu.join('\n');
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
@@ -3,10 +3,11 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import * as THREE from 'three';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			selected: null,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,22 +1,14 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="hveuntkp">
 | 
			
		||||
	<portal to="avatar" v-if="user"><mk-avatar class="avatar" :user="user" :disable-preview="true"/></portal>
 | 
			
		||||
	<portal to="title" v-if="user">
 | 
			
		||||
		<mfm 
 | 
			
		||||
			:text="$t('_rooms.roomOf', { user: user.name || user.username })"
 | 
			
		||||
			:plain="true" :nowrap="true" :custom-emojis="user.emojis" :is-note="false"
 | 
			
		||||
		/>
 | 
			
		||||
	</portal>
 | 
			
		||||
 | 
			
		||||
	<div class="controller _card _vMargin" v-if="objectSelected">
 | 
			
		||||
	<div class="controller _section" v-if="objectSelected">
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<p class="name">{{ selectedFurnitureName }}</p>
 | 
			
		||||
			<x-preview ref="preview"/>
 | 
			
		||||
			<XPreview ref="preview"/>
 | 
			
		||||
			<template v-if="selectedFurnitureInfo.props">
 | 
			
		||||
				<div v-for="k in Object.keys(selectedFurnitureInfo.props)" :key="k">
 | 
			
		||||
					<p>{{ k }}</p>
 | 
			
		||||
					<template v-if="selectedFurnitureInfo.props[k] === 'image'">
 | 
			
		||||
						<mk-button @click="chooseImage(k, $event)">{{ $t('_rooms.chooseImage') }}</mk-button>
 | 
			
		||||
						<MkButton @click="chooseImage(k, $event)">{{ $t('_rooms.chooseImage') }}</MkButton>
 | 
			
		||||
					</template>
 | 
			
		||||
					<template v-else-if="selectedFurnitureInfo.props[k] === 'color'">
 | 
			
		||||
						<input type="color" :value="selectedFurnitureProps ? selectedFurnitureProps[k] : null" @change="updateColor(k, $event)"/>
 | 
			
		||||
@@ -25,54 +17,55 @@
 | 
			
		||||
			</template>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-button inline @click="translate()" :primary="isTranslateMode"><fa :icon="faArrowsAlt"/> {{ $t('_rooms.translate') }}</mk-button>
 | 
			
		||||
			<mk-button inline @click="rotate()" :primary="isRotateMode"><fa :icon="faUndo"/> {{ $t('_rooms.rotate') }}</mk-button>
 | 
			
		||||
			<mk-button inline v-if="isTranslateMode || isRotateMode" @click="exit()"><fa :icon="faBan"/> {{ $t('_rooms.exit') }}</mk-button>
 | 
			
		||||
			<MkButton inline @click="translate()" :primary="isTranslateMode"><Fa :icon="faArrowsAlt"/> {{ $t('_rooms.translate') }}</MkButton>
 | 
			
		||||
			<MkButton inline @click="rotate()" :primary="isRotateMode"><Fa :icon="faUndo"/> {{ $t('_rooms.rotate') }}</MkButton>
 | 
			
		||||
			<MkButton inline v-if="isTranslateMode || isRotateMode" @click="exit()"><Fa :icon="faBan"/> {{ $t('_rooms.exit') }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-button @click="remove()"><fa :icon="faTrashAlt"/> {{ $t('_rooms.remove') }}</mk-button>
 | 
			
		||||
			<MkButton @click="remove()"><Fa :icon="faTrashAlt"/> {{ $t('_rooms.remove') }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<div class="menu _card _vMargin" v-if="isMyRoom">
 | 
			
		||||
	<div class="menu _section" v-if="isMyRoom">
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-button @click="add()"><fa :icon="faBoxOpen"/> {{ $t('_rooms.addFurniture') }}</mk-button>
 | 
			
		||||
			<MkButton @click="add()"><Fa :icon="faBoxOpen"/> {{ $t('_rooms.addFurniture') }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-select :value="roomType" @input="updateRoomType($event)">
 | 
			
		||||
			<MkSelect :value="roomType" @update:value="updateRoomType($event)">
 | 
			
		||||
				<template #label>{{ $t('_rooms.roomType') }}</template>
 | 
			
		||||
				<option value="default">{{ $t('_rooms._roomType.default') }}</option>
 | 
			
		||||
				<option value="washitsu">{{ $t('_rooms._roomType.washitsu') }}</option>
 | 
			
		||||
			</mk-select>
 | 
			
		||||
			</MkSelect>
 | 
			
		||||
			<label v-if="roomType === 'default'">
 | 
			
		||||
				<span>{{ $t('_rooms.carpetColor') }}</span>
 | 
			
		||||
				<input type="color" :value="carpetColor" @change="updateCarpetColor($event)"/>
 | 
			
		||||
			</label>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<mk-button inline :disabled="!changed" primary @click="save()"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
 | 
			
		||||
			<mk-button inline @click="clear()"><fa :icon="faBroom"/> {{ $t('_rooms.clear') }}</mk-button>
 | 
			
		||||
			<MkButton inline :disabled="!changed" primary @click="save()"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
 | 
			
		||||
			<MkButton inline @click="clear()"><Fa :icon="faBroom"/> {{ $t('_rooms.clear') }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { Room } from '../../scripts/room/room';
 | 
			
		||||
import { computed, defineComponent } from 'vue';
 | 
			
		||||
import { Room } from '@/scripts/room/room';
 | 
			
		||||
import parseAcct from '../../../misc/acct/parse';
 | 
			
		||||
import XPreview from './preview.vue';
 | 
			
		||||
const storeItems = require('../../scripts/room/furnitures.json5');
 | 
			
		||||
const storeItems = require('@/scripts/room/furnitures.json5');
 | 
			
		||||
import { faBoxOpen, faUndo, faArrowsAlt, faBan, faBroom } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faSave, faTrashAlt } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import { query as urlQuery } from '../../../prelude/url';
 | 
			
		||||
import MkButton from '../../components/ui/button.vue';
 | 
			
		||||
import MkSelect from '../../components/ui/select.vue';
 | 
			
		||||
import { selectFile } from '../../scripts/select-file';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import MkSelect from '@/components/ui/select.vue';
 | 
			
		||||
import { selectFile } from '@/scripts/select-file';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
let room: Room;
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XPreview,
 | 
			
		||||
		MkButton,
 | 
			
		||||
@@ -88,6 +81,12 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: computed(() => this.user ? {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('room'),
 | 
			
		||||
					avatar: this.user,
 | 
			
		||||
				}],
 | 
			
		||||
			} : null),
 | 
			
		||||
			user: null,
 | 
			
		||||
			objectSelected: false,
 | 
			
		||||
			selectedFurnitureName: null,
 | 
			
		||||
@@ -106,13 +105,13 @@ export default Vue.extend({
 | 
			
		||||
	async mounted() {
 | 
			
		||||
		window.addEventListener('beforeunload', this.beforeunload);
 | 
			
		||||
 | 
			
		||||
		this.user = await this.$root.api('users/show', {
 | 
			
		||||
		this.user = await os.api('users/show', {
 | 
			
		||||
			...parseAcct(this.acct)
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		this.isMyRoom = this.$store.getters.isSignedIn && (this.$store.state.i.id === this.user.id);
 | 
			
		||||
 | 
			
		||||
		const roomInfo = await this.$root.api('room/show', {
 | 
			
		||||
		const roomInfo = await os.api('room/show', {
 | 
			
		||||
			userId: this.user.id
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
@@ -141,7 +140,7 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
	beforeRouteLeave(to, from, next) {
 | 
			
		||||
		if (this.changed) {
 | 
			
		||||
			this.$root.dialog({
 | 
			
		||||
			os.dialog({
 | 
			
		||||
				type: 'warning',
 | 
			
		||||
				text: this.$t('leaveConfirm'),
 | 
			
		||||
				showCancelButton: true
 | 
			
		||||
@@ -157,7 +156,7 @@ export default Vue.extend({
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	beforeDestroy() {
 | 
			
		||||
	beforeUnmount() {
 | 
			
		||||
		room.destroy();
 | 
			
		||||
		window.removeEventListener('beforeunload', this.beforeunload);
 | 
			
		||||
	},
 | 
			
		||||
@@ -171,7 +170,7 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async add() {
 | 
			
		||||
			const { canceled, result: id } = await this.$root.dialog({
 | 
			
		||||
			const { canceled, result: id } = await os.dialog({
 | 
			
		||||
				type: null,
 | 
			
		||||
				title: this.$t('_rooms.addFurniture'),
 | 
			
		||||
				select: {
 | 
			
		||||
@@ -194,16 +193,13 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		save() {
 | 
			
		||||
			this.$root.api('room/update', {
 | 
			
		||||
			os.api('room/update', {
 | 
			
		||||
				room: room.getRoomInfo()
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				this.changed = false;
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
					type: 'success',
 | 
			
		||||
					iconOnly: true, autoClose: true
 | 
			
		||||
				});
 | 
			
		||||
				os.success();
 | 
			
		||||
			}).catch((e: any) => {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
				os.dialog({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: e.message
 | 
			
		||||
				});
 | 
			
		||||
@@ -211,7 +207,7 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		clear() {
 | 
			
		||||
			this.$root.dialog({
 | 
			
		||||
			os.dialog({
 | 
			
		||||
				type: 'warning',
 | 
			
		||||
				text: this.$t('_rooms.clearConfirm'),
 | 
			
		||||
				showCancelButton: true
 | 
			
		||||
@@ -223,7 +219,7 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		chooseImage(key, e) {
 | 
			
		||||
			selectFile(this, e.currentTarget || e.target, null, false).then(file => {
 | 
			
		||||
			selectFile(e.currentTarget || e.target, null, false).then(file => {
 | 
			
		||||
				room.updateProp(key, `/proxy/?${urlQuery({ url: file.thumbnailUrl })}`);
 | 
			
		||||
				this.$refs.preview.selected(room.getSelectedObject());
 | 
			
		||||
				this.changed = true;
 | 
			
		||||
@@ -285,7 +281,7 @@ export default Vue.extend({
 | 
			
		||||
	position: relative;
 | 
			
		||||
	min-height: 500px;
 | 
			
		||||
 | 
			
		||||
	> ::v-deep canvas {
 | 
			
		||||
	> ::v-deep(canvas) {
 | 
			
		||||
		display: block;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,28 +1,25 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="">
 | 
			
		||||
	<portal to="icon"><fa :icon="faTerminal"/></portal>
 | 
			
		||||
	<portal to="title">{{ $t('scratchpad') }}</portal>
 | 
			
		||||
 | 
			
		||||
	<div class="_panel">
 | 
			
		||||
		<prism-editor class="_code" v-model="code" :highlight="highlighter" :line-numbers="false"/>
 | 
			
		||||
		<mk-button style="position: absolute; top: 8px; right: 8px;" @click="run()" primary><fa :icon="faPlay"/></mk-button>
 | 
			
		||||
		<prism-editor class="_code" v-model:value="code" :highlight="highlighter" :line-numbers="false"/>
 | 
			
		||||
		<MkButton style="position: absolute; top: 8px; right: 8px;" @click="run()" primary><Fa :icon="faPlay"/></MkButton>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<mk-container :body-togglable="true">
 | 
			
		||||
		<template #header><fa fixed-width/>{{ $t('output') }}</template>
 | 
			
		||||
	<MkContainer :body-togglable="true">
 | 
			
		||||
		<template #header><Fa fixed-width/>{{ $t('output') }}</template>
 | 
			
		||||
		<div class="bepmlvbi">
 | 
			
		||||
			<div v-for="log in logs" class="log" :key="log.id" :class="{ print: log.print }">{{ log.text }}</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</mk-container>
 | 
			
		||||
	</MkContainer>
 | 
			
		||||
 | 
			
		||||
	<section class="_card" style="margin-top: var(--margin);">
 | 
			
		||||
	<section class="_section" style="margin-top: var(--margin);">
 | 
			
		||||
		<div class="_content">{{ $t('scratchpadDescription') }}</div>
 | 
			
		||||
	</section>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faTerminal, faPlay } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import 'prismjs';
 | 
			
		||||
import { highlight, languages } from 'prismjs/components/prism-core';
 | 
			
		||||
@@ -32,17 +29,12 @@ import 'prismjs/themes/prism-okaidia.css';
 | 
			
		||||
import { PrismEditor } from 'vue-prism-editor';
 | 
			
		||||
import 'vue-prism-editor/dist/prismeditor.min.css';
 | 
			
		||||
import { AiScript, parse, utils, values } from '@syuilo/aiscript';
 | 
			
		||||
import MkContainer from '../components/ui/container.vue';
 | 
			
		||||
import MkButton from '../components/ui/button.vue';
 | 
			
		||||
import { createAiScriptEnv } from '../scripts/aiscript/api';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: this.$t('scratchpad') as string
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
import MkContainer from '@/components/ui/container.vue';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import { createAiScriptEnv } from '@/scripts/aiscript/api';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkContainer,
 | 
			
		||||
		MkButton,
 | 
			
		||||
@@ -51,6 +43,12 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('scratchpad'),
 | 
			
		||||
					icon: faTerminal,
 | 
			
		||||
				}],
 | 
			
		||||
			},
 | 
			
		||||
			code: '',
 | 
			
		||||
			logs: [],
 | 
			
		||||
			faTerminal, faPlay
 | 
			
		||||
@@ -73,12 +71,12 @@ export default Vue.extend({
 | 
			
		||||
	methods: {
 | 
			
		||||
		async run() {
 | 
			
		||||
			this.logs = [];
 | 
			
		||||
			const aiscript = new AiScript(createAiScriptEnv(this, {
 | 
			
		||||
			const aiscript = new AiScript(createAiScriptEnv({
 | 
			
		||||
				storageKey: 'scratchpad'
 | 
			
		||||
			}), {
 | 
			
		||||
				in: (q) => {
 | 
			
		||||
					return new Promise(ok => {
 | 
			
		||||
						this.$root.dialog({
 | 
			
		||||
						os.dialog({
 | 
			
		||||
							title: q,
 | 
			
		||||
							input: {}
 | 
			
		||||
						}).then(({ canceled, result: a }) => {
 | 
			
		||||
@@ -109,7 +107,7 @@ export default Vue.extend({
 | 
			
		||||
			try {
 | 
			
		||||
				ast = parse(this.code);
 | 
			
		||||
			} catch (e) {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
				os.dialog({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: 'Syntax error :('
 | 
			
		||||
				});
 | 
			
		||||
@@ -118,7 +116,7 @@ export default Vue.extend({
 | 
			
		||||
			try {
 | 
			
		||||
				await aiscript.exec(ast);
 | 
			
		||||
			} catch (e) {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
				os.dialog({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: e
 | 
			
		||||
				});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,30 +1,30 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
	<portal to="icon"><fa :icon="faSearch"/></portal>
 | 
			
		||||
	<portal to="title">{{ $t('searchWith', { q: $route.query.q }) }}</portal>
 | 
			
		||||
	<x-notes ref="notes" :pagination="pagination" @before="before" @after="after"/>
 | 
			
		||||
<div class="_section">
 | 
			
		||||
	<div class="_content">
 | 
			
		||||
		<XNotes ref="notes" :pagination="pagination" @before="before" @after="after"/>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faSearch } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import Progress from '../scripts/loading';
 | 
			
		||||
import XNotes from '../components/notes.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: this.$t('searchWith', { q: this.$route.query.q }) as string
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
import Progress from '@/scripts/loading';
 | 
			
		||||
import XNotes from '@/components/notes.vue';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XNotes
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('searchWith', { q: this.$route.query.q }),
 | 
			
		||||
					icon: faSearch
 | 
			
		||||
				}],
 | 
			
		||||
			},
 | 
			
		||||
			pagination: {
 | 
			
		||||
				endpoint: 'notes/search',
 | 
			
		||||
				limit: 10,
 | 
			
		||||
@@ -32,7 +32,6 @@ export default Vue.extend({
 | 
			
		||||
					query: this.$route.query.q,
 | 
			
		||||
				})
 | 
			
		||||
			},
 | 
			
		||||
			faSearch
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										59
									
								
								src/client/pages/settings/api.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/client/pages/settings/api.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
			
		||||
<template>
 | 
			
		||||
<section class="_section">
 | 
			
		||||
	<div class="_content">
 | 
			
		||||
		<MkButton @click="generateToken">{{ $t('generateAccessToken') }}</MkButton>
 | 
			
		||||
	</div>
 | 
			
		||||
</section>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faKey } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import MkInput from '@/components/ui/input.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkButton, MkInput
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	emits: ['info'],
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: 'API',
 | 
			
		||||
					icon: faKey
 | 
			
		||||
				}]
 | 
			
		||||
			},
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	mounted() {
 | 
			
		||||
		this.$emit('info', this.INFO);
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		async generateToken() {
 | 
			
		||||
			os.popup(await import('@/components/token-generate-window.vue'), {}, {
 | 
			
		||||
				done: async result => {
 | 
			
		||||
					const { name, permissions } = result;
 | 
			
		||||
					const { token } = await os.api('miauth/gen-token', {
 | 
			
		||||
						session: null,
 | 
			
		||||
						name: name,
 | 
			
		||||
						permission: permissions,
 | 
			
		||||
					});
 | 
			
		||||
 | 
			
		||||
					os.dialog({
 | 
			
		||||
						type: 'success',
 | 
			
		||||
						title: this.$t('token'),
 | 
			
		||||
						text: token
 | 
			
		||||
					});
 | 
			
		||||
				},
 | 
			
		||||
			}, 'closed');
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
@@ -1,21 +1,21 @@
 | 
			
		||||
<template>
 | 
			
		||||
<section class="uawsfosz _card">
 | 
			
		||||
	<div class="_title"><fa :icon="faCloud"/> {{ $t('drive') }}</div>
 | 
			
		||||
<section class="uawsfosz _section">
 | 
			
		||||
	<div class="_title"><Fa :icon="faCloud"/> {{ $t('drive') }}</div>
 | 
			
		||||
	<div class="_content">
 | 
			
		||||
		<span>{{ $t('uploadFolder') }}: {{ uploadFolder ? uploadFolder.name : '-' }}</span>
 | 
			
		||||
		<mk-button primary @click="chooseUploadFolder()"><fa :icon="faFolderOpen"/> {{ $t('selectFolder') }}</mk-button>
 | 
			
		||||
		<MkButton primary @click="chooseUploadFolder()"><Fa :icon="faFolderOpen"/> {{ $t('selectFolder') }}</MkButton>
 | 
			
		||||
	</div>
 | 
			
		||||
</section>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faCloud, faFolderOpen } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faClock, faEyeSlash, faTrashAlt } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import MkButton from '../../components/ui/button.vue';
 | 
			
		||||
import { selectDriveFolder } from '../../scripts/select-drive-folder';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkButton,
 | 
			
		||||
	},
 | 
			
		||||
@@ -29,7 +29,7 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
	async created() {
 | 
			
		||||
		if (this.$store.state.settings.uploadFolder) {
 | 
			
		||||
			this.uploadFolder = await this.$root.api('drive/folders/show', {
 | 
			
		||||
			this.uploadFolder = await os.api('drive/folders/show', {
 | 
			
		||||
				folderId: this.$store.state.settings.uploadFolder
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
@@ -37,14 +37,11 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		chooseUploadFolder() {
 | 
			
		||||
			selectDriveFolder(this.$root, false).then(async folder => {
 | 
			
		||||
			os.selectDriveFolder(false).then(async folder => {
 | 
			
		||||
				await this.$store.dispatch('settings/set', { key: 'uploadFolder', value: folder ? folder.id : null });
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
					type: 'success',
 | 
			
		||||
					iconOnly: true, autoClose: true
 | 
			
		||||
				});
 | 
			
		||||
				os.success();
 | 
			
		||||
				if (this.$store.state.settings.uploadFolder) {
 | 
			
		||||
					this.uploadFolder = await this.$root.api('drive/folders/show', {
 | 
			
		||||
					this.uploadFolder = await os.api('drive/folders/show', {
 | 
			
		||||
						folderId: this.$store.state.settings.uploadFolder
 | 
			
		||||
					});
 | 
			
		||||
				} else {
 | 
			
		||||
							
								
								
									
										219
									
								
								src/client/pages/settings/general.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										219
									
								
								src/client/pages/settings/general.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,219 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="_section">
 | 
			
		||||
	<section class="_card _vMargin">
 | 
			
		||||
		<div class="_title"><Fa :icon="faCog"/> {{ $t('general') }}</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<div>{{ $t('whenServerDisconnected') }}</div>
 | 
			
		||||
			<MkRadio v-model="serverDisconnectedBehavior" value="reload">{{ $t('_serverDisconnectedBehavior.reload') }}</MkRadio>
 | 
			
		||||
			<MkRadio v-model="serverDisconnectedBehavior" value="dialog">{{ $t('_serverDisconnectedBehavior.dialog') }}</MkRadio>
 | 
			
		||||
			<MkRadio v-model="serverDisconnectedBehavior" value="quiet">{{ $t('_serverDisconnectedBehavior.quiet') }}</MkRadio>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<MkSwitch v-model:value="imageNewTab">{{ $t('openImageInNewTab') }}</MkSwitch>
 | 
			
		||||
			<MkSwitch v-model:value="showFixedPostForm">{{ $t('showFixedPostForm') }}</MkSwitch>
 | 
			
		||||
			<MkSwitch v-model:value="enableInfiniteScroll">{{ $t('enableInfiniteScroll') }}</MkSwitch>
 | 
			
		||||
			<MkSwitch v-model:value="disablePagesScript">{{ $t('disablePagesScript') }}</MkSwitch>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<div>{{ $t('chatOpenBehavior') }}</div>
 | 
			
		||||
			<MkRadio v-model="chatOpenBehavior" value="page">{{ $t('showInPage') }}</MkRadio>
 | 
			
		||||
			<MkRadio v-model="chatOpenBehavior" value="window">{{ $t('openInWindow') }}</MkRadio>
 | 
			
		||||
			<MkRadio v-model="chatOpenBehavior" value="popout">{{ $t('popout') }}</MkRadio>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<MkSelect v-model:value="lang">
 | 
			
		||||
				<template #label>{{ $t('uiLanguage') }}</template>
 | 
			
		||||
 | 
			
		||||
				<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
 | 
			
		||||
			</MkSelect>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<section class="_card _vMargin">
 | 
			
		||||
		<div class="_title"><Fa :icon="faCog"/> {{ $t('appearance') }}</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<MkSwitch v-model:value="disableAnimatedMfm">{{ $t('disableAnimatedMfm') }}</MkSwitch>
 | 
			
		||||
			<MkSwitch v-model:value="reduceAnimation">{{ $t('reduceUiAnimation') }}</MkSwitch>
 | 
			
		||||
			<MkSwitch v-model:value="useBlurEffectForModal">{{ $t('useBlurEffectForModal') }}</MkSwitch>
 | 
			
		||||
			<MkSwitch v-model:value="useOsNativeEmojis">
 | 
			
		||||
				{{ $t('useOsNativeEmojis') }}
 | 
			
		||||
				<template #desc><Mfm text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></template>
 | 
			
		||||
			</MkSwitch>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<div>{{ $t('fontSize') }}</div>
 | 
			
		||||
			<MkRadio v-model="fontSize" value="small"><span style="font-size: 14px;">Aa</span></MkRadio>
 | 
			
		||||
			<MkRadio v-model="fontSize" :value="null"><span style="font-size: 16px;">Aa</span></MkRadio>
 | 
			
		||||
			<MkRadio v-model="fontSize" value="large"><span style="font-size: 18px;">Aa</span></MkRadio>
 | 
			
		||||
			<MkRadio v-model="fontSize" value="veryLarge"><span style="font-size: 20px;">Aa</span></MkRadio>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<section class="_card _vMargin">
 | 
			
		||||
		<div class="_title"><Fa :icon="faColumns"/> {{ $t('deck') }}</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<MkSwitch v-model:value="deckAlwaysShowMainColumn">
 | 
			
		||||
				{{ $t('_deck.alwaysShowMainColumn') }}
 | 
			
		||||
			</MkSwitch>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<div>{{ $t('_deck.columnAlign') }}</div>
 | 
			
		||||
			<MkRadio v-model="deckColumnAlign" value="left">{{ $t('left') }}</MkRadio>
 | 
			
		||||
			<MkRadio v-model="deckColumnAlign" value="center">{{ $t('center') }}</MkRadio>
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<MkButton @click="cacheClear()" primary style="margin: var(--margin) auto;">{{ $t('cacheClear') }}</MkButton>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faImage, faCog, faColumns, faCogs } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import MkSwitch from '@/components/ui/switch.vue';
 | 
			
		||||
import MkSelect from '@/components/ui/select.vue';
 | 
			
		||||
import MkRadio from '@/components/ui/radio.vue';
 | 
			
		||||
import MkRange from '@/components/ui/range.vue';
 | 
			
		||||
import { langs } from '@/config';
 | 
			
		||||
import { clientDb, set } from '@/db';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkButton,
 | 
			
		||||
		MkSwitch,
 | 
			
		||||
		MkSelect,
 | 
			
		||||
		MkRadio,
 | 
			
		||||
		MkRange,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	emits: ['info'],
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('general'),
 | 
			
		||||
					icon: faCogs
 | 
			
		||||
				}]
 | 
			
		||||
			},
 | 
			
		||||
			langs,
 | 
			
		||||
			lang: localStorage.getItem('lang'),
 | 
			
		||||
			fontSize: localStorage.getItem('fontSize'),
 | 
			
		||||
			faImage, faCog, faColumns
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	computed: {
 | 
			
		||||
		serverDisconnectedBehavior: {
 | 
			
		||||
			get() { return this.$store.state.device.serverDisconnectedBehavior; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'serverDisconnectedBehavior', value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		reduceAnimation: {
 | 
			
		||||
			get() { return !this.$store.state.device.animation; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'animation', value: !value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		useBlurEffectForModal: {
 | 
			
		||||
			get() { return this.$store.state.device.useBlurEffectForModal; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'useBlurEffectForModal', value: value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		disableAnimatedMfm: {
 | 
			
		||||
			get() { return !this.$store.state.device.animatedMfm; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'animatedMfm', value: !value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		useOsNativeEmojis: {
 | 
			
		||||
			get() { return this.$store.state.device.useOsNativeEmojis; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'useOsNativeEmojis', value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		imageNewTab: {
 | 
			
		||||
			get() { return this.$store.state.device.imageNewTab; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'imageNewTab', value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		disablePagesScript: {
 | 
			
		||||
			get() { return this.$store.state.device.disablePagesScript; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'disablePagesScript', value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		showFixedPostForm: {
 | 
			
		||||
			get() { return this.$store.state.device.showFixedPostForm; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'showFixedPostForm', value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		chatOpenBehavior: {
 | 
			
		||||
			get() { return this.$store.state.device.chatOpenBehavior; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'chatOpenBehavior', value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		enableInfiniteScroll: {
 | 
			
		||||
			get() { return this.$store.state.device.enableInfiniteScroll; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'enableInfiniteScroll', value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		deckAlwaysShowMainColumn: {
 | 
			
		||||
			get() { return this.$store.state.device.deckAlwaysShowMainColumn; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'deckAlwaysShowMainColumn', value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		deckColumnAlign: {
 | 
			
		||||
			get() { return this.$store.state.device.deckColumnAlign; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'deckColumnAlign', value }); }
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	watch: {
 | 
			
		||||
		lang() {
 | 
			
		||||
			localStorage.setItem('lang', this.lang);
 | 
			
		||||
 | 
			
		||||
			return set('_version_', `changeLang-${(new Date()).toJSON()}`, clientDb.i18n)
 | 
			
		||||
				.then(() => location.reload())
 | 
			
		||||
				.catch(() => {
 | 
			
		||||
					os.dialog({
 | 
			
		||||
						type: 'error',
 | 
			
		||||
					});
 | 
			
		||||
				});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		fontSize() {
 | 
			
		||||
			if (this.fontSize == null) {
 | 
			
		||||
				localStorage.removeItem('fontSize');
 | 
			
		||||
			} else {
 | 
			
		||||
				localStorage.setItem('fontSize', this.fontSize);
 | 
			
		||||
			}
 | 
			
		||||
			location.reload();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		enableInfiniteScroll() {
 | 
			
		||||
			location.reload()
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	mounted() {
 | 
			
		||||
		this.$emit('info', this.INFO);
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		cacheClear() {
 | 
			
		||||
			// Clear cache (service worker)
 | 
			
		||||
			try {
 | 
			
		||||
				navigator.serviceWorker.controller.postMessage('clear');
 | 
			
		||||
 | 
			
		||||
				navigator.serviceWorker.getRegistrations().then(registrations => {
 | 
			
		||||
					for (const registration of registrations) registration.unregister();
 | 
			
		||||
				});
 | 
			
		||||
			} catch (e) {
 | 
			
		||||
				console.error(e);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Force reload
 | 
			
		||||
			location.reload(true);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
@@ -1,29 +1,30 @@
 | 
			
		||||
<template>
 | 
			
		||||
<section class="_card">
 | 
			
		||||
	<div class="_title"><fa :icon="faBoxes"/> {{ $t('importAndExport') }}</div>
 | 
			
		||||
<section class="_section">
 | 
			
		||||
	<div class="_title"><Fa :icon="faBoxes"/> {{ $t('importAndExport') }}</div>
 | 
			
		||||
	<div class="_content">
 | 
			
		||||
		<mk-select v-model="exportTarget">
 | 
			
		||||
		<MkSelect v-model:value="exportTarget">
 | 
			
		||||
			<option value="notes">{{ $t('_exportOrImport.allNotes') }}</option>
 | 
			
		||||
			<option value="following">{{ $t('_exportOrImport.followingList') }}</option>
 | 
			
		||||
			<option value="user-lists">{{ $t('_exportOrImport.userLists') }}</option>
 | 
			
		||||
			<option value="mute">{{ $t('_exportOrImport.muteList') }}</option>
 | 
			
		||||
			<option value="blocking">{{ $t('_exportOrImport.blockingList') }}</option>
 | 
			
		||||
		</mk-select>
 | 
			
		||||
		<mk-button inline @click="doExport()"><fa :icon="faDownload"/> {{ $t('export') }}</mk-button>
 | 
			
		||||
		<mk-button inline @click="doImport()" :disabled="!['following', 'user-lists'].includes(exportTarget)"><fa :icon="faUpload"/> {{ $t('import') }}</mk-button>
 | 
			
		||||
		</MkSelect>
 | 
			
		||||
		<MkButton inline @click="doExport()"><Fa :icon="faDownload"/> {{ $t('export') }}</MkButton>
 | 
			
		||||
		<MkButton inline @click="doImport()" :disabled="!['following', 'user-lists'].includes(exportTarget)"><Fa :icon="faUpload"/> {{ $t('import') }}</MkButton>
 | 
			
		||||
	</div>
 | 
			
		||||
	<input ref="file" type="file" style="display: none;" @change="onChangeFile"/>
 | 
			
		||||
</section>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faDownload, faUpload, faBoxes } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import MkButton from '../../components/ui/button.vue';
 | 
			
		||||
import MkSelect from '../../components/ui/select.vue';
 | 
			
		||||
import { apiUrl } from '../../config';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import MkSelect from '@/components/ui/select.vue';
 | 
			
		||||
import { apiUrl } from '@/config';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkButton,
 | 
			
		||||
		MkSelect,
 | 
			
		||||
@@ -38,19 +39,19 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		doExport() {
 | 
			
		||||
			this.$root.api(
 | 
			
		||||
			os.api(
 | 
			
		||||
				this.exportTarget == 'notes' ? 'i/export-notes' :
 | 
			
		||||
				this.exportTarget == 'following' ? 'i/export-following' :
 | 
			
		||||
				this.exportTarget == 'blocking' ? 'i/export-blocking' :
 | 
			
		||||
				this.exportTarget == 'user-lists' ? 'i/export-user-lists' :
 | 
			
		||||
				null, {})
 | 
			
		||||
			.then(() => {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
				os.dialog({
 | 
			
		||||
					type: 'info',
 | 
			
		||||
					text: this.$t('exportRequested')
 | 
			
		||||
				});
 | 
			
		||||
			}).catch((e: any) => {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
				os.dialog({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: e.message
 | 
			
		||||
				});
 | 
			
		||||
@@ -68,7 +69,7 @@ export default Vue.extend({
 | 
			
		||||
			data.append('file', file);
 | 
			
		||||
			data.append('i', this.$store.state.i.token);
 | 
			
		||||
 | 
			
		||||
			const dialog = this.$root.dialog({
 | 
			
		||||
			const dialog = os.dialog({
 | 
			
		||||
				type: 'waiting',
 | 
			
		||||
				text: this.$t('uploading') + '...',
 | 
			
		||||
				showOkButton: false,
 | 
			
		||||
@@ -85,7 +86,7 @@ export default Vue.extend({
 | 
			
		||||
				this.reqImport(f);
 | 
			
		||||
			})
 | 
			
		||||
			.catch(e => {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
				os.dialog({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: e
 | 
			
		||||
				});
 | 
			
		||||
@@ -96,18 +97,18 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		reqImport(file) {
 | 
			
		||||
			this.$root.api(
 | 
			
		||||
			os.api(
 | 
			
		||||
				this.exportTarget == 'following' ? 'i/import-following' :
 | 
			
		||||
				this.exportTarget == 'user-lists' ? 'i/import-user-lists' :
 | 
			
		||||
				null, {
 | 
			
		||||
					fileId: file.id
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
				os.dialog({
 | 
			
		||||
					type: 'info',
 | 
			
		||||
					text: this.$t('importRequested')
 | 
			
		||||
				});
 | 
			
		||||
			}).catch((e: any) => {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
				os.dialog({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: e.message
 | 
			
		||||
				});
 | 
			
		||||
							
								
								
									
										154
									
								
								src/client/pages/settings/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								src/client/pages/settings/index.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,154 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="vvcocwet" :class="{ wide: !narrow }" ref="el">
 | 
			
		||||
	<div class="nav" v-if="!narrow || $route.name === 'settings'">
 | 
			
		||||
		<div class="menu">
 | 
			
		||||
			<div class="label">{{ $t('basicSettings') }}</div>
 | 
			
		||||
			<router-link class="item" replace to="/settings/profile"><Fa :icon="faUser" fixed-width class="icon"/>{{ $t('profile') }}</router-link>
 | 
			
		||||
			<router-link class="item" replace to="/settings/privacy"><Fa :icon="faLockOpen" fixed-width class="icon"/>{{ $t('privacy') }}</router-link>
 | 
			
		||||
			<router-link class="item" replace to="/settings/reaction"><Fa :icon="faLaugh" fixed-width class="icon"/>{{ $t('reaction') }}</router-link>
 | 
			
		||||
			<router-link class="item" replace to="/settings/notifications"><Fa :icon="faBell" fixed-width class="icon"/>{{ $t('notifications') }}</router-link>
 | 
			
		||||
			<router-link class="item" replace to="/settings/integration"><Fa :icon="faShareAlt" fixed-width class="icon"/>{{ $t('integration') }}</router-link>
 | 
			
		||||
			<router-link class="item" replace to="/settings/security"><Fa :icon="faLock" fixed-width class="icon"/>{{ $t('security') }}</router-link>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="menu">
 | 
			
		||||
			<div class="label">{{ $t('clientSettings') }}</div>
 | 
			
		||||
			<router-link class="item" replace to="/settings/general"><Fa :icon="faCogs" fixed-width class="icon"/>{{ $t('general') }}</router-link>
 | 
			
		||||
			<router-link class="item" replace to="/settings/theme"><Fa :icon="faPalette" fixed-width class="icon"/>{{ $t('theme') }}</router-link>
 | 
			
		||||
			<router-link class="item" replace to="/settings/sidebar"><Fa :icon="faListUl" fixed-width class="icon"/>{{ $t('sidebar') }}</router-link>
 | 
			
		||||
			<router-link class="item" replace to="/settings/sounds"><Fa :icon="faMusic" fixed-width class="icon"/>{{ $t('sounds') }}</router-link>
 | 
			
		||||
			<router-link class="item" replace to="/settings/plugins"><Fa :icon="faPlug" fixed-width class="icon"/>{{ $t('plugins') }}</router-link>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="menu">
 | 
			
		||||
			<div class="label">{{ $t('otherSettings') }}</div>
 | 
			
		||||
			<router-link class="item" replace to="/settings/mute-block"><Fa :icon="faBan" fixed-width class="icon"/>{{ $t('muteAndBlock') }}</router-link>
 | 
			
		||||
			<router-link class="item" replace to="/settings/word-mute"><Fa :icon="faCommentSlash" fixed-width class="icon"/>{{ $t('wordMute') }}</router-link>
 | 
			
		||||
			<router-link class="item" replace to="/settings/api"><Fa :icon="faKey" fixed-width class="icon"/>API</router-link>
 | 
			
		||||
			<router-link class="item" replace to="/settings/other"><Fa :icon="faEllipsisH" fixed-width class="icon"/>{{ $t('other') }}</router-link>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="menu">
 | 
			
		||||
			<button class="_button item" @click="logout">{{ $t('logout') }}</button>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="main">
 | 
			
		||||
		<router-view v-slot="{ Component }">
 | 
			
		||||
			<transition :name="($store.state.device.animation && !narrow) ? 'view-slide' : ''" appear mode="out-in">
 | 
			
		||||
				<component :is="Component" @info="onInfo"/>
 | 
			
		||||
			</transition>
 | 
			
		||||
		</router-view>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, onMounted, ref } from 'vue';
 | 
			
		||||
import { faCog, faPalette, faPlug, faUser, faListUl, faLock, faCommentSlash, faMusic, faCogs, faEllipsisH, faBan, faShareAlt, faLockOpen, faKey } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faLaugh, faBell } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import { store } from '@/store';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	setup(props, context) {
 | 
			
		||||
		const INFO = ref({
 | 
			
		||||
			header: [{
 | 
			
		||||
				title: i18n.global.t('settings'),
 | 
			
		||||
				icon: faCog
 | 
			
		||||
			}]
 | 
			
		||||
		});
 | 
			
		||||
		const narrow = ref(false);
 | 
			
		||||
		const view = ref(null);
 | 
			
		||||
		const el = ref(null);
 | 
			
		||||
		const onInfo = (viewInfo) => {
 | 
			
		||||
			INFO.value = viewInfo;
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		onMounted(() => {
 | 
			
		||||
			narrow.value = el.value.offsetWidth < 650;
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			INFO,
 | 
			
		||||
			narrow,
 | 
			
		||||
			view,
 | 
			
		||||
			el,
 | 
			
		||||
			onInfo,
 | 
			
		||||
			logout: () => {
 | 
			
		||||
				store.dispatch('logout');
 | 
			
		||||
				location.href = '/';
 | 
			
		||||
			},
 | 
			
		||||
			faPalette, faPlug, faUser, faListUl, faLock, faLaugh, faCommentSlash, faMusic, faBell, faCogs, faEllipsisH, faBan, faShareAlt, faLockOpen, faKey,
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.view-slide-enter-active, .view-slide-leave-active {
 | 
			
		||||
	transition: opacity 0.3s, transform 0.3s !important;
 | 
			
		||||
}
 | 
			
		||||
.view-slide-enter-from, .view-slide-leave-to {
 | 
			
		||||
	opacity: 0;
 | 
			
		||||
	transform: translateX(32px);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.vvcocwet {
 | 
			
		||||
	max-width: 1000px;
 | 
			
		||||
	margin: 0 auto;
 | 
			
		||||
 | 
			
		||||
	> .nav {
 | 
			
		||||
		> .menu {
 | 
			
		||||
			margin: 16px 0;
 | 
			
		||||
 | 
			
		||||
			> .label {
 | 
			
		||||
				padding: 8px 32px;
 | 
			
		||||
				font-size: 80%;
 | 
			
		||||
				opacity: 0.7;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> .item {
 | 
			
		||||
				display: block;
 | 
			
		||||
				width: 100%;
 | 
			
		||||
				box-sizing: border-box;
 | 
			
		||||
				padding: 0 32px;
 | 
			
		||||
				line-height: 48px;
 | 
			
		||||
				white-space: nowrap;
 | 
			
		||||
				overflow: hidden;
 | 
			
		||||
				text-overflow: ellipsis;
 | 
			
		||||
				//background: var(--panel);
 | 
			
		||||
				//border-bottom: solid 1px var(--divider);
 | 
			
		||||
				transition: padding 0.2s ease, color 0.1s ease;
 | 
			
		||||
 | 
			
		||||
				&:first-of-type {
 | 
			
		||||
					//border-top: solid 1px var(--divider);
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				&.router-link-active {
 | 
			
		||||
					color: var(--accent);
 | 
			
		||||
					padding-left: 42px;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				&:hover {
 | 
			
		||||
					text-decoration: none;
 | 
			
		||||
					padding-left: 42px;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				> .icon {
 | 
			
		||||
					margin-right: 0.5em;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&.wide {
 | 
			
		||||
		display: flex;
 | 
			
		||||
 | 
			
		||||
		> .nav {
 | 
			
		||||
			width: 30%;
 | 
			
		||||
			max-width: 260px;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> .main {
 | 
			
		||||
			flex: 1;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,44 +1,51 @@
 | 
			
		||||
<template>
 | 
			
		||||
<section class="_card" v-if="enableTwitterIntegration || enableDiscordIntegration || enableGithubIntegration">
 | 
			
		||||
	<div class="_title"><fa :icon="faShareAlt"/> {{ $t('integration') }}</div>
 | 
			
		||||
 | 
			
		||||
<section class="_section" v-if="enableTwitterIntegration || enableDiscordIntegration || enableGithubIntegration">
 | 
			
		||||
	<div class="_content" v-if="enableTwitterIntegration">
 | 
			
		||||
		<header><fa :icon="faTwitter"/> Twitter</header>
 | 
			
		||||
		<header><Fa :icon="faTwitter"/> Twitter</header>
 | 
			
		||||
		<p v-if="integrations.twitter">{{ $t('connectedTo') }}: <a :href="`https://twitter.com/${integrations.twitter.screenName}`" rel="nofollow noopener" target="_blank">@{{ integrations.twitter.screenName }}</a></p>
 | 
			
		||||
		<mk-button v-if="integrations.twitter" @click="disconnectTwitter">{{ $t('disconnectSerice') }}</mk-button>
 | 
			
		||||
		<mk-button v-else @click="connectTwitter">{{ $t('connectSerice') }}</mk-button>
 | 
			
		||||
		<MkButton v-if="integrations.twitter" @click="disconnectTwitter">{{ $t('disconnectSerice') }}</MkButton>
 | 
			
		||||
		<MkButton v-else @click="connectTwitter">{{ $t('connectSerice') }}</MkButton>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<div class="_content" v-if="enableDiscordIntegration">
 | 
			
		||||
		<header><fa :icon="faDiscord"/> Discord</header>
 | 
			
		||||
		<header><Fa :icon="faDiscord"/> Discord</header>
 | 
			
		||||
		<p v-if="integrations.discord">{{ $t('connectedTo') }}: <a :href="`https://discordapp.com/users/${integrations.discord.id}`" rel="nofollow noopener" target="_blank">@{{ integrations.discord.username }}#{{ integrations.discord.discriminator }}</a></p>
 | 
			
		||||
		<mk-button v-if="integrations.discord" @click="disconnectDiscord">{{ $t('disconnectSerice') }}</mk-button>
 | 
			
		||||
		<mk-button v-else @click="connectDiscord">{{ $t('connectSerice') }}</mk-button>
 | 
			
		||||
		<MkButton v-if="integrations.discord" @click="disconnectDiscord">{{ $t('disconnectSerice') }}</MkButton>
 | 
			
		||||
		<MkButton v-else @click="connectDiscord">{{ $t('connectSerice') }}</MkButton>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<div class="_content" v-if="enableGithubIntegration">
 | 
			
		||||
		<header><fa :icon="faGithub"/> GitHub</header>
 | 
			
		||||
		<header><Fa :icon="faGithub"/> GitHub</header>
 | 
			
		||||
		<p v-if="integrations.github">{{ $t('connectedTo') }}: <a :href="`https://github.com/${integrations.github.login}`" rel="nofollow noopener" target="_blank">@{{ integrations.github.login }}</a></p>
 | 
			
		||||
		<mk-button v-if="integrations.github" @click="disconnectGithub">{{ $t('disconnectSerice') }}</mk-button>
 | 
			
		||||
		<mk-button v-else @click="connectGithub">{{ $t('connectSerice') }}</mk-button>
 | 
			
		||||
		<MkButton v-if="integrations.github" @click="disconnectGithub">{{ $t('disconnectSerice') }}</MkButton>
 | 
			
		||||
		<MkButton v-else @click="connectGithub">{{ $t('connectSerice') }}</MkButton>
 | 
			
		||||
	</div>
 | 
			
		||||
</section>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faShareAlt } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faTwitter, faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons';
 | 
			
		||||
import { apiUrl } from '../../config';
 | 
			
		||||
import MkButton from '../../components/ui/button.vue';
 | 
			
		||||
import { apiUrl } from '@/config';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkButton
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	emits: ['info'],
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('integration'),
 | 
			
		||||
					icon: faShareAlt
 | 
			
		||||
				}]
 | 
			
		||||
			},
 | 
			
		||||
			apiUrl,
 | 
			
		||||
			twitterForm: null,
 | 
			
		||||
			discordForm: null,
 | 
			
		||||
@@ -67,6 +74,8 @@ export default Vue.extend({
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	mounted() {
 | 
			
		||||
		this.$emit('info', this.INFO);
 | 
			
		||||
 | 
			
		||||
		document.cookie = `igi=${this.$store.state.i.token}; path=/;` +
 | 
			
		||||
			` max-age=31536000;` +
 | 
			
		||||
			(document.location.protocol.startsWith('https') ? ' secure' : '');
 | 
			
		||||
							
								
								
									
										93
									
								
								src/client/pages/settings/mute-block.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								src/client/pages/settings/mute-block.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,93 @@
 | 
			
		||||
<template>
 | 
			
		||||
<section class="rrfwjxfl _section">
 | 
			
		||||
	<MkTab v-model:value="tab" :items="[{ label: $t('mutedUsers'), value: 'mute' }, { label: $t('blockedUsers'), value: 'block' }]" style="margin-bottom: var(--margin);"/>
 | 
			
		||||
	<div class="_content" v-if="tab === 'mute'">
 | 
			
		||||
		<MkPagination :pagination="mutingPagination" class="muting">
 | 
			
		||||
			<template #empty><MkInfo>{{ $t('noUsers') }}</MkInfo></template>
 | 
			
		||||
			<template #default="{items}">
 | 
			
		||||
				<div class="user" v-for="mute in items" :key="mute.id">
 | 
			
		||||
					<router-link class="name" :to="userPage(mute.mutee)">
 | 
			
		||||
						<MkAcct :user="mute.mutee"/>
 | 
			
		||||
					</router-link>
 | 
			
		||||
				</div>
 | 
			
		||||
			</template>
 | 
			
		||||
		</MkPagination>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="_content" v-if="tab === 'block'">
 | 
			
		||||
		<MkPagination :pagination="blockingPagination" class="blocking">
 | 
			
		||||
			<template #empty><MkInfo>{{ $t('noUsers') }}</MkInfo></template>
 | 
			
		||||
			<template #default="{items}">
 | 
			
		||||
				<div class="user" v-for="block in items" :key="block.id">
 | 
			
		||||
					<router-link class="name" :to="userPage(block.blockee)">
 | 
			
		||||
						<MkAcct :user="block.blockee"/>
 | 
			
		||||
					</router-link>
 | 
			
		||||
				</div>
 | 
			
		||||
			</template>
 | 
			
		||||
		</MkPagination>
 | 
			
		||||
	</div>
 | 
			
		||||
</section>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faBan } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import MkPagination from '@/components/ui/pagination.vue';
 | 
			
		||||
import MkTab from '@/components/tab.vue';
 | 
			
		||||
import MkInfo from '@/components/ui/info.vue';
 | 
			
		||||
import { userPage } from '@/filters/user';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkPagination,
 | 
			
		||||
		MkTab,
 | 
			
		||||
		MkInfo,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	emits: ['info'],
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('muteAndBlock'),
 | 
			
		||||
					icon: faBan
 | 
			
		||||
				}]
 | 
			
		||||
			},
 | 
			
		||||
			tab: 'mute',
 | 
			
		||||
			mutingPagination: {
 | 
			
		||||
				endpoint: 'mute/list',
 | 
			
		||||
				limit: 10,
 | 
			
		||||
			},
 | 
			
		||||
			blockingPagination: {
 | 
			
		||||
				endpoint: 'blocking/list',
 | 
			
		||||
				limit: 10,
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	mounted() {
 | 
			
		||||
		this.$emit('info', this.INFO);
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		userPage
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.rrfwjxfl {
 | 
			
		||||
	> ._content {
 | 
			
		||||
		max-height: 350px;
 | 
			
		||||
		overflow: auto;
 | 
			
		||||
 | 
			
		||||
		> .muting,
 | 
			
		||||
		> .blocking {
 | 
			
		||||
			> .empty {
 | 
			
		||||
				opacity: 0.5 !important;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										93
									
								
								src/client/pages/settings/notifications.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								src/client/pages/settings/notifications.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,93 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
	<div class="_section">
 | 
			
		||||
		<MkButton full primary @click="configure"><Fa :icon="faCog"/> {{ $t('notificationSetting') }}</MkButton>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="_section">
 | 
			
		||||
		<div class="_card">
 | 
			
		||||
			<div class="_content">
 | 
			
		||||
				<MkSwitch v-model:value="$store.state.i.autoWatch" @update:value="onChangeAutoWatch">
 | 
			
		||||
					{{ $t('autoNoteWatch') }}<template #desc>{{ $t('autoNoteWatchDescription') }}</template>
 | 
			
		||||
				</MkSwitch>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="_section">
 | 
			
		||||
		<MkButton full @click="readAllNotifications">{{ $t('markAsReadAllNotifications') }}</MkButton>
 | 
			
		||||
		<MkButton full @click="readAllUnreadNotes">{{ $t('markAsReadAllUnreadNotes') }}</MkButton>
 | 
			
		||||
		<MkButton full @click="readAllMessagingMessages">{{ $t('markAsReadAllTalkMessages') }}</MkButton>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faCog } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faBell } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import MkSwitch from '@/components/ui/switch.vue';
 | 
			
		||||
import { notificationTypes } from '../../../types';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkButton,
 | 
			
		||||
		MkSwitch,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	emits: ['info'],
 | 
			
		||||
	
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('notifications'),
 | 
			
		||||
					icon: faBell
 | 
			
		||||
				}]
 | 
			
		||||
			},
 | 
			
		||||
			faCog
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	mounted() {
 | 
			
		||||
		this.$emit('info', this.INFO);
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		onChangeAutoWatch(v) {
 | 
			
		||||
			os.api('i/update', {
 | 
			
		||||
				autoWatch: v
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		readAllUnreadNotes() {
 | 
			
		||||
			os.api('i/read-all-unread-notes');
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		readAllMessagingMessages() {
 | 
			
		||||
			os.api('i/read-all-messaging-messages');
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		readAllNotifications() {
 | 
			
		||||
			os.api('notifications/mark-all-as-read');
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async configure() {
 | 
			
		||||
			const includingTypes = notificationTypes.filter(x => !this.$store.state.i.mutingNotificationTypes.includes(x));
 | 
			
		||||
			os.popup(await import('@/components/notification-setting-window.vue'), {
 | 
			
		||||
				includingTypes,
 | 
			
		||||
				showGlobalToggle: false,
 | 
			
		||||
			}, {
 | 
			
		||||
				done: async (res) => {
 | 
			
		||||
					const { includingTypes: value } = res;
 | 
			
		||||
					await os.apiWithDialog('i/update', {
 | 
			
		||||
						mutingNotificationTypes: notificationTypes.filter(x => !value.includes(x)),
 | 
			
		||||
					}).then(i => {
 | 
			
		||||
						this.$store.state.i.mutingNotificationTypes = i.mutingNotificationTypes;
 | 
			
		||||
					});
 | 
			
		||||
				}
 | 
			
		||||
			}, 'closed');
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										51
									
								
								src/client/pages/settings/other.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/client/pages/settings/other.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="_section">
 | 
			
		||||
	<div class="_card">
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<MkSwitch v-model:value="$store.state.i.injectFeaturedNote" @update:value="onChangeInjectFeaturedNote">
 | 
			
		||||
				{{ $t('showFeaturedNotesInTimeline') }}
 | 
			
		||||
			</MkSwitch>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faEllipsisH } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import MkSelect from '@/components/ui/select.vue';
 | 
			
		||||
import MkSwitch from '@/components/ui/switch.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkSelect,
 | 
			
		||||
		MkSwitch,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	emits: ['info'],
 | 
			
		||||
	
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('other'),
 | 
			
		||||
					icon: faEllipsisH
 | 
			
		||||
				}]
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	mounted() {
 | 
			
		||||
		this.$emit('info', this.INFO);
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		onChangeInjectFeaturedNote(v) {
 | 
			
		||||
			os.api('i/update', {
 | 
			
		||||
				injectFeaturedNote: v
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
@@ -1,25 +1,25 @@
 | 
			
		||||
<template>
 | 
			
		||||
<section class="_card">
 | 
			
		||||
	<div class="_title"><fa :icon="faPlug"/> {{ $t('plugins') }}</div>
 | 
			
		||||
<section class="_section">
 | 
			
		||||
	<div class="_title"><Fa :icon="faPlug"/> {{ $t('plugins') }}</div>
 | 
			
		||||
	<div class="_content">
 | 
			
		||||
		<details>
 | 
			
		||||
			<summary><fa :icon="faDownload"/> {{ $t('install') }}</summary>
 | 
			
		||||
			<mk-info warn>{{ $t('pluginInstallWarn') }}</mk-info>
 | 
			
		||||
			<mk-textarea v-model="script" tall>
 | 
			
		||||
			<summary><Fa :icon="faDownload"/> {{ $t('install') }}</summary>
 | 
			
		||||
			<MkInfo warn>{{ $t('pluginInstallWarn') }}</MkInfo>
 | 
			
		||||
			<MkTextarea v-model:value="script" tall>
 | 
			
		||||
				<span>{{ $t('script') }}</span>
 | 
			
		||||
			</mk-textarea>
 | 
			
		||||
			<mk-button @click="install()" primary><fa :icon="faSave"/> {{ $t('install') }}</mk-button>
 | 
			
		||||
			</MkTextarea>
 | 
			
		||||
			<MkButton @click="install()" primary><Fa :icon="faSave"/> {{ $t('install') }}</MkButton>
 | 
			
		||||
		</details>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="_content">
 | 
			
		||||
		<details>
 | 
			
		||||
			<summary><fa :icon="faFolderOpen"/> {{ $t('manage') }}</summary>
 | 
			
		||||
			<mk-select v-model="selectedPluginId">
 | 
			
		||||
			<summary><Fa :icon="faFolderOpen"/> {{ $t('manage') }}</summary>
 | 
			
		||||
			<MkSelect v-model:value="selectedPluginId">
 | 
			
		||||
				<option v-for="x in $store.state.deviceUser.plugins" :value="x.id" :key="x.id">{{ x.name }}</option>
 | 
			
		||||
			</mk-select>
 | 
			
		||||
			</MkSelect>
 | 
			
		||||
			<template v-if="selectedPlugin">
 | 
			
		||||
				<div style="margin: -8px 0 8px 0;">
 | 
			
		||||
					<mk-switch :value="selectedPlugin.active" @change="changeActive(selectedPlugin, $event)">{{ $t('makeActive') }}</mk-switch>
 | 
			
		||||
					<MkSwitch :value="selectedPlugin.active" @update:value="changeActive(selectedPlugin, $event)">{{ $t('makeActive') }}</MkSwitch>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div class="_keyValue">
 | 
			
		||||
					<div>{{ $t('version') }}:</div>
 | 
			
		||||
@@ -34,8 +34,8 @@
 | 
			
		||||
					<div>{{ selectedPlugin.description }}</div>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div style="margin-top: 8px;">
 | 
			
		||||
					<mk-button @click="config()" inline v-if="selectedPlugin.config"><fa :icon="faCog"/> {{ $t('settings') }}</mk-button>
 | 
			
		||||
					<mk-button @click="uninstall()" inline><fa :icon="faTrashAlt"/> {{ $t('uninstall') }}</mk-button>
 | 
			
		||||
					<MkButton @click="config()" inline v-if="selectedPlugin.config"><Fa :icon="faCog"/> {{ $t('settings') }}</MkButton>
 | 
			
		||||
					<MkButton @click="uninstall()" inline><Fa :icon="faTrashAlt"/> {{ $t('uninstall') }}</MkButton>
 | 
			
		||||
				</div>
 | 
			
		||||
			</template>
 | 
			
		||||
		</details>
 | 
			
		||||
@@ -44,18 +44,19 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { AiScript, parse } from '@syuilo/aiscript';
 | 
			
		||||
import { serialize } from '@syuilo/aiscript/built/serializer';
 | 
			
		||||
import { v4 as uuid } from 'uuid';
 | 
			
		||||
import { faPlug, faSave, faTrashAlt, faFolderOpen, faDownload, faCog } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import MkButton from '../../components/ui/button.vue';
 | 
			
		||||
import MkTextarea from '../../components/ui/textarea.vue';
 | 
			
		||||
import MkSelect from '../../components/ui/select.vue';
 | 
			
		||||
import MkInfo from '../../components/ui/info.vue';
 | 
			
		||||
import MkSwitch from '../../components/ui/switch.vue';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import MkTextarea from '@/components/ui/textarea.vue';
 | 
			
		||||
import MkSelect from '@/components/ui/select.vue';
 | 
			
		||||
import MkInfo from '@/components/ui/info.vue';
 | 
			
		||||
import MkSwitch from '@/components/ui/switch.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkButton,
 | 
			
		||||
		MkTextarea,
 | 
			
		||||
@@ -85,7 +86,7 @@ export default Vue.extend({
 | 
			
		||||
			try {
 | 
			
		||||
				ast = parse(this.script);
 | 
			
		||||
			} catch (e) {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
				os.dialog({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: 'Syntax error :('
 | 
			
		||||
				});
 | 
			
		||||
@@ -93,7 +94,7 @@ export default Vue.extend({
 | 
			
		||||
			}
 | 
			
		||||
			const meta = AiScript.collectMetadata(ast);
 | 
			
		||||
			if (meta == null) {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
				os.dialog({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: 'No metadata found :('
 | 
			
		||||
				});
 | 
			
		||||
@@ -101,7 +102,7 @@ export default Vue.extend({
 | 
			
		||||
			}
 | 
			
		||||
			const data = meta.get(null);
 | 
			
		||||
			if (data == null) {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
				os.dialog({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: 'No metadata found :('
 | 
			
		||||
				});
 | 
			
		||||
@@ -109,7 +110,7 @@ export default Vue.extend({
 | 
			
		||||
			}
 | 
			
		||||
			const { name, version, author, description, permissions, config } = data;
 | 
			
		||||
			if (name == null || version == null || author == null) {
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
				os.dialog({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: 'Required property not found :('
 | 
			
		||||
				});
 | 
			
		||||
@@ -117,20 +118,23 @@ export default Vue.extend({
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const token = permissions == null || permissions.length === 0 ? null : await new Promise(async (res, rej) => {
 | 
			
		||||
				this.$root.new(await import('../../components/token-generate-window.vue').then(m => m.default), {
 | 
			
		||||
				os.popup(await import('@/components/token-generate-window.vue'), {
 | 
			
		||||
					title: this.$t('tokenRequested'),
 | 
			
		||||
					information: this.$t('pluginTokenRequestedDescription'),
 | 
			
		||||
					initialName: name,
 | 
			
		||||
					initialPermissions: permissions
 | 
			
		||||
				}).$on('ok', async ({ name, permissions }) => {
 | 
			
		||||
					const { token } = await this.$root.api('miauth/gen-token', {
 | 
			
		||||
						session: null,
 | 
			
		||||
						name: name,
 | 
			
		||||
						permission: permissions,
 | 
			
		||||
					});
 | 
			
		||||
				}, {
 | 
			
		||||
					done: async result => {
 | 
			
		||||
						const { name, permissions } = result;
 | 
			
		||||
						const { token } = await os.api('miauth/gen-token', {
 | 
			
		||||
							session: null,
 | 
			
		||||
							name: name,
 | 
			
		||||
							permission: permissions,
 | 
			
		||||
						});
 | 
			
		||||
 | 
			
		||||
					res(token);
 | 
			
		||||
				});
 | 
			
		||||
						res(token);
 | 
			
		||||
					}
 | 
			
		||||
				}, 'closed');
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			this.$store.commit('deviceUser/installPlugin', {
 | 
			
		||||
@@ -142,10 +146,7 @@ export default Vue.extend({
 | 
			
		||||
				ast: serialize(ast)
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			this.$root.dialog({
 | 
			
		||||
				type: 'success',
 | 
			
		||||
				iconOnly: true, autoClose: true
 | 
			
		||||
			});
 | 
			
		||||
			os.success();
 | 
			
		||||
 | 
			
		||||
			this.$nextTick(() => {
 | 
			
		||||
				location.reload();
 | 
			
		||||
@@ -154,10 +155,7 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
		uninstall() {
 | 
			
		||||
			this.$store.commit('deviceUser/uninstallPlugin', this.selectedPluginId);
 | 
			
		||||
			this.$root.dialog({
 | 
			
		||||
				type: 'success',
 | 
			
		||||
				iconOnly: true, autoClose: true
 | 
			
		||||
			});
 | 
			
		||||
			os.success();
 | 
			
		||||
			this.$nextTick(() => {
 | 
			
		||||
				location.reload();
 | 
			
		||||
			});
 | 
			
		||||
@@ -170,7 +168,7 @@ export default Vue.extend({
 | 
			
		||||
				config[key].default = this.selectedPlugin.configData[key];
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const { canceled, result } = await this.$root.form(this.selectedPlugin.name, config);
 | 
			
		||||
			const { canceled, result } = await os.form(this.selectedPlugin.name, config);
 | 
			
		||||
			if (canceled) return;
 | 
			
		||||
 | 
			
		||||
			this.$store.commit('deviceUser/configPlugin', {
 | 
			
		||||
							
								
								
									
										86
									
								
								src/client/pages/settings/privacy.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								src/client/pages/settings/privacy.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,86 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="_section">
 | 
			
		||||
	<div class="_card">
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<MkSwitch v-model:value="isLocked" @update:value="save()">{{ $t('makeFollowManuallyApprove') }}</MkSwitch>
 | 
			
		||||
			<MkSwitch v-model:value="autoAcceptFollowed" v-if="isLocked" @update:value="save()">{{ $t('autoAcceptFollowed') }}</MkSwitch>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_content">
 | 
			
		||||
			<MkSwitch v-model:value="rememberNoteVisibility" @update:value="save()">{{ $t('rememberNoteVisibility') }}</MkSwitch>
 | 
			
		||||
			<MkSelect v-model:value="defaultNoteVisibility" style="margin-bottom: 8px;" v-if="!rememberNoteVisibility">
 | 
			
		||||
				<template #label>{{ $t('defaultNoteVisibility') }}</template>
 | 
			
		||||
				<option value="public">{{ $t('_visibility.public') }}</option>
 | 
			
		||||
				<option value="home">{{ $t('_visibility.home') }}</option>
 | 
			
		||||
				<option value="followers">{{ $t('_visibility.followers') }}</option>
 | 
			
		||||
				<option value="specified">{{ $t('_visibility.specified') }}</option>
 | 
			
		||||
			</MkSelect>
 | 
			
		||||
			<MkSwitch v-model:value="defaultNoteLocalOnly" v-if="!rememberNoteVisibility">{{ $t('_visibility.localOnly') }}</MkSwitch>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { faLockOpen } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import MkSelect from '@/components/ui/select.vue';
 | 
			
		||||
import MkSwitch from '@/components/ui/switch.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkSelect,
 | 
			
		||||
		MkSwitch,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	emits: ['info'],
 | 
			
		||||
	
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			INFO: {
 | 
			
		||||
				header: [{
 | 
			
		||||
					title: this.$t('privacy'),
 | 
			
		||||
					icon: faLockOpen
 | 
			
		||||
				}]
 | 
			
		||||
			},
 | 
			
		||||
			isLocked: false,
 | 
			
		||||
			autoAcceptFollowed: false,
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	computed: {
 | 
			
		||||
		defaultNoteVisibility: {
 | 
			
		||||
			get() { return this.$store.state.settings.defaultNoteVisibility; },
 | 
			
		||||
			set(value) { this.$store.dispatch('settings/set', { key: 'defaultNoteVisibility', value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		defaultNoteLocalOnly: {
 | 
			
		||||
			get() { return this.$store.state.settings.defaultNoteLocalOnly; },
 | 
			
		||||
			set(value) { this.$store.dispatch('settings/set', { key: 'defaultNoteLocalOnly', value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		rememberNoteVisibility: {
 | 
			
		||||
			get() { return this.$store.state.settings.rememberNoteVisibility; },
 | 
			
		||||
			set(value) { this.$store.dispatch('settings/set', { key: 'rememberNoteVisibility', value }); }
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		this.isLocked = this.$store.state.i.isLocked;
 | 
			
		||||
		this.autoAcceptFollowed = this.$store.state.i.autoAcceptFollowed;
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	mounted() {
 | 
			
		||||
		this.$emit('info', this.INFO);
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		save() {
 | 
			
		||||
			os.api('i/update', {
 | 
			
		||||
				isLocked: !!this.isLocked,
 | 
			
		||||
				autoAcceptFollowed: !!this.autoAcceptFollowed,
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user