feat(renderer): 实现多窗口创建,实现多窗口通信

This commit is contained in:
himeditator
2025-06-16 23:24:28 +08:00
parent eb3711f6af
commit fbe3fcffdb
12 changed files with 325 additions and 95 deletions

66
src/main/caption.ts Normal file
View File

@@ -0,0 +1,66 @@
import { shell, BrowserWindow, ipcMain } from 'electron'
import path from 'path'
import { is } from '@electron-toolkit/utils'
import icon from '../../resources/icon.png?asset'
import { controlWindow } from './control'
import { sendStyles } from './data'
class CaptionWindow {
window: BrowserWindow | undefined;
public createWindow(): void {
this.window = new BrowserWindow({
icon: icon,
width: 900,
height: 320,
show: false,
// center: true,
autoHideMenuBar: true,
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
sandbox: false
}
})
setTimeout(() => {
if (this.window) {
sendStyles(this.window);
}
}, 1000);
this.window.on('ready-to-show', () => {
this.window?.show()
})
this.window.on('closed', () => {
console.log('INFO caption window closed')
this.window = undefined
})
this.window.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
this.window.loadURL(`${process.env['ELECTRON_RENDERER_URL']}/#/caption`)
} else {
this.window.loadFile(path.join(__dirname, '../renderer/index.html'), {
hash: 'caption'
})
}
}
public handleMessage() {
// 字幕窗口请求创建控制窗口
ipcMain.on('caption.controlWindow.create', () => {
if(!controlWindow.window){
controlWindow.createWindow()
console.log('caption.controlWindow.create')
}
})
}
}
export const captionWindow = new CaptionWindow()

73
src/main/control.ts Normal file
View File

@@ -0,0 +1,73 @@
import { shell, BrowserWindow, ipcMain } from 'electron'
import path from 'path'
import { is } from '@electron-toolkit/utils'
import icon from '../../resources/icon.png?asset'
import { setStyles, sendStyles } from './data'
import { captionWindow } from './caption'
class ControlWindow {
window: BrowserWindow | undefined;
public createWindow(): void {
this.window = new BrowserWindow({
icon: icon,
width: 900,
height: 670,
show: false,
center: true,
autoHideMenuBar: true,
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
sandbox: false
}
})
setTimeout(() => {
if (this.window) {
sendStyles(this.window);
}
}, 1000);
this.window.on('ready-to-show', () => {
this.window?.show()
})
this.window.on('closed', () => {
console.log('INFO control window closed')
this.window = undefined
})
this.window.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
this.window.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else {
this.window.loadFile(path.join(__dirname, '../renderer/index.html'))
}
}
public handleMessage() {
// 控制窗口样式更新
ipcMain.on('control.style.change', (_, args) => {
console.log('GET control.style.change', args)
setStyles(args)
if(captionWindow.window){
sendStyles(captionWindow.window)
}
})
// 控制窗口请求创建字幕窗口
ipcMain.on('control.captionWindow.create', () => {
if(!captionWindow.window){
captionWindow.createWindow()
console.log('GET control.captionWindow.create')
}
})
}
}
export const controlWindow = new ControlWindow()

42
src/main/data.ts Normal file
View File

@@ -0,0 +1,42 @@
import { BrowserWindow } from 'electron'
export interface Styles {
fontFamily: string,
fontSize: number,
fontColor: string,
background: string,
opacity: number,
transDisplay: boolean,
transFontFamily: string,
transFontSize: number,
transFontColor: string
}
export let styles: Styles = {
fontFamily: 'sans-serif',
fontSize: 24,
fontColor: '#000000',
background: '#dbe2ef',
opacity: 50,
transDisplay: true,
transFontFamily: 'sans-serif',
transFontSize: 24,
transFontColor: '#000000'
}
export function setStyles(args: any) {
styles.fontFamily = args.fontFamily
styles.fontSize = args.fontSize
styles.fontColor = args.fontColor
styles.background = args.background
styles.opacity = args.opacity
styles.transDisplay = args.transDisplay
styles.transFontFamily = args.transFontFamily
styles.transFontSize = args.transFontSize
styles.transFontColor = args.transFontColor
}
export function sendStyles(window: BrowserWindow) {
window.webContents.send('caption.style.set', styles)
console.log('SNED caption.style.set')
}

View File

