Apps lassen sich in Kategorien gruppieren

Ein Feld an der App, kein zweites Datenmodell: Eine Kategorie besteht genau
so lange, wie eine App sie traegt. Damit gibt es nichts anzulegen, nichts zu
loeschen und keine zweite Reihenfolge, die mit der bestehenden aus dem Tritt
geraten koennte.

Die Reihenfolge der Ueberschriften ist die ihres ersten Auftretens in der
Liste -- also ordnen dieselben Pfeile wie bisher beides. Am Rand einer Gruppe
haengt der Pfeil die App in die Nachbargruppe um, statt zu tauschen: derselbe
Knopf zum Sortieren und zum Umkategorisieren, und keine App kann zwischen
zwei Gruppen liegen bleiben.

Eingesammelt wird im Server, beim Schreiben wie beim Lesen. Das war nicht der
erste Entwurf: Zuerst sortierte die Oberflaeche und die HoneyBar setzte ihre
Ueberschriften beim Durchlaufen -- prompt stand eine App ohne Kategorie in der
HoneyBar unter der letzten Ueberschrift und auf dem Schreibtisch ganz oben.
Bei zwei Lesern derselben Liste gehoert die Reihenfolge dorthin, wo die Liste
liegt.

Ueberschriften erscheinen, wo man sucht: Kacheln, Schublade beider Leisten,
Programme-Fenster. Nicht im App-Streifen (32 Pixel) und nicht bei den
Schreibtischsymbolen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-09-12 22:12:03 +02:00
1 parent 3b47003492
commit 501c3b8930
8 files changed
+394 -16

No files matched your search

