From c33e93c66282839c3e721d651720a7573da41b25 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 10 Oct 2021 15:19:16 +0900 Subject: [PATCH] improve ui --- locales/ja-JP.yml | 2 + src/client/account.ts | 74 +++++- src/client/components/ui/folder.vue | 23 +- src/client/components/ui/super-menu.vue | 151 +++++++++++++ src/client/pages/emojis.category.vue | 1 - src/client/pages/explore.vue | 48 ++-- src/client/pages/instance/database.vue | 3 +- src/client/pages/instance/email-settings.vue | 3 +- src/client/pages/instance/files-settings.vue | 3 +- src/client/pages/instance/index.link.vue | 97 -------- src/client/pages/instance/index.vue | 225 ++++++++++++++----- src/client/pages/instance/instance-block.vue | 3 +- src/client/pages/instance/integrations.vue | 3 +- src/client/pages/instance/object-storage.vue | 3 +- src/client/pages/instance/other-settings.vue | 3 +- src/client/pages/instance/overview.vue | 10 - src/client/pages/instance/proxy-account.vue | 3 +- src/client/pages/instance/queue.vue | 1 + src/client/pages/instance/relays.vue | 1 + src/client/pages/instance/security.vue | 3 +- src/client/pages/instance/service-worker.vue | 3 +- src/client/pages/instance/settings.vue | 3 +- src/client/pages/instance/users.vue | 2 + src/client/pages/settings/index.link.vue | 105 --------- src/client/pages/settings/index.vue | 181 ++++++++++----- src/client/ui/_common_/sidebar.vue | 68 +----- src/client/ui/chat/index.vue | 3 + src/client/ui/default.header.vue | 68 +----- src/client/ui/default.sidebar.vue | 68 +----- 29 files changed, 609 insertions(+), 552 deletions(-) create mode 100644 src/client/components/ui/super-menu.vue delete mode 100644 src/client/pages/instance/index.link.vue delete mode 100644 src/client/pages/settings/index.link.vue diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 9a5ec30787..5798ce7ec2 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -794,6 +794,8 @@ itsOff: "オフになっています" emailRequiredForSignup: "アカウント登録にメールアドレスを必須にする" unread: "未読" filter: "フィルタ" +controllPanel: "コントロールパネル" +manageAccounts: "アカウントを管理" _signup: almostThere: "ほとんど完了です" diff --git a/src/client/account.ts b/src/client/account.ts index 6e26ac1f7d..a3fe082a22 100644 --- a/src/client/account.ts +++ b/src/client/account.ts @@ -1,9 +1,10 @@ import { del, get, set } from '@client/scripts/idb-proxy'; import { reactive } from 'vue'; import { apiUrl } from '@client/config'; -import { waiting } from '@client/os'; +import { waiting, api, popup, popupMenu, success } from '@client/os'; import { unisonReload, reloadChannel } from '@client/scripts/unison-reload'; import { showSuspendedDialog } from './scripts/show-suspended-dialog'; +import { i18n } from './i18n'; // TODO: 他のタブと永続化されたstateを同期 @@ -129,6 +130,77 @@ export async function login(token: Account['token'], redirect?: string) { unisonReload(); } +export async function openAccountMenu(ev: MouseEvent) { + function showSigninDialog() { + popup(import('@client/components/signin-dialog.vue'), {}, { + done: res => { + addAccount(res.id, res.i); + success(); + }, + }, 'closed'); + } + + function createAccount() { + popup(import('@client/components/signup-dialog.vue'), {}, { + done: res => { + addAccount(res.id, res.i); + switchAccountWithToken(res.i); + }, + }, 'closed'); + } + + async function switchAccount(account: any) { + const storedAccounts = await getAccounts(); + const token = storedAccounts.find(x => x.id === account.id).token; + switchAccountWithToken(token); + } + + function switchAccountWithToken(token: string) { + login(token); + } + + const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== $i.id)); + const accountsPromise = api('users/show', { userIds: storedAccounts.map(x => x.id) }); + + const accountItemPromises = storedAccounts.map(a => new Promise(res => { + accountsPromise.then(accounts => { + const account = accounts.find(x => x.id === a.id); + if (account == null) return res(null); + res({ + type: 'user', + user: account, + action: () => { switchAccount(account); } + }); + }); + })); + + popupMenu([...[{ + type: 'link', + text: i18n.locale.profile, + to: `/@${ $i.username }`, + avatar: $i, + }, null, ...accountItemPromises, { + icon: 'fas fa-plus', + text: i18n.locale.addAccount, + action: () => { + popupMenu([{ + text: i18n.locale.existingAccount, + action: () => { showSigninDialog(); }, + }, { + text: i18n.locale.createAccount, + action: () => { createAccount(); }, + }], ev.currentTarget || ev.target); + }, + }, { + type: 'link', + icon: 'fas fa-users', + text: i18n.locale.manageAccounts, + to: `/settings/accounts`, + }]], ev.currentTarget || ev.target, { + align: 'left' + }); +} + // このファイルに書きたくないけどここに書かないと何故かVeturが認識しない declare module '@vue/runtime-core' { interface ComponentCustomProperties { diff --git a/src/client/components/ui/folder.vue b/src/client/components/ui/folder.vue index eecf1d8be1..cc3da083c5 100644 --- a/src/client/components/ui/folder.vue +++ b/src/client/components/ui/folder.vue @@ -1,6 +1,6 @@ <template> <div class="ssazuxis" v-size="{ max: [500] }"> - <header @click="showBody = !showBody" class="_button"> + <header @click="showBody = !showBody" class="_button" :style="{ background: bg }"> <div class="title"><slot name="header"></slot></div> <div class="divider"></div> <button class="_button"> @@ -23,6 +23,7 @@ <script lang="ts"> import { defineComponent } from 'vue'; +import * as tinycolor from 'tinycolor2'; const localStoragePrefix = 'ui:folder:'; @@ -41,6 +42,7 @@ export default defineComponent({ }, data() { return { + bg: null, showBody: (this.persistKey && localStorage.getItem(localStoragePrefix + this.persistKey)) ? localStorage.getItem(localStoragePrefix + this.persistKey) === 't' : this.expanded, }; }, @@ -51,6 +53,21 @@ export default defineComponent({ } } }, + mounted() { + function getParentBg(el: Element | null): string { + if (el == null || el.tagName === 'BODY') return 'var(--bg)'; + const bg = el.style.background || el.style.backgroundColor; + if (bg) { + return bg; + } else { + return getParentBg(el.parentElement); + } + } + const rawBg = getParentBg(this.$el); + const bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg); + bg.setAlpha(0.85); + this.bg = bg.toRgbString(); + }, methods: { toggleContent(show: boolean) { this.showBody = show; @@ -100,12 +117,8 @@ export default defineComponent({ position: sticky; top: var(--stickyTop, 0px); padding: var(--x-padding); - background: var(--x-header, var(--panel)); - /* TODO panelの半透明バージョンをプログラマティックに作りたい - background: var(--X17); -webkit-backdrop-filter: var(--blur, blur(8px)); backdrop-filter: var(--blur, blur(20px)); - */ > .title { margin: 0; diff --git a/src/client/components/ui/super-menu.vue b/src/client/components/ui/super-menu.vue new file mode 100644 index 0000000000..f43b545fd6 --- /dev/null +++ b/src/client/components/ui/super-menu.vue @@ -0,0 +1,151 @@ +<template> +<div class="rrevdjwu" :class="{ grid }"> + <div class="group" v-for="group in def"> + <div class="title" v-if="group.title">{{ group.title }}</div> + + <div class="items"> + <template v-for="(item, i) in group.items"> + <a v-if="item.type === 'a'" :href="item.href" :target="item.target" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }"> + <i v-if="item.icon" class="icon fa-fw" :class="item.icon"></i> + <span class="text">{{ item.text }}</span> + </a> + <button v-else-if="item.type === 'button'" @click="ev => item.action(ev)" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active"> + <i v-if="item.icon" class="icon fa-fw" :class="item.icon"></i> + <span class="text">{{ item.text }}</span> + </button> + <MkA v-else :to="item.to" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }"> + <i v-if="item.icon" class="icon fa-fw" :class="item.icon"></i> + <span class="text">{{ item.text }}</span> + </MkA> + </template> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import { defineComponent, ref, unref } from 'vue'; + +export default defineComponent({ + props: { + def: { + type: Array, + required: true + }, + grid: { + type: Boolean, + required: false, + default: false, + }, + }, +}); +</script> + +<style lang="scss" scoped> +.rrevdjwu { + > .group { + & + .group { + margin-top: 16px; + padding-top: 16px; + border-top: solid 0.5px var(--divider); + } + + margin-left: 16px; + margin-right: 16px; + + > .title { + font-size: 0.9em; + opacity: 0.7; + margin: 0 0 8px 12px; + } + + > .items { + > .item { + display: flex; + align-items: center; + width: 100%; + box-sizing: border-box; + padding: 10px 16px 10px 14px; + border-radius: 999px; + font-size: 0.9em; + + &:hover { + text-decoration: none; + background: var(--panelHighlight); + } + + &.active { + color: var(--accent); + background: var(--accentedBg); + } + + &.danger { + color: var(--error); + } + + > .icon { + width: 32px; + margin-right: 2px; + flex-shrink: 0; + text-align: center; + opacity: 0.8; + } + + > .text { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + padding-right: 12px; + } + + } + } + } + + &.grid { + > .group { + & + .group { + padding-top: 0; + border-top: none; + } + + margin-left: 0; + margin-right: 0; + + > .title { + font-size: 1em; + opacity: 0.7; + margin: 0 0 8px 16px; + } + + > .items { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); + grid-gap: 8px; + padding: 0 16px; + + > .item { + flex-direction: column; + padding: 18px 16px 16px 16px; + background: var(--panel); + border-radius: 8px; + text-align: center; + + > .icon { + display: block; + margin-right: 0; + margin-bottom: 12px; + font-size: 1.5em; + } + + > .text { + padding-right: 0; + width: 100%; + font-size: 0.8em; + } + } + } + } + } +} +</style> diff --git a/src/client/pages/emojis.category.vue b/src/client/pages/emojis.category.vue index d7737523d2..e725bcb31f 100644 --- a/src/client/pages/emojis.category.vue +++ b/src/client/pages/emojis.category.vue @@ -122,7 +122,6 @@ export default defineComponent({ } > .emojis { - --x-header: var(--bg); --x-padding: 0 16px; .zuvgdzyt { diff --git a/src/client/pages/explore.vue b/src/client/pages/explore.vue index 15ebf8efad..26ce412612 100644 --- a/src/client/pages/explore.vue +++ b/src/client/pages/explore.vue @@ -3,16 +3,7 @@ <MkHeader :info="header"/> <div class="lznhrdub _root"> - <div> - <div class="_isolated"> - <MkInput v-model="query" :debounce="true" type="search"> - <template #prefix><i class="fas fa-search"></i></template> - <template #label>{{ $ts.searchUser }}</template> - </MkInput> - </div> - - <XUserList v-if="query" class="_gap" :pagination="searchPagination" ref="search"/> - + <div v-if="tab === 'local'"> <div class="localfedi7 _block _isolated" 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> @@ -37,7 +28,7 @@ </MkFolder> </template> </div> - <div> + <div v-else-if="tab === 'remote'"> <div class="localfedi7 _block _isolated" v-if="tag == null" :style="{ backgroundImage: `url(/static-assets/client/fedi.jpg)` }"> <header><span>{{ $ts.exploreFediverse }}</span></header> </div> @@ -71,6 +62,16 @@ </MkFolder> </template> </div> + <div v-else-if="tab === 'search'"> + <div class="_isolated"> + <MkInput v-model="query" :debounce="true" type="search"> + <template #prefix><i class="fas fa-search"></i></template> + <template #label>{{ $ts.searchUser }}</template> + </MkInput> + </div> + + <XUserList v-if="query" class="_gap" :pagination="searchPagination" ref="search"/> + </div> </div> </div> </template> @@ -102,12 +103,28 @@ export default defineComponent({ return { [symbols.PAGE_INFO]: { title: this.$ts.explore, - icon: 'fas fa-hashtag' + icon: 'fas fa-hashtag', + bg: 'var(--bg)', }, - header: { + tab: 'local', + header: computed(() => ({ title: this.$ts.explore, - icon: 'fas fa-hashtag' - }, + icon: 'fas fa-hashtag', + bg: 'var(--bg)', + tabs: [{ + active: this.tab === 'local', + title: this.$ts.local, + onClick: () => { this.tab = 'local'; }, + }, { + active: this.tab === 'remote', + title: this.$ts.remote, + onClick: () => { this.tab = 'remote'; }, + }, { + active: this.tab === 'search', + title: this.$ts.search, + onClick: () => { this.tab = 'search'; }, + },] + })), pinnedUsers: { endpoint: 'pinned-users' }, popularUsers: { endpoint: 'users', limit: 10, noPaging: true, params: { state: 'alive', @@ -200,6 +217,7 @@ export default defineComponent({ .lznhrdub { max-width: 1400px; margin: 0 auto; + padding: 16px; } .localfedi7 { diff --git a/src/client/pages/instance/database.vue b/src/client/pages/instance/database.vue index a8a1e9a54a..ffbeed8b30 100644 --- a/src/client/pages/instance/database.vue +++ b/src/client/pages/instance/database.vue @@ -43,7 +43,8 @@ export default defineComponent({ return { [symbols.PAGE_INFO]: { title: this.$ts.database, - icon: 'fas fa-database' + icon: 'fas fa-database', + bg: 'var(--bg)', }, databasePromiseFactory: () => os.api('admin/get-table-stats', {}).then(res => Object.entries(res).sort((a, b) => b[1].size - a[1].size)), } diff --git a/src/client/pages/instance/email-settings.vue b/src/client/pages/instance/email-settings.vue index 251354a43a..ebf724fcdd 100644 --- a/src/client/pages/instance/email-settings.vue +++ b/src/client/pages/instance/email-settings.vue @@ -66,7 +66,8 @@ export default defineComponent({ return { [symbols.PAGE_INFO]: { title: this.$ts.emailServer, - icon: 'fas fa-envelope' + icon: 'fas fa-envelope', + bg: 'var(--bg)', }, enableEmail: false, email: null, diff --git a/src/client/pages/instance/files-settings.vue b/src/client/pages/instance/files-settings.vue index 8bf4613a76..8aefa9e90d 100644 --- a/src/client/pages/instance/files-settings.vue +++ b/src/client/pages/instance/files-settings.vue @@ -56,7 +56,8 @@ export default defineComponent({ return { [symbols.PAGE_INFO]: { title: this.$ts.files, - icon: 'fas fa-cloud' + icon: 'fas fa-cloud', + bg: 'var(--bg)', }, cacheRemoteFiles: false, proxyRemoteFiles: false, diff --git a/src/client/pages/instance/index.link.vue b/src/client/pages/instance/index.link.vue deleted file mode 100644 index e1f4773800..0000000000 --- a/src/client/pages/instance/index.link.vue +++ /dev/null @@ -1,97 +0,0 @@ -<template> -<div class="qmfkfnzh"> - <a class="main _button" :href="to" target="_blank" v-if="external"> - <span class="icon"><slot name="icon"></slot></span> - <span class="text"><slot></slot></span> - </a> - <MkA class="main _button" :class="{ active }" :to="to" :behavior="behavior" v-else> - <span class="icon"><slot name="icon"></slot></span> - <span class="text"><slot></slot></span> - </MkA> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; - -export default defineComponent({ - props: { - to: { - type: String, - required: true - }, - active: { - type: Boolean, - required: false - }, - external: { - type: Boolean, - required: false - }, - behavior: { - type: String, - required: false, - }, - }, - data() { - return { - }; - } -}); -</script> - -<style lang="scss" scoped> -.qmfkfnzh { - > .main { - display: flex; - align-items: center; - width: 100%; - box-sizing: border-box; - padding: 10px 16px 10px 14px; - border-radius: 999px; - font-size: 0.9em; - - &:hover { - text-decoration: none; - background: var(--panelHighlight); - } - - &.active { - color: var(--accent); - background: var(--accentedBg); - } - - > .icon { - width: 32px; - margin-right: 2px; - flex-shrink: 0; - text-align: center; - opacity: 0.8; - - &:empty { - display: none; - - & + .text { - padding-left: 4px; - } - } - } - - > .text { - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - padding-right: 12px; - } - - > .right { - margin-left: auto; - opacity: 0.7; - - > .text:not(:empty) { - margin-right: 0.75em; - } - } - } -} -</style> diff --git a/src/client/pages/instance/index.vue b/src/client/pages/instance/index.vue index 959c4be6cd..e2cb5b8f58 100644 --- a/src/client/pages/instance/index.vue +++ b/src/client/pages/instance/index.vue @@ -1,47 +1,15 @@ <template> <div class="hiyeyicy" :class="{ wide: !narrow }" ref="el"> <div class="nav" v-if="!narrow || page == null"> - <div class="group"> - <div class="lxpfedzu"> - <img :src="$instance.iconUrl || '/favicon.ico'" alt="" class="icon"/> - </div> - <XLink :active="page === 'overview'" replace to="/instance/overview"><template #icon><i class="fas fa-tachometer-alt"></i></template>{{ $ts.overview }}</XLink> - </div> - <div class="group"> - <div class="label">{{ $ts.quickAction }}</div> - <FormButton @click="lookup"><i class="fas fa-search"></i> {{ $ts.lookup }}</FormButton> - <FormButton v-if="$instance.disableRegistration" @click="invite"><i class="fas fa-user"></i> {{ $ts.invite }}</FormButton> - </div> - <div class="group"> - <div class="label">{{ $ts.administration }}</div> - <XLink :active="page === 'users'" replace to="/instance/users"><template #icon><i class="fas fa-users"></i></template>{{ $ts.users }}</XLink> - <XLink :active="page === 'emojis'" replace to="/instance/emojis"><template #icon><i class="fas fa-laugh"></i></template>{{ $ts.customEmojis }}</XLink> - <XLink :active="page === 'federation'" replace to="/instance/federation"><template #icon><i class="fas fa-globe"></i></template>{{ $ts.federation }}</XLink> - <XLink :active="page === 'queue'" replace to="/instance/queue"><template #icon><i class="fas fa-clipboard-list"></i></template>{{ $ts.jobQueue }}</XLink> - <XLink :active="page === 'files'" replace to="/instance/files"><template #icon><i class="fas fa-cloud"></i></template>{{ $ts.files }}</XLink> - <XLink :active="page === 'announcements'" replace to="/instance/announcements"><template #icon><i class="fas fa-broadcast-tower"></i></template>{{ $ts.announcements }}</XLink> - <XLink :active="page === 'ads'" replace to="/instance/ads"><template #icon><i class="fas fa-audio-description"></i></template>{{ $ts.ads }}</XLink> - <XLink :active="page === 'abuses'" replace to="/instance/abuses"><template #icon><i class="fas fa-exclamation-circle"></i></template>{{ $ts.abuseReports }}</XLink> - </div> - <div class="group"> - <div class="label">{{ $ts.settings }}</div> - <XLink :active="page === 'settings'" replace to="/instance/settings"><template #icon><i class="fas fa-cog"></i></template>{{ $ts.general }}</XLink> - <XLink :active="page === 'files-settings'" replace to="/instance/files-settings"><template #icon><i class="fas fa-cloud"></i></template>{{ $ts.files }}</XLink> - <XLink :active="page === 'email-settings'" replace to="/instance/email-settings"><template #icon><i class="fas fa-envelope"></i></template>{{ $ts.emailServer }}</XLink> - <XLink :active="page === 'object-storage'" replace to="/instance/object-storage"><template #icon><i class="fas fa-cloud"></i></template>{{ $ts.objectStorage }}</XLink> - <XLink :active="page === 'security'" replace to="/instance/security"><template #icon><i class="fas fa-lock"></i></template>{{ $ts.security }}</XLink> - <XLink :active="page === 'service-worker'" replace to="/instance/service-worker"><template #icon><i class="fas fa-bolt"></i></template>ServiceWorker</XLink> - <XLink :active="page === 'relays'" replace to="/instance/relays"><template #icon><i class="fas fa-globe"></i></template>{{ $ts.relays }}</XLink> - <XLink :active="page === 'integrations'" replace to="/instance/integrations"><template #icon><i class="fas fa-share-alt"></i></template>{{ $ts.integration }}</XLink> - <XLink :active="page === 'instance-block'" replace to="/instance/instance-block"><template #icon><i class="fas fa-ban"></i></template>{{ $ts.instanceBlocking }}</XLink> - <XLink :active="page === 'proxy-account'" replace to="/instance/proxy-account"><template #icon><i class="fas fa-ghost"></i></template>{{ $ts.proxyAccount }}</XLink> - <XLink :active="page === 'other-settings'" replace to="/instance/other-settings"><template #icon><i class="fas fa-cogs"></i></template>{{ $ts.other }}</XLink> - </div> - <div class="group"> - <div class="label">{{ $ts.info }}</div> - <XLink :active="page === 'database'" replace to="/instance/database"><template #icon><i class="fas fa-database"></i></template>{{ $ts.database }}</XLink> - <XLink :active="page === 'logs'" replace to="/instance/logs"><template #icon><i class="fas fa-stream"></i></template>{{ $ts.logs }}</XLink> + <MkHeader :info="header"></MkHeader> + + <MkInfo v-if="noMaintainerInformation" warn class="info">{{ $ts.noMaintainerInformationWarning }} <MkA to="/instance/settings" class="_link">{{ $ts.configure }}</MkA></MkInfo> + <MkInfo v-if="noBotProtection" warn class="info">{{ $ts.noBotProtectionWarning }} <MkA to="/instance/bot-protection" class="_link">{{ $ts.configure }}</MkA></MkInfo> + + <div class="lxpfedzu"> + <img :src="$instance.iconUrl || '/favicon.ico'" alt="" class="icon"/> </div> + <MkSuperMenu :def="menuDef" :grid="page == null"></MkSuperMenu> </div> <div class="main"> <component :is="component" :key="page" @info="onInfo" v-bind="pageProps"/> @@ -52,11 +20,13 @@ <script lang="ts"> import { computed, defineAsyncComponent, defineComponent, nextTick, onMounted, reactive, ref, watch } from 'vue'; import { i18n } from '@client/i18n'; -import XLink from './index.link.vue'; +import MkSuperMenu from '@client/components/ui/super-menu.vue'; import FormGroup from '@client/components/debobigego/group.vue'; import FormBase from '@client/components/debobigego/base.vue'; import FormButton from '@client/components/debobigego/button.vue'; +import MkInfo from '@client/components/ui/info.vue'; import { scroll } from '@client/scripts/scroll'; +import { instance } from '@client/instance'; import * as symbols from '@client/symbols'; import * as os from '@client/os'; import { lookupUser } from '@client/scripts/lookup-user'; @@ -64,9 +34,10 @@ import { lookupUser } from '@client/scripts/lookup-user'; export default defineComponent({ components: { FormBase, - XLink, + MkSuperMenu, FormGroup, FormButton, + MkInfo, }, props: { @@ -91,6 +62,151 @@ export default defineComponent({ INFO.value = viewInfo; }; const pageProps = ref({}); + + const isEmpty = (x: any) => x == null || x == ''; + + const noMaintainerInformation = ref(false); + const noBotProtection = ref(false); + + os.api('meta', { detail: true }).then(meta => { + // TODO: 設定が完了しても残ったままになるので、ストリーミングでmeta更新イベントを受け取ってよしなに更新する + noMaintainerInformation.value = isEmpty(meta.maintainerName) || isEmpty(meta.maintainerEmail); + noBotProtection.value = !meta.enableHcaptcha && !meta.enableRecaptcha; + }); + + const menuDef = computed(() => [{ + title: i18n.locale.quickAction, + items: [{ + type: 'button', + icon: 'fas fa-search', + text: i18n.locale.lookup, + action: lookup, + }, ...(instance.disableRegistration ? [{ + type: 'button', + icon: 'fas fa-user', + text: i18n.locale.invite, + action: invite, + }] : [])], + }, { + title: i18n.locale.administration, + items: [{ + icon: 'fas fa-tachometer-alt', + text: i18n.locale.dashboard, + to: '/instance/overview', + active: page.value === 'overview', + }, { + icon: 'fas fa-users', + text: i18n.locale.users, + to: '/instance/users', + active: page.value === 'users', + }, { + icon: 'fas fa-laugh', + text: i18n.locale.customEmojis, + to: '/instance/emojis', + active: page.value === 'emojis', + }, { + icon: 'fas fa-globe', + text: i18n.locale.federation, + to: '/instance/federation', + active: page.value === 'federation', + }, { + icon: 'fas fa-clipboard-list', + text: i18n.locale.jobQueue, + to: '/instance/queue', + active: page.value === 'queue', + }, { + icon: 'fas fa-cloud', + text: i18n.locale.files, + to: '/instance/files', + active: page.value === 'files', + }, { + icon: 'fas fa-broadcast-tower', + text: i18n.locale.announcements, + to: '/instance/announcements', + active: page.value === 'announcements', + }, { + icon: 'fas fa-audio-description', + text: i18n.locale.ads, + to: '/instance/ads', + active: page.value === 'ads', + }, { + icon: 'fas fa-exclamation-circle', + text: i18n.locale.abuseReports, + to: '/instance/abuses', + active: page.value === 'abuses', + }], + }, { + title: i18n.locale.settings, + items: [{ + icon: 'fas fa-cog', + text: i18n.locale.general, + to: '/instance/settings', + active: page.value === 'settings', + }, { + icon: 'fas fa-cloud', + text: i18n.locale.files, + to: '/instance/files-settings', + active: page.value === 'files-settings', + }, { + icon: 'fas fa-envelope', + text: i18n.locale.emailServer, + to: '/instance/email-settings', + active: page.value === 'email-settings', + }, { + icon: 'fas fa-cloud', + text: i18n.locale.objectStorage, + to: '/instance/object-storage', + active: page.value === 'object-storage', + }, { + icon: 'fas fa-lock', + text: i18n.locale.security, + to: '/instance/security', + active: page.value === 'security', + }, { + icon: 'fas fa-bolt', + text: 'ServiceWorker', + to: '/instance/service-worker', + active: page.value === 'service-worker', + }, { + icon: 'fas fa-globe', + text: i18n.locale.relays, + to: '/instance/relays', + active: page.value === 'relays', + }, { + icon: 'fas fa-share-alt', + text: i18n.locale.integration, + to: '/instance/integrations', + active: page.value === 'integrations', + }, { + icon: 'fas fa-ban', + text: i18n.locale.instanceBlocking, + to: '/instance/instance-block', + active: page.value === 'instance-block', + }, { + icon: 'fas fa-ghost', + text: i18n.locale.proxyAccount, + to: '/instance/proxy-account', + active: page.value === 'proxy-account', + }, { + icon: 'fas fa-cogs', + text: i18n.locale.other, + to: '/instance/other-settings', + active: page.value === 'other-settings', + }], + }, { + title: i18n.locale.info, + items: [{ + icon: 'fas fa-database', + text: i18n.locale.database, + to: '/instance/database', + active: page.value === 'database', + }, { + icon: 'fas fa-stream', + text: i18n.locale.logs, + to: '/instance/logs', + active: page.value === 'logs', + }], + }]); const component = computed(() => { if (page.value == null) return null; switch (page.value) { @@ -193,6 +309,12 @@ export default defineComponent({ return { [symbols.PAGE_INFO]: INFO, + menuDef, + header: { + title: i18n.locale.controllPanel, + }, + noMaintainerInformation, + noBotProtection, page, narrow, view, @@ -216,20 +338,11 @@ export default defineComponent({ > .nav { width: 32%; - max-width: 320px; + max-width: 280px; box-sizing: border-box; border-right: solid 0.5px var(--divider); overflow: auto; - - > .group { - padding: 16px; - - > .label { - font-size: 0.9em; - opacity: 0.7; - margin: 0 0 8px 12px; - } - } + height: 100%; } > .main { @@ -238,6 +351,12 @@ export default defineComponent({ --baseContentWidth: 100%; } } + + > .nav { + > .info { + margin: 16px; + } + } } .lxpfedzu { diff --git a/src/client/pages/instance/instance-block.vue b/src/client/pages/instance/instance-block.vue index a00970d85b..105cdb4941 100644 --- a/src/client/pages/instance/instance-block.vue +++ b/src/client/pages/instance/instance-block.vue @@ -43,7 +43,8 @@ export default defineComponent({ return { [symbols.PAGE_INFO]: { title: this.$ts.instanceBlocking, - icon: 'fas fa-ban' + icon: 'fas fa-ban', + bg: 'var(--bg)', }, blockedHosts: '', } diff --git a/src/client/pages/instance/integrations.vue b/src/client/pages/instance/integrations.vue index bfd9e2f349..6964ae5704 100644 --- a/src/client/pages/instance/integrations.vue +++ b/src/client/pages/instance/integrations.vue @@ -49,7 +49,8 @@ export default defineComponent({ return { [symbols.PAGE_INFO]: { title: this.$ts.integration, - icon: 'fas fa-share-alt' + icon: 'fas fa-share-alt', + bg: 'var(--bg)', }, enableTwitterIntegration: false, enableGithubIntegration: false, diff --git a/src/client/pages/instance/object-storage.vue b/src/client/pages/instance/object-storage.vue index ba6a249685..2d765270e6 100644 --- a/src/client/pages/instance/object-storage.vue +++ b/src/client/pages/instance/object-storage.vue @@ -91,7 +91,8 @@ export default defineComponent({ return { [symbols.PAGE_INFO]: { title: this.$ts.objectStorage, - icon: 'fas fa-cloud' + icon: 'fas fa-cloud', + bg: 'var(--bg)', }, useObjectStorage: false, objectStorageBaseUrl: null, diff --git a/src/client/pages/instance/other-settings.vue b/src/client/pages/instance/other-settings.vue index b9f9ce30f7..4e55df41fb 100644 --- a/src/client/pages/instance/other-settings.vue +++ b/src/client/pages/instance/other-settings.vue @@ -49,7 +49,8 @@ export default defineComponent({ return { [symbols.PAGE_INFO]: { title: this.$ts.other, - icon: 'fas fa-cogs' + icon: 'fas fa-cogs', + bg: 'var(--bg)', }, summalyProxy: '', deeplAuthKey: '', diff --git a/src/client/pages/instance/overview.vue b/src/client/pages/instance/overview.vue index 61fbc03c64..c6db9d0c04 100644 --- a/src/client/pages/instance/overview.vue +++ b/src/client/pages/instance/overview.vue @@ -1,9 +1,6 @@ <template> <FormBase> <FormSuspense :p="init"> - <FormInfo v-if="noMaintainerInformation" warn>{{ $ts.noMaintainerInformationWarning }} <MkA to="/instance/settings" class="_link">{{ $ts.configure }}</MkA></FormInfo> - <FormInfo v-if="noBotProtection" warn>{{ $ts.noBotProtectionWarning }} <MkA to="/instance/bot-protection" class="_link">{{ $ts.configure }}</MkA></FormInfo> - <FormSuspense :p="fetchStats" v-slot="{ result: stats }"> <FormGroup> <FormKeyValueView> @@ -98,8 +95,6 @@ export default defineComponent({ fetchServerInfo: () => os.api('admin/server-info', {}), fetchJobs: () => os.api('admin/queue/deliver-delayed', {}), fetchModLogs: () => os.api('admin/show-moderation-logs', {}), - noMaintainerInformation: false, - noBotProtection: false, } }, @@ -110,11 +105,6 @@ export default defineComponent({ methods: { async init() { this.meta = await os.api('meta', { detail: true }); - - const isEmpty = (x: any) => x == null || x == ''; - - this.noMaintainerInformation = isEmpty(this.meta.maintainerName) || isEmpty(this.meta.maintainerEmail); - this.noBotProtection = !this.meta.enableHcaptcha && !this.meta.enableRecaptcha; }, async showInstanceInfo(q) { diff --git a/src/client/pages/instance/proxy-account.vue b/src/client/pages/instance/proxy-account.vue index a80ecccb05..b1ece19710 100644 --- a/src/client/pages/instance/proxy-account.vue +++ b/src/client/pages/instance/proxy-account.vue @@ -46,7 +46,8 @@ export default defineComponent({ return { [symbols.PAGE_INFO]: { title: this.$ts.proxyAccount, - icon: 'fas fa-ghost' + icon: 'fas fa-ghost', + bg: 'var(--bg)', }, proxyAccount: null, proxyAccountId: null, diff --git a/src/client/pages/instance/queue.vue b/src/client/pages/instance/queue.vue index 031bda2bed..f88825eb19 100644 --- a/src/client/pages/instance/queue.vue +++ b/src/client/pages/instance/queue.vue @@ -34,6 +34,7 @@ export default defineComponent({ [symbols.PAGE_INFO]: { title: this.$ts.jobQueue, icon: 'fas fa-clipboard-list', + bg: 'var(--bg)', }, connection: markRaw(os.stream.useChannel('queueStats')), } diff --git a/src/client/pages/instance/relays.vue b/src/client/pages/instance/relays.vue index cb9b27a625..7d7888eaa8 100644 --- a/src/client/pages/instance/relays.vue +++ b/src/client/pages/instance/relays.vue @@ -36,6 +36,7 @@ export default defineComponent({ [symbols.PAGE_INFO]: { title: this.$ts.relays, icon: 'fas fa-globe', + bg: 'var(--bg)', }, relays: [], inbox: '', diff --git a/src/client/pages/instance/security.vue b/src/client/pages/instance/security.vue index 2b525261ae..a854b6dbd0 100644 --- a/src/client/pages/instance/security.vue +++ b/src/client/pages/instance/security.vue @@ -47,7 +47,8 @@ export default defineComponent({ return { [symbols.PAGE_INFO]: { title: this.$ts.security, - icon: 'fas fa-lock' + icon: 'fas fa-lock', + bg: 'var(--bg)', }, enableHcaptcha: false, enableRecaptcha: false, diff --git a/src/client/pages/instance/service-worker.vue b/src/client/pages/instance/service-worker.vue index 9fa10def07..430e02ad2e 100644 --- a/src/client/pages/instance/service-worker.vue +++ b/src/client/pages/instance/service-worker.vue @@ -51,7 +51,8 @@ export default defineComponent({ return { [symbols.PAGE_INFO]: { title: 'ServiceWorker', - icon: 'fas fa-bolt' + icon: 'fas fa-bolt', + bg: 'var(--bg)', }, enableServiceWorker: false, swPublicKey: null, diff --git a/src/client/pages/instance/settings.vue b/src/client/pages/instance/settings.vue index 2ce9361d08..c9a948da7d 100644 --- a/src/client/pages/instance/settings.vue +++ b/src/client/pages/instance/settings.vue @@ -84,7 +84,8 @@ export default defineComponent({ return { [symbols.PAGE_INFO]: { title: this.$ts.general, - icon: 'fas fa-cog' + icon: 'fas fa-cog', + bg: 'var(--bg)', }, name: null, description: null, diff --git a/src/client/pages/instance/users.vue b/src/client/pages/instance/users.vue index 69242f3786..f7f9306b70 100644 --- a/src/client/pages/instance/users.vue +++ b/src/client/pages/instance/users.vue @@ -29,9 +29,11 @@ </div> <div class="inputs"> <MkInput v-model="searchUsername" style="flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.users.reload()"> + <template #prefix>@</template> <template #label>{{ $ts.username }}</template> </MkInput> <MkInput v-model="searchHost" style="flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.users.reload()" :disabled="pagination.params().origin === 'local'"> + <template #prefix>@</template> <template #label>{{ $ts.host }}</template> </MkInput> </div> diff --git a/src/client/pages/settings/index.link.vue b/src/client/pages/settings/index.link.vue deleted file mode 100644 index 895efffc9c..0000000000 --- a/src/client/pages/settings/index.link.vue +++ /dev/null @@ -1,105 +0,0 @@ -<template> -<div class="qmfkfnzj"> - <a v-if="external" class="main _button" :href="to" target="_blank"> - <span class="icon"><slot name="icon"></slot></span> - <span class="text"><slot></slot></span> - </a> - <MkA v-else-if="to" class="main _button" :class="{ active }" :to="to" :behavior="behavior"> - <span class="icon"><slot name="icon"></slot></span> - <span class="text"><slot></slot></span> - </MkA> - <button v-else class="main _button button" :class="{ danger }"> - <span class="icon"><slot name="icon"></slot></span> - <span class="text"><slot></slot></span> - </button> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; - -export default defineComponent({ - props: { - to: { - type: String, - required: false - }, - active: { - type: Boolean, - required: false - }, - danger: { - type: Boolean, - required: false - }, - external: { - type: Boolean, - required: false - }, - behavior: { - type: String, - required: false, - }, - }, -}); -</script> - -<style lang="scss" scoped> -.qmfkfnzj { - > .main { - display: flex; - align-items: center; - width: 100%; - box-sizing: border-box; - padding: 10px 16px 10px 14px; - border-radius: 999px; - font-size: 0.9em; - - &:hover { - text-decoration: none; - background: var(--panelHighlight); - } - - &.active { - color: var(--accent); - background: var(--accentedBg); - } - - &.danger { - color: var(--error); - } - - > .icon { - width: 32px; - margin-right: 2px; - flex-shrink: 0; - text-align: center; - opacity: 0.8; - - &:empty { - display: none; - - & + .text { - padding-left: 4px; - } - } - } - - > .text { - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - padding-right: 12px; - } - - > .right { - margin-left: auto; - opacity: 0.7; - - > .text:not(:empty) { - margin-right: 0.75em; - } - } - } -} -</style> diff --git a/src/client/pages/settings/index.vue b/src/client/pages/settings/index.vue index 2d5ced2181..39b9a85618 100644 --- a/src/client/pages/settings/index.vue +++ b/src/client/pages/settings/index.vue @@ -1,42 +1,9 @@ <template> <div class="vvcocwet" :class="{ wide: !narrow }" ref="el"> <div class="nav" v-if="!narrow || page == null"> - <div class="group accounts"> - <MkAvatar :user="$i" class="avatar"/> - <XLink :active="page === 'accounts'" replace to="/settings/accounts"><template #icon><i class="fas fa-users"></i></template>{{ $ts.accounts }}</XLink> - </div> + <div class="title">{{ $ts.settings }}</div> <MkInfo v-if="emailNotConfigured" warn class="info">{{ $ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ $ts.configure }}</MkA></MkInfo> - <div class="group"> - <div class="label">{{ $ts.basicSettings }}</div> - <XLink :active="page === 'profile'" replace to="/settings/profile"><template #icon><i class="fas fa-user"></i></template>{{ $ts.profile }}</XLink> - <XLink :active="page === 'privacy'" replace to="/settings/privacy"><template #icon><i class="fas fa-lock-open"></i></template>{{ $ts.privacy }}</XLink> - <XLink :active="page === 'reaction'" replace to="/settings/reaction"><template #icon><i class="fas fa-laugh"></i></template>{{ $ts.reaction }}</XLink> - <XLink :active="page === 'drive'" replace to="/settings/drive"><template #icon><i class="fas fa-cloud"></i></template>{{ $ts.drive }}</XLink> - <XLink :active="page === 'notifications'" replace to="/settings/notifications"><template #icon><i class="fas fa-bell"></i></template>{{ $ts.notifications }}</XLink> - <XLink :active="page === 'email'" replace to="/settings/email"><template #icon><i class="fas fa-envelope"></i></template>{{ $ts.email }}</XLink> - <XLink :active="page === 'integration'" replace to="/settings/integration"><template #icon><i class="fas fa-share-alt"></i></template>{{ $ts.integration }}</XLink> - <XLink :active="page === 'security'" replace to="/settings/security"><template #icon><i class="fas fa-lock"></i></template>{{ $ts.security }}</XLink> - </div> - <div class="group"> - <div class="label">{{ $ts.clientSettings }}</div> - <XLink :active="page === 'general'" replace to="/settings/general"><template #icon><i class="fas fa-cogs"></i></template>{{ $ts.general }}</XLink> - <XLink :active="page === 'theme'" replace to="/settings/theme"><template #icon><i class="fas fa-palette"></i></template>{{ $ts.theme }}</XLink> - <XLink :active="page === 'menu'" replace to="/settings/menu"><template #icon><i class="fas fa-list-ul"></i></template>{{ $ts.menu }}</XLink> - <XLink :active="page === 'sounds'" replace to="/settings/sounds"><template #icon><i class="fas fa-music"></i></template>{{ $ts.sounds }}</XLink> - <XLink :active="page === 'plugin'" replace to="/settings/plugin"><template #icon><i class="fas fa-plug"></i></template>{{ $ts.plugins }}</XLink> - </div> - <div class="group"> - <div class="label">{{ $ts.otherSettings }}</div> - <XLink :active="page === 'import-export'" replace to="/settings/import-export"><template #icon><i class="fas fa-boxes"></i></template>{{ $ts.importAndExport }}</XLink> - <XLink :active="page === 'mute-block'" replace to="/settings/mute-block"><template #icon><i class="fas fa-ban"></i></template>{{ $ts.muteAndBlock }}</XLink> - <XLink :active="page === 'word-mute'" replace to="/settings/word-mute"><template #icon><i class="fas fa-comment-slash"></i></template>{{ $ts.wordMute }}</XLink> - <XLink :active="page === 'api'" replace to="/settings/api"><template #icon><i class="fas fa-key"></i></template>API</XLink> - <XLink :active="page === 'other'" replace to="/settings/other"><template #icon><i class="fas fa-ellipsis-h"></i></template>{{ $ts.other }}</XLink> - </div> - <div class="group"> - <XLink @click="clear"><template #icon><i class="fas fa-trash"></i></template>{{ $ts.clearCache }}</XLink> - <XLink @click="logout" danger><template #icon><i class="fas fa-sign-in-alt fa-flip-horizontal"></i></template>{{ $ts.logout }}</XLink> - </div> + <MkSuperMenu :def="menuDef" :grid="page == null"></MkSuperMenu> </div> <div class="main"> <component :is="component" :key="page" v-bind="pageProps"/> @@ -47,8 +14,8 @@ <script lang="ts"> import { computed, defineAsyncComponent, defineComponent, nextTick, onMounted, reactive, ref, watch } from 'vue'; import { i18n } from '@client/i18n'; -import XLink from './index.link.vue'; import MkInfo from '@client/components/ui/info.vue'; +import MkSuperMenu from '@client/components/ui/super-menu.vue'; import { scroll } from '@client/scripts/scroll'; import { signout } from '@client/account'; import { unisonReload } from '@client/scripts/unison-reload'; @@ -58,8 +25,8 @@ import { $i } from '@client/account'; export default defineComponent({ components: { - XLink, MkInfo, + MkSuperMenu, }, props: { @@ -71,7 +38,7 @@ export default defineComponent({ setup(props, context) { const indexInfo = { - title: i18n.locale.settings, + title: i18n.locale.controllPanel, icon: 'fas fa-cog', bg: 'var(--bg)', }; @@ -80,6 +47,125 @@ export default defineComponent({ const narrow = ref(false); const view = ref(null); const el = ref(null); + const menuDef = computed(() => [{ + title: i18n.locale.basicSettings, + items: [{ + icon: 'fas fa-user', + text: i18n.locale.profile, + to: '/settings/profile', + active: page.value === 'profile', + }, { + icon: 'fas fa-lock-open', + text: i18n.locale.privacy, + to: '/settings/privacy', + active: page.value === 'privacy', + }, { + icon: 'fas fa-laugh', + text: i18n.locale.reaction, + to: '/settings/reaction', + active: page.value === 'reaction', + }, { + icon: 'fas fa-cloud', + text: i18n.locale.drive, + to: '/settings/drive', + active: page.value === 'drive', + }, { + icon: 'fas fa-bell', + text: i18n.locale.notifications, + to: '/settings/notifications', + active: page.value === 'notifications', + }, { + icon: 'fas fa-envelope', + text: i18n.locale.email, + to: '/settings/email', + active: page.value === 'email', + }, { + icon: 'fas fa-share-alt', + text: i18n.locale.integration, + to: '/settings/integration', + active: page.value === 'integration', + }, { + icon: 'fas fa-lock', + text: i18n.locale.security, + to: '/settings/security', + active: page.value === 'security', + }], + }, { + title: i18n.locale.clientSettings, + items: [{ + icon: 'fas fa-cogs', + text: i18n.locale.general, + to: '/settings/general', + active: page.value === 'general', + }, { + icon: 'fas fa-palette', + text: i18n.locale.theme, + to: '/settings/theme', + active: page.value === 'theme', + }, { + icon: 'fas fa-list-ul', + text: i18n.locale.menu, + to: '/settings/menu', + active: page.value === 'menu', + }, { + icon: 'fas fa-music', + text: i18n.locale.sounds, + to: '/settings/sounds', + active: page.value === 'sounds', + }, { + icon: 'fas fa-plug', + text: i18n.locale.plugins, + to: '/settings/plugin', + active: page.value === 'plugin', + }], + }, { + title: i18n.locale.otherSettings, + items: [{ + icon: 'fas fa-boxes', + text: i18n.locale.importAndExport, + to: '/settings/import-export', + active: page.value === 'import-export', + }, { + icon: 'fas fa-ban', + text: i18n.locale.muteAndBlock, + to: '/settings/mute-block', + active: page.value === 'mute-block', + }, { + icon: 'fas fa-comment-slash', + text: i18n.locale.wordMute, + to: '/settings/word-mute', + active: page.value === 'word-mute', + }, { + icon: 'fas fa-key', + text: 'API', + to: '/settings/api', + active: page.value === 'api', + }, { + icon: 'fas fa-ellipsis-h', + text: i18n.locale.other, + to: '/settings/other', + active: page.value === 'other', + }], + }, { + items: [{ + type: 'button', + icon: 'fas fa-trash', + text: i18n.locale.clearCache, + action: () => { + localStorage.removeItem('locale'); + localStorage.removeItem('theme'); + unisonReload(); + }, + }, { + type: 'button', + icon: 'fas fa-sign-in-alt fa-flip-horizontal', + text: i18n.locale.logout, + action: () => { + signout(); + }, + danger: true, + },], + }]); const pageProps = ref({}); const component = computed(() => { @@ -170,20 +256,13 @@ export default defineComponent({ return { [symbols.PAGE_INFO]: INFO, page, + menuDef, narrow, view, el, pageProps, component, emailNotConfigured, - logout: () => { - signout(); - }, - clear: () => { - localStorage.removeItem('locale'); - localStorage.removeItem('theme'); - unisonReload(); - }, }; }, }); @@ -192,16 +271,6 @@ export default defineComponent({ <style lang="scss" scoped> .vvcocwet { > .nav { - > .group { - padding: 16px; - - > .label { - font-size: 0.9em; - opacity: 0.7; - margin: 0 0 8px 12px; - } - } - > .info { margin: 0 16px; } diff --git a/src/client/ui/_common_/sidebar.vue b/src/client/ui/_common_/sidebar.vue index 4bb7bbd985..33aea31c39 100644 --- a/src/client/ui/_common_/sidebar.vue +++ b/src/client/ui/_common_/sidebar.vue @@ -50,7 +50,7 @@ import { host } from '@client/config'; import { search } from '@client/scripts/search'; import * as os from '@client/os'; import { menuDef } from '@client/menu'; -import { getAccounts, addAccount, login } from '@client/account'; +import { openAccountMenu } from '@client/account'; export default defineComponent({ props: { @@ -134,76 +134,12 @@ export default defineComponent({ search(); }, - async openAccountMenu(ev) { - const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== this.$i.id)); - const accountsPromise = os.api('users/show', { userIds: storedAccounts.map(x => x.id) }); - - const accountItemPromises = storedAccounts.map(a => new Promise(res => { - accountsPromise.then(accounts => { - const account = accounts.find(x => x.id === a.id); - if (account == null) return res(null); - res({ - type: 'user', - user: account, - action: () => { this.switchAccount(account); } - }); - }); - })); - - os.popupMenu([...[{ - type: 'link', - text: this.$ts.profile, - to: `/@${ this.$i.username }`, - avatar: this.$i, - }, null, ...accountItemPromises, { - icon: 'fas fa-plus', - text: this.$ts.addAccount, - action: () => { - os.popupMenu([{ - text: this.$ts.existingAccount, - action: () => { this.addAccount(); }, - }, { - text: this.$ts.createAccount, - action: () => { this.createAccount(); }, - }], ev.currentTarget || ev.target); - }, - }]], ev.currentTarget || ev.target, { - align: 'left' - }); - }, - more(ev) { os.popup(import('@client/components/launch-pad.vue'), {}, { }, 'closed'); }, - addAccount() { - os.popup(import('@client/components/signin-dialog.vue'), {}, { - done: res => { - addAccount(res.id, res.i); - os.success(); - }, - }, 'closed'); - }, - - createAccount() { - os.popup(import('@client/components/signup-dialog.vue'), {}, { - done: res => { - addAccount(res.id, res.i); - this.switchAccountWithToken(res.i); - }, - }, 'closed'); - }, - - async switchAccount(account: any) { - const storedAccounts = await getAccounts(); - const token = storedAccounts.find(x => x.id === account.id).token; - this.switchAccountWithToken(token); - }, - - switchAccountWithToken(token: string) { - login(token); - }, + openAccountMenu, } }); </script> diff --git a/src/client/ui/chat/index.vue b/src/client/ui/chat/index.vue index 4194a9919f..4c068b0d94 100644 --- a/src/client/ui/chat/index.vue +++ b/src/client/ui/chat/index.vue @@ -109,6 +109,7 @@ import { search } from '@client/scripts/search'; import copyToClipboard from '@client/scripts/copy-to-clipboard'; import { store } from './store'; import * as symbols from '@client/symbols'; +import { openAccountMenu } from '@client/account'; export default defineComponent({ components: { @@ -253,6 +254,8 @@ export default defineComponent({ } }], e); }, + + openAccountMenu, } }); </script> diff --git a/src/client/ui/default.header.vue b/src/client/ui/default.header.vue index 75c5c0c051..c64b6cf695 100644 --- a/src/client/ui/default.header.vue +++ b/src/client/ui/default.header.vue @@ -44,7 +44,7 @@ import { host } from '@client/config'; import { search } from '@client/scripts/search'; import * as os from '@client/os'; import { menuDef } from '@client/menu'; -import { getAccounts, addAccount, login } from '@client/account'; +import { openAccountMenu } from '@client/account'; import MkButton from '@client/components/ui/button.vue'; export default defineComponent({ @@ -100,76 +100,12 @@ export default defineComponent({ search(); }, - async openAccountMenu(ev) { - const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== this.$i.id)); - const accountsPromise = os.api('users/show', { userIds: storedAccounts.map(x => x.id) }); - - const accountItemPromises = storedAccounts.map(a => new Promise(res => { - accountsPromise.then(accounts => { - const account = accounts.find(x => x.id === a.id); - if (account == null) return res(null); - res({ - type: 'user', - user: account, - action: () => { this.switchAccount(account); } - }); - }); - })); - - os.popupMenu([...[{ - type: 'link', - text: this.$ts.profile, - to: `/@${ this.$i.username }`, - avatar: this.$i, - }, null, ...accountItemPromises, { - icon: 'fas fa-plus', - text: this.$ts.addAccount, - action: () => { - os.popupMenu([{ - text: this.$ts.existingAccount, - action: () => { this.addAccount(); }, - }, { - text: this.$ts.createAccount, - action: () => { this.createAccount(); }, - }], ev.currentTarget || ev.target); - }, - }]], ev.currentTarget || ev.target, { - align: 'left' - }); - }, - more(ev) { os.popup(import('@client/components/launch-pad.vue'), {}, { }, 'closed'); }, - addAccount() { - os.popup(import('@client/components/signin-dialog.vue'), {}, { - done: res => { - addAccount(res.id, res.i); - os.success(); - }, - }, 'closed'); - }, - - createAccount() { - os.popup(import('@client/components/signup-dialog.vue'), {}, { - done: res => { - addAccount(res.id, res.i); - this.switchAccountWithToken(res.i); - }, - }, 'closed'); - }, - - async switchAccount(account: any) { - const storedAccounts = await getAccounts(); - const token = storedAccounts.find(x => x.id === account.id).token; - this.switchAccountWithToken(token); - }, - - switchAccountWithToken(token: string) { - login(token); - }, + openAccountMenu, } }); </script> diff --git a/src/client/ui/default.sidebar.vue b/src/client/ui/default.sidebar.vue index be907aa2a4..745ad2d602 100644 --- a/src/client/ui/default.sidebar.vue +++ b/src/client/ui/default.sidebar.vue @@ -46,7 +46,7 @@ import { host } from '@client/config'; import { search } from '@client/scripts/search'; import * as os from '@client/os'; import { menuDef } from '@client/menu'; -import { getAccounts, addAccount, login } from '@client/account'; +import { openAccountMenu } from '@client/account'; import MkButton from '@client/components/ui/button.vue'; import { StickySidebar } from '@client/scripts/sticky-sidebar'; import MisskeyLogo from '@/../assets/client/misskey.svg'; @@ -120,76 +120,12 @@ export default defineComponent({ search(); }, - async openAccountMenu(ev) { - const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== this.$i.id)); - const accountsPromise = os.api('users/show', { userIds: storedAccounts.map(x => x.id) }); - - const accountItemPromises = storedAccounts.map(a => new Promise(res => { - accountsPromise.then(accounts => { - const account = accounts.find(x => x.id === a.id); - if (account == null) return res(null); - res({ - type: 'user', - user: account, - action: () => { this.switchAccount(account); } - }); - }); - })); - - os.popupMenu([...[{ - type: 'link', - text: this.$ts.profile, - to: `/@${ this.$i.username }`, - avatar: this.$i, - }, null, ...accountItemPromises, { - icon: 'fas fa-plus', - text: this.$ts.addAccount, - action: () => { - os.popupMenu([{ - text: this.$ts.existingAccount, - action: () => { this.addAccount(); }, - }, { - text: this.$ts.createAccount, - action: () => { this.createAccount(); }, - }], ev.currentTarget || ev.target); - }, - }]], ev.currentTarget || ev.target, { - align: 'left' - }); - }, - more(ev) { os.popup(import('@client/components/launch-pad.vue'), {}, { }, 'closed'); }, - addAccount() { - os.popup(import('@client/components/signin-dialog.vue'), {}, { - done: res => { - addAccount(res.id, res.i); - os.success(); - }, - }, 'closed'); - }, - - createAccount() { - os.popup(import('@client/components/signup-dialog.vue'), {}, { - done: res => { - addAccount(res.id, res.i); - this.switchAccountWithToken(res.i); - }, - }, 'closed'); - }, - - async switchAccount(account: any) { - const storedAccounts = await getAccounts(); - const token = storedAccounts.find(x => x.id === account.id).token; - this.switchAccountWithToken(token); - }, - - switchAccountWithToken(token: string) { - login(token); - }, + openAccountMenu, } }); </script>