| package launcher |
|
|
| import ( |
| "fmt" |
| "log" |
| "net/url" |
|
|
| "fyne.io/fyne/v2" |
| "fyne.io/fyne/v2/container" |
| "fyne.io/fyne/v2/dialog" |
| "fyne.io/fyne/v2/driver/desktop" |
| "fyne.io/fyne/v2/widget" |
| ) |
|
|
| |
| type SystrayManager struct { |
| launcher *Launcher |
| window fyne.Window |
| app fyne.App |
| desk desktop.App |
|
|
| |
| startStopItem *fyne.MenuItem |
| hasUpdateAvailable bool |
| latestVersion string |
| icon *fyne.StaticResource |
| } |
|
|
| |
| func NewSystrayManager(launcher *Launcher, window fyne.Window, desktop desktop.App, app fyne.App, icon *fyne.StaticResource) *SystrayManager { |
| sm := &SystrayManager{ |
| launcher: launcher, |
| window: window, |
| app: app, |
| desk: desktop, |
| icon: icon, |
| } |
| sm.setupMenu(desktop) |
| return sm |
| } |
|
|
| |
| func (sm *SystrayManager) setupMenu(desk desktop.App) { |
| sm.desk = desk |
|
|
| |
| sm.startStopItem = fyne.NewMenuItem("Start LocalAI", func() { |
| sm.toggleLocalAI() |
| }) |
|
|
| desk.SetSystemTrayIcon(sm.icon) |
|
|
| |
| sm.recreateMenu() |
| } |
|
|
| |
| func (sm *SystrayManager) toggleLocalAI() { |
| if sm.launcher.IsRunning() { |
| go func() { |
| if err := sm.launcher.StopLocalAI(); err != nil { |
| log.Printf("Failed to stop LocalAI: %v", err) |
| sm.showErrorDialog("Failed to Stop LocalAI", err.Error()) |
| } |
| }() |
| } else { |
| go func() { |
| if err := sm.launcher.StartLocalAI(); err != nil { |
| log.Printf("Failed to start LocalAI: %v", err) |
| sm.showStartupErrorDialog(err) |
| } |
| }() |
| } |
| } |
|
|
| |
| func (sm *SystrayManager) openWebUI() { |
| if !sm.launcher.IsRunning() { |
| return |
| } |
|
|
| webURL := sm.launcher.GetWebUIURL() |
| if parsedURL, err := url.Parse(webURL); err == nil { |
| sm.app.OpenURL(parsedURL) |
| } |
| } |
|
|
| |
| func (sm *SystrayManager) openDocumentation() { |
| if parsedURL, err := url.Parse("https://localai.io"); err == nil { |
| sm.app.OpenURL(parsedURL) |
| } |
| } |
|
|
| |
| func (sm *SystrayManager) updateStartStopItem() { |
| |
| sm.recreateMenu() |
| } |
|
|
| |
| func (sm *SystrayManager) recreateMenu() { |
| if sm.desk == nil { |
| return |
| } |
|
|
| |
| var actionItem *fyne.MenuItem |
| if !sm.launcher.GetReleaseManager().IsLocalAIInstalled() { |
| |
| actionItem = fyne.NewMenuItem("📥 Install Latest Version", func() { |
| sm.launcher.showDownloadLocalAIDialog() |
| }) |
| } else if sm.launcher.IsRunning() { |
| |
| actionItem = fyne.NewMenuItem("🛑 Stop LocalAI", func() { |
| sm.toggleLocalAI() |
| }) |
| } else { |
| |
| actionItem = fyne.NewMenuItem("▶️ Start LocalAI", func() { |
| sm.toggleLocalAI() |
| }) |
| } |
|
|
| menuItems := []*fyne.MenuItem{} |
|
|
| |
| status := sm.launcher.GetLastStatus() |
| statusText := sm.truncateText(status, 30) |
| statusItem := fyne.NewMenuItem("📊 Status: "+statusText, func() { |
| sm.showStatusDetails(status, "") |
| }) |
| menuItems = append(menuItems, statusItem) |
|
|
| |
| if sm.launcher.GetReleaseManager().IsLocalAIInstalled() { |
| version := sm.launcher.GetCurrentVersion() |
| versionText := sm.truncateText(version, 25) |
| versionItem := fyne.NewMenuItem("🔧 Version: "+versionText, func() { |
| sm.showStatusDetails(status, version) |
| }) |
| menuItems = append(menuItems, versionItem) |
| } |
|
|
| menuItems = append(menuItems, fyne.NewMenuItemSeparator()) |
|
|
| |
| if sm.hasUpdateAvailable { |
| updateItem := fyne.NewMenuItem("🔔 New version available ("+sm.latestVersion+")", func() { |
| sm.downloadUpdate() |
| }) |
| menuItems = append(menuItems, updateItem) |
| menuItems = append(menuItems, fyne.NewMenuItemSeparator()) |
| } |
|
|
| |
| menuItems = append(menuItems, |
| actionItem, |
| ) |
|
|
| |
| if sm.launcher.GetReleaseManager().IsLocalAIInstalled() && sm.launcher.IsRunning() { |
| menuItems = append(menuItems, |
| fyne.NewMenuItem("Open WebUI", func() { |
| sm.openWebUI() |
| }), |
| ) |
| } |
|
|
| menuItems = append(menuItems, |
| fyne.NewMenuItemSeparator(), |
| fyne.NewMenuItem("Check for Updates", func() { |
| sm.checkForUpdates() |
| }), |
| fyne.NewMenuItemSeparator(), |
| fyne.NewMenuItem("Settings", func() { |
| sm.showSettings() |
| }), |
| fyne.NewMenuItem("Show Welcome Window", func() { |
| sm.showWelcomeWindow() |
| }), |
| fyne.NewMenuItem("Open Data Folder", func() { |
| sm.openDataFolder() |
| }), |
| fyne.NewMenuItemSeparator(), |
| fyne.NewMenuItem("Documentation", func() { |
| sm.openDocumentation() |
| }), |
| fyne.NewMenuItemSeparator(), |
| fyne.NewMenuItem("Quit", func() { |
| |
| if err := sm.launcher.Shutdown(); err != nil { |
| log.Printf("Error during shutdown: %v", err) |
| } |
| sm.app.Quit() |
| }), |
| ) |
|
|
| menu := fyne.NewMenu("LocalAI", menuItems...) |
| sm.desk.SetSystemTrayMenu(menu) |
| } |
|
|
| |
| func (sm *SystrayManager) UpdateRunningState(isRunning bool) { |
| sm.updateStartStopItem() |
| } |
|
|
| |
| func (sm *SystrayManager) UpdateStatus(status string) { |
| sm.recreateMenu() |
| } |
|
|
| |
| func (sm *SystrayManager) checkForUpdates() { |
| go func() { |
| log.Printf("Checking for updates...") |
| available, version, err := sm.launcher.CheckForUpdates() |
| if err != nil { |
| log.Printf("Failed to check for updates: %v", err) |
| return |
| } |
|
|
| log.Printf("Update check result: available=%v, version=%s", available, version) |
| if available { |
| sm.hasUpdateAvailable = true |
| sm.latestVersion = version |
| sm.recreateMenu() |
| } |
| }() |
| } |
|
|
| |
| func (sm *SystrayManager) downloadUpdate() { |
| if !sm.hasUpdateAvailable { |
| return |
| } |
|
|
| |
| sm.showDownloadProgress(sm.latestVersion) |
| } |
|
|
| |
| func (sm *SystrayManager) showSettings() { |
| sm.window.Show() |
| sm.window.RequestFocus() |
| } |
|
|
| |
| func (sm *SystrayManager) showWelcomeWindow() { |
| if sm.launcher.GetUI() != nil { |
| sm.launcher.GetUI().ShowWelcomeWindow() |
| } |
| } |
|
|
| |
| func (sm *SystrayManager) openDataFolder() { |
| dataPath := sm.launcher.GetDataPath() |
| if parsedURL, err := url.Parse("file://" + dataPath); err == nil { |
| sm.app.OpenURL(parsedURL) |
| } |
| } |
|
|
| |
| func (sm *SystrayManager) NotifyUpdateAvailable(version string) { |
| sm.hasUpdateAvailable = true |
| sm.latestVersion = version |
| sm.recreateMenu() |
| } |
|
|
| |
| func (sm *SystrayManager) truncateText(text string, maxLength int) string { |
| if len(text) <= maxLength { |
| return text |
| } |
| return text[:maxLength-3] + "..." |
| } |
|
|
| |
| func (sm *SystrayManager) showStatusDetails(status, version string) { |
| fyne.DoAndWait(func() { |
| |
| statusWindow := sm.app.NewWindow("LocalAI Status Details") |
| statusWindow.Resize(fyne.NewSize(500, 400)) |
| statusWindow.CenterOnScreen() |
|
|
| |
| statusLabel := widget.NewLabel("Current Status:") |
| statusValue := widget.NewLabel(status) |
| statusValue.Wrapping = fyne.TextWrapWord |
|
|
| |
| var versionContainer fyne.CanvasObject |
| if version != "" { |
| versionLabel := widget.NewLabel("Installed Version:") |
| versionValue := widget.NewLabel(version) |
| versionValue.Wrapping = fyne.TextWrapWord |
| versionContainer = container.NewVBox(versionLabel, versionValue) |
| } |
|
|
| |
| runningLabel := widget.NewLabel("Running State:") |
| runningValue := widget.NewLabel("") |
| if sm.launcher.IsRunning() { |
| runningValue.SetText("🟢 Running") |
| } else { |
| runningValue.SetText("🔴 Stopped") |
| } |
|
|
| |
| webuiLabel := widget.NewLabel("WebUI URL:") |
| webuiValue := widget.NewLabel(sm.launcher.GetWebUIURL()) |
| webuiValue.Wrapping = fyne.TextWrapWord |
|
|
| |
| logsLabel := widget.NewLabel("Recent Logs:") |
| logsText := widget.NewMultiLineEntry() |
| logsText.SetText(sm.launcher.GetRecentLogs()) |
| logsText.Wrapping = fyne.TextWrapWord |
| logsText.Disable() |
|
|
| |
| closeButton := widget.NewButton("Close", func() { |
| statusWindow.Close() |
| }) |
|
|
| refreshButton := widget.NewButton("Refresh", func() { |
| |
| statusValue.SetText(sm.launcher.GetLastStatus()) |
|
|
| |
| |
|
|
| if sm.launcher.IsRunning() { |
| runningValue.SetText("🟢 Running") |
| } else { |
| runningValue.SetText("🔴 Stopped") |
| } |
| logsText.SetText(sm.launcher.GetRecentLogs()) |
| }) |
|
|
| openWebUIButton := widget.NewButton("Open WebUI", func() { |
| sm.openWebUI() |
| }) |
|
|
| |
| buttons := container.NewHBox(closeButton, refreshButton, openWebUIButton) |
|
|
| |
| infoItems := []fyne.CanvasObject{ |
| statusLabel, statusValue, |
| widget.NewSeparator(), |
| } |
|
|
| |
| if versionContainer != nil { |
| infoItems = append(infoItems, versionContainer, widget.NewSeparator()) |
| } |
|
|
| infoItems = append(infoItems, |
| runningLabel, runningValue, |
| widget.NewSeparator(), |
| webuiLabel, webuiValue, |
| ) |
|
|
| infoContainer := container.NewVBox(infoItems...) |
|
|
| content := container.NewVBox( |
| infoContainer, |
| widget.NewSeparator(), |
| logsLabel, |
| logsText, |
| widget.NewSeparator(), |
| buttons, |
| ) |
|
|
| statusWindow.SetContent(content) |
| statusWindow.Show() |
| }) |
| } |
|
|
| |
| func (sm *SystrayManager) showErrorDialog(title, message string) { |
| fyne.DoAndWait(func() { |
| dialog.ShowError(fmt.Errorf("%s", message), sm.window) |
| }) |
| } |
|
|
| |
| func (sm *SystrayManager) showStartupErrorDialog(err error) { |
| fyne.DoAndWait(func() { |
| |
| logs := sm.launcher.GetRecentLogs() |
|
|
| |
| errorWindow := sm.app.NewWindow("LocalAI Startup Failed") |
| errorWindow.Resize(fyne.NewSize(600, 500)) |
| errorWindow.CenterOnScreen() |
|
|
| |
| errorLabel := widget.NewLabel(fmt.Sprintf("Failed to start LocalAI:\n%s", err.Error())) |
| errorLabel.Wrapping = fyne.TextWrapWord |
|
|
| |
| logsLabel := widget.NewLabel("Process Logs:") |
| logsText := widget.NewMultiLineEntry() |
| logsText.SetText(logs) |
| logsText.Wrapping = fyne.TextWrapWord |
| logsText.Disable() |
|
|
| |
| closeButton := widget.NewButton("Close", func() { |
| errorWindow.Close() |
| }) |
|
|
| retryButton := widget.NewButton("Retry", func() { |
| errorWindow.Close() |
| |
| go func() { |
| if retryErr := sm.launcher.StartLocalAI(); retryErr != nil { |
| sm.showStartupErrorDialog(retryErr) |
| } |
| }() |
| }) |
|
|
| openLogsButton := widget.NewButton("Open Logs Folder", func() { |
| sm.openDataFolder() |
| }) |
|
|
| |
| buttons := container.NewHBox(closeButton, retryButton, openLogsButton) |
| content := container.NewVBox( |
| errorLabel, |
| widget.NewSeparator(), |
| logsLabel, |
| logsText, |
| widget.NewSeparator(), |
| buttons, |
| ) |
|
|
| errorWindow.SetContent(content) |
| errorWindow.Show() |
| }) |
| } |
|
|
| |
| func (sm *SystrayManager) showDownloadProgress(version string) { |
| |
| progressWindow := sm.app.NewWindow("Downloading LocalAI Update") |
| progressWindow.Resize(fyne.NewSize(400, 250)) |
| progressWindow.CenterOnScreen() |
|
|
| |
| progressBar := widget.NewProgressBar() |
| progressBar.SetValue(0) |
|
|
| |
| statusLabel := widget.NewLabel("Preparing download...") |
|
|
| |
| releaseNotesButton := widget.NewButton("View Release Notes", func() { |
| releaseNotesURL, err := sm.launcher.githubReleaseNotesURL(version) |
| if err != nil { |
| log.Printf("Failed to parse URL: %v", err) |
| return |
| } |
|
|
| sm.app.OpenURL(releaseNotesURL) |
| }) |
|
|
| |
| progressContainer := container.NewVBox( |
| widget.NewLabel(fmt.Sprintf("Downloading LocalAI version %s", version)), |
| progressBar, |
| statusLabel, |
| widget.NewSeparator(), |
| releaseNotesButton, |
| ) |
|
|
| progressWindow.SetContent(progressContainer) |
| progressWindow.Show() |
|
|
| |
| go func() { |
| err := sm.launcher.DownloadUpdate(version, func(progress float64) { |
| |
| fyne.Do(func() { |
| progressBar.SetValue(progress) |
| percentage := int(progress * 100) |
| statusLabel.SetText(fmt.Sprintf("Downloading... %d%%", percentage)) |
| }) |
| }) |
|
|
| |
| fyne.Do(func() { |
| if err != nil { |
| statusLabel.SetText(fmt.Sprintf("Download failed: %v", err)) |
| |
| dialog.ShowError(err, progressWindow) |
| } else { |
| statusLabel.SetText("Download completed successfully!") |
| progressBar.SetValue(1.0) |
|
|
| |
| dialog.ShowConfirm("Update Downloaded", |
| "LocalAI has been updated successfully. Please restart the launcher to use the new version.", |
| func(restart bool) { |
| if restart { |
| sm.app.Quit() |
| } |
| progressWindow.Close() |
| }, progressWindow) |
| } |
| }) |
|
|
| |
| if err == nil { |
| sm.hasUpdateAvailable = false |
| sm.latestVersion = "" |
| sm.recreateMenu() |
| } |
| }() |
| } |
|
|