feat: add custom title bar

This commit is contained in:
tiny-craft 2023-09-02 18:23:40 +08:00
parent b580fedade
commit 1a4622cfd0
24 changed files with 369 additions and 214 deletions

View File

@ -63,7 +63,7 @@ watch(
class="fill-height">
<n-global-style />
<n-dialog-provider>
<n-spin v-show="initializing" :theme-overrides="{ opacitySpinning: 0 }">
<n-spin v-show="initializing" :theme-overrides="{ opacitySpinning: 0 }" style="--wails-draggable: drag">
<div id="launch-container" />
</n-spin>
<app-content v-if="!initializing" class="flex-item-expand" />

View File

@ -11,6 +11,11 @@ import useTabStore from './stores/tab.js'
import usePreferencesStore from './stores/preferences.js'
import useConnectionStore from './stores/connections.js'
import ContentLogPane from './components/content/ContentLogPane.vue'
import ContentValueTab from '@/components/content/ContentValueTab.vue'
import ToolbarControlWidget from '@/components/common/ToolbarControlWidget.vue'
import { WindowToggleMaximise } from 'wailsjs/runtime/runtime.js'
import { isMacOS } from '@/utils/platform.js'
import iconUrl from '@/assets/images/icon.png'
const themeVars = useThemeVars()
@ -19,6 +24,7 @@ const data = reactive({
navMenuWidth: 60,
hoverResize: false,
resizing: false,
toolbarHeight: 45,
})
const tabStore = useTabStore()
@ -69,49 +75,85 @@ watch(
<template>
<!-- app content-->
<div id="app-content-wrapper" :class="{ dragging }" :style="prefStore.generalFont" class="flex-box-h">
<nav-menu v-model:value="tabStore.nav" :width="data.navMenuWidth" />
<!-- browser page-->
<div v-show="tabStore.nav === 'browser'" class="flex-box-h flex-item-expand">
<div id="app-side" :style="{ width: asideWidthVal }" class="flex-box-h flex-item">
<browser-pane
v-for="t in tabStore.tabs"
v-show="get(tabStore.currentTab, 'name') === t.name"
:key="t.name"
class="flex-item-expand" />
<div
:class="{
'resize-divider-hover': data.hoverResize,
'resize-divider-drag': data.resizing,
}"
class="resize-divider"
@mousedown="startResize"
@mouseout="data.hoverResize = false"
@mouseover="data.hoverResize = true" />
<div id="app-content-wrapper" class="flex-box-v">
<!-- title bar -->
<div
id="app-toolbar"
class="flex-box-h"
style="--wails-draggable: drag"
:style="{ height: data.toolbarHeight + 'px' }"
@dblclick="WindowToggleMaximise">
<!-- title -->
<div
id="app-toolbar-title"
:style="{
width: `${data.navMenuWidth + prefStore.general.asideWidth}px`,
paddingLeft: isMacOS() ? '70px' : '10px',
}">
<n-space align="center" :wrap-item="false" :wrap="false" :size="3">
<n-avatar :src="iconUrl" color="#0000" :size="35" style="min-width: 35px" />
<div style="min-width: 68px; font-weight: 800">Tiny RDM</div>
<transition name="fade">
<n-text v-if="tabStore.nav === 'browser'" strong class="ellipsis" style="font-size: 13px">
- {{ get(tabStore.currentTab, 'name') }}
</n-text>
</transition>
</n-space>
</div>
<content-pane class="flex-item-expand" />
<!-- browser tabs -->
<div v-show="tabStore.nav === 'browser'" class="app-toolbar-tab flex-item-expand">
<content-value-tab />
</div>
<div class="flex-item-expand"></div>
<!-- simulate window control buttons -->
<toolbar-control-widget v-if="!isMacOS()" :size="data.toolbarHeight" style="align-self: flex-start" />
</div>
<!-- server list page -->
<div v-show="tabStore.nav === 'server'" class="flex-box-h flex-item-expand">
<div id="app-side" :style="{ width: asideWidthVal }" class="flex-box-h flex-item">
<connection-pane class="flex-item-expand" />
<div
:class="{
'resize-divider-hover': data.hoverResize,
'resize-divider-drag': data.resizing,
}"
class="resize-divider"
@mousedown="startResize"
@mouseout="data.hoverResize = false"
@mouseover="data.hoverResize = true" />
<!-- content -->
<div id="app-content" :style="prefStore.generalFont" class="flex-box-h flex-item-expand">
<nav-menu v-model:value="tabStore.nav" :width="data.navMenuWidth" />
<!-- browser page-->
<div v-show="tabStore.nav === 'browser'" :class="{ dragging }" class="flex-box-h flex-item-expand">
<div id="app-side" :style="{ width: asideWidthVal }" class="flex-box-h flex-item">
<browser-pane
v-for="t in tabStore.tabs"
v-show="get(tabStore.currentTab, 'name') === t.name"
:key="t.name"
class="flex-item-expand" />
<div
:class="{
'resize-divider-hover': data.hoverResize,
'resize-divider-drag': data.resizing,
}"
class="resize-divider"
@mousedown="startResize"
@mouseout="data.hoverResize = false"
@mouseover="data.hoverResize = true" />
</div>
<content-pane class="flex-item-expand" />
</div>
<content-server-pane class="flex-item-expand" />
</div>
<!-- log page -->
<div v-show="tabStore.nav === 'log'" class="flex-box-h flex-item-expand">
<content-log-pane ref="logPaneRef" class="flex-item-expand" />
<!-- server list page -->
<div v-show="tabStore.nav === 'server'" :class="{ dragging }" class="flex-box-h flex-item-expand">
<div id="app-side" :style="{ width: asideWidthVal }" class="flex-box-h flex-item">
<connection-pane class="flex-item-expand" />
<div
:class="{
'resize-divider-hover': data.hoverResize,
'resize-divider-drag': data.resizing,
}"
class="resize-divider"
@mousedown="startResize"
@mouseout="data.hoverResize = false"
@mouseover="data.hoverResize = true" />
</div>
<content-server-pane class="flex-item-expand" />
</div>
<!-- log page -->
<div v-show="tabStore.nav === 'log'" class="flex-box-h flex-item-expand">
<content-log-pane ref="logPaneRef" class="flex-item-expand" />
</div>
</div>
</div>
</template>
@ -120,12 +162,28 @@ watch(
#app-content-wrapper {
height: 100%;
overflow: hidden;
border-top: v-bind('themeVars.borderColor') 1px solid;
box-sizing: border-box;
#app-toolbar {
height: 40px;
border-bottom: v-bind('themeVars.borderColor') 1px solid;
background-color: v-bind('themeVars.tabColor');
border-bottom: 1px solid v-bind('themeVars.borderColor');
&-title {
padding-left: 10px;
padding-right: 10px;
box-sizing: border-box;
align-self: center;
align-items: baseline;
}
}
.app-toolbar-tab {
align-self: flex-end;
margin-bottom: -1px;
}
#app-content {
height: calc(100% - 60px);
}
#app-side {
@ -157,4 +215,14 @@ watch(
.dragging {
cursor: col-resize !important;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@ -0,0 +1,82 @@
<script setup>
import WindowMin from '@/components/icons/WindowMin.vue'
import WindowMax from '@/components/icons/WindowMax.vue'
import WindowClose from '@/components/icons/WindowClose.vue'
import { computed } from 'vue'
import { useThemeVars } from 'naive-ui'
import { Quit, WindowMinimise, WindowToggleMaximise } from 'wailsjs/runtime/runtime.js'
const themeVars = useThemeVars()
const props = defineProps({
size: {
type: Number,
default: 35,
},
})
const buttonSize = computed(() => {
return props.size + 'px'
})
const handleMinimise = async () => {
WindowMinimise()
}
const handleMaximise = () => {
WindowToggleMaximise()
}
const handleClose = () => {
Quit()
}
</script>
<template>
<n-space :wrap-item="false" align="center" justify="center" :size="0">
<div class="btn-wrapper" @click="handleMinimise">
<window-min />
</div>
<div class="btn-wrapper" @click="handleMaximise">
<window-max />
</div>
<div class="btn-wrapper" @click="handleClose">
<window-close />
</div>
</n-space>
</template>
<style scoped lang="scss">
.btn-wrapper {
width: v-bind('buttonSize');
height: v-bind('buttonSize');
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
--wails-draggable: none;
&:hover {
cursor: pointer;
}
&:not(:last-child) {
&:hover {
background-color: v-bind('themeVars.closeColorHover');
}
&:active {
background-color: v-bind('themeVars.closeColorPressed');
}
}
&:last-child {
&:hover {
background-color: v-bind('themeVars.primaryColorHover');
}
&:active {
background-color: v-bind('themeVars.primaryColorPressed');
}
}
}
</style>

View File

@ -90,7 +90,7 @@ defineExpose({
<icon-button :icon="Delete" border t-tooltip="clean_log" @click="cleanHistory" />
</n-form-item>
</n-form>
<div class="fill-height flex-box-h" style="user-select: text">
<div class="content-value fill-height flex-box-h">
<n-data-table
ref="tableRef"
:columns="[

View File

@ -121,34 +121,10 @@ const onReloadKey = async () => {
}
await connectionStore.loadKeyValue(tab.name, tab.db, tab.key)
}
const i18n = useI18n()
const onCloseTab = (tabIndex) => {
$dialog.warning(i18n.t('close_confirm'), () => {
const tab = get(tabStore.tabs, tabIndex)
if (tab != null) {
connectionStore.closeConnection(tab.name)
}
})
}
</script>
<template>
<div class="content-container flex-box-v">
<!-- <content-value-tab :tabs="tab" />-->
<n-tabs
v-model:value="tabStore.activatedIndex"
:closable="true"
size="small"
type="card"
@close="onCloseTab"
@update:value="onUpdateValue">
<n-tab v-for="(t, i) in tab" :key="i" :name="i">
<n-ellipsis style="max-width: 150px">{{ t.label }}</n-ellipsis>
</n-tab>
</n-tabs>
<!-- TODO: add loading status -->
<div v-if="showServerStatus" class="content-container flex-item-expand flex-box-v">
<!-- select nothing or select server node, display server status -->
<content-server-status

View File

@ -1,136 +1,88 @@
<script setup>
import { ref } from 'vue'
import { ConnectionType } from '@/consts/connection_type.js'
import Close from '@/components/icons/Close.vue'
import useConnectionStore from 'stores/connections.js'
import ToggleServer from '@/components/icons/ToggleServer.vue'
import useTabStore from 'stores/tab.js'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { get, map } from 'lodash'
import { useThemeVars } from 'naive-ui'
const emit = defineEmits(['switchTab', 'closeTab', 'update:modelValue'])
import useConnectionStore from 'stores/connections.js'
const themeVars = useThemeVars()
const i18n = useI18n()
const tabStore = useTabStore()
const connectionStore = useConnectionStore()
const props = defineProps({
selectedIndex: {
type: Number,
default: 0,
},
modelValue: {
type: Object,
default: [
{
// label: 'tab1',
// key: 'key',
// bgColor: 'white',
},
],
},
tabs: {
type: Array,
default: [
{
// label: 'tab1',
// key: 'key',
// bgColor: 'white',
},
],
},
backgroundColor: String,
})
const connectionStore = useConnectionStore()
const onCurrentSelectChange = ({ type, group = '', server = '', db = 0, key = '' }) => {
console.log(`group: ${group}\n server: ${server}\n db: ${db}\n key: ${key}`)
if (type === ConnectionType.RedisValue) {
// load and update content value
}
}
// watch(() => databaseStore.currentSelect, throttle(onCurrentSelectChange, 1000))
const items = ref(props.modelValue)
const selIndex = ref(props.selectedIndex)
const onClickTab = (idx, key) => {
if (idx !== selIndex.value) {
selIndex.value = idx
emit('update:modelValue', idx, key)
}
const onCloseTab = (tabIndex) => {
$dialog.warning(i18n.t('close_confirm'), () => {
const tab = get(tabStore.tabs, tabIndex)
if (tab != null) {
connectionStore.closeConnection(tab.name)
}
})
}
const onCloseTab = (idx, key) => {
emit('closeTab', idx, key)
}
const activeTabStyle = computed(() => ({
backgroundColor: themeVars.value.baseColor,
borderTopWidth: '1px',
borderTopColor: themeVars.value.borderColor,
borderBottomColor: themeVars.value.baseColor,
borderTopLeftRadius: themeVars.value.borderRadius,
borderTopRightRadius: themeVars.value.borderRadius,
}))
const inactiveTabStyle = computed(() => ({
borderWidth: '0 0 1px',
borderBottomColor: themeVars.value.borderColor,
borderTopLeftRadius: themeVars.value.borderRadius,
borderTopRightRadius: themeVars.value.borderRadius,
}))
const tab = computed(() =>
map(tabStore.tabs, (item) => ({
key: item.name,
label: item.title,
})),
)
</script>
<template>
<!-- TODO: 检查标签是否太多, 左右两边显示左右切换翻页按钮 -->
<div class="content-tab flex-box-h">
<div
v-for="(item, i) in props.tabs"
:key="item.key"
:class="{ 'content-tab_selected': selIndex === i }"
:style="{ backgroundColor: item.bgColor || '' }"
:title="item.label"
class="content-tab_item flex-item-expand icon-btn flex-box-h"
@click="onClickTab(i, item.key)">
<n-icon :component="Close" class="content-tab_item-close" size="20" @click.stop="onCloseTab(i, item.key)" />
<div class="content-tab_item-label ellipsis flex-item-expand">
{{ item.label }}
</div>
</div>
</div>
<n-tabs
v-model:value="tabStore.activatedIndex"
:closable="true"
:tab-style="{
borderStyle: 'solid',
borderWidth: '1px',
}"
size="small"
type="card"
@close="onCloseTab"
@update:value="(tabIndex) => tabStore.switchTab(tabIndex)"
:theme-overrides="{
tabFontWeightActive: 800,
tabBorderRadius: 0,
tabGapSmallCard: 0,
tabGapMediumCard: 0,
tabGapLargeCard: 0,
tabColor: '#0000',
tabBorderColor: themeVars.borderColor,
}">
<n-tab
v-for="(t, i) in tab"
:key="i"
:name="i"
:closable="tabStore.activatedIndex === i"
:style="tabStore.activatedIndex === i ? activeTabStyle : inactiveTabStyle"
style="--wails-draggable: none"
@dblclick.stop="() => {}">
<n-space align="center" justify="center" :wrap-item="false" :size="5" inline>
<n-icon :component="ToggleServer" size="18" />
<n-ellipsis style="max-width: 150px">{{ t.label }}</n-ellipsis>
</n-space>
</n-tab>
</n-tabs>
</template>
<style lang="scss" scoped>
.content-tab {
align-items: center;
//justify-content: center;
width: 100%;
height: 40px;
overflow: hidden;
font-size: 14px;
&_item {
flex: 1 0;
overflow: hidden;
align-items: center;
justify-content: center;
gap: 3px;
height: 100%;
box-sizing: border-box;
background-color: var(--bg-color-page);
color: var(--text-color-secondary);
padding: 0 5px;
//border-top: var(--el-border-color) 1px solid;
border-right: var(--border-color) 1px solid;
transition: all var(--transition-duration-fast) var(--transition-function-ease-in-out-bezier);
&-label {
text-align: center;
}
&-close {
//display: none;
display: inline-flex;
width: 0;
transition: width 0.3s;
&:hover {
background-color: rgb(176, 177, 182, 0.4);
}
}
&:hover {
.content-tab_item-close {
//display: block;
width: 20px;
transition: width 0.3s;
}
}
}
&_selected {
border-top: v-bind('themeVars.primaryColor') 4px solid !important;
background-color: #ffffff;
color: #303133;
}
}
</style>
<style scoped lang="scss"></style>

View File

@ -265,13 +265,15 @@ const onUpdateFilter = (filters, sourceColumn) => {
{{ $t('add_row') }}
</n-button>
</div>
<div class="fill-height flex-box-h" style="user-select: text">
<div class="value-wrapper fill-height flex-box-h">
<n-data-table
:key="(row) => row.no"
:columns="columns"
:data="tableData"
:single-column="true"
:single-line="false"
:bordered="false"
:bottom-bordered="false"
flex-height
max-height="100%"
size="small"

View File

@ -189,13 +189,15 @@ const onUpdateFilter = (filters, sourceColumn) => {
{{ $t('add_row') }}
</n-button>
</div>
<div class="fill-height flex-box-h" style="user-select: text">
<div class="value-wrapper fill-height flex-box-h">
<n-data-table
:key="(row) => row.no"
:columns="columns"
:data="tableData"
:single-column="true"
:single-line="false"
:bordered="false"
:bottom-bordered="false"
flex-height
max-height="100%"
size="small"

View File

@ -185,13 +185,15 @@ const onUpdateFilter = (filters, sourceColumn) => {
{{ $t('add_row') }}
</n-button>
</div>
<div class="fill-height flex-box-h" style="user-select: text">
<div class="value-wrapper fill-height flex-box-h">
<n-data-table
:key="(row) => row.no"
:columns="columns"
:data="tableData"
:single-column="true"
:single-line="false"
:bordered="false"
:bottom-bordered="false"
flex-height
max-height="100%"
size="small"

View File

@ -178,13 +178,15 @@ const onUpdateFilter = (filters, sourceColumn) => {
{{ $t('add_row') }}
</n-button>
</div>
<div class="fill-height flex-box-h" style="user-select: text">
<div class="value-wrapper fill-height flex-box-h">
<n-data-table
:key="(row) => row.id"
:columns="columns"
:data="tableData"
:single-column="true"
:single-line="false"
:bordered="false"
:bottom-bordered="false"
flex-height
max-height="100%"
size="small"

View File

@ -220,5 +220,6 @@ const onSaveValue = async () => {
.value-wrapper {
overflow: hidden;
border-top: v-bind('themeVars.borderColor') 1px solid;
padding: 5px;
}
</style>

View File

@ -296,13 +296,15 @@ const onUpdateFilter = (filters, sourceColumn) => {
{{ $t('add_row') }}
</n-button>
</div>
<div class="fill-height flex-box-h" style="user-select: text">
<div class="value-wrapper fill-height flex-box-h">
<n-data-table
:key="(row) => row.no"
:columns="columns"
:data="tableData"
:single-column="true"
:single-line="false"
:bordered="false"
:bottom-bordered="false"
flex-height
max-height="100%"
size="small"

View File

@ -0,0 +1,19 @@
<script setup>
const props = defineProps({
size: {
type: [Number, String],
default: 12,
},
})
</script>
<template>
<svg aria-hidden="false" :width="props.size" :height="props.size" viewBox="0 0 12 12">
<polygon
fill="currentColor"
fill-rule="evenodd"
points="11 1.576 6.583 6 11 10.424 10.424 11 6 6.583 1.576 11 1 10.424 5.417 6 1 1.576 1.576 1 6 5.417 10.424 1"></polygon>
</svg>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,16 @@
<script setup>
const props = defineProps({
size: {
type: [Number, String],
default: 12,
},
})
</script>
<template>
<svg aria-hidden="false" :width="props.size" :height="props.size" viewBox="0 0 12 12">
<rect width="9" height="9" x="1.5" y="1.5" fill="none" stroke="currentColor"></rect>
</svg>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,16 @@
<script setup>
const props = defineProps({
size: {
type: [Number, String],
default: 12,
},
})
</script>
<template>
<svg aria-hidden="false" :width="props.size" :height="props.size" viewBox="0 0 12 12">
<rect fill="currentColor" width="10" height="1" x="1" y="6"></rect>
</svg>
</template>
<style lang="scss" scoped></style>

View File

@ -50,14 +50,15 @@ const selectedKeys = computed(() => {
const data = computed(() => {
const dbs = get(connectionStore.databases, props.server, [])
return [
{
key: `${props.server}`,
label: props.server,
type: ConnectionType.Server,
children: dbs,
},
]
return dbs
// return [
// {
// key: `${props.server}`,
// label: props.server,
// type: ConnectionType.Server,
// children: dbs,
// },
// ]
})
const backgroundColor = computed(() => {
@ -368,6 +369,8 @@ const renderPrefix = ({ option }) => {
// render tree item label
const renderLabel = ({ option }) => {
switch (option.type) {
case ConnectionType.Server:
return h('b', {}, { default: () => option.label })
case ConnectionType.RedisDB:
const { name: server, db } = option
let { match: matchPattern, type: typeFilter } = connectionStore.getKeyFilter(server, db)

View File

@ -32,7 +32,7 @@ const emit = defineEmits(['update:value'])
const iconSize = computed(() => Math.floor(props.width * 0.4))
const renderIcon = (icon) => {
return () => h(NIcon, null, { default: () => h(icon, { strokeWidth: 4 }) })
return () => h(NIcon, null, { default: () => h(icon, { strokeWidth: 3 }) })
}
const connectionStore = useConnectionStore()
@ -122,7 +122,7 @@ const openGithub = () => {
:render-label="renderContextLabel"
trigger="click"
@select="onSelectPreferenceMenu">
<icon-button :icon="Config" :size="iconSize" stroke-width="4" class="nav-menu-button" />
<icon-button :icon="Config" :size="iconSize" :stroke-width="3" class="nav-menu-button" />
</n-dropdown>
<icon-button :icon="Github" :size="iconSize" class="nav-menu-button" @click="openGithub" />
</div>
@ -131,7 +131,7 @@ const openGithub = () => {
<style lang="scss">
#app-nav-menu {
height: 100vh;
//height: 100vh;
//border-right: v-bind('themeVars.borderColor') solid 1px;
.nav-menu-item {

View File

@ -8,6 +8,7 @@ import relativeTime from 'dayjs/plugin/relativeTime'
import { i18n } from '@/utils/i18n.js'
import { setupDiscreteApi } from '@/utils/discrete.js'
import usePreferencesStore from 'stores/preferences.js'
import { loadEnvironment } from '@/utils/platform.js'
dayjs.extend(duration)
dayjs.extend(relativeTime)
@ -17,6 +18,7 @@ async function setupApp() {
app.use(i18n)
app.use(createPinia())
await loadEnvironment()
const prefStore = usePreferencesStore()
await prefStore.loadPreferences()
await setupDiscreteApi()

View File

@ -11,5 +11,9 @@
justify-content: center;
}
.content-value {
user-select: text;
}
.tab-content {
}

View File

@ -96,7 +96,6 @@ body {
.value-wrapper {
//border-top: v-bind('themeVars.borderColor') 1px solid;
padding: 5px;
user-select: text;
}
}

View File

@ -0,0 +1,12 @@
import { Environment } from 'wailsjs/runtime/runtime.js'
let os = ''
export async function loadEnvironment() {
const env = await Environment()
os = env.platform
}
export function isMacOS() {
return os === 'darwin'
}

View File

@ -20,6 +20,7 @@ export const themeOverrides = {
tabGapSmallCard: '2px',
tabGapMediumCard: '2px',
tabGapLargeCard: '2px',
tabFontWeightActive: 450,
},
Form: {
labelFontSizeTopSmall: '12px',

10
main.go
View File

@ -41,6 +41,7 @@ func main() {
Height: 768,
MinWidth: 1024,
MinHeight: 768,
Frameless: runtime.GOOS != "darwin",
Menu: appMenu,
AssetServer: &assetserver.Options{
Assets: assets,
@ -59,14 +60,7 @@ func main() {
prefSvc,
},
Mac: &mac.Options{
TitleBar: &mac.TitleBar{
TitlebarAppearsTransparent: false,
HideTitle: false,
HideTitleBar: false,
FullSizeContent: false,
UseToolbar: false,
HideToolbarSeparator: true,
},
TitleBar: mac.TitleBarHiddenInset(),
About: &mac.AboutInfo{
Title: "Tiny RDM " + version,
Message: "A modern lightweight cross-platform Redis desktop client.\n\nCopyright © 2023",