/ internal / desktop / ui.go
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  }