summaryrefslogtreecommitdiff
path: root/src/client/pages
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2020-10-17 20:12:00 +0900
committerGitHub <noreply@github.com>2020-10-17 20:12:00 +0900
commit7199e6f4e0b3a2c2bc198e689c3e0cd0d0f0354a (patch)
tree2263a06acec7fa21882366bae26d1a983ce21135 /src/client/pages
parentCW の input でも投稿ショートカットが動作するように (#6690) (diff)
downloadmisskey-7199e6f4e0b3a2c2bc198e689c3e0cd0d0f0354a.tar.gz
misskey-7199e6f4e0b3a2c2bc198e689c3e0cd0d0f0354a.tar.bz2
misskey-7199e6f4e0b3a2c2bc198e689c3e0cd0d0f0354a.zip
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
Diffstat (limited to 'src/client/pages')
-rw-r--r--src/client/pages/_error_.vue55
-rw-r--r--src/client/pages/_loading_.vue10
-rw-r--r--src/client/pages/about-misskey.vue122
-rw-r--r--src/client/pages/about.vue34
-rw-r--r--src/client/pages/announcements.vue42
-rw-r--r--src/client/pages/apps.vue34
-rw-r--r--src/client/pages/auth.form.vue21
-rwxr-xr-xsrc/client/pages/auth.vue29
-rw-r--r--src/client/pages/channel-editor.vue59
-rw-r--r--src/client/pages/channel.vue46
-rw-r--r--src/client/pages/channels.vue57
-rw-r--r--src/client/pages/doc.vue58
-rw-r--r--src/client/pages/docs.vue22
-rw-r--r--src/client/pages/drive.vue83
-rw-r--r--src/client/pages/explore.vue149
-rw-r--r--src/client/pages/favorites.vue28
-rw-r--r--src/client/pages/featured.vue26
-rw-r--r--src/client/pages/follow-requests.vue47
-rw-r--r--src/client/pages/follow.vue28
-rw-r--r--src/client/pages/index.vue31
-rw-r--r--src/client/pages/index.welcome.entrance.vue95
-rw-r--r--src/client/pages/index.welcome.vue33
-rw-r--r--src/client/pages/instance/announcements.vue97
-rw-r--r--src/client/pages/instance/emoji-edit-dialog.vue116
-rw-r--r--src/client/pages/instance/emojis.vue307
-rw-r--r--src/client/pages/instance/federation.vue143
-rw-r--r--src/client/pages/instance/file-dialog.vue136
-rw-r--r--src/client/pages/instance/files.vue184
-rw-r--r--src/client/pages/instance/index.metrics.vue576
-rw-r--r--src/client/pages/instance/index.queue-chart.vue198
-rw-r--r--src/client/pages/instance/index.vue768
-rw-r--r--src/client/pages/instance/instance.vue164
-rw-r--r--src/client/pages/instance/logs.vue95
-rw-r--r--src/client/pages/instance/queue.chart.vue24
-rw-r--r--src/client/pages/instance/queue.vue51
-rw-r--r--src/client/pages/instance/relays.vue50
-rw-r--r--src/client/pages/instance/settings.vue284
-rw-r--r--src/client/pages/instance/user-dialog.vue233
-rw-r--r--src/client/pages/instance/users.user.vue206
-rw-r--r--src/client/pages/instance/users.vue165
-rw-r--r--src/client/pages/mentions.vue26
-rw-r--r--src/client/pages/messages.vue24
-rw-r--r--src/client/pages/messaging/index.vue148
-rw-r--r--src/client/pages/messaging/messaging-room.form.vue53
-rw-r--r--src/client/pages/messaging/messaging-room.message.vue43
-rw-r--r--src/client/pages/messaging/messaging-room.vue150
-rw-r--r--src/client/pages/miauth.vue33
-rw-r--r--src/client/pages/my-antennas/index.antenna.vue82
-rw-r--r--src/client/pages/my-antennas/index.vue41
-rw-r--r--src/client/pages/my-groups/group.vue99
-rw-r--r--src/client/pages/my-groups/index.vue114
-rw-r--r--src/client/pages/my-lists/index.vue45
-rw-r--r--src/client/pages/my-lists/list.vue83
-rw-r--r--src/client/pages/my-settings/api.vue58
-rw-r--r--src/client/pages/my-settings/index.vue137
-rw-r--r--src/client/pages/my-settings/mute-block.vue73
-rw-r--r--src/client/pages/my-settings/privacy.vue73
-rw-r--r--src/client/pages/my-settings/reaction.vue84
-rw-r--r--src/client/pages/my-settings/security.vue84
-rw-r--r--src/client/pages/my-settings/word-mute.vue81
-rw-r--r--src/client/pages/not-found.vue21
-rw-r--r--src/client/pages/note.vue81
-rw-r--r--src/client/pages/notifications.vue28
-rw-r--r--src/client/pages/page-editor/els/page-editor.el.button.vue53
-rw-r--r--src/client/pages/page-editor/els/page-editor.el.canvas.vue25
-rw-r--r--src/client/pages/page-editor/els/page-editor.el.counter.vue21
-rw-r--r--src/client/pages/page-editor/els/page-editor.el.if.vue27
-rw-r--r--src/client/pages/page-editor/els/page-editor.el.image.vue26
-rw-r--r--src/client/pages/page-editor/els/page-editor.el.number-input.vue21
-rw-r--r--src/client/pages/page-editor/els/page-editor.el.post.vue29
-rw-r--r--src/client/pages/page-editor/els/page-editor.el.radio-button.vue36
-rw-r--r--src/client/pages/page-editor/els/page-editor.el.section.vue25
-rw-r--r--src/client/pages/page-editor/els/page-editor.el.switch.vue23
-rw-r--r--src/client/pages/page-editor/els/page-editor.el.text-input.vue21
-rw-r--r--src/client/pages/page-editor/els/page-editor.el.text.vue13
-rw-r--r--src/client/pages/page-editor/els/page-editor.el.textarea-input.vue23
-rw-r--r--src/client/pages/page-editor/els/page-editor.el.textarea.vue13
-rw-r--r--src/client/pages/page-editor/page-editor.blocks.vue19
-rw-r--r--src/client/pages/page-editor/page-editor.container.vue15
-rw-r--r--src/client/pages/page-editor/page-editor.script-block.vue61
-rw-r--r--src/client/pages/page-editor/page-editor.vue199
-rw-r--r--src/client/pages/page.vue92
-rw-r--r--src/client/pages/pages.vue41
-rw-r--r--src/client/pages/preferences/index.vue360
-rw-r--r--src/client/pages/preferences/sidebar.vue95
-rw-r--r--src/client/pages/room/preview.vue5
-rw-r--r--src/client/pages/room/room.vue80
-rw-r--r--src/client/pages/scratchpad.vue46
-rw-r--r--src/client/pages/search.vue29
-rw-r--r--src/client/pages/settings/api.vue59
-rw-r--r--src/client/pages/settings/drive.vue (renamed from src/client/pages/my-settings/drive.vue)25
-rw-r--r--src/client/pages/settings/general.vue219
-rw-r--r--src/client/pages/settings/import-export.vue (renamed from src/client/pages/my-settings/import-export.vue)39
-rw-r--r--src/client/pages/settings/index.vue154
-rw-r--r--src/client/pages/settings/integration.vue (renamed from src/client/pages/my-settings/integration.vue)41
-rw-r--r--src/client/pages/settings/mute-block.vue93
-rw-r--r--src/client/pages/settings/notifications.vue93
-rw-r--r--src/client/pages/settings/other.vue51
-rw-r--r--src/client/pages/settings/plugins.vue (renamed from src/client/pages/preferences/plugins.vue)82
-rw-r--r--src/client/pages/settings/privacy.vue86
-rw-r--r--src/client/pages/settings/profile.vue (renamed from src/client/pages/my-settings/profile.vue)142
-rw-r--r--src/client/pages/settings/reaction.vue95
-rw-r--r--src/client/pages/settings/security.2fa.vue (renamed from src/client/pages/my-settings/2fa.vue)111
-rw-r--r--src/client/pages/settings/security.vue102
-rw-r--r--src/client/pages/settings/sidebar.vue110
-rw-r--r--src/client/pages/settings/sounds.vue152
-rw-r--r--src/client/pages/settings/theme.vue (renamed from src/client/pages/preferences/theme.vue)184
-rw-r--r--src/client/pages/settings/word-mute.vue101
-rw-r--r--src/client/pages/share.vue61
-rw-r--r--src/client/pages/tag.vue27
-rw-r--r--src/client/pages/test.vue232
-rw-r--r--src/client/pages/theme-editor.vue195
-rw-r--r--src/client/pages/timeline.tutorial.vue (renamed from src/client/pages/index.home.tutorial.vue)38
-rw-r--r--src/client/pages/timeline.vue (renamed from src/client/pages/index.home.vue)163
-rw-r--r--src/client/pages/user/follow-list.vue112
-rw-r--r--src/client/pages/user/index.activity.vue7
-rw-r--r--src/client/pages/user/index.photos.vue19
-rw-r--r--src/client/pages/user/index.timeline.vue9
-rw-r--r--src/client/pages/user/index.vue563
-rw-r--r--src/client/pages/welcome.entrance.vue89
-rw-r--r--src/client/pages/welcome.setup.vue (renamed from src/client/pages/index.welcome.setup.vue)30
-rw-r--r--src/client/pages/welcome.vue37
122 files changed, 6097 insertions, 5389 deletions
diff --git a/src/client/pages/_error_.vue b/src/client/pages/_error_.vue
new file mode 100644
index 0000000000..c2497c17b3
--- /dev/null
+++ b/src/client/pages/_error_.vue
@@ -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>
diff --git a/src/client/pages/_loading_.vue b/src/client/pages/_loading_.vue
new file mode 100644
index 0000000000..05c6af1cd7
--- /dev/null
+++ b/src/client/pages/_loading_.vue
@@ -0,0 +1,10 @@
+<template>
+<MkLoading/>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as os from '@/os';
+
+export default defineComponent({});
+</script>
diff --git a/src/client/pages/about-misskey.vue b/src/client/pages/about-misskey.vue
index 2c4a257b15..721e736902 100644
--- a/src/client/pages/about-misskey.vue
+++ b/src/client/pages/about-misskey.vue
@@ -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="text-align: center;">{{ $t('aboutMisskeyText') }}</div>
+ </div>
+ </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>
- <mk-url url="https://crowdin.com/project/misskey"/>
+ <MkUrl url="https://crowdin.com/project/misskey"/>
<div style="margin-top: 1em;">💴 {{ $t('misskeyDonate') }}</div>
- <mk-url url="https://www.patreon.com/syuilo"/>
+ <MkUrl url="https://www.patreon.com/syuilo"/>
</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('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
}
diff --git a/src/client/pages/about.vue b/src/client/pages/about.vue
index 25fb0ca13e..042122ceec 100644
--- a/src/client/pages/about.vue
+++ b/src/client/pages/about.vue
@@ -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
diff --git a/src/client/pages/announcements.vue b/src/client/pages/announcements.vue
index 0047599749..9815f2df9e 100644
--- a/src/client/pages/announcements.vue
+++ b/src/client/pages/announcements.vue
@@ -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 });
},
}
});
diff --git a/src/client/pages/apps.vue b/src/client/pages/apps.vue
index 445bba34c8..790fd80961 100644
--- a/src/client/pages/apps.vue
+++ b/src/client/pages/apps.vue
@@ -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();
});
}
diff --git a/src/client/pages/auth.form.vue b/src/client/pages/auth.form.vue
index c5a9b769ac..dd5aa34e6f 100644
--- a/src/client/pages/auth.form.vue
+++ b/src/client/pages/auth.form.vue
@@ -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');
diff --git a/src/client/pages/auth.vue b/src/client/pages/auth.vue
index 5c40842da1..4b67658b7d 100755
--- a/src/client/pages/auth.vue
+++ b/src/client/pages/auth.vue
@@ -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();
diff --git a/src/client/pages/channel-editor.vue b/src/client/pages/channel-editor.vue
index 0178662119..c011acc52e 100644
--- a/src/client/pages/channel-editor.vue
+++ b/src/client/pages/channel-editor.vue
@@ -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;
});
},
diff --git a/src/client/pages/channel.vue b/src/client/pages/channel.vue
index 69631af74b..305b2ab2b1 100644
--- a/src/client/pages/channel.vue
+++ b/src/client/pages/channel.vue
@@ -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,
});
},
diff --git a/src/client/pages/channels.vue b/src/client/pages/channels.vue
index 34a79e70eb..7d18e8cb51 100644
--- a/src/client/pages/channels.vue
+++ b/src/client/pages/channels.vue
@@ -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',
diff --git a/src/client/pages/doc.vue b/src/client/pages/doc.vue
index e4c4ef5c6c..f2c70df212 100644
--- a/src/client/pages/doc.vue
+++ b/src/client/pages/doc.vue
@@ -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,17 +40,14 @@ export default Vue.extend({
}
},
- watch: {
- doc: {
- handler() {
- this.fetchDoc();
- },
- immediate: true,
- }
- },
-
data() {
return {
+ INFO: {
+ header: [{
+ title: this.title,
+ icon: faFileAlt
+ }],
+ },
faFileAlt,
title: '',
body: '',
@@ -66,6 +55,15 @@ export default Vue.extend({
}
},
+ watch: {
+ doc: {
+ handler() {
+ this.fetchDoc();
+ },
+ immediate: true,
+ }
+ },
+
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);
diff --git a/src/client/pages/docs.vue b/src/client/pages/docs.vue
index a880e8abe4..ea3e16df95 100644
--- a/src/client/pages/docs.vue
+++ b/src/client/pages/docs.vue
@@ -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
}
diff --git a/src/client/pages/drive.vue b/src/client/pages/drive.vue
index 30202b7070..1456fb2922 100644
--- a/src/client/pages/drive.vue
+++ b/src/client/pages/drive.vue
@@ -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>
diff --git a/src/client/pages/explore.vue b/src/client/pages/explore.vue
index 39904846cf..cf191a7481 100644
--- a/src/client/pages/explore.vue
+++ b/src/client/pages/explore.vue
@@ -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 _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>
- <div class="localfedi7 _panel" v-if="tag == null" :style="{ backgroundImage: `url(/assets/fedi.jpg)`, marginTop: 'var(--margin)' }">
- <header><span>{{ $t('exploreFediverse') }}</span></header>
+ <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>
- <mk-container :body-togglable="true" :expanded="false" ref="tags">
- <template #header><fa :icon="faHashtag" fixed-width/>{{ $t('popularTags') }}</template>
+ <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>
- </mk-container>
+ <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>
- <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>
+ <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;
diff --git a/src/client/pages/favorites.vue b/src/client/pages/favorites.vue
index 0e625f84cf..4360dc6ec3 100644
--- a/src/client/pages/favorites.vue
+++ b/src/client/pages/favorites.vue
@@ -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
};
},
diff --git a/src/client/pages/featured.vue b/src/client/pages/featured.vue
index e6293e9e83..c3a9e25105 100644
--- a/src/client/pages/featured.vue
+++ b/src/client/pages/featured.vue
@@ -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,
diff --git a/src/client/pages/follow-requests.vue b/src/client/pages/follow-requests.vue
index b310d9f581..86e409ebbd 100644
--- a/src/client/pages/follow-requests.vue
+++ b/src/client/pages/follow-requests.vue
@@ -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>
diff --git a/src/client/pages/follow.vue b/src/client/pages/follow.vue
index 8659763bb7..35d5cc3b26 100644
--- a/src/client/pages/follow.vue
+++ b/src/client/pages/follow.vue
@@ -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(() => {
diff --git a/src/client/pages/index.vue b/src/client/pages/index.vue
deleted file mode 100644
index 788df3929a..0000000000
--- a/src/client/pages/index.vue
+++ /dev/null
@@ -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>
diff --git a/src/client/pages/index.welcome.entrance.vue b/src/client/pages/index.welcome.entrance.vue
deleted file mode 100644
index 9bb2e85fc3..0000000000
--- a/src/client/pages/index.welcome.entrance.vue
+++ /dev/null
@@ -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>
diff --git a/src/client/pages/index.welcome.vue b/src/client/pages/index.welcome.vue
deleted file mode 100644
index fb4aba6588..0000000000
--- a/src/client/pages/index.welcome.vue
+++ /dev/null
@@ -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>
diff --git a/src/client/pages/instance/announcements.vue b/src/client/pages/instance/announcements.vue
index 0e11e2932e..7abec88042 100644
--- a/src/client/pages/instance/announcements.vue
+++ b/src/client/pages/instance/announcements.vue
@@ -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>
diff --git a/src/client/pages/instance/emoji-edit-dialog.vue b/src/client/pages/instance/emoji-edit-dialog.vue
new file mode 100644
index 0000000000..ed81f15f6e
--- /dev/null
+++ b/src/client/pages/instance/emoji-edit-dialog.vue
@@ -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>
diff --git a/src/client/pages/instance/emojis.vue b/src/client/pages/instance/emojis.vue
index 25897ea7d9..465a9ebe00 100644
--- a/src/client/pages/instance/emojis.vue
+++ b/src/client/pages/instance/emojis.vue
@@ -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>
+ <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>
- </mk-pagination>
- </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>
+ </MkPagination>
</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">
+
+ <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="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 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>
- </div>
- <div class="_footer">
- <mk-button inline primary :disabled="selectedRemote == null" @click="im()"><fa :icon="faPlus"/> {{ $t('import') }}</mk-button>
+ </MkPagination>
</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;
}
}
}
diff --git a/src/client/pages/instance/federation.vue b/src/client/pages/instance/federation.vue
index 8c5cbe2ff3..f2143fa003 100644
--- a/src/client/pages/instance/federation.vue
+++ b/src/client/pages/instance/federation.vue
@@ -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;
+ }
+
+ &.red {
+ color: #c54949;
+ }
- &.off {
- color: rgba(0, 0, 0, 0.5);
- }
- }
- }
+ &.off {
+ color: rgba(0, 0, 0, 0.5);
+ }
+ }
+ }
- > .status {
- display: flex;
- align-items: center;
- font-size: 90%;
+ > .status {
+ display: flex;
+ align-items: center;
+ font-size: 90%;
- > span {
- flex: 1;
-
- > .icon {
- margin-right: 6px;
- }
- }
- }
- }
+ > span {
+ flex: 1;
+
+ > .icon {
+ margin-right: 6px;
}
}
}
diff --git a/src/client/pages/instance/file-dialog.vue b/src/client/pages/instance/file-dialog.vue
new file mode 100644
index 0000000000..c03a691bfd
--- /dev/null
+++ b/src/client/pages/instance/file-dialog.vue
@@ -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>
diff --git a/src/client/pages/instance/files.vue b/src/client/pages/instance/files.vue
index 0bc1c81e6f..ea90e3b5cd 100644
--- a/src/client/pages/instance/files.vue
+++ b/src/client/pages/instance/files.vue
@@ -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>
diff --git a/src/client/pages/instance/index.metrics.vue b/src/client/pages/instance/index.metrics.vue
new file mode 100644
index 0000000000..f3060b29d5
--- /dev/null
+++ b/src/client/pages/instance/index.metrics.vue
@@ -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>
diff --git a/src/client/pages/instance/index.queue-chart.vue b/src/client/pages/instance/index.queue-chart.vue
deleted file mode 100644
index 3b7823d924..0000000000
--- a/src/client/pages/instance/index.queue-chart.vue
+++ /dev/null
@@ -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>
diff --git a/src/client/pages/instance/index.vue b/src/client/pages/instance/index.vue
index f55a53b5f3..9383f256eb 100644
--- a/src/client/pages/instance/index.vue
+++ b/src/client/pages/instance/index.vue
@@ -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"/>
-
- <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>
+ <MkInstanceStats :chart-limit="300" :detailed="true" class="_vMargin" ref="stats"/>
- <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>
+ <MkContainer :body-togglable="true" class="_vMargin">
+ <template #header><Fa :icon="faInfoCircle"/>{{ $t('instanceInfo') }}</template>
- <div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
- <canvas ref="net"></canvas>
+ <div class="_content">
+ <div class="_keyValue"><b>Misskey</b><span>v{{ version }}</span></div>
</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 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>
- </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>
+
+ <MkContainer :body-togglable="true" :scrollable="true" class="_vMargin" style="height: 300px;">
+ <template #header><Fa :icon="faDatabase"/>{{ $t('database') }}</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="_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>
- </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>
+ </MkContainer>
</div>
- </mk-folder>
-
- <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>
+ </MkFolder>
+</div>
+<div v-if="page === 'logs'" class="_section">
+ <MkFolder>
+ <template #header><Fa :icon="faStream"/> {{ $t('logs') }}</template>
- <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
- });
- });
- },
-
- 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;
- },
+ bytes,
- 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>
diff --git a/src/client/pages/instance/instance.vue b/src/client/pages/instance/instance.vue
index 30893f381b..97f85d3b1f 100644
--- a/src/client/pages/instance/instance.vue
+++ b/src/client/pages/instance/instance.vue
@@ -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;
diff --git a/src/client/pages/instance/logs.vue b/src/client/pages/instance/logs.vue
new file mode 100644
index 0000000000..5549bd5a1a
--- /dev/null
+++ b/src/client/pages/instance/logs.vue
@@ -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>
diff --git a/src/client/pages/instance/queue.chart.vue b/src/client/pages/instance/queue.chart.vue
index 8f66c8e486..742c2b7d3c 100644
--- a/src/client/pages/instance/queue.chart.vue
+++ b/src/client/pages/instance/queue.chart.vue
@@ -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>
diff --git a/src/client/pages/instance/queue.vue b/src/client/pages/instance/queue.vue
index d9f12577e4..5dec95c670 100644
--- a/src/client/pages/instance/queue.vue
+++ b/src/client/pages/instance/queue.vue
@@ -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', {});
});
}
}
diff --git a/src/client/pages/instance/relays.vue b/src/client/pages/instance/relays.vue
index eaf6c0b682..82b7b006ed 100644
--- a/src/client/pages/instance/relays.vue
+++ b/src/client/pages/instance/relays.vue
@@ -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;
});
}
diff --git a/src/client/pages/instance/settings.vue b/src/client/pages/instance/settings.vue
index 0c0e506ab8..e8bf4a0bda 100644
--- a/src/client/pages/instance/settings.vue
+++ b/src/client/pages/instance/settings.vue
@@ -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
});
diff --git a/src/client/pages/instance/user-dialog.vue b/src/client/pages/instance/user-dialog.vue
new file mode 100644
index 0000000000..3cf30e115f
--- /dev/null
+++ b/src/client/pages/instance/user-dialog.vue
@@ -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>
diff --git a/src/client/pages/instance/users.user.vue b/src/client/pages/instance/users.user.vue
deleted file mode 100644
index 25f0260637..0000000000
--- a/src/client/pages/instance/users.user.vue
+++ /dev/null
@@ -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>
diff --git a/src/client/pages/instance/users.vue b/src/client/pages/instance/users.vue
index cf3786c965..b891ed8412 100644
--- a/src/client/pages/instance/users.vue
+++ b/src/client/pages/instance/users.vue
@@ -1,33 +1,33 @@
<template>
<div class="mk-instance-users">
- <portal to="icon"><fa :icon="faUsers"/></portal>
- <portal to="title">{{ $t('users') }}</portal>
+ <div class="_section">
+ <div class="_content">
+ <MkButton inline primary @click="addUser()"><Fa :icon="faPlus"/> {{ $t('addUser') }}</MkButton>
+ </div>
+ </div>
- <section class="_card _vMargin lookup">
- <div class="_title"><fa :icon="faSearch"/> {{ $t('lookup') }}</div>
+ <div class="_section lookup">
+ <div class="_title"><Fa :icon="faSearch"/> {{ $t('lookup') }}</div>
<div class="_content">
- <mk-input class="target" v-model="target" type="text" @enter="showUser()">
+ <MkInput class="target" v-model:value="target" type="text" @enter="showUser()">
<span>{{ $t('usernameOrUserId') }}</span>
- </mk-input>
- <mk-button @click="showUser()" primary><fa :icon="faSearch"/> {{ $t('lookup') }}</mk-button>
- </div>
- <div class="_footer">
- <mk-button inline primary @click="searchUser()"><fa :icon="faSearch"/> {{ $t('search') }}</mk-button>
+ </MkInput>
+ <MkButton @click="showUser()" primary><Fa :icon="faSearch"/> {{ $t('lookup') }}</MkButton>
</div>
- </section>
+ </div>
- <section class="_card _vMargin users">
- <div class="_title"><fa :icon="faUsers"/> {{ $t('users') }}</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>
- </div>
- <div class="_footer">
- <mk-button inline primary @click="addUser()"><fa :icon="faPlus"/> {{ $t('addUser') }}</mk-button>
+ </MkPagination>
</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;
}
diff --git a/src/client/pages/mentions.vue b/src/client/pages/mentions.vue
index 8c57a1342d..0ad3def03c 100644
--- a/src/client/pages/mentions.vue
+++ b/src/client/pages/mentions.vue
@@ -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,
diff --git a/src/client/pages/messages.vue b/src/client/pages/messages.vue
index e607b86546..4803891d0e 100644
--- a/src/client/pages/messages.vue
+++ b/src/client/pages/messages.vue
@@ -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,
diff --git a/src/client/pages/messaging/index.vue b/src/client/pages/messaging/index.vue
index 049d918595..07b1cbab83 100644
--- a/src/client/pages/messaging/index.vue
+++ b/src/client/pages/messaging/index.vue
@@ -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>
- </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>
+ </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>
- <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;
diff --git a/src/client/pages/messaging/messaging-room.form.vue b/src/client/pages/messaging/messaging-room.form.vue
index eda8914c4a..3b5b9aa966 100644
--- a/src/client/pages/messaging/messaging-room.form.vue
+++ b/src/client/pages/messaging/messaging-room.form.vue
@@ -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();
});
}
}
diff --git a/src/client/pages/messaging/messaging-room.message.vue b/src/client/pages/messaging/messaging-room.message.vue
index 40ee4e8b92..37fc6ba319 100644
--- a/src/client/pages/messaging/messaging-room.message.vue
+++ b/src/client/pages/messaging/messaging-room.message.vue
@@ -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>
diff --git a/src/client/pages/messaging/messaging-room.vue b/src/client/pages/messaging/messaging-room.vue
index abff3062c9..4210b8cf89 100644
--- a/src/client/pages/messaging/messaging-room.vue
+++ b/src/client/pages/messaging/messaging-room.vue
@@ -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;
}
diff --git a/src/client/pages/miauth.vue b/src/client/pages/miauth.vue
index 25170725da..2de058d0dd 100644
--- a/src/client/pages/miauth.vue
+++ b/src/client/pages/miauth.vue
@@ -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,
diff --git a/src/client/pages/my-antennas/index.antenna.vue b/src/client/pages/my-antennas/index.antenna.vue
index 6435e4fc9a..509600590a 100644
--- a/src/client/pages/my-antennas/index.antenna.vue
+++ b/src/client/pages/my-antennas/index.antenna.vue
@@ -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();
diff --git a/src/client/pages/my-antennas/index.vue b/src/client/pages/my-antennas/index.vue
index a5f6076ebf..08e4d43c26 100644
--- a/src/client/pages/my-antennas/index.vue
+++ b/src/client/pages/my-antennas/index.vue
@@ -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,
diff --git a/src/client/pages/my-groups/group.vue b/src/client/pages/my-groups/group.vue
index 5ac6db8e98..008c71d1fa 100644
--- a/src/client/pages/my-groups/group.vue
+++ b/src/client/pages/my-groups/group.vue
@@ -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;
diff --git a/src/client/pages/my-groups/index.vue b/src/client/pages/my-groups/index.vue
index 30d7dabbfb..f05226faaf 100644
--- a/src/client/pages/my-groups/index.vue
+++ b/src/client/pages/my-groups/index.vue
@@ -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();
diff --git a/src/client/pages/my-lists/index.vue b/src/client/pages/my-lists/index.vue
index c3f6d9c774..5e29436ede 100644
--- a/src/client/pages/my-lists/index.vue
+++ b/src/client/pages/my-lists/index.vue
@@ -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();
},
}
});
diff --git a/src/client/pages/my-lists/list.vue b/src/client/pages/my-lists/list.vue
index a418bdded5..4d965b06b3 100644
--- a/src/client/pages/my-lists/list.vue
+++ b/src/client/pages/my-lists/list.vue
@@ -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;
diff --git a/src/client/pages/my-settings/api.vue b/src/client/pages/my-settings/api.vue
deleted file mode 100644
index 44f099ea1d..0000000000
--- a/src/client/pages/my-settings/api.vue
+++ /dev/null
@@ -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>
diff --git a/src/client/pages/my-settings/index.vue b/src/client/pages/my-settings/index.vue
deleted file mode 100644
index ae4ad4dff5..0000000000
--- a/src/client/pages/my-settings/index.vue
+++ /dev/null
@@ -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>
diff --git a/src/client/pages/my-settings/mute-block.vue b/src/client/pages/my-settings/mute-block.vue
deleted file mode 100644
index 8eb43a6e29..0000000000
--- a/src/client/pages/my-settings/mute-block.vue
+++ /dev/null
@@ -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>
diff --git a/src/client/pages/my-settings/privacy.vue b/src/client/pages/my-settings/privacy.vue
deleted file mode 100644
index 527ac9ea37..0000000000
--- a/src/client/pages/my-settings/privacy.vue
+++ /dev/null
@@ -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>
diff --git a/src/client/pages/my-settings/reaction.vue b/src/client/pages/my-settings/reaction.vue
deleted file mode 100644
index ef4f6f6723..0000000000
--- a/src/client/pages/my-settings/reaction.vue
+++ /dev/null
@@ -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>
diff --git a/src/client/pages/my-settings/security.vue b/src/client/pages/my-settings/security.vue
deleted file mode 100644
index dc77ca12c5..0000000000
--- a/src/client/pages/my-settings/security.vue
+++ /dev/null
@@ -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>
diff --git a/src/client/pages/my-settings/word-mute.vue b/src/client/pages/my-settings/word-mute.vue
deleted file mode 100644
index f9bb68cd10..0000000000
--- a/src/client/pages/my-settings/word-mute.vue
+++ /dev/null
@@ -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>
diff --git a/src/client/pages/not-found.vue b/src/client/pages/not-found.vue
index 5bc4d4589a..a90a6344e4 100644
--- a/src/client/pages/not-found.vue
+++ b/src/client/pages/not-found.vue
@@ -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
+ }]
+ },
}
},
});
diff --git a/src/client/pages/note.vue b/src/client/pages/note.vue
index 3f42516323..6a9167284f 100644
--- a/src/client/pages/note.vue
+++ b/src/client/pages/note.vue
@@ -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>
diff --git a/src/client/pages/notifications.vue b/src/client/pages/notifications.vue
index 49e67bc8f7..97ed36a750 100644
--- a/src/client/pages/notifications.vue
+++ b/src/client/pages/notifications.vue
@@ -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
+ }]
+ },
};
},
diff --git a/src/client/pages/page-editor/els/page-editor.el.button.vue b/src/client/pages/page-editor/els/page-editor.el.button.vue
index 9821201666..7c65904574 100644
--- a/src/client/pages/page-editor/els/page-editor.el.button.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.button.vue
@@ -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>
diff --git a/src/client/pages/page-editor/els/page-editor.el.canvas.vue b/src/client/pages/page-editor/els/page-editor.el.canvas.vue
index a499207806..ff7e16064e 100644
--- a/src/client/pages/page-editor/els/page-editor.el.canvas.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.canvas.vue
@@ -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>
diff --git a/src/client/pages/page-editor/els/page-editor.el.counter.vue b/src/client/pages/page-editor/els/page-editor.el.counter.vue
index f439f3e6ff..ae62c2fa83 100644
--- a/src/client/pages/page-editor/els/page-editor.el.counter.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.counter.vue
@@ -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>
diff --git a/src/client/pages/page-editor/els/page-editor.el.if.vue b/src/client/pages/page-editor/els/page-editor.el.if.vue
index 53cb9e2aee..415c5ff4c0 100644
--- a/src/client/pages/page-editor/els/page-editor.el.if.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.if.vue
@@ -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: {
diff --git a/src/client/pages/page-editor/els/page-editor.el.image.vue b/src/client/pages/page-editor/els/page-editor.el.image.vue
index d26d7f603f..f5c2fe816a 100644
--- a/src/client/pages/page-editor/els/page-editor.el.image.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.image.vue
@@ -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;
});
diff --git a/src/client/pages/page-editor/els/page-editor.el.number-input.vue b/src/client/pages/page-editor/els/page-editor.el.number-input.vue
index 62d2e1bf8a..37b9ac90c3 100644
--- a/src/client/pages/page-editor/els/page-editor.el.number-input.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.number-input.vue
@@ -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>
diff --git a/src/client/pages/page-editor/els/page-editor.el.post.vue b/src/client/pages/page-editor/els/page-editor.el.post.vue
index 06dea51c1f..19c9c9d7dc 100644
--- a/src/client/pages/page-editor/els/page-editor.el.post.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.post.vue
@@ -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>
diff --git a/src/client/pages/page-editor/els/page-editor.el.radio-button.vue b/src/client/pages/page-editor/els/page-editor.el.radio-button.vue
index 34a9366d62..e30a7d363e 100644
--- a/src/client/pages/page-editor/els/page-editor.el.radio-button.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.radio-button.vue
@@ -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');
},
});
diff --git a/src/client/pages/page-editor/els/page-editor.el.section.vue b/src/client/pages/page-editor/els/page-editor.el.section.vue
index e89a8b840c..df423f0020 100644
--- a/src/client/pages/page-editor/els/page-editor.el.section.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.section.vue
@@ -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: {
diff --git a/src/client/pages/page-editor/els/page-editor.el.switch.vue b/src/client/pages/page-editor/els/page-editor.el.switch.vue
index 5055da4f6f..94ebda40b1 100644
--- a/src/client/pages/page-editor/els/page-editor.el.switch.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.switch.vue
@@ -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>
diff --git a/src/client/pages/page-editor/els/page-editor.el.text-input.vue b/src/client/pages/page-editor/els/page-editor.el.text-input.vue
index bd5fb37617..90039a3c9a 100644
--- a/src/client/pages/page-editor/els/page-editor.el.text-input.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.text-input.vue
@@ -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>
diff --git a/src/client/pages/page-editor/els/page-editor.el.text.vue b/src/client/pages/page-editor/els/page-editor.el.text.vue
index a50b1113bd..fcce180f38 100644
--- a/src/client/pages/page-editor/els/page-editor.el.text.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.text.vue
@@ -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>
diff --git a/src/client/pages/page-editor/els/page-editor.el.textarea-input.vue b/src/client/pages/page-editor/els/page-editor.el.textarea-input.vue
index 33c49c705b..ea00860fe1 100644
--- a/src/client/pages/page-editor/els/page-editor.el.textarea-input.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.textarea-input.vue
@@ -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>
diff --git a/src/client/pages/page-editor/els/page-editor.el.textarea.vue b/src/client/pages/page-editor/els/page-editor.el.textarea.vue
index e2e8848ccf..38c901d79b 100644
--- a/src/client/pages/page-editor/els/page-editor.el.textarea.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.textarea.vue
@@ -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>
diff --git a/src/client/pages/page-editor/page-editor.blocks.vue b/src/client/pages/page-editor/page-editor.blocks.vue
index 6e9408e0b7..48e7fde404 100644
--- a/src/client/pages/page-editor/page-editor.blocks.vue
+++ b/src/client/pages/page-editor/page-editor.blocks.vue
@@ -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);
},
}
});
diff --git a/src/client/pages/page-editor/page-editor.container.vue b/src/client/pages/page-editor/page-editor.container.vue
index be243b8990..6f7443599d 100644
--- a/src/client/pages/page-editor/page-editor.container.vue
+++ b/src/client/pages/page-editor/page-editor.container.vue
@@ -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;
}
diff --git a/src/client/pages/page-editor/page-editor.script-block.vue b/src/client/pages/page-editor/page-editor.script-block.vue
index f3270f02e3..edef095168 100644
--- a/src/client/pages/page-editor/page-editor.script-block.vue
+++ b/src/client/pages/page-editor/page-editor.script-block.vue
@@ -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: {
diff --git a/src/client/pages/page-editor/page-editor.vue b/src/client/pages/page-editor/page-editor.vue
index 8f09ccec33..363f46c34b 100644
--- a/src/client/pages/page-editor/page-editor.vue
+++ b/src/client/pages/page-editor/page-editor.vue
@@ -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>
+<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>
+ </header>
- <section>
- <router-link class="view" v-if="pageId" :to="`/@${ author.username }/pages/${ currentName }`"><fa :icon="faExternalLinkSquareAlt"/> {{ $t('_pages.viewPage') }}</router-link>
+ <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>
+ <MkInput v-model:value="title">
+ <span>{{ $t('_pages.title') }}</span>
+ </MkInput>
- <template v-if="showOptions">
- <mk-input v-model="summary">
- <span>{{ $t('_pages.summary') }}</span>
- </mk-input>
+ <template v-if="showOptions">
+ <MkInput v-model:value="summary">
+ <span>{{ $t('_pages.summary') }}</span>
+ </MkInput>
- <mk-input v-model="name">
- <template #prefix>{{ url }}/@{{ author.username }}/pages/</template>
- <span>{{ $t('_pages.url') }}</span>
- </mk-input>
+ <MkInput v-model:value="name">
+ <template #prefix>{{ url }}/@{{ author.username }}/pages/</template>
+ <span>{{ $t('_pages.url') }}</span>
+ </MkInput>
- <mk-switch v-model="alignCenter">{{ $t('_pages.alignCenter') }}</mk-switch>
+ <MkSwitch v-model:value="alignCenter">{{ $t('_pages.alignCenter') }}</MkSwitch>
- <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>
+ <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>
- <mk-switch v-model="hideTitleWhenPinned">{{ $t('_pages.hideTitleWhenPinned') }}</mk-switch>
+ <MkSwitch v-model:value="hideTitleWhenPinned">{{ $t('_pages.hideTitleWhenPinned') }}</MkSwitch>
- <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 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>
- </div>
- </template>
+ </template>
- <x-blocks class="content" v-model="content" :hpml="hpml"/>
+ <XBlocks class="content" v-model:value="content" :hpml="hpml"/>
- <mk-button @click="add()" v-if="!readonly"><fa :icon="faPlus"/></mk-button>
- </section>
- </div>
+ <MkButton @click="add()" v-if="!readonly"><Fa :icon="faPlus"/></MkButton>
+ </section>
+ </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>
+ <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>
- <mk-button @click="addVariable()" class="add" v-if="!readonly"><fa :icon="faPlus"/></mk-button>
- </div>
- </mk-container>
+ <MkButton @click="addVariable()" class="add" v-if="!readonly"><Fa :icon="faPlus"/></MkButton>
+ </div>
+ </MkContainer>
- <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>
+ <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>
</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 {
diff --git a/src/client/pages/page.vue b/src/client/pages/page.vue
index 093a3e5e2f..eb470fdc19 100644
--- a/src/client/pages/page.vue
+++ b/src/client/pages/page.vue
@@ -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 class="xcukqgmh" v-if="page" :key="page.id">
+ <div class="_section main">
+ <div class="_content">
+ <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">
- <x-page :page="page"/>
+ <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 class="_footer">
- <small>@{{ page.user.username }}</small>
+ </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}`">{{ $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>
+ <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>
- <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>
</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;
}
}
}
diff --git a/src/client/pages/pages.vue b/src/client/pages/pages.vue
index 9f9c68ee28..e8f364bb8d 100644
--- a/src/client/pages/pages.vue
+++ b/src/client/pages/pages.vue
@@ -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',
diff --git a/src/client/pages/preferences/index.vue b/src/client/pages/preferences/index.vue
deleted file mode 100644
index 4e171bfcbe..0000000000
--- a/src/client/pages/preferences/index.vue
+++ /dev/null
@@ -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>
diff --git a/src/client/pages/preferences/sidebar.vue b/src/client/pages/preferences/sidebar.vue
deleted file mode 100644
index 10aad0f3a0..0000000000
--- a/src/client/pages/preferences/sidebar.vue
+++ /dev/null
@@ -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>
diff --git a/src/client/pages/room/preview.vue b/src/client/pages/room/preview.vue
index 22228cf8cb..b0e600d4fb 100644
--- a/src/client/pages/room/preview.vue
+++ b/src/client/pages/room/preview.vue
@@ -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,
diff --git a/src/client/pages/room/room.vue b/src/client/pages/room/room.vue
index e20b9e2002..89e141c436 100644
--- a/src/client/pages/room/room.vue
+++ b/src/client/pages/room/room.vue
@@ -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;
}
}
diff --git a/src/client/pages/scratchpad.vue b/src/client/pages/scratchpad.vue
index d14d4452bf..7549ed32c6 100644
--- a/src/client/pages/scratchpad.vue
+++ b/src/client/pages/scratchpad.vue
@@ -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
});
diff --git a/src/client/pages/search.vue b/src/client/pages/search.vue
index c3e87c0d0c..7a395a964f 100644
--- a/src/client/pages/search.vue
+++ b/src/client/pages/search.vue
@@ -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
};
},
diff --git a/src/client/pages/settings/api.vue b/src/client/pages/settings/api.vue
new file mode 100644
index 0000000000..326ba90062
--- /dev/null
+++ b/src/client/pages/settings/api.vue
@@ -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>
diff --git a/src/client/pages/my-settings/drive.vue b/src/client/pages/settings/drive.vue
index 7612c5011f..a7d623be37 100644
--- a/src/client/pages/my-settings/drive.vue
+++ b/src/client/pages/settings/drive.vue
@@ -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 {
diff --git a/src/client/pages/settings/general.vue b/src/client/pages/settings/general.vue
new file mode 100644
index 0000000000..80152c5e6a
--- /dev/null
+++ b/src/client/pages/settings/general.vue
@@ -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>
diff --git a/src/client/pages/my-settings/import-export.vue b/src/client/pages/settings/import-export.vue
index cc148d48d4..a5a0085277 100644
--- a/src/client/pages/my-settings/import-export.vue
+++ b/src/client/pages/settings/import-export.vue
@@ -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
});
diff --git a/src/client/pages/settings/index.vue b/src/client/pages/settings/index.vue
new file mode 100644
index 0000000000..4ca30ee686
--- /dev/null
+++ b/src/client/pages/settings/index.vue
@@ -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>
diff --git a/src/client/pages/my-settings/integration.vue b/src/client/pages/settings/integration.vue
index 2d6e57e22c..4f07417160 100644
--- a/src/client/pages/my-settings/integration.vue
+++ b/src/client/pages/settings/integration.vue
@@ -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' : '');
diff --git a/src/client/pages/settings/mute-block.vue b/src/client/pages/settings/mute-block.vue
new file mode 100644
index 0000000000..5a08a8caae
--- /dev/null
+++ b/src/client/pages/settings/mute-block.vue
@@ -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>
diff --git a/src/client/pages/settings/notifications.vue b/src/client/pages/settings/notifications.vue
new file mode 100644
index 0000000000..98dc85ea52
--- /dev/null
+++ b/src/client/pages/settings/notifications.vue
@@ -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>
diff --git a/src/client/pages/settings/other.vue b/src/client/pages/settings/other.vue
new file mode 100644
index 0000000000..ebc5644162
--- /dev/null
+++ b/src/client/pages/settings/other.vue
@@ -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>
diff --git a/src/client/pages/preferences/plugins.vue b/src/client/pages/settings/plugins.vue
index 10f86de1e4..246624ddd4 100644
--- a/src/client/pages/preferences/plugins.vue
+++ b/src/client/pages/settings/plugins.vue
@@ -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', {
diff --git a/src/client/pages/settings/privacy.vue b/src/client/pages/settings/privacy.vue
new file mode 100644
index 0000000000..a92baca9d9
--- /dev/null
+++ b/src/client/pages/settings/privacy.vue
@@ -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>
diff --git a/src/client/pages/my-settings/profile.vue b/src/client/pages/settings/profile.vue
index 16bba7a270..4444b4f484 100644
--- a/src/client/pages/my-settings/profile.vue
+++ b/src/client/pages/settings/profile.vue
@@ -1,71 +1,74 @@
<template>
-<section class="llvierxe _card">
- <div class="_title"><fa :icon="faUser"/> {{ $t('profile') }}<small style="display: block; font-weight: normal; opacity: 0.6;">@{{ $store.state.i.username }}@{{ host }}</small></div>
- <div class="_content">
- <div class="header" :style="{ backgroundImage: $store.state.i.bannerUrl ? `url(${ $store.state.i.bannerUrl })` : null }" @click="changeBanner">
- <mk-avatar class="avatar" :user="$store.state.i" :disable-preview="true" :disable-link="true" @click.stop="changeAvatar"/>
- </div>
-
- <mk-input v-model="name" :max="30">
- <span>{{ $t('_profile.name') }}</span>
- </mk-input>
+<div class="_section">
+ <div class="llvierxe _card">
+ <div class="_title"><Fa :icon="faUser"/> {{ $t('profile') }}<small style="display: block; font-weight: normal; opacity: 0.6;">@{{ $store.state.i.username }}@{{ host }}</small></div>
+ <div class="_content">
+ <div class="header" :style="{ backgroundImage: $store.state.i.bannerUrl ? `url(${ $store.state.i.bannerUrl })` : null }" @click="changeBanner">
+ <MkAvatar class="avatar" :user="$store.state.i" :disable-preview="true" :disable-link="true" @click.stop="changeAvatar"/>
+ </div>
+
+ <MkInput v-model:value="name" :max="30">
+ <span>{{ $t('_profile.name') }}</span>
+ </MkInput>
- <mk-textarea v-model="description" :max="500">
- <span>{{ $t('_profile.description') }}</span>
- <template #desc>{{ $t('_profile.youCanIncludeHashtags') }}</template>
- </mk-textarea>
+ <MkTextarea v-model:value="description" :max="500">
+ <span>{{ $t('_profile.description') }}</span>
+ <template #desc>{{ $t('_profile.youCanIncludeHashtags') }}</template>
+ </MkTextarea>
- <mk-input v-model="location">
- <span>{{ $t('location') }}</span>
- <template #prefix><fa :icon="faMapMarkerAlt"/></template>
- </mk-input>
+ <MkInput v-model:value="location">
+ <span>{{ $t('location') }}</span>
+ <template #prefix><Fa :icon="faMapMarkerAlt"/></template>
+ </MkInput>
- <mk-input v-model="birthday" type="date">
- <template #title>{{ $t('birthday') }}</template>
- <template #prefix><fa :icon="faBirthdayCake"/></template>
- </mk-input>
+ <MkInput v-model:value="birthday" type="date">
+ <template #title>{{ $t('birthday') }}</template>
+ <template #prefix><Fa :icon="faBirthdayCake"/></template>
+ </MkInput>
- <details class="fields">
- <summary>{{ $t('_profile.metadata') }}</summary>
- <div class="row">
- <mk-input v-model="fieldName0">{{ $t('_profile.metadataLabel') }}</mk-input>
- <mk-input v-model="fieldValue0">{{ $t('_profile.metadataContent') }}</mk-input>
- </div>
- <div class="row">
- <mk-input v-model="fieldName1">{{ $t('_profile.metadataLabel') }}</mk-input>
- <mk-input v-model="fieldValue1">{{ $t('_profile.metadataContent') }}</mk-input>
- </div>
- <div class="row">
- <mk-input v-model="fieldName2">{{ $t('_profile.metadataLabel') }}</mk-input>
- <mk-input v-model="fieldValue2">{{ $t('_profile.metadataContent') }}</mk-input>
- </div>
- <div class="row">
- <mk-input v-model="fieldName3">{{ $t('_profile.metadataLabel') }}</mk-input>
- <mk-input v-model="fieldValue3">{{ $t('_profile.metadataContent') }}</mk-input>
- </div>
- </details>
+ <details class="fields">
+ <summary>{{ $t('_profile.metadata') }}</summary>
+ <div class="row">
+ <MkInput v-model:value="fieldName0">{{ $t('_profile.metadataLabel') }}</MkInput>
+ <MkInput v-model:value="fieldValue0">{{ $t('_profile.metadataContent') }}</MkInput>
+ </div>
+ <div class="row">
+ <MkInput v-model:value="fieldName1">{{ $t('_profile.metadataLabel') }}</MkInput>
+ <MkInput v-model:value="fieldValue1">{{ $t('_profile.metadataContent') }}</MkInput>
+ </div>
+ <div class="row">
+ <MkInput v-model:value="fieldName2">{{ $t('_profile.metadataLabel') }}</MkInput>
+ <MkInput v-model:value="fieldValue2">{{ $t('_profile.metadataContent') }}</MkInput>
+ </div>
+ <div class="row">
+ <MkInput v-model:value="fieldName3">{{ $t('_profile.metadataLabel') }}</MkInput>
+ <MkInput v-model:value="fieldValue3">{{ $t('_profile.metadataContent') }}</MkInput>
+ </div>
+ </details>
- <mk-switch v-model="isBot">{{ $t('flagAsBot') }}</mk-switch>
- <mk-switch v-model="isCat">{{ $t('flagAsCat') }}</mk-switch>
- </div>
- <div class="_footer">
- <mk-button @click="save(true)" primary><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
+ <MkSwitch v-model:value="isBot">{{ $t('flagAsBot') }}</MkSwitch>
+ <MkSwitch v-model:value="isCat">{{ $t('flagAsCat') }}</MkSwitch>
+ </div>
+ <div class="_footer">
+ <MkButton @click="save(true)" primary><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
+ </div>
</div>
-</section>
+</div>
</template>
<script lang="ts">
-import Vue from 'vue';
+import { defineComponent } from 'vue';
import { faUnlockAlt, faCogs, faUser, faMapMarkerAlt, faBirthdayCake } from '@fortawesome/free-solid-svg-icons';
import { faSave } 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';
-import MkSwitch from '../../components/ui/switch.vue';
-import { host } from '../../config';
-import { selectFile } from '../../scripts/select-file';
+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 { host } from '@/config';
+import { selectFile } from '@/scripts/select-file';
+import * as os from '@/os';
-export default Vue.extend({
+export default defineComponent({
components: {
MkButton,
MkInput,
@@ -73,8 +76,16 @@ export default Vue.extend({
MkSwitch,
},
+ emits: ['info'],
+
data() {
return {
+ INFO: {
+ header: [{
+ title: this.$t('profile'),
+ icon: faUser
+ }]
+ },
host,
name: null,
description: null,
@@ -117,18 +128,22 @@ export default Vue.extend({
this.fieldValue3 = this.$store.state.i.fields[3] ? this.$store.state.i.fields[3].value : null;
},
+ mounted() {
+ this.$emit('info', this.INFO);
+ },
+
methods: {
changeAvatar(e) {
- selectFile(this, e.currentTarget || e.target, this.$t('avatar')).then(file => {
- this.$root.api('i/update', {
+ selectFile(e.currentTarget || e.target, this.$t('avatar')).then(file => {
+ os.api('i/update', {
avatarId: file.id,
});
});
},
changeBanner(e) {
- selectFile(this, e.currentTarget || e.target, this.$t('banner')).then(file => {
- this.$root.api('i/update', {
+ selectFile(e.currentTarget || e.target, this.$t('banner')).then(file => {
+ os.api('i/update', {
bannerId: file.id,
});
});
@@ -144,7 +159,7 @@ export default Vue.extend({
this.saving = true;
- this.$root.api('i/update', {
+ os.api('i/update', {
name: this.name || null,
description: this.description || null,
location: this.location || null,
@@ -160,14 +175,11 @@ export default Vue.extend({
this.$store.state.i.bannerUrl = i.bannerUrl;
if (notify) {
- this.$root.dialog({
- type: 'success',
- iconOnly: true, autoClose: true
- });
+ os.success();
}
}).catch(err => {
this.saving = false;
- this.$root.dialog({
+ os.dialog({
type: 'error',
text: err.id
});
diff --git a/src/client/pages/settings/reaction.vue b/src/client/pages/settings/reaction.vue
new file mode 100644
index 0000000000..683cf6dfbe
--- /dev/null
+++ b/src/client/pages/settings/reaction.vue
@@ -0,0 +1,95 @@
+<template>
+<div class="_section">
+ <div class="_card">
+ <div class="_title"><Fa :icon="faLaugh"/> {{ $t('reaction') }}</div>
+ <div class="_content">
+ <MkInput v-model:value="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>
+ </MkInput>
+ <MkButton inline @click="setDefault"><Fa :icon="faUndo"/> {{ $t('default') }}</MkButton>
+ </div>
+ <div class="_footer">
+ <MkButton @click="save()" primary inline :disabled="!changed"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
+ <MkButton inline @click="preview"><Fa :icon="faEye"/> {{ $t('preview') }}</MkButton>
+ </div>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } 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 { emojiRegexWithCustom } from '../../../misc/emoji-regex';
+import { defaultSettings } from '@/store';
+import * as os from '@/os';
+
+export default defineComponent({
+ components: {
+ MkInput,
+ MkButton,
+ },
+
+ emits: ['info'],
+
+ data() {
+ return {
+ INFO: {
+ header: [{
+ title: this.$t('reaction'),
+ icon: faLaugh
+ }]
+ },
+ reactions: this.$store.state.settings.reactions.join(''),
+ changed: false,
+ faLaugh, faSave, faEye, faUndo
+ }
+ },
+
+ computed: {
+ splited(): any {
+ return this.reactions.match(emojiRegexWithCustom);
+ },
+ },
+
+ watch: {
+ reactions: {
+ handler() {
+ this.changed = true;
+ },
+ deep: true
+ }
+ },
+
+ mounted() {
+ this.$emit('info', this.INFO);
+ },
+
+ methods: {
+ save() {
+ this.$store.dispatch('settings/set', { key: 'reactions', value: this.splited });
+ this.changed = false;
+ },
+
+ async preview(ev) {
+ os.popup(await import('@/components/reaction-picker.vue'), {
+ reactions: this.splited,
+ showFocus: false,
+ src: ev.currentTarget || ev.target,
+ }, {}, 'closed');
+ },
+
+ setDefault() {
+ this.reactions = defaultSettings.reactions.join('');
+ },
+
+ async chooseEmoji(ev) {
+ os.pickEmoji(ev.currentTarget || ev.target).then(emoji => {
+ this.reactions += emoji;
+ });
+ }
+ }
+});
+</script>
diff --git a/src/client/pages/my-settings/2fa.vue b/src/client/pages/settings/security.2fa.vue
index 58ba03c41c..22b3878445 100644
--- a/src/client/pages/my-settings/2fa.vue
+++ b/src/client/pages/settings/security.2fa.vue
@@ -1,11 +1,11 @@
<template>
<section class="_card">
- <div class="_title"><fa :icon="faLock"/> {{ $t('twoStepAuthentication') }}</div>
+ <div class="_title"><Fa :icon="faLock"/> {{ $t('twoStepAuthentication') }}</div>
<div class="_content">
- <p v-if="!data && !$store.state.i.twoFactorEnabled"><mk-button @click="register">{{ $t('_2fa.registerDevice') }}</mk-button></p>
+ <MkButton v-if="!data && !$store.state.i.twoFactorEnabled" @click="register">{{ $t('_2fa.registerDevice') }}</MkButton>
<template v-if="$store.state.i.twoFactorEnabled">
<p>{{ $t('_2fa.alreadyRegistered') }}</p>
- <mk-button @click="unregister">{{ $t('unregister') }}</mk-button>
+ <MkButton @click="unregister">{{ $t('unregister') }}</MkButton>
<template v-if="supportsCredentials">
<hr class="totp-method-sep">
@@ -15,29 +15,29 @@
<div class="key-list">
<div class="key" v-for="key in $store.state.i.securityKeysList">
<h3>{{ key.name }}</h3>
- <div class="last-used">{{ $t('lastUsed') }}<mk-time :time="key.lastUsed"/></div>
- <mk-button @click="unregisterKey(key)">{{ $t('unregister') }}</mk-button>
+ <div class="last-used">{{ $t('lastUsed') }}<MkTime :time="key.lastUsed"/></div>
+ <MkButton @click="unregisterKey(key)">{{ $t('unregister') }}</MkButton>
</div>
</div>
- <mk-switch v-model="usePasswordLessLogin" @change="updatePasswordLessLogin" v-if="$store.state.i.securityKeysList.length > 0">{{ $t('passwordLessLogin') }}</mk-switch>
+ <MkSwitch v-model:value="usePasswordLessLogin" @update:value="updatePasswordLessLogin" v-if="$store.state.i.securityKeysList.length > 0">{{ $t('passwordLessLogin') }}</MkSwitch>
- <mk-info warn v-if="registration && registration.error">{{ $t('error') }} {{ registration.error }}</mk-info>
- <mk-button v-if="!registration || registration.error" @click="addSecurityKey">{{ $t('_2fa.registerKey') }}</mk-button>
+ <MkInfo warn v-if="registration && registration.error">{{ $t('error') }} {{ registration.error }}</MkInfo>
+ <MkButton v-if="!registration || registration.error" @click="addSecurityKey">{{ $t('_2fa.registerKey') }}</MkButton>
<ol v-if="registration && !registration.error">
<li v-if="registration.stage >= 0">
{{ $t('tapSecurityKey') }}
- <fa icon="spinner" pulse fixed-width v-if="registration.saving && registration.stage == 0" />
+ <Fa icon="spinner" pulse fixed-width v-if="registration.saving && registration.stage == 0" />
</li>
<li v-if="registration.stage >= 1">
- <mk-form :disabled="registration.stage != 1 || registration.saving">
- <mk-input v-model="keyName" :max="30">
+ <MkForm :disabled="registration.stage != 1 || registration.saving">
+ <MkInput v-model:value="keyName" :max="30">
<span>{{ $t('securityKeyName') }}</span>
- </mk-input>
- <mk-button @click="registerKey" :disabled="keyName.length == 0">{{ $t('registerSecurityKey') }}</mk-button>
- <fa icon="spinner" pulse fixed-width v-if="registration.saving && registration.stage == 1" />
- </mk-form>
+ </MkInput>
+ <MkButton @click="registerKey" :disabled="keyName.length == 0">{{ $t('registerSecurityKey') }}</MkButton>
+ <Fa icon="spinner" pulse fixed-width v-if="registration.saving && registration.stage == 1" />
+ </MkForm>
</li>
</ol>
</template>
@@ -45,34 +45,39 @@
<div v-if="data && !$store.state.i.twoFactorEnabled">
<ol style="margin: 0; padding: 0 0 0 1em;">
<li>
- <i18n path="_2fa.step1" tag="span">
- <a href="https://authy.com/" rel="noopener" target="_blank" place="a" class="_link">Authy</a>
- <a href="https://support.google.com/accounts/answer/1066447" rel="noopener" target="_blank" place="b" class="_link">Google Authenticator</a>
- </i18n>
+ <i18n-t keypath="_2fa.step1" tag="span">
+ <template #a>
+ <a href="https://authy.com/" rel="noopener" target="_blank" class="_link">Authy</a>
+ </template>
+ <template #b>
+ <a href="https://support.google.com/accounts/answer/1066447" rel="noopener" target="_blank" class="_link">Google Authenticator</a>
+ </template>
+ </i18n-t>
</li>
<li>{{ $t('_2fa.step2') }}<br><img :src="data.qr"></li>
<li>{{ $t('_2fa.step3') }}<br>
- <mk-input v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false">{{ $t('token') }}</mk-input>
- <mk-button primary @click="submit">{{ $t('done') }}</mk-button>
+ <MkInput v-model:value="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false">{{ $t('token') }}</MkInput>
+ <MkButton primary @click="submit">{{ $t('done') }}</MkButton>
</li>
</ol>
- <mk-info>{{ $t('_2fa.step4') }}</mk-info>
+ <MkInfo>{{ $t('_2fa.step4') }}</MkInfo>
</div>
</div>
</section>
</template>
<script lang="ts">
-import Vue from 'vue';
+import { defineComponent } from 'vue';
import { faLock } from '@fortawesome/free-solid-svg-icons';
-import { hostname } from '../../config';
-import { byteify, hexify, stringify } from '../../scripts/2fa';
-import MkButton from '../../components/ui/button.vue';
-import MkInfo from '../../components/ui/info.vue';
-import MkInput from '../../components/ui/input.vue';
-import MkSwitch from '../../components/ui/switch.vue';
+import { hostname } from '@/config';
+import { byteify, hexify, stringify } from '@/scripts/2fa';
+import MkButton from '@/components/ui/button.vue';
+import MkInfo from '@/components/ui/info.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: {
MkButton, MkInfo, MkInput, MkSwitch
},
@@ -89,14 +94,14 @@ export default Vue.extend({
},
methods: {
register() {
- this.$root.dialog({
+ os.dialog({
title: this.$t('password'),
input: {
type: 'password'
}
}).then(({ canceled, result: password }) => {
if (canceled) return;
- this.$root.api('i/2fa/register', {
+ os.api('i/2fa/register', {
password: password
}).then(data => {
this.data = data;
@@ -105,48 +110,42 @@ export default Vue.extend({
},
unregister() {
- this.$root.dialog({
+ os.dialog({
title: this.$t('password'),
input: {
type: 'password'
}
}).then(({ canceled, result: password }) => {
if (canceled) return;
- this.$root.api('i/2fa/unregister', {
+ os.api('i/2fa/unregister', {
password: password
}).then(() => {
this.usePasswordLessLogin = false;
this.updatePasswordLessLogin();
}).then(() => {
- this.$root.dialog({
- type: 'success',
- iconOnly: true, autoClose: true
- });
+ os.success();
this.$store.state.i.twoFactorEnabled = false;
});
});
},
submit() {
- this.$root.api('i/2fa/done', {
+ os.api('i/2fa/done', {
token: this.token
}).then(() => {
- this.$root.dialog({
- type: 'success',
- iconOnly: true, autoClose: true
- });
+ os.success();
this.$store.state.i.twoFactorEnabled = true;
}).catch(e => {
- this.$root.dialog({
+ os.dialog({
type: 'error',
- iconOnly: true, autoClose: true
+ text: e
});
});
},
registerKey() {
this.registration.saving = true;
- this.$root.api('i/2fa/key-done', {
+ os.api('i/2fa/key-done', {
password: this.registration.password,
name: this.keyName,
challengeId: this.registration.challengeId,
@@ -156,45 +155,39 @@ export default Vue.extend({
}).then(key => {
this.registration = null;
key.lastUsed = new Date();
- this.$root.dialog({
- type: 'success',
- iconOnly: true, autoClose: true
- });
+ os.success();
})
},
unregisterKey(key) {
- this.$root.dialog({
+ os.dialog({
title: this.$t('password'),
input: {
type: 'password'
}
}).then(({ canceled, result: password }) => {
if (canceled) return;
- return this.$root.api('i/2fa/remove-key', {
+ return os.api('i/2fa/remove-key', {
password,
credentialId: key.id
}).then(() => {
this.usePasswordLessLogin = false;
this.updatePasswordLessLogin();
}).then(() => {
- this.$root.dialog({
- type: 'success',
- iconOnly: true, autoClose: true
- });
+ os.success();
});
});
},
addSecurityKey() {
- this.$root.dialog({
+ os.dialog({
title: this.$t('password'),
input: {
type: 'password'
}
}).then(({ canceled, result: password }) => {
if (canceled) return;
- this.$root.api('i/2fa/register-key', {
+ os.api('i/2fa/register-key', {
password
}).then(registration => {
this.registration = {
@@ -233,7 +226,7 @@ export default Vue.extend({
});
},
updatePasswordLessLogin() {
- this.$root.api('i/2fa/password-less', {
+ os.api('i/2fa/password-less', {
value: !!this.usePasswordLessLogin
});
}
diff --git a/src/client/pages/settings/security.vue b/src/client/pages/settings/security.vue
new file mode 100644
index 0000000000..e56d4ae99d
--- /dev/null
+++ b/src/client/pages/settings/security.vue
@@ -0,0 +1,102 @@
+<template>
+<div>
+ <div class="_section">
+ <X2fa/>
+ </div>
+ <div class="_section">
+ <MkButton primary @click="change()" full>{{ $t('changePassword') }}</MkButton>
+ </div>
+ <div class="_section">
+ <MkButton class="_vMargin" primary @click="regenerateToken" full><Fa :icon="faSyncAlt"/> {{ $t('regenerateLoginToken') }}</MkButton>
+ <div class="_caption _vMargin" style="padding: 0 6px;">{{ $t('regenerateLoginTokenDescription') }}</div>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { faLock, faSyncAlt } from '@fortawesome/free-solid-svg-icons';
+import MkButton from '@/components/ui/button.vue';
+import X2fa from './security.2fa.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+ components: {
+ MkButton,
+ X2fa,
+ },
+
+ emits: ['info'],
+
+ data() {
+ return {
+ INFO: {
+ header: [{
+ title: this.$t('security'),
+ icon: faLock
+ }]
+ },
+ faLock, faSyncAlt
+ }
+ },
+
+ mounted() {
+ this.$emit('info', this.INFO);
+ },
+
+ methods: {
+ async change() {
+ const { canceled: canceled1, result: currentPassword } = await os.dialog({
+ title: this.$t('currentPassword'),
+ input: {
+ type: 'password'
+ }
+ });
+ if (canceled1) return;
+
+ const { canceled: canceled2, result: newPassword } = await os.dialog({
+ title: this.$t('newPassword'),
+ input: {
+ type: 'password'
+ }
+ });
+ if (canceled2) return;
+
+ const { canceled: canceled3, result: newPassword2 } = await os.dialog({
+ title: this.$t('newPasswordRetype'),
+ input: {
+ type: 'password'
+ }
+ });
+ if (canceled3) return;
+
+ if (newPassword !== newPassword2) {
+ os.dialog({
+ type: 'error',
+ text: this.$t('retypedNotMatch')
+ });
+ return;
+ }
+
+ os.apiWithDialog('i/change-password', {
+ currentPassword,
+ newPassword
+ });
+ },
+
+ regenerateToken() {
+ os.dialog({
+ title: this.$t('password'),
+ input: {
+ type: 'password'
+ }
+ }).then(({ canceled, result: password }) => {
+ if (canceled) return;
+ os.api('i/regenerate_token', {
+ password: password
+ });
+ });
+ },
+ }
+});
+</script>
diff --git a/src/client/pages/settings/sidebar.vue b/src/client/pages/settings/sidebar.vue
new file mode 100644
index 0000000000..e55899df97
--- /dev/null
+++ b/src/client/pages/settings/sidebar.vue
@@ -0,0 +1,110 @@
+<template>
+<div class="_section">
+ <div class="_card">
+ <div class="_content">
+ <MkTextarea v-model:value="items" tall>
+ <span>{{ $t('sidebar') }}</span>
+ <template #desc><button class="_textButton" @click="addItem">{{ $t('addItem') }}</button></template>
+ </MkTextarea>
+ </div>
+ <div class="_content">
+ <div>{{ $t('display') }}</div>
+ <MkRadio v-model="sidebarDisplay" value="full">{{ $t('_sidebar.full') }}</MkRadio>
+ <MkRadio v-model="sidebarDisplay" value="icon">{{ $t('_sidebar.icon') }}</MkRadio>
+ <!-- <MkRadio v-model="sidebarDisplay" value="hide" disabled>{{ $t('_sidebar.hide') }}</MkRadio>--> <!-- TODO: サイドバーを完全に隠せるようにすると、別途ハンバーガーボタンのようなものをUIに表示する必要があり面倒 -->
+ </div>
+ <div class="_footer">
+ <MkButton inline @click="save()" primary><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
+ <MkButton inline @click="reset()"><Fa :icon="faRedo"/> {{ $t('default') }}</MkButton>
+ </div>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } 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';
+import * as os from '@/os';
+import { sidebarDef } from '@/sidebar';
+
+export default defineComponent({
+ components: {
+ MkButton,
+ MkTextarea,
+ MkRadio,
+ },
+
+ emits: ['info'],
+
+ data() {
+ return {
+ INFO: {
+ header: [{
+ title: this.$t('sidebar'),
+ icon: faListUl
+ }]
+ },
+ menuDef: sidebarDef,
+ items: '',
+ 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');
+ },
+
+ mounted() {
+ this.$emit('info', this.INFO);
+ },
+
+ methods: {
+ async addItem() {
+ const menu = Object.keys(this.menuDef).filter(k => !this.$store.state.deviceUser.menu.includes(k));
+ const { canceled, result: item } = await os.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>
diff --git a/src/client/pages/settings/sounds.vue b/src/client/pages/settings/sounds.vue
new file mode 100644
index 0000000000..fc6b751fed
--- /dev/null
+++ b/src/client/pages/settings/sounds.vue
@@ -0,0 +1,152 @@
+<template>
+<div class="_section">
+ <div class="_card">
+ <div class="_title"><Fa :icon="faMusic"/> {{ $t('sounds') }}</div>
+ <div class="_content">
+ <MkRange v-model:value="sfxVolume" :min="0" :max="1" :step="0.1">
+ <Fa slot="icon" :icon="volumeIcon"/>
+ <span slot="title">{{ $t('volume') }}</span>
+ </MkRange>
+ </div>
+ <div class="_content">
+ <MkSelect v-model:value="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>
+ </MkSelect>
+ <MkSelect v-model:value="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>
+ </MkSelect>
+ <MkSelect v-model:value="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>
+ </MkSelect>
+ <MkSelect v-model:value="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>
+ </MkSelect>
+ <MkSelect v-model:value="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>
+ </MkSelect>
+ <MkSelect v-model:value="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>
+ </MkSelect>
+ <MkSelect v-model:value="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>
+ </MkSelect>
+ </div>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { faMusic, faPlay, faVolumeUp, faVolumeMute } from '@fortawesome/free-solid-svg-icons';
+import MkSelect from '@/components/ui/select.vue';
+import MkRange from '@/components/ui/range.vue';
+import * as os from '@/os';
+
+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 defineComponent({
+ components: {
+ MkSelect,
+ MkRange,
+ },
+
+ data() {
+ return {
+ sounds,
+ faMusic, faPlay, faVolumeUp, faVolumeMute,
+ }
+ },
+
+ computed: {
+ 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;
+ }
+ }
+ },
+
+ methods: {
+ listen(sound) {
+ const audio = new Audio(`/assets/sounds/${sound}.mp3`);
+ audio.volume = this.$store.state.device.sfxVolume;
+ audio.play();
+ },
+ }
+});
+</script>
diff --git a/src/client/pages/preferences/theme.vue b/src/client/pages/settings/theme.vue
index 2461504a42..0571b6c5d1 100644
--- a/src/client/pages/preferences/theme.vue
+++ b/src/client/pages/settings/theme.vue
@@ -1,105 +1,115 @@
<template>
-<section class="rfqxtzch _card">
- <div class="_title"><fa :icon="faPalette"/> {{ $t('theme') }}</div>
- <div class="_content">
- <div class="darkMode" :class="{ disabled: syncDeviceDarkMode }">
- <div class="toggleWrapper">
- <input type="checkbox" class="dn" id="dn" v-model="darkMode" :disabled="syncDeviceDarkMode"/>
- <label for="dn" class="toggle">
- <span class="before">{{ $t('light') }}</span>
- <span class="after">{{ $t('dark') }}</span>
- <span class="toggle__handler">
- <span class="crater crater--1"></span>
- <span class="crater crater--2"></span>
- <span class="crater crater--3"></span>
- </span>
- <span class="star star--1"></span>
- <span class="star star--2"></span>
- <span class="star star--3"></span>
- <span class="star star--4"></span>
- <span class="star star--5"></span>
- <span class="star star--6"></span>
- </label>
+<div class="_section">
+ <div class="rfqxtzch _card _vMargin">
+ <div class="_content">
+ <div class="darkMode" :class="{ disabled: syncDeviceDarkMode }">
+ <div class="toggleWrapper">
+ <input type="checkbox" class="dn" id="dn" v-model="darkMode" :disabled="syncDeviceDarkMode"/>
+ <label for="dn" class="toggle">
+ <span class="before">{{ $t('light') }}</span>
+ <span class="after">{{ $t('dark') }}</span>
+ <span class="toggle__handler">
+ <span class="crater crater--1"></span>
+ <span class="crater crater--2"></span>
+ <span class="crater crater--3"></span>
+ </span>
+ <span class="star star--1"></span>
+ <span class="star star--2"></span>
+ <span class="star star--3"></span>
+ <span class="star star--4"></span>
+ <span class="star star--5"></span>
+ <span class="star star--6"></span>
+ </label>
+ </div>
</div>
+ <MkSwitch v-model:value="syncDeviceDarkMode">{{ $t('syncDeviceDarkMode') }}</MkSwitch>
+ </div>
+ <div class="_content">
+ <MkSelect v-model:value="lightTheme">
+ <template #label>{{ $t('themeForLightMode') }}</template>
+ <optgroup :label="$t('lightThemes')">
+ <option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
+ </optgroup>
+ <optgroup :label="$t('darkThemes')">
+ <option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
+ </optgroup>
+ </MkSelect>
+ <MkSelect v-model:value="darkTheme">
+ <template #label>{{ $t('themeForDarkMode') }}</template>
+ <optgroup :label="$t('darkThemes')">
+ <option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
+ </optgroup>
+ <optgroup :label="$t('lightThemes')">
+ <option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
+ </optgroup>
+ </MkSelect>
+ <a href="https://assets.msky.cafe/theme/list" rel="noopener" target="_blank" class="_link">{{ $t('_theme.explore') }}</a>・<router-link to="/theme-editor" class="_link">{{ $t('_theme.make') }}</router-link>
+ </div>
+ <div class="_content">
+ <MkButton primary v-if="wallpaper == null" @click="setWallpaper">{{ $t('setWallpaper') }}</MkButton>
+ <MkButton primary v-else @click="wallpaper = null">{{ $t('removeWallpaper') }}</MkButton>
</div>
- <mk-switch v-model="syncDeviceDarkMode">{{ $t('syncDeviceDarkMode') }}</mk-switch>
- </div>
- <div class="_content">
- <mk-select v-model="lightTheme">
- <template #label>{{ $t('themeForLightMode') }}</template>
- <optgroup :label="$t('lightThemes')">
- <option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
- </optgroup>
- <optgroup :label="$t('darkThemes')">
- <option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
- </optgroup>
- </mk-select>
- <mk-select v-model="darkTheme">
- <template #label>{{ $t('themeForDarkMode') }}</template>
- <optgroup :label="$t('darkThemes')">
- <option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
- </optgroup>
- <optgroup :label="$t('lightThemes')">
- <option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
- </optgroup>
- </mk-select>
- <a href="https://assets.msky.cafe/theme/list" rel="noopener" target="_blank" class="_link">{{ $t('_theme.explore') }}</a>・<router-link to="/theme-editor" class="_link">{{ $t('_theme.make') }}</router-link>
- </div>
- <div class="_content">
- <mk-button primary v-if="wallpaper == null" @click="setWallpaper">{{ $t('setWallpaper') }}</mk-button>
- <mk-button primary v-else @click="wallpaper = null">{{ $t('removeWallpaper') }}</mk-button>
</div>
- <div class="_content">
- <details>
- <summary><fa :icon="faDownload"/> {{ $t('_theme.install') }}</summary>
- <mk-textarea v-model="installThemeCode">
+ <div class="_card _vMargin">
+ <div class="_title"><Fa :icon="faDownload"/> {{ $t('_theme.install') }}</div>
+ <div class="_content">
+ <MkTextarea v-model:value="installThemeCode">
<span>{{ $t('_theme.code') }}</span>
- </mk-textarea>
- <mk-button @click="() => install(installThemeCode)" :disabled="installThemeCode == null" primary inline><fa :icon="faCheck"/> {{ $t('install') }}</mk-button>
- <mk-button @click="() => preview(installThemeCode)" :disabled="installThemeCode == null" inline><fa :icon="faEye"/> {{ $t('preview') }}</mk-button>
- </details>
+ </MkTextarea>
+ <MkButton @click="() => install(installThemeCode)" :disabled="installThemeCode == null" primary inline><Fa :icon="faCheck"/> {{ $t('install') }}</MkButton>
+ <MkButton @click="() => preview(installThemeCode)" :disabled="installThemeCode == null" inline><Fa :icon="faEye"/> {{ $t('preview') }}</MkButton>
+ </div>
</div>
- <div class="_content">
- <details>
- <summary><fa :icon="faFolderOpen"/> {{ $t('_theme.manage') }}</summary>
- <mk-select v-model="selectedThemeId">
+ <div class="_card _vMargin">
+ <div class="_title"><Fa :icon="faFolderOpen"/> {{ $t('_theme.manage') }}</div>
+ <div class="_content">
+ <MkSelect v-model:value="selectedThemeId">
<option v-for="x in installedThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
- </mk-select>
+ </MkSelect>
<template v-if="selectedTheme">
- <mk-textarea readonly tall :value="selectedThemeCode">
+ <MkTextarea readonly tall :value="selectedThemeCode">
<span>{{ $t('_theme.code') }}</span>
<template #desc><button @click="copyThemeCode()" class="_textButton">{{ $t('copy') }}</button></template>
- </mk-textarea>
- <mk-button @click="uninstall()" v-if="!builtinThemes.some(t => t.id == selectedTheme.id)"><fa :icon="faTrashAlt"/> {{ $t('uninstall') }}</mk-button>
+ </MkTextarea>
+ <MkButton @click="uninstall()" v-if="!builtinThemes.some(t => t.id == selectedTheme.id)"><Fa :icon="faTrashAlt"/> {{ $t('uninstall') }}</MkButton>
</template>
- </details>
+ </div>
</div>
-</section>
+</div>
</template>
<script lang="ts">
-import Vue from 'vue';
+import { defineComponent } from 'vue';
import { faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye } from '@fortawesome/free-solid-svg-icons';
import * as JSON5 from 'json5';
-import MkButton from '../../components/ui/button.vue';
-import MkSelect from '../../components/ui/select.vue';
-import MkSwitch from '../../components/ui/switch.vue';
-import MkTextarea from '../../components/ui/textarea.vue';
-import { Theme, builtinThemes, applyTheme, validateTheme } from '../../scripts/theme';
-import { selectFile } from '../../scripts/select-file';
-import { isDeviceDarkmode } from '../../scripts/is-device-darkmode';
-import copyToClipboard from '../../scripts/copy-to-clipboard';
+import MkButton from '@/components/ui/button.vue';
+import MkSelect from '@/components/ui/select.vue';
+import MkSwitch from '@/components/ui/switch.vue';
+import MkTextarea from '@/components/ui/textarea.vue';
+import { Theme, builtinThemes, applyTheme, validateTheme } from '@/scripts/theme';
+import { selectFile } from '@/scripts/select-file';
+import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
+import copyToClipboard from '@/scripts/copy-to-clipboard';
+import * as os from '@/os';
-export default Vue.extend({
+export default defineComponent({
components: {
MkButton,
MkSelect,
MkSwitch,
MkTextarea,
},
+
+ emits: ['info'],
data() {
return {
+ INFO: {
+ header: [{
+ title: this.$t('theme'),
+ icon: faPalette
+ }]
+ },
builtinThemes,
installThemeCode: null,
selectedThemeId: null,
@@ -185,19 +195,20 @@ export default Vue.extend({
}
},
+ mounted() {
+ this.$emit('info', this.INFO);
+ },
+
methods: {
setWallpaper(e) {
- selectFile(this, e.currentTarget || e.target, null, false).then(file => {
+ selectFile(e.currentTarget || e.target, null, false).then(file => {
this.wallpaper = file.url;
});
},
copyThemeCode() {
copyToClipboard(this.selectedThemeCode);
- this.$root.dialog({
- type: 'success',
- iconOnly: true, autoClose: true
- });
+ os.success();
},
parseThemeCode(code) {
@@ -206,21 +217,21 @@ export default Vue.extend({
try {
theme = JSON5.parse(code);
} catch (e) {
- this.$root.dialog({
+ os.dialog({
type: 'error',
text: this.$t('_theme.invalid')
});
return false;
}
if (!validateTheme(theme)) {
- this.$root.dialog({
+ os.dialog({
type: 'error',
text: this.$t('_theme.invalid')
});
return false;
}
if (this.$store.state.device.themes.some(t => t.id === theme.id)) {
- this.$root.dialog({
+ os.dialog({
type: 'info',
text: this.$t('_theme.alreadyInstalled')
});
@@ -242,7 +253,7 @@ export default Vue.extend({
this.$store.commit('device/set', {
key: 'themes', value: themes
});
- this.$root.dialog({
+ os.dialog({
type: 'success',
text: this.$t('_theme.installed', { name: theme.name })
});
@@ -254,10 +265,7 @@ export default Vue.extend({
this.$store.commit('device/set', {
key: 'themes', value: themes
});
- this.$root.dialog({
- type: 'success',
- iconOnly: true, autoClose: true
- });
+ os.success();
},
}
});
diff --git a/src/client/pages/settings/word-mute.vue b/src/client/pages/settings/word-mute.vue
new file mode 100644
index 0000000000..a517536a1c
--- /dev/null
+++ b/src/client/pages/settings/word-mute.vue
@@ -0,0 +1,101 @@
+<template>
+<div class="_section">
+ <div class="_card">
+ <MkTab v-model:value="tab" :items="[{ label: $t('_wordMute.soft'), value: 'soft' }, { label: $t('_wordMute.hard'), value: 'hard' }]"/>
+ <div class="_content">
+ <div v-show="tab === 'soft'">
+ <MkInfo>{{ $t('_wordMute.softDescription') }}</MkInfo>
+ <MkTextarea v-model:value="softMutedWords">
+ <span>{{ $t('_wordMute.muteWords') }}</span>
+ <template #desc>{{ $t('_wordMute.muteWordsDescription') }}<br>{{ $t('_wordMute.muteWordsDescription2') }}</template>
+ </MkTextarea>
+ </div>
+ <div v-show="tab === 'hard'">
+ <MkInfo>{{ $t('_wordMute.hardDescription') }}</MkInfo>
+ <MkTextarea v-model:value="hardMutedWords" style="margin-bottom: 16px;">
+ <span>{{ $t('_wordMute.muteWords') }}</span>
+ <template #desc>{{ $t('_wordMute.muteWordsDescription') }}<br>{{ $t('_wordMute.muteWordsDescription2') }}</template>
+ </MkTextarea>
+ <div v-if="hardWordMutedNotesCount != null" class="_caption">{{ $t('_wordMute.mutedNotes') }}: {{ hardWordMutedNotesCount | number }}</div>
+ </div>
+ </div>
+ <div class="_footer">
+ <MkButton @click="save()" primary inline :disabled="!changed"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
+ </div>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } 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';
+import * as os from '@/os';
+
+export default defineComponent({
+ components: {
+ MkButton,
+ MkTextarea,
+ MkTab,
+ MkInfo,
+ },
+
+ emits: ['info'],
+
+ data() {
+ return {
+ INFO: {
+ header: [{
+ title: this.$t('wordMute'),
+ icon: faCommentSlash
+ }]
+ },
+ tab: 'soft',
+ softMutedWords: '',
+ hardMutedWords: '',
+ hardWordMutedNotesCount: null,
+ changed: false,
+ faSave,
+ }
+ },
+
+ watch: {
+ softMutedWords: {
+ handler() {
+ this.changed = true;
+ },
+ deep: true
+ },
+ hardMutedWords: {
+ handler() {
+ this.changed = true;
+ },
+ deep: 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 os.api('i/get-word-muted-notes-count', {})).count;
+ },
+
+ mounted() {
+ this.$emit('info', this.INFO);
+ },
+
+ methods: {
+ async save() {
+ this.$store.dispatch('settings/set', { key: 'mutedWords', value: this.softMutedWords.trim().split('\n').map(x => x.trim().split(' ')) });
+ await os.api('i/update', {
+ mutedWords: this.hardMutedWords.trim().split('\n').map(x => x.trim().split(' ')),
+ });
+ this.changed = false;
+ },
+ }
+});
+</script>
diff --git a/src/client/pages/share.vue b/src/client/pages/share.vue
index 153de76801..dd1e82dedb 100644
--- a/src/client/pages/share.vue
+++ b/src/client/pages/share.vue
@@ -1,14 +1,10 @@
<template>
<div class="">
- <portal to="icon"><fa :icon="faShareAlt"/></portal>
- <portal to="title">{{ $t('share') }}</portal>
-
- <section class="_card">
+ <section class="_section">
<div class="_title" v-if="title">{{ title }}</div>
<div class="_content">
- <div>{{ text }}</div>
- <mk-button @click="post()" v-if="!posted">{{ $t('post') }}</mk-button>
- <mk-button primary @click="close()" v-else>{{ $t('close') }}</mk-button>
+ <XPostForm v-if="!posted" fixed :instant="true" :initial-text="initialText" @posted="posted = true" class="_panel"/>
+ <MkButton v-else primary @click="close()">{{ $t('close') }}</MkButton>
</div>
<div class="_footer" v-if="url">{{ url }}</div>
</section>
@@ -16,27 +12,30 @@
</template>
<script lang="ts">
-import Vue from 'vue';
+import { defineComponent } from 'vue';
import { faShareAlt } from '@fortawesome/free-solid-svg-icons';
-import PostFormDialog from '../components/post-form-dialog.vue';
-import MkButton from '../components/ui/button.vue';
-
-export default Vue.extend({
- metaInfo() {
- return {
- title: this.$t('share') as string
- };
- },
+import MkButton from '@/components/ui/button.vue';
+import XPostForm from '@/components/post-form.vue';
+import * as os from '@/os';
+export default defineComponent({
components: {
- MkButton
+ XPostForm,
+ MkButton,
},
data() {
return {
+ INFO: {
+ header: [{
+ title: this.$t('share'),
+ icon: faShareAlt
+ }],
+ },
title: null,
text: null,
url: null,
+ initialText: null,
posted: false,
faShareAlt
@@ -48,29 +47,15 @@ export default Vue.extend({
this.title = urlParams.get('title');
this.text = urlParams.get('text');
this.url = urlParams.get('url');
- },
-
- mounted() {
- this.post();
+
+ let text = '';
+ if (this.title) text += `【${this.title}】\n`;
+ if (this.text) text += `${this.text}\n`;
+ if (this.url) text += `${this.url}`;
+ this.initialText = text.trim();
},
methods: {
- post() {
- let text = '';
- if (this.title) text += `【${this.title}】\n`;
- if (this.text) text += `${this.text}\n`;
- if (this.url) text += `${this.url}`;
- this.$root.new(PostFormDialog, {
- instant: true,
- initialText: text.trim()
- }).$once('posted', () => {
- this.posted = true;
- this.$root.dialog({
- type: 'success',
- iconOnly: true, autoClose: true
- });
- });
- },
close() {
window.close()
}
diff --git a/src/client/pages/tag.vue b/src/client/pages/tag.vue
index 81a96960f7..cea74d1e17 100644
--- a/src/client/pages/tag.vue
+++ b/src/client/pages/tag.vue
@@ -1,31 +1,28 @@
<template>
-<div>
- <portal to="icon"><fa :icon="faHashtag"/></portal>
- <portal to="title">{{ $route.params.tag }}</portal>
-
- <x-notes ref="notes" :pagination="pagination" @before="before" @after="after"/>
+<div class="_section">
+ <XNotes ref="notes" class="_content" :pagination="pagination" @before="before" @after="after"/>
</div>
</template>
<script lang="ts">
-import Vue from 'vue';
+import { defineComponent } from 'vue';
import { faHashtag } 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.$route.params.tag
- };
- },
+import Progress from '@/scripts/loading';
+import XNotes from '@/components/notes.vue';
+export default defineComponent({
components: {
XNotes
},
data() {
return {
+ INFO: {
+ header: [{
+ title: this.$route.params.tag,
+ icon: faHashtag
+ }],
+ },
pagination: {
endpoint: 'notes/search-by-tag',
limit: 10,
diff --git a/src/client/pages/test.vue b/src/client/pages/test.vue
new file mode 100644
index 0000000000..02b4d1614d
--- /dev/null
+++ b/src/client/pages/test.vue
@@ -0,0 +1,232 @@
+<template>
+<div class="_section">
+ <div class="_content">
+ <div class="_card _vMargin">
+ <div class="_title">Dialog</div>
+ <div class="_content">
+ <MkInput v-model:value="dialogTitle">
+ <span>Title</span>
+ </MkInput>
+ <MkInput v-model:value="dialogBody">
+ <span>Body</span>
+ </MkInput>
+ <MkSwitch v-model:value="dialogCancel">
+ <span>With cancel button</span>
+ </MkSwitch>
+ <MkSwitch v-model:value="dialogCancelByBgClick">
+ <span>Can cancel by modal bg click</span>
+ </MkSwitch>
+ <MkSwitch v-model:value="dialogInput">
+ <span>With input field</span>
+ </MkSwitch>
+ <MkButton @click="showDialog()">Show</MkButton>
+ </div>
+ <div class="_content">
+ <code>Result: {{ dialogResult }}</code>
+ </div>
+ </div>
+
+ <div class="_card _vMargin">
+ <div class="_title">Form</div>
+ <div class="_content">
+ <MkInput v-model:value="formTitle">
+ <span>Title</span>
+ </MkInput>
+ <MkTextarea v-model:value="formForm">
+ <span>Form</span>
+ </MkTextarea>
+ <MkButton @click="form()">Show</MkButton>
+ </div>
+ <div class="_content">
+ <code>Result: {{ formResult }}</code>
+ </div>
+ </div>
+
+ <div class="_card _vMargin">
+ <div class="_title">MFM</div>
+ <div class="_content">
+ <MkTextarea v-model:value="mfm">
+ <span>MFM</span>
+ </MkTextarea>
+ </div>
+ <div class="_content">
+ <Mfm :text="mfm"/>
+ </div>
+ </div>
+
+ <div class="_card _vMargin">
+ <div class="_title">selectDriveFile</div>
+ <div class="_content">
+ <MkSwitch v-model:value="selectDriveFileMultiple">
+ <span>Multiple</span>
+ </MkSwitch>
+ <MkButton @click="selectDriveFile()">selectDriveFile</MkButton>
+ </div>
+ <div class="_content">
+ <code>Result: {{ JSON.stringify(selectDriveFileResult) }}</code>
+ </div>
+ </div>
+
+ <div class="_card _vMargin">
+ <div class="_title">selectDriveFolder</div>
+ <div class="_content">
+ <MkSwitch v-model:value="selectDriveFolderMultiple">
+ <span>Multiple</span>
+ </MkSwitch>
+ <MkButton @click="selectDriveFolder()">selectDriveFolder</MkButton>
+ </div>
+ <div class="_content">
+ <code>Result: {{ JSON.stringify(selectDriveFolderResult) }}</code>
+ </div>
+ </div>
+
+ <div class="_card _vMargin">
+ <div class="_title">selectUser</div>
+ <div class="_content">
+ <MkButton @click="selectUser()">selectUser</MkButton>
+ </div>
+ <div class="_content">
+ <code>Result: {{ user }}</code>
+ </div>
+ </div>
+
+ <div class="_card _vMargin">
+ <div class="_title">Notification</div>
+ <div class="_content">
+ <MkInput v-model:value="notificationIconUrl">
+ <span>Icon URL</span>
+ </MkInput>
+ <MkInput v-model:value="notificationHeader">
+ <span>Header</span>
+ </MkInput>
+ <MkTextarea v-model:value="notificationBody">
+ <span>Body</span>
+ </MkTextarea>
+ <MkButton @click="createNotification()">createNotification</MkButton>
+ </div>
+ </div>
+
+ <div class="_card _vMargin">
+ <div class="_title">Messaging window</div>
+ <div class="_content">
+ <MkButton @click="messagingWindowOpen()">open</MkButton>
+ </div>
+ </div>
+
+ <MkButton @click="resetTutorial()">Reset tutorial</MkButton>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent } from 'vue';
+import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
+import MkButton from '@/components/ui/button.vue';
+import MkInput from '@/components/ui/input.vue';
+import MkSwitch from '@/components/ui/switch.vue';
+import MkTextarea from '@/components/ui/textarea.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+ components: {
+ MkButton,
+ MkInput,
+ MkSwitch,
+ MkTextarea,
+ },
+
+ data() {
+ return {
+ INFO: {
+ header: [{
+ title: 'TEST',
+ icon: faExclamationTriangle
+ }]
+ },
+ dialogTitle: 'Hello',
+ dialogBody: 'World!',
+ dialogCancel: false,
+ dialogCancelByBgClick: true,
+ dialogInput: false,
+ dialogResult: null,
+ formTitle: null,
+ formForm: JSON.stringify({
+ foo: {
+ type: 'boolean',
+ default: true,
+ label: 'This is a boolean property'
+ },
+ bar: {
+ type: 'number',
+ default: 300,
+ label: 'This is a number property'
+ },
+ baz: {
+ type: 'string',
+ default: 'Misskey makes you happy.',
+ label: 'This is a string property'
+ },
+ }, null, '\t'),
+ formResult: null,
+ mfm: '',
+ selectDriveFileMultiple: false,
+ selectDriveFolderMultiple: false,
+ selectDriveFileResult: null,
+ selectDriveFolderResult: null,
+ user: null,
+ notificationIconUrl: null,
+ notificationHeader: '',
+ notificationBody: '',
+ }
+ },
+
+ methods: {
+ async showDialog() {
+ this.dialogResult = null;
+ this.dialogResult = await os.dialog({
+ title: this.dialogTitle,
+ text: this.dialogBody,
+ showCancelButton: this.dialogCancel,
+ cancelableByBgClick: this.dialogCancelByBgClick,
+ input: this.dialogInput ? {} : null
+ });
+ },
+
+ async form() {
+ this.formResult = null;
+ this.formResult = await os.form(this.formTitle, JSON.parse(this.formForm));
+ },
+
+ async selectDriveFile() {
+ this.selectDriveFileResult = null;
+ this.selectDriveFileResult = await os.selectDriveFile(this.selectDriveFileMultiple);
+ },
+
+ async selectDriveFolder() {
+ this.selectDriveFolderResult = null;
+ this.selectDriveFolderResult = await os.selectDriveFolder(this.selectDriveFolderMultiple);
+ },
+
+ async selectUser() {
+ this.user = null;
+ this.user = await os.selectUser();
+ },
+
+ async createNotification() {
+ os.api('notifications/create', {
+ header: this.notificationHeader,
+ body: this.notificationBody,
+ icon: this.notificationIconUrl,
+ });
+ },
+
+ messagingWindowOpen() {
+ os.pageWindow('/my/messaging', defineAsyncComponent(() => import('@/pages/messaging/index.vue')));
+ },
+
+ resetTutorial() {
+ this.$store.dispatch('settings/set', { key: 'tutorial', value: 0 });
+ },
+ }
+});
+</script>
diff --git a/src/client/pages/theme-editor.vue b/src/client/pages/theme-editor.vue
index 2ad95c065e..5b59d025d9 100644
--- a/src/client/pages/theme-editor.vue
+++ b/src/client/pages/theme-editor.vue
@@ -1,21 +1,31 @@
<template>
<div class="t9makv94">
- <portal to="icon"><fa :icon="faPalette"/></portal>
- <portal to="title">{{ $t('themeEditor') }}</portal>
-
- <section class="_card">
+ <section class="_section">
<div class="_content">
- <mk-input v-model="name" required><span>{{ $t('name') }}</span></mk-input>
- <mk-input v-model="author" required><span>{{ $t('author') }}</span></mk-input>
- <mk-textarea v-model="description"><span>{{ $t('description') }}</span></mk-textarea>
- <div class="_inputs">
- <div v-text="$t('_theme.base')" />
- <mk-radio v-model="baseTheme" value="light">{{ $t('light') }}</mk-radio>
- <mk-radio v-model="baseTheme" value="dark">{{ $t('dark') }}</mk-radio>
+ <details>
+ <summary>{{ $t('import') }}</summary>
+ <MkTextarea v-model:value="themeToImport">
+ {{ $t('_theme.importInfo') }}
+ </MkTextarea>
+ <MkButton :disabled="!themeToImport.trim()" @click="importTheme">{{ $t('import') }}</MkButton>
+ </details>
+ </div>
+ </section>
+ <section class="_section">
+ <div class="_content _card _vMargin">
+ <div class="_content">
+ <MkInput v-model:value="name" required><span>{{ $t('name') }}</span></MkInput>
+ <MkInput v-model:value="author" required><span>{{ $t('author') }}</span></MkInput>
+ <MkTextarea v-model:value="description"><span>{{ $t('description') }}</span></MkTextarea>
+ <div class="_inputs">
+ <div v-text="$t('_theme.base')" />
+ <MkRadio v-model="baseTheme" value="light">{{ $t('light') }}</MkRadio>
+ <MkRadio v-model="baseTheme" value="dark">{{ $t('dark') }}</MkRadio>
+ </div>
</div>
</div>
- <div class="_content">
- <div class="list-view">
+ <div class="_content _card _vMargin">
+ <div class="list-view _content">
<div class="item" v-for="([ k, v ], i) in theme" :key="k">
<div class="_inputs">
<div>
@@ -24,73 +34,74 @@
</div>
<div>
<div class="type" @click="chooseType($event, i)">
- {{ getTypeOf(v) }} <fa :icon="faChevronDown"/>
+ {{ getTypeOf(v) }} <Fa :icon="faChevronDown"/>
</div>
<!-- default -->
<div v-if="v === null" v-text="baseProps[k]" class="default-value" />
<!-- color -->
<div v-else-if="typeof v === 'string'" class="color">
<input type="color" :value="v" @input="colorChanged($event.target.value, i)"/>
- <mk-input class="select" :value="v" @input="colorChanged($event, i)"/>
+ <MkInput class="select" :value="v" @update:value="colorChanged($event, i)"/>
</div>
<!-- ref const -->
- <mk-input v-else-if="v.type === 'refConst'" v-model="v.key">
+ <MkInput v-else-if="v.type === 'refConst'" v-model:value="v.key">
<template #prefix>$</template>
<span>{{ $t('name') }}</span>
- </mk-input>
+ </MkInput>
<!-- ref props -->
- <mk-select class="select" v-else-if="v.type === 'refProp'" v-model="v.key">
+ <MkSelect class="select" v-else-if="v.type === 'refProp'" v-model:value="v.key">
<option v-for="key in themeProps" :value="key" :key="key">{{ $t('_theme.keys.' + key) }}</option>
- </mk-select>
+ </MkSelect>
<!-- func -->
<template v-else-if="v.type === 'func'">
- <mk-select class="select" v-model="v.name">
+ <MkSelect class="select" v-model:value="v.name">
<template #label>{{ $t('_theme.funcKind') }}</template>
<option v-for="n in ['alpha', 'darken', 'lighten']" :value="n" :key="n">{{ $t('_theme.' + n) }}</option>
- </mk-select>
- <mk-input type="number" v-model="v.arg"><span>{{ $t('_theme.argument') }}</span></mk-input>
- <mk-select class="select" v-model="v.value">
+ </MkSelect>
+ <MkInput type="number" v-model:value="v.arg"><span>{{ $t('_theme.argument') }}</span></MkInput>
+ <MkSelect class="select" v-model:value="v.value">
<template #label>{{ $t('_theme.basedProp') }}</template>
<option v-for="key in themeProps" :value="key" :key="key">{{ $t('_theme.keys.' + key) }}</option>
- </mk-select>
+ </MkSelect>
</template>
+ <!-- CSS -->
+ <MkInput v-else-if="v.type === 'css'" v-model:value="v.value">
+ <span>CSS</span>
+ </MkInput>
</div>
</div>
</div>
- <mk-button primary @click="addConst">{{ $t('_theme.addConstant') }}</mk-button>
+ <MkButton primary @click="addConst">{{ $t('_theme.addConstant') }}</MkButton>
</div>
</div>
+ </section>
+ <section class="_section">
<div class="_content">
- <mk-textarea v-model="themeToImport">
- {{ $t('_theme.importInfo') }}
- </mk-textarea>
- <mk-button :disabled="!themeToImport.trim()" @click="importTheme">{{ $t('import') }}</mk-button>
- </div>
- <div class="_footer">
- <mk-button inline @click="preview">{{ $t('preview') }}</mk-button>
- <mk-button inline primary :disabled="!name || !author" @click="save">{{ $t('save') }}</mk-button>
+ <MkButton inline @click="preview">{{ $t('preview') }}</MkButton>
+ <MkButton inline primary :disabled="!name || !author" @click="save">{{ $t('save') }}</MkButton>
</div>
</section>
</div>
</template>
<script lang="ts">
-import Vue from 'vue';
+import { defineComponent } from 'vue';
import { faPalette, faChevronDown, faKeyboard } from '@fortawesome/free-solid-svg-icons';
import * as JSON5 from 'json5';
-import MkRadio from '../components/ui/radio.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 MkRadio from '@/components/ui/radio.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 { convertToMisskeyTheme, ThemeValue, convertToViewModel, ThemeViewModel } from '../scripts/theme-editor';
-import { Theme, applyTheme, lightTheme, darkTheme, themeProps, validateTheme } from '../scripts/theme';
+import { convertToMisskeyTheme, ThemeValue, convertToViewModel, ThemeViewModel } from '@/scripts/theme-editor';
+import { Theme, applyTheme, lightTheme, darkTheme, themeProps, validateTheme } from '@/scripts/theme';
import { toUnicode } from 'punycode';
-import { host } from '../config';
+import { host } from '@/config';
+import * as os from '@/os';
-export default Vue.extend({
+export default defineComponent({
components: {
MkRadio,
MkButton,
@@ -98,14 +109,15 @@ export default Vue.extend({
MkTextarea,
MkSelect
},
- metaInfo() {
- return {
- title: this.$t('themeEditor') + (this.changed ? '*' : '')
- };
- },
data() {
return {
+ INFO: {
+ header: [{
+ title: this.$t('themeEditor'),
+ icon: faPalette,
+ }],
+ },
theme: [] as ThemeViewModel,
name: '',
description: '',
@@ -113,8 +125,8 @@ export default Vue.extend({
author: `@${this.$store.state.i.username}@${toUnicode(host)}`,
themeToImport: '',
changed: false,
- faPalette, faChevronDown, faKeyboard,
lightTheme, darkTheme, themeProps,
+ faPalette, faChevronDown, faKeyboard,
}
},
@@ -124,7 +136,7 @@ export default Vue.extend({
},
},
- beforeDestroy() {
+ beforeUnmount() {
window.removeEventListener('beforeunload', this.beforeunload);
},
@@ -156,7 +168,7 @@ export default Vue.extend({
},
async confirm(): Promise<boolean> {
- const { canceled } = await this.$root.dialog({
+ const { canceled } = await os.dialog({
type: 'warning',
text: this.$t('leaveConfirm'),
showCancelButton: true
@@ -173,7 +185,7 @@ export default Vue.extend({
},
async del(i: number) {
- const { canceled } = await this.$root.dialog({
+ const { canceled } = await os.dialog({
type: 'warning',
showCancelButton: true,
text: this.$t('_theme.deleteConstantConfirm', { const: this.theme[i][0] }),
@@ -183,7 +195,7 @@ export default Vue.extend({
},
async addConst() {
- const { canceled, result } = await this.$root.dialog({
+ const { canceled, result } = await os.dialog({
title: this.$t('_theme.inputConstantName'),
input: true
});
@@ -197,7 +209,7 @@ export default Vue.extend({
this.$store.commit('device/set', {
key: 'themes', value: themes
});
- this.$root.dialog({
+ os.dialog({
type: 'success',
text: this.$t('_theme.installed', { name: theme.name })
});
@@ -209,7 +221,7 @@ export default Vue.extend({
try {
applyTheme(theme, false);
} catch (e) {
- this.$root.dialog({
+ os.dialog({
type: 'error',
text: e.message
});
@@ -230,7 +242,7 @@ export default Vue.extend({
this.theme = convertToViewModel(theme);
this.themeToImport = '';
} catch (e) {
- this.$root.dialog({
+ os.dialog({
type: 'error',
text: e.message
});
@@ -238,9 +250,9 @@ export default Vue.extend({
},
colorChanged(color: string, i: number) {
- Vue.set(this.theme, i, [this.theme[i][0], color]);
+ this.theme[i] = [this.theme[i][0], color];
},
-
+
getTypeOf(v: ThemeValue) {
return v === null
? this.$t('_theme.defaultValue')
@@ -251,36 +263,38 @@ export default Vue.extend({
async chooseType(e: MouseEvent, i: number) {
const newValue = await this.showTypeMenu(e);
- Vue.set(this.theme, i, [ this.theme[i][0], newValue ]);
+ this.theme[i] = [ this.theme[i][0], newValue ];
},
showTypeMenu(e: MouseEvent) {
return new Promise<ThemeValue>((resolve) => {
- this.$root.menu({
- items: [{
- text: this.$t('_theme.defaultValue'),
- action: () => resolve(null),
- }, {
- text: this.$t('_theme.color'),
- action: () => resolve('#000000'),
- }, {
- text: this.$t('_theme.func'),
- action: () => resolve({
- type: 'func', name: 'alpha', arg: 1, value: 'accent'
- }),
- }, {
- text: this.$t('_theme.refProp'),
- action: () => resolve({
- type: 'refProp', key: 'accent',
- }),
- }, {
- text: this.$t('_theme.refConst'),
- action: () => resolve({
- type: 'refConst', key: '',
- }),
- },],
- source: e.currentTarget || e.target,
- });
+ os.modalMenu([{
+ text: this.$t('_theme.defaultValue'),
+ action: () => resolve(null),
+ }, {
+ text: this.$t('_theme.color'),
+ action: () => resolve('#000000'),
+ }, {
+ text: this.$t('_theme.func'),
+ action: () => resolve({
+ type: 'func', name: 'alpha', arg: 1, value: 'accent'
+ }),
+ }, {
+ text: this.$t('_theme.refProp'),
+ action: () => resolve({
+ type: 'refProp', key: 'accent',
+ }),
+ }, {
+ text: this.$t('_theme.refConst'),
+ action: () => resolve({
+ type: 'refConst', key: '',
+ }),
+ }, {
+ text: 'CSS',
+ action: () => resolve({
+ type: 'css', value: '',
+ }),
+ }], e.currentTarget || e.target);
});
}
}
@@ -289,20 +303,15 @@ export default Vue.extend({
<style lang="scss" scoped>
.t9makv94 {
- > ._card {
+ > ._section {
> ._content {
> .list-view {
- height: 480px;
- overflow: auto;
- border: 1px solid var(--divider);
-
> .item {
min-height: 48px;
- padding: 0 16px;
word-break: break-all;
&:not(:last-child) {
- padding-bottom: 8px;
+ margin-bottom: 8px;
}
.select {
@@ -332,10 +341,6 @@ export default Vue.extend({
}
}
}
-
- > ._button {
- margin: 16px;
- }
}
}
}
diff --git a/src/client/pages/index.home.tutorial.vue b/src/client/pages/timeline.tutorial.vue
index dc255bf6a3..506e97e1b5 100644
--- a/src/client/pages/index.home.tutorial.vue
+++ b/src/client/pages/timeline.tutorial.vue
@@ -1,6 +1,6 @@
<template>
<div class="_card tbkwesmv">
- <div class="_title"><fa :icon="faInfoCircle"/> {{ $t('_tutorial.title') }}</div>
+ <div class="_title"><Fa :icon="faInfoCircle"/> {{ $t('_tutorial.title') }}</div>
<div class="_content" v-if="tutorial === 0">
<div>{{ $t('_tutorial.step1_1') }}</div>
<div>{{ $t('_tutorial.step1_2') }}</div>
@@ -9,7 +9,7 @@
<div class="_content" v-else-if="tutorial === 1">
<div>{{ $t('_tutorial.step2_1') }}</div>
<div>{{ $t('_tutorial.step2_2') }}</div>
- <router-link class="_link" to="/my/settings">{{ $t('editProfile') }}</router-link>
+ <router-link class="_link" to="/settings/profile">{{ $t('editProfile') }}</router-link>
</div>
<div class="_content" v-else-if="tutorial === 2">
<div>{{ $t('_tutorial.step3_1') }}</div>
@@ -23,10 +23,14 @@
</div>
<div class="_content" v-else-if="tutorial === 4">
<div>{{ $t('_tutorial.step5_1') }}</div>
- <i18n path="_tutorial.step5_2" tag="div">
- <router-link class="_link" place="featured" to="/featured">{{ $t('featured') }}</router-link>
- <router-link class="_link" place="explore" to="/explore">{{ $t('explore') }}</router-link>
- </i18n>
+ <i18n-t keypath="_tutorial.step5_2" tag="div">
+ <template #featured>
+ <router-link class="_link" to="/featured">{{ $t('featured') }}</router-link>
+ </template>
+ <template #explore>
+ <router-link class="_link" to="/explore">{{ $t('explore') }}</router-link>
+ </template>
+ </i18n-t>
<div>{{ $t('_tutorial.step5_3') }}</div>
<small>{{ $t('_tutorial.step5_4') }}</small>
</div>
@@ -37,34 +41,36 @@
</div>
<div class="_content" v-else-if="tutorial === 6">
<div>{{ $t('_tutorial.step7_1') }}</div>
- <i18n path="_tutorial.step7_2" tag="div">
- <router-link class="_link" place="help" to="/docs">{{ $t('help') }}</router-link>
- </i18n>
+ <i18n-t keypath="_tutorial.step7_2" tag="div">
+ <template #help>
+ <router-link class="_link" to="/docs">{{ $t('help') }}</router-link>
+ </template>
+ </i18n-t>
<div>{{ $t('_tutorial.step7_3') }}</div>
</div>
<div class="_footer navigation">
<div class="step">
<button class="arrow _button" @click="tutorial--" :disabled="tutorial === 0">
- <fa :icon="faChevronLeft"/>
+ <Fa :icon="faChevronLeft"/>
</button>
<span>{{ tutorial + 1 }} / 7</span>
<button class="arrow _button" @click="tutorial++" :disabled="tutorial === 6">
- <fa :icon="faChevronRight"/>
+ <Fa :icon="faChevronRight"/>
</button>
</div>
- <mk-button class="ok" @click="tutorial = -1" primary v-if="tutorial === 6"><fa :icon="faCheck"/> {{ $t('gotIt') }}</mk-button>
- <mk-button class="ok" @click="tutorial++" primary v-else><fa :icon="faCheck"/> {{ $t('next') }}</mk-button>
+ <MkButton class="ok" @click="tutorial = -1" primary v-if="tutorial === 6"><Fa :icon="faCheck"/> {{ $t('gotIt') }}</MkButton>
+ <MkButton class="ok" @click="tutorial++" primary v-else><Fa :icon="faCheck"/> {{ $t('next') }}</MkButton>
</div>
</div>
</template>
<script lang="ts">
-import Vue from 'vue';
+import { defineComponent } from 'vue';
import { faInfoCircle, faChevronLeft, faChevronRight, faCheck } from '@fortawesome/free-solid-svg-icons'
-import MkButton from '../components/ui/button.vue';
+import MkButton from '@/components/ui/button.vue';
-export default Vue.extend({
+export default defineComponent({
components: {
MkButton,
},
diff --git a/src/client/pages/index.home.vue b/src/client/pages/timeline.vue
index d3f60ea910..a15d57e37e 100644
--- a/src/client/pages/index.home.vue
+++ b/src/client/pages/timeline.vue
@@ -1,58 +1,45 @@
<template>
<div class="mk-home" v-hotkey.global="keymap">
- <portal to="header" v-if="showTitle">
- <button @click="choose" class="_button _kjvfvyph_">
- <i><fa v-if="$store.state.i.hasUnreadAntenna || $store.state.i.hasUnreadChannel" :icon="faCircle"/></i>
- <fa v-if="src === 'home'" :icon="faHome"/>
- <fa v-if="src === 'local'" :icon="faComments"/>
- <fa v-if="src === 'social'" :icon="faShareAlt"/>
- <fa v-if="src === 'global'" :icon="faGlobe"/>
- <fa v-if="src === 'list'" :icon="faListUl"/>
- <fa v-if="src === 'antenna'" :icon="faSatellite"/>
- <fa v-if="src === 'channel'" :icon="faSatelliteDish"/>
- <span style="margin-left: 8px;">{{ src === 'list' ? list.name : src === 'antenna' ? antenna.name : src === 'channel' ? channel.name : $t('_timelines.' + src) }}</span>
- <fa :icon="menuOpened ? faAngleUp : faAngleDown" style="margin-left: 8px;"/>
- </button>
- </portal>
-
<div class="new" v-if="queue > 0" :style="{ width: width + 'px' }"><button class="_buttonPrimary" @click="top()">{{ $t('newNoteRecived') }}</button></div>
- <x-tutorial class="tutorial" v-if="$store.state.settings.tutorial != -1"/>
-
- <x-post-form class="post-form _panel" fixed v-if="$store.state.device.showFixedPostForm"/>
- <x-timeline ref="tl" :key="src === 'list' ? `list:${list.id}` : src === 'antenna' ? `antenna:${antenna.id}` : src === 'channel' ? `channel:${channel.id}` : src" :src="src" :list="list ? list.id : null" :antenna="antenna ? antenna.id : null" :channel="channel ? channel.id : null" :sound="true" @before="before()" @after="after()" @queue="queueUpdated"/>
+ <div class="_section">
+ <XTutorial v-if="$store.state.settings.tutorial != -1" class="tutorial _content _vMargin"/>
+ <XPostForm v-if="$store.state.device.showFixedPostForm" class="post-form _panel _content _vMargin" fixed/>
+ <XTimeline ref="tl"
+ class="_content _vMargin"
+ :key="src === 'list' ? `list:${list.id}` : src === 'antenna' ? `antenna:${antenna.id}` : src === 'channel' ? `channel:${channel.id}` : src"
+ :src="src"
+ :list="list ? list.id : null"
+ :antenna="antenna ? antenna.id : null"
+ :channel="channel ? channel.id : null"
+ :sound="true"
+ @before="before()"
+ @after="after()"
+ @queue="queueUpdated"
+ />
+ </div>
</div>
</template>
<script lang="ts">
-import Vue from 'vue';
-import { faAngleDown, faAngleUp, faHome, faShareAlt, faGlobe, faListUl, faSatellite, faSatelliteDish, faCircle } from '@fortawesome/free-solid-svg-icons';
+import { defineComponent, defineAsyncComponent, computed } from 'vue';
+import { faAngleDown, faAngleUp, faHome, faShareAlt, faGlobe, faListUl, faSatellite, faSatelliteDish, faCircle, faEllipsisH, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
import { faComments } from '@fortawesome/free-regular-svg-icons';
-import Progress from '../scripts/loading';
-import XTimeline from '../components/timeline.vue';
-import XPostForm from '../components/post-form.vue';
-import { scroll } from '../scripts/scroll';
+import Progress from '@/scripts/loading';
+import XTimeline from '@/components/timeline.vue';
+import XPostForm from '@/components/post-form.vue';
+import { scroll } from '@/scripts/scroll';
+import * as os from '@/os';
-export default Vue.extend({
- metaInfo() {
- return {
- title: this.$t('timeline') as string
- };
- },
+export default defineComponent({
+ name: 'timeline',
components: {
XTimeline,
- XTutorial: () => import('./index.home.tutorial.vue').then(m => m.default),
+ XTutorial: defineAsyncComponent(() => import('./timeline.tutorial.vue')),
XPostForm,
},
- props: {
- showTitle: {
- type: Boolean,
- required: true
- }
- },
-
data() {
return {
src: 'home',
@@ -62,6 +49,47 @@ export default Vue.extend({
menuOpened: false,
queue: 0,
width: 0,
+ INFO: {
+ header: [{
+ id: 'home',
+ title: null,
+ tooltip: this.$t('_timelines.home'),
+ icon: faHome,
+ onClick: () => { this.src = 'home'; this.saveSrc(); },
+ selected: computed(() => this.src === 'home')
+ }, {
+ id: 'local',
+ title: null,
+ tooltip: this.$t('_timelines.local'),
+ icon: faComments,
+ onClick: () => { this.src = 'local'; this.saveSrc(); },
+ selected: computed(() => this.src === 'local')
+ }, {
+ id: 'social',
+ title: null,
+ tooltip: this.$t('_timelines.social'),
+ icon: faShareAlt,
+ onClick: () => { this.src = 'social'; this.saveSrc(); },
+ selected: computed(() => this.src === 'social')
+ }, {
+ id: 'global',
+ title: null,
+ tooltip: this.$t('_timelines.global'),
+ icon: faGlobe,
+ onClick: () => { this.src = 'global'; this.saveSrc(); },
+ selected: computed(() => this.src === 'global')
+ }, {
+ id: 'other',
+ title: null,
+ icon: faEllipsisH,
+ onClick: this.choose,
+ indicate: computed(() => this.$store.state.i.hasUnreadAntenna || this.$store.state.i.hasUnreadChannel)
+ }],
+ action: {
+ icon: faPencilAlt,
+ handler: () => os.post()
+ }
+ },
faAngleDown, faAngleUp, faHome, faShareAlt, faGlobe, faComments, faListUl, faSatellite, faSatelliteDish, faCircle
};
},
@@ -134,11 +162,10 @@ export default Vue.extend({
async choose(ev) {
if (this.meta == null) return;
- this.menuOpened = true;
const [antennas, lists, channels] = await Promise.all([
- this.$root.api('antennas/list'),
- this.$root.api('users/lists/list'),
- this.$root.api('channels/followed'),
+ os.api('antennas/list'),
+ os.api('users/lists/list'),
+ os.api('channels/followed'),
]);
const antennaItems = antennas.map(antenna => ({
text: antenna.name,
@@ -171,30 +198,7 @@ export default Vue.extend({
this.$router.push(`/channels/${channel.id}`);
}
}));
- this.$root.menu({
- items: [{
- text: this.$t('_timelines.home'),
- icon: faHome,
- action: () => { this.src = 'home'; this.saveSrc(); }
- }, this.meta.disableLocalTimeline && !this.$store.state.i.isModerator && !this.$store.state.i.isAdmin ? undefined : {
- text: this.$t('_timelines.local'),
- icon: faComments,
- action: () => { this.src = 'local'; this.saveSrc(); }
- }, this.meta.disableLocalTimeline && !this.$store.state.i.isModerator && !this.$store.state.i.isAdmin ? undefined : {
- text: this.$t('_timelines.social'),
- icon: faShareAlt,
- action: () => { this.src = 'social'; this.saveSrc(); }
- }, this.meta.disableGlobalTimeline && !this.$store.state.i.isModerator && !this.$store.state.i.isAdmin ? undefined : {
- text: this.$t('_timelines.global'),
- icon: faGlobe,
- action: () => { this.src = 'global'; this.saveSrc(); }
- }, antennaItems.length > 0 ? null : undefined, ...antennaItems, listItems.length > 0 ? null : undefined, ...listItems, channelItems.length > 0 ? null : undefined, ...channelItems],
- fixed: true,
- noCenter: true,
- source: ev.currentTarget || ev.target
- }).then(() => {
- this.menuOpened = false;
- });
+ os.modalMenu([...antennaItems, listItems.length > 0 ? null : undefined, ...listItems, channelItems.length > 0 ? null : undefined, ...channelItems], ev.currentTarget || ev.target);
},
saveSrc() {
@@ -222,35 +226,14 @@ export default Vue.extend({
> button {
display: block;
- margin: 0 auto;
+ margin: var(--margin) auto 0 auto;
padding: 8px 16px;
border-radius: 32px;
}
}
- > .tutorial {
- margin-bottom: var(--margin);
- }
-
- > .post-form {
- position: relative;
- margin-bottom: var(--margin);
- }
-}
-
-._kjvfvyph_ {
- position: relative;
- height: 100%;
- padding: 0 16px;
- font-weight: bold;
+ > ._section {
- > i {
- position: absolute;
- top: initial;
- right: 8px;
- color: var(--indicator);
- font-size: 12px;
- animation: blink 1s infinite;
}
}
</style>
diff --git a/src/client/pages/user/follow-list.vue b/src/client/pages/user/follow-list.vue
index 666e2d04fe..411109c890 100644
--- a/src/client/pages/user/follow-list.vue
+++ b/src/client/pages/user/follow-list.vue
@@ -1,31 +1,24 @@
<template>
-<mk-pagination :pagination="pagination" #default="{items}" class="mk-following-or-followers" ref="list">
- <div class="user _panel" v-for="(user, i) in items.map(x => type === 'following' ? x.followee : x.follower)" :key="user.id">
- <mk-avatar class="avatar" :user="user"/>
- <div class="body">
- <div class="name">
- <router-link class="name" :to="user | userPage" v-user-preview="user.id"><mk-user-name :user="user"/></router-link>
- <p class="acct">@{{ user | acct }}</p>
- </div>
- <div class="description" v-if="user.description" :title="user.description">
- <mfm :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :plain="true" :nowrap="true"/>
- </div>
- <mk-follow-button class="koudoku-button" v-if="$store.getters.isSignedIn && user.id != $store.state.i.id" :user="user" mini/>
+<div class="_section">
+ <MkPagination :pagination="pagination" #default="{items}" class="mk-following-or-followers _content" ref="list">
+ <div class="users">
+ <MkUserInfo class="user" v-for="user in items.map(x => type === 'following' ? x.followee : x.follower)" :user="user" :key="user.id"/>
</div>
- </div>
-</mk-pagination>
+ </MkPagination>
+</div>
</template>
<script lang="ts">
-import Vue from 'vue';
+import { defineComponent } from 'vue';
import parseAcct from '../../../misc/acct/parse';
-import MkFollowButton from '../../components/follow-button.vue';
-import MkPagination from '../../components/ui/pagination.vue';
+import MkUserInfo from '@/components/user-info.vue';
+import MkPagination from '@/components/ui/pagination.vue';
+import { userPage, acct } from '../../filters/user';
-export default Vue.extend({
+export default defineComponent({
components: {
MkPagination,
- MkFollowButton,
+ MkUserInfo,
},
props: {
@@ -55,83 +48,22 @@ export default Vue.extend({
'$route'() {
this.$refs.list.reload();
}
+ },
+
+ methods: {
+ userPage,
+
+ acct
}
});
</script>
<style lang="scss" scoped>
.mk-following-or-followers {
- > .user {
- display: flex;
- padding: 16px;
-
- > .avatar {
- display: block;
- flex-shrink: 0;
- margin: 0 12px 0 0;
- width: 42px;
- height: 42px;
- border-radius: 8px;
- }
-
- > .body {
- display: flex;
- width: calc(100% - 54px);
- position: relative;
-
- > .name {
- width: 45%;
-
- @media (max-width: 500px) {
- width: 100%;
- }
-
- > .name,
- > .acct {
- display: block;
- white-space: nowrap;
- text-overflow: ellipsis;
- overflow: hidden;
- margin: 0;
- }
-
- > .name {
- font-size: 16px;
- line-height: 24px;
- }
-
- > .acct {
- font-size: 15px;
- line-height: 16px;
- opacity: 0.7;
- }
- }
-
- > .description {
- width: 55%;
- line-height: 42px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- opacity: 0.7;
- font-size: 14px;
- padding-right: 40px;
- padding-left: 8px;
- box-sizing: border-box;
-
- @media (max-width: 500px) {
- display: none;
- }
- }
-
- > .koudoku-button {
- position: absolute;
- top: 0;
- bottom: 0;
- right: 0;
- margin: auto 0;
- }
- }
+ > .users {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
+ grid-gap: var(--margin);
}
}
</style>
diff --git a/src/client/pages/user/index.activity.vue b/src/client/pages/user/index.activity.vue
index 29dcca0664..30c02ec54a 100644
--- a/src/client/pages/user/index.activity.vue
+++ b/src/client/pages/user/index.activity.vue
@@ -5,10 +5,11 @@
</template>
<script lang="ts">
-import Vue from 'vue';
+import { defineComponent } from 'vue';
import ApexCharts from 'apexcharts';
+import * as os from '@/os';
-export default Vue.extend({
+export default defineComponent({
props: {
user: {
type: Object,
@@ -28,7 +29,7 @@ export default Vue.extend({
};
},
mounted() {
- this.$root.api('charts/user/notes', {
+ os.api('charts/user/notes', {
userId: this.user.id,
span: 'day',
limit: this.limit
diff --git a/src/client/pages/user/index.photos.vue b/src/client/pages/user/index.photos.vue
index 83a2618403..dcd4d1fce8 100644
--- a/src/client/pages/user/index.photos.vue
+++ b/src/client/pages/user/index.photos.vue
@@ -1,11 +1,11 @@
<template>
<div class="ujigsodd">
- <mk-loading v-if="fetching"/>
+ <MkLoading v-if="fetching"/>
<div class="stream" v-if="!fetching && images.length > 0">
<router-link v-for="(image, i) in images" :key="i"
class="img"
:style="`background-image: url(${thumbnail(image.file)})`"
- :to="image.note | notePage"
+ :to="notePage(image.note)"
></router-link>
</div>
<p class="empty" v-if="!fetching && images.length == 0">{{ $t('nothing') }}</p>
@@ -13,10 +13,12 @@
</template>
<script lang="ts">
-import Vue from 'vue';
-import { getStaticImageUrl } from '../../scripts/get-static-image-url';
+import { defineComponent } from 'vue';
+import { getStaticImageUrl } from '@/scripts/get-static-image-url';
+import notePage from '../../filters/note';
+import * as os from '@/os';
-export default Vue.extend({
+export default defineComponent({
props: ['user'],
data() {
return {
@@ -32,7 +34,7 @@ export default Vue.extend({
'image/apng',
'image/vnd.mozilla.apng',
];
- this.$root.api('users/notes', {
+ os.api('users/notes', {
userId: this.user.id,
fileType: image,
excludeNsfw: !this.$store.state.device.alwaysShowNsfw,
@@ -57,18 +59,17 @@ export default Vue.extend({
? getStaticImageUrl(image.thumbnailUrl)
: image.thumbnailUrl;
},
+ notePage
},
});
</script>
<style lang="scss" scoped>
.ujigsodd {
-
> .stream {
display: flex;
justify-content: center;
flex-wrap: wrap;
- padding: 8px;
> .img {
flex: 1 1 33%;
@@ -79,7 +80,7 @@ export default Vue.extend({
background-size: cover;
background-clip: content-box;
border: solid 2px transparent;
- border-radius: 4px;
+ border-radius: 6px;
}
}
diff --git a/src/client/pages/user/index.timeline.vue b/src/client/pages/user/index.timeline.vue
index 13ed49ea07..e60feca538 100644
--- a/src/client/pages/user/index.timeline.vue
+++ b/src/client/pages/user/index.timeline.vue
@@ -5,15 +5,16 @@
<button class="_button" @click="with_ = 'replies'" :class="{ active: with_ === 'replies' }">{{ $t('notesAndReplies') }}</button>
<button class="_button" @click="with_ = 'files'" :class="{ active: with_ === 'files' }">{{ $t('withFiles') }}</button>
</div>
- <x-notes ref="timeline" :pagination="pagination" @before="$emit('before')" @after="e => $emit('after', e)"/>
+ <XNotes ref="timeline" :pagination="pagination" @before="$emit('before')" @after="e => $emit('after', e)"/>
</div>
</template>
<script lang="ts">
-import Vue from 'vue';
-import XNotes from '../../components/notes.vue';
+import { defineComponent } from 'vue';
+import XNotes from '@/components/notes.vue';
+import * as os from '@/os';
-export default Vue.extend({
+export default defineComponent({
components: {
XNotes
},
diff --git a/src/client/pages/user/index.vue b/src/client/pages/user/index.vue
index 21aa7bece0..bbbf15210b 100644
--- a/src/client/pages/user/index.vue
+++ b/src/client/pages/user/index.vue
@@ -1,144 +1,160 @@
<template>
<div class="mk-user-page" v-if="user" v-size="{ max: [500] }">
- <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>
+ <MkRemoteCaution v-if="user.host != null" :href="user.url" style="margin-bottom: var(--margin)"/>
- <mk-remote-caution v-if="user.host != null" :href="user.url" style="margin-bottom: var(--margin)"/>
- <div class="punished _panel" v-if="user.isSuspended"><fa :icon="faExclamationTriangle" style="margin-right: 8px;"/> {{ $t('userSuspended') }}</div>
- <div class="punished _panel" v-if="user.isSilenced"><fa :icon="faExclamationTriangle" style="margin-right: 8px;"/> {{ $t('userSilenced') }}</div>
- <div class="profile _panel" :key="user.id">
- <div class="banner-container" :style="style">
- <div class="banner" ref="banner" :style="style"></div>
- <div class="fade"></div>
+ <!-- TODO -->
+ <!-- <div class="punished" v-if="user.isSuspended"><Fa :icon="faExclamationTriangle" style="margin-right: 8px;"/> {{ $t('userSuspended') }}</div> -->
+ <!-- <div class="punished" v-if="user.isSilenced"><Fa :icon="faExclamationTriangle" style="margin-right: 8px;"/> {{ $t('userSilenced') }}</div> -->
+
+ <div class="profile _section _fitBottom">
+ <div class="_content" :key="user.id">
+ <div class="banner-container" :style="style">
+ <div class="banner" ref="banner" :style="style"></div>
+ <div class="fade"></div>
+ <div class="title">
+ <MkUserName class="name" :user="user" :nowrap="true"/>
+ <div class="bottom">
+ <span class="username"><MkAcct :user="user" :detail="true" /></span>
+ <span v-if="user.isAdmin" :title="$t('isAdmin')" style="color: var(--badge);"><Fa :icon="faBookmark"/></span>
+ <span v-if="!user.isAdmin && user.isModerator" :title="$t('isModerator')" style="color: var(--badge);"><Fa :icon="farBookmark"/></span>
+ <span v-if="user.isLocked" :title="$t('isLocked')"><Fa :icon="faLock"/></span>
+ <span v-if="user.isBot" :title="$t('isBot')"><Fa :icon="faRobot"/></span>
+ </div>
+ </div>
+ <span class="followed" v-if="$store.getters.isSignedIn && $store.state.i.id != user.id && user.isFollowed">{{ $t('followsYou') }}</span>
+ <div class="actions" v-if="$store.getters.isSignedIn">
+ <button @click="menu" class="menu _button"><Fa :icon="faEllipsisH"/></button>
+ <MkFollowButton v-if="$store.state.i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
+ </div>
+ </div>
+ <MkAvatar class="avatar" :user="user" :disable-preview="true"/>
<div class="title">
- <mk-user-name class="name" :user="user" :nowrap="true"/>
+ <MkUserName :user="user" :nowrap="false" class="name"/>
<div class="bottom">
- <span class="username"><mk-acct :user="user" :detail="true" /></span>
- <span v-if="user.isAdmin" :title="$t('isAdmin')" style="color: var(--badge);"><fa :icon="faBookmark"/></span>
- <span v-if="!user.isAdmin && user.isModerator" :title="$t('isModerator')" style="color: var(--badge);"><fa :icon="farBookmark"/></span>
- <span v-if="user.isLocked" :title="$t('isLocked')"><fa :icon="faLock"/></span>
- <span v-if="user.isBot" :title="$t('isBot')"><fa :icon="faRobot"/></span>
+ <span class="username"><MkAcct :user="user" :detail="true" /></span>
+ <span v-if="user.isAdmin" :title="$t('isAdmin')" style="color: var(--badge);"><Fa :icon="faBookmark"/></span>
+ <span v-if="!user.isAdmin && user.isModerator" :title="$t('isModerator')" style="color: var(--badge);"><Fa :icon="farBookmark"/></span>
+ <span v-if="user.isLocked" :title="$t('isLocked')"><Fa :icon="faLock"/></span>
+ <span v-if="user.isBot" :title="$t('isBot')"><Fa :icon="faRobot"/></span>
</div>
</div>
- <span class="followed" v-if="$store.getters.isSignedIn && $store.state.i.id != user.id && user.isFollowed">{{ $t('followsYou') }}</span>
- <div class="actions" v-if="$store.getters.isSignedIn">
- <button @click="menu" class="menu _button" ref="menu"><fa :icon="faEllipsisH"/></button>
- <mk-follow-button v-if="$store.state.i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
+ <div class="description">
+ <Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
+ <p v-else class="empty">{{ $t('noAccountDescription') }}</p>
</div>
- </div>
- <mk-avatar class="avatar" :user="user" :disable-preview="true"/>
- <div class="title">
- <mk-user-name :user="user" :nowrap="false" class="name"/>
- <div class="bottom">
- <span class="username"><mk-acct :user="user" :detail="true" /></span>
- <span v-if="user.isAdmin" :title="$t('isAdmin')" style="color: var(--badge);"><fa :icon="faBookmark"/></span>
- <span v-if="!user.isAdmin && user.isModerator" :title="$t('isModerator')" style="color: var(--badge);"><fa :icon="farBookmark"/></span>
- <span v-if="user.isLocked" :title="$t('isLocked')"><fa :icon="faLock"/></span>
- <span v-if="user.isBot" :title="$t('isBot')"><fa :icon="faRobot"/></span>
+ <div class="fields system">
+ <dl class="field" v-if="user.location">
+ <dt class="name"><Fa :icon="faMapMarker" fixed-width/> {{ $t('location') }}</dt>
+ <dd class="value">{{ user.location }}</dd>
+ </dl>
+ <dl class="field" v-if="user.birthday">
+ <dt class="name"><Fa :icon="faBirthdayCake" fixed-width/> {{ $t('birthday') }}</dt>
+ <dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd>
+ </dl>
+ <dl class="field">
+ <dt class="name"><Fa :icon="faCalendarAlt" fixed-width/> {{ $t('registeredDate') }}</dt>
+ <dd class="value">{{ new Date(user.createdAt).toLocaleString() }} (<MkTime :time="user.createdAt"/>)</dd>
+ </dl>
+ </div>
+ <div class="fields" v-if="user.fields.length > 0">
+ <dl class="field" v-for="(field, i) in user.fields" :key="i">
+ <dt class="name">
+ <Mfm :text="field.name" :plain="true" :custom-emojis="user.emojis" :colored="false"/>
+ </dt>
+ <dd class="value">
+ <Mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :colored="false"/>
+ </dd>
+ </dl>
+ </div>
+ <div class="status">
+ <router-link :to="userPage(user)" :class="{ active: $route.name === 'user' }">
+ <b>{{ number(user.notesCount) }}</b>
+ <span>{{ $t('notes') }}</span>
+ </router-link>
+ <router-link :to="userPage(user, 'following')" :class="{ active: $route.name === 'userFollowing' }">
+ <b>{{ number(user.followingCount) }}</b>
+ <span>{{ $t('following') }}</span>
+ </router-link>
+ <router-link :to="userPage(user, 'followers')" :class="{ active: $route.name === 'userFollowers' }">
+ <b>{{ number(user.followersCount) }}</b>
+ <span>{{ $t('followers') }}</span>
+ </router-link>
</div>
- </div>
- <div class="description">
- <mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
- <p v-else class="empty">{{ $t('noAccountDescription') }}</p>
- </div>
- <div class="fields system">
- <dl class="field" v-if="user.location">
- <dt class="name"><fa :icon="faMapMarker" fixed-width/> {{ $t('location') }}</dt>
- <dd class="value">{{ user.location }}</dd>
- </dl>
- <dl class="field" v-if="user.birthday">
- <dt class="name"><fa :icon="faBirthdayCake" fixed-width/> {{ $t('birthday') }}</dt>
- <dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd>
- </dl>
- <dl class="field">
- <dt class="name"><fa :icon="faCalendarAlt" fixed-width/> {{ $t('registeredDate') }}</dt>
- <dd class="value">{{ new Date(user.createdAt).toLocaleString() }} (<mk-time :time="user.createdAt"/>)</dd>
- </dl>
- </div>
- <div class="fields" v-if="user.fields.length > 0">
- <dl class="field" v-for="(field, i) in user.fields" :key="i">
- <dt class="name">
- <mfm :text="field.name" :plain="true" :custom-emojis="user.emojis" :colored="false"/>
- </dt>
- <dd class="value">
- <mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :colored="false"/>
- </dd>
- </dl>
- </div>
- <div class="status">
- <router-link :to="user | userPage()" :class="{ active: $route.name === 'user' }">
- <b>{{ user.notesCount | number }}</b>
- <span>{{ $t('notes') }}</span>
- </router-link>
- <router-link :to="user | userPage('following')" :class="{ active: $route.name === 'userFollowing' }">
- <b>{{ user.followingCount | number }}</b>
- <span>{{ $t('following') }}</span>
- </router-link>
- <router-link :to="user | userPage('followers')" :class="{ active: $route.name === 'userFollowers' }">
- <b>{{ user.followersCount | number }}</b>
- <span>{{ $t('followers') }}</span>
- </router-link>
</div>
</div>
+
<router-view :user="user"></router-view>
<template v-if="$route.name == 'user'">
- <div class="pins">
- <x-note v-for="note in user.pinnedNotes" class="note" :note="note" @updated="pinnedNoteUpdated(note, $event)" :key="note.id" :detail="true" :pinned="true"/>
- </div>
- <mk-container :body-togglable="true" class="content">
- <template #header><fa :icon="faImage"/>{{ $t('images') }}</template>
- <div>
- <x-photos :user="user" :key="user.id"/>
+ <div class="_section" v-if="user.pinnedNotes.length > 0">
+ <div class="_content _vMargin">
+ <XNote v-for="note in user.pinnedNotes" class="note _vMargin" :note="note" @update:note="pinnedNoteUpdated(note, $event)" :key="note.id" :detail="true" :pinned="true"/>
</div>
- </mk-container>
- <mk-container :body-togglable="true" class="content">
- <template #header><fa :icon="faChartBar"/>{{ $t('activity') }}</template>
- <div style="padding:8px;">
- <x-activity :user="user" :key="user.id"/>
- </div>
- </mk-container>
- <x-user-timeline :user="user"/>
+ <MkFolder :body-togglable="true" class="_content _vMargin" persist-key="user-images">
+ <template #header><Fa :icon="faImage" style="margin-right: 0.5em;"/>{{ $t('images') }}</template>
+ <div>
+ <XPhotos :user="user" :key="user.id"/>
+ </div>
+ </MkFolder>
+ <MkFolder :body-togglable="true" class="_content _vMargin" persist-key="user-activity">
+ <template #header><Fa :icon="faChartBar" style="margin-right: 0.5em;"/>{{ $t('activity') }}</template>
+ <div>
+ <XActivity :user="user" :key="user.id"/>
+ </div>
+ </MkFolder>
+ </div>
+ <div class="_section">
+ <XUserTimeline :user="user" class="_content"/>
+ </div>
</template>
</div>
<div v-else-if="error">
- <mk-error @retry="fetch()"/>
+ <MkError @retry="fetch()"/>
</div>
</template>
<script lang="ts">
-import Vue from 'vue';
+import { defineComponent, defineAsyncComponent, computed } from 'vue';
import { faExclamationTriangle, faEllipsisH, faRobot, faLock, faBookmark, faChartBar, faImage, faBirthdayCake, faMapMarker } from '@fortawesome/free-solid-svg-icons';
import { faCalendarAlt, faBookmark as farBookmark } from '@fortawesome/free-regular-svg-icons';
import * as age from 's-age';
import XUserTimeline from './index.timeline.vue';
-import XUserMenu from '../../components/user-menu.vue';
-import XNote from '../../components/note.vue';
-import MkFollowButton from '../../components/follow-button.vue';
-import MkContainer from '../../components/ui/container.vue';
-import MkRemoteCaution from '../../components/remote-caution.vue';
-import Progress from '../../scripts/loading';
+import XNote from '@/components/note.vue';
+import MkFollowButton from '@/components/follow-button.vue';
+import MkContainer from '@/components/ui/container.vue';
+import MkFolder from '@/components/ui/folder.vue';
+import MkRemoteCaution from '@/components/remote-caution.vue';
+import Progress from '@/scripts/loading';
import parseAcct from '../../../misc/acct/parse';
-import { getScrollPosition } from '../../scripts/scroll';
+import { getScrollPosition } from '@/scripts/scroll';
+import { getUserMenu } from '@/scripts/get-user-menu';
+import number from '../../filters/number';
+import { userPage, acct } from '../../filters/user';
+import * as os from '@/os';
-export default Vue.extend({
+export default defineComponent({
components: {
XUserTimeline,
XNote,
MkFollowButton,
MkContainer,
MkRemoteCaution,
- XPhotos: () => import('./index.photos.vue').then(m => m.default),
- XActivity: () => import('./index.activity.vue').then(m => m.default),
- },
-
- metaInfo() {
- return {
- title: (this.user ? '@' + Vue.filter('acct')(this.user).replace('@', ' | ') : null) as string
- };
+ MkFolder,
+ XPhotos: defineAsyncComponent(() => import('./index.photos.vue')),
+ XActivity: defineAsyncComponent(() => import('./index.activity.vue')),
},
data() {
return {
+ INFO: computed(() => this.user ? {
+ header: [{
+ userName: this.user,
+ avatar: this.user,
+ }],
+ action: {
+ icon: faEllipsisH,
+ handler: this.menu
+ }
+ } : null),
user: null,
error: null,
parallaxAnimationId: null,
@@ -169,15 +185,17 @@ export default Vue.extend({
mounted() {
window.requestAnimationFrame(this.parallaxLoop);
- this.$once('hook:beforeDestroy', () => {
- window.cancelAnimationFrame(this.parallaxAnimationId);
- });
+ },
+
+ beforeUnmount() {
+ window.cancelAnimationFrame(this.parallaxAnimationId);
},
methods: {
fetch() {
+ if (this.$route.params.user == null) return;
Progress.start();
- this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => {
+ os.api('users/show', parseAcct(this.$route.params.user)).then(user => {
this.user = user;
}).catch(e => {
this.error = e;
@@ -186,11 +204,8 @@ export default Vue.extend({
});
},
- menu() {
- this.$root.new(XUserMenu, {
- source: this.$refs.menu,
- user: this.user
- });
+ menu(ev) {
+ os.modalMenu(getUserMenu(this.user), ev.currentTarget || ev.target);
},
parallaxLoop() {
@@ -213,8 +228,12 @@ export default Vue.extend({
pinnedNoteUpdated(oldValue, newValue) {
const i = this.user.pinnedNotes.findIndex(n => n === oldValue);
- Vue.set(this.user.pinnedNotes, i, newValue);
+ this.user.pinnedNotes[i] = newValue;
},
+
+ number,
+
+ userPage
}
});
</script>
@@ -227,218 +246,214 @@ export default Vue.extend({
}
> .profile {
- position: relative;
- margin-bottom: var(--margin);
- overflow: hidden;
-
- > .banner-container {
+ > ._content {
position: relative;
- height: 250px;
overflow: hidden;
- background-size: cover;
- background-position: center;
- > .banner {
- height: 100%;
- background-color: #4c5e6d;
+ > .banner-container {
+ position: relative;
+ height: 250px;
+ overflow: hidden;
background-size: cover;
background-position: center;
- box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset;
- will-change: background-position;
- }
+ border-radius: 12px;
- > .fade {
- position: absolute;
- bottom: 0;
- left: 0;
- width: 100%;
- height: 78px;
- background: linear-gradient(transparent, rgba(#000, 0.7));
- }
+ > .banner {
+ height: 100%;
+ background-color: #4c5e6d;
+ background-size: cover;
+ background-position: center;
+ box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset;
+ will-change: background-position;
+ }
- > .followed {
- position: absolute;
- top: 12px;
- left: 12px;
- padding: 4px 8px;
- color: #fff;
- background: rgba(0, 0, 0, 0.7);
- font-size: 0.7em;
- border-radius: 6px;
- }
+ > .fade {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: 78px;
+ background: linear-gradient(transparent, rgba(#000, 0.7));
+ }
- > .actions {
- position: absolute;
- top: 12px;
- right: 12px;
- -webkit-backdrop-filter: blur(8px);
- backdrop-filter: blur(8px);
- background: rgba(0, 0, 0, 0.2);
- padding: 8px;
- border-radius: 24px;
-
- > .menu {
- vertical-align: bottom;
- height: 31px;
- width: 31px;
+ > .followed {
+ position: absolute;
+ top: 12px;
+ left: 12px;
+ padding: 4px 8px;
color: #fff;
- text-shadow: 0 0 8px #000;
- font-size: 16px;
+ background: rgba(0, 0, 0, 0.7);
+ font-size: 0.7em;
+ border-radius: 6px;
}
- > .koudoku {
- margin-left: 4px;
- vertical-align: bottom;
- }
- }
+ > .actions {
+ position: absolute;
+ top: 12px;
+ right: 12px;
+ -webkit-backdrop-filter: blur(8px);
+ backdrop-filter: blur(8px);
+ background: rgba(0, 0, 0, 0.2);
+ padding: 8px;
+ border-radius: 24px;
- > .title {
- position: absolute;
- bottom: 0;
- left: 0;
- width: 100%;
- padding: 0 0 8px 154px;
- box-sizing: border-box;
- color: #fff;
+ > .menu {
+ vertical-align: bottom;
+ height: 31px;
+ width: 31px;
+ color: #fff;
+ text-shadow: 0 0 8px #000;
+ font-size: 16px;
+ }
- > .name {
- display: block;
- margin: 0;
- line-height: 32px;
- font-weight: bold;
- font-size: 1.8em;
- text-shadow: 0 0 8px #000;
+ > .koudoku {
+ margin-left: 4px;
+ vertical-align: bottom;
+ }
}
- > .bottom {
- > * {
- display: inline-block;
- margin-right: 16px;
- line-height: 20px;
- opacity: 0.8;
+ > .title {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ padding: 0 0 8px 154px;
+ box-sizing: border-box;
+ color: #fff;
+
+ > .name {
+ display: block;
+ margin: 0;
+ line-height: 32px;
+ font-weight: bold;
+ font-size: 1.8em;
+ text-shadow: 0 0 8px #000;
+ }
+
+ > .bottom {
+ > * {
+ display: inline-block;
+ margin-right: 16px;
+ line-height: 20px;
+ opacity: 0.8;
- &.username {
- font-weight: bold;
+ &.username {
+ font-weight: bold;
+ }
}
}
}
}
- }
- > .title {
- display: none;
- text-align: center;
- padding: 50px 8px 16px 8px;
- font-weight: bold;
- border-bottom: solid 1px var(--divider);
+ > .title {
+ display: none;
+ text-align: center;
+ padding: 50px 8px 16px 8px;
+ font-weight: bold;
+ border-bottom: solid 1px var(--divider);
- > .bottom {
- > * {
- display: inline-block;
- margin-right: 8px;
- opacity: 0.8;
+ > .bottom {
+ > * {
+ display: inline-block;
+ margin-right: 8px;
+ opacity: 0.8;
+ }
}
}
- }
- > .avatar {
- display: block;
- position: absolute;
- top: 170px;
- left: 16px;
- z-index: 2;
- width: 120px;
- height: 120px;
- box-shadow: 1px 1px 3px rgba(#000, 0.2);
- }
+ > .avatar {
+ display: block;
+ position: absolute;
+ top: 170px;
+ left: 16px;
+ z-index: 2;
+ width: 120px;
+ height: 120px;
+ box-shadow: 1px 1px 3px rgba(#000, 0.2);
+ }
- > .description {
- padding: 24px 24px 24px 154px;
- font-size: 0.95em;
+ > .description {
+ padding: 24px 24px 24px 154px;
+ font-size: 0.95em;
- > .empty {
- margin: 0;
- opacity: 0.5;
+ > .empty {
+ margin: 0;
+ opacity: 0.5;
+ }
}
- }
- > .fields {
- padding: 24px;
- font-size: 0.9em;
- border-top: solid 1px var(--divider);
+ > .fields {
+ padding: 24px;
+ font-size: 0.9em;
+ border-top: solid 1px var(--divider);
- > .field {
- display: flex;
- padding: 0;
- margin: 0;
- align-items: center;
+ > .field {
+ display: flex;
+ padding: 0;
+ margin: 0;
+ align-items: center;
- &:not(:last-child) {
- margin-bottom: 8px;
- }
+ &:not(:last-child) {
+ margin-bottom: 8px;
+ }
- > .name {
- width: 30%;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- font-weight: bold;
- text-align: center;
- }
+ > .name {
+ width: 30%;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ font-weight: bold;
+ text-align: center;
+ }
- > .value {
- width: 70%;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
+ > .value {
+ width: 70%;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
}
- }
- &.system > .field > .name {
+ &.system > .field > .name {
+ }
}
- }
- > .status {
- display: flex;
- padding: 24px;
- border-top: solid 1px var(--divider);
+ > .status {
+ display: flex;
+ padding: 24px;
+ border-top: solid 1px var(--divider);
- > a {
- flex: 1;
- text-align: center;
+ > a {
+ flex: 1;
+ text-align: center;
- &.active {
- color: var(--accent);
- }
+ &.active {
+ color: var(--accent);
+ }
- &:hover {
- text-decoration: none;
- }
+ &:hover {
+ text-decoration: none;
+ }
- > b {
- display: block;
- line-height: 16px;
- }
+ > b {
+ display: block;
+ line-height: 16px;
+ }
- > span {
- font-size: 70%;
+ > span {
+ font-size: 70%;
+ }
}
}
}
}
- > .pins {
- > .note {
- margin-bottom: var(--margin);
- }
- }
-
> .content {
margin-bottom: var(--margin);
}
&.max-width_500px {
- > .profile {
+ > .profile > ._content {
> .banner-container {
height: 140px;
diff --git a/src/client/pages/welcome.entrance.vue b/src/client/pages/welcome.entrance.vue
new file mode 100644
index 0000000000..ff946f7452
--- /dev/null
+++ b/src/client/pages/welcome.entrance.vue
@@ -0,0 +1,89 @@
+<template>
+<div class="rsqzvsbo">
+ <div class="_section">
+ <div class="_content _panel about" v-if="meta">
+ <div class="body">
+ <div class="desc" v-html="meta.description || $t('introMisskey')"></div>
+ <MkButton @click="signup()" style="display: inline-block; margin-right: 16px;" primary>{{ $t('signup') }}</MkButton>
+ <MkButton @click="signin()" style="display: inline-block;">{{ $t('login') }}</MkButton>
+ </div>
+ </div>
+ </div>
+ <div class="_section">
+ <div class="_content">
+ <XNotes :pagination="featuredPagination"/>
+ </div>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } 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';
+import * as os from '@/os';
+
+export default defineComponent({
+ 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() {
+ os.api('stats').then(stats => {
+ this.stats = stats;
+ });
+ },
+
+ methods: {
+ signin() {
+ os.popup(XSigninDialog, {
+ autoSet: true
+ }, {}, 'closed');
+ },
+
+ signup() {
+ os.popup(XSignupDialog, {
+ autoSet: true
+ }, {}, 'closed');
+ }
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.rsqzvsbo {
+ > ._section {
+ > .about {
+ > .body {
+ padding: 32px;
+
+ @media (max-width: 500px) {
+ padding: 16px;
+ }
+ }
+ }
+ }
+}
+</style>
diff --git a/src/client/pages/index.welcome.setup.vue b/src/client/pages/welcome.setup.vue
index 9a66a4dffb..ef39a4ca06 100644
--- a/src/client/pages/index.welcome.setup.vue
+++ b/src/client/pages/welcome.setup.vue
@@ -3,31 +3,31 @@
<h1>Welcome to Misskey!</h1>
<div>
<p>{{ $t('intro') }}</p>
- <mk-input v-model="username" pattern="^[a-zA-Z0-9_]{1,20}$" spellcheck="false" required>
+ <MkInput v-model:value="username" pattern="^[a-zA-Z0-9_]{1,20}$" spellcheck="false" required>
<span>{{ $t('username') }}</span>
<template #prefix>@</template>
<template #suffix>@{{ host }}</template>
- </mk-input>
- <mk-input v-model="password" type="password">
+ </MkInput>
+ <MkInput v-model:value="password" type="password">
<span>{{ $t('password') }}</span>
- <template #prefix><fa :icon="faLock"/></template>
- </mk-input>
+ <template #prefix><Fa :icon="faLock"/></template>
+ </MkInput>
<footer>
- <mk-button primary type="submit" :disabled="submitting">{{ submitting ? $t('processing') : $t('done') }}<mk-ellipsis v-if="submitting"/></mk-button>
+ <MkButton primary type="submit" :disabled="submitting">{{ submitting ? $t('processing') : $t('done') }}<MkEllipsis v-if="submitting"/></MkButton>
</footer>
</div>
</form>
</template>
<script lang="ts">
-import Vue from 'vue';
+import { defineComponent } from 'vue';
import { faLock } from '@fortawesome/free-solid-svg-icons';
-import MkButton from '../components/ui/button.vue';
-import MkInput from '../components/ui/input.vue';
-import { host } from '../config';
+import MkButton from '@/components/ui/button.vue';
+import MkInput from '@/components/ui/input.vue';
+import { host } from '@/config';
+import * as os from '@/os';
-export default Vue.extend({
-
+export default defineComponent({
components: {
MkButton,
MkInput,
@@ -48,7 +48,7 @@ export default Vue.extend({
if (this.submitting) return;
this.submitting = true;
- this.$root.api('admin/accounts/create', {
+ os.api('admin/accounts/create', {
username: this.username,
password: this.password,
}).then(res => {
@@ -57,9 +57,9 @@ export default Vue.extend({
}).catch(() => {
this.submitting = false;
- this.$root.dialog({
+ os.dialog({
type: 'error',
- text: this.$t('error')
+ text: this.$t('somethingHappened')
});
});
}
diff --git a/src/client/pages/welcome.vue b/src/client/pages/welcome.vue
new file mode 100644
index 0000000000..fb130cba5c
--- /dev/null
+++ b/src/client/pages/welcome.vue
@@ -0,0 +1,37 @@
+<template>
+<div v-if="meta">
+ <XSetup v-if="meta.requireSetup"/>
+ <XEntrance v-else/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XSetup from './welcome.setup.vue';
+import XEntrance from './welcome.entrance.vue';
+import { instanceName } from '@/config';
+
+export default defineComponent({
+ components: {
+ XSetup,
+ XEntrance,
+ },
+
+ data() {
+ return {
+ INFO: {
+ header: [{
+ title: instanceName || 'Misskey',
+ icon: null
+ }]
+ },
+ }
+ },
+
+ computed: {
+ meta() {
+ return this.$store.state.instance.meta;
+ },
+ },
+});
+</script>