summaryrefslogtreecommitdiff
path: root/main.go
blob: ea16eeee685bc51c35d4b39271f56e588ceb4c6a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
package main

import (
	"errors"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"time"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/dialog"
	"fyne.io/fyne/v2/widget"
)

const (
	appID           = "org.buetow.quicklogger"
	placeholderText = "Enter text here..."
	maxTextLength   = 5000 // Limit text length to prevent performance issues
)

const defaultDirectory = "."

var windowSize = fyne.NewSize(400, 100)

// logEntry writes text to a timestamped markdown file in dir.
// Separates persistence logic from the UI so it can be tested independently.
func logEntry(dir, text string) error {
	filename := filepath.Join(dir, "ql-"+time.Now().Format("060102-150405")+".md")
	return os.WriteFile(filename, []byte(text), 0o644)
}

// newInputWidget creates the multi-line text entry with platform-appropriate
// wrapping and row count settings.
func newInputWidget() *widget.Entry {
	input := widget.NewMultiLineEntry()
	input.SetPlaceHolder(placeholderText)

	// On mobile, disable word wrapping and reduce visible rows to limit
	// expensive recalculations and rendering area.
	if fyne.CurrentDevice().IsMobile() {
		input.Wrapping = fyne.TextWrapOff
		input.SetMinRowsVisible(10)
	} else {
		input.Wrapping = fyne.TextWrapWord
		input.SetMinRowsVisible(30)
	}

	return input
}

func createPreferenceWindow(a fyne.App) fyne.Window {
	window := a.NewWindow("Preferences")
	directoryPreference := widget.NewEntry()
	directoryPreference.SetText(a.Preferences().StringWithFallback("Directory", defaultDirectory))

	window.SetContent(container.NewVBox(
		container.NewVBox(
			widget.NewLabel("Directory:"),
			directoryPreference,
		),
		container.NewHBox(
			widget.NewButton("Save", func() {
				a.Preferences().SetString("Directory", directoryPreference.Text)
				window.Hide()
			}),
		)))
	window.Resize(windowSize)

	return window
}

func createMainWindow(a fyne.App) fyne.Window {
	window := a.NewWindow("Quick logger")
	input := newInputWidget()
	charCount := widget.NewLabel("0 chars")

	// Track whether the length warning has been shown so we don't fire a
	// modal dialog on every keystroke above the limit.
	warnShown := false
	input.OnChanged = func(text string) {
		charCount.SetText(fmt.Sprintf("%d chars", len(text)))
		if len(text) > maxTextLength && !warnShown {
			warnShown = true
			dialog.ShowInformation("Text Limit",
				fmt.Sprintf("Text is getting long (%d chars). Consider logging to avoid performance issues.", len(text)),
				window)
		} else if len(text) <= maxTextLength {
			warnShown = false
		}
	}

	// resetInput clears the text entry and character count.
	resetInput := func() {
		input.SetText("")
		charCount.SetText("0 chars")
	}

	logTextButton := widget.NewButton("Log text", func() {
		dir := a.Preferences().StringWithFallback("Directory", defaultDirectory)
		if err := logEntry(dir, input.Text); err != nil {
			dialog.ShowError(err, window)
			return
		}
		resetInput()
	})

	clearButton := widget.NewButton("Clear", func() {
		resetInput()
		window.Canvas().Focus(input)
	})

	// loadSharedText reads Android-shared text from cache and populates the input.
	// Used both at startup and when the app returns to the foreground.
	// A missing cache file is expected (no share pending); real errors are logged.
	loadSharedText := func() {
		txt, err := readSharedFromCache()
		if err != nil {
			if !errors.Is(err, os.ErrNotExist) {
				log.Printf("readSharedFromCache: %v", err)
			}
			return
		}
		if txt != "" {
			input.SetText(txt)
			charCount.SetText(fmt.Sprintf("%d chars", len(txt)))
			window.Canvas().Focus(input)
		}
	}

	if fyne.CurrentDevice().IsMobile() {
		loadSharedText()
	}

	window.SetContent(container.NewVBox(
		input,
		container.NewHBox(
			logTextButton,
			clearButton,
			widget.NewButton("Preferences", func() {
				createPreferenceWindow(a).Show()
			}),
			charCount,
		),
	))
	window.Resize(windowSize)
	window.Canvas().Focus(input)

	// On Android, also check for new shared text whenever app returns to foreground.
	if lc := a.Lifecycle(); lc != nil {
		lc.SetOnEnteredForeground(loadSharedText)
	}

	return window
}

func main() {
	createMainWindow(app.NewWithID(appID)).ShowAndRun()
}