M README.md +1 -1
@@ 31,7 31,7 @@ Forage connects to your Mealie instance
- Forage is built with Go and Fyne. Executables on desktop are 12MB, and Android APKs are 80MB. IMO this is pretty huge, but it's the price of the tooling.
- Fyne is like Swing: widgets and interactions are bespoke, not calls down to the underlying windowing widget GUI. You won't see a native-looking UI.
-- Forage minimizes network calls, and does not check if the list has changed on the server before writing your changes back to it. If you have two people actively changing a list, Forage will overwrite their changes.
+- There's a bug, on Android at least, where backspacing in text fields deletes two characters. The bug's been reported, and is (IMO) not a show-stopper, but be aware.
That said, it's not (IMO) ugly, and Fyne is surprisingly light on the battery. I haven't seen it yet near the top of battery use on my phone, or at the top of `top` on my laptop.
M db.go +0 -1
@@ 23,7 23,6 @@ func (n *NotFound) Error() string {
return fmt.Sprintf("list %s not found in %s", n.listName, n.cacheName)
}
-
// DB abstracts local list storage objects.
type DB interface {
// TODO (D) The DB exhibits feature envy; some of this should be moved into Store
A => excludes.txt +36 -0
@@ 0,0 1,36 @@
+tsp
+teaspoon
+teaspoons
+tbsp
+tablespoon
+tablespoons
+floz
+fluid ounce
+fluid ounces
+oz
+ounce
+ounces
+c
+cup
+cups
+pt
+pint
+pints
+qt
+quart
+quarts
+gal
+gallon
+gallons
+lb
+pound
+pounds
+g
+gram
+grams
+pinch
+pinches
+mL
+L
+fresh
+can
M gui.go +9 -5
@@ 19,18 19,18 @@ import (
// TODO (D) "Delete" items and lists
// TODO (A) Edit items and lists @UI
-// TODO (B) Setting for only sync on wifi @Prefs
+// TODO (C) Setting for only sync on wifi @Prefs
// TODO (E) Systray icon @UI
-// TODO (A) Show sync'd status, progress @UI
+// TODO (B) Show sync'd status, progress @UI
// TODO Never online folks will have endlessly increasing queues; turning off queues would prevent ever syncing. Solve this.
const (
serverURLC = "ServerUrl"
userNameC = "UserName"
- passwordC = "Password"
+ passwordC = "Password"
selectedTabC = "SelectedTab"
selectedListC = "SelectedList"
- onlineC = "Online"
+ onlineC = "Online"
serverTabC = 0
listsTabC = 1
@@ 180,6 180,7 @@ func Gui() {
}
},
)
+ // TODO add help text to list entry
addList := widget.NewEntry()
addListλ := func(val string) {
addList.SetText("")
@@ 193,6 194,7 @@ func Gui() {
lists.Refresh()
}
addList.OnSubmitted = addListλ
+ // TODO disable the addList button if the text field is empty
addListButton := widget.NewButtonWithIcon("", theme.ContentAddIcon(), func() { addListλ(addList.Text) })
addListRow := container.New(layout.NewBorderLayout(nil, nil, nil, addListButton), addList, addListButton)
listsContent := container.New(layout.NewBorderLayout(addListRow, nil, nil, nil), addListRow, lists)
@@ 200,7 202,7 @@ func Gui() {
///////////////////////////////////////////////////////////////////////
// SHOP ITEMS TAB
///////////////////////////////////////////////////////////////////////
- // TODO (A) Add Item quantity @UI
+ // TODO (B) Add Item quantity @UI
itemsW = widget.NewList(
func() int {
return len(cachedList.Items)
@@ 223,6 225,7 @@ func Gui() {
c.Refresh()
},
)
+ // TODO add help text to item entry
addItem := widget.NewEntry()
addItemλ := func(val string) {
addItem.SetText("")
@@ 241,6 244,7 @@ func Gui() {
itemsW.Refresh()
}
addItem.OnSubmitted = addItemλ
+ // TODO disable add(item)Button when item text is empty
addButton := widget.NewButtonWithIcon("", theme.ContentAddIcon(), func() { addItemλ(addItem.Text) })
addRow := container.New(layout.NewBorderLayout(nil, nil, nil, addButton), addItem, addButton)
itemsContent := container.New(layout.NewBorderLayout(addRow, nil, nil, nil), addRow, itemsW)
M lists.go +31 -2
@@ 4,9 4,30 @@ package main
// data from either a server or a local database.
import (
+ _ "embed"
"strings"
)
+//go:embed "excludes.txt"
+var _excludes string
+var excludes []string
+var replacer *strings.Replacer
+
+func init() {
+ us := strings.Fields(_excludes)
+ excludes = make([]string, len(us))
+ for i, u := range us {
+ excludes[i] = strings.ToUpper(u) + " "
+ }
+ trimChars := "0123456789-()/½⅓¼⅕⅙⅐⅛⅑⅔⅖¾⅗⅘"
+ tcs := make([]string, len(trimChars)*2)
+ for i, c := range trimChars {
+ tcs[i*2] = string(c)
+ tcs[i*2+1] = ""
+ }
+ replacer = strings.NewReplacer(tcs...)
+}
+
// List represents a single Mealie shopping list
type List struct {
// Name is the name Mealie displays for the list
@@ 33,13 54,21 @@ func (l List) Less(i, j int) bool {
li := l.Items[i]
lit := li.Text
if it, ok = sortableText[lit]; !ok {
- it = strings.ToUpper(strings.Trim(lit, "0123456789 "))
+ it = replacer.Replace(strings.ToUpper(lit))
+ it = strings.TrimLeft(it, " ")
+ for _, u := range excludes {
+ it = strings.TrimPrefix(it, u)
+ }
sortableText[lit] = it
}
lj := l.Items[j]
ljt := lj.Text
if jt, ok = sortableText[ljt]; !ok {
- jt = strings.ToUpper(strings.Trim(ljt, "0123456789 "))
+ jt = replacer.Replace(strings.ToUpper(ljt))
+ jt = strings.TrimLeft(jt, " ")
+ for _, u := range excludes {
+ jt = strings.TrimPrefix(jt, u)
+ }
sortableText[ljt] = jt
}
if !li.Checked {