feat(renderer): 添加界面主题颜色功能,添加复制最新字幕选项(#13)

- 新增界面主题颜色功能,支持自定义主题颜色
- 使用 antd 滑块替代原生 input 元素
- 添加复制字幕记录功能,可选择复制最近的字幕记录
This commit is contained in:
himeditator
2025-08-18 16:03:46 +08:00
parent 1c0bf1f9c4
commit 01936d5f12
18 changed files with 148 additions and 51 deletions

2
.gitignore vendored
View File

@@ -10,3 +10,5 @@ test.py
engine/build engine/build
engine/models engine/models
engine/notebook engine/notebook
.repomap
.virtualme

View File

@@ -18,9 +18,13 @@
- [x] 添加字幕记录按时间降序排列选择 *2025/07/26* - [x] 添加字幕记录按时间降序排列选择 *2025/07/26*
- [x] 重构字幕引擎 *2025/07/28* - [x] 重构字幕引擎 *2025/07/28*
- [x] 优化前端界面提示消息 *2025/07/29* - [x] 优化前端界面提示消息 *2025/07/29*
- [x] 复制字幕记录可选择只复制最近的字幕记录 *2025/08/18*
- [x] 添加颜色主题设置 *2025/08/18*
## 待完成 ## 待完成
- [ ] 前端页面添加日志内容展示
- [ ] 调研更多的云端模型火山、OpenAI、Google等
- [ ] 验证 / 添加基于 sherpa-onnx 的字幕引擎 - [ ] 验证 / 添加基于 sherpa-onnx 的字幕引擎
## 后续计划 ## 后续计划

View File

@@ -84,7 +84,7 @@
### `control.uiTheme.change` ### `control.uiTheme.change`
**介绍:** 前端修改界面主题,将修改同步给后端 **介绍:** 前端修改界面主题,将修改同步给后端
**发起方:** 前端控制窗口 **发起方:** 前端控制窗口
@@ -92,6 +92,16 @@
**数据类型:** `UITheme` **数据类型:** `UITheme`
### `control.uiColor.change`
**介绍:** 前端修改界面主题颜色,将修改同步给后端
**发起方:** 前端控制窗口
**接收方:** 后端控制窗口实例
**数据类型:** `string`
### `control.leftBarWidth.change` ### `control.leftBarWidth.change`
**介绍:** 前端修改边栏宽度,将修改同步给后端 **介绍:** 前端修改边栏宽度,将修改同步给后端

View File

@@ -55,7 +55,7 @@ def resample_chunk_mono(chunk: bytes, channels: int, orig_sr: int, target_sr: in
return chunk_mono_r.tobytes() return chunk_mono_r.tobytes()
def resample_chunk_mono_np(chunk: bytes, channels: int, orig_sr: int, target_sr: int, mode="sinc_best") -> np.ndarray: def resample_chunk_mono_np(chunk: bytes, channels: int, orig_sr: int, target_sr: int, mode="sinc_best", dtype=np.float32) -> np.ndarray:
""" """
将当前多通道音频数据块转换成单通道音频数据块,然后进行重采样,返回 Numpy 数组 将当前多通道音频数据块转换成单通道音频数据块,然后进行重采样,返回 Numpy 数组
@@ -65,6 +65,7 @@ def resample_chunk_mono_np(chunk: bytes, channels: int, orig_sr: int, target_sr:
orig_sr: 原始采样率 orig_sr: 原始采样率
target_sr: 目标采样率 target_sr: 目标采样率
mode: 重采样模式,可选:'sinc_best' | 'sinc_medium' | 'sinc_fastest' | 'zero_order_hold' | 'linear' mode: 重采样模式,可选:'sinc_best' | 'sinc_medium' | 'sinc_fastest' | 'zero_order_hold' | 'linear'
dtype: 返回 Numpy 数组的数据类型
Return: Return:
单通道音频数据块 单通道音频数据块
@@ -82,7 +83,7 @@ def resample_chunk_mono_np(chunk: bytes, channels: int, orig_sr: int, target_sr:
ratio = target_sr / orig_sr ratio = target_sr / orig_sr
chunk_mono_r = samplerate.resample(chunk_mono, ratio, converter_type=mode) chunk_mono_r = samplerate.resample(chunk_mono, ratio, converter_type=mode)
chunk_mono_r = np.round(chunk_mono_r).astype(np.int16) chunk_mono_r = chunk_mono_r.astype(dtype)
return chunk_mono_r return chunk_mono_r

View File

@@ -109,6 +109,10 @@ class ControlWindow {
allConfig.uiTheme = args allConfig.uiTheme = args
}) })
ipcMain.on('control.uiColor.change', (_, args) => {
allConfig.uiColor = args
})
ipcMain.on('control.leftBarWidth.change', (_, args) => { ipcMain.on('control.leftBarWidth.change', (_, args) => {
allConfig.leftBarWidth = args allConfig.leftBarWidth = args
}) })

View File

@@ -49,6 +49,7 @@ export interface FullConfig {
platform: string, platform: string,
uiLanguage: UILanguage, uiLanguage: UILanguage,
uiTheme: UITheme, uiTheme: UITheme,
uiColor: string,
leftBarWidth: number, leftBarWidth: number,
styles: Styles, styles: Styles,
controls: Controls, controls: Controls,

View File

@@ -49,6 +49,7 @@ class AllConfig {
uiLanguage: UILanguage = 'zh'; uiLanguage: UILanguage = 'zh';
leftBarWidth: number = 8; leftBarWidth: number = 8;
uiTheme: UITheme = 'system'; uiTheme: UITheme = 'system';
uiColor: string = '#1677ff';
styles: Styles = {...defaultStyles}; styles: Styles = {...defaultStyles};
controls: Controls = {...defaultControls}; controls: Controls = {...defaultControls};
@@ -64,6 +65,7 @@ class AllConfig {
if(config.captionWindowWidth) this.captionWindowWidth = config.captionWindowWidth if(config.captionWindowWidth) this.captionWindowWidth = config.captionWindowWidth
if(config.uiLanguage) this.uiLanguage = config.uiLanguage if(config.uiLanguage) this.uiLanguage = config.uiLanguage
if(config.uiTheme) this.uiTheme = config.uiTheme if(config.uiTheme) this.uiTheme = config.uiTheme
if(config.uiColor) this.uiColor = config.uiColor
if(config.leftBarWidth) this.leftBarWidth = config.leftBarWidth if(config.leftBarWidth) this.leftBarWidth = config.leftBarWidth
if(config.styles) this.setStyles(config.styles) if(config.styles) this.setStyles(config.styles)
if(config.controls) this.setControls(config.controls) if(config.controls) this.setControls(config.controls)
@@ -76,6 +78,7 @@ class AllConfig {
captionWindowWidth: this.captionWindowWidth, captionWindowWidth: this.captionWindowWidth,
uiLanguage: this.uiLanguage, uiLanguage: this.uiLanguage,
uiTheme: this.uiTheme, uiTheme: this.uiTheme,
uiColor: this.uiColor,
leftBarWidth: this.leftBarWidth, leftBarWidth: this.leftBarWidth,
controls: this.controls, controls: this.controls,
styles: this.styles styles: this.styles
@@ -90,6 +93,7 @@ class AllConfig {
platform: process.platform, platform: process.platform,
uiLanguage: this.uiLanguage, uiLanguage: this.uiLanguage,
uiTheme: this.uiTheme, uiTheme: this.uiTheme,
uiColor: this.uiColor,
leftBarWidth: this.leftBarWidth, leftBarWidth: this.leftBarWidth,
styles: this.styles, styles: this.styles,
controls: this.controls, controls: this.controls,
@@ -123,7 +127,9 @@ class AllConfig {
} }
} }
this.controls.engineEnabled = engineEnabled this.controls.engineEnabled = engineEnabled
Log.info('Set Controls:', this.controls) let _controls = {...this.controls}
_controls.API_KEY = _controls.API_KEY.replace(/./g, '*')
Log.info('Set Controls:', _controls)
} }
public sendControls(window: BrowserWindow, info = true) { public sendControls(window: BrowserWindow, info = true) {

View File

@@ -14,6 +14,7 @@ onMounted(() => {
window.electron.ipcRenderer.invoke('both.window.mounted').then((data: FullConfig) => { window.electron.ipcRenderer.invoke('both.window.mounted').then((data: FullConfig) => {
useGeneralSettingStore().uiLanguage = data.uiLanguage useGeneralSettingStore().uiLanguage = data.uiLanguage
useGeneralSettingStore().uiTheme = data.uiTheme useGeneralSettingStore().uiTheme = data.uiTheme
useGeneralSettingStore().uiColor = data.uiColor
useGeneralSettingStore().leftBarWidth = data.leftBarWidth useGeneralSettingStore().leftBarWidth = data.leftBarWidth
useCaptionStyleStore().setStyles(data.styles) useCaptionStyleStore().setStyles(data.styles)
useEngineControlStore().platform = data.platform useEngineControlStore().platform = data.platform

View File

@@ -17,6 +17,7 @@
} }
.input-area { .input-area {
display: inline-block;
width: calc(100% - 100px); width: calc(100% - 100px);
min-width: 100px; min-width: 100px;
} }

View File

@@ -55,8 +55,8 @@
<div class="input-item"> <div class="input-item">
<span class="input-label">{{ $t('log.exportFormat') }}</span> <span class="input-label">{{ $t('log.exportFormat') }}</span>
<a-radio-group v-model:value="exportFormat"> <a-radio-group v-model:value="exportFormat">
<a-radio-button value="srt">.srt</a-radio-button> <a-radio-button value="srt"><code>.srt</code></a-radio-button>
<a-radio-button value="json">.json</a-radio-button> <a-radio-button value="json"><code>.json</code></a-radio-button>
</a-radio-group> </a-radio-group>
</div> </div>
<div class="input-item"> <div class="input-item">
@@ -90,6 +90,15 @@
<a-radio-button value="target">{{ $t('log.translation') }}</a-radio-button> <a-radio-button value="target">{{ $t('log.translation') }}</a-radio-button>
</a-radio-group> </a-radio-group>
</div> </div>
<div class="input-item">
<span class="input-label">{{ $t('log.copyNum') }}</span>
<a-radio-group v-model:value="copyNum">
<a-radio-button :value="0"><code>[:]</code></a-radio-button>
<a-radio-button :value="1"><code>[-1:]</code></a-radio-button>
<a-radio-button :value="2"><code>[-2:]</code></a-radio-button>
<a-radio-button :value="3"><code>[-3:]</code></a-radio-button>
</a-radio-group>
</div>
</template> </template>
<a-button <a-button
style="margin-right: 20px;" style="margin-right: 20px;"
@@ -147,6 +156,7 @@ const exportFormat = ref('srt')
const showIndex = ref(true) const showIndex = ref(true)
const copyTime = ref(true) const copyTime = ref(true)
const contentOption = ref('both') const contentOption = ref('both')
const copyNum = ref(0)
const baseHH = ref<number>(0) const baseHH = ref<number>(0)
const baseMM = ref<number>(0) const baseMM = ref<number>(0)
@@ -255,7 +265,12 @@ function getExportData() {
function copyCaptions() { function copyCaptions() {
let content = '' let content = ''
for(let i = 0; i < captionData.value.length; i++){ let start = 0
if(copyNum.value > 0) {
start = captionData.value.length - copyNum.value
if(start < 0) start = 0
}
for(let i = start; i < captionData.value.length; i++){
const item = captionData.value[i] const item = captionData.value[i]
if(showIndex.value) content += `${i+1}\n` if(showIndex.value) content += `${i+1}\n`
if(copyTime.value) content += `${item.time_s} --> ${item.time_t}\n`.replace(/\./g, ',') if(copyTime.value) content += `${item.time_s} --> ${item.time_t}\n`.replace(/\./g, ',')

View File

@@ -34,20 +34,18 @@
</div> </div>
<div class="input-item"> <div class="input-item">
<span class="input-label">{{ $t('style.fontSize') }}</span> <span class="input-label">{{ $t('style.fontSize') }}</span>
<a-input <a-slider
class="input-area" class="input-area"
type="range" :min="0" :max="72"
min="0" max="72"
v-model:value="currentFontSize" v-model:value="currentFontSize"
/> />
<div class="input-item-value">{{ currentFontSize }}px</div> <div class="input-item-value">{{ currentFontSize }}px</div>
</div> </div>
<div class="input-item"> <div class="input-item">
<span class="input-label">{{ $t('style.fontWeight') }}</span> <span class="input-label">{{ $t('style.fontWeight') }}</span>
<a-input <a-slider
class="input-area" class="input-area"
type="range" :min="1" :max="9"
min="1" max="9"
v-model:value="currentFontWeight" v-model:value="currentFontWeight"
/> />
<div class="input-item-value">{{ currentFontWeight*100 }}</div> <div class="input-item-value">{{ currentFontWeight*100 }}</div>
@@ -63,11 +61,10 @@
</div> </div>
<div class="input-item"> <div class="input-item">
<span class="input-label">{{ $t('style.opacity') }}</span> <span class="input-label">{{ $t('style.opacity') }}</span>
<a-input <a-slider
class="input-area" class="input-area"
type="range" :min="0"
min="0" :max="100"
max="100"
v-model:value="currentOpacity" v-model:value="currentOpacity"
/> />
<div class="input-item-value">{{ currentOpacity }}%</div> <div class="input-item-value">{{ currentOpacity }}%</div>
@@ -111,20 +108,18 @@
</div> </div>
<div class="input-item"> <div class="input-item">
<span class="input-label">{{ $t('style.fontSize') }}</span> <span class="input-label">{{ $t('style.fontSize') }}</span>
<a-input <a-slider
class="input-area" class="input-area"
type="range" :min="0" :max="72"
min="0" max="72"
v-model:value="currentTransFontSize" v-model:value="currentTransFontSize"
/> />
<div class="input-item-value">{{ currentTransFontSize }}px</div> <div class="input-item-value">{{ currentTransFontSize }}px</div>
</div> </div>
<div class="input-item"> <div class="input-item">
<span class="input-label">{{ $t('style.fontWeight') }}</span> <span class="input-label">{{ $t('style.fontWeight') }}</span>
<a-input <a-slider
class="input-area" class="input-area"
type="range" :min="1" :max="9"
min="1" max="9"
v-model:value="currentTransFontWeight" v-model:value="currentTransFontWeight"
/> />
<div class="input-item-value">{{ currentTransFontWeight*100 }}</div> <div class="input-item-value">{{ currentTransFontWeight*100 }}</div>
@@ -136,30 +131,27 @@
<a-card size="small" :title="$t('style.shadow.title')"> <a-card size="small" :title="$t('style.shadow.title')">
<div class="input-item"> <div class="input-item">
<span class="input-label">{{ $t('style.shadow.offsetX') }}</span> <span class="input-label">{{ $t('style.shadow.offsetX') }}</span>
<a-input <a-slider
class="input-area" class="input-area"
type="range" :min="-10" :max="10"
min="-10" max="10"
v-model:value="currentOffsetX" v-model:value="currentOffsetX"
/> />
<div class="input-item-value">{{ currentOffsetX }}px</div> <div class="input-item-value">{{ currentOffsetX }}px</div>
</div> </div>
<div class="input-item"> <div class="input-item">
<span class="input-label">{{ $t('style.shadow.offsetY') }}</span> <span class="input-label">{{ $t('style.shadow.offsetY') }}</span>
<a-input <a-slider
class="input-area" class="input-area"
type="range" :min="-10" :max="10"
min="-10" max="10"
v-model:value="currentOffsetY" v-model:value="currentOffsetY"
/> />
<div class="input-item-value">{{ currentOffsetY }}px</div> <div class="input-item-value">{{ currentOffsetY }}px</div>
</div> </div>
<div class="input-item"> <div class="input-item">
<span class="input-label">{{ $t('style.shadow.blur') }}</span> <span class="input-label">{{ $t('style.shadow.blur') }}</span>
<a-input <a-slider
class="input-area" class="input-area"
type="range" :min="0" :max="12"
min="0" max="12"
v-model:value="currentBlur" v-model:value="currentBlur"
/> />
<div class="input-item-value">{{ currentBlur }}px</div> <div class="input-item-value">{{ currentBlur }}px</div>
@@ -315,7 +307,7 @@ function resetStyle() {
} }
watch(changeSignal, (val) => { watch(changeSignal, (val) => {
if(val == true) { if(val === true) {
backStyle(); backStyle();
captionStyle.changeSignal = false; captionStyle.changeSignal = false;
} }

View File

@@ -28,11 +28,23 @@
</a-radio-group> </a-radio-group>
</div> </div>
<div class="input-item">
<span class="input-label">{{ $t('general.color') }}</span>
<a-radio-group v-model:value="uiColor">
<template v-for="color in colorList" :key="color">
<a-radio-button :value="color"
:style="{
backgroundColor: color
}"
>&nbsp;</a-radio-button>
</template>
</a-radio-group>
</div>
<div class="input-item"> <div class="input-item">
<span class="input-label">{{ $t('general.barWidth') }}</span> <span class="input-label">{{ $t('general.barWidth') }}</span>
<a-input <a-slider class="span-input"
type="range" class="span-input" :min="6" :max="12" v-model:value="leftBarWidth"
min="6" max="12" v-model:value="leftBarWidth"
/> />
<div class="input-item-value">{{ (leftBarWidth * 100 / 24).toFixed(0) }}%</div> <div class="input-item-value">{{ (leftBarWidth * 100 / 24).toFixed(0) }}%</div>
</div> </div>
@@ -45,15 +57,26 @@ import { storeToRefs } from 'pinia'
import { useGeneralSettingStore } from '@renderer/stores/generalSetting' import { useGeneralSettingStore } from '@renderer/stores/generalSetting'
import { InfoCircleOutlined } from '@ant-design/icons-vue'; import { InfoCircleOutlined } from '@ant-design/icons-vue';
const colorList = [
'#1677ff',
'#00b96b',
'#fa8c16',
'#722ed1',
'#eb2f96',
'#000000'
]
const generalSettingStore = useGeneralSettingStore() const generalSettingStore = useGeneralSettingStore()
const { uiLanguage, uiTheme, leftBarWidth } = storeToRefs(generalSettingStore) const { uiLanguage, uiTheme, uiColor, leftBarWidth } = storeToRefs(generalSettingStore)
</script> </script>
<style scoped> <style scoped>
@import url(../assets/input.css); @import url(../assets/input.css);
.span-input { .span-input {
display: inline-block;
width: 100px; width: 100px;
margin: 0;
} }
.general-note { .general-note {

View File

@@ -1,10 +1,29 @@
import { theme } from 'ant-design-vue'; import { theme } from 'ant-design-vue';
export const antDesignTheme = { let isLight = true
light: { let themeColor = '#1677ff'
token: {}
}, export function setThemeColor(color: string) {
dark: { themeColor = color
algorithm: theme.darkAlgorithm,
}
} }
export function getTheme(curIsLight?: boolean) {
const lightTheme = {
token: {
colorPrimary: themeColor,
colorInfo: themeColor
}
}
const darkTheme = {
algorithm: theme.darkAlgorithm,
token: {
colorPrimary: themeColor,
colorInfo: themeColor
}
}
if(curIsLight !== undefined){
isLight = curIsLight
}
return isLight ? lightTheme : darkTheme
}

View File

@@ -37,7 +37,8 @@ export default {
"theme": "Theme", "theme": "Theme",
"light": "light", "light": "light",
"dark": "dark", "dark": "dark",
"system": "system" "system": "system",
"color": "Color"
}, },
engine: { engine: {
"title": "Caption Engine Settings", "title": "Caption Engine Settings",
@@ -142,6 +143,8 @@ export default {
"both": "Both", "both": "Both",
"source": "Original", "source": "Original",
"translation": "Translation", "translation": "Translation",
"copyNum": "Copy Count",
"all": "All",
"copySuccess": "Subtitle copied to clipboard", "copySuccess": "Subtitle copied to clipboard",
"clear": "Clear Log" "clear": "Clear Log"
} }

View File

@@ -37,7 +37,8 @@ export default {
"theme": "テーマ", "theme": "テーマ",
"light": "明るい", "light": "明るい",
"dark": "暗い", "dark": "暗い",
"system": "システム" "system": "システム",
"color": "カラー"
}, },
engine: { engine: {
"title": "字幕エンジン設定", "title": "字幕エンジン設定",
@@ -142,6 +143,8 @@ export default {
"both": "すべて", "both": "すべて",
"source": "原文", "source": "原文",
"translation": "翻訳", "translation": "翻訳",
"copyNum": "コピー数",
"all": "すべて",
"copySuccess": "字幕がクリップボードにコピーされました", "copySuccess": "字幕がクリップボードにコピーされました",
"clear": "ログをクリア" "clear": "ログをクリア"
} }

View File

@@ -37,7 +37,8 @@ export default {
"theme": "主题", "theme": "主题",
"light": "浅色", "light": "浅色",
"dark": "深色", "dark": "深色",
"system": "系统" "system": "系统",
"color": "颜色"
}, },
engine: { engine: {
"title": "字幕引擎设置", "title": "字幕引擎设置",
@@ -142,6 +143,8 @@ export default {
"both": "全部", "both": "全部",
"source": "原文", "source": "原文",
"translation": "翻译", "translation": "翻译",
"copyNum": "复制数量",
"all": "全部",
"copySuccess": "字幕已复制到剪贴板", "copySuccess": "字幕已复制到剪贴板",
"clear": "清空记录" "clear": "清空记录"
} }

View File

@@ -3,16 +3,17 @@ import { defineStore } from 'pinia'
import { i18n } from '../i18n' import { i18n } from '../i18n'
import type { UILanguage, UITheme } from '../types' import type { UILanguage, UITheme } from '../types'
import { engines, audioTypes, antDesignTheme, breakOptions } from '../i18n' import { engines, audioTypes, breakOptions, setThemeColor, getTheme } from '../i18n'
import { useEngineControlStore } from './engineControl' import { useEngineControlStore } from './engineControl'
import { useCaptionStyleStore } from './captionStyle' import { useCaptionStyleStore } from './captionStyle'
export const useGeneralSettingStore = defineStore('generalSetting', () => { export const useGeneralSettingStore = defineStore('generalSetting', () => {
const uiLanguage = ref<UILanguage>('zh') const uiLanguage = ref<UILanguage>('zh')
const uiTheme = ref<UITheme>('system') const uiTheme = ref<UITheme>('system')
const uiColor = ref<string>('#1677ff')
const leftBarWidth = ref<number>(8) const leftBarWidth = ref<number>(8)
const antdTheme = ref<Object>(antDesignTheme['light']) const antdTheme = ref<Object>(getTheme())
window.electron.ipcRenderer.invoke('control.nativeTheme.get').then((theme) => { window.electron.ipcRenderer.invoke('control.nativeTheme.get').then((theme) => {
if(theme === 'light') setLightTheme() if(theme === 'light') setLightTheme()
@@ -39,6 +40,12 @@ export const useGeneralSettingStore = defineStore('generalSetting', () => {
else if(newValue === 'dark') setDarkTheme() else if(newValue === 'dark') setDarkTheme()
}) })
watch(uiColor, (newValue) => {
setThemeColor(newValue)
antdTheme.value = getTheme()
window.electron.ipcRenderer.send('control.uiColor.change', newValue)
})
watch(leftBarWidth, (newValue) => { watch(leftBarWidth, (newValue) => {
window.electron.ipcRenderer.send('control.leftBarWidth.change', newValue) window.electron.ipcRenderer.send('control.leftBarWidth.change', newValue)
}) })
@@ -53,7 +60,7 @@ export const useGeneralSettingStore = defineStore('generalSetting', () => {
}) })
function setLightTheme(){ function setLightTheme(){
antdTheme.value = antDesignTheme.light antdTheme.value = getTheme(true)
const root = document.documentElement const root = document.documentElement
root.style.setProperty('--control-background', '#fff') root.style.setProperty('--control-background', '#fff')
root.style.setProperty('--tag-color', 'rgba(0, 0, 0, 0.45)') root.style.setProperty('--tag-color', 'rgba(0, 0, 0, 0.45)')
@@ -61,7 +68,7 @@ export const useGeneralSettingStore = defineStore('generalSetting', () => {
} }
function setDarkTheme(){ function setDarkTheme(){
antdTheme.value = antDesignTheme.dark antdTheme.value = getTheme(false)
const root = document.documentElement const root = document.documentElement
root.style.setProperty('--control-background', '#000') root.style.setProperty('--control-background', '#000')
root.style.setProperty('--tag-color', 'rgba(255, 255, 255, 0.45)') root.style.setProperty('--tag-color', 'rgba(255, 255, 255, 0.45)')
@@ -71,6 +78,7 @@ export const useGeneralSettingStore = defineStore('generalSetting', () => {
return { return {
uiLanguage, uiLanguage,
uiTheme, uiTheme,
uiColor,
leftBarWidth, leftBarWidth,
antdTheme antdTheme
} }

View File

@@ -49,6 +49,7 @@ export interface FullConfig {
platform: string, platform: string,
uiLanguage: UILanguage, uiLanguage: UILanguage,
uiTheme: UITheme, uiTheme: UITheme,
uiColor: string,
leftBarWidth: number, leftBarWidth: number,
styles: Styles, styles: Styles,
controls: Controls, controls: Controls,