240 lines
6.3 KiB
Go
240 lines
6.3 KiB
Go
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))
|
|
}
|
|
}
|
|
}()
|
|
} |