Initial commit

This commit is contained in:
2026-02-11 16:28:52 +01:00
parent 1a66229833
commit 2c121305f1
5 changed files with 360 additions and 0 deletions

240
main.go Normal file
View File

@@ -0,0 +1,240 @@
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))
}
}
}()
}