diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index a92d838388..36d1f62ac0 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -528,7 +528,7 @@ removeAllFollowing: "フォローを全解除" removeAllFollowingDescription: "{host}からのフォローをすべて解除します。そのインスタンスがもう存在しなくなった場合などに実行してください。" userSuspended: "このユーザーは凍結されています。" userSilenced: "このユーザーはサイレンスされています。" -sidebar: "サイドバー" +menu: "メニュー" divider: "分割線" addItem: "項目を追加" rooms: "ルーム" @@ -927,9 +927,10 @@ _channel: usersCount: "{n}人が参加中" notesCount: "{n}投稿があります" -_sidebar: - full: "フル" - icon: "アイコン" +_menuDisplay: + sideFull: "横" + sideIcon: "横(アイコン)" + top: "上部" hide: "隠す" _wordMute: diff --git a/src/client/components/launch-pad.vue b/src/client/components/launch-pad.vue index 58b74bdaee..6f97d4d3aa 100644 --- a/src/client/components/launch-pad.vue +++ b/src/client/components/launch-pad.vue @@ -36,7 +36,7 @@ <script lang="ts"> import { defineComponent } from 'vue'; import MkModal from '@client/components/ui/modal.vue'; -import { sidebarDef } from '@client/sidebar'; +import { menuDef } from '@client/menu'; import { instanceName } from '@client/config'; export default defineComponent({ @@ -48,7 +48,7 @@ export default defineComponent({ data() { return { - menuDef: sidebarDef, + menuDef: menuDef, items: [], instanceName, }; diff --git a/src/client/components/ui/container.vue b/src/client/components/ui/container.vue index 2e8eea7132..1940099096 100644 --- a/src/client/components/ui/container.vue +++ b/src/client/components/ui/container.vue @@ -191,6 +191,8 @@ export default defineComponent({ } > .content { + --stickyTop: 0px; + &.omitted { position: relative; max-height: var(--maxHeight); diff --git a/src/client/components/widgets.vue b/src/client/components/widgets.vue index 790ca56112..0baef86565 100644 --- a/src/client/components/widgets.vue +++ b/src/client/components/widgets.vue @@ -43,6 +43,7 @@ export default defineComponent({ props: { widgets: { + type: Array, required: true, }, edit: { diff --git a/src/client/sidebar.ts b/src/client/menu.ts similarity index 99% rename from src/client/sidebar.ts rename to src/client/menu.ts index 7686da10b2..8a9f4d4ac6 100644 --- a/src/client/sidebar.ts +++ b/src/client/menu.ts @@ -5,7 +5,7 @@ import { i18n } from '@client/i18n'; import { $i } from './account'; import { unisonReload } from '@client/scripts/unison-reload'; -export const sidebarDef = { +export const menuDef = { notifications: { title: 'notifications', icon: 'fas fa-bell', diff --git a/src/client/pages/settings/index.vue b/src/client/pages/settings/index.vue index 8a0171a6e8..17b373fcd8 100644 --- a/src/client/pages/settings/index.vue +++ b/src/client/pages/settings/index.vue @@ -26,7 +26,7 @@ <template #label>{{ $ts.clientSettings }}</template> <FormLink :active="page === 'general'" replace to="/settings/general"><template #icon><i class="fas fa-cogs"></i></template>{{ $ts.general }}</FormLink> <FormLink :active="page === 'theme'" replace to="/settings/theme"><template #icon><i class="fas fa-palette"></i></template>{{ $ts.theme }}</FormLink> - <FormLink :active="page === 'sidebar'" replace to="/settings/sidebar"><template #icon><i class="fas fa-list-ul"></i></template>{{ $ts.sidebar }}</FormLink> + <FormLink :active="page === 'menu'" replace to="/settings/menu"><template #icon><i class="fas fa-list-ul"></i></template>{{ $ts.menu }}</FormLink> <FormLink :active="page === 'sounds'" replace to="/settings/sounds"><template #icon><i class="fas fa-music"></i></template>{{ $ts.sounds }}</FormLink> <FormLink :active="page === 'plugin'" replace to="/settings/plugin"><template #icon><i class="fas fa-plug"></i></template>{{ $ts.plugins }}</FormLink> </FormGroup> @@ -121,7 +121,7 @@ export default defineComponent({ case 'theme': return defineAsyncComponent(() => import('./theme.vue')); case 'theme/install': return defineAsyncComponent(() => import('./theme.install.vue')); case 'theme/manage': return defineAsyncComponent(() => import('./theme.manage.vue')); - case 'sidebar': return defineAsyncComponent(() => import('./sidebar.vue')); + case 'menu': return defineAsyncComponent(() => import('./menu.vue')); case 'sounds': return defineAsyncComponent(() => import('./sounds.vue')); case 'custom-css': return defineAsyncComponent(() => import('./custom-css.vue')); case 'deck': return defineAsyncComponent(() => import('./deck.vue')); diff --git a/src/client/pages/settings/sidebar.vue b/src/client/pages/settings/menu.vue similarity index 73% rename from src/client/pages/settings/sidebar.vue rename to src/client/pages/settings/menu.vue index d91cae42f4..4b315145e1 100644 --- a/src/client/pages/settings/sidebar.vue +++ b/src/client/pages/settings/menu.vue @@ -1,18 +1,18 @@ <template> <FormBase> - <FormTextarea v-model:value="items" tall> - <span>{{ $ts.sidebar }}</span> + <FormTextarea v-model:value="items" tall manual-save> + <span>{{ $ts.menu }}</span> <template #desc><button class="_textButton" @click="addItem">{{ $ts.addItem }}</button></template> </FormTextarea> - <FormRadios v-model="sidebarDisplay"> + <FormRadios v-model="menuDisplay"> <template #desc>{{ $ts.display }}</template> - <option value="full">{{ $ts._sidebar.full }}</option> - <option value="icon">{{ $ts._sidebar.icon }}</option> - <!-- <MkRadio v-model="sidebarDisplay" value="hide" disabled>{{ $ts._sidebar.hide }}</MkRadio>--> <!-- TODO: サイドバーを完全に隠せるようにすると、別途ハンバーガーボタンのようなものをUIに表示する必要があり面倒 --> + <option value="sideFull">{{ $ts._menuDisplay.sideFull }}</option> + <option value="sideIcon">{{ $ts._menuDisplay.sideIcon }}</option> + <option value="top">{{ $ts._menuDisplay.top }}</option> + <!-- <MkRadio v-model="menuDisplay" value="hide" disabled>{{ $ts._menuDisplay.hide }}</MkRadio>--> <!-- TODO: サイドバーを完全に隠せるようにすると、別途ハンバーガーボタンのようなものをUIに表示する必要があり面倒 --> </FormRadios> - <FormButton @click="save()" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> <FormButton @click="reset()" danger><i class="fas fa-redo"></i> {{ $ts.default }}</FormButton> </FormBase> </template> @@ -26,7 +26,7 @@ import FormBase from '@client/components/form/base.vue'; import FormGroup from '@client/components/form/group.vue'; import FormButton from '@client/components/form/button.vue'; import * as os from '@client/os'; -import { sidebarDef } from '@client/sidebar'; +import { menuDef } from '@client/menu'; import { defaultStore } from '@client/store'; import * as symbols from '@client/symbols'; import { unisonReload } from '@client/scripts/unison-reload'; @@ -44,11 +44,11 @@ export default defineComponent({ data() { return { [symbols.PAGE_INFO]: { - title: this.$ts.sidebar, + title: this.$ts.menu, icon: 'fas fa-list-ul' }, - menuDef: sidebarDef, - items: '', + menuDef: menuDef, + items: defaultStore.state.menu.join('\n'), } }, @@ -57,11 +57,17 @@ export default defineComponent({ return this.items.trim().split('\n').filter(x => x.trim() !== ''); }, - sidebarDisplay: defaultStore.makeGetterSetter('sidebarDisplay') + menuDisplay: defaultStore.makeGetterSetter('menuDisplay') }, - created() { - this.items = this.$store.state.menu.join('\n'); + watch: { + menuDisplay() { + this.reloadAsk(); + }, + + items() { + this.save(); + }, }, mounted() { @@ -85,7 +91,6 @@ export default defineComponent({ }); if (canceled) return; this.items = [...this.splited, item].join('\n'); - this.save(); }, save() { @@ -96,7 +101,6 @@ export default defineComponent({ reset() { this.$store.reset('menu'); this.items = this.$store.state.menu.join('\n'); - this.reloadAsk(); }, async reloadAsk() { diff --git a/src/client/scripts/sticky-sidebar.ts b/src/client/scripts/sticky-sidebar.ts index 18670bc037..c67b8f37ac 100644 --- a/src/client/scripts/sticky-sidebar.ts +++ b/src/client/scripts/sticky-sidebar.ts @@ -7,8 +7,9 @@ export class StickySidebar { private isTop = false; private isBottom = false; private offsetTop: number; + private globalHeaderHeight: number = 59; - constructor(container: StickySidebar['container'], marginTop = 0) { + constructor(container: StickySidebar['container'], marginTop = 0, globalHeaderHeight = 0) { this.container = container; this.el = this.container.children[0] as HTMLElement; this.el.style.position = 'sticky'; @@ -16,30 +17,31 @@ export class StickySidebar { this.container.prepend(this.spacer); this.marginTop = marginTop; this.offsetTop = this.container.getBoundingClientRect().top; + this.globalHeaderHeight = globalHeaderHeight; } public calc(scrollTop: number) { if (scrollTop > this.lastScrollTop) { // downscroll - const overflow = Math.max(0, (this.el.clientHeight + this.marginTop) - window.innerHeight); + const overflow = Math.max(0, this.globalHeaderHeight + (this.el.clientHeight + this.marginTop) - window.innerHeight); this.el.style.bottom = null; - this.el.style.top = `${-overflow + this.marginTop}px`; + this.el.style.top = `${-overflow + this.marginTop + this.globalHeaderHeight}px`; this.isBottom = (scrollTop + window.innerHeight) >= (this.el.offsetTop + this.el.clientHeight); if (this.isTop) { this.isTop = false; - this.spacer.style.marginTop = `${Math.max(0, this.lastScrollTop + this.marginTop - this.offsetTop)}px`; + this.spacer.style.marginTop = `${Math.max(0, this.globalHeaderHeight + this.lastScrollTop + this.marginTop - this.offsetTop)}px`; } } else { // upscroll - const overflow = (this.el.clientHeight + this.marginTop) - window.innerHeight; + const overflow = this.globalHeaderHeight + (this.el.clientHeight + this.marginTop) - window.innerHeight; this.el.style.top = null; this.el.style.bottom = `${-overflow}px`; - this.isTop = scrollTop <= this.el.offsetTop; + this.isTop = scrollTop + this.marginTop + this.globalHeaderHeight <= this.el.offsetTop; if (this.isBottom) { this.isBottom = false; - this.spacer.style.marginTop = `${this.lastScrollTop + this.marginTop - this.offsetTop - overflow}px`; + this.spacer.style.marginTop = `${this.globalHeaderHeight + this.lastScrollTop + this.marginTop - this.offsetTop - overflow}px`; } } diff --git a/src/client/store.ts b/src/client/store.ts index 07ea1868a6..04f9e83c61 100644 --- a/src/client/store.ts +++ b/src/client/store.ts @@ -90,6 +90,7 @@ export const defaultStore = markRaw(new Storage('base', { default: [] as { name: string; id: string; + place: string; data: Record<string, any>; }[] }, @@ -185,9 +186,9 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: false }, - sidebarDisplay: { + menuDisplay: { where: 'device', - default: 'full' as 'full' | 'icon' + default: 'sideFull' as 'sideFull' | 'sideIcon' | 'top' }, reportError: { where: 'device', diff --git a/src/client/style.scss b/src/client/style.scss index dc419bd872..578e7543c7 100644 --- a/src/client/style.scss +++ b/src/client/style.scss @@ -161,7 +161,7 @@ hr { background: none; border: none; cursor: pointer; - color: var(--fg); + color: inherit; touch-action: manipulation; tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent; diff --git a/src/client/ui/_common_/sidebar.vue b/src/client/ui/_common_/sidebar.vue index df11877147..073907cde9 100644 --- a/src/client/ui/_common_/sidebar.vue +++ b/src/client/ui/_common_/sidebar.vue @@ -49,7 +49,7 @@ import { defineComponent } from 'vue'; import { host } from '@client/config'; import { search } from '@client/scripts/search'; import * as os from '@client/os'; -import { sidebarDef } from '@client/sidebar'; +import { menuDef } from '@client/menu'; import { getAccounts, addAccount, login } from '@client/account'; export default defineComponent({ @@ -67,7 +67,7 @@ export default defineComponent({ showing: false, accounts: [], connection: null, - menuDef: sidebarDef, + menuDef: menuDef, iconOnly: false, hidden: this.defaultHidden, }; @@ -92,7 +92,7 @@ export default defineComponent({ this.showing = false; }, - '$store.reactiveState.sidebarDisplay.value'() { + '$store.reactiveState.menuDisplay.value'() { this.calcViewState(); }, @@ -116,7 +116,7 @@ export default defineComponent({ methods: { calcViewState() { - this.iconOnly = (window.innerWidth <= 1279) || (this.$store.state.sidebarDisplay === 'icon'); + this.iconOnly = (window.innerWidth <= 1279) || (this.$store.state.menuDisplay === 'sideIcon'); if (!this.defaultHidden) { this.hidden = (window.innerWidth <= 650); } diff --git a/src/client/ui/chat/index.vue b/src/client/ui/chat/index.vue index c28436ed5c..d45369e8b0 100644 --- a/src/client/ui/chat/index.vue +++ b/src/client/ui/chat/index.vue @@ -142,7 +142,7 @@ import XTimeline from './timeline.vue'; import XHeaderClock from './header-clock.vue'; import * as os from '@client/os'; import { router } from '@client/router'; -import { sidebarDef } from '@client/sidebar'; +import { menuDef } from '@client/menu'; import { search } from '@client/scripts/search'; import copyToClipboard from '@client/scripts/copy-to-clipboard'; import { store } from './store'; @@ -190,7 +190,7 @@ export default defineComponent({ followedChannels: null, featuredChannels: null, currentChannel: null, - menuDef: sidebarDef, + menuDef: menuDef, sideViewOpening: false, instanceName, }; diff --git a/src/client/ui/deck.vue b/src/client/ui/deck.vue index 935445a54d..4b0189ba77 100644 --- a/src/client/ui/deck.vue +++ b/src/client/ui/deck.vue @@ -37,7 +37,7 @@ import DeckColumnCore from '@client/ui/deck/column-core.vue'; import XSidebar from '@client/ui/_common_/sidebar.vue'; import { getScrollContainer } from '@client/scripts/scroll'; import * as os from '@client/os'; -import { sidebarDef } from '@client/sidebar'; +import { menuDef } from '@client/menu'; import XCommon from './_common_/common.vue'; import { deckStore, addColumn, loadDeck } from './deck/deck-store'; @@ -60,7 +60,7 @@ export default defineComponent({ return { deckStore, host: host, - menuDef: sidebarDef, + menuDef: menuDef, wallpaper: localStorage.getItem('wallpaper') != null, }; }, diff --git a/src/client/ui/default.header.vue b/src/client/ui/default.header.vue new file mode 100644 index 0000000000..a67883020f --- /dev/null +++ b/src/client/ui/default.header.vue @@ -0,0 +1,274 @@ +<template> +<div class="azykntjl"> + <div class="body"> + <div class="left"> + <MkA class="item index" active-class="active" to="/" exact v-click-anime v-tooltip="$ts.timeline"> + <i class="fas fa-home fa-fw"></i> + </MkA> + <template v-for="item in menu"> + <div v-if="item === '-'" class="divider"></div> + <component v-else-if="menuDef[item] && (menuDef[item].show !== false)" :is="menuDef[item].to ? 'MkA' : 'button'" class="item _button" :class="item" active-class="active" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}" :to="menuDef[item].to" v-click-anime v-tooltip="$ts[menuDef[item].title]"> + <i class="fa-fw" :class="menuDef[item].icon"></i> + <span v-if="menuDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span> + </component> + </template> + <div class="divider"></div> + <MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/instance" :behavior="settingsWindowed ? 'modalWindow' : null" v-click-anime v-tooltip="$ts.instance"> + <i class="fas fa-server fa-fw"></i> + </MkA> + <button class="item _button" @click="more" v-click-anime> + <i class="fas fa-ellipsis-h fa-fw"></i> + <span v-if="otherNavItemIndicated" class="indicator"><i class="fas fa-circle"></i></span> + </button> + </div> + <div class="right"> + <MkA class="item" active-class="active" to="/settings" :behavior="settingsWindowed ? 'modalWindow' : null" v-click-anime v-tooltip="$ts.settings"> + <i class="fas fa-cog fa-fw"></i> + </MkA> + <button class="item _button account" @click="openAccountMenu" v-click-anime> + <MkAvatar :user="$i" class="avatar"/><MkAcct class="acct" :user="$i"/> + </button> + <div class="post" @click="post"> + <MkButton class="button" primary full> + <i class="fas fa-pencil-alt fa-fw"></i> + </MkButton> + </div> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import { defineComponent } from 'vue'; +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 MkButton from '@client/components/ui/button.vue'; + +export default defineComponent({ + components: { + MkButton, + }, + + data() { + return { + host: host, + accounts: [], + connection: null, + menuDef: menuDef, + settingsWindowed: false, + }; + }, + + computed: { + menu(): string[] { + return this.$store.state.menu; + }, + + otherNavItemIndicated(): boolean { + for (const def in this.menuDef) { + if (this.menu.includes(def)) continue; + if (this.menuDef[def].indicated) return true; + } + return false; + }, + }, + + watch: { + '$store.reactiveState.menuDisplay.value'() { + this.calcViewState(); + }, + }, + + created() { + window.addEventListener('resize', this.calcViewState); + this.calcViewState(); + }, + + methods: { + calcViewState() { + this.settingsWindowed = (window.innerWidth > 1400); + }, + + post() { + os.post(); + }, + + search() { + search(); + }, + + async openAccountMenu(ev) { + const storedAccounts = getAccounts().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.modalMenu([...[{ + 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.modalMenu([{ + 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'); + }, + + switchAccount(account: any) { + const storedAccounts = getAccounts(); + const token = storedAccounts.find(x => x.id === account.id).token; + this.switchAccountWithToken(token); + }, + + switchAccountWithToken(token: string) { + login(token); + }, + } +}); +</script> + +<style lang="scss" scoped> +.azykntjl { + $height: 60px; + $avatar-size: 32px; + $avatar-margin: 8px; + + position: sticky; + top: 0; + z-index: 1000; + width: 100%; + height: $height; + background-color: var(--bg); + + > .body { + max-width: 1380px; + margin: 0 auto; + display: flex; + + > .right, + > .left { + + > .item { + position: relative; + font-size: 0.9em; + display: inline-block; + padding: 0 12px; + line-height: $height; + + > i, + > .avatar { + margin-right: 0; + } + + > i { + left: 10px; + } + + > .avatar { + width: $avatar-size; + height: $avatar-size; + vertical-align: middle; + } + + > .indicator { + position: absolute; + top: 0; + left: 0; + color: var(--navIndicator); + font-size: 8px; + animation: blink 1s infinite; + } + + &:hover { + text-decoration: none; + color: var(--navHoverFg); + } + + &.active { + color: var(--navActive); + } + } + + > .divider { + display: inline-block; + height: 16px; + margin: 0 10px; + border-right: solid 0.5px var(--divider); + } + + > .post { + display: inline-block; + + > .button { + width: 40px; + height: 40px; + padding: 0; + min-width: 0; + } + } + + > .account { + display: inline-flex; + align-items: center; + vertical-align: top; + margin-right: 8px; + + > .acct { + margin-left: 8px; + } + } + } + + > .right { + margin-left: auto; + } + } +} +</style> diff --git a/src/client/ui/default.sidebar.vue b/src/client/ui/default.sidebar.vue index c7e2d30c7a..2e0336878d 100644 --- a/src/client/ui/default.sidebar.vue +++ b/src/client/ui/default.sidebar.vue @@ -45,7 +45,7 @@ import { defineComponent } from 'vue'; import { host } from '@client/config'; import { search } from '@client/scripts/search'; import * as os from '@client/os'; -import { sidebarDef } from '@client/sidebar'; +import { menuDef } from '@client/menu'; import { getAccounts, addAccount, login } from '@client/account'; import MkButton from '@client/components/ui/button.vue'; import { StickySidebar } from '@client/scripts/sticky-sidebar'; @@ -62,7 +62,7 @@ export default defineComponent({ host: host, accounts: [], connection: null, - menuDef: sidebarDef, + menuDef: menuDef, iconOnly: false, settingsWindowed: false, }; @@ -83,7 +83,7 @@ export default defineComponent({ }, watch: { - '$store.reactiveState.sidebarDisplay.value'() { + '$store.reactiveState.menuDisplay.value'() { this.calcViewState(); }, @@ -108,7 +108,7 @@ export default defineComponent({ methods: { calcViewState() { - this.iconOnly = (window.innerWidth <= 1400) || (this.$store.state.sidebarDisplay === 'icon'); + this.iconOnly = (window.innerWidth <= 1400) || (this.$store.state.menuDisplay === 'sideIcon'); this.settingsWindowed = (window.innerWidth > 1400); }, diff --git a/src/client/ui/default.vue b/src/client/ui/default.vue index 3c87bf7ab4..f18685d78c 100644 --- a/src/client/ui/default.vue +++ b/src/client/ui/default.vue @@ -1,9 +1,16 @@ <template> <div class="mk-app" :class="{ wallpaper, isMobile }"> - <div class="columns" :class="{ fullView }"> - <div class="sidebar" ref="sidebar" v-if="!isMobile"> - <XSidebar/> - </div> + <XHeaderMenu v-if="showMenuOnTop"/> + + <div class="columns" :class="{ fullView, withGlobalHeader: showMenuOnTop }"> + <template v-if="!isMobile"> + <div class="sidebar" v-if="!showMenuOnTop"> + <XSidebar/> + </div> + <div class="widgets left" ref="widgetsLeft" v-else> + <XWidgets @mounted="attachSticky('widgetsLeft')" :place="'left'"/> + </div> + </template> <main class="main _panel" @contextmenu.stop="onContextmenu"> <header class="header" @click="onHeaderClick"> @@ -20,8 +27,8 @@ </div> </main> - <div v-if="isDesktop" class="widgets" ref="widgets"> - <XWidgets @mounted="attachSticky"/> + <div v-if="isDesktop" class="widgets right" ref="widgetsRight"> + <XWidgets @mounted="attachSticky('widgetsRight')" :place="null"/> </div> </div> @@ -60,7 +67,7 @@ import XDrawerSidebar from '@client/ui/_common_/sidebar.vue'; import XCommon from './_common_/common.vue'; import XHeader from './_common_/header.vue'; import * as os from '@client/os'; -import { sidebarDef } from '@client/sidebar'; +import { menuDef } from '@client/menu'; import * as symbols from '@client/symbols'; const DESKTOP_THRESHOLD = 1100; @@ -72,13 +79,14 @@ export default defineComponent({ XSidebar, XDrawerSidebar, XHeader, + XHeaderMenu: defineAsyncComponent(() => import('./default.header.vue')), XWidgets: defineAsyncComponent(() => import('./default.widgets.vue')), }, data() { return { pageInfo: null, - menuDef: sidebarDef, + menuDef: menuDef, isMobile: window.innerWidth <= MOBILE_THRESHOLD, isDesktop: window.innerWidth >= DESKTOP_THRESHOLD, widgetsShowing: false, @@ -94,6 +102,10 @@ export default defineComponent({ if (this.menuDef[def].indicated) return true; } return false; + }, + + showMenuOnTop(): boolean { + return !this.isMobile && this.$store.state.menuDisplay === 'top'; } }, @@ -130,8 +142,8 @@ export default defineComponent({ } }, - attachSticky() { - const sticky = new StickySidebar(this.$refs.widgets, 16); + attachSticky(ref) { + const sticky = new StickySidebar(this.$refs[ref], this.$store.state.menuDisplay === 'top' ? 0 : 16, this.$store.state.menuDisplay === 'top' ? 60 : 0); // TODO: ヘッダーの高さを60pxと決め打ちしているのを直す window.addEventListener('scroll', () => { sticky.calc(window.scrollY); }, { passive: true }); @@ -285,7 +297,7 @@ export default defineComponent({ > .header { position: sticky; z-index: 1000; - top: 0; + top: var(--globalHeaderHeight, 0px); height: $header-height; line-height: $header-height; -webkit-backdrop-filter: blur(32px); @@ -296,7 +308,7 @@ export default defineComponent({ > .content { background: var(--bg); - --stickyTop: #{$header-height}; + --stickyTop: calc(var(--globalHeaderHeight, 0px) + #{$header-height}); } @media (max-width: 850px) { @@ -317,12 +329,31 @@ export default defineComponent({ @media (max-width: $widgets-hide-threshold) { display: none; } + + &.left { + margin-right: 16px; + } } > .sidebar { margin-top: 16px; } + &.withGlobalHeader { + --globalHeaderHeight: 60px; // TODO: 60pxと決め打ちしているのを直す + + > .main { + margin-top: 2px; + border-radius: var(--radius); + box-shadow: 0 0 0 2px var(--divider); + } + + > .widgets { + --stickyTop: var(--globalHeaderHeight); + margin-top: 0px; + } + } + @media (max-width: 850px) { margin: 0; diff --git a/src/client/ui/default.widgets.vue b/src/client/ui/default.widgets.vue index cf5d1e07ce..0bacc83d52 100644 --- a/src/client/ui/default.widgets.vue +++ b/src/client/ui/default.widgets.vue @@ -1,6 +1,6 @@ <template> <div class="ddiqwdnk"> - <XWidgets class="widgets" :edit="editMode" :widgets="$store.reactiveState.widgets.value" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="editMode = false"/> + <XWidgets class="widgets" :edit="editMode" :widgets="$store.reactiveState.widgets.value.filter(w => w.place === place)" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="editMode = false"/> <MkAd class="a" prefer="square"/> <button v-if="editMode" @click="editMode = false" class="_textButton edit" style="font-size: 0.9em;"><i class="fas fa-check"></i> {{ $ts.editWidgetsExit }}</button> @@ -11,13 +11,18 @@ <script lang="ts"> import { defineComponent, defineAsyncComponent } from 'vue'; import XWidgets from '@client/components/widgets.vue'; -import * as os from '@client/os'; export default defineComponent({ components: { XWidgets }, + props: { + place: { + type: String, + } + }, + emits: ['mounted'], data() { @@ -34,7 +39,7 @@ export default defineComponent({ addWidget(widget) { this.$store.set('widgets', [{ ...widget, - place: null, + place: this.place, }, ...this.$store.state.widgets]); }, @@ -50,7 +55,10 @@ export default defineComponent({ }, updateWidgets(widgets) { - this.$store.set('widgets', widgets); + this.$store.set('widgets', [ + ...this.$store.state.widgets.filter(w => w.place !== this.place), + ...widgets + ]); } } }); diff --git a/src/client/ui/desktop.vue b/src/client/ui/desktop.vue index a60aed6841..b256527a4a 100644 --- a/src/client/ui/desktop.vue +++ b/src/client/ui/desktop.vue @@ -13,7 +13,7 @@ import { search } from '@client/scripts/search'; import XCommon from './_common_/common.vue'; import * as os from '@client/os'; import XSidebar from '@client/ui/_common_/sidebar.vue'; -import { sidebarDef } from '@client/sidebar'; +import { menuDef } from '@client/menu'; import { ColdDeviceStorage } from '@client/store'; export default defineComponent({ @@ -33,7 +33,7 @@ export default defineComponent({ data() { return { host: host, - menuDef: sidebarDef, + menuDef: menuDef, wallpaper: localStorage.getItem('wallpaper') != null, }; }, diff --git a/src/client/ui/universal.vue b/src/client/ui/universal.vue index fb67ea8985..1e8c4b36d5 100644 --- a/src/client/ui/universal.vue +++ b/src/client/ui/universal.vue @@ -61,7 +61,7 @@ import XCommon from './_common_/common.vue'; import XHeader from './_common_/header.vue'; import XSide from './default.side.vue'; import * as os from '@client/os'; -import { sidebarDef } from '@client/sidebar'; +import { menuDef } from '@client/menu'; import * as symbols from '@client/symbols'; const DESKTOP_THRESHOLD = 1100; @@ -87,7 +87,7 @@ export default defineComponent({ return { pageInfo: null, isDesktop: window.innerWidth >= DESKTOP_THRESHOLD, - menuDef: sidebarDef, + menuDef: menuDef, navHidden: false, widgetsShowing: false, wallpaper: localStorage.getItem('wallpaper') != null,