fix(frontend): drop classic ui

This commit is contained in:
syuilo 2025-03-30 14:25:56 +09:00
parent c03f9bff0a
commit 4f4cb6738c
6 changed files with 3 additions and 793 deletions

View File

@ -51,6 +51,9 @@
- Enhance 全体的なパフォーマンス向上
- Fix: 読み込み直後にスクロールしようとすると途中で止まる場合があるのを修正
- Fix: テーマ切り替え時に一部の色が変わらない問題を修正
- NOTE: 構造上クラシックUIを新しいデザインシステムに移行することが困難なため、クラシックUIが削除されました
- デッキUIでカラムを中央寄せにし、メインカラムの左右にウィジェットカラムを配置することである程度クラシックUIを再現できます
- また、デッキでナビゲーションバーを上部に表示するオプションを実装予定です
### Server
- Enhance 全体的なパフォーマンス向上

View File

@ -54,9 +54,6 @@ export async function mainBoot() {
case 'visitor':
rootComponent = await import('@/ui/visitor.vue').then(x => x.default);
break;
case 'classic':
rootComponent = await import('@/ui/classic.vue').then(x => x.default);
break;
default:
rootComponent = await import('@/ui/universal.vue').then(x => x.default);
break;

View File

@ -145,13 +145,6 @@ export const navbarItemDef = reactive({
miLocalStorage.setItem('ui', 'deck');
unisonReload();
},
}, {
text: i18n.ts.classic,
active: ui === 'classic',
action: () => {
miLocalStorage.setItem('ui', 'classic');
unisonReload();
},
}], ev.currentTarget ?? ev.target);
},
},

View File

