Initial commit
This commit is contained in:
240
main.go
Normal file
240
main.go
Normal 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))
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
Reference in New Issue
Block a user