@@ -1,73 +1,7 @@
import { app, shell, BrowserWindow } from 'electron'
import path from 'path'
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import icon from '../../resources/icon.png?asset'
let mainWindow: BrowserWindow | undefined
let captionWindow: BrowserWindow | undefined
function createMainWindow(): void {
mainWindow = new BrowserWindow({
icon: icon,
width: 900,
height: 670,
show: false,
center: true,
autoHideMenuBar: true,
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
sandbox: false
}
})
mainWindow.on('ready-to-show', () => {
mainWindow?.show()
})
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else {
mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'))
}
}
function createCaptionWindow(): void {
captionWindow = new BrowserWindow({
icon: icon,
width: 900,
height: 670,
show: false,
center: true,
autoHideMenuBar: true,
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
sandbox: false
}
})
captionWindow.on('ready-to-show', () => {
captionWindow?.show()
})
captionWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
captionWindow.loadURL(`${process.env['ELECTRON_RENDERER_URL']}/#/caption`)
} else {
captionWindow.loadFile(path.join(__dirname, '../renderer/index.html'), {
hash: 'caption'
})
}
}
import { app, BrowserWindow } from 'electron'
import { electronApp, optimizer } from '@electron-toolkit/utils'
import { controlWindow } from './control'
import { captionWindow } from './caption'
app.whenReady().then(() => {
electronApp.setAppUserModelId('com.himeditator.autocaption')
@@ -76,11 +10,15 @@ app.whenReady().then(() => {
optimizer.watchWindowShortcuts(window)
})
createMainWindow()
createCaptionWindow()
controlWindow.handleMessage()
captionWindow.handleMessage()
controlWindow.createWindow()
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createMainWindow()
if (BrowserWindow.getAllWindows().length === 0){
controlWindow.createWindow()
}
})
})

View File

