feat(frontend): タイトルバーを表示できるように

This commit is contained in:
syuilo 2025-04-29 09:43:15 +09:00
parent 2e91cd6d45
commit d6ae4c980b
11 changed files with 213 additions and 100 deletions

View File

@ -13,6 +13,7 @@
### Client
- Feat: チャットウィジェットを追加
- Feat: デッキにチャットカラムを追加
- Feat: タイトルバーを表示できるように
- Enhance: Unicode絵文字をslugから入力する際に`:ok:`のように最後の`:`を入力したあとにUnicode絵文字に変換できるように
- Enhance: コントロールパネルでジョブキューをクリアできるように
- Enhance: テーマでページヘッダーの色を変更できるように

View File

@ -31,6 +31,7 @@ html {
margin: auto;
width: 64px;
height: 64px;
border-radius: 10px;
pointer-events: none;
}

View File

@ -53,6 +53,7 @@ html.embed.noborder #splash {
margin: auto;
width: 64px;
height: 64px;
border-radius: 10px;
pointer-events: none;
}

View File

@ -42,6 +42,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</SearchMarker>
<div class="_gaps_s">
<SearchMarker :keywords="['titlebar', 'show']">
<MkPreferenceContainer k="showTitlebar">
<MkSwitch v-model="showTitlebar">
<template #label><SearchLabel>{{ i18n.ts.showTitlebar }}</SearchLabel></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['avatar', 'icon', 'decoration', 'show']">
<MkPreferenceContainer k="showAvatarDecorations">
<MkSwitch v-model="showAvatarDecorations">
@ -742,6 +750,7 @@ const lang = ref(miLocalStorage.getItem('lang'));
const dataSaver = ref(prefer.s.dataSaver);
const overridedDeviceKind = prefer.model('overridedDeviceKind');
const showTitlebar = prefer.model('showTitlebar');
const keepCw = prefer.model('keepCw');
const serverDisconnectedBehavior = prefer.model('serverDisconnectedBehavior');
const hemisphere = prefer.model('hemisphere');

View File

