package main import ( "fmt" "image/color" "strconv" "time" "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/data/binding" "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/driver/desktop" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/storage" "fyne.io/fyne/v2/widget" ) // App-Metadaten const ( appVersion = "1.0.2" appAuthor = "Thomas Krampe" appTitle = "Countdown Pro" appCopy = "© 2026 Thoma Krampe. Alle Rechte vorbehalten." ) // Speicher-Keys const ( prefLastMinutes = "last_minutes" prefLastImage = "last_image_uri" prefLastMode = "last_mode" ) // LargeTimer Komponente type LargeTimer struct { widget.BaseWidget text *canvas.Text } func NewLargeTimer(data binding.String, colorData binding.Untyped) *LargeTimer { t := &LargeTimer{text: canvas.NewText("", color.White)} t.text.TextSize = 150 t.text.TextStyle = fyne.TextStyle{Bold: true} t.ExtendBaseWidget(t) data.AddListener(binding.NewDataListener(func() { val, _ := data.Get() t.text.Text = val t.Refresh() })) colorData.AddListener(binding.NewDataListener(func() { raw, _ := colorData.Get() if c, ok := raw.(color.Color); ok { t.text.Color = c t.Refresh() } })) return t } func (t *LargeTimer) CreateRenderer() fyne.WidgetRenderer { return widget.NewSimpleRenderer(container.NewCenter(t.text)) } func main() { myApp := app.NewWithID("com.thomas.countdown.pro") myWindow := myApp.NewWindow(appTitle) myWindow.Resize(fyne.NewSize(550, 450)) // --- About Menü für macOS --- setupMenu(myApp, myWindow) // --- Preferences laden --- prefs := myApp.Preferences() savedMins := prefs.StringWithFallback(prefLastMinutes, "15") savedImg := prefs.String(prefLastImage) savedMode := prefs.StringWithFallback(prefLastMode, "Minuten-Countdown") // UI Setup modeSelect := widget.NewSelect([]string{"Minuten-Countdown", "Ziel-Datum & Uhrzeit"}, nil) modeSelect.SetSelected(savedMode) minutesInput := widget.NewEntry() minutesInput.SetText(savedMins) dateInput := widget.NewEntry() dateInput.SetText(time.Now().Add(1 * time.Hour).Format("02.01.2006 15:04")) pathLabel := widget.NewLabel("Kein Bild ausgewählt") var selectedImageURI fyne.URI if savedImg != "" { if u, err := storage.ParseURI(savedImg); err == nil { selectedImageURI = u pathLabel.SetText(u.Name()) } } updateUI := func(mode string) { if mode == "Minuten-Countdown" { minutesInput.Show() dateInput.Hide() } else { minutesInput.Hide() dateInput.Show() } } updateUI(savedMode) modeSelect.OnChanged = updateUI filePickerBtn := widget.NewButton("Hintergrundbild wählen", func() { fd := dialog.NewFileOpen(func(reader fyne.URIReadCloser, err error) { if err != nil || reader == nil { return } selectedImageURI = reader.URI() pathLabel.SetText(selectedImageURI.Name()) prefs.SetString(prefLastImage, selectedImageURI.String()) }, myWindow) fd.SetFilter(storage.NewExtensionFileFilter([]string{".jpg", ".png", ".jpeg"})) fd.Show() }) startBtn := widget.NewButton("Countdown starten", func() { var endTime time.Time prefs.SetString(prefLastMode, modeSelect.Selected) if modeSelect.Selected == "Minuten-Countdown" { prefs.SetString(prefLastMinutes, minutesInput.Text) mins, _ := strconv.Atoi(minutesInput.Text) endTime = time.Now().Add(time.Duration(mins) * time.Minute) } else { parsedTime, err := time.ParseInLocation("02.01.2006 15:04", dateInput.Text, time.Local) if err != nil { dialog.ShowError(fmt.Errorf("Format: TT.MM.JJJJ HH:MM"), myWindow) return } endTime = parsedTime } if selectedImageURI == nil { dialog.ShowError(fmt.Errorf("Bitte Bild wählen"), myWindow) return } showCountdown(myApp, myWindow, endTime, selectedImageURI) }) startBtn.Importance = widget.HighImportance myWindow.SetContent(container.NewPadded(container.NewVBox( widget.NewLabelWithStyle(appTitle, fyne.TextAlignCenter, fyne.TextStyle{Bold: true}), modeSelect, minutesInput, dateInput, layout.NewSpacer(), filePickerBtn, pathLabel, layout.NewSpacer(), startBtn, ))) myWindow.CenterOnScreen() myWindow.ShowAndRun() } func setupMenu(a fyne.App, w fyne.Window) { if desk, ok := a.(desktop.App); ok { aboutItem := fyne.NewMenuItem("Über "+appTitle, func() { dialog.ShowCustom("Info", "Schließen", container.NewVBox( widget.NewLabelWithStyle(appTitle, fyne.TextAlignCenter, fyne.TextStyle{Bold: true}), widget.NewLabelWithStyle("Version "+appVersion, fyne.TextAlignCenter, fyne.TextStyle{Italic: true}), widget.NewLabelWithStyle("Autor: "+appAuthor, fyne.TextAlignCenter, fyne.TextStyle{}), widget.NewLabelWithStyle(appCopy, fyne.TextAlignCenter, fyne.TextStyle{}), ), w) }) menu := fyne.NewMenu(appTitle, aboutItem) desk.SetSystemTrayMenu(menu) } } func showCountdown(a fyne.App, w fyne.Window, endTime time.Time, imgURI fyne.URI) { bgImage := canvas.NewImageFromURI(imgURI) bgImage.FillMode = canvas.ImageFillStretch timerString := binding.NewString() colorBinding := binding.NewUntyped() colorBinding.Set(color.White) timerDisplay := NewLargeTimer(timerString, colorBinding) exitBtn := widget.NewButton("Beenden", func() { w.SetFullScreen(false) // Statt nur das Fenster zu schließen, beenden wir die ganze App: a.Quit() }) content := container.NewMax( bgImage, container.NewCenter(timerDisplay), container.NewVBox(container.NewHBox(layout.NewSpacer(), exitBtn), layout.NewSpacer()), ) w.SetContent(content) w.SetFullScreen(true) go func() { ticker := time.NewTicker(time.Second) defer ticker.Stop() notified := false for range ticker.C { remaining := time.Until(endTime) if remaining <= 0 { timerString.Set("00:00:00") colorBinding.Set(color.RGBA{R: 255, G: 0, B: 0, A: 255}) if !notified { a.SendNotification(fyne.NewNotification("Zeit abgelaufen!", "Das Event ist jetzt.")) notified = true } return } if remaining.Seconds() < 60 { colorBinding.Set(color.RGBA{R: 255, G: 0, B: 0, A: 255}) } else { colorBinding.Set(color.White) } d := int(remaining.Hours()) / 24 h := int(remaining.Hours()) % 24 m := int(remaining.Minutes()) % 60 s := int(remaining.Seconds()) % 60 if d > 0 { timerString.Set(fmt.Sprintf("%dT %02d:%02d:%02d", d, h, m, s)) } else { timerString.Set(fmt.Sprintf("%02d:%02d:%02d", h, m, s)) } } }() }