Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ffdfc2548 | ||
|
|
83e7727aab |
+26
-3
@@ -23,8 +23,18 @@ type RowColorRule struct {
|
|||||||
|
|
||||||
// RowColorSettings is the whole appearance block.
|
// RowColorSettings is the whole appearance block.
|
||||||
type RowColorSettings struct {
|
type RowColorSettings struct {
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
Rules []RowColorRule `json:"rules"`
|
// Style is how the colour is shown: "bar" paints a stripe down the left edge,
|
||||||
|
// "tint" washes the row, "both" does each.
|
||||||
|
//
|
||||||
|
// A bar is the default because a filled row is a poor signal in a log where
|
||||||
|
// nearly every contact has SOME QSL state: colour that is always present
|
||||||
|
// stops being information and becomes a striped background, with the data
|
||||||
|
// behind it. The stripe says the same thing and costs nothing to read.
|
||||||
|
Style string `json:"style"`
|
||||||
|
// Intensity is the tint strength in percent. Only used by "tint"/"both".
|
||||||
|
Intensity int `json:"intensity"`
|
||||||
|
Rules []RowColorRule `json:"rules"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// The rule ids, in priority order. The frontend matches on these and holds the
|
// The rule ids, in priority order. The frontend matches on these and holds the
|
||||||
@@ -56,7 +66,20 @@ func normRowColors(s RowColorSettings) RowColorSettings {
|
|||||||
for _, r := range s.Rules {
|
for _, r := range s.Rules {
|
||||||
byID[r.ID] = r
|
byID[r.ID] = r
|
||||||
}
|
}
|
||||||
out := RowColorSettings{Enabled: s.Enabled}
|
out := RowColorSettings{Enabled: s.Enabled, Style: s.Style, Intensity: s.Intensity}
|
||||||
|
switch out.Style {
|
||||||
|
case "bar", "tint", "both":
|
||||||
|
default:
|
||||||
|
out.Style = "bar"
|
||||||
|
}
|
||||||
|
// Clamped rather than rejected: the value only shapes a colour-mix, and a
|
||||||
|
// number outside the range is a slider that got away, not a fault worth
|
||||||
|
// resetting the operator's whole choice for.
|
||||||
|
if out.Intensity < 5 {
|
||||||
|
out.Intensity = 12
|
||||||
|
} else if out.Intensity > 45 {
|
||||||
|
out.Intensity = 45
|
||||||
|
}
|
||||||
for _, id := range rowColorOrder {
|
for _, id := range rowColorOrder {
|
||||||
r := byID[id]
|
r := byID[id]
|
||||||
r.ID = id
|
r.ID = id
|
||||||
|
|||||||
@@ -49,3 +49,29 @@ func TestRowColorsKeepPriorityOrder(t *testing.T) {
|
|||||||
t.Errorf("confirmed_lotw lost its colour: %q", got.Rules[0].Color)
|
t.Errorf("confirmed_lotw lost its colour: %q", got.Rules[0].Color)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Style and strength are normalised, not rejected: they only shape a colour, and
|
||||||
|
// a value that got away is a slider, not a fault worth resetting the operator's
|
||||||
|
// whole choice for.
|
||||||
|
func TestRowColorsNormaliseStyleAndIntensity(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
inStyle string
|
||||||
|
inPct int
|
||||||
|
wantStyle string
|
||||||
|
wantPct int
|
||||||
|
}{
|
||||||
|
{"bar", 12, "bar", 12},
|
||||||
|
{"tint", 30, "tint", 30},
|
||||||
|
{"both", 45, "both", 45},
|
||||||
|
{"", 0, "bar", 12}, // never configured
|
||||||
|
{"wallpaper", 12, "bar", 12}, // not a style we draw
|
||||||
|
{"tint", 900, "tint", 45}, // clamped, not reset
|
||||||
|
{"tint", -5, "tint", 12},
|
||||||
|
} {
|
||||||
|
got := normRowColors(RowColorSettings{Style: tc.inStyle, Intensity: tc.inPct})
|
||||||
|
if got.Style != tc.wantStyle || got.Intensity != tc.wantPct {
|
||||||
|
t.Errorf("(%q,%d) -> (%q,%d), want (%q,%d)",
|
||||||
|
tc.inStyle, tc.inPct, got.Style, got.Intensity, tc.wantStyle, tc.wantPct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,14 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"version": "0.24.9",
|
||||||
|
"date": "",
|
||||||
|
"en": [
|
||||||
|
"Appearance: row colouring now defaults to a left stripe, with a filled row and its strength offered as choices."
|
||||||
|
],
|
||||||
|
"fr": [
|
||||||
|
"Apparence : la coloration des lignes se fait par défaut sur une barre à gauche, la ligne remplie et son intensité restant proposées."
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "0.24.8",
|
"version": "0.24.8",
|
||||||
"date": "",
|
"date": "",
|
||||||
|
|||||||
@@ -42,6 +42,17 @@ export function AppearancePanel() {
|
|||||||
save({ ...cfg, rules: cfg.rules.map((r) => (r.id === id ? { ...r, ...patch } : r)) });
|
save({ ...cfg, rules: cfg.rules.map((r) => (r.id === id ? { ...r, ...patch } : r)) });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The rule card shows the row exactly as the grid will draw it, so the choice
|
||||||
|
// is made by looking rather than by imagining.
|
||||||
|
const preview = (color: string): Record<string, string> => {
|
||||||
|
const st = cfg?.style ?? 'bar';
|
||||||
|
const pct = cfg?.intensity ?? 12;
|
||||||
|
const out: Record<string, string> = {};
|
||||||
|
if (st === 'tint' || st === 'both') out.backgroundColor = `color-mix(in srgb, ${color} ${pct}%, transparent)`;
|
||||||
|
if (st === 'bar' || st === 'both') out.boxShadow = `inset 3px 0 0 ${color}`;
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
if (!cfg) return <div className="p-1 text-sm text-muted-foreground">…</div>;
|
if (!cfg) return <div className="p-1 text-sm text-muted-foreground">…</div>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -53,13 +64,35 @@ export function AppearancePanel() {
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
{cfg.enabled && (
|
{cfg.enabled && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-3">
|
||||||
|
{/* Style first: it decides whether the colours below are a signal or a
|
||||||
|
wallpaper, which matters more than which hue they are. */}
|
||||||
|
<div className="flex items-center gap-3 flex-wrap">
|
||||||
|
<span className="text-sm">{t('appr.style')}</span>
|
||||||
|
<div className="inline-flex rounded-md border border-border overflow-hidden text-xs">
|
||||||
|
{(['bar', 'tint', 'both'] as const).map((v) => (
|
||||||
|
<button key={v} type="button" onClick={() => save({ ...cfg, style: v })}
|
||||||
|
className={cn('px-3 py-1.5 font-medium', (cfg.style ?? 'bar') === v ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-muted')}>
|
||||||
|
{t('appr.style' + v[0].toUpperCase() + v.slice(1))}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{(cfg.style ?? 'bar') !== 'bar' && (
|
||||||
|
<label className="flex items-center gap-2 text-sm">
|
||||||
|
{t('appr.intensity')}
|
||||||
|
<input type="range" min={5} max={45} step={1} value={cfg.intensity ?? 12}
|
||||||
|
onChange={(e) => save({ ...cfg, intensity: parseInt(e.target.value, 10) })}
|
||||||
|
className="w-32 accent-[var(--primary)]" />
|
||||||
|
<span className="font-mono text-xs text-muted-foreground w-8">{cfg.intensity ?? 12}%</span>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{/* Order matters and is shown: a contact is usually several of these at
|
{/* Order matters and is shown: a contact is usually several of these at
|
||||||
once, and the first match wins. */}
|
once, and the first match wins. */}
|
||||||
<p className="text-xs text-muted-foreground">{t('appr.orderHint')}</p>
|
<p className="text-xs text-muted-foreground">{t('appr.orderHint')}</p>
|
||||||
{cfg.rules.map((r, i) => (
|
{cfg.rules.map((r, i) => (
|
||||||
<div key={r.id} className="rounded-lg border border-border/60 p-2.5 space-y-2"
|
<div key={r.id} className="rounded-lg border border-border/60 p-2.5 space-y-2"
|
||||||
style={{ backgroundColor: r.enabled ? `color-mix(in srgb, ${r.color} 24%, transparent)` : undefined }}>
|
style={r.enabled ? preview(r.color) : undefined}>
|
||||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||||
<Checkbox checked={r.enabled} onCheckedChange={(c) => patchRule(r.id, { enabled: !!c })} />
|
<Checkbox checked={r.enabled} onCheckedChange={(c) => patchRule(r.id, { enabled: !!c })} />
|
||||||
<span className="font-mono text-xs text-muted-foreground">{i + 1}.</span>
|
<span className="font-mono text-xs text-muted-foreground">{i + 1}.</span>
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ const en: Dict = {
|
|||||||
'nav.user': 'User Configuration', 'nav.software': 'Software Configuration', 'nav.hardware': 'Hardware Configuration', 'nav.lists': 'Lists',
|
'nav.user': 'User Configuration', 'nav.software': 'Software Configuration', 'nav.hardware': 'Hardware Configuration', 'nav.lists': 'Lists',
|
||||||
'sec.station': 'Station Information', 'sec.profiles': 'Profiles', 'sec.operating': 'Operating conditions',
|
'sec.station': 'Station Information', 'sec.profiles': 'Profiles', 'sec.operating': 'Operating conditions',
|
||||||
'sec.confirmations': 'Confirmations', 'sec.external': 'External services',
|
'sec.confirmations': 'Confirmations', 'sec.external': 'External services',
|
||||||
'sec.general': 'General', 'sec.appearance': 'Appearance', 'appr.enable': 'Colour whole rows by QSL status', 'appr.enableHint': '(in the log grid, like Logger32)', 'appr.orderHint': 'A contact is often several of these at once — the first rule that matches decides the colour.', 'appr.confirmedLotw': 'Confirmed on LoTW', 'appr.confirmedPaper': 'Confirmed by card or eQSL', 'appr.sentWaiting': 'Sent, no answer yet', 'appr.toSend': 'Queued to send (card, LoTW or eQSL)', 'appr.custom': 'Pick any colour', 'sec.email': 'E-mail (SMTP)', 'sec.lookup': 'Callsign Lookup',
|
'sec.general': 'General', 'sec.appearance': 'Appearance', 'appr.enable': 'Colour whole rows by QSL status', 'appr.enableHint': '(in the log grid, like Logger32)', 'appr.orderHint': 'A contact is often several of these at once — the first rule that matches decides the colour.', 'appr.confirmedLotw': 'Confirmed on LoTW', 'appr.confirmedPaper': 'Confirmed by card or eQSL', 'appr.sentWaiting': 'Sent, no answer yet', 'appr.toSend': 'Queued to send (card, LoTW or eQSL)', 'appr.custom': 'Pick any colour', 'appr.style': 'Style', 'appr.styleBar': 'Left stripe', 'appr.styleTint': 'Filled row', 'appr.styleBoth': 'Both', 'appr.intensity': 'Strength', 'sec.email': 'E-mail (SMTP)', 'sec.lookup': 'Callsign Lookup',
|
||||||
'sec.bands': 'Bands', 'sec.modes': 'Modes & default RST', 'sec.cluster': 'DX Cluster',
|
'sec.bands': 'Bands', 'sec.modes': 'Modes & default RST', 'sec.cluster': 'DX Cluster',
|
||||||
'sec.udp': 'UDP integrations', 'sec.database': 'Database', 'sec.autostart': 'Autostart', 'sec.backup': 'Database backup', 'sec.uscounties': 'US Counties',
|
'sec.udp': 'UDP integrations', 'sec.database': 'Database', 'sec.autostart': 'Autostart', 'sec.backup': 'Database backup', 'sec.uscounties': 'US Counties',
|
||||||
'sec.webpublish': 'Web publishing', 'wpub.hint': 'Publishes your log as a file for a website: a standalone HTML page or a CSV, written locally and optionally uploaded by FTP. It is refreshed when you log a QSO and, if you set an interval, on a timer.', 'wpub.enable': 'Publish the log to a file', 'wpub.fileSection': 'The file', 'wpub.format': 'Format', 'wpub.formatHtml': 'HTML page', 'wpub.formatCsv': 'CSV', 'wpub.folder': 'Output folder', 'wpub.browse': 'Browse…', 'wpub.fileName': 'File name', 'wpub.title': 'Page title', 'wpub.titlePh': 'blank = your callsign', 'wpub.count': 'Last N QSOs', 'wpub.every': 'Refresh every', 'wpub.everyHint': 'minutes — 0 = only when a QSO is logged', 'wpub.columns': 'Columns', 'wpub.columnsCount': '{n} of {total} chosen', 'wpub.columnsPick': 'Choose columns…', 'wpub.columnsSearch': 'Search a field…', 'wpub.removeColumn': 'Click to remove', 'wpub.columnsHint': 'Click to add or remove. The order shown here is the order in the file.', 'wpub.ftpEnable': 'Upload by FTP', 'wpub.ftpHost': 'Server / port', 'wpub.ftpUser': 'User', 'wpub.ftpPassword': 'Password', 'wpub.ftpFolder': 'Remote folder', 'wpub.ftpFileName': 'Remote file name', 'wpub.ftpTls': 'Use TLS (FTPS)', 'wpub.publishNow': 'Publish now', 'wpub.testFtp': 'Test connection', 'wpub.lastRun': 'Last run:', 'sec.adifmon': 'ADIF monitor',
|
'sec.webpublish': 'Web publishing', 'wpub.hint': 'Publishes your log as a file for a website: a standalone HTML page or a CSV, written locally and optionally uploaded by FTP. It is refreshed when you log a QSO and, if you set an interval, on a timer.', 'wpub.enable': 'Publish the log to a file', 'wpub.fileSection': 'The file', 'wpub.format': 'Format', 'wpub.formatHtml': 'HTML page', 'wpub.formatCsv': 'CSV', 'wpub.folder': 'Output folder', 'wpub.browse': 'Browse…', 'wpub.fileName': 'File name', 'wpub.title': 'Page title', 'wpub.titlePh': 'blank = your callsign', 'wpub.count': 'Last N QSOs', 'wpub.every': 'Refresh every', 'wpub.everyHint': 'minutes — 0 = only when a QSO is logged', 'wpub.columns': 'Columns', 'wpub.columnsCount': '{n} of {total} chosen', 'wpub.columnsPick': 'Choose columns…', 'wpub.columnsSearch': 'Search a field…', 'wpub.removeColumn': 'Click to remove', 'wpub.columnsHint': 'Click to add or remove. The order shown here is the order in the file.', 'wpub.ftpEnable': 'Upload by FTP', 'wpub.ftpHost': 'Server / port', 'wpub.ftpUser': 'User', 'wpub.ftpPassword': 'Password', 'wpub.ftpFolder': 'Remote folder', 'wpub.ftpFileName': 'Remote file name', 'wpub.ftpTls': 'Use TLS (FTPS)', 'wpub.publishNow': 'Publish now', 'wpub.testFtp': 'Test connection', 'wpub.lastRun': 'Last run:', 'sec.adifmon': 'ADIF monitor',
|
||||||
@@ -541,7 +541,7 @@ const fr: Dict = {
|
|||||||
'nav.user': 'Configuration utilisateur', 'nav.software': 'Configuration logicielle', 'nav.hardware': 'Configuration matérielle', 'nav.lists': 'Listes',
|
'nav.user': 'Configuration utilisateur', 'nav.software': 'Configuration logicielle', 'nav.hardware': 'Configuration matérielle', 'nav.lists': 'Listes',
|
||||||
'sec.station': 'Informations station', 'sec.profiles': 'Profils', 'sec.operating': "Conditions d'opération",
|
'sec.station': 'Informations station', 'sec.profiles': 'Profils', 'sec.operating': "Conditions d'opération",
|
||||||
'sec.confirmations': 'Confirmations', 'sec.external': 'Services externes',
|
'sec.confirmations': 'Confirmations', 'sec.external': 'Services externes',
|
||||||
'sec.general': 'Général', 'sec.appearance': 'Apparence', 'appr.enable': 'Colorer les lignes entières selon le statut QSL', 'appr.enableHint': '(dans le tableau du log, comme Logger32)', 'appr.orderHint': "Un contact est souvent plusieurs de ces états à la fois — la première règle qui correspond décide de la couleur.", 'appr.confirmedLotw': 'Confirmé sur LoTW', 'appr.confirmedPaper': 'Confirmé par carte ou eQSL', 'appr.sentWaiting': 'Envoyé, sans réponse', 'appr.toSend': 'En attente d envoi (carte, LoTW ou eQSL)', 'appr.custom': 'Choisir une couleur', 'sec.email': 'E-mail (SMTP)', 'sec.lookup': "Recherche d'indicatif",
|
'sec.general': 'Général', 'sec.appearance': 'Apparence', 'appr.enable': 'Colorer les lignes entières selon le statut QSL', 'appr.enableHint': '(dans le tableau du log, comme Logger32)', 'appr.orderHint': "Un contact est souvent plusieurs de ces états à la fois — la première règle qui correspond décide de la couleur.", 'appr.confirmedLotw': 'Confirmé sur LoTW', 'appr.confirmedPaper': 'Confirmé par carte ou eQSL', 'appr.sentWaiting': 'Envoyé, sans réponse', 'appr.toSend': 'En attente d envoi (carte, LoTW ou eQSL)', 'appr.custom': 'Choisir une couleur', 'appr.style': 'Style', 'appr.styleBar': 'Barre à gauche', 'appr.styleTint': 'Ligne remplie', 'appr.styleBoth': 'Les deux', 'appr.intensity': 'Intensité', 'sec.email': 'E-mail (SMTP)', 'sec.lookup': "Recherche d'indicatif",
|
||||||
'sec.bands': 'Bandes', 'sec.modes': 'Modes & RST par défaut', 'sec.cluster': 'DX Cluster',
|
'sec.bands': 'Bandes', 'sec.modes': 'Modes & RST par défaut', 'sec.cluster': 'DX Cluster',
|
||||||
'sec.udp': 'Intégrations UDP', 'sec.database': 'Base de données', 'sec.autostart': 'Démarrage auto', 'sec.backup': 'Sauvegarde base', 'sec.uscounties': 'Comtés US',
|
'sec.udp': 'Intégrations UDP', 'sec.database': 'Base de données', 'sec.autostart': 'Démarrage auto', 'sec.backup': 'Sauvegarde base', 'sec.uscounties': 'Comtés US',
|
||||||
'sec.webpublish': 'Publication web', 'wpub.hint': "Publie ton journal dans un fichier destiné à un site web : une page HTML autonome ou un CSV, écrit en local et envoyé par FTP si tu le souhaites. Il est rafraîchi à chaque QSO enregistré et, si tu règles un intervalle, périodiquement.", 'wpub.enable': 'Publier le journal dans un fichier', 'wpub.fileSection': 'Le fichier', 'wpub.format': 'Format', 'wpub.formatHtml': 'Page HTML', 'wpub.formatCsv': 'CSV', 'wpub.folder': 'Dossier de sortie', 'wpub.browse': 'Parcourir…', 'wpub.fileName': 'Nom du fichier', 'wpub.title': 'Titre de la page', 'wpub.titlePh': 'vide = ton indicatif', 'wpub.count': 'N derniers QSO', 'wpub.every': 'Rafraîchir toutes les', 'wpub.everyHint': 'minutes — 0 = seulement à chaque QSO', 'wpub.columns': 'Colonnes', 'wpub.columnsCount': '{n} sur {total} choisis', 'wpub.columnsPick': 'Choisir les colonnes…', 'wpub.columnsSearch': 'Chercher un champ…', 'wpub.removeColumn': 'Cliquer pour retirer', 'wpub.columnsHint': 'Clique pour ajouter ou retirer. L ordre affiché ici est celui du fichier.', 'wpub.ftpEnable': 'Envoyer par FTP', 'wpub.ftpHost': 'Serveur / port', 'wpub.ftpUser': 'Utilisateur', 'wpub.ftpPassword': 'Mot de passe', 'wpub.ftpFolder': 'Dossier distant', 'wpub.ftpFileName': 'Nom du fichier distant', 'wpub.ftpTls': 'Utiliser TLS (FTPS)', 'wpub.publishNow': 'Publier maintenant', 'wpub.testFtp': 'Tester la connexion', 'wpub.lastRun': 'Dernière exécution :', 'sec.adifmon': 'Moniteur ADIF',
|
'sec.webpublish': 'Publication web', 'wpub.hint': "Publie ton journal dans un fichier destiné à un site web : une page HTML autonome ou un CSV, écrit en local et envoyé par FTP si tu le souhaites. Il est rafraîchi à chaque QSO enregistré et, si tu règles un intervalle, périodiquement.", 'wpub.enable': 'Publier le journal dans un fichier', 'wpub.fileSection': 'Le fichier', 'wpub.format': 'Format', 'wpub.formatHtml': 'Page HTML', 'wpub.formatCsv': 'CSV', 'wpub.folder': 'Dossier de sortie', 'wpub.browse': 'Parcourir…', 'wpub.fileName': 'Nom du fichier', 'wpub.title': 'Titre de la page', 'wpub.titlePh': 'vide = ton indicatif', 'wpub.count': 'N derniers QSO', 'wpub.every': 'Rafraîchir toutes les', 'wpub.everyHint': 'minutes — 0 = seulement à chaque QSO', 'wpub.columns': 'Colonnes', 'wpub.columnsCount': '{n} sur {total} choisis', 'wpub.columnsPick': 'Choisir les colonnes…', 'wpub.columnsSearch': 'Chercher un champ…', 'wpub.removeColumn': 'Cliquer pour retirer', 'wpub.columnsHint': 'Clique pour ajouter ou retirer. L ordre affiché ici est celui du fichier.', 'wpub.ftpEnable': 'Envoyer par FTP', 'wpub.ftpHost': 'Serveur / port', 'wpub.ftpUser': 'Utilisateur', 'wpub.ftpPassword': 'Mot de passe', 'wpub.ftpFolder': 'Dossier distant', 'wpub.ftpFileName': 'Nom du fichier distant', 'wpub.ftpTls': 'Utiliser TLS (FTPS)', 'wpub.publishNow': 'Publier maintenant', 'wpub.testFtp': 'Tester la connexion', 'wpub.lastRun': 'Dernière exécution :', 'sec.adifmon': 'Moniteur ADIF',
|
||||||
|
|||||||
@@ -5,11 +5,16 @@
|
|||||||
// "sent, awaiting reply".
|
// "sent, awaiting reply".
|
||||||
|
|
||||||
export type RowColorRule = { id: string; color: string; enabled: boolean };
|
export type RowColorRule = { id: string; color: string; enabled: boolean };
|
||||||
export type RowColorSettings = { enabled: boolean; rules: RowColorRule[] };
|
export type RowColorSettings = {
|
||||||
|
enabled: boolean;
|
||||||
|
style?: 'bar' | 'tint' | 'both';
|
||||||
|
intensity?: number;
|
||||||
|
rules: RowColorRule[];
|
||||||
|
};
|
||||||
|
|
||||||
// ADIF QSL fields are single letters. Y is the only one that means "yes";
|
// ADIF QSL fields are single letters. Y is the only one that means "yes";
|
||||||
// R (requested) and Q (queued) mean a card is owed, which is a different state
|
// R (requested) and Q (queued) mean it has not gone out yet, which is a
|
||||||
// and the one an operator is looking for when deciding what to post.
|
// different state and the one an operator looks for when deciding what to send.
|
||||||
const yes = (v: any) => String(v ?? '').trim().toUpperCase() === 'Y';
|
const yes = (v: any) => String(v ?? '').trim().toUpperCase() === 'Y';
|
||||||
const owed = (v: any) => {
|
const owed = (v: any) => {
|
||||||
const s = String(v ?? '').trim().toUpperCase();
|
const s = String(v ?? '').trim().toUpperCase();
|
||||||
@@ -22,20 +27,35 @@ export function matchRowRule(q: any): string | null {
|
|||||||
if (yes(q.qsl_rcvd) || yes(q.eqsl_rcvd)) return 'confirmed_paper';
|
if (yes(q.qsl_rcvd) || yes(q.eqsl_rcvd)) return 'confirmed_paper';
|
||||||
if (yes(q.qsl_sent) || yes(q.lotw_sent) || yes(q.eqsl_sent)) return 'sent_waiting';
|
if (yes(q.qsl_sent) || yes(q.lotw_sent) || yes(q.eqsl_sent)) return 'sent_waiting';
|
||||||
// Any route still queued, not just the paper card. LoTW marks a pending upload
|
// Any route still queued, not just the paper card. LoTW marks a pending upload
|
||||||
// as R, which is the commonest "not gone out yet" state in a digital log and
|
// as R, which is the commonest "not gone out yet" state in a digital log.
|
||||||
// was matching nothing at all while this only looked at qsl_sent.
|
|
||||||
if (owed(q.qsl_sent) || owed(q.lotw_sent) || owed(q.eqsl_sent)) return 'to_send';
|
if (owed(q.qsl_sent) || owed(q.lotw_sent) || owed(q.eqsl_sent)) return 'to_send';
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The colour is applied as a TINT, not a fill. The grid is dark and a solid
|
// The colour is never a fill.
|
||||||
// user-picked colour behind white text is unreadable at exactly the moment it
|
//
|
||||||
// matters — Logger32 gets away with it because its grid is white.
|
// A log where nearly every contact has SOME QSL state ends up with every row
|
||||||
export function rowStyleFor(q: any, cfg: RowColorSettings | null): { backgroundColor: string } | undefined {
|
// painted, and colour that is always present stops being information — it
|
||||||
|
// becomes a striped background with the data behind it. The default is a stripe
|
||||||
|
// down the left edge: same signal, nothing lost to read it. A tint is offered
|
||||||
|
// for operators who want the block, at a strength they choose.
|
||||||
|
export function rowStyleFor(q: any, cfg: RowColorSettings | null): Record<string, string> | undefined {
|
||||||
if (!cfg?.enabled) return undefined;
|
if (!cfg?.enabled) return undefined;
|
||||||
const id = matchRowRule(q);
|
const id = matchRowRule(q);
|
||||||
if (!id) return undefined;
|
if (!id) return undefined;
|
||||||
const rule = cfg.rules?.find((r) => r.id === id);
|
const rule = cfg.rules?.find((r) => r.id === id);
|
||||||
if (!rule?.enabled || !rule.color) return undefined;
|
if (!rule?.enabled || !rule.color) return undefined;
|
||||||
return { backgroundColor: `color-mix(in srgb, ${rule.color} 24%, transparent)` };
|
|
||||||
|
const style = cfg.style ?? 'bar';
|
||||||
|
const pct = Math.max(5, Math.min(45, cfg.intensity ?? 12));
|
||||||
|
const out: Record<string, string> = {};
|
||||||
|
if (style === 'tint' || style === 'both') {
|
||||||
|
out.backgroundColor = `color-mix(in srgb, ${rule.color} ${pct}%, transparent)`;
|
||||||
|
}
|
||||||
|
if (style === 'bar' || style === 'both') {
|
||||||
|
// inset shadow rather than a border: a border would shift the cell layout by
|
||||||
|
// three pixels on coloured rows only, and the columns would no longer line up.
|
||||||
|
out.boxShadow = `inset 3px 0 0 ${rule.color}`;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3010,6 +3010,8 @@ export namespace main {
|
|||||||
}
|
}
|
||||||
export class RowColorSettings {
|
export class RowColorSettings {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
style: string;
|
||||||
|
intensity: number;
|
||||||
rules: RowColorRule[];
|
rules: RowColorRule[];
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
@@ -3019,6 +3021,8 @@ export namespace main {
|
|||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.enabled = source["enabled"];
|
this.enabled = source["enabled"];
|
||||||
|
this.style = source["style"];
|
||||||
|
this.intensity = source["intensity"];
|
||||||
this.rules = this.convertValues(source["rules"], RowColorRule);
|
this.rules = this.convertValues(source["rules"], RowColorRule);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user