Migrate to Vue3 (#6587)
* Update reaction.vue * fix bug * wip * wip * wjio * wip * Revert "wip" This reverts commit e427f2160adf4e8a4147006e25a89854edab0033. * 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:
parent
a40f38b2b5
commit
7199e6f4e0
357 changed files with 15053 additions and 12496 deletions
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
Loading…
Add table
Add a link
Reference in a new issue