@ -1,212 +0,0 @@
<!--
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_/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: var(--MI_THEME-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(--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>

View File

@ -1,245 +0,0 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div class="npcljfve" :class="{ iconOnly }">
<button v-click-anime class="item _button account" @click="openAccountMenu">
<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
</button>
<div class="post" data-cy-open-post-form @click="os.post">
<MkButton class="button" gradate full rounded>
<i class="ti ti-pencil ti-fw"></i><span v-if="!iconOnly" class="text">{{ i18n.ts.note }}</span>
</MkButton>
</div>
<div class="divider"></div>
<MkA v-click-anime class="item index" activeClass="active" to="/" exact>
<i class="ti ti-home ti-fw"></i><span class="text">{{ i18n.ts.timeline }}</span>
</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 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 class="text">{{ navbarItemDef[item].title }}</span>
<span v-if="navbarItemDef[item].indicated" class="indicator _blink">
<span v-if="navbarItemDef[item].indicateValue" class="_indicateCounter itemIndicateValueIcon">{{ navbarItemDef[item].indicateValue }}</span>
<i v-else class="_indicatorCircle"></i>
</span>
</component>
</template>
<div class="divider"></div>
<MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime class="item" activeClass="active" to="/admin" :behavior="settingsWindowed ? 'window' : null">
<i class="ti ti-dashboard ti-fw"></i><span class="text">{{ i18n.ts.controlPanel }}</span>
</MkA>
<button v-click-anime class="item _button" @click="more">
<i class="ti ti-dots ti-fw"></i><span class="text">{{ i18n.ts.more }}</span>
<span v-if="otherNavItemIndicated" class="indicator _blink"><i class="_indicatorCircle"></i></span>
</button>
<MkA v-click-anime class="item" activeClass="active" to="/settings" :behavior="settingsWindowed ? 'window' : null">
<i class="ti ti-settings ti-fw"></i><span class="text">{{ i18n.ts.settings }}</span>
</MkA>
<div class="divider"></div>
<div class="about">
<button v-click-anime class="item _button" @click="openInstanceMenu">
<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" draggable="false"/>
</button>
</div>
<!--<MisskeyLogo class="misskey"/>-->
</div>
</template>
<script lang="ts" setup>
import { defineAsyncComponent, computed, watch, ref, useTemplateRef } from 'vue';
import { openInstanceMenu } from './_common_/common.js';
// import { host } from '@@/js/config.js';
import * as os from '@/os.js';
import { navbarItemDef } from '@/navbar.js';
import MkButton from '@/components/MkButton.vue';
// import { StickySidebar } from '@/utility/sticky-sidebar.js';
// import { mainRouter } from '@/router.js';
//import MisskeyLogo from '@assets/client/misskey.svg';
import { store } from '@/store.js';
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 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;
});
const el = useTemplateRef('el');
// let accounts = $ref([]);
// let connection = $ref(null);
const iconOnly = ref(false);
const settingsWindowed = ref(false);
function calcViewState() {
iconOnly.value = (window.innerWidth <= WINDOW_THRESHOLD) || (menuDisplay.value === 'sideIcon');
settingsWindowed.value = (window.innerWidth > WINDOW_THRESHOLD);
}
function more(ev: MouseEvent) {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), {
src: ev.currentTarget ?? ev.target,
}, {
closed: () => dispose(),
});
}
function openAccountMenu(ev: MouseEvent) {
openAccountMenu_({
withExtraOperation: true,
}, ev);
}
watch(store.r.menuDisplay, () => {
calcViewState();
});
</script>
<style lang="scss" scoped>
.npcljfve {
$ui-font-size: 1em; // TODO:
$nav-icon-only-width: 78px; // TODO:
$avatar-size: 32px;
$avatar-margin: 8px;
padding: 0 16px;
box-sizing: border-box;
width: 260px;
&.iconOnly {
flex: 0 0 $nav-icon-only-width;
width: $nav-icon-only-width !important;
> .divider {
margin: 8px auto;
width: calc(100% - 32px);
}
> .post {
> .button {
width: 46px;
height: 46px;
padding: 0;
}
}
> .item {
padding-left: 0;
width: 100%;
text-align: center;
font-size: $ui-font-size * 1.1;
line-height: 3.7rem;
> i,
> .avatar {
margin-right: 0;
}
> i {
left: 10px;
}
> .text {
display: none;
}
}
}
> .divider {
margin: 10px 0;
border-top: solid 0.5px var(--MI_THEME-divider);
}
> .post {
position: sticky;
top: 0;
z-index: 1;
padding: 16px 0;
background: var(--MI_THEME-bg);
> .button {
min-width: 0;
}
}
> .about {
fill: currentColor;
padding: 8px 0 16px 0;
text-align: center;
> .item {
display: block;
width: 32px;
margin: 0 auto;
img {
display: block;
width: 100%;
}
}
}
> .item {
position: relative;
display: block;
font-size: $ui-font-size;
line-height: 2.6rem;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
width: 100%;
text-align: left;
box-sizing: border-box;
> i {
width: 32px;
}
> i,
> .avatar {
margin-right: $avatar-margin;
}
> .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;
&:has(.itemIndicateValueIcon) {
animation: none;
left: auto;
right: 20px;
}
}
&:hover {
text-decoration: none;
color: var(--MI_THEME-navHoverFg);
}
&.active {
color: var(--MI_THEME-navActive);
}
}
}
</style>

View File

