diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2020-10-17 20:12:00 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-10-17 20:12:00 +0900 |
| commit | 7199e6f4e0b3a2c2bc198e689c3e0cd0d0f0354a (patch) | |
| tree | 2263a06acec7fa21882366bae26d1a983ce21135 /src/client/pages | |
| parent | CW の input でも投稿ショートカットが動作するように (#6690) (diff) | |
| download | misskey-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')
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> |