mirror of
https://github.com/purocean/yn.git
synced 2026-09-01 15:37:16 +08:00
feat(right-side-panel): add content right side panel with toggle functionality
This commit is contained in:
@@ -21,6 +21,9 @@
|
||||
<template v-slot:preview>
|
||||
<Previewer />
|
||||
</template>
|
||||
<template v-slot:content-right-side>
|
||||
<ContentRightSide />
|
||||
</template>
|
||||
<template v-slot:right-before>
|
||||
<FileTabs />
|
||||
</template>
|
||||
@@ -57,6 +60,7 @@ import Terminal from '@fe/components/Terminal.vue'
|
||||
import FileTabs from '@fe/components/FileTabs.vue'
|
||||
import Editor from '@fe/components/Editor.vue'
|
||||
import Previewer from '@fe/components/Previewer.vue'
|
||||
import ContentRightSide from '@fe/components/ContentRightSide.vue'
|
||||
|
||||
import SettingPanel from '@fe/components/SettingPanel.vue'
|
||||
import ExportPanel from '@fe/components/ExportPanel.vue'
|
||||
@@ -82,6 +86,7 @@ export default defineComponent({
|
||||
FileTabs,
|
||||
Editor,
|
||||
Previewer,
|
||||
ContentRightSide,
|
||||
XFilter,
|
||||
Premium,
|
||||
SettingPanel,
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
<template>
|
||||
<div class="content-right-side-panel">
|
||||
<div class="panel-header">
|
||||
<div
|
||||
:class="{'panel-title': true, clickable: panels.length > 1}"
|
||||
:title="currentPanel?.displayName"
|
||||
@click="showPanelSwitcher"
|
||||
>
|
||||
<span class="title-text">{{ currentPanel?.displayName }}</span>
|
||||
<svg-icon v-if="panels.length > 1" class="dropdown-icon" name="chevron-down" width="10px" />
|
||||
</div>
|
||||
<div class="panel-actions">
|
||||
<template v-for="btn in actionBtns" :key="btn.type === 'separator' ? undefined : btn.key">
|
||||
<div v-if="btn.type === 'separator' && !btn.hidden" class="action-separator"></div>
|
||||
<div
|
||||
v-else-if="btn.type === 'normal' && !btn.hidden"
|
||||
class="action-btn"
|
||||
:title="btn.title"
|
||||
@click="btn.onClick"
|
||||
>
|
||||
<svg-icon :name="btn.icon" width="12px" />
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="actionBtns.length > 0" class="action-separator"></div>
|
||||
<div class="action-btn" :title="$t('close')" @click="hide">
|
||||
<svg-icon name="times" width="10px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-content">
|
||||
<keep-alive>
|
||||
<component v-if="keepAlivePanel" :is="keepAlivePanel.component" :key="keepAlivePanel.name" />
|
||||
</keep-alive>
|
||||
<component v-if="nonKeepAlivePanel" :is="nonKeepAlivePanel.component" :key="nonKeepAlivePanel.name" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, onBeforeUnmount, onMounted, ref, shallowRef } from 'vue'
|
||||
import { registerHook, removeHook } from '@fe/core/hook'
|
||||
import type { RightSidePanel, Components } from '@fe/types'
|
||||
import store from '@fe/support/store'
|
||||
import { ContentRightSide } from '@fe/services/workbench'
|
||||
import { toggleContentRightSide } from '@fe/services/layout'
|
||||
import { useQuickFilter } from '@fe/support/ui/quick-filter'
|
||||
import { useI18n } from '@fe/services/i18n'
|
||||
import { orderBy } from 'lodash-es'
|
||||
import SvgIcon from './SvgIcon.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'content-right-side',
|
||||
components: { SvgIcon },
|
||||
setup () {
|
||||
useI18n()
|
||||
|
||||
const titleRef = ref<HTMLElement>()
|
||||
|
||||
const panels = shallowRef<RightSidePanel[]>(ContentRightSide.getAllPanels())
|
||||
|
||||
const currentPanel = computed<RightSidePanel | null>(() => {
|
||||
const name = store.state.currentRightSidePanel
|
||||
if (!name) {
|
||||
return panels.value[0] || null
|
||||
}
|
||||
return panels.value.find(p => p.name === name) || panels.value[0] || null
|
||||
})
|
||||
|
||||
const keepAlivePanel = computed<RightSidePanel | null>(() => {
|
||||
return currentPanel.value?.keepAlive ? currentPanel.value : null
|
||||
})
|
||||
|
||||
const nonKeepAlivePanel = computed<RightSidePanel | null>(() => {
|
||||
return currentPanel.value && !currentPanel.value.keepAlive ? currentPanel.value : null
|
||||
})
|
||||
|
||||
const actionBtns = computed<Components.RightSidePanel.ActionBtn[]>(() => {
|
||||
const btns = currentPanel.value?.actionBtns || []
|
||||
return orderBy(btns.filter(btn => !btn.hidden), x => x.order ?? 256, 'asc')
|
||||
})
|
||||
|
||||
function switchPanel (name: string) {
|
||||
ContentRightSide.switchPanel(name)
|
||||
}
|
||||
|
||||
function showPanelSwitcher (e: MouseEvent) {
|
||||
if (panels.value.length <= 1) {
|
||||
return
|
||||
}
|
||||
|
||||
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect()
|
||||
useQuickFilter().show({
|
||||
filterInputHidden: true,
|
||||
top: `${rect.bottom + 4}px`,
|
||||
left: `${rect.left}px`,
|
||||
list: panels.value.map(p => ({ key: p.name, label: p.displayName })),
|
||||
current: currentPanel.value?.name,
|
||||
onChoose: ({ key }) => {
|
||||
switchPanel(key)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function hide () {
|
||||
toggleContentRightSide(false)
|
||||
}
|
||||
|
||||
function refresh () {
|
||||
panels.value = ContentRightSide.getAllPanels()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
registerHook('RIGHT_SIDE_PANEL_CHANGE', refresh)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
removeHook('RIGHT_SIDE_PANEL_CHANGE', refresh)
|
||||
})
|
||||
|
||||
return {
|
||||
titleRef,
|
||||
panels,
|
||||
currentPanel,
|
||||
keepAlivePanel,
|
||||
nonKeepAlivePanel,
|
||||
actionBtns,
|
||||
switchPanel,
|
||||
showPanelSwitcher,
|
||||
hide,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.content-right-side-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 30px;
|
||||
padding: 0 8px;
|
||||
border-bottom: 1px solid var(--g-color-86);
|
||||
background: var(--g-color-96);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--g-color-20);
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
|
||||
.title-text {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
color: var(--g-color-40);
|
||||
}
|
||||
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
padding: 4px 6px;
|
||||
margin: -4px -6px;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background: var(--g-color-90);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.panel-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
color: var(--g-color-40);
|
||||
|
||||
&:hover {
|
||||
background: var(--g-color-86);
|
||||
color: var(--g-color-0);
|
||||
}
|
||||
}
|
||||
|
||||
.action-separator {
|
||||
width: 1px;
|
||||
height: 14px;
|
||||
background: var(--g-color-80);
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
height: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -65,6 +65,14 @@ export default {
|
||||
subTitle: getKeysLabel('layout.toggle-xterm'),
|
||||
onClick: () => ctx.layout.toggleXterm()
|
||||
}] : []),
|
||||
...(ctx.workbench.ContentRightSide.getAllPanels().length > 0 ? [{
|
||||
id: 'toggle-content-right-side',
|
||||
type: 'normal' as any,
|
||||
checked: ctx.store.state.showContentRightSide,
|
||||
title: ctx.i18n.t('status-bar.view.content-right-side'),
|
||||
subTitle: getKeysLabel('layout.toggle-content-right-side'),
|
||||
onClick: () => ctx.layout.toggleContentRightSide()
|
||||
}] : []),
|
||||
{
|
||||
id: 'toggle-editor-preview-exclusive',
|
||||
type: 'normal',
|
||||
|
||||
@@ -146,10 +146,3 @@ registerAction({
|
||||
keys: [Alt, 't']
|
||||
})
|
||||
|
||||
registerAction({
|
||||
name: 'layout.toggle-content-right-side',
|
||||
description: t('command-desc.layout_toggle-content-right-side'),
|
||||
handler: toggleContentRightSide,
|
||||
forUser: true,
|
||||
keys: [Alt, 'b']
|
||||
})
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { debounce } from 'lodash-es'
|
||||
import { debounce, orderBy } from 'lodash-es'
|
||||
import * as ioc from '@fe/core/ioc'
|
||||
import { triggerHook } from '@fe/core/hook'
|
||||
import { getActionHandler, registerAction } from '@fe/core/action'
|
||||
import { Alt, Shift } from '@fe/core/keybinding'
|
||||
import store from '@fe/support/store'
|
||||
import type { Components } from '@fe/types'
|
||||
import type { Components, RightSidePanel } from '@fe/types'
|
||||
import { t } from './i18n'
|
||||
|
||||
/**
|
||||
@@ -136,3 +137,111 @@ export const ControlCenter = {
|
||||
getActionHandler('control-center.toggle')(visible)
|
||||
},
|
||||
}
|
||||
|
||||
export const ContentRightSide = {
|
||||
/**
|
||||
* Register a right side panel.
|
||||
* @param panel Panel
|
||||
* @param override Override the existing panel
|
||||
*/
|
||||
registerPanel (panel: RightSidePanel, override = false) {
|
||||
if (!panel.component) {
|
||||
throw new Error('Panel component is required')
|
||||
}
|
||||
|
||||
// check if the panel is already registered
|
||||
if (ioc.get('RIGHT_SIDE_PANEL').some(item => item.name === panel.name)) {
|
||||
if (override) {
|
||||
ContentRightSide.removePanel(panel.name)
|
||||
} else {
|
||||
throw new Error(`Panel ${panel.name} is already registered`)
|
||||
}
|
||||
}
|
||||
|
||||
ioc.register('RIGHT_SIDE_PANEL', panel)
|
||||
triggerHook('RIGHT_SIDE_PANEL_CHANGE', { type: 'register' })
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a right side panel.
|
||||
* @param name Panel name
|
||||
*/
|
||||
removePanel (name: string) {
|
||||
ioc.removeWhen('RIGHT_SIDE_PANEL', item => item.name === name)
|
||||
|
||||
// if the current panel is removed, switch to another one or hide
|
||||
if (store.state.currentRightSidePanel === name) {
|
||||
const panels = ContentRightSide.getAllPanels()
|
||||
if (panels.length > 0) {
|
||||
ContentRightSide.switchPanel(panels[0].name)
|
||||
} else {
|
||||
store.state.currentRightSidePanel = null
|
||||
}
|
||||
}
|
||||
|
||||
triggerHook('RIGHT_SIDE_PANEL_CHANGE', { type: 'remove' })
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all registered panels.
|
||||
* @returns Panels
|
||||
*/
|
||||
getAllPanels (): RightSidePanel[] {
|
||||
return [...orderBy(ioc.get('RIGHT_SIDE_PANEL'), x => x.order ?? 256, 'asc')]
|
||||
},
|
||||
|
||||
/**
|
||||
* Switch to a panel by name.
|
||||
* @param name Panel name
|
||||
*/
|
||||
switchPanel (name: string) {
|
||||
const panel = ContentRightSide.getAllPanels().find(p => p.name === name)
|
||||
if (!panel) {
|
||||
throw new Error(`Panel ${name} not found`)
|
||||
}
|
||||
|
||||
store.state.currentRightSidePanel = name
|
||||
triggerHook('RIGHT_SIDE_PANEL_CHANGE', { type: 'switch' })
|
||||
getActionHandler('layout.toggle-content-right-side')(true)
|
||||
},
|
||||
|
||||
/**
|
||||
* Show right side panel with a specific panel.
|
||||
* @param name Panel name, if not provided, show the current or first panel
|
||||
*/
|
||||
show (name?: string) {
|
||||
const panels = ContentRightSide.getAllPanels()
|
||||
if (panels.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (name) {
|
||||
ContentRightSide.switchPanel(name)
|
||||
} else {
|
||||
const currentPanel = store.state.currentRightSidePanel
|
||||
if (!currentPanel || !panels.find(p => p.name === currentPanel)) {
|
||||
store.state.currentRightSidePanel = panels[0].name
|
||||
}
|
||||
getActionHandler('layout.toggle-content-right-side')(true)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide right side panel.
|
||||
*/
|
||||
hide () {
|
||||
getActionHandler('layout.toggle-content-right-side')(false)
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle right side panel visibility.
|
||||
* @param visible
|
||||
*/
|
||||
toggle (visible?: boolean) {
|
||||
// Do nothing if no panels registered
|
||||
if (ContentRightSide.getAllPanels().length === 0) {
|
||||
return
|
||||
}
|
||||
getActionHandler('layout.toggle-content-right-side')(visible)
|
||||
},
|
||||
}
|
||||
|
||||
+22
-5
@@ -1,6 +1,7 @@
|
||||
import { Fragment, h } from 'vue'
|
||||
import { init } from '@fe/core/plugin'
|
||||
import { getActionHandler } from '@fe/core/action'
|
||||
import { Alt } from '@fe/core/keybinding'
|
||||
import { getActionHandler, registerAction } from '@fe/core/action'
|
||||
import { registerHook, triggerHook } from '@fe/core/hook'
|
||||
import store from '@fe/support/store'
|
||||
import { isElectron, isWindows } from '@fe/support/env'
|
||||
@@ -26,7 +27,7 @@ import ctx from '@fe/context'
|
||||
import ga from '@fe/support/ga'
|
||||
import * as jsonrpc from '@fe/support/jsonrpc'
|
||||
import { getLogger, sleep } from '@fe/utils'
|
||||
import { removeOldDatabases } from './others/db'
|
||||
import { removeOldDatabases } from '@fe/others/db'
|
||||
|
||||
const logger = getLogger('startup')
|
||||
|
||||
@@ -83,6 +84,7 @@ registerHook('DOC_MOVED', refreshTree)
|
||||
registerHook('DOC_SWITCH_FAILED', refreshTree)
|
||||
registerHook('DOC_BEFORE_DELETE', reWatchFsOnWindows)
|
||||
registerHook('DOC_BEFORE_MOVE', reWatchFsOnWindows)
|
||||
registerHook('RIGHT_SIDE_PANEL_CHANGE', ctx.statusBar.refreshMenu)
|
||||
|
||||
registerHook('INDEXER_FS_CHANGE', async () => {
|
||||
if (Date.now() - autoRefreshedAt > 3000) {
|
||||
@@ -276,21 +278,36 @@ whenEditorReady().then(() => {
|
||||
})
|
||||
|
||||
// json-rpc
|
||||
|
||||
jsonrpc.init({ ctx }, whenEditorReady())
|
||||
|
||||
setTimeout(() => {
|
||||
removeOldDatabases()
|
||||
}, 20000)
|
||||
|
||||
// google analytics
|
||||
|
||||
registerHook('DOC_SWITCHED', () => {
|
||||
setTimeout(() => {
|
||||
ga.logEvent('yn_doc_switched')
|
||||
}, 0)
|
||||
})
|
||||
|
||||
registerHook('RIGHT_SIDE_PANEL_CHANGE', ({ type }) => {
|
||||
if (type === 'remove' && ctx.workbench.ContentRightSide.getAllPanels().length < 1) {
|
||||
ctx.layout.toggleContentRightSide(false)
|
||||
}
|
||||
})
|
||||
|
||||
registerAction({
|
||||
name: 'layout.toggle-content-right-side',
|
||||
description: t('command-desc.layout_toggle-content-right-side'),
|
||||
handler: ctx.layout.toggleContentRightSide,
|
||||
forUser: true,
|
||||
when() {
|
||||
return ctx.workbench.ContentRightSide.getAllPanels().length > 0
|
||||
},
|
||||
keys: [Alt, 'b']
|
||||
})
|
||||
|
||||
// google analytics
|
||||
ga.logEvent('page_view', {
|
||||
page_title: '--STARTUP--',
|
||||
page_location: window.location.href,
|
||||
|
||||
@@ -23,6 +23,7 @@ export const initState = {
|
||||
showView: storage.get('showView', true),
|
||||
showEditor: storage.get('showEditor', true),
|
||||
showContentRightSide: false,
|
||||
currentRightSidePanel: null as string | null,
|
||||
editorPreviewExclusive: storage.get('editorPreviewExclusive', false),
|
||||
showXterm: false,
|
||||
showOutline: false,
|
||||
|
||||
@@ -202,6 +202,19 @@ export namespace Components {
|
||||
}
|
||||
}
|
||||
|
||||
export namespace RightSidePanel {
|
||||
export type ActionBtn = {
|
||||
type: 'normal',
|
||||
key?: string | number,
|
||||
icon: string,
|
||||
title: string,
|
||||
order?: number,
|
||||
hidden?: boolean,
|
||||
onClick: (e: MouseEvent) => void,
|
||||
}
|
||||
| { type: 'separator', order?: number, hidden?: boolean }
|
||||
}
|
||||
|
||||
export namespace FixedFloat {
|
||||
export interface Props {
|
||||
disableAutoFocus?: boolean;
|
||||
@@ -471,6 +484,7 @@ export type BuildInActions = {
|
||||
'layout.toggle-side': (visible?: boolean) => void,
|
||||
'layout.toggle-xterm': (visible?: boolean) => void,
|
||||
'layout.toggle-editor': (visible?: boolean) => void,
|
||||
'layout.toggle-content-right-side': (visible?: boolean) => void,
|
||||
'control-center.toggle': (visible?: boolean) => void,
|
||||
'status-bar.refresh-menu': () => void,
|
||||
'control-center.refresh': () => void,
|
||||
@@ -567,6 +581,7 @@ export type BuildInHookTypes = {
|
||||
EDITOR_READY: { editor: Monaco.editor.IStandaloneCodeEditor, monaco: typeof Monaco },
|
||||
EDITOR_CUSTOM_EDITOR_CHANGE: { type: 'register' | 'remove' | 'switch' },
|
||||
EDITOR_CURRENT_EDITOR_CHANGE: { current?: CustomEditor | null },
|
||||
RIGHT_SIDE_PANEL_CHANGE: { type: 'register' | 'remove' | 'switch' },
|
||||
EDITOR_CONTENT_CHANGE: { uri: string, value: string },
|
||||
EDITOR_ATTEMPT_READONLY_EDIT: { doc: Doc | null, readonlyType: 'app-readonly' | 'no-file' | 'file-not-writable' | 'unsupported-file-type' },
|
||||
DOC_CREATED: { doc: Doc },
|
||||
@@ -625,6 +640,15 @@ export type CustomEditor = {
|
||||
getIsDirty?: () => boolean | Promise<boolean>,
|
||||
}
|
||||
|
||||
export type RightSidePanel = {
|
||||
name: string,
|
||||
displayName: string,
|
||||
order?: number,
|
||||
keepAlive?: boolean,
|
||||
component: any,
|
||||
actionBtns?: Components.RightSidePanel.ActionBtn[],
|
||||
}
|
||||
|
||||
export type Renderer = {
|
||||
name: string,
|
||||
order?: number,
|
||||
@@ -684,6 +708,7 @@ export type BuildInIOCTypes = { [key in keyof BuildInHookTypes]: any; } & {
|
||||
THEME_STYLES: any;
|
||||
VIEW_PREVIEWER: Previewer;
|
||||
EDITOR_CUSTOM_EDITOR: CustomEditor,
|
||||
RIGHT_SIDE_PANEL: RightSidePanel,
|
||||
RENDERERS: Renderer,
|
||||
CODE_RUNNER: CodeRunner;
|
||||
DOC_CATEGORIES: DocCategory;
|
||||
|
||||
@@ -241,6 +241,7 @@ const data = {
|
||||
'preview': 'Show Preview',
|
||||
'editor': 'Show Editor',
|
||||
'side-bar': 'Show Side Bar',
|
||||
'content-right-side': 'Show Right Side Panel',
|
||||
'word-wrap': 'Word Wrap',
|
||||
'typewriter-mode': 'Typewriter Mode',
|
||||
'editor-preview-exclusive': 'Editor/Preview Exclusive',
|
||||
|
||||
@@ -242,6 +242,7 @@ const data: BaseLanguage = {
|
||||
'preview': 'Предварительный просмотр',
|
||||
'editor': 'Показать редактор',
|
||||
'side-bar': 'Показать боковую панель',
|
||||
'content-right-side': 'Показать правую панель',
|
||||
'word-wrap': 'Перенос снов',
|
||||
'typewriter-mode': 'Режим печатной машинки',
|
||||
'editor-preview-exclusive': 'Только режим редактора/просмотра',
|
||||
|
||||
@@ -241,8 +241,7 @@ const data: BaseLanguage = {
|
||||
'xterm': '显示终端',
|
||||
'preview': '显示预览',
|
||||
'editor': '显示编辑',
|
||||
'side-bar': '显示侧栏',
|
||||
'word-wrap': '文本换行',
|
||||
'side-bar': '显示侧栏', 'content-right-side': '显示右侧面板', 'word-wrap': '文本换行',
|
||||
'typewriter-mode': '打字机模式',
|
||||
'editor-preview-exclusive': '编辑器/预览互斥',
|
||||
},
|
||||
|
||||
@@ -241,8 +241,7 @@ const data: BaseLanguage = {
|
||||
'xterm': '顯示終端',
|
||||
'preview': '顯示預覽',
|
||||
'editor': '顯示編輯',
|
||||
'side-bar': '顯示側欄',
|
||||
'word-wrap': '文本換行',
|
||||
'side-bar': '顯示側欄', 'content-right-side': '顯示右側面板', 'word-wrap': '文本換行',
|
||||
'typewriter-mode': '打字機模式',
|
||||
'editor-preview-exclusive': '編輯器/預覽互斥',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user