+48 -2
View File
@@ -33,6 +33,13 @@ type App struct {
Subtitle string `json:"subtitle,omitempty"`
URL string `json:"url"`
Glyph string `json:"glyph,omitempty"`
// Die Kategorie ist freier Text, kein Verweis auf eine Liste von
// Kategorien — es gibt keine. Eine Kategorie besteht genau so lange, wie
// eine App sie trägt; damit gibt es nichts anzulegen, nichts zu löschen
// und vor allem keine zweite Reihenfolge, die mit dieser aus dem Tritt
// geraten könnte. Die Reihenfolge der Überschriften ergibt sich aus dem
// ersten Auftreten in dieser Liste.
Kategorie string `json:"kategorie,omitempty"`
}
type User struct {
@@ -94,6 +101,10 @@ func Open(path string) (*Store, error) {
if s.d.Apps == nil {
s.d.Apps = []App{}
}
// Auch beim Lesen einsammeln: honeyos.json darf von Hand bearbeitet werden
// (das ist der halbe Grund für eine Datei statt einer Datenbank), und dann
// stehen Kategorien schnell durcheinander.
s.d.Apps = sortiereNachKategorie(s.d.Apps)
if s.d.Users == nil {
s.d.Users = map[string]*User{}
}
@@ -137,6 +148,7 @@ func (s *Store) SetApps(apps []App) error {
a.Subtitle = strings.TrimSpace(a.Subtitle)
a.URL = strings.TrimSpace(a.URL)
a.Glyph = strings.TrimSpace(a.Glyph)
a.Kategorie = strings.TrimSpace(a.Kategorie)
if !idPattern.MatchString(a.ID) {
return fmt.Errorf("App %d: ID muss klein geschrieben sein und mit Buchstabe oder Ziffer beginnen", i+1)
@@ -156,7 +168,7 @@ func (s *Store) SetApps(apps []App) error {
if a.Glyph != "" && !knownGlyphs[a.Glyph] {
a.Glyph = ""
}
for _, f := range []string{a.Name, a.Subtitle, a.URL} {
for _, f := range []string{a.Name, a.Subtitle, a.URL, a.Kategorie} {
if len(f) > maxFieldLen {
return fmt.Errorf("App %q: ein Feld ist länger als %d Zeichen", a.ID, maxFieldLen)
}
@@ -166,10 +178,44 @@ func (s *Store) SetApps(apps []App) error {
s.mu.Lock()
defer s.mu.Unlock()
s.d.Apps = cleaned
s.d.Apps = sortiereNachKategorie(cleaned)
return s.save()
}
// sortiereNachKategorie sammelt Apps derselben Kategorie zusammen, ohne die
// Reihenfolge innerhalb einer Gruppe oder die der Gruppen zu ändern: Die
// Gruppen stehen in der Reihenfolge ihres ersten Auftretens, die namenlose
// immer zuerst.
//
// Das gehört hierher und nicht in die Oberfläche, weil es mehrere Leser gibt:
// der Schreibtisch, die HoneyBar in fremden Apps, irgendwann ein weiterer.
// Sortierte jeder für sich, sähe dieselbe Liste an zwei Stellen verschieden
// aus — genau das ist passiert, als die HoneyBar die Überschriften einfach
// beim Durchlaufen setzte und eine App ohne Kategorie am Ende unter der
// letzten Überschrift landete.
func sortiereNachKategorie(apps []App) []App {
ohne := make([]App, 0, len(apps))
reihenfolge := []string{}
nach := map[string][]App{}
for _, a := range apps {
if a.Kategorie == "" {
ohne = append(ohne, a)
continue
}
if _, da := nach[a.Kategorie]; !da {
reihenfolge = append(reihenfolge, a.Kategorie)
}
nach[a.Kategorie] = append(nach[a.Kategorie], a)
}
out := ohne
for _, k := range reihenfolge {
out = append(out, nach[k]...)
}
return out
}
func (s *Store) User(sub string) User {
s.mu.RLock()
defer s.mu.RUnlock()
+54
View File
@@ -0,0 +1,54 @@
package store
import (
"path/filepath"
"testing"
)
// Die Reihenfolge der Apps ist die Anzeige-Reihenfolge — für den Schreibtisch
// wie für die HoneyBar in fremden Apps. Deshalb legt der Server sie fest und
// nicht jeder Leser für sich: Als die HoneyBar ihre Überschriften noch selbst
// beim Durchlaufen setzte, landete eine App ohne Kategorie am Ende unter der
// letzten Überschrift, während der Schreibtisch sie oben zeigte.
func TestKategorienWerdenEingesammelt(t *testing.T) {
s, err := Open(filepath.Join(t.TempDir(), "honeyos.json"))
if err != nil {
t.Fatal(err)
}
// Absichtlich durcheinander: zwei Gruppen verschränkt, die App ohne
// Kategorie zuletzt.
err = s.SetApps([]App{
{ID: "a", URL: "https://e.test/a", Kategorie: "Apps"},
{ID: "g", URL: "https://e.test/g", Kategorie: "Dienste"},
{ID: "i", URL: "https://e.test/i", Kategorie: "Apps"},
{ID: "p", URL: "https://e.test/p", Kategorie: "Dienste"},
{ID: "l", URL: "https://e.test/l"},
})
if err != nil {
t.Fatal(err)
}
will := []string{"l", "a", "i", "g", "p"}
hat := []string{}
for _, a := range s.Apps() {
hat = append(hat, a.ID)
}
for i := range will {
if i >= len(hat) || hat[i] != will[i] {
t.Fatalf("Reihenfolge %v, erwartet %v — ohne Kategorie zuerst, dann die Gruppen in der Reihenfolge ihres ersten Auftretens", hat, will)
}
}
// Und nach dem Neuladen dasselbe: Wer honeyos.json von Hand bearbeitet,
// bekommt seine Kategorien ebenfalls eingesammelt.
s2, err := Open(s.path)
if err != nil {
t.Fatal(err)
}
for i, a := range s2.Apps() {
if a.ID != will[i] {
t.Fatalf("nach dem Neuladen %q an Stelle %d, erwartet %q", a.ID, i, will[i])
}
}
}