@ -1,326 +0,0 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div class="gbhvwtnk" :class="{ wallpaper }" :style="`--globalHeaderHeight:${globalHeaderHeight}px`">
<XHeaderMenu v-if="showMenuOnTop" v-get-size="(w, h) => globalHeaderHeight = h"/>
<div class="columns" :class="{ fullView, withGlobalHeader: showMenuOnTop }">
<div v-if="!showMenuOnTop" class="sidebar">
<XSidebar/>
</div>
<div v-else-if="!pageMetadata?.needWideArea" ref="widgetsLeft" class="widgets left">
<XWidgets place="left" :marginTop="'var(--MI-margin)'" @mounted="attachSticky(widgetsLeft)"/>
</div>
<main class="main" @contextmenu.stop="onContextmenu">
<div class="content">
<StackingRouterView v-if="prefer.s['experimental.stackingRouterView']"/>
<RouterView v-else/>
</div>
</main>
<div v-if="isDesktop && !pageMetadata?.needWideArea" ref="widgetsRight" class="widgets right">
<XWidgets :place="showMenuOnTop ? 'right' : null" :marginTop="showMenuOnTop ? '0' : 'var(--MI-margin)'" @mounted="attachSticky(widgetsRight)"/>
</div>
</div>
<Transition :name="prefer.s.animation ? 'tray-back' : ''">
<div
v-if="widgetsShowing"
class="tray-back _modalBg"
@click="widgetsShowing = false"
@touchstart.passive="widgetsShowing = false"
></div>
</Transition>
<Transition :name="prefer.s.animation ? 'tray' : ''">
<XWidgets v-if="widgetsShowing" class="tray"/>
</Transition>
<iframe v-if="prefer.s.aiChanMode" ref="live2d" class="ivnzpscs" src="https://misskey-dev.github.io/mascot-web/?scale=2&y=1.4"></iframe>
<XCommon/>
</div>
</template>
<script lang="ts" setup>
import { defineAsyncComponent, onMounted, provide, ref, computed, useTemplateRef } from 'vue';
import { instanceName } from '@@/js/config.js';
import { isLink } from '@@/js/is-link.js';
import XSidebar from './classic.sidebar.vue';
import XCommon from './_common_/common.vue';
import type { PageMetadata } from '@/page.js';
import { StickySidebar } from '@/utility/sticky-sidebar.js';
import * as os from '@/os.js';
import { provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
import { store } from '@/store.js';
import { i18n } from '@/i18n.js';
import { miLocalStorage } from '@/local-storage.js';
import { mainRouter } from '@/router.js';
import { prefer } from '@/preferences.js';
import { DI } from '@/di.js';
const XHeaderMenu = defineAsyncComponent(() => import('./classic.header.vue'));
const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
const isRoot = computed(() => mainRouter.currentRoute.value.name === 'index');
const DESKTOP_THRESHOLD = 1100;
const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD);
const pageMetadata = ref<null | PageMetadata>(null);
const widgetsShowing = ref(false);
const fullView = ref(false);
const globalHeaderHeight = ref(0);
const wallpaper = miLocalStorage.getItem('wallpaper') != null;
const showMenuOnTop = computed(() => store.s.menuDisplay === 'top');
const live2d = useTemplateRef('live2d');
const widgetsLeft = ref<HTMLElement>();
const widgetsRight = ref<HTMLElement>();
provide(DI.router, mainRouter);
provideMetadataReceiver((metadataGetter) => {
const info = metadataGetter();
pageMetadata.value = info;
if (pageMetadata.value) {
if (isRoot.value && pageMetadata.value.title === instanceName) {
window.document.title = pageMetadata.value.title;
} else {
window.document.title = `${pageMetadata.value.title} | ${instanceName}`;
}
}
});
provideReactiveMetadata(pageMetadata);
provide('shouldHeaderThin', showMenuOnTop.value);
provide('forceSpacerMin', true);
function attachSticky(el: HTMLElement) {
const sticky = new StickySidebar(el, 0, store.s.menuDisplay === 'top' ? 60 : 0); // TODO: 60px
window.addEventListener('scroll', () => {
sticky.calc(window.scrollY);
}, { passive: true });
}
function top() {
window.scroll({ top: 0, behavior: 'smooth' });
}
function onContextmenu(ev: MouseEvent) {
if (isLink(ev.target)) return;
if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return;
if (window.getSelection().toString() !== '') return;
const path = mainRouter.getCurrentFullPath();
os.contextMenu([{
type: 'label',
text: path,
}, {
icon: fullView.value ? 'ti ti-minimize' : 'ti ti-maximize',
text: fullView.value ? i18n.ts.quitFullView : i18n.ts.fullView,
action: () => {
fullView.value = !fullView.value;
},
}, {
icon: 'ti ti-window-maximize',
text: i18n.ts.openInWindow,
action: () => {
os.pageWindow(path);
},
}], ev);
}
function onAiClick(ev) {
//if (this.live2d) this.live2d.click(ev);
}
if (window.innerWidth < 1024) {
const currentUI = miLocalStorage.getItem('ui');
miLocalStorage.setItem('ui_temp', currentUI ?? 'default');
miLocalStorage.setItem('ui', 'default');
window.location.reload();
}
window.document.documentElement.style.overflowY = 'scroll';
onMounted(() => {
window.addEventListener('resize', () => {
isDesktop.value = (window.innerWidth >= DESKTOP_THRESHOLD);
}, { passive: true });
if (prefer.s.aiChanMode) {
const iframeRect = live2d.value.getBoundingClientRect();
window.addEventListener('mousemove', ev => {
live2d.value.contentWindow.postMessage({
type: 'moveCursor',
body: {
x: ev.clientX - iframeRect.left,
y: ev.clientY - iframeRect.top,
},
}, '*');
}, { passive: true });
window.addEventListener('touchmove', ev => {
live2d.value.contentWindow.postMessage({
type: 'moveCursor',
body: {
x: ev.touches[0].clientX - iframeRect.left,
y: ev.touches[0].clientY - iframeRect.top,
},
}, '*');
}, { passive: true });
}
});
</script>
<style lang="scss" scoped>
.tray-enter-active,
.tray-leave-active {
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);
}
.tray-enter-from,
.tray-leave-active {
opacity: 0;
transform: translateX(240px);
}
.tray-back-enter-active,
.tray-back-leave-active {
opacity: 1;
transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.tray-back-enter-from,
.tray-back-leave-active {
opacity: 0;
}
.gbhvwtnk {
$ui-font-size: 1em;
$widgets-hide-threshold: 1200px;
min-height: 100dvh;
box-sizing: border-box;
&.wallpaper {
background: var(--MI_THEME-wallpaperOverlay);
//backdrop-filter: var(--MI-blur, blur(4px));
}
> .columns {
display: flex;
justify-content: center;
max-width: 100%;
//margin: 32px 0;
&.fullView {
margin: 0;
> .sidebar {
display: none;
}
> .widgets {
display: none;
}
> .main {
margin: 0;
border-radius: 0;
box-shadow: none;
width: 100%;
}
}
> .main {
min-width: 0;
width: 750px;
margin: 0 16px 0 0;
border-left: solid 1px var(--MI_THEME-divider);
border-right: solid 1px var(--MI_THEME-divider);
border-radius: 0;
overflow: clip;
--MI-margin: 12px;
}
> .widgets {
//--MI_THEME-panelBorder: none;
width: 300px;
padding-bottom: calc(var(--MI-margin) + env(safe-area-inset-bottom, 0px));
@media (max-width: $widgets-hide-threshold) {
display: none;
}
&.left {
margin-right: 16px;
}
}
> .sidebar {
margin-top: 16px;
}
&.withGlobalHeader {
> .main {
margin-top: 0;
border: solid 1px var(--MI_THEME-divider);
border-radius: var(--MI-radius);
--MI-stickyTop: var(--globalHeaderHeight);
}
> .widgets {
--MI-stickyTop: var(--globalHeaderHeight);
margin-top: 0;
}
}
@media (max-width: 850px) {
margin: 0;
> .sidebar {
border-right: solid 0.5px var(--MI_THEME-divider);
}
> .main {
margin: 0;
border-radius: 0;
box-shadow: none;
width: 100%;
}
}
}
> .tray-back {
z-index: 1001;
}
> .tray {
position: fixed;
top: 0;
right: 0;
z-index: 1001;
height: 100dvh;
padding: var(--MI-margin) var(--MI-margin) calc(var(--MI-margin) + env(safe-area-inset-bottom, 0px));
box-sizing: border-box;
overflow: auto;
background: var(--MI_THEME-bg);
}
> .ivnzpscs {
position: fixed;
bottom: 0;
right: 0;
width: 300px;
height: 600px;
border: none;
pointer-events: none;
}
}
.content {
height: 100%;
}
</style>