1
0

Merge tag '2025.4.0' into merge/2025-03-24

# Conflicts:
#	.github/workflows/storybook.yml
#	locales/index.d.ts
#	package.json
#	packages/backend/src/models/json-schema/role.ts
#	packages/frontend/src/components/MkPageWindow.vue
#	packages/frontend/src/pages/admin/roles.editor.vue
#	packages/frontend/src/pages/admin/roles.vue
#	packages/frontend/src/pages/settings/preferences.vue
#	packages/frontend/src/pages/settings/privacy.vue
#	packages/frontend/src/pages/timeline.vue
#	packages/frontend/src/pref-migrate.ts
#	packages/frontend/src/ui/_common_/common.vue
#	packages/frontend/src/ui/deck.vue
#	packages/frontend/src/ui/universal.vue
#	packages/misskey-js/src/autogen/types.ts
This commit is contained in:
Hazelnoot
2025-04-13 13:04:57 -04:00
98 changed files with 1621 additions and 2087 deletions
@@ -4,6 +4,59 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<Transition
:enterActiveClass="prefer.s.animation ? $style.transition_menuDrawerBg_enterActive : ''"
:leaveActiveClass="prefer.s.animation ? $style.transition_menuDrawerBg_leaveActive : ''"
:enterFromClass="prefer.s.animation ? $style.transition_menuDrawerBg_enterFrom : ''"
:leaveToClass="prefer.s.animation ? $style.transition_menuDrawerBg_leaveTo : ''"
>
<div
v-if="drawerMenuShowing"
:class="$style.menuDrawerBg"
class="_modalBg"
@click="drawerMenuShowing = false"
@touchstart.passive="drawerMenuShowing = false"
></div>
</Transition>
<Transition
:enterActiveClass="prefer.s.animation ? $style.transition_menuDrawer_enterActive : ''"
:leaveActiveClass="prefer.s.animation ? $style.transition_menuDrawer_leaveActive : ''"
:enterFromClass="prefer.s.animation ? $style.transition_menuDrawer_enterFrom : ''"
:leaveToClass="prefer.s.animation ? $style.transition_menuDrawer_leaveTo : ''"
>
<div v-if="drawerMenuShowing" :class="$style.menuDrawer">
<XDrawerMenu/>
</div>
</Transition>
<Transition
:enterActiveClass="prefer.s.animation ? $style.transition_widgetsDrawerBg_enterActive : ''"
:leaveActiveClass="prefer.s.animation ? $style.transition_widgetsDrawerBg_leaveActive : ''"
:enterFromClass="prefer.s.animation ? $style.transition_widgetsDrawerBg_enterFrom : ''"
:leaveToClass="prefer.s.animation ? $style.transition_widgetsDrawerBg_leaveTo : ''"
>
<div
v-if="widgetsShowing"
:class="$style.widgetsDrawerBg"
class="_modalBg"
@click="widgetsShowing = false"
@touchstart.passive="widgetsShowing = false"
></div>
</Transition>
<Transition
:enterActiveClass="prefer.s.animation ? $style.transition_widgetsDrawer_enterActive : ''"
:leaveActiveClass="prefer.s.animation ? $style.transition_widgetsDrawer_leaveActive : ''"
:enterFromClass="prefer.s.animation ? $style.transition_widgetsDrawer_enterFrom : ''"
:leaveToClass="prefer.s.animation ? $style.transition_widgetsDrawer_leaveTo : ''"
>
<div v-if="widgetsShowing" :class="$style.widgetsDrawer">
<button class="_button" :class="$style.widgetsCloseButton" @click="widgetsShowing = false"><i class="ti ti-x"></i></button>
<XWidgets/>
</div>
</Transition>
<component
:is="popup.component"
v-for="popup in popups"
@@ -61,11 +114,16 @@ import { useStream } from '@/stream.js';
import { i18n } from '@/i18n.js';
import { prefer } from '@/preferences.js';
import { globalEvents } from '@/events.js';
import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
import { store } from '@/store.js';
const XStreamIndicator = defineAsyncComponent(() => import('./stream-indicator.vue'));
const XUpload = defineAsyncComponent(() => import('./upload.vue'));
const SkOneko = defineAsyncComponent(() => import('@/components/SkOneko.vue'));
const XWidgets = defineAsyncComponent(() => import('./widgets.vue'));
const drawerMenuShowing = defineModel<boolean>('drawerMenuShowing');
const widgetsShowing = defineModel<boolean>('widgetsShowing');
const dev = _DEV_;
@@ -108,6 +166,50 @@ function getPointerEvents() {
</script>
<style lang="scss" module>
.transition_menuDrawerBg_enterActive,
.transition_menuDrawerBg_leaveActive {
opacity: 1;
transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.transition_menuDrawerBg_enterFrom,
.transition_menuDrawerBg_leaveTo {
opacity: 0;
}
.transition_menuDrawer_enterActive,
.transition_menuDrawer_leaveActive {
opacity: 1;
transform: translateX(0);
transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.transition_menuDrawer_enterFrom,
.transition_menuDrawer_leaveTo {
opacity: 0;
transform: translateX(-240px);
}
.transition_widgetsDrawerBg_enterActive,
.transition_widgetsDrawerBg_leaveActive {
opacity: 1;
transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.transition_widgetsDrawerBg_enterFrom,
.transition_widgetsDrawerBg_leaveTo {
opacity: 0;
}
.transition_widgetsDrawer_enterActive,
.transition_widgetsDrawer_leaveActive {
opacity: 1;
transform: translateX(0);
transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.transition_widgetsDrawer_enterFrom,
.transition_widgetsDrawer_leaveTo {
opacity: 0;
transform: translateX(-240px);
}
.transition_notification_move,
.transition_notification_enterActive,
.transition_notification_leaveActive {
@@ -122,6 +224,54 @@ function getPointerEvents() {
transform: translateX(-250px);
}
.menuDrawerBg {
z-index: 1001;
}
.menuDrawer {
position: fixed;
top: 0;
left: 0;
z-index: 1001;
height: 100dvh;
width: 240px;
box-sizing: border-box;
contain: strict;
overflow: auto;
overscroll-behavior: contain;
background: var(--MI_THEME-navBg);
}
.widgetsDrawerBg {
z-index: 1001;
}
.widgetsDrawer {
position: fixed;
top: 0;
left: 0;
z-index: 1001;
width: 310px;
height: 100dvh;
padding: var(--MI-margin) var(--MI-margin) calc(var(--MI-margin) + env(safe-area-inset-bottom, 0px)) !important;
box-sizing: border-box;
overflow: auto;
overscroll-behavior: contain;
background: var(--MI_THEME-bg);
}
.widgetsCloseButton {
padding: 8px;
display: block;
margin: 0 auto;
}
@media (min-width: 370px) {
.widgetsCloseButton {
display: none;
}
}
.notifications {
position: fixed;
z-index: 3900000;
@@ -0,0 +1,144 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div ref="rootEl" :class="$style.root">
<button :class="$style.item" class="_button" @click="drawerMenuShowing = true">
<div :class="$style.itemInner">
<i :class="$style.itemIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.itemIndicator" class="_blink"><i class="_indicatorCircle"></i></span>
</div>
</button>
<button :class="$style.item" class="_button" @click="mainRouter.push('/')">
<div :class="$style.itemInner">
<i :class="$style.itemIcon" class="ti ti-home"></i>
</div>
</button>
<button :class="$style.item" class="_button" @click="mainRouter.push('/my/notifications')">
<div :class="$style.itemInner">
<i :class="$style.itemIcon" class="ti ti-bell"></i>
<span v-if="$i?.hasUnreadNotification" :class="$style.itemIndicator" class="_blink">
<span class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ $i.unreadNotificationsCount > 99 ? '99+' : $i.unreadNotificationsCount }}</span>
</span>
</div>
</button>
<button :class="$style.item" class="_button" @click="widgetsShowing = true">
<div :class="$style.itemInner">
<i :class="$style.itemIcon" class="ti ti-apps"></i>
</div>
</button>
<button :class="[$style.item, $style.post]" class="_button" @click="os.post()">
<div :class="$style.itemInner">
<i :class="$style.itemIcon" class="ti ti-pencil"></i>
</div>
</button>
</div>
</template>
<script lang="ts" setup>
import { computed, ref, useTemplateRef, watch } from 'vue';
import { $i } from '@/i.js';
import * as os from '@/os.js';
import { mainRouter } from '@/router.js';
import { navbarItemDef } from '@/navbar.js';
const drawerMenuShowing = defineModel<boolean>('drawerMenuShowing');
const widgetsShowing = defineModel<boolean>('widgetsShowing');
const rootEl = useTemplateRef('rootEl');
const menuIndicated = computed(() => {
for (const def in navbarItemDef) {
if (def === 'notifications') continue; // 通知は下にボタンとして表示されてるから
if (navbarItemDef[def].indicated) return true;
}
return false;
});
const rootElHeight = ref(0);
watch(rootEl, () => {
if (rootEl.value) {
rootElHeight.value = rootEl.value.offsetHeight;
window.document.body.style.setProperty('--MI-minBottomSpacing', 'var(--MI-minBottomSpacingMobile)');
} else {
rootElHeight.value = 0;
window.document.body.style.setProperty('--MI-minBottomSpacing', '0px');
}
}, {
immediate: true,
});
</script>
<style lang="scss" module>
.root {
padding: 12px 12px max(12px, env(safe-area-inset-bottom, 0px)) 12px;
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
grid-gap: 8px;
width: 100%;
box-sizing: border-box;
background: var(--MI_THEME-bg);
border-top: solid 0.5px var(--MI_THEME-divider);
}
.item {
&.post {
.itemInner {
background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
color: var(--MI_THEME-fgOnAccent);
&:hover {
background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
}
&:active {
background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
}
}
}
}
.itemInner {
position: relative;
padding: 0;
aspect-ratio: 1;
width: 100%;
max-width: 50px;
margin: auto;
align-content: center;
border-radius: 100%;
background: var(--MI_THEME-panel);
color: var(--MI_THEME-fg);
&:hover {
background: var(--MI_THEME-panelHighlight);
}
&:active {
background: hsl(from var(--MI_THEME-panel) h s calc(l - 2));
}
}
.itemIcon {
font-size: 14px;
}
.itemIndicator {
position: absolute;
top: 0;
left: 0;
color: var(--MI_THEME-indicator);
font-size: 16px;
&:has(.itemIndicateValueIcon) {
animation: none;
font-size: 12px;
}
}
</style>
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkA :class="$style.item" :activeClass="$style.active" to="/" exact>
<i :class="$style.itemIcon" class="ti ti-home ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.timeline }}</span>
</MkA>
<template v-for="item in menu">
<template v-for="item in prefer.r.menu.value">
<div v-if="item === '-'" :class="$style.divider"></div>
<component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" class="_button" :class="[$style.item, { [$style.active]: navbarItemDef[item].active }]" :activeClass="$style.active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
<i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[item].icon]"></i><span :class="$style.itemText">{{ navbarItemDef[item].title }}</span>
@@ -49,7 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent, toRef } from 'vue';
import { computed, defineAsyncComponent } from 'vue';
import { openInstanceMenu } from './common.js';
import * as os from '@/os.js';
import { navbarItemDef } from '@/navbar.js';
@@ -59,10 +59,9 @@ import { instance } from '@/instance.js';
import { openAccountMenu as openAccountMenu_ } from '@/accounts.js';
import { $i } from '@/i.js';
const menu = toRef(prefer.s, 'menu');
const otherMenuItemIndicated = computed(() => {
for (const def in navbarItemDef) {
if (menu.value.includes(def)) continue;
if (prefer.r.menu.value.includes(def)) continue;
if (navbarItemDef[def].indicated) return true;
}
return false;
+2 -3
View File
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkA v-tooltip.noDelay.right="i18n.ts.timeline" :class="$style.item" :activeClass="$style.active" to="/" exact>
<i :class="$style.itemIcon" class="ti ti-home ti-fw" style="viewTransitionName: navbar-homeIcon;"></i><span :class="$style.itemText">{{ i18n.ts.timeline }}</span>
</MkA>
<template v-for="item in menu">
<template v-for="item in prefer.r.menu.value">
<div v-if="item === '-'" :class="$style.divider"></div>
<component
:is="navbarItemDef[item].to ? 'MkA' : 'button'"
@@ -120,10 +120,9 @@ const iconOnly = computed(() => {
return forceIconOnly.value || (store.r.menuDisplay.value === 'sideIcon');
});
const menu = computed(() => prefer.s.menu);
const otherMenuItemIndicated = computed(() => {
for (const def in navbarItemDef) {
if (menu.value.includes(def)) continue;
if (prefer.r.menu.value.includes(def)) continue;
if (navbarItemDef[def].indicated) return true;
}
return false;
+11 -160
View File
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<XAnnouncements v-if="$i"/>
<XStatusBars/>
<div :class="$style.columnsWrapper">
<!-- passive: https://bugs.webkit.org/show_bug.cgi?id=281300 -->
<!-- passive: https://bugs.webkit.org/show_bug.cgi?id=281300 -->
<div ref="columnsEl" :class="[$style.columns, { [$style.center]: prefer.r['deck.columnAlign'].value === 'center', [$style.snapScroll]: snapScroll }]" @contextmenu.self.prevent="onContextmenu" @wheel.passive.self="onWheel">
<!-- sectionを利用しているのはdeck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
<section
@@ -73,58 +73,21 @@ SPDX-License-Identifier: AGPL-3.0-only
<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>
<XMobileFooterMenu v-if="isMobile" v-model:drawerMenuShowing="drawerMenuShowing" v-model:widgetsShowing="widgetsShowing"/>
</div>
<Transition
:enterActiveClass="prefer.s.animation ? $style.transition_menuDrawerBg_enterActive : ''"
:leaveActiveClass="prefer.s.animation ? $style.transition_menuDrawerBg_leaveActive : ''"
:enterFromClass="prefer.s.animation ? $style.transition_menuDrawerBg_enterFrom : ''"
:leaveToClass="prefer.s.animation ? $style.transition_menuDrawerBg_leaveTo : ''"
>
<div
v-if="drawerMenuShowing"
:class="$style.menuBg"
class="_modalBg"
@click="drawerMenuShowing = false"
@touchstart.passive="drawerMenuShowing = false"
></div>
</Transition>
<Transition
:enterActiveClass="prefer.s.animation ? $style.transition_menuDrawer_enterActive : ''"
:leaveActiveClass="prefer.s.animation ? $style.transition_menuDrawer_leaveActive : ''"
:enterFromClass="prefer.s.animation ? $style.transition_menuDrawer_enterFrom : ''"
:leaveToClass="prefer.s.animation ? $style.transition_menuDrawer_leaveTo : ''"
>
<div v-if="drawerMenuShowing" :class="$style.menu">
<XDrawerMenu/>
</div>
</Transition>
<XCommon/>
<XCommon v-model:drawerMenuShowing="drawerMenuShowing" v-model:widgetsShowing="widgetsShowing"/>
</div>
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent, ref, useTemplateRef } from 'vue';
import { defineAsyncComponent, ref, useTemplateRef } 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 XMobileFooterMenu from '@/ui/_common_/mobile-footer-menu.vue';
import * as os from '@/os.js';
import { navbarItemDef } from '@/navbar.js';
import { $i } from '@/i.js';
import { i18n } from '@/i18n.js';
import { deviceKind } from '@/utility/device-kind.js';
@@ -179,6 +142,7 @@ window.addEventListener('resize', () => {
const snapScroll = ref(deviceKind === 'smartphone' || deviceKind === 'tablet');
const withWallpaper = prefer.s['deck.wallpaper'] != null;
const drawerMenuShowing = ref(false);
const widgetsShowing = ref(false);
const gap = prefer.r['deck.columnGap'];
/*
@@ -188,14 +152,6 @@ watch(route, () => {
});
*/
const menuIndicated = computed(() => {
if ($i == null) return false;
for (const def in navbarItemDef) {
if (navbarItemDef[def].indicated) return true;
}
return false;
});
function showSettings() {
os.pageWindow('/settings/deck');
}
@@ -235,8 +191,8 @@ function pointerEvent(ev: PointerEvent) {
window.document.addEventListener('pointerdown', pointerEvent, { passive: true });
function onWheel(ev: WheelEvent) {
// WheelEvent はマウスからしか発火しないのでスナップスクロールは無効化する
snapScroll.value = false;
// WheelEvent はマウスからしか発火しないのでスナップスクロールは無効化する
snapScroll.value = false;
if (ev.deltaX === 0 && columnsEl.value != null) {
columnsEl.value.scrollLeft += ev.deltaY;
}
@@ -265,28 +221,6 @@ if (prefer.s['deck.wallpaper'] != null) {
</script>
<style lang="scss" module>
.transition_menuDrawerBg_enterActive,
.transition_menuDrawerBg_leaveActive {
opacity: 1;
transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.transition_menuDrawerBg_enterFrom,
.transition_menuDrawerBg_leaveTo {
opacity: 0;
}
.transition_menuDrawer_enterActive,
.transition_menuDrawer_leaveActive {
opacity: 1;
transform: translateX(0);
transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.transition_menuDrawer_enterFrom,
.transition_menuDrawer_leaveTo {
opacity: 0;
transform: translateX(-240px);
}
.root {
$nav-hide-threshold: 650px; // TODO: どこかに集約したい
@@ -318,6 +252,9 @@ if (prefer.s['deck.wallpaper'] != null) {
flex: 1;
display: flex;
flex-direction: row;
// これがないと狭い画面でマージンが広いデッキを表示したときにナビゲーションフッターが画面の外に追いやられて操作不能になる場合がある
min-height: 0;
}
.columns {
@@ -415,90 +352,4 @@ if (prefer.s['deck.wallpaper'] != null) {
.bottomMenuRight {
margin-left: auto;
}
.menuBg {
z-index: 1001;
}
.menu {
position: fixed;
top: 0;
left: 0;
z-index: 1001;
height: 100dvh;
width: 240px;
box-sizing: border-box;
contain: strict;
overflow: auto;
overscroll-behavior: contain;
background: var(--MI_THEME-navBg);
}
.nav {
padding: 12px 12px max(12px, env(safe-area-inset-bottom, 0px)) 12px;
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
grid-gap: 8px;
width: 100%;
box-sizing: border-box;
-webkit-backdrop-filter: var(--MI-blur, blur(32px));
backdrop-filter: var(--MI-blur, blur(32px));
background-color: var(--MI_THEME-header);
border-top: solid 0.5px var(--MI_THEME-divider);
}
.navButton {
position: relative;
padding: 0;
height: 32px;
width: 100%;
max-width: 60px;
margin: auto;
border-radius: var(--MI-radius-lg);
background: transparent;
color: var(--MI_THEME-fg);
&:hover {
color: var(--MI_THEME-accent);
}
&:active {
color: var(--MI_THEME-accent);
background: hsl(from var(--MI_THEME-panel) h s calc(l - 2));
}
}
.postButton {
composes: navButton;
background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
color: var(--MI_THEME-fgOnAccent);
&:hover {
background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
color: var(--MI_THEME-fgOnAccent);
}
&:active {
background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
color: var(--MI_THEME-fgOnAccent);
}
}
.navButtonIcon {
font-size: 16px;
vertical-align: middle;
}
.navButtonIndicator {
position: absolute;
top: 0;
left: 0;
color: var(--MI_THEME-indicator);
font-size: 16px;
&:has(.itemIndicateValueIcon) {
animation: none;
font-size: 12px;
}
}
</style>
+2 -1
View File
@@ -49,10 +49,11 @@ import { updateColumn, swapLeftColumn, swapRightColumn, swapUpColumn, swapDownCo
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { prefer } from '@/preferences.js';
import { DI } from '@/di.js';
provide('shouldHeaderThin', true);
provide('shouldOmitHeaderTitle', true);
provide('forceSpacerMin', true);
provide(DI.forceSpacerMin, true);
const withWallpaper = prefer.s['deck.wallpaper'] != null;
+6 -253
View File
@@ -15,90 +15,26 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<StackingRouterView v-if="prefer.s['experimental.stackingRouterView']" :class="$style.content"/>
<RouterView v-else :class="$style.content"/>
<div v-if="isMobile" ref="navFooter" :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.navButton" class="_button" @click="widgetsShowing = true"><i :class="$style.navButtonIcon" class="ti ti-apps"></i></button>
<button :class="$style.postButton" class="_button" @click="os.post()"><i :class="$style.navButtonIcon" class="ti ti-pencil"></i></button>
</div>
<XMobileFooterMenu v-if="isMobile" ref="navFooter" v-model:drawerMenuShowing="drawerMenuShowing" v-model:widgetsShowing="widgetsShowing"/>
</div>
<div v-if="isDesktop && !pageMetadata?.needWideArea" :class="$style.widgets">
<XWidgets/>
</div>
<Transition
:enterActiveClass="prefer.s.animation ? $style.transition_menuDrawerBg_enterActive : ''"
:leaveActiveClass="prefer.s.animation ? $style.transition_menuDrawerBg_leaveActive : ''"
:enterFromClass="prefer.s.animation ? $style.transition_menuDrawerBg_enterFrom : ''"
:leaveToClass="prefer.s.animation ? $style.transition_menuDrawerBg_leaveTo : ''"
>
<div
v-if="drawerMenuShowing"
:class="$style.menuDrawerBg"
class="_modalBg"
@click="drawerMenuShowing = false"
@touchstart.passive="drawerMenuShowing = false"
></div>
</Transition>
<Transition
:enterActiveClass="prefer.s.animation ? $style.transition_menuDrawer_enterActive : ''"
:leaveActiveClass="prefer.s.animation ? $style.transition_menuDrawer_leaveActive : ''"
:enterFromClass="prefer.s.animation ? $style.transition_menuDrawer_enterFrom : ''"
:leaveToClass="prefer.s.animation ? $style.transition_menuDrawer_leaveTo : ''"
>
<div v-if="drawerMenuShowing" :class="$style.menuDrawer">
<XDrawerMenu/>
</div>
</Transition>
<Transition
:enterActiveClass="prefer.s.animation ? $style.transition_widgetsDrawerBg_enterActive : ''"
:leaveActiveClass="prefer.s.animation ? $style.transition_widgetsDrawerBg_leaveActive : ''"
:enterFromClass="prefer.s.animation ? $style.transition_widgetsDrawerBg_enterFrom : ''"
:leaveToClass="prefer.s.animation ? $style.transition_widgetsDrawerBg_leaveTo : ''"
>
<div
v-if="widgetsShowing"
:class="$style.widgetsDrawerBg"
class="_modalBg"
@click="widgetsShowing = false"
@touchstart.passive="widgetsShowing = false"
></div>
</Transition>
<Transition
:enterActiveClass="prefer.s.animation ? $style.transition_widgetsDrawer_enterActive : ''"
:leaveActiveClass="prefer.s.animation ? $style.transition_widgetsDrawer_leaveActive : ''"
:enterFromClass="prefer.s.animation ? $style.transition_widgetsDrawer_enterFrom : ''"
:leaveToClass="prefer.s.animation ? $style.transition_widgetsDrawer_leaveTo : ''"
>
<div v-if="widgetsShowing" :class="$style.widgetsDrawer">
<button class="_button" :class="$style.widgetsCloseButton" @click="widgetsShowing = false"><i class="ti ti-x"></i></button>
<XWidgets/>
</div>
</Transition>
<XCommon/>
<XCommon v-model:drawerMenuShowing="drawerMenuShowing" v-model:widgetsShowing="widgetsShowing"/>
</div>
</template>
<script lang="ts" setup>
import { defineAsyncComponent, provide, onMounted, computed, ref, watch, useTemplateRef } from 'vue';
import { defineAsyncComponent, provide, onMounted, computed, ref } from 'vue';
import { instanceName } from '@@/js/config.js';
import { isLink } from '@@/js/is-link.js';
import XCommon from './_common_/common.vue';
import type { PageMetadata } from '@/page.js';
import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
import XMobileFooterMenu from '@/ui/_common_/mobile-footer-menu.vue';
import XPreferenceRestore from '@/ui/_common_/PreferenceRestore.vue';
import * as os from '@/os.js';
import { navbarItemDef } from '@/navbar.js';
import { i18n } from '@/i18n.js';
import { $i } from '@/i.js';
import { provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
@@ -109,11 +45,10 @@ import { prefer } from '@/preferences.js';
import { shouldSuggestRestoreBackup } from '@/preferences/utility.js';
import { DI } from '@/di.js';
const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
const XWidgets = defineAsyncComponent(() => import('./_common_/widgets.vue'));
const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/navbar.vue'));
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue'));
const XPreferenceRestore = defineAsyncComponent(() => import('@/ui/_common_/PreferenceRestore.vue'));
const isRoot = computed(() => mainRouter.currentRoute.value.name === 'index');
@@ -129,7 +64,6 @@ window.addEventListener('resize', () => {
const pageMetadata = ref<null | PageMetadata>(null);
const widgetsShowing = ref(false);
const navFooter = useTemplateRef('navFooter');
provide(DI.router, mainRouter);
provideMetadataReceiver((metadataGetter) => {
@@ -145,14 +79,6 @@ provideMetadataReceiver((metadataGetter) => {
});
provideReactiveMetadata(pageMetadata);
const menuIndicated = computed(() => {
for (const def in navbarItemDef) {
if (def === 'notifications') continue; // 通知は下にボタンとして表示されてるから
if (navbarItemDef[def].indicated) return true;
}
return false;
});
const drawerMenuShowing = ref(false);
mainRouter.on('change', () => {
@@ -192,70 +118,12 @@ const onContextmenu = (ev) => {
},
}], ev);
};
const navFooterHeight = ref(0);
watch(navFooter, () => {
if (navFooter.value) {
navFooterHeight.value = navFooter.value.offsetHeight;
window.document.body.style.setProperty('--MI-minBottomSpacing', 'var(--MI-minBottomSpacingMobile)');
} else {
navFooterHeight.value = 0;
window.document.body.style.setProperty('--MI-minBottomSpacing', '0px');
}
}, {
immediate: true,
});
</script>
<style lang="scss" module>
$ui-font-size: 1em; // TODO: どこかに集約したい
$widgets-hide-threshold: 1090px;
.transition_menuDrawerBg_enterActive,
.transition_menuDrawerBg_leaveActive {
opacity: 1;
transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.transition_menuDrawerBg_enterFrom,
.transition_menuDrawerBg_leaveTo {
opacity: 0;
}
.transition_menuDrawer_enterActive,
.transition_menuDrawer_leaveActive {
opacity: 1;
transform: translateX(0);
transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.transition_menuDrawer_enterFrom,
.transition_menuDrawer_leaveTo {
opacity: 0;
transform: translateX(-240px);
}
.transition_widgetsDrawerBg_enterActive,
.transition_widgetsDrawerBg_leaveActive {
opacity: 1;
transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.transition_widgetsDrawerBg_enterFrom,
.transition_widgetsDrawerBg_leaveTo {
opacity: 0;
}
.transition_widgetsDrawer_enterActive,
.transition_widgetsDrawer_leaveActive {
opacity: 1;
transform: translateX(0);
transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.transition_widgetsDrawer_enterFrom,
.transition_widgetsDrawer_leaveTo {
opacity: 0;
transform: translateX(-240px);
}
.root {
height: 100dvh;
overflow: clip;
@@ -282,91 +150,6 @@ $widgets-hide-threshold: 1090px;
min-height: 0;
}
.nav {
padding: 12px 12px max(12px, env(safe-area-inset-bottom, 0px)) 12px;
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
grid-gap: 8px;
width: 100%;
box-sizing: border-box;
background: var(--MI_THEME-bg);
border-top: solid 0.5px var(--MI_THEME-divider);
}
.navButton {
position: relative;
padding: 0;
height: 32px;
width: 100%;
max-width: 60px;
margin: auto;
border-radius: var(--MI-radius-lg);
background: transparent;
color: var(--MI_THEME-fg);
&:hover {
background: var(--MI_THEME-panelHighlight);
color: var(--MI_THEME-accent);
}
&:active {
background: hsl(from var(--MI_THEME-panel) h s calc(l - 2));
color: var(--MI_THEME-accent);
}
}
.postButton {
composes: navButton;
background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
color: var(--MI_THEME-fgOnAccent);
&:hover {
background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
color: var(--MI_THEME-fgOnAccent);
}
&:active {
color: var(--MI_THEME-fgOnAccent);
background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
}
}
.navButtonIcon {
font-size: 16px;
vertical-align: middle;
}
.navButtonIndicator {
position: absolute;
top: 0;
left: 0;
color: var(--MI_THEME-indicator);
font-size: 16px;
&:has(.itemIndicateValueIcon) {
animation: none;
font-size: 12px;
}
}
.menuDrawerBg {
z-index: 1001;
}
.menuDrawer {
position: fixed;
top: 0;
left: 0;
z-index: 1001;
height: 100dvh;
width: 240px;
box-sizing: border-box;
contain: strict;
overflow: auto;
overscroll-behavior: contain;
background: var(--MI_THEME-navBg);
}
.statusbars {
position: sticky;
top: 0;
@@ -386,34 +169,4 @@ $widgets-hide-threshold: 1090px;
display: none;
}
}
.widgetsDrawerBg {
z-index: 1001;
}
.widgetsDrawer {
position: fixed;
top: 0;
left: 0;
z-index: 1001;
width: 310px;
height: 100dvh;
padding: var(--MI-margin) var(--MI-margin) calc(var(--MI-margin) + env(safe-area-inset-bottom, 0px)) !important;
box-sizing: border-box;
overflow: auto;
overscroll-behavior: contain;
background: var(--MI_THEME-bg);
}
.widgetsCloseButton {
padding: 8px;
display: block;
margin: 0 auto;
}
@media (min-width: 370px) {
.widgetsCloseButton {
display: none;
}
}
</style>
+8 -12
View File
@@ -6,16 +6,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div :class="$style.root">
<div :class="$style.contents">
<div style="flex: 1; min-height: 0;">
<RouterView/>
</div>
<!--
デッキUIが設定されている場合はデッキUIに戻れるようにする (ただし?zenが明示された場合は表示しない)
See https://github.com/misskey-dev/misskey/issues/10905
-->
<div v-if="showBottom" :class="$style.bottom">
<button v-tooltip="i18n.ts.goToMisskey" :class="['_button', '_shadow', $style.button]" @click="goToMisskey"><i class="ti ti-home"></i></button>
<button v-if="showDeckNav" class="_buttonPrimary" :class="$style.deckNav" @click="goToDeck">{{ i18n.ts.goToDeck }}</button>
<div style="flex: 1; min-height: 0;">
<RouterView/>
</div>
</div>
@@ -37,7 +35,7 @@ const isRoot = computed(() => mainRouter.currentRoute.value.name === 'index');
const pageMetadata = ref<null | PageMetadata>(null);
const showBottom = !(new URLSearchParams(window.location.search)).has('zen') && ui === 'deck';
const showDeckNav = !(new URLSearchParams(window.location.search)).has('zen') && ui === 'deck';
provide(DI.router, mainRouter);
provideMetadataReceiver((metadataGetter) => {
@@ -53,7 +51,7 @@ provideMetadataReceiver((metadataGetter) => {
});
provideReactiveMetadata(pageMetadata);
function goToMisskey() {
function goToDeck() {
window.location.href = '/';
}
</script>
@@ -68,10 +66,8 @@ function goToMisskey() {
height: 100dvh;
}
.bottom {
height: calc(60px + (var(--MI-margin) * 2) + env(safe-area-inset-bottom, 0px));
width: 100%;
margin-top: auto;
.deckNav {
padding: 4px;
}
.button {