feat(calendar): Wiederholungsmuster, Terminfarbe synchronisierbar, Selbsttest
Terminfarbe: Sie kam auf dem Telefon nie an. RFC 7986 schreibt für COLOR
einen CSS3-*Namen* vor, wir schrieben Hex – DAVx5 liest die Eigenschaft
über eine Namenstabelle und verwirft alles andere, also blieb der Termin
im Samsung-Kalender farblos. Jetzt beides nebeneinander:
COLOR:tomato für fremde Clients (damit die Farbe ankommt)
X-AETHER-COLOR:#ea4335 für die eigene Palette (damit sie genau bleibt)
Beim Lesen gewinnt der genaue Wert; ein von außen eingefärbter Termin
wird über die Namenstabelle übersetzt. Nächstgelegener Ton per Abstand im
RGB-Würfel (color.go).
Wiederholungen können jetzt mehr als "alle N":
- wöchentlich an ausgewählten Wochentagen (Mo–So, Mehrfachauswahl). Der
Wochentag des Starts bleibt fest angehakt – eine Reihe, die ihren
eigenen ersten Termin auslässt, ist keine.
- monatlich und jährlich wahlweise am festen Kalendertag (am 4.) oder am
n-ten Wochentag (am 1. Freitag). Beide Angaben leitet der Server aus
dem Startdatum ab, statt sie getrennt entgegenzunehmen – so kann die
Regel gar nicht erst auf einen anderen Tag zeigen als der Termin.
- Der fünfte Wochentag eines Monats wird bewusst zu "letzter": Einen
fünften Freitag hat nicht jeder Monat, BYDAY=5FR ließe die Reihe sonst
in etwa jedem zweiten Monat stillschweigend ausfallen.
Regeln, die das Formular weiterhin nicht abbildet, bleiben lesbar, aber
nicht bearbeitbar (409) – unverändert.
Dazu scripts/kalender-selbsttest.py, 19 Prüfungen gegen die laufende
Instanz, in TESTING.md verlinkt. Alle grün, unter anderem:
am 1. Freitag monatlich -> 04.09. 02.10. 06.11. 04.12.
am 1. Freitag im September -> 2026-09-04, 2027-09-03
Mo+Fr wöchentlich -> 07./11./14./18./21./25.09.
ganztägig am 3. -> bleibt der 3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
79a7cc6a3d
commit
0c9cc69493
7 files changed
+608
-23
No files matched your search
@@ -0,0 +1,111 @@
|
||||
package calendar
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Terminfarben und die Synchronisierung.
|
||||
//
|
||||
// RFC 7986 schreibt für COLOR einen **CSS3-Farbnamen** vor ("tomato"), keinen
|
||||
// Hex-Wert. Genau daran hängt, ob die Farbe eines Termins auf dem Telefon
|
||||
// ankommt: DAVx5 liest die Eigenschaft über eine Namenstabelle und wirft
|
||||
// alles weg, was es dort nicht findet – ein "#ea4335" landet also nirgends,
|
||||
// und im Samsung-Kalender bliebe der Termin farblos.
|
||||
//
|
||||
// Deshalb zwei Eigenschaften nebeneinander:
|
||||
//
|
||||
// COLOR:tomato – der Standard, damit fremde Clients es verstehen
|
||||
// X-AETHER-COLOR:#ea4335 – der genaue Wert, damit *hier* die Palette stimmt
|
||||
//
|
||||
// Beim Lesen gewinnt X-AETHER-COLOR, wenn vorhanden; sonst wird der Name in
|
||||
// Hex zurückübersetzt. So geht in beide Richtungen nichts verloren: Ein
|
||||
// Termin, den das Telefon einfärbt, kommt hier mit seiner Farbe an, und
|
||||
// unsere Palette bleibt auf dem Telefon erkennbar – wenn auch auf den
|
||||
// nächstgelegenen benannten Ton gerundet.
|
||||
const (
|
||||
propColor = "COLOR"
|
||||
propColorX = "X-AETHER-COLOR"
|
||||
)
|
||||
|
||||
// css3Colors ist eine Auswahl aus der CSS3-Namensliste – über den Farbkreis
|
||||
// verteilt, dazu die Töne, die Kalender-Clients üblicherweise selbst
|
||||
// vergeben. Nicht die vollständige Liste (147 Namen): Für "nächstgelegener
|
||||
// Ton" bringt jeder weitere Zwischenwert kaum etwas, und die Liste will
|
||||
// gepflegt werden.
|
||||
var css3Colors = map[string]string{
|
||||
"black": "#000000", "dimgray": "#696969", "gray": "#808080",
|
||||
"darkgray": "#a9a9a9", "silver": "#c0c0c0", "white": "#ffffff",
|
||||
"maroon": "#800000", "brown": "#a52a2a", "firebrick": "#b22222",
|
||||
"crimson": "#dc143c", "red": "#ff0000", "tomato": "#ff6347",
|
||||
"coral": "#ff7f50", "orangered": "#ff4500", "darkorange": "#ff8c00",
|
||||
"orange": "#ffa500", "goldenrod": "#daa520", "gold": "#ffd700",
|
||||
"yellow": "#ffff00", "olive": "#808000", "yellowgreen": "#9acd32",
|
||||
"darkolivegreen": "#556b2f", "lawngreen": "#7cfc00", "limegreen": "#32cd32",
|
||||
"green": "#008000", "darkgreen": "#006400", "seagreen": "#2e8b57",
|
||||
"mediumseagreen": "#3cb371", "teal": "#008080", "darkcyan": "#008b8b",
|
||||
"lightseagreen": "#20b2aa", "turquoise": "#40e0d0", "cyan": "#00ffff",
|
||||
"deepskyblue": "#00bfff", "steelblue": "#4682b4", "cornflowerblue": "#6495ed",
|
||||
"royalblue": "#4169e1", "dodgerblue": "#1e90ff", "blue": "#0000ff",
|
||||
"mediumblue": "#0000cd", "navy": "#000080", "slateblue": "#6a5acd",
|
||||
"mediumpurple": "#9370db", "blueviolet": "#8a2be2", "darkviolet": "#9400d3",
|
||||
"purple": "#800080", "orchid": "#da70d6", "magenta": "#ff00ff",
|
||||
"mediumvioletred": "#c71585", "deeppink": "#ff1493", "hotpink": "#ff69b4",
|
||||
"pink": "#ffc0cb", "slategray": "#708090", "lightslategray": "#778899",
|
||||
"tan": "#d2b48c", "peru": "#cd853f", "sienna": "#a0522d",
|
||||
}
|
||||
|
||||
func parseHex(s string) (r, g, b int, ok bool) {
|
||||
s = strings.TrimSpace(strings.TrimPrefix(strings.ToLower(s), "#"))
|
||||
if len(s) == 3 { // Kurzform #abc
|
||||
s = string([]byte{s[0], s[0], s[1], s[1], s[2], s[2]})
|
||||
}
|
||||
if len(s) == 8 { // mit Alpha – der Kanal interessiert hier nicht
|
||||
s = s[:6]
|
||||
}
|
||||
if len(s) != 6 {
|
||||
return 0, 0, 0, false
|
||||
}
|
||||
v, err := strconv.ParseUint(s, 16, 32)
|
||||
if err != nil {
|
||||
return 0, 0, 0, false
|
||||
}
|
||||
return int(v>>16) & 0xff, int(v>>8) & 0xff, int(v) & 0xff, true
|
||||
}
|
||||
|
||||
// nearestCSS3 findet den nächstgelegenen benannten Ton. Der Abstand wird im
|
||||
// RGB-Würfel gemessen – grob, aber für "welcher Name kommt dem am nächsten"
|
||||
// vollkommen ausreichend, und ohne Farbmetrik-Bibliothek im Gepäck.
|
||||
func nearestCSS3(hex string) string {
|
||||
r, g, b, ok := parseHex(hex)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
best, bestDist := "", 1<<30
|
||||
for name, ref := range css3Colors {
|
||||
rr, rg, rb, _ := parseHex(ref)
|
||||
d := (r-rr)*(r-rr) + (g-rg)*(g-rg) + (b-rb)*(b-rb)
|
||||
// Bei gleichem Abstand der alphabetisch erste Name – sonst wechselt
|
||||
// die Ausgabe zwischen zwei Läufen, weil map-Reihenfolge zufällig ist.
|
||||
if d < bestDist || (d == bestDist && name < best) {
|
||||
best, bestDist = name, d
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
// css3ToHex übersetzt zurück. Unbekannter Name: leer – der Termin erbt dann
|
||||
// die Kalenderfarbe, was allemal besser ist als eine geratene.
|
||||
func css3ToHex(name string) string {
|
||||
return css3Colors[strings.ToLower(strings.TrimSpace(name))]
|
||||
}
|
||||
|
||||
// normalizeHex bringt eine Farbe auf die Schreibweise "#rrggbb".
|
||||
func normalizeHex(s string) string {
|
||||
r, g, b, ok := parseHex(s)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("#%02x%02x%02x", r, g, b)
|
||||
}
|
||||
Reference in new issue
Block a user