@@ -14,7 +14,7 @@
</div>
<div class="caption-control">
<a-button type="primary" class="control-button">打开字幕窗口</a-button>
<a-button type="primary" class="control-button" @click="openCaptionWindow">打开字幕窗口</a-button>
<a-button class="control-button">启动字幕引擎</a-button>
<a-button danger class="control-button">关闭字幕引擎</a-button>
</div>
@@ -49,21 +49,10 @@
<script setup lang="ts">
import { ref } from 'vue'
const captionData = ref([
{index: 1, time_s: "00:00:00", time_t: "00:00:00", text: "Long time no see.", translation: "好久不见"},
{index: 2, time_s: "00:00:00", time_t: "00:00:00", text: "How have you been?", translation: "你最近怎么样?"},
{index: 3, time_s: "00:00:00", time_t: "00:00:00", text: "I've missed you a lot.", translation: "我非常想念你。"},
{index: 4, time_s: "00:00:00", time_t: "00:00:00", text: "It's good to see you again.", translation: "很高兴再次见到你。"},
{index: 5, time_s: "00:00:00", time_t: "00:00:00", text: "What have you been up to?", translation: "你最近在忙什么?"},
{index: 6, time_s: "00:00:00", time_t: "00:00:00", text: "Let's catch up over coffee.", translation: "我们去喝杯咖啡聊聊天吧。"},
{index: 7, time_s: "00:00:00", time_t: "00:00:00", text: "You look great!", translation: "你看起来很棒!"},
{index: 8, time_s: "00:00:00", time_t: "00:00:00", text: "I can't believe it's been so long.", translation: "真不敢相信已经这么久了。"},
{index: 9, time_s: "00:00:00", time_t: "00:00:00", text: "We should do this more often.", translation: "我们应该多聚聚。"},
{index: 10, time_s: "00:00:00", time_t: "00:00:00", text: "Thanks for coming to see me.", translation: "谢谢你来看我。"},
{index: 11, time_s: "00:00:00", time_t: "00:00:00", text: "We show case the utility of Macformer when combined with molecular docking simulations and wet lab based experimental validation, by applying it to the prospective design of macrocyclic JAK2 inhibitors.", translation: "我们通过将其应用于大环JAK2抑制剂的前瞻性设计展示了Macformer与分子对接模拟和湿实验验证相结合的实用性。"},
{index: 12, time_s: "00:00:00", time_t: "00:00:00", text: "Macrocycles, typically defined as cyclic small molecules or peptides with ring structures consisting of 12 or more atoms, has emerged as promising chemical scaffolds in the field of new drug discovery1,2. The distinct physicochemical properties, including high molecular weight and abundant hydrogen bond donors3, render this structural class occupy a chemical space beyond Lipinski's rule of five4.", translation: "大环分子通常定义为具有由 12 个或更多原子组成的环状结构的环状小分子或肽,已成为新药发现领域中具有前景的化学骨架 [1,2]。其独特的理化性质(包括高分子量和丰富的氢键供体)[3],使这类结构占据了超越 Lipinski 五规则 [4] 的化学空间。"}
])
import { storeToRefs } from 'pinia'
import { useCaptionLogStore } from '@renderer/stores/captionLog'
const captionLog = useCaptionLogStore()
const { captionData } = storeToRefs(captionLog)
const pagination = ref({
current: 1,
@@ -100,6 +89,11 @@ const columns = [
key: 'content',
},
]
function openCaptionWindow() {
window.electron.ipcRenderer.send('control.captionWindow.create')
}
</script>
<style scoped>

View File

@@ -123,9 +123,12 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ref, watch } from 'vue'
import { useCaptionStyleStore } from '@renderer/stores/captionStyle'
import { storeToRefs } from 'pinia'
const captionStyle = useCaptionStyleStore()
const { changeSignal } = storeToRefs(captionStyle)
const currentFontFamily = ref<string>('sans-serif')
const currentFontSize = ref<number>(24)
@@ -161,6 +164,8 @@ function applyStyle(){
captionStyle.transFontFamily = currentTransFontFamily.value;
captionStyle.transFontSize = currentTransFontSize.value;
captionStyle.transFontColor = currentTransFontColor.value;
captionStyle.sendStyleChange();
}
function resetStyle(){
@@ -175,6 +180,13 @@ function resetStyle(){
currentTransFontSize.value = captionStyle.transFontSize;
currentTransFontColor.value = captionStyle.transFontColor;
}
watch(changeSignal, (val) => {
if(val == true) {
resetStyle();
captionStyle.changeSignal = false;
}
})
</script>
<style scoped>

View File

@@ -1,5 +1,5 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import HomePage from '../views/HomePage.vue'
import ControlPage from '../views/ControlPage.vue'
import CaptionPage from '@renderer/views/CaptionPage.vue'
const router = createRouter({
@@ -8,7 +8,7 @@ const router = createRouter({
{
path: '/',
name: 'home',
component: HomePage
component: ControlPage
},
{
path: '/caption',

View File

@@ -30,6 +30,7 @@ export const useCaptionControlStore = defineStore('captionControl', () => {
const engine = ref<string>('gummy')
const port = ref<number>(8765)
const translation = ref<boolean>(false)
return {
captionEngine, // 字幕引擎
sourceLang, // 源语言

View File

@@ -0,0 +1,23 @@
import { ref } from 'vue'
import { defineStore } from 'pinia'
export const useCaptionLogStore = defineStore('captionLog', () => {
const captionData = ref([
{index: 1, time_s: "00:00:00", time_t: "00:00:00", text: "Long time no see.", translation: "好久不见"},
{index: 2, time_s: "00:00:00", time_t: "00:00:00", text: "How have you been?", translation: "你最近怎么样?"},
{index: 3, time_s: "00:00:00", time_t: "00:00:00", text: "I've missed you a lot.", translation: "我非常想念你。"},
{index: 4, time_s: "00:00:00", time_t: "00:00:00", text: "It's good to see you again.", translation: "很高兴再次见到你。"},
{index: 5, time_s: "00:00:00", time_t: "00:00:00", text: "What have you been up to?", translation: "你最近在忙什么?"},
{index: 6, time_s: "00:00:00", time_t: "00:00:00", text: "Let's catch up over coffee.", translation: "我们去喝杯咖啡聊聊天吧。"},
{index: 7, time_s: "00:00:00", time_t: "00:00:00", text: "You look great!", translation: "你看起来很棒!"},
{index: 8, time_s: "00:00:00", time_t: "00:00:00", text: "I can't believe it's been so long.", translation: "真不敢相信已经这么久了。"},
{index: 9, time_s: "00:00:00", time_t: "00:00:00", text: "We should do this more often.", translation: "我们应该多聚聚。"},
{index: 10, time_s: "00:00:00", time_t: "00:00:00", text: "Thanks for coming to see me.", translation: "谢谢你来看我。"},
{index: 11, time_s: "00:00:00", time_t: "00:00:00", text: "We show case the utility of Macformer when combined with molecular docking simulations and wet lab based experimental validation, by applying it to the prospective design of macrocyclic JAK2 inhibitors.", translation: "我们通过将其应用于大环JAK2抑制剂的前瞻性设计展示了Macformer与分子对接模拟和湿实验验证相结合的实用性。"},
{index: 12, time_s: "00:00:00", time_t: "00:00:00", text: "Macrocycles, typically defined as cyclic small molecules or peptides with ring structures consisting of 12 or more atoms, has emerged as promising chemical scaffolds in the field of new drug discovery1,2. The distinct physicochemical properties, including high molecular weight and abundant hydrogen bond donors3, render this structural class occupy a chemical space beyond Lipinski's rule of five4.", translation: "大环分子通常定义为具有由 12 个或更多原子组成的环状结构的环状小分子或肽,已成为新药发现领域中具有前景的化学骨架 [1,2]。其独特的理化性质(包括高分子量和丰富的氢键供体)[3],使这类结构占据了超越 Lipinski 五规则 [4] 的化学空间。"}
])
return {
captionData
}
})

View File

@@ -1,4 +1,4 @@
import { ref } from 'vue'
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCaptionStyleStore = defineStore('captionStyle', () => {
@@ -11,6 +11,48 @@ export const useCaptionStyleStore = defineStore('captionStyle', () => {
const transFontFamily = ref<string>('sans-serif')
const transFontSize = ref<number>(24)
const transFontColor = ref<string>('#000000')
const changeSignal = ref<boolean>(false)
function addOpicityToColor(color: string, opicity: number) {
const opicityValue = Math.round(opicity * 255 / 100);
const opicityHex = opicityValue.toString(16).padStart(2, '0');
return `${color}${opicityHex}`;
}
const backgroundRGBA = computed(() => {
return addOpicityToColor(background.value, opacity.value)
})
function sendStyleChange() {
const styles = {
fontFamily: fontFamily.value,
fontSize: fontSize.value,
fontColor: fontColor.value,
background: background.value,
opacity: opacity.value,
transDisplay: transDisplay.value,
transFontFamily: transFontFamily.value,
transFontSize: transFontSize.value,
transFontColor: transFontColor.value
}
window.electron.ipcRenderer.send('control.style.change', styles)
console.log('SEND control.style.change', styles)
}
window.electron.ipcRenderer.on('caption.style.set', async (_, args) => {
fontFamily.value = args.fontFamily
fontSize.value = args.fontSize
fontColor.value = args.fontColor
background.value = args.background
opacity.value = args.opacity
transDisplay.value = args.transDisplay
transFontFamily.value = args.transFontFamily
transFontSize.value = args.transFontSize
transFontColor.value = args.transFontColor
changeSignal.value = true
console.log('GET caption.style.set', args)
})
return {
fontFamily, // 字体族
fontSize, // 字体大小
@@ -20,6 +62,9 @@ export const useCaptionStyleStore = defineStore('captionStyle', () => {
transDisplay, // 是否显示翻译
transFontFamily, // 翻译字体族
transFontSize, // 翻译字体大小
transFontColor // 翻译字体颜色
transFontColor, // 翻译字体颜色
backgroundRGBA, // 带透明度的背景颜色
sendStyleChange, // 发送样式改变
changeSignal // 样式改变信号
}
})

View File

@@ -1,11 +1,47 @@
<template>
<h1>字幕测试 字幕测试 字幕测试</h1>
<a-button @click="test">打开控制窗口</a-button>
<div class="preview-container" :style="{
backgroundColor: captionStyle.backgroundRGBA
}">
<p class="preview-caption" :style="{
fontFamily: captionStyle.fontFamily,
fontSize: captionStyle.fontSize + 'px',
color: captionStyle.fontColor
}">
{{ "This is a preview of subtitle styles." }}
</p>
<p class="preview-translation" v-if="captionStyle.transDisplay" :style="{
fontFamily: captionStyle.transFontFamily,
fontSize: captionStyle.transFontSize + 'px',
color: captionStyle.transFontColor
}">这是字幕样式预览(翻译)</p>
</div>
</template>
<script setup lang="ts">
import { useCaptionStyleStore } from '@renderer/stores/captionStyle';
const captionStyle = useCaptionStyleStore();
function test() {
window.electron.ipcRenderer.send('caption.controlWindow.create')
}
</script>
<style scoped>
.preview-container {
line-height: 2em;
width: 60%;
text-align: center;
position: absolute;
padding: 20px;
border-radius: 10px;
left: 50%;
transform: translateX(-50%);
bottom: 20px;
}
.preview-container p {
margin: 0;
line-height: 1.5em;
}
</style>