@ -333,6 +333,9 @@ export const PREF_DEF = {
showNavbarSubButtons: {
default: true,
},
showTitlebar: {
default: false,
},
plugins: {
default: [] as Plugin[],
},

View File

@ -413,7 +413,7 @@ if ($i) {
#devTicker {
position: fixed;
top: 0;
bottom: 0;
left: 0;
z-index: 2147483647;
color: #ff0;

View File

@ -121,6 +121,7 @@ function more() {
display: inline-block;
width: 38px;
aspect-ratio: 1;
border-radius: 8px;
}
.bottom {

View File

@ -7,7 +7,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="[$style.root, { [$style.iconOnly]: iconOnly }]">
<div :class="$style.body">
<div :class="$style.top">
<div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }"></div>
<button v-tooltip.noDelay.right="instance.name ?? i18n.ts.instance" class="_button" :class="$style.instance" @click="openInstanceMenu">
<img :src="instance.iconUrl || instance.faviconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon" style="viewTransitionName: navbar-serverIcon;"/>
</button>
@ -183,12 +182,9 @@ function menuEdit() {
}
.body {
position: fixed;
top: 0;
left: 0;
z-index: 1001;
position: relative;
width: var(--nav-icon-only-width);
height: 100dvh;
height: 100%;
box-sizing: border-box;
overflow: auto;
overflow-x: clip;
@ -303,18 +299,6 @@ function menuEdit() {
backdrop-filter: var(--MI-blur, blur(8px));
}
.banner {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-size: cover;
background-position: center center;
-webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%);
mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%);
}
.instance {
position: relative;
display: block;
@ -335,6 +319,7 @@ function menuEdit() {
display: inline-block;
width: 38px;
aspect-ratio: 1;
border-radius: 8px;
}
.bottom {
@ -559,6 +544,7 @@ function menuEdit() {
display: inline-block;
width: 30px;
aspect-ratio: 1;
border-radius: 8px;
}
.bottom {

View File

@ -0,0 +1,87 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.root">
<div :class="$style.title">
<img :src="instance.iconUrl || instance.faviconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon"/>
<span :class="$style.instanceTitle">{{ instance.name ?? host }}</span>
</div>
<div :class="$style.controls">
<span :class="$style.left">
<button v-if="canBack" class="_button" :class="$style.button" @click="goBack"><i class="ti ti-arrow-left"></i></button>
</span>
<span :class="$style.right">
</span>
</div>
</div>
</template>
<script lang="ts" setup>
import { host } from '@@/js/config.js';
import { ref } from 'vue';
import { instance } from '@/instance.js';
import { prefer } from '@/preferences.js';
const canBack = ref(true);
function goBack() {
window.history.back();
}
</script>
<style lang="scss" module>
.root {
--height: 36px;
background: var(--MI_THEME-navBg);
height: var(--height);
font-size: 90%;
}
.title {
display: flex;
justify-content: center;
align-items: center;
text-align: center;
height: var(--height);
}
.controls {
position: absolute;
top: 0;
left: 0;
display: flex;
justify-content: center;
align-items: center;
height: var(--height);
}
.instanceIcon {
display: inline-block;
width: 20px;
aspect-ratio: 1;
border-radius: 5px;
margin-right: 8px;
}
.instanceTitle {
display: inline-block;
}
.left {
margin-right: auto;
}
.right {
margin-left: auto;
}
.button {
display: inline-block;
height: var(--height);
aspect-ratio: 1;
}
</style>

View File

@ -4,71 +4,75 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="[$style.root, { [$style.withWallpaper]: withWallpaper }]">
<XSidebar v-if="!isMobile && prefer.r['deck.navbarPosition'].value === 'left'"/>
<div :class="[$style.root]">
<XTitlebar v-if="prefer.r.showTitlebar.value" style="flex-shrink: 0;"/>
<div :class="$style.main">
<XNavbarH v-if="!isMobile && prefer.r['deck.navbarPosition'].value === 'top'"/>
<div :class="$style.nonTitlebarArea">
<XSidebar v-if="!isMobile && prefer.r['deck.navbarPosition'].value === 'left'"/>
<XAnnouncements v-if="$i"/>
<XStatusBars/>
<div :class="$style.columnsWrapper">
<!-- 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
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.passive.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, { '_shadow': withWallpaper }]"
: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>
<div>{{ i18n.ts._deck.introduction2 }}</div>
<div :class="[$style.main, { [$style.withWallpaper]: withWallpaper, [$style.withSidebarAndTitlebar]: !isMobile && prefer.r['deck.navbarPosition'].value === 'left' && prefer.r.showTitlebar.value }]" :style="{ backgroundImage: prefer.s['deck.wallpaper'] != null ? `url(${ prefer.s['deck.wallpaper'] })` : null }">
<XNavbarH v-if="!isMobile && prefer.r['deck.navbarPosition'].value === 'top'"/>
<XAnnouncements v-if="$i"/>
<XStatusBars/>
<div :class="$style.columnsWrapper">
<!-- 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
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.passive.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, { '_shadow': withWallpaper }]"
: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>
<div>{{ i18n.ts._deck.introduction2 }}</div>
</div>
</div>
<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>
</div>
<div :class="$style.sideMenuMiddle">
<button v-tooltip.noDelay.left="i18n.ts._deck.addColumn" :class="$style.sideMenuButton" class="_button" @click="addColumn"><i class="ti ti-plus"></i></button>
</div>
<div :class="$style.sideMenuBottom">
<button v-tooltip.noDelay.left="i18n.ts.settings" :class="$style.sideMenuButton" class="_button" @click="showSettings"><i class="ti ti-settings-2"></i></button>
</div>
</div>
</div>
<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>
<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.sideMenuMiddle">
<button v-tooltip.noDelay.left="i18n.ts._deck.addColumn" :class="$style.sideMenuButton" class="_button" @click="addColumn"><i class="ti ti-plus"></i></button>
<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.sideMenuBottom">
<button v-tooltip.noDelay.left="i18n.ts.settings" :class="$style.sideMenuButton" class="_button" @click="showSettings"><i class="ti ti-settings-2"></i></button>
<div :class="$style.bottomMenuRight">
<button v-tooltip.noDelay.left="i18n.ts.settings" :class="$style.bottomMenuButton" class="_button" @click="showSettings"><i class="ti ti-settings-2"></i></button>
</div>
</div>
<XNavbarH v-if="!isMobile && prefer.r['deck.navbarPosition'].value === 'bottom'"/>
<XMobileFooterMenu v-if="isMobile" v-model:drawerMenuShowing="drawerMenuShowing" v-model:widgetsShowing="widgetsShowing"/>
</div>
<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-2"></i></button>
</div>
</div>
<XNavbarH v-if="!isMobile && prefer.r['deck.navbarPosition'].value === 'bottom'"/>
<XMobileFooterMenu v-if="isMobile" v-model:drawerMenuShowing="drawerMenuShowing" v-model:widgetsShowing="widgetsShowing"/>
</div>
<XCommon v-model:drawerMenuShowing="drawerMenuShowing" v-model:widgetsShowing="widgetsShowing"/>
@ -82,6 +86,7 @@ import XCommon from './_common_/common.vue';
import XSidebar from '@/ui/_common_/navbar.vue';
import XNavbarH from '@/ui/_common_/navbar-h.vue';
import XMobileFooterMenu from '@/ui/_common_/mobile-footer-menu.vue';
import XTitlebar from '@/ui/_common_/titlebar.vue';
import * as os from '@/os.js';
import { $i } from '@/i.js';
import { i18n } from '@/i18n.js';
@ -209,30 +214,25 @@ async function deleteProfile() {
window.document.documentElement.style.overflowY = 'hidden';
window.document.documentElement.style.scrollBehavior = 'auto';
if (prefer.s['deck.wallpaper'] != null) {
window.document.documentElement.style.backgroundImage = `url(${prefer.s['deck.wallpaper']})`;
}
</script>
<style lang="scss" module>
.root {
$nav-hide-threshold: 650px; // TODO:
--MI-margin: var(--MI-marginHalf);
--columnGap: v-bind("gap + 'px'");
display: flex;
flex-direction: column;
height: 100dvh;
box-sizing: border-box;
flex: 1;
background: var(--MI_THEME-navBg);
}
&.withWallpaper {
.main {
background: transparent;
}
}
.nonTitlebarArea {
display: flex;
flex: 1;
}
.main {
@ -240,7 +240,15 @@ if (prefer.s['deck.wallpaper'] != null) {
min-width: 0;
display: flex;
flex-direction: column;
background: var(--MI_THEME-deckBg);
&:not(.withWallpaper) {
background: var(--MI_THEME-deckBg);
}
&.withSidebarAndTitlebar {
border-radius: 12px 0 0 0;
overflow: clip;
}
}
.columnsWrapper {

View File

@ -5,21 +5,25 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div :class="[$style.root, { '_forceShrinkSpacer': deviceKind === 'smartphone' }]">
<XSidebar v-if="!isMobile" :class="$style.sidebar" :showWidgetButton="!isDesktop" @widgetButtonClick="widgetsShowing = true"/>
<XTitlebar v-if="prefer.r.showTitlebar.value" style="flex-shrink: 0;"/>
<div :class="$style.contents" @contextmenu.stop="onContextmenu">
<div>
<XPreferenceRestore v-if="shouldSuggestRestoreBackup"/>
<XAnnouncements v-if="$i"/>
<XStatusBars :class="$style.statusbars"/>
<div :class="$style.nonTitlebarArea">
<XSidebar v-if="!isMobile" :class="$style.sidebar" :showWidgetButton="!isDesktop" @widgetButtonClick="widgetsShowing = true"/>
<div :class="[$style.contents, !isMobile && prefer.r.showTitlebar.value ? $style.withSidebarAndTitlebar : null]" @contextmenu.stop="onContextmenu">
<div>
<XPreferenceRestore v-if="shouldSuggestRestoreBackup"/>
<XAnnouncements v-if="$i"/>
<XStatusBars :class="$style.statusbars"/>
</div>
<StackingRouterView v-if="prefer.s['experimental.stackingRouterView']" :class="$style.content"/>
<RouterView v-else :class="$style.content"/>
<XMobileFooterMenu v-if="isMobile" ref="navFooter" v-model:drawerMenuShowing="drawerMenuShowing" v-model:widgetsShowing="widgetsShowing"/>
</div>
<StackingRouterView v-if="prefer.s['experimental.stackingRouterView']" :class="$style.content"/>
<RouterView v-else :class="$style.content"/>
<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 v-if="isDesktop && !pageMetadata?.needWideArea" :class="$style.widgets">
<XWidgets/>
</div>
</div>
<XCommon v-model:drawerMenuShowing="drawerMenuShowing" v-model:widgetsShowing="widgetsShowing"/>
@ -34,6 +38,7 @@ import XCommon from './_common_/common.vue';
import type { PageMetadata } from '@/page.js';
import XMobileFooterMenu from '@/ui/_common_/mobile-footer-menu.vue';
import XPreferenceRestore from '@/ui/_common_/PreferenceRestore.vue';
import XTitlebar from '@/ui/_common_/titlebar.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { $i } from '@/i.js';
@ -128,8 +133,14 @@ $widgets-hide-threshold: 1090px;
height: 100dvh;
overflow: clip;
contain: strict;
box-sizing: border-box;
display: flex;
flex-direction: column;
background: var(--MI_THEME-navBg);
}
.nonTitlebarArea {
display: flex;
flex: 1;
}
.sidebar {
@ -142,7 +153,12 @@ $widgets-hide-threshold: 1090px;
flex: 1;
height: 100%;
min-width: 0;
background: var(--MI_THEME-bg);
&.withSidebarAndTitlebar {
background: var(--MI_THEME-navBg);
border-radius: 12px 0 0 0;
overflow: clip;
}
}
.content {