Sorting now excludes common units, more special characters, and UTF fractions.
5 files changed, 77 insertions(+), 9 deletions(-)

M README.md
M db.go
A => excludes.txt
M gui.go
M lists.go
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 {