summaryrefslogtreecommitdiff
path: root/main.go
blob: d913835455497820e1eb55b591710f8339d4ebf4 (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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
package main

import (
	"errors"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"strings"
	"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)

type sharedTextLoadMode int

const (
	sharedTextLoadPrefill sharedTextLoadMode = iota
	sharedTextLoadAutoLog
)

// 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)
}

// prepareSharedTextLoad validates shared text and decides whether to prefill
// the editor or log the entry immediately.
func prepareSharedTextLoad(text string, autoLog bool) (sharedTextLoadMode, string, bool) {
	if strings.TrimSpace(text) == "" {
		return sharedTextLoadPrefill, "", false
	}
	if autoLog {
		return sharedTextLoadAutoLog, text, true
	}
	return sharedTextLoadPrefill, text, true
}

// 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))
	autoLogSharedTextPreference := widget.NewCheck("Auto-log shared text", nil)
	autoLogSharedTextPreference.SetChecked(a.Preferences().BoolWithFallback("AutoLogSharedText", false))

	window.SetContent(container.NewVBox(
		container.NewVBox(
			widget.NewLabel("Directory:"),
			directoryPreference,
		),
		autoLogSharedTextPreference,
		container.NewHBox(
			widget.NewButton("Save", func() {
				a.Preferences().SetString("Directory", directoryPreference.Text)
				a.Preferences().SetBool("AutoLogSharedText", autoLogSharedTextPreference.Checked)
				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
	loadingSharedText := false
	input.OnChanged = func(text string) {
		charCount.SetText(fmt.Sprintf("%d chars", len(text)))
		if loadingSharedText {
			warnShown = false
			return
		}
		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
		}
		mode, sharedText, ok := prepareSharedTextLoad(txt, a.Preferences().BoolWithFallback("AutoLogSharedText", false))
		if !ok {
			return
		}
		loadingSharedText = true
		defer func() {
			loadingSharedText = false
		}()
		if mode == sharedTextLoadAutoLog {
			dir := a.Preferences().StringWithFallback("Directory", defaultDirectory)
			if err := logEntry(dir, sharedText); err != nil {
				dialog.ShowError(err, window)
				return
			}
			dialog.ShowInformation("Logged", "Shared text has been logged.", window)
			resetInput()
			return
		}
		input.SetText(sharedText)
		charCount.SetText(fmt.Sprintf("%d chars", len(sharedText)))
		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()
}