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:
syuilo 2020-10-17 20:12:00 +09:00 committed by GitHub
parent a40f38b2b5
commit 7199e6f4e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
357 changed files with 15053 additions and 12496 deletions

View 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>

View file

@ -0,0 +1,10 @@
<template>
<MkLoading/>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import * as os from '@/os';
export default defineComponent({});
</script>

View file

@ -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
}

View file

@ -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

View file

@ -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 });
},
}
});

View file

@ -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();
});
}

View file

@ -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');

View file

@ -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();

View file

@ -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;
});
},

View file

@ -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,
});
},

View file

@ -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',

View file

@ -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);

View file

@ -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
}

View file

@ -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>

View file

@ -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;

View file

@ -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
};
},

View file

@ -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,

View file

@ -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>

View file

@ -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(() => {

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View 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>

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View 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>

View file

@ -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>

View 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>

View file

@ -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>

View file

@ -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>

View file

@ -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;

View 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>

View file

@ -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>

View file

@ -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', {});
});
}
}

View file

@ -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;
});
}

View file

@ -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
});

View 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>

View file

@ -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>

View file

@ -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;
}

View file

@ -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,

View file

@ -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,

View file

@ -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;

View file

@ -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();
});
}
}

View file

@ -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>

View file

@ -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;
}

View file

@ -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,

View file

@ -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();

View file

@ -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,

View file

@ -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;

View file

@ -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();

View file

@ -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();
},
}
});

View file

@ -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;

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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
}]
},
}
},
});

View file

@ -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>

View file

@ -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
}]
},
};
},

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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: {

View file

@ -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;
});

View file

@ -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>

View file

@ -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>

View file

@ -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');
},
});

View file

@ -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: {

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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);
},
}
});

View file

@ -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;
}

View file

@ -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: {

View file

@ -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 {

View file

@ -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;
}
}
}

View file

@ -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',

View file

@ -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>

View file

@ -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>

View file

@ -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,

View file

@ -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;
}
}

View file

@ -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
});

View file

@ -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
};
},

View 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>

View file

@ -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 {

View 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>

View file

@ -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
});

View 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>

View file

@ -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' : '');

View 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>

View 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>

View 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>

View file

@ -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', {

View 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