package ui import ( "github.com/rs/zerolog/log" "net/url" "strings" "time" "epochsilicon/pkg/launcher" "epochsilicon/pkg/patching" "epochsilicon/pkg/service" "epochsilicon/pkg/utils" "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/widget" ) // createOptionsComponents initializes all option-related UI components func createOptionsComponents() { // Load preferences for initial values prefs, _ := utils.LoadPrefs() metalHudCheckbox = widget.NewCheck("Enable Metal Hud (show FPS)", func(checked bool) { launcher.EnableMetalHud = checked // Save to preferences prefs, _ := utils.LoadPrefs() prefs.EnableMetalHud = checked utils.SavePrefs(prefs) log.Debug().Msgf("Metal HUD enabled: %v", launcher.EnableMetalHud) }) metalHudCheckbox.SetChecked(prefs.EnableMetalHud) launcher.EnableMetalHud = prefs.EnableMetalHud showTerminalCheckbox = widget.NewCheck("Show Terminal", func(checked bool) { // Save to preferences prefs, _ := utils.LoadPrefs() prefs.ShowTerminalNormally = checked utils.SavePrefs(prefs) log.Debug().Msgf("Show terminal normally: %v", checked) }) showTerminalCheckbox.SetChecked(prefs.ShowTerminalNormally) autoDeleteWdbCheckbox = widget.NewCheck("Auto-delete WDB directory on launch", func(checked bool) { launcher.AutoDeleteWdb = checked // Save to preferences prefs, _ := utils.LoadPrefs() prefs.AutoDeleteWdb = checked utils.SavePrefs(prefs) log.Debug().Msgf("Auto-delete WDB enabled: %v", launcher.AutoDeleteWdb) }) autoDeleteWdbCheckbox.SetChecked(prefs.AutoDeleteWdb) launcher.AutoDeleteWdb = prefs.AutoDeleteWdb // Create Wine registry Option-as-Alt buttons and status createWineRegistryComponents() // Load environment variables from preferences if prefs.EnvironmentVariables != "" { launcher.CustomEnvVars = prefs.EnvironmentVariables } envVarsEntry = widget.NewEntry() envVarsEntry.SetPlaceHolder(`Custom environment variables (KEY=VALUE format)`) envVarsEntry.SetText(launcher.CustomEnvVars) envVarsEntry.OnChanged = func(text string) { launcher.CustomEnvVars = text // Save to preferences prefs, _ := utils.LoadPrefs() prefs.EnvironmentVariables = text utils.SavePrefs(prefs) log.Debug().Msgf("Environment variables updated: %v", launcher.CustomEnvVars) } } // createPatchingButtons creates all patching-related buttons func createPatchingButtons(myWindow fyne.Window) { patchEpochButton = widget.NewButton("Patch Epoch", func() { patching.PatchEpoch(myWindow, UpdateAllStatuses) }) unpatchEpochButton = widget.NewButton("Unpatch Epoch", func() { patching.UnpatchEpoch(myWindow, UpdateAllStatuses) }) patchCrossOverButton = widget.NewButton("Patch CrossOver", func() { patching.PatchCrossOver(myWindow, UpdateAllStatuses) }) unpatchCrossOverButton = widget.NewButton("Unpatch CrossOver", func() { patching.UnpatchCrossOver(myWindow, UpdateAllStatuses) }) } // createServiceButtons creates service-related buttons func createServiceButtons(myWindow fyne.Window) { startServiceButton = widget.NewButton("Start Service", func() { service.StartRosettaX87Service(myWindow, UpdateAllStatuses) }) stopServiceButton = widget.NewButton("Stop Service", func() { service.StopRosettaX87Service(myWindow, UpdateAllStatuses) }) } // createLaunchButton creates the legacy launch button func createLaunchButton(myWindow fyne.Window) { launchButton = widget.NewButton("Launch Game", func() { launcher.LaunchGame(myWindow) }) } // createBottomBar creates the bottom bar with Options, GitHub, and PLAY buttons func createBottomBar(myWindow fyne.Window) fyne.CanvasObject { // Set the current window for popup functionality currentWindow = myWindow // Options button optionsButton := widget.NewButton("Options", func() { showOptionsPopup() }) // Troubleshooting button troubleshootingButton = widget.NewButton("Troubleshooting", func() { showTroubleshootingPopup() }) // Git button gitButton := widget.NewButton("Source Code", func() { githubURL := "https://git.burkey.co/eburk/epochsilicon" parsedURL, err := url.Parse(githubURL) if err != nil { log.Debug().Msgf("Error parsing git URL: %v", err) return } fyne.CurrentApp().OpenURL(parsedURL) }) playButtonText = widget.NewRichTextFromMarkdown("# PLAY") playButtonText.Wrapping = fyne.TextWrapOff playButton = widget.NewButton("", func() { launcher.LaunchGame(myWindow) }) playButton.Importance = widget.HighImportance playButton.Disable() playButtonWithText := container.NewStack( playButton, container.NewCenter(playButtonText), ) leftButtons := container.NewHBox( optionsButton, troubleshootingButton, gitButton, ) // Create the large play button with fixed size buttonWidth := float32(120) buttonHeight := float32(80) playButtonWithText.Resize(fyne.NewSize(buttonWidth, buttonHeight)) // Create a container for the play button that ensures it's positioned at bottom-right playButtonContainer := container.NewWithoutLayout(playButtonWithText) playButtonContainer.Resize(fyne.NewSize(buttonWidth+40, buttonHeight+20)) // Add padding playButtonWithText.Move(fyne.NewPos(-50, -32)) // Use border layout to position elements bottomContainer := container.NewBorder( nil, // top nil, // bottom leftButtons, // left playButtonContainer, // right - our large play button nil, // center ) return container.NewPadded(bottomContainer) } // createWineRegistryComponents creates Wine registry Option-as-Alt buttons and status func createWineRegistryComponents() { // Create status label to show current state optionAsAltStatusLabel = widget.NewRichText() // Create enable button enableOptionAsAltButton = widget.NewButton("Enable", func() { enableOptionAsAltButton.Disable() disableOptionAsAltButton.Disable() remapOperationInProgress = true // Show loading state in status label fyne.Do(func() { optionAsAltStatusLabel.ParseMarkdown("**Remap Option key as Alt key:** Enabling...") startPulsingEffect() }) // Run in goroutine to avoid blocking UI go func() { defer func() { remapOperationInProgress = false }() if err := utils.SetOptionAsAltEnabled(true); err != nil { log.Debug().Msgf("Failed to enable Option-as-Alt mapping: %v", err) // Update UI on main thread fyne.Do(func() { stopPulsingEffect() optionAsAltStatusLabel.ParseMarkdown("**Remap Option key as Alt key:** Enable Failed") }) time.Sleep(2 * time.Second) // Show error briefly } else { log.Debug().Msgf("Successfully enabled Option-as-Alt mapping") // Update preferences prefs, _ := utils.LoadPrefs() prefs.RemapOptionAsAlt = true utils.SavePrefs(prefs) } // Update UI on main thread fyne.Do(func() { stopPulsingEffect() updateWineRegistryStatusWithMethod(true) // Use Wine command for accurate check after modifications }) }() }) // Create disable button disableOptionAsAltButton = widget.NewButton("Disable", func() { enableOptionAsAltButton.Disable() disableOptionAsAltButton.Disable() remapOperationInProgress = true // Show loading state in status label fyne.Do(func() { optionAsAltStatusLabel.ParseMarkdown("**Remap Option key as Alt key:** Disabling...") startPulsingEffect() }) // Run in goroutine to avoid blocking UI go func() { defer func() { remapOperationInProgress = false }() if err := utils.SetOptionAsAltEnabled(false); err != nil { log.Debug().Msgf("Failed to disable Option-as-Alt mapping: %v", err) // Update UI on main thread fyne.Do(func() { stopPulsingEffect() optionAsAltStatusLabel.ParseMarkdown("**Remap Option key as Alt key:** Disable Failed") }) time.Sleep(2 * time.Second) // Show error briefly } else { log.Debug().Msgf("Successfully disabled Option-as-Alt mapping") // Update preferences prefs, _ := utils.LoadPrefs() prefs.RemapOptionAsAlt = false utils.SavePrefs(prefs) } // Update UI on main thread fyne.Do(func() { stopPulsingEffect() updateWineRegistryStatusWithMethod(true) // Use Wine command for accurate check after modifications }) }() }) // Style the buttons similar to other action buttons enableOptionAsAltButton.Importance = widget.MediumImportance disableOptionAsAltButton.Importance = widget.MediumImportance // Initialize status and button states updateWineRegistryStatus() } // updateWineRegistryStatus updates the Wine registry status label and button states func updateWineRegistryStatus() { updateWineRegistryStatusWithMethod(false) } // updateWineRegistryStatusWithMethod updates status with choice of checking method func updateWineRegistryStatusWithMethod(useWineCommand bool) { if useWineCommand { // Use Wine command for accurate check after modifications currentWineRegistryEnabled = utils.CheckOptionAsAltEnabled() } else { // Use fast file-based check for regular status updates currentWineRegistryEnabled = utils.CheckOptionAsAltEnabledFast() } // Update UI with simple white text if currentWineRegistryEnabled { optionAsAltStatusLabel.ParseMarkdown("**Remap Option key as Alt key:** Enabled") } else { optionAsAltStatusLabel.ParseMarkdown("**Remap Option key as Alt key:** Disabled") } // Update button states based on current status if currentWineRegistryEnabled { // If enabled, only show disable button as clickable enableOptionAsAltButton.Disable() disableOptionAsAltButton.Enable() } else { // If disabled, only show enable button as clickable enableOptionAsAltButton.Enable() disableOptionAsAltButton.Disable() } } // startPulsingEffect starts a pulsing animation for the status label during loading func startPulsingEffect() { if pulsingActive { return // Already pulsing } pulsingActive = true pulsingTicker = time.NewTicker(500 * time.Millisecond) go func() { dots := "" for pulsingActive { <-pulsingTicker.C if pulsingActive { // Cycle through different dot patterns for visual effect switch len(dots) { case 0: dots = "." case 1: dots = ".." case 2: dots = "..." default: dots = "" } // Update the label with pulsing dots fyne.Do(func() { if pulsingActive && optionAsAltStatusLabel != nil { // Use the dots directly in the status text if strings.Contains(optionAsAltStatusLabel.String(), "Enabling") { optionAsAltStatusLabel.ParseMarkdown("**Remap Option key as Alt key:** Enabling" + dots) } else if strings.Contains(optionAsAltStatusLabel.String(), "Disabling") { optionAsAltStatusLabel.ParseMarkdown("**Remap Option key as Alt key:** Disabling" + dots) } } }) } } }() } // stopPulsingEffect stops the pulsing animation func stopPulsingEffect() { if !pulsingActive { return } pulsingActive = false if pulsingTicker != nil { pulsingTicker.Stop() pulsingTicker = nil } } // updateRecommendedSettingsButton updates the state of the recommended settings button func updateRecommendedSettingsButton() { if applyRecommendedSettingsButton == nil { return } // Check if all recommended settings are already applied if launcher.CheckRecommendedSettings() { applyRecommendedSettingsButton.Disable() applyRecommendedSettingsButton.SetText("Settings applied") } else { applyRecommendedSettingsButton.Enable() applyRecommendedSettingsButton.SetText("Apply recommended settings") } // Help button should always be enabled if recommendedSettingsHelpButton != nil { recommendedSettingsHelpButton.Enable() } } // showRecommendedSettingsHelpPopup shows a popup explaining the recommended graphics settings func showRecommendedSettingsHelpPopup() { if currentWindow == nil { return } // Create help content helpTitle := widget.NewRichTextFromMarkdown("# 📋 Recommended Graphics Settings") // Create individual setting labels for better formatting settingsTitle := widget.NewLabel("The following settings will be applied to your Config.wtf file:") settingsTitle.TextStyle = fyne.TextStyle{Bold: true} setting1 := widget.NewLabel("• Terrain Distance (farclip): 177 - Reduces CPU overhead - more fps") setting2 := widget.NewLabel("• Vertex Animation Shaders (M2UseShaders): Enabled - Prevents graphic glitches") setting3 := widget.NewLabel("• Multisampling (gxMultisample): 2x - Makes portraits load properly") settingsContainer := container.NewVBox( settingsTitle, widget.NewSeparator(), setting1, setting2, setting3, widget.NewSeparator(), ) // Create OK button okButton := widget.NewButton("OK", func() { // This will be set when the popup is created }) okButton.Importance = widget.MediumImportance // Create help content container helpContentContainer := container.NewVBox( container.NewCenter(helpTitle), widget.NewSeparator(), settingsContainer, widget.NewSeparator(), container.NewCenter(okButton), ) // Calculate popup size windowSize := currentWindow.Content().Size() popupWidth := windowSize.Width * 3 / 4 popupHeight := windowSize.Height * 3 / 4 // Create the help popup helpPopup := widget.NewModalPopUp(container.NewPadded(helpContentContainer), currentWindow.Canvas()) helpPopup.Resize(fyne.NewSize(popupWidth, popupHeight)) // Set the OK button action to hide the help popup okButton.OnTapped = func() { helpPopup.Hide() } helpPopup.Show() }