ui.go
1 package desktop 2 3 import ( 4 "fmt" 5 "image/color" 6 "krillin-ai/config" 7 "krillin-ai/internal/deps" 8 "krillin-ai/internal/server" 9 "krillin-ai/internal/types" 10 "krillin-ai/log" 11 "krillin-ai/static" 12 "net/url" 13 "path/filepath" 14 "strconv" 15 "time" 16 17 "fyne.io/fyne/v2" 18 "fyne.io/fyne/v2/canvas" 19 "fyne.io/fyne/v2/container" 20 "fyne.io/fyne/v2/data/binding" 21 "fyne.io/fyne/v2/dialog" 22 "fyne.io/fyne/v2/layout" 23 "fyne.io/fyne/v2/theme" 24 "fyne.io/fyne/v2/widget" 25 "go.uber.org/zap" 26 ) 27 28 // 创建配置界面 29 func CreateConfigTab(window fyne.Window) fyne.CanvasObject { 30 pageTitle := TitleText("应用配置") 31 32 appGroup := createAppConfigGroup() 33 serverGroup := createServerConfigGroup() 34 transcribeGroup := createTranscribeConfigGroup() 35 ttsGroup := createTtsConfigGroup() 36 37 var background *canvas.LinearGradient 38 if GetCurrentThemeIsDark() { 39 background = canvas.NewLinearGradient( 40 color.NRGBA{R: 15, G: 23, B: 42, A: 255}, 41 color.NRGBA{R: 30, G: 41, B: 59, A: 255}, 42 0.0, 43 ) 44 } else { 45 background = canvas.NewLinearGradient( 46 color.NRGBA{R: 248, G: 250, B: 252, A: 255}, 47 color.NRGBA{R: 241, G: 245, B: 249, A: 255}, 48 0.0, 49 ) 50 } 51 52 spacer1 := canvas.NewRectangle(color.NRGBA{R: 0, G: 0, B: 0, A: 0}) 53 spacer1.SetMinSize(fyne.NewSize(0, 15)) 54 spacer2 := canvas.NewRectangle(color.NRGBA{R: 0, G: 0, B: 0, A: 0}) 55 spacer2.SetMinSize(fyne.NewSize(0, 15)) 56 57 configContainer := container.NewVBox( 58 container.NewPadded(pageTitle), 59 spacer1, 60 container.NewPadded(appGroup), 61 container.NewPadded(serverGroup), 62 container.NewPadded(transcribeGroup), 63 container.NewPadded(ttsGroup), 64 spacer2, 65 ) 66 67 scroll := container.NewScroll(configContainer) 68 69 configStack := container.NewStack(background, scroll) 70 71 return container.NewPadded(configStack) 72 } 73 74 // LLM 配置控件引用,供供应商卡片点击时联动 75 var llmBaseUrlEntryRef *widget.Entry 76 var llmModelEntryRef *widget.Entry 77 var llmModelSelectRef *widget.Select 78 79 func CreateLlmTab() fyne.CanvasObject { 80 pageTitle := TitleText("LLM 配置") 81 82 // 创建LLM配置表单 83 llmConfigCard := createLlmConfigGroup() 84 85 // 创建API供应商快捷设置区域(依赖上面的表单控件引用) 86 providersCard := createApiProvidersCard() 87 88 // 创建使用指南卡片 89 guideCard := createLlmGuideCard() 90 91 var background *canvas.LinearGradient 92 if GetCurrentThemeIsDark() { 93 background = canvas.NewLinearGradient( 94 color.NRGBA{R: 15, G: 23, B: 42, A: 255}, 95 color.NRGBA{R: 30, G: 41, B: 59, A: 255}, 96 0.0, 97 ) 98 } else { 99 background = canvas.NewLinearGradient( 100 color.NRGBA{R: 248, G: 250, B: 252, A: 255}, 101 color.NRGBA{R: 241, G: 245, B: 249, A: 255}, 102 0.0, 103 ) 104 } 105 106 spacer1 := canvas.NewRectangle(color.NRGBA{R: 0, G: 0, B: 0, A: 0}) 107 spacer1.SetMinSize(fyne.NewSize(0, 15)) 108 spacer2 := canvas.NewRectangle(color.NRGBA{R: 0, G: 0, B: 0, A: 0}) 109 spacer2.SetMinSize(fyne.NewSize(0, 15)) 110 111 llmContainer := container.NewVBox( 112 container.NewPadded(pageTitle), 113 spacer1, 114 container.NewPadded(providersCard), 115 container.NewPadded(llmConfigCard), 116 container.NewPadded(guideCard), 117 spacer2, 118 ) 119 120 scroll := container.NewScroll(llmContainer) 121 llmStack := container.NewStack(background, scroll) 122 123 return container.NewPadded(llmStack) 124 } 125 126 // 创建API供应商快捷链接卡片 127 func createApiProvidersCard() *fyne.Container { 128 // 内部工具:设置 BaseURL 和 推荐模型 129 setProvider := func(baseURL string, models []string) { 130 if llmBaseUrlEntryRef != nil { 131 llmBaseUrlEntryRef.SetText(baseURL) 132 } 133 if llmModelSelectRef != nil { 134 llmModelSelectRef.Options = models 135 llmModelSelectRef.Refresh() 136 if len(models) > 0 { 137 llmModelSelectRef.SetSelected(models[0]) 138 if llmModelEntryRef != nil { 139 llmModelEntryRef.SetText(models[0]) 140 } 141 } else { 142 if llmModelEntryRef != nil { 143 llmModelEntryRef.SetText("") 144 } 145 } 146 } 147 } 148 // 通义千问卡片 149 qwenCard := createProviderCard( 150 "通义千问 Qwen", 151 "阿里云大模型服务", 152 "https://bailian.console.aliyun.com/", 153 color.NRGBA{R: 99, G: 54, B: 231, A: 255}, // 通义千问紫色 154 "qwen", 155 func() { 156 setProvider("https://dashscope.aliyuncs.com/compatible-mode/v1", []string{ 157 "qwen-turbo", "qwen-plus", "qwen-max", 158 }) 159 }, 160 ) 161 162 // OpenAI卡片 163 openaiCard := createProviderCard( 164 "OpenAI", 165 "GPT模型API服务", 166 "https://platform.openai.com/", 167 color.NRGBA{R: 116, G: 195, B: 101, A: 255}, // OpenAI绿色 168 "openai", 169 func() { 170 setProvider("https://api.openai.com/v1", []string{ 171 "gpt-4o-mini", "gpt-4o", "gpt-4.1-mini", "o3-mini", 172 }) 173 }, 174 ) 175 176 // DeepSeek卡片 177 deepseekCard := createProviderCard( 178 "DeepSeek", 179 "高性价比AI模型", 180 "https://platform.deepseek.com/", 181 color.NRGBA{R: 77, G: 107, B: 254, A: 255}, // DeepSeek蓝色 182 "deepseek", 183 func() { 184 setProvider("https://api.deepseek.com/v1", []string{ 185 "deepseek-chat", "deepseek-coder", "DeepSeek-V3", "DeepSeek-R1", 186 }) 187 }, 188 ) 189 190 // 新增自定义供应商卡片 191 addProviderCard := createProviderCard( 192 "新增", 193 "添加自定义供应商", 194 "https://example.com/krillinai/add-provider", // 占位链接,后续可替换 195 color.NRGBA{R: 14, G: 165, B: 233, A: 255}, // 青色强调 196 "add", 197 func() { 198 setProvider("", []string{}) 199 }, 200 ) 201 202 providersGrid := container.New( 203 layout.NewGridLayoutWithColumns(2), 204 qwenCard, 205 openaiCard, 206 deepseekCard, 207 addProviderCard, 208 ) 209 210 return GlassmorphismCard( 211 "API 供应商", 212 "点击下方卡片快速跳转到对应平台购买API", 213 providersGrid, 214 GetCurrentThemeIsDark(), 215 ) 216 } 217 218 // 获取供应商图标 219 func getProviderIcon(provider string) fyne.CanvasObject { 220 var pngPath string 221 switch provider { 222 case "qwen": 223 pngPath = "source/qwen-color.png" 224 case "openai": 225 pngPath = "source/openai.png" 226 case "deepseek": 227 pngPath = "source/deepseek-color.png" 228 // case "siliconcloud": 229 // pngPath = "source/siliconcloud-color.png" 230 default: 231 return container.NewWithoutLayout() 232 } 233 234 data, err := static.EmbeddedFiles.ReadFile(pngPath) 235 if err != nil { 236 log.GetLogger().Error("Failed to load PNG icon", zap.String("path", pngPath), zap.Error(err)) 237 return container.NewWithoutLayout() 238 } 239 240 res := fyne.NewStaticResource(pngPath, data) 241 img := canvas.NewImageFromResource(res) 242 img.FillMode = canvas.ImageFillContain 243 img.SetMinSize(fyne.NewSize(24, 24)) 244 img.Resize(fyne.NewSize(24, 24)) 245 return img 246 } 247 248 // 创建单个供应商卡片 249 func createProviderCard(name, description, url string, accentColor color.Color, provider string, onTap func()) *fyne.Container { 250 isDark := GetCurrentThemeIsDark() 251 252 var bgColor color.Color 253 var textColor color.Color 254 var descColor color.Color 255 var shadowColor color.Color 256 var hoverBgColor color.Color 257 258 if isDark { 259 bgColor = color.NRGBA{R: 51, G: 65, B: 85, A: 120} 260 hoverBgColor = color.NRGBA{R: 71, G: 85, B: 105, A: 150} 261 textColor = color.NRGBA{R: 248, G: 250, B: 252, A: 255} 262 descColor = color.NRGBA{R: 148, G: 163, B: 184, A: 255} 263 shadowColor = color.NRGBA{R: 0, G: 0, B: 0, A: 60} 264 } else { 265 bgColor = color.NRGBA{R: 255, G: 255, B: 255, A: 200} 266 hoverBgColor = color.NRGBA{R: 245, G: 247, B: 250, A: 220} 267 textColor = color.NRGBA{R: 17, G: 24, B: 39, A: 255} 268 descColor = color.NRGBA{R: 107, G: 114, B: 128, A: 255} 269 shadowColor = color.NRGBA{R: 0, G: 0, B: 0, A: 30} 270 } 271 272 // 创建阴影效果 273 shadow := canvas.NewRectangle(shadowColor) 274 shadow.CornerRadius = 12 275 shadow.Move(fyne.NewPos(2, 2)) 276 277 // 背景 278 background := canvas.NewRectangle(bgColor) 279 background.CornerRadius = 12 280 background.StrokeColor = accentColor 281 background.StrokeWidth = 2 282 283 // 图标 284 icon := getProviderIcon(provider) 285 // 顶部留白,避免图标贴近上边缘 286 topPadding := canvas.NewRectangle(color.NRGBA{R: 0, G: 0, B: 0, A: 0}) 287 topPadding.SetMinSize(fyne.NewSize(0, 12)) 288 // 为图标创建容器以确保居中 289 iconContainer := container.NewCenter(icon) 290 291 // 标题 292 nameLabel := canvas.NewText(name, textColor) 293 nameLabel.TextSize = 16 294 nameLabel.TextStyle = fyne.TextStyle{Bold: true} 295 nameLabel.Alignment = fyne.TextAlignCenter 296 297 // 描述 298 descLabel := canvas.NewText(description, descColor) 299 descLabel.TextSize = 12 300 descLabel.Alignment = fyne.TextAlignCenter 301 302 // 创建可点击的容器 303 content := container.NewVBox( 304 topPadding, 305 iconContainer, 306 container.NewPadded(nameLabel), 307 container.NewPadded(descLabel), 308 ) 309 310 // 创建卡片容器,包含阴影和背景 311 card := container.NewStack(shadow, background, content) 312 card.Resize(fyne.NewSize(200, 100)) // 增加高度以适应图标 313 314 // 创建透明的可点击区域 315 clickableArea := canvas.NewRectangle(color.NRGBA{R: 0, G: 0, B: 0, A: 0}) 316 clickableArea.Resize(fyne.NewSize(200, 100)) 317 318 // 创建自定义的可点击对象 319 tappable := &tappableObject{ 320 rect: clickableArea, 321 onTap: func() { 322 // 点击效果:内陷动画 323 originalPos := card.Position() 324 originalShadowPos := shadow.Position() 325 326 // 内陷效果:卡片向下移动,阴影缩小 327 card.Move(fyne.NewPos(originalPos.X+1, originalPos.Y+1)) 328 shadow.Move(fyne.NewPos(originalShadowPos.X+1, originalShadowPos.Y+1)) 329 330 // 背景颜色变化 331 background.FillColor = hoverBgColor 332 background.Refresh() 333 334 // 执行点击回调,若未提供回调且存在 URL 则尝试打开浏览器 335 if onTap != nil { 336 onTap() 337 } else { 338 if app := fyne.CurrentApp(); app != nil && url != "" { 339 app.OpenURL(parseURL(url)) 340 } 341 } 342 343 // 恢复原位置和颜色 344 go func() { 345 time.Sleep(150 * time.Millisecond) 346 card.Move(fyne.NewPos(0, 0)) 347 shadow.Move(fyne.NewPos(2, 2)) 348 background.FillColor = bgColor 349 background.Refresh() 350 }() 351 }, 352 onHover: func(hovering bool) { 353 if hovering { 354 // 悬停:仅做颜色和阴影变化,避免尺寸变化引发布局抖动 355 background.FillColor = hoverBgColor 356 background.StrokeWidth = 3 357 shadow.Move(fyne.NewPos(3, 3)) 358 background.Refresh() 359 } else { 360 background.FillColor = bgColor 361 background.StrokeWidth = 2 362 shadow.Move(fyne.NewPos(2, 2)) 363 background.Refresh() 364 } 365 }, 366 } 367 368 // 创建最终容器 369 finalContainer := container.NewStack(card, tappable) 370 371 return finalContainer 372 } 373 374 // 创建LLM使用指南卡片 375 func createLlmGuideCard() *fyne.Container { 376 guideText := `# LLM 配置指南: 377 378 ## API Base URL:(根据实际情况选择) 379 - OpenAI官方:https://api.openai.com/v1 380 - 阿里云百炼:https://dashscope.aliyuncs.com/compatible-mode/v1 381 - DeepSeek:https://api.deepseek.com/v1 382 383 ## API Key: 384 - 在对应平台的控制台中获取 385 - 请妥善保管,避免泄露 386 387 ## 模型名称: 388 - OpenAI:gpt-3.5-turbo, gpt-4, gpt-4-turbo... 389 - 阿里云:qwen-turbo, qwen-plus, qwen-max... 390 - DeepSeek:deepseek-chat, deepseek-coder... 391 392 ## 使用建议: 393 - 根据实际需求选择合适的模型 394 - 注意API调用费用` 395 396 guideLabel := widget.NewRichTextFromMarkdown(guideText) 397 guideLabel.Wrapping = fyne.TextWrapWord 398 399 return GlassmorphismCard( 400 "使用指南", 401 "LLM API配置说明", 402 guideLabel, 403 GetCurrentThemeIsDark(), 404 ) 405 } 406 407 // 解析URL的辅助函数 408 func parseURL(urlStr string) *url.URL { 409 u, err := url.Parse(urlStr) 410 if err != nil { 411 log.GetLogger().Error("解析URL失败", zap.Error(err)) 412 return nil 413 } 414 return u 415 } 416 417 // 创建字幕任务界面 418 func CreateSubtitleTab(window fyne.Window) fyne.CanvasObject { 419 sm := NewSubtitleManager(window) 420 421 title1 := TitleText("视频翻译配音") 422 title2 := TitleText("Video Translate & Dubbing") 423 titleContainer := container.NewVBox(title1, title2) 424 425 videoInputContainer := createVideoInputContainer(sm) 426 subtitleSettingsCard := createSubtitleSettingsCard(sm) 427 voiceSettingsCard := createVoiceSettingsCard(sm) 428 embedSettingsCard := createEmbedSettingsCard(sm) 429 430 progress, downloadContainer, tipsLabel := createProgressAndDownloadArea(sm) 431 432 startButton := createStartButton(window, sm, videoInputContainer, embedSettingsCard, progress, downloadContainer) 433 startButtonContainer := container.NewHBox(layout.NewSpacer(), startButton, layout.NewSpacer()) 434 435 var background *canvas.LinearGradient 436 if GetCurrentThemeIsDark() { 437 background = canvas.NewLinearGradient( 438 color.NRGBA{R: 15, G: 23, B: 42, A: 255}, 439 color.NRGBA{R: 30, G: 41, B: 59, A: 255}, 440 0.0, 441 ) 442 } else { 443 background = canvas.NewLinearGradient( 444 color.NRGBA{R: 248, G: 250, B: 252, A: 255}, 445 color.NRGBA{R: 241, G: 245, B: 249, A: 255}, 446 0.0, 447 ) 448 } 449 450 spacer1 := canvas.NewRectangle(color.NRGBA{R: 0, G: 0, B: 0, A: 0}) 451 spacer1.SetMinSize(fyne.NewSize(0, 15)) 452 spacer2 := canvas.NewRectangle(color.NRGBA{R: 0, G: 0, B: 0, A: 0}) 453 spacer2.SetMinSize(fyne.NewSize(0, 15)) 454 spacer3 := canvas.NewRectangle(color.NRGBA{R: 0, G: 0, B: 0, A: 0}) 455 spacer3.SetMinSize(fyne.NewSize(0, 15)) 456 457 progressArea := container.NewVBox(progress) 458 459 mainContent := container.NewVBox( 460 container.NewPadded(titleContainer), 461 spacer1, 462 container.NewPadded(videoInputContainer), 463 container.NewPadded(subtitleSettingsCard), 464 container.NewPadded(voiceSettingsCard), 465 container.NewPadded(embedSettingsCard), 466 spacer2, 467 container.NewPadded(startButtonContainer), 468 spacer3, 469 progressArea, 470 downloadContainer, 471 tipsLabel, 472 ) 473 474 scroll := container.NewScroll(mainContent) 475 476 // 使用一个Stack将背景和滚动内容组合 477 contentStack := container.NewStack(background, scroll) 478 479 return container.NewPadded(contentStack) 480 } 481 482 // 创建应用配置组 483 func createAppConfigGroup() *fyne.Container { 484 appSegmentDurationEntry := StyledEntry("字幕分段处理时长(分钟)") 485 appSegmentDurationEntry.Bind(binding.IntToString(binding.BindInt(&config.Conf.App.SegmentDuration))) 486 appSegmentDurationEntry.Validator = func(s string) error { 487 val, err := strconv.Atoi(s) 488 if err != nil { 489 return fmt.Errorf("请输入数字") 490 } 491 if val < 1 || val > 30 { 492 return fmt.Errorf("请输入1-30之间的数字") 493 } 494 return nil 495 } 496 497 appTranscribeParallelNumEntry := StyledEntry("转录并行数量") 498 appTranscribeParallelNumEntry.Bind(binding.IntToString(binding.BindInt(&config.Conf.App.TranscribeParallelNum))) 499 appTranscribeParallelNumEntry.Validator = func(s string) error { 500 val, err := strconv.Atoi(s) 501 if err != nil { 502 return fmt.Errorf("请输入数字") 503 } 504 if val < 1 || val > 10 { 505 return fmt.Errorf("请输入1-10之间的数字") 506 } 507 return nil 508 } 509 510 appTranslateParallelNumEntry := StyledEntry("翻译并行数量") 511 appTranslateParallelNumEntry.Bind(binding.IntToString(binding.BindInt(&config.Conf.App.TranslateParallelNum))) 512 appTranslateParallelNumEntry.Validator = func(s string) error { 513 val, err := strconv.Atoi(s) 514 if err != nil { 515 return fmt.Errorf("请输入数字") 516 } 517 if val < 1 || val > 20 { 518 return fmt.Errorf("请输入1-20之间的数字") 519 } 520 return nil 521 } 522 523 appTranscribeMaxAttemptsEntry := StyledEntry("转录最大尝试次数") 524 appTranscribeMaxAttemptsEntry.Bind(binding.IntToString(binding.BindInt(&config.Conf.App.TranscribeMaxAttempts))) 525 appTranscribeMaxAttemptsEntry.Validator = func(s string) error { 526 val, err := strconv.Atoi(s) 527 if err != nil { 528 return fmt.Errorf("请输入数字") 529 } 530 if val < 1 || val > 10 { 531 return fmt.Errorf("请输入1-10之间的数字") 532 } 533 return nil 534 } 535 536 appTranslateMaxAttemptsEntry := StyledEntry("翻译最大尝试次数") 537 appTranslateMaxAttemptsEntry.Bind(binding.IntToString(binding.BindInt(&config.Conf.App.TranslateMaxAttempts))) 538 appTranslateMaxAttemptsEntry.Validator = func(s string) error { 539 val, err := strconv.Atoi(s) 540 if err != nil { 541 return fmt.Errorf("请输入数字") 542 } 543 if val < 1 || val > 20 { 544 return fmt.Errorf("请输入1-20之间的数字") 545 } 546 return nil 547 } 548 549 appMaxSentenceLengthEntry := StyledEntry("每个句子最大字符数 Max sentence length") 550 appMaxSentenceLengthEntry.Bind(binding.IntToString(binding.BindInt(&config.Conf.App.MaxSentenceLength))) 551 appMaxSentenceLengthEntry.Validator = func(s string) error { 552 val, err := strconv.Atoi(s) 553 if err != nil { 554 return fmt.Errorf("请输入数字") 555 } 556 if val < 1 || val > 200 { 557 return fmt.Errorf("请输入1-200之间的数字") 558 } 559 return nil 560 } 561 562 appProxyEntry := StyledEntry("网络代理地址") 563 appProxyEntry.Bind(binding.BindString(&config.Conf.App.Proxy)) 564 565 form := widget.NewForm( 566 widget.NewFormItem("分段处理时长(分钟) Segment duration (minutes)", appSegmentDurationEntry), 567 widget.NewFormItem("转录最大并行数量 Transcribe parallel num", appTranscribeParallelNumEntry), 568 widget.NewFormItem("翻译最大并行数量 Translate parallel num", appTranslateParallelNumEntry), 569 widget.NewFormItem("转录最大尝试次数 Transcribe max attempts", appTranscribeMaxAttemptsEntry), 570 widget.NewFormItem("翻译最大尝试次数 Translate max attempts", appTranslateMaxAttemptsEntry), 571 widget.NewFormItem("每个句子最大字符数 Max sentence length", appMaxSentenceLengthEntry), 572 widget.NewFormItem("网络代理地址 proxy", appProxyEntry), 573 ) 574 575 return GlassmorphismCard("应用配置 App Config", "基本参数 Basic config", form, GetCurrentThemeIsDark()) 576 } 577 578 // 创建server配置组 579 func createServerConfigGroup() *fyne.Container { 580 serverHostEntry := StyledEntry("服务器地址 Server address") 581 serverHostEntry.Bind(binding.BindString(&config.Conf.Server.Host)) 582 583 serverPortEntry := StyledEntry("服务器端口 Server port") 584 serverPortEntry.Bind(binding.IntToString(binding.BindInt(&config.Conf.Server.Port))) 585 serverPortEntry.Validator = func(s string) error { 586 val, err := strconv.Atoi(s) 587 if err != nil { 588 return fmt.Errorf("请输入数字") 589 } 590 if val < 1 || val > 65535 { 591 return fmt.Errorf("请输入1-65535之间的有效端口") 592 } 593 return nil 594 } 595 596 form := widget.NewForm( 597 widget.NewFormItem("服务器地址 Server address", serverHostEntry), 598 widget.NewFormItem("服务器端口 Server port", serverPortEntry), 599 ) 600 601 return GlassmorphismCard("服务器配置 Server Config", "API服务器设置 API server settings", form, GetCurrentThemeIsDark()) 602 } 603 604 // 创建LLM配置组 605 func createLlmConfigGroup() *fyne.Container { 606 baseUrlEntry := StyledEntry("API Base URL") 607 baseUrlEntry.Bind(binding.BindString(&config.Conf.Llm.BaseUrl)) 608 llmBaseUrlEntryRef = baseUrlEntry 609 610 apiKeyEntry := StyledPasswordEntry("API Key") 611 apiKeyEntry.Bind(binding.BindString(&config.Conf.Llm.ApiKey)) 612 613 modelEntry := StyledEntry("模型名称 Model name") 614 modelEntry.Bind(binding.BindString(&config.Conf.Llm.Model)) 615 llmModelEntryRef = modelEntry 616 617 // 推荐模型下拉(只展示、选中后同步到文本框) 618 modelSelect := StyledSelect([]string{}, func(v string) { 619 if v != "" && llmModelEntryRef != nil { 620 llmModelEntryRef.SetText(v) 621 } 622 }) 623 modelSelect.PlaceHolder = "选择推荐模型(可选)" 624 llmModelSelectRef = modelSelect 625 626 form := widget.NewForm( 627 widget.NewFormItem("API Base URL", baseUrlEntry), 628 widget.NewFormItem("API Key", apiKeyEntry), 629 widget.NewFormItem("模型名称 Model name", modelEntry), 630 widget.NewFormItem("支持模型 Supported models", modelSelect), 631 ) 632 return GlassmorphismCard("LLM 配置 LLM Config", "LLM配置 LLM config", form, GetCurrentThemeIsDark()) 633 } 634 635 // 创建语音识别配置组 636 func createTranscribeConfigGroup() *fyne.Container { 637 providerOptions := []string{"openai", "fasterwhisper", "whisperkit", "whispercpp", "aliyun"} 638 providerSelect := widget.NewSelect(providerOptions, func(value string) { 639 config.Conf.Transcribe.Provider = value 640 }) 641 providerSelect.SetSelected(config.Conf.Transcribe.Provider) 642 643 openaiBaseUrlEntry := StyledEntry("API Base URL") 644 openaiBaseUrlEntry.Bind(binding.BindString(&config.Conf.Transcribe.Openai.BaseUrl)) 645 openaiApiKeyEntry := StyledPasswordEntry("API Key") 646 openaiApiKeyEntry.Bind(binding.BindString(&config.Conf.Transcribe.Openai.ApiKey)) 647 openaiModelEntry := StyledEntry("模型名称 Model name") 648 openaiModelEntry.Bind(binding.BindString(&config.Conf.Transcribe.Openai.Model)) 649 650 fasterWhisperModelEntry := StyledEntry("模型名称 Model name") 651 fasterWhisperModelEntry.Bind(binding.BindString(&config.Conf.Transcribe.Fasterwhisper.Model)) 652 653 whisperKitModelEntry := StyledEntry("模型名称 Model name") 654 whisperKitModelEntry.Bind(binding.BindString(&config.Conf.Transcribe.Whisperkit.Model)) 655 656 whisperCppModelEntry := StyledEntry("模型名称 Model name") 657 whisperCppModelEntry.Bind(binding.BindString(&config.Conf.Transcribe.Whispercpp.Model)) 658 659 aliyunOssKeyIdEntry := StyledEntry("阿里云 Aliyun Access Key ID") 660 aliyunOssKeyIdEntry.Bind(binding.BindString(&config.Conf.Transcribe.Aliyun.Oss.AccessKeyId)) 661 aliyunOssKeySecretEntry := StyledPasswordEntry("阿里云 Aliyun Access Key Secret") 662 aliyunOssKeySecretEntry.Bind(binding.BindString(&config.Conf.Transcribe.Aliyun.Oss.AccessKeySecret)) 663 aliyunOssBucketEntry := StyledEntry("阿里云 Aliyun OSS Bucket名称") 664 aliyunOssBucketEntry.Bind(binding.BindString(&config.Conf.Transcribe.Aliyun.Oss.Bucket)) 665 666 aliyunSpeechKeyIdEntry := StyledEntry("阿里云 Aliyun Speech Access Key ID") 667 aliyunSpeechKeyIdEntry.Bind(binding.BindString(&config.Conf.Transcribe.Aliyun.Speech.AccessKeyId)) 668 aliyunSpeechKeySecretEntry := StyledPasswordEntry("阿里云 Aliyun Speech Access Key Secret") 669 aliyunSpeechKeySecretEntry.Bind(binding.BindString(&config.Conf.Transcribe.Aliyun.Speech.AccessKeySecret)) 670 aliyunSpeechAppKeyEntry := StyledEntry("阿里云 Aliyun Speech App Key") 671 aliyunSpeechAppKeyEntry.Bind(binding.BindString(&config.Conf.Transcribe.Aliyun.Speech.AppKey)) 672 673 form := widget.NewForm( 674 widget.NewFormItem("提供商 Provider", providerSelect), 675 widget.NewFormItem("GPU加速 GPU acceleration", widget.NewCheckWithData("启用 Enable", binding.BindBool(&config.Conf.Transcribe.EnableGpuAcceleration))), 676 677 widget.NewFormItem("OpenAI Base URL", openaiBaseUrlEntry), 678 widget.NewFormItem("OpenAI API Key", openaiApiKeyEntry), 679 widget.NewFormItem("OpenAI 模型 Model", openaiModelEntry), 680 681 widget.NewFormItem("FasterWhisper 模型 Model", fasterWhisperModelEntry), 682 683 widget.NewFormItem("WhisperKit 模型 Model", whisperKitModelEntry), 684 685 widget.NewFormItem("WhisperCpp 模型 Model", whisperCppModelEntry), 686 687 widget.NewFormItem("阿里云 Aliyun OSS Access Key ID", aliyunOssKeyIdEntry), 688 widget.NewFormItem("阿里云 Aliyun OSS Access Key Secret", aliyunOssKeySecretEntry), 689 widget.NewFormItem("阿里云 Aliyun OSS Bucket Name", aliyunOssBucketEntry), 690 691 widget.NewFormItem("阿里云语音 Aliyun Speech Access Key ID", aliyunSpeechKeyIdEntry), 692 widget.NewFormItem("阿里云语音 Aliyun Speech Access Key Secret", aliyunSpeechKeySecretEntry), 693 widget.NewFormItem("阿里云语音 Aliyun Speech App Key", aliyunSpeechAppKeyEntry), 694 ) 695 696 return GlassmorphismCard("语音识别配置 Transcribe Config", "语音识别配置 Transcribe config", form, GetCurrentThemeIsDark()) 697 } 698 699 // 创建文本转语音配置组 700 func createTtsConfigGroup() *fyne.Container { 701 providerOptions := []string{"openai", "aliyun", "edge-tts"} 702 providerSelect := widget.NewSelect(providerOptions, func(value string) { 703 config.Conf.Tts.Provider = value 704 }) 705 providerSelect.SetSelected(config.Conf.Tts.Provider) 706 707 openaiBaseUrlEntry := StyledEntry("API Base URL") 708 openaiBaseUrlEntry.Bind(binding.BindString(&config.Conf.Tts.Openai.BaseUrl)) 709 openaiApiKeyEntry := StyledPasswordEntry("API Key") 710 openaiApiKeyEntry.Bind(binding.BindString(&config.Conf.Tts.Openai.ApiKey)) 711 openaiModelEntry := StyledEntry("模型名称 Model name") 712 openaiModelEntry.Bind(binding.BindString(&config.Conf.Tts.Openai.Model)) 713 714 aliyunOssKeyIdEntry := StyledEntry("阿里云 Aliyun Access Key ID") 715 aliyunOssKeyIdEntry.Bind(binding.BindString(&config.Conf.Tts.Aliyun.Oss.AccessKeyId)) 716 aliyunOssKeySecretEntry := StyledPasswordEntry("阿里云 Aliyun Access Key Secret") 717 aliyunOssKeySecretEntry.Bind(binding.BindString(&config.Conf.Tts.Aliyun.Oss.AccessKeySecret)) 718 aliyunOssBucketEntry := StyledEntry("阿里云 Aliyun OSS Bucket名称") 719 aliyunOssBucketEntry.Bind(binding.BindString(&config.Conf.Tts.Aliyun.Oss.Bucket)) 720 721 aliyunSpeechKeyIdEntry := StyledEntry("阿里云 Aliyun Speech Access Key ID") 722 aliyunSpeechKeyIdEntry.Bind(binding.BindString(&config.Conf.Tts.Aliyun.Speech.AccessKeyId)) 723 aliyunSpeechKeySecretEntry := StyledPasswordEntry("阿里云 Aliyun Speech Access Key Secret") 724 aliyunSpeechKeySecretEntry.Bind(binding.BindString(&config.Conf.Tts.Aliyun.Speech.AccessKeySecret)) 725 aliyunSpeechAppKeyEntry := StyledEntry("阿里云 Aliyun Speech App Key") 726 aliyunSpeechAppKeyEntry.Bind(binding.BindString(&config.Conf.Tts.Aliyun.Speech.AppKey)) 727 728 form := widget.NewForm( 729 widget.NewFormItem("提供商 Provider", providerSelect), 730 731 widget.NewFormItem("OpenAI Base URL", openaiBaseUrlEntry), 732 widget.NewFormItem("OpenAI API Key", openaiApiKeyEntry), 733 widget.NewFormItem("OpenAI 模型 Model", openaiModelEntry), 734 735 widget.NewFormItem("阿里云 Aliyun OSS Access Key ID", aliyunOssKeyIdEntry), 736 widget.NewFormItem("阿里云 Aliyun OSS Access Key Secret", aliyunOssKeySecretEntry), 737 widget.NewFormItem("阿里云 Aliyun OSS Bucket", aliyunOssBucketEntry), 738 739 widget.NewFormItem("阿里云 Aliyun Speech Access Key ID", aliyunSpeechKeyIdEntry), 740 widget.NewFormItem("阿里云 Aliyun Speech Access Key Secret", aliyunSpeechKeySecretEntry), 741 widget.NewFormItem("阿里云 Aliyun Speech App Key", aliyunSpeechAppKeyEntry), 742 ) 743 744 return GlassmorphismCard("文本转语音配置 TTS Config", "文本转语音配置 TTS config", form, GetCurrentThemeIsDark()) 745 } 746 747 // 创建视频输入容器 748 func createVideoInputContainer(sm *SubtitleManager) *fyne.Container { 749 inputTypeRadio := widget.NewRadioGroup([]string{"本地上传 Upload a file", "输入链接 Paste a link"}, nil) 750 inputTypeRadio.Horizontal = true 751 inputTypeContainer := container.NewHBox( 752 inputTypeRadio, 753 ) 754 755 urlEntry := StyledEntry("输入视频链接 Paste a link here") 756 urlEntry.Hide() 757 urlEntry.OnChanged = func(text string) { 758 sm.SetVideoUrl(text) 759 } 760 761 selectButton := PrimaryButton("选择视频文件 Select Video Files", theme.FolderOpenIcon(), sm.ShowFileDialog) 762 763 selectedVideoLabel := widget.NewLabel("") 764 selectedVideoLabel.Hide() 765 766 sm.SetVideoSelectedCallback(func(path string) { // 设置视频地址+控制信息展示 767 if path != "" { 768 sm.SetVideoUrl(path) 769 selectedVideoLabel.SetText("已选择Chosen: " + filepath.Base(path)) 770 selectedVideoLabel.Show() 771 } else { 772 selectedVideoLabel.Hide() 773 } 774 }) 775 776 sm.SetVideosSelectedCallback(func(paths []string) { 777 if len(paths) > 0 { 778 sm.SetVideoUrl(paths[0]) 779 780 fileNames := make([]string, 0, len(paths)) 781 for _, path := range paths { 782 fileNames = append(fileNames, filepath.Base(path)) 783 } 784 785 displayText := fmt.Sprintf("已选择 %d 个文件:\n", len(paths)) 786 for i, name := range fileNames { 787 displayText += fmt.Sprintf("%d. %s\n", i+1, name) 788 } 789 790 selectedVideoLabel.SetText(displayText) 791 selectedVideoLabel.Show() 792 } else { 793 selectedVideoLabel.Hide() 794 } 795 }) 796 797 videoInputContainer := container.NewVBox() 798 videoInputContainer.Objects = []fyne.CanvasObject{selectButton, selectedVideoLabel} 799 800 inputTypeRadio.SetSelected("本地上传 Upload a file") 801 inputTypeRadio.OnChanged = func(value string) { 802 if value == "本地上传 Upload a file" { 803 urlEntry.Hide() 804 selectButton.Show() 805 selectedVideoLabel.Show() 806 videoInputContainer.Objects = []fyne.CanvasObject{selectButton, selectedVideoLabel} 807 sm.SetVideoUrl("") 808 } else { 809 selectButton.Hide() 810 selectedVideoLabel.Hide() 811 urlEntry.Show() 812 videoInputContainer.Objects = []fyne.CanvasObject{urlEntry} 813 } 814 videoInputContainer.Refresh() 815 } 816 817 content := container.NewVBox( 818 container.NewPadded(inputTypeContainer), 819 container.NewPadded(videoInputContainer), 820 ) 821 822 return GlassmorphismCard("1. 选择视频 Select Video", "", content, GetCurrentThemeIsDark()) 823 } 824 825 // 创建字幕设置卡片 826 func createSubtitleSettingsCard(sm *SubtitleManager) *fyne.Container { 827 positionSelect := widget.NewSelect([]string{ 828 "翻译在上 Translation Above", 829 "翻译在下 Translation Below", 830 }, func(value string) { 831 if value == "翻译在上 Translation Above" { 832 sm.SetBilingualPosition(1) 833 } else { 834 sm.SetBilingualPosition(2) 835 } 836 }) 837 positionSelect.SetSelected("翻译在上 Translation Above") 838 839 bilingualCheck := widget.NewCheck("启用双语字幕 Bilingual Subtitles", func(checked bool) { 840 sm.SetBilingualEnabled(checked) 841 if checked { 842 positionSelect.Enable() 843 } else { 844 positionSelect.Disable() 845 } 846 }) 847 bilingualCheck.SetChecked(true) 848 849 var targetSelectOptions []string 850 targetLangMap := make(map[string]string) 851 for code, name := range types.StandardLanguageCode2Name { 852 targetSelectOptions = append(targetSelectOptions, name) 853 targetLangMap[name] = string(code) 854 } 855 targetLangSelector := StyledSelect(targetSelectOptions, func(value string) { 856 sm.SetTargetLang(targetLangMap[value]) 857 }) 858 859 langContainer := container.NewVBox( 860 container.NewHBox( 861 widget.NewLabel("源语言 Original Language:"), 862 StyledSelect([]string{ 863 "简体中文", "English", "日本語", "Türkçe", "Deutsch", "한국어", "Русский язык", "Bahasa Melayu", 864 }, func(value string) { 865 sourceLangMap := map[string]string{ 866 "简体中文": "zh_cn", "English": "en", "日本語": "ja", 867 "Türkçe": "tr", "Deutsch": "de", "한국어": "ko", "Русский язык": "ru", 868 "Bahasa Melayu": "ms", 869 } 870 sm.SetSourceLang(sourceLangMap[value]) 871 }), 872 ), 873 container.NewHBox( 874 widget.NewLabel("翻译成 Translate To:"), 875 targetLangSelector, 876 ), 877 ) 878 879 // 设置默认语言 880 langContainer.Objects[0].(*fyne.Container).Objects[1].(*widget.Select).SetSelected("English") 881 langContainer.Objects[1].(*fyne.Container).Objects[1].(*widget.Select).SetSelected("简体中文") 882 883 fillerCheck := widget.NewCheck("启用语气词过滤 Tone Word Filtering", func(checked bool) { 884 sm.SetFillerFilter(checked) 885 }) 886 fillerCheck.SetChecked(true) 887 888 content := container.NewVBox( 889 container.NewHBox(bilingualCheck, fillerCheck), 890 langContainer, 891 positionSelect, 892 ) 893 894 return ModernCard("2. 字幕设置 Subtitle settings", content, GetCurrentThemeIsDark()) 895 } 896 897 // 创建配音设置卡片 898 func createVoiceSettingsCard(sm *SubtitleManager) *fyne.Container { 899 voiceCodeEntry := widget.NewEntry() 900 voiceCodeEntry.SetPlaceHolder("输入声音代码 Enter voice code") 901 voiceCodeEntry.OnChanged = func(text string) { 902 sm.SetTtsVoiceCode(text) 903 } 904 voiceCodeEntry.Disable() 905 906 // 音色克隆功能 - 当前支持阿里云TTS,未来可扩展其他提供商 907 audioSampleButton := SecondaryButton("选择音色克隆样本 Select Voice Clone Sample(Aliyun TTS Supported)", theme.MediaMusicIcon(), sm.ShowAudioFileDialog) 908 audioSampleButton.Disable() 909 910 voiceoverCheck := widget.NewCheck("启用配音 Apply Dubbing", func(checked bool) { 911 sm.SetVoiceoverEnabled(checked) 912 if checked { 913 voiceCodeEntry.Enable() 914 audioSampleButton.Enable() 915 } else { 916 voiceCodeEntry.Disable() 917 audioSampleButton.Disable() 918 } 919 }) 920 921 grid := container.NewVBox( 922 container.NewHBox(voiceoverCheck), 923 container.NewHBox(container.NewBorder(voiceCodeEntry, nil, nil, audioSampleButton)), 924 ) 925 926 return ModernCard("3. 配音设置 Dubbing settings", grid, GetCurrentThemeIsDark()) 927 } 928 929 // 视频合成卡片 930 func createEmbedSettingsCard(sm *SubtitleManager) *fyne.Container { 931 embedCheck := widget.NewCheck("合成 Composite", nil) 932 933 embedTypeSelect := StyledSelect([]string{ 934 "横屏输出 Landscape(16:9)", "竖屏输出 Portrait(9:16)", "横屏+竖屏 (Landscape+Portrait)", 935 }, nil) 936 embedTypeSelect.Disable() 937 938 mainTitleEntry := StyledEntry("请输入主标题 Enter main title") 939 subTitleEntry := StyledEntry("请输入副标题 Enter sub title") 940 941 titleInputContainer := container.NewVBox( 942 container.NewGridWithColumns(2, 943 widget.NewLabel("主标题 Main title:"), 944 mainTitleEntry, 945 ), 946 container.NewGridWithColumns(2, 947 widget.NewLabel("副标题 Sub title:"), 948 subTitleEntry, 949 ), 950 ) 951 titleInputContainer.Hide() 952 953 embedCheck.OnChanged = func(checked bool) { 954 if checked { 955 embedTypeSelect.Enable() 956 embedTypeSelect.SetSelected("横屏输出 Landscape(16:9)") 957 } else { 958 embedTypeSelect.Disable() 959 sm.SetEmbedSubtitle("none") 960 } 961 } 962 963 embedTypeSelect.OnChanged = func(value string) { 964 switch value { 965 case "横屏输出 Landscape(16:9)": 966 titleInputContainer.Hide() 967 sm.SetEmbedSubtitle("horizontal") 968 case "竖屏输出 Portrait(9:16)": 969 titleInputContainer.Show() 970 sm.SetEmbedSubtitle("vertical") 971 case "横屏+竖屏 (Landscape+Portrait)": 972 titleInputContainer.Show() 973 sm.SetEmbedSubtitle("all") 974 } 975 } 976 977 topContainer := container.NewHBox(embedCheck, embedTypeSelect) 978 979 mainContainer := container.NewVBox( 980 topContainer, 981 container.NewPadded(titleInputContainer), 982 ) 983 984 return ModernCard("视频合成设置 Composition Settings", mainContainer, GetCurrentThemeIsDark()) 985 } 986 987 // 创建进度和下载区域 988 func createProgressAndDownloadArea(sm *SubtitleManager) (*widget.ProgressBar, *fyne.Container, *fyne.Container) { 989 progress := widget.NewProgressBar() 990 progress.Hide() 991 992 percentLabel := widget.NewLabel("0%") 993 percentLabel.Hide() 994 percentLabel.Alignment = fyne.TextAlignTrailing 995 996 progressContainer := container.NewBorder(nil, nil, nil, percentLabel, progress) 997 progressContainer.Hide() 998 999 progressBg := canvas.NewRectangle(color.NRGBA{R: 240, G: 245, B: 250, A: 230}) 1000 progressBg.SetMinSize(fyne.NewSize(0, 40)) 1001 progressBg.CornerRadius = 8 1002 1003 progressShadow := canvas.NewRectangle(color.NRGBA{R: 0, G: 0, B: 0, A: 20}) 1004 progressShadow.Move(fyne.NewPos(2, 2)) 1005 progressShadow.SetMinSize(fyne.NewSize(0, 40)) 1006 progressShadow.CornerRadius = 8 1007 1008 progressWithBg := container.NewStack( 1009 progressShadow, 1010 progressBg, 1011 container.NewPadded(progressContainer), 1012 ) 1013 progressWithBg.Hide() 1014 1015 sm.SetProgressBar(progress) 1016 sm.SetProgressLabel(percentLabel) 1017 1018 downloadBg := canvas.NewRectangle(color.NRGBA{R: 240, G: 250, B: 255, A: 230}) 1019 downloadBg.CornerRadius = 10 1020 1021 downloadContainer := container.NewVBox() 1022 downloadContainer.Hide() 1023 sm.SetDownloadContainer(downloadContainer) 1024 1025 downloadWithBg := container.NewStack( 1026 downloadBg, 1027 container.NewPadded(downloadContainer), 1028 ) 1029 downloadWithBg.Hide() 1030 1031 tipsLabel := widget.NewLabel("") 1032 tipsLabel.Hide() 1033 tipsLabel.Alignment = fyne.TextAlignCenter 1034 tipsLabel.Wrapping = fyne.TextWrapWord 1035 sm.SetTipsLabel(tipsLabel) 1036 1037 tipsBg := canvas.NewRectangle(color.NRGBA{R: 255, G: 250, B: 230, A: 200}) 1038 tipsBg.CornerRadius = 6 1039 1040 tipsWithBg := container.NewStack( 1041 tipsBg, 1042 container.NewPadded(tipsLabel), 1043 ) 1044 tipsWithBg.Hide() 1045 1046 return progress, downloadWithBg, tipsWithBg 1047 } 1048 1049 // 开始按钮 1050 func createStartButton(window fyne.Window, sm *SubtitleManager, videoInputContainer *fyne.Container, embedSettingsCard *fyne.Container, progress *widget.ProgressBar, downloadContainer *fyne.Container) *widget.Button { 1051 btn := widget.NewButtonWithIcon("开始翻译 Start Translating", theme.MediaPlayIcon(), nil) 1052 btn.Importance = widget.HighImportance 1053 1054 btn.OnTapped = func() { 1055 originalImportance := btn.Importance 1056 btn.Importance = widget.DangerImportance 1057 btn.Refresh() 1058 1059 go func() { 1060 time.Sleep(300 * time.Millisecond) 1061 btn.Importance = originalImportance 1062 btn.Refresh() 1063 }() 1064 1065 var mainTitle, subTitle string 1066 1067 if embedSettingsCard != nil && len(embedSettingsCard.Objects) > 1 { 1068 if titleContainer, ok := embedSettingsCard.Objects[1].(*fyne.Container); ok && titleContainer != nil && len(titleContainer.Objects) >= 2 { 1069 if mainTitleRow, ok := titleContainer.Objects[0].(*fyne.Container); ok && mainTitleRow != nil && len(mainTitleRow.Objects) >= 2 { 1070 if mainTitleEntry, ok := mainTitleRow.Objects[1].(*widget.Entry); ok { 1071 mainTitle = mainTitleEntry.Text 1072 } 1073 } 1074 1075 if subTitleRow, ok := titleContainer.Objects[1].(*fyne.Container); ok && subTitleRow != nil && len(subTitleRow.Objects) >= 2 { 1076 if subTitleEntry, ok := subTitleRow.Objects[1].(*widget.Entry); ok { 1077 subTitle = subTitleEntry.Text 1078 } 1079 } 1080 } 1081 } 1082 1083 sm.SetVerticalTitles(mainTitle, subTitle) 1084 1085 progress.Show() 1086 sm.progressBar.SetValue(0) 1087 downloadContainer.Hide() 1088 1089 if sm.GetVideoUrl() == "" { 1090 inputType := "本地视频" 1091 1092 if videoInputContainer != nil && len(videoInputContainer.Objects) > 0 { 1093 for i := 0; i < len(videoInputContainer.Objects); i++ { 1094 // 如果对象是Container,查找其中的RadioGroup 1095 if container, ok := videoInputContainer.Objects[i].(*fyne.Container); ok { 1096 for j := 0; j < len(container.Objects); j++ { 1097 if radio, ok := container.Objects[j].(*widget.RadioGroup); ok { 1098 inputType = radio.Selected 1099 break 1100 } 1101 } 1102 } 1103 } 1104 } 1105 1106 if inputType == "本地视频" { 1107 dialog.ShowError(fmt.Errorf("请先选择视频文件"), window) 1108 } else { 1109 dialog.ShowError(fmt.Errorf("请输入视频链接"), window) 1110 } 1111 progress.Hide() 1112 return 1113 } 1114 1115 err := config.CheckConfig() 1116 if err != nil { 1117 dialog.ShowError(fmt.Errorf("配置不正确: %v", err), window) 1118 log.GetLogger().Error("配置不正确", zap.Error(err)) 1119 progress.Hide() 1120 return 1121 } 1122 1123 err = deps.CheckDependency() 1124 if err != nil { 1125 dialog.ShowError(fmt.Errorf("依赖环境准备失败: %v", err), window) 1126 log.GetLogger().Error("依赖环境准备失败", zap.Error(err)) 1127 progress.Hide() 1128 return 1129 } 1130 btn.Hide() 1131 1132 if config.ConfigBackup != config.Conf { 1133 if err = server.StopBackend(); err != nil { 1134 dialog.ShowError(fmt.Errorf("停止后端服务失败: %v", err), window) 1135 log.GetLogger().Error("停止后端服务失败", zap.Error(err)) 1136 progress.Hide() 1137 return 1138 } 1139 1140 go func() { 1141 err := server.StartBackend() 1142 if err != nil { 1143 dialog.ShowError(fmt.Errorf("启动后端服务失败: %v", err), window) 1144 log.GetLogger().Error("启动后端服务失败", zap.Error(err)) 1145 progress.Hide() 1146 return 1147 } 1148 }() 1149 1150 time.Sleep(1 * time.Second) 1151 config.ConfigBackup = config.Conf 1152 } 1153 1154 if err = sm.StartTask(); err != nil { 1155 dialog.ShowError(err, window) 1156 progress.Hide() 1157 return 1158 } 1159 1160 go func() { 1161 for { 1162 time.Sleep(1 * time.Second) 1163 if sm.progressBar.Value < 1 { 1164 continue 1165 } 1166 time.Sleep(1 * time.Second) 1167 if sm.progressBar.Value < 1 { 1168 continue 1169 } 1170 break 1171 } 1172 btn.Show() 1173 downloadContainer.Show() 1174 }() 1175 sm.progressBar.Refresh() 1176 } 1177 1178 return btn 1179 }