diff options
| author | syuilo <4439005+syuilo@users.noreply.github.com> | 2025-03-30 20:44:00 +0900 |
|---|---|---|
| committer | syuilo <4439005+syuilo@users.noreply.github.com> | 2025-03-30 20:44:00 +0900 |
| commit | 87a723897611fff9b5ddda8a8bec3cdb427a21dc (patch) | |
| tree | 808415a4dfd20dfd3ff1a6456c47ed55e1f8c300 /packages/frontend/src/ui | |
| parent | perf(frontend): tweak MkRange (diff) | |
| download | sharkey-87a723897611fff9b5ddda8a8bec3cdb427a21dc.tar.gz sharkey-87a723897611fff9b5ddda8a8bec3cdb427a21dc.tar.bz2 sharkey-87a723897611fff9b5ddda8a8bec3cdb427a21dc.zip | |
enhance(frontend): デッキのオプションを追加
Diffstat (limited to 'packages/frontend/src/ui')
| -rw-r--r-- | packages/frontend/src/ui/_common_/navbar-h.vue | 214 | ||||
| -rw-r--r-- | packages/frontend/src/ui/deck.vue | 166 |
2 files changed, 324 insertions, 56 deletions
diff --git a/packages/frontend/src/ui/_common_/navbar-h.vue b/packages/frontend/src/ui/_common_/navbar-h.vue new file mode 100644 index 0000000000..c93935dd26 --- /dev/null +++ b/packages/frontend/src/ui/_common_/navbar-h.vue @@ -0,0 +1,214 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div class="azykntjl"> + <div class="body"> + <div class="left"> + <button v-click-anime class="item _button instance" @click="openInstanceMenu"> + <img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" draggable="false"/> + </button> + <MkA v-click-anime v-tooltip="i18n.ts.timeline" class="item index" activeClass="active" to="/" exact> + <i class="ti ti-home ti-fw"></i> + </MkA> + <template v-for="item in menu"> + <div v-if="item === '-'" class="divider"></div> + <component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime v-tooltip="navbarItemDef[item].title" class="item _button" :class="item" activeClass="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}"> + <i class="ti-fw" :class="navbarItemDef[item].icon"></i> + <span v-if="navbarItemDef[item].indicated" class="indicator _blink"><i class="_indicatorCircle"></i></span> + </component> + </template> + <div class="divider"></div> + <MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime v-tooltip="i18n.ts.controlPanel" class="item" activeClass="active" to="/admin" :behavior="settingsWindowed ? 'window' : null"> + <i class="ti ti-dashboard ti-fw"></i> + </MkA> + <button v-click-anime class="item _button" @click="more"> + <i class="ti ti-dots ti-fw"></i> + <span v-if="otherNavItemIndicated" class="indicator _blink"><i class="_indicatorCircle"></i></span> + </button> + </div> + <div class="right"> + <MkA v-click-anime v-tooltip="i18n.ts.settings" class="item" activeClass="active" to="/settings" :behavior="settingsWindowed ? 'window' : null"> + <i class="ti ti-settings ti-fw"></i> + </MkA> + <button v-click-anime class="item _button account" @click="openAccountMenu"> + <MkAvatar :user="$i" class="avatar"/><MkAcct class="acct" :user="$i"/> + </button> + <div class="post" @click="os.post()"> + <MkButton class="button" gradate full rounded> + <i class="ti ti-pencil ti-fw"></i> + </MkButton> + </div> + </div> + </div> +</div> +</template> + +<script lang="ts" setup> +import { computed, defineAsyncComponent, onMounted, ref } from 'vue'; +import { openInstanceMenu } from './common.js'; +import * as os from '@/os.js'; +import { navbarItemDef } from '@/navbar.js'; +import MkButton from '@/components/MkButton.vue'; +import { instance } from '@/instance.js'; +import { i18n } from '@/i18n.js'; +import { prefer } from '@/preferences.js'; +import { openAccountMenu as openAccountMenu_ } from '@/accounts.js'; +import { $i } from '@/i.js'; + +const WINDOW_THRESHOLD = 1400; + +const settingsWindowed = ref(window.innerWidth > WINDOW_THRESHOLD); +const menu = ref(prefer.s.menu); +// const menuDisplay = computed(store.makeGetterSetter('menuDisplay')); +const otherNavItemIndicated = computed<boolean>(() => { + for (const def in navbarItemDef) { + if (menu.value.includes(def)) continue; + if (navbarItemDef[def].indicated) return true; + } + return false; +}); + +function more(ev: MouseEvent) { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), { + src: ev.currentTarget ?? ev.target, + anchor: { x: 'center', y: 'bottom' }, + }, { + closed: () => dispose(), + }); +} + +function openAccountMenu(ev: MouseEvent) { + openAccountMenu_({ + withExtraOperation: true, + }, ev); +} + +onMounted(() => { + window.addEventListener('resize', () => { + settingsWindowed.value = (window.innerWidth >= WINDOW_THRESHOLD); + }, { passive: true }); +}); + +</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(from var(--MI_THEME-bg) srgb r g b / 0.75); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); + + > .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(--MI_THEME-navIndicator); + font-size: 8px; + } + + &:hover { + text-decoration: none; + color: var(--MI_THEME-navHoverFg); + } + + &.active { + color: var(--MI_THEME-navActive); + } + } + + > .divider { + display: inline-block; + height: 16px; + margin: 0 10px; + border-right: solid 0.5px var(--MI_THEME-divider); + } + + > .instance { + display: inline-block; + position: relative; + width: 56px; + height: 100%; + vertical-align: bottom; + + > img { + display: inline-block; + width: 24px; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + margin: auto; + } + } + + > .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/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue index 3f3bc32fad..3de8137404 100644 --- a/packages/frontend/src/ui/deck.vue +++ b/packages/frontend/src/ui/deck.vue @@ -4,37 +4,43 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div :class="[$style.root, { [$style.rootIsMobile]: isMobile }]"> - <XSidebar v-if="!isMobile"/> +<div :class="[$style.root, { [$style.withWallpaper]: withWallpaper }]"> + <XSidebar v-if="!isMobile && prefer.r['deck.navbarPosition'].value === 'left'"/> <div :class="$style.main"> + <XNavbarH v-if="!isMobile && prefer.r['deck.navbarPosition'].value === 'top'"/> + <XAnnouncements v-if="$i"/> <XStatusBars/> - <div ref="columnsEl" :class="[$style.sections, { [$style.center]: prefer.r['deck.columnAlign'].value === 'center', [$style.snapScroll]: snapScroll }]" @contextmenu.self.prevent="onContextmenu" @wheel.self="onWheel"> - <!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため --> - <section - v-for="ids in layout" - :class="$style.section" - :style="columns.filter(c => ids.includes(c.id)).some(c => c.flexible) ? { flex: 1, minWidth: '350px' } : { width: Math.max(...columns.filter(c => ids.includes(c.id)).map(c => c.width)) + 'px' }" - @wheel.self="onWheel" - > - <component - :is="columnComponents[columns.find(c => c.id === id)!.type] ?? XTlColumn" - v-for="id in ids" - :ref="id" - :key="id" - :class="$style.column" - :column="columns.find(c => c.id === id)!" - :isStacked="ids.length > 1" - @headerWheel="onWheel" - /> - </section> - <div v-if="layout.length === 0" class="_panel" :class="$style.onboarding"> - <div>{{ i18n.ts._deck.introduction }}</div> - <MkButton primary style="margin: 1em auto;" @click="addColumn">{{ i18n.ts._deck.addColumn }}</MkButton> - <div>{{ i18n.ts._deck.introduction2 }}</div> + + <div :class="$style.columnsWrapper"> + <div ref="columnsEl" :class="[$style.columns, { [$style.center]: prefer.r['deck.columnAlign'].value === 'center', [$style.snapScroll]: snapScroll }]" @contextmenu.self.prevent="onContextmenu" @wheel.self="onWheel"> + <!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため --> + <section + v-for="ids in layout" + :class="$style.section" + :style="columns.filter(c => ids.includes(c.id)).some(c => c.flexible) ? { flex: 1, minWidth: '350px' } : { width: Math.max(...columns.filter(c => ids.includes(c.id)).map(c => c.width)) + 'px' }" + @wheel.self="onWheel" + > + <component + :is="columnComponents[columns.find(c => c.id === id)!.type] ?? XTlColumn" + v-for="id in ids" + :ref="id" + :key="id" + :class="$style.column" + :column="columns.find(c => c.id === id)!" + :isStacked="ids.length > 1" + @headerWheel="onWheel" + /> + </section> + <div v-if="layout.length === 0" class="_panel" :class="$style.onboarding"> + <div>{{ i18n.ts._deck.introduction }}</div> + <MkButton primary style="margin: 1em auto;" @click="addColumn">{{ i18n.ts._deck.addColumn }}</MkButton> + <div>{{ i18n.ts._deck.introduction2 }}</div> + </div> </div> - <div :class="$style.sideMenu"> + + <div v-if="prefer.r['deck.menuPosition'].value === 'right'" :class="$style.sideMenu"> <div :class="$style.sideMenuTop"> <button v-tooltip.noDelay.left="`${i18n.ts._deck.profile}: ${prefer.s['deck.profile']}`" :class="$style.sideMenuButton" class="_button" @click="switchProfileMenu"><i class="ti ti-caret-down"></i></button> <button v-tooltip.noDelay.left="i18n.ts._deck.deleteProfile" :class="$style.sideMenuButton" class="_button" @click="deleteProfile"><i class="ti ti-trash"></i></button> @@ -47,18 +53,33 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> </div> - </div> - <div v-if="isMobile" :class="$style.nav"> - <button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator" class="_blink"><i class="_indicatorCircle"></i></span></button> - <button :class="$style.navButton" class="_button" @click="mainRouter.push('/')"><i :class="$style.navButtonIcon" class="ti ti-home"></i></button> - <button :class="$style.navButton" class="_button" @click="mainRouter.push('/my/notifications')"> - <i :class="$style.navButtonIcon" class="ti ti-bell"></i> - <span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator" class="_blink"> - <span class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ $i.unreadNotificationsCount > 99 ? '99+' : $i.unreadNotificationsCount }}</span> - </span> - </button> - <button :class="$style.postButton" class="_button" @click="os.post()"><i :class="$style.navButtonIcon" class="ti ti-pencil"></i></button> + <div v-if="prefer.r['deck.menuPosition'].value === 'bottom'" :class="$style.bottomMenu"> + <div :class="$style.bottomMenuLeft"> + <button v-tooltip.noDelay.left="`${i18n.ts._deck.profile}: ${prefer.s['deck.profile']}`" :class="$style.bottomMenuButton" class="_button" @click="switchProfileMenu"><i class="ti ti-caret-down"></i></button> + <button v-tooltip.noDelay.left="i18n.ts._deck.deleteProfile" :class="$style.bottomMenuButton" class="_button" @click="deleteProfile"><i class="ti ti-trash"></i></button> + </div> + <div :class="$style.bottomMenuMiddle"> + <button v-tooltip.noDelay.left="i18n.ts._deck.addColumn" :class="$style.bottomMenuButton" class="_button" @click="addColumn"><i class="ti ti-plus"></i></button> + </div> + <div :class="$style.bottomMenuRight"> + <button v-tooltip.noDelay.left="i18n.ts.settings" :class="$style.bottomMenuButton" class="_button" @click="showSettings"><i class="ti ti-settings"></i></button> + </div> + </div> + + <XNavbarH v-if="!isMobile && prefer.r['deck.navbarPosition'].value === 'bottom'"/> + + <div v-if="isMobile" :class="$style.nav"> + <button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator" class="_blink"><i class="_indicatorCircle"></i></span></button> + <button :class="$style.navButton" class="_button" @click="mainRouter.push('/')"><i :class="$style.navButtonIcon" class="ti ti-home"></i></button> + <button :class="$style.navButton" class="_button" @click="mainRouter.push('/my/notifications')"> + <i :class="$style.navButtonIcon" class="ti ti-bell"></i> + <span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator" class="_blink"> + <span class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ $i.unreadNotificationsCount > 99 ? '99+' : $i.unreadNotificationsCount }}</span> + </span> + </button> + <button :class="$style.postButton" class="_button" @click="os.post()"><i :class="$style.navButtonIcon" class="ti ti-pencil"></i></button> + </div> </div> <Transition @@ -92,10 +113,11 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, defineAsyncComponent, ref, useTemplateRef } from 'vue'; +import { computed, defineAsyncComponent, ref, useTemplateRef, watch } from 'vue'; import { v4 as uuid } from 'uuid'; import XCommon from './_common_/common.vue'; import XSidebar from '@/ui/_common_/navbar.vue'; +import XNavbarH from '@/ui/_common_/navbar-h.vue'; import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; @@ -116,6 +138,8 @@ import XDirectColumn from '@/ui/deck/direct-column.vue'; import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue'; import { mainRouter } from '@/router.js'; import { columns, layout, columnTypes, switchProfileMenu, addColumn as addColumnToStore, deleteProfile as deleteProfile_ } from '@/deck.js'; +import { miLocalStorage } from '@/local-storage.js'; + const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue')); const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue')); @@ -148,7 +172,9 @@ window.addEventListener('resize', () => { }); const snapScroll = deviceKind === 'smartphone' || deviceKind === 'tablet'; +const withWallpaper = miLocalStorage.getItem('wallpaper') != null; const drawerMenuShowing = ref(false); +const gap = prefer.r['deck.columnGap']; /* const route = 'TODO'; @@ -249,16 +275,18 @@ async function deleteProfile() { --MI-margin: var(--MI-marginHalf); - --columnGap: 6px; + --columnGap: v-bind("gap + 'px'"); display: flex; height: 100dvh; box-sizing: border-box; flex: 1; -} -.rootIsMobile { - padding-bottom: 100px; + &.withWallpaper { + .main { + background: transparent; + } + } } .main { @@ -266,15 +294,23 @@ async function deleteProfile() { min-width: 0; display: flex; flex-direction: column; + background: var(--MI_THEME-deckBg); } -.sections { +.columnsWrapper { + flex: 1; + display: flex; + flex-direction: row; +} + +.columns { flex: 1; display: flex; overflow-x: auto; overflow-y: clip; overscroll-behavior: contain; - background: var(--MI_THEME-deckBg); + padding: var(--columnGap); + gap: var(--columnGap); &.center { > .section:first-of-type { @@ -294,15 +330,10 @@ async function deleteProfile() { .section { display: flex; flex-direction: column; - scroll-snap-align: start; flex-shrink: 0; - padding-top: var(--columnGap); - padding-bottom: var(--columnGap); - padding-left: var(--columnGap); - - > .column:not(:last-of-type) { - margin-bottom: var(--columnGap); - } + gap: var(--columnGap); + scroll-snap-align: start; + scroll-margin-left: var(--columnGap); } .onboarding { @@ -341,6 +372,33 @@ async function deleteProfile() { margin-top: auto; } +.bottomMenu { + flex-shrink: 0; + display: flex; + flex-direction: row; + justify-content: center; + height: 32px; +} + +.bottomMenuButton { + display: block; + height: 100%; + aspect-ratio: 1; +} + +.bottomMenuLeft { + margin-right: auto; +} + +.bottomMenuMiddle { + margin-left: auto; + margin-right: auto; +} + +.bottomMenuRight { + margin-left: auto; +} + .menuBg { z-index: 1001; } @@ -360,10 +418,6 @@ async function deleteProfile() { } .nav { - position: fixed; - z-index: 1000; - bottom: 0; - left: 0; padding: 12px 12px max(12px, env(safe-area-inset-bottom, 0px)) 12px; display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; |