Compare commits
3
Commits
9d4aecb668
...
e190364c77
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e190364c77 | ||
|
|
dde5410788 | ||
|
|
3fddab6815 |
@@ -29,6 +29,7 @@ type userRecord struct {
|
|||||||
Theme string `json:"theme"`
|
Theme string `json:"theme"`
|
||||||
Locale string `json:"locale"`
|
Locale string `json:"locale"`
|
||||||
DateFormat string `json:"date_format"`
|
DateFormat string `json:"date_format"`
|
||||||
|
TimeFormat string `json:"time_format"`
|
||||||
Currency string `json:"currency"`
|
Currency string `json:"currency"`
|
||||||
FontSize string `json:"font_size"`
|
FontSize string `json:"font_size"`
|
||||||
DragLocked bool `json:"drag_locked"`
|
DragLocked bool `json:"drag_locked"`
|
||||||
@@ -97,6 +98,7 @@ func (rec userRecord) toModel() models.User {
|
|||||||
Theme: orDefault(rec.Theme, "system"),
|
Theme: orDefault(rec.Theme, "system"),
|
||||||
Locale: orDefault(rec.Locale, "en-US"),
|
Locale: orDefault(rec.Locale, "en-US"),
|
||||||
DateFormat: orDefault(rec.DateFormat, "YMD"),
|
DateFormat: orDefault(rec.DateFormat, "YMD"),
|
||||||
|
TimeFormat: orDefault(rec.TimeFormat, "auto"),
|
||||||
Currency: orDefault(rec.Currency, "USD"),
|
Currency: orDefault(rec.Currency, "USD"),
|
||||||
FontSize: orDefault(rec.FontSize, "medium"),
|
FontSize: orDefault(rec.FontSize, "medium"),
|
||||||
DragLocked: rec.DragLocked,
|
DragLocked: rec.DragLocked,
|
||||||
@@ -161,6 +163,7 @@ type updateMeRequest struct {
|
|||||||
Theme *string `json:"theme"`
|
Theme *string `json:"theme"`
|
||||||
Locale *string `json:"locale"`
|
Locale *string `json:"locale"`
|
||||||
DateFormat *string `json:"dateFormat"`
|
DateFormat *string `json:"dateFormat"`
|
||||||
|
TimeFormat *string `json:"timeFormat"`
|
||||||
Currency *string `json:"currency"`
|
Currency *string `json:"currency"`
|
||||||
FontSize *string `json:"fontSize"`
|
FontSize *string `json:"fontSize"`
|
||||||
DragLocked *bool `json:"dragLocked"`
|
DragLocked *bool `json:"dragLocked"`
|
||||||
@@ -256,6 +259,11 @@ func normalizeOrder(field string, in []string, max int) ([]string, error) {
|
|||||||
|
|
||||||
var validThemes = map[string]bool{"light": true, "dark": true, "system": true}
|
var validThemes = map[string]bool{"light": true, "dark": true, "system": true}
|
||||||
var validDateFormats = map[string]bool{"YMD": true, "DMY_NUM": true, "DMY": true, "MDY": true}
|
var validDateFormats = map[string]bool{"YMD": true, "DMY_NUM": true, "DMY": true, "MDY": true}
|
||||||
|
|
||||||
|
// "auto" reads the clock the way the chosen region writes it, which is what
|
||||||
|
// the app did before there was a setting; the other two say it outright, for
|
||||||
|
// the people whose region and habit disagree.
|
||||||
|
var validTimeFormats = map[string]bool{"auto": true, "24": true, "12": true}
|
||||||
var validFontSizes = map[string]bool{"small": true, "medium": true, "large": true}
|
var validFontSizes = map[string]bool{"small": true, "medium": true, "large": true}
|
||||||
|
|
||||||
// Kept in step with the users.currency select options in setup-pocketbase.mjs:
|
// Kept in step with the users.currency select options in setup-pocketbase.mjs:
|
||||||
@@ -318,6 +326,13 @@ func (s *Server) handleUpdateMe(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
payload["date_format"] = *in.DateFormat
|
payload["date_format"] = *in.DateFormat
|
||||||
}
|
}
|
||||||
|
if in.TimeFormat != nil {
|
||||||
|
if !validTimeFormats[*in.TimeFormat] {
|
||||||
|
writeError(w, http.StatusBadRequest, "timeFormat must be auto, 24, or 12")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
payload["time_format"] = *in.TimeFormat
|
||||||
|
}
|
||||||
if in.Currency != nil {
|
if in.Currency != nil {
|
||||||
if !validCurrencies[*in.Currency] {
|
if !validCurrencies[*in.Currency] {
|
||||||
writeError(w, http.StatusBadRequest, "currency must be a supported ISO 4217 code")
|
writeError(w, http.StatusBadRequest, "currency must be a supported ISO 4217 code")
|
||||||
|
|||||||
@@ -231,6 +231,9 @@ var collectionsSchema = map[string][]fieldDef{
|
|||||||
fSelect("theme", []string{"light", "dark", "system"}, false),
|
fSelect("theme", []string{"light", "dark", "system"}, false),
|
||||||
fText("locale", false),
|
fText("locale", false),
|
||||||
fSelect("date_format", []string{"YMD", "DMY_NUM", "DMY", "MDY"}, false),
|
fSelect("date_format", []string{"YMD", "DMY_NUM", "DMY", "MDY"}, false),
|
||||||
|
// "auto" is the region's own convention, which is what every clock in the
|
||||||
|
// app read before this field existed.
|
||||||
|
fSelect("time_format", []string{"auto", "24", "12"}, false),
|
||||||
fSelect("currency", []string{
|
fSelect("currency", []string{
|
||||||
"EUR", "GBP", "CHF", "PLN", "CZK", "HUF", "RON", "BGN", "DKK", "SEK", "NOK",
|
"EUR", "GBP", "CHF", "PLN", "CZK", "HUF", "RON", "BGN", "DKK", "SEK", "NOK",
|
||||||
"ISK", "ALL", "AMD", "AZN", "BAM", "BYN", "GEL", "MDL", "MKD", "RSD", "RUB",
|
"ISK", "ALL", "AMD", "AZN", "BAM", "BYN", "GEL", "MDL", "MKD", "RSD", "RUB",
|
||||||
|
|||||||
@@ -476,6 +476,7 @@ type User struct {
|
|||||||
Theme string `json:"theme"` // light | dark | system
|
Theme string `json:"theme"` // light | dark | system
|
||||||
Locale string `json:"locale"` // e.g. "en-US"
|
Locale string `json:"locale"` // e.g. "en-US"
|
||||||
DateFormat string `json:"dateFormat"` // YMD | DMY | MDY
|
DateFormat string `json:"dateFormat"` // YMD | DMY | MDY
|
||||||
|
TimeFormat string `json:"timeFormat"` // auto (the region's own) | 24 | 12
|
||||||
Currency string `json:"currency"` // ISO 4217 code, e.g. "EUR"
|
Currency string `json:"currency"` // ISO 4217 code, e.g. "EUR"
|
||||||
FontSize string `json:"fontSize"` // small | medium | large
|
FontSize string `json:"fontSize"` // small | medium | large
|
||||||
Role string `json:"role"` // user | admin
|
Role string `json:"role"` // user | admin
|
||||||
|
|||||||
@@ -490,6 +490,9 @@ const DESIRED = {
|
|||||||
F.select("theme", ["light", "dark", "system"]),
|
F.select("theme", ["light", "dark", "system"]),
|
||||||
F.text("locale"),
|
F.text("locale"),
|
||||||
F.select("date_format", ["YMD", "DMY_NUM", "DMY", "MDY"]),
|
F.select("date_format", ["YMD", "DMY_NUM", "DMY", "MDY"]),
|
||||||
|
// "auto" is the region's own convention. Kept in step with
|
||||||
|
// validTimeFormats in internal/api/me.go.
|
||||||
|
F.select("time_format", ["auto", "24", "12"]),
|
||||||
// European currencies plus the non-European ones the panel already offered.
|
// European currencies plus the non-European ones the panel already offered.
|
||||||
// Kept in step with validCurrencies in internal/api/me.go and CURRENCY_CODES
|
// Kept in step with validCurrencies in internal/api/me.go and CURRENCY_CODES
|
||||||
// in the web app's Settings.vue.
|
// in the web app's Settings.vue.
|
||||||
|
|||||||
@@ -520,6 +520,11 @@
|
|||||||
"regionHint": "Tal- og valutaformat.",
|
"regionHint": "Tal- og valutaformat.",
|
||||||
"dateFormat": "Datoformat",
|
"dateFormat": "Datoformat",
|
||||||
"dateExample": "Eksempel: {example}",
|
"dateExample": "Eksempel: {example}",
|
||||||
|
"timeFormat": "Tidsformat",
|
||||||
|
"timeAuto": "Følg regionen",
|
||||||
|
"time24": "24-timers",
|
||||||
|
"time12": "12-timers",
|
||||||
|
"timeExample": "Eksempel: {example}",
|
||||||
"currency": "Valuta",
|
"currency": "Valuta",
|
||||||
"currencyExample": "Eksempel: {example} — kun visning, ingen beløb omregnes.",
|
"currencyExample": "Eksempel: {example} — kun visning, ingen beløb omregnes.",
|
||||||
"fontSize": "Skriftstørrelse",
|
"fontSize": "Skriftstørrelse",
|
||||||
|
|||||||
@@ -519,6 +519,11 @@
|
|||||||
"regionHint": "Number and currency layout.",
|
"regionHint": "Number and currency layout.",
|
||||||
"dateFormat": "Date format",
|
"dateFormat": "Date format",
|
||||||
"dateExample": "Example: {example}",
|
"dateExample": "Example: {example}",
|
||||||
|
"timeFormat": "Time format",
|
||||||
|
"timeAuto": "Follow the region",
|
||||||
|
"time24": "24-hour",
|
||||||
|
"time12": "12-hour",
|
||||||
|
"timeExample": "Example: {example}",
|
||||||
"currency": "Currency",
|
"currency": "Currency",
|
||||||
"currencyExample": "Example: {example} — display only, no amounts are converted.",
|
"currencyExample": "Example: {example} — display only, no amounts are converted.",
|
||||||
"fontSize": "Font size",
|
"fontSize": "Font size",
|
||||||
|
|||||||
@@ -524,6 +524,11 @@
|
|||||||
"regionHint": "Format liczb i waluty.",
|
"regionHint": "Format liczb i waluty.",
|
||||||
"dateFormat": "Format daty",
|
"dateFormat": "Format daty",
|
||||||
"dateExample": "Przykład: {example}",
|
"dateExample": "Przykład: {example}",
|
||||||
|
"timeFormat": "Format godziny",
|
||||||
|
"timeAuto": "Jak w regionie",
|
||||||
|
"time24": "24-godzinny",
|
||||||
|
"time12": "12-godzinny",
|
||||||
|
"timeExample": "Przykład: {example}",
|
||||||
"currency": "Waluta",
|
"currency": "Waluta",
|
||||||
"currencyExample": "Przykład: {example} — tylko wyświetlanie, kwoty nie są przeliczane.",
|
"currencyExample": "Przykład: {example} — tylko wyświetlanie, kwoty nie są przeliczane.",
|
||||||
"fontSize": "Rozmiar czcionki",
|
"fontSize": "Rozmiar czcionki",
|
||||||
|
|||||||
@@ -76,8 +76,25 @@ export function formatDateTime(value) {
|
|||||||
if (!value) return "—";
|
if (!value) return "—";
|
||||||
const d = new Date(value);
|
const d = new Date(value);
|
||||||
if (isNaN(d)) return "—";
|
if (isNaN(d)) return "—";
|
||||||
const time = d.toLocaleTimeString(prefs.locale || undefined, { hour: "2-digit", minute: "2-digit" });
|
return `${formatDate(value)} ${formatTime(d)}`;
|
||||||
return `${formatDate(value)} ${time}`;
|
}
|
||||||
|
|
||||||
|
// The clock alone. "auto" leaves the reading to the region, which is what every
|
||||||
|
// time in the app said before there was a setting; the other two are for the
|
||||||
|
// people whose region and habit disagree — plenty of Poles read 12-hour clocks
|
||||||
|
// and plenty of Americans read 24-hour ones, and the region picker also decides
|
||||||
|
// how money and numbers are grouped, so it is the wrong lever to reach for.
|
||||||
|
//
|
||||||
|
// hourCycle rather than hour12: with hour12:false the en-US formatter prints
|
||||||
|
// midnight as 24:00.
|
||||||
|
export function formatTime(value) {
|
||||||
|
if (!value) return "—";
|
||||||
|
const d = value instanceof Date ? value : new Date(value);
|
||||||
|
if (isNaN(d)) return "—";
|
||||||
|
const opts = { hour: "2-digit", minute: "2-digit" };
|
||||||
|
if (prefs.timeFormat === "24") opts.hourCycle = "h23";
|
||||||
|
else if (prefs.timeFormat === "12") opts.hourCycle = "h12";
|
||||||
|
return d.toLocaleTimeString(prefs.locale || undefined, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Every number we render goes through here so the grouping separator follows
|
// Every number we render goes through here so the grouping separator follows
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const prefs = reactive({
|
|||||||
theme: "system", // light | dark | system
|
theme: "system", // light | dark | system
|
||||||
locale: "en-US",
|
locale: "en-US",
|
||||||
dateFormat: "YMD", // YMD | DMY | MDY
|
dateFormat: "YMD", // YMD | DMY | MDY
|
||||||
|
timeFormat: "auto", // auto (the region's own convention) | 24 | 12
|
||||||
currency: "USD", // ISO 4217 code
|
currency: "USD", // ISO 4217 code
|
||||||
fontSize: "medium", // small | medium | large
|
fontSize: "medium", // small | medium | large
|
||||||
// Holds every arrangement still: the garage, a car's tabs, its Information
|
// Holds every arrangement still: the garage, a car's tabs, its Information
|
||||||
@@ -56,6 +57,7 @@ export function applyProfilePrefs(profile) {
|
|||||||
prefs.theme = profile.theme || "system";
|
prefs.theme = profile.theme || "system";
|
||||||
prefs.locale = profile.locale || "en-US";
|
prefs.locale = profile.locale || "en-US";
|
||||||
prefs.dateFormat = profile.dateFormat || "YMD";
|
prefs.dateFormat = profile.dateFormat || "YMD";
|
||||||
|
prefs.timeFormat = profile.timeFormat || "auto";
|
||||||
prefs.currency = profile.currency || "USD";
|
prefs.currency = profile.currency || "USD";
|
||||||
prefs.fontSize = profile.fontSize || "medium";
|
prefs.fontSize = profile.fontSize || "medium";
|
||||||
prefs.dragLocked = !!profile.dragLocked;
|
prefs.dragLocked = !!profile.dragLocked;
|
||||||
|
|||||||
@@ -602,7 +602,10 @@ const MQTT_SETTING_BLOCKS = [
|
|||||||
id: "charging",
|
id: "charging",
|
||||||
title: "blockCharging",
|
title: "blockCharging",
|
||||||
fields: [
|
fields: [
|
||||||
{ key: "maxCurrentA", at: "settings.maxCurrentA", label: "maxCurrentSet", type: "number", min: LIMIT_FLOOR, max: null, step: 1, unit: "A" },
|
// A slider rather than a box, like the control card had and the Modbus
|
||||||
|
// settings card still has: it is the same value they set, and a ceiling
|
||||||
|
// is a thing you slide between two known ends rather than type.
|
||||||
|
{ key: "maxCurrentA", at: "settings.maxCurrentA", label: "maxCurrentSet", type: "slider", min: LIMIT_FLOOR, max: null, step: 1, unit: "A", hint: "limitFloorHint" },
|
||||||
{ key: "autoStart", at: "settings.autoStart", label: "autoStart", type: "switch" },
|
{ key: "autoStart", at: "settings.autoStart", label: "autoStart", type: "switch" },
|
||||||
{ key: "randomDelay", at: "settings.randomDelay", label: "randomDelay", type: "switch" },
|
{ key: "randomDelay", at: "settings.randomDelay", label: "randomDelay", type: "switch" },
|
||||||
{ key: "plugLock", at: "settings.plugLock", label: "plugLock", type: "switch" },
|
{ key: "plugLock", at: "settings.plugLock", label: "plugLock", type: "switch" },
|
||||||
@@ -1000,6 +1003,31 @@ function liveFor(c) {
|
|||||||
return chargerLive.value[c.providerChargerId] || chargerLive.value[c.serial] || null;
|
return chargerLive.value[c.providerChargerId] || chargerLive.value[c.serial] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- The charger the buttons act on, as a picture and a name ------------------
|
||||||
|
//
|
||||||
|
// The control card drives whichever serial is in force, which is not always the
|
||||||
|
// record highlighted in the list beside it, so it identifies its charger by that
|
||||||
|
// serial rather than by the selection. Two sources carry the same product shot:
|
||||||
|
// the account's own charger list, and the live half held per provider. Either
|
||||||
|
// will do; the account's is the one that arrives without the home tab having
|
||||||
|
// been opened.
|
||||||
|
//
|
||||||
|
// It lives here rather than up with the other ctl* values because both of its
|
||||||
|
// sources do, and a lookup reads better next to the map it reads from.
|
||||||
|
function accountCharger(sn) {
|
||||||
|
if (!sn) return null;
|
||||||
|
return chargers.value.find((c) => c.sn === sn) || chargerLive.value[sn] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctlImageUrl = computed(() => accountCharger(ctlSerial.value.trim())?.imageUrl || "");
|
||||||
|
const ctlChargerName = computed(() => accountCharger(ctlSerial.value.trim())?.name || "");
|
||||||
|
|
||||||
|
// A product shot is a URL from the service, and a URL can 404. Remembered per
|
||||||
|
// URL rather than per charger, so the picture simply stops being drawn and comes
|
||||||
|
// back on its own if the service starts answering for it again — no reset to
|
||||||
|
// forget when the serial changes.
|
||||||
|
const imageFailed = ref({});
|
||||||
|
|
||||||
// How the charger is registered on the account, in the service's own terms.
|
// How the charger is registered on the account, in the service's own terms.
|
||||||
function sourcesLabel(sources) {
|
function sourcesLabel(sources) {
|
||||||
if (!sources?.length) return "";
|
if (!sources?.length) return "";
|
||||||
@@ -1929,6 +1957,26 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<div v-show="isOpen('control')">
|
<div v-show="isOpen('control')">
|
||||||
|
|
||||||
|
<!-- Which charger these buttons act on. The card said nothing about
|
||||||
|
that before: the name is two cards further down, and the picture
|
||||||
|
is the fastest way to tell two chargers on one account apart. -->
|
||||||
|
<div
|
||||||
|
v-if="ctlImageUrl || ctlChargerName"
|
||||||
|
class="mt-3 flex items-center gap-3 rounded-control bg-sunken p-3"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="ctlImageUrl && !imageFailed[ctlImageUrl]"
|
||||||
|
:src="ctlImageUrl"
|
||||||
|
alt=""
|
||||||
|
class="h-16 w-16 shrink-0 rounded object-contain"
|
||||||
|
@error="imageFailed[ctlImageUrl] = true"
|
||||||
|
/>
|
||||||
|
<div class="min-w-0">
|
||||||
|
<p v-if="ctlChargerName" class="truncate text-sm font-semibold text-strong">{{ ctlChargerName }}</p>
|
||||||
|
<p class="data truncate text-[11px] text-muted">{{ ctlSerial }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-3 grid grid-cols-2 gap-2">
|
<div class="mt-3 grid grid-cols-2 gap-2">
|
||||||
<div class="rounded-control bg-sunken px-3 py-2">
|
<div class="rounded-control bg-sunken px-3 py-2">
|
||||||
<div class="data text-sm font-semibold text-strong">{{ ctlStatusLabel }}</div>
|
<div class="data text-sm font-semibold text-strong">{{ ctlStatusLabel }}</div>
|
||||||
@@ -1968,14 +2016,18 @@ onMounted(async () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- The limit, everywhere but the local path. There the settings card
|
<!-- The limit, only where nothing else owns it. Both transports that
|
||||||
owns it: it is the charger's own ceiling register, read back and
|
read the charger have a settings card now, and on both the limit is
|
||||||
written, and the same slider in two cards was the same register
|
the charger's own ceiling — the same register over Modbus, the same
|
||||||
twice. Neither OCPP nor the cloud has a settings card to move it to
|
wire field over the cloud, where "limit" and the maxCurrentA
|
||||||
— a charging profile is not a setting the charger reports — so here
|
setting are built from one table. The same slider in two cards was
|
||||||
it stays. Clearing it is OCPP's alone: the cloud sets a ceiling and
|
that one value twice.
|
||||||
has no message for "no ceiling". -->
|
|
||||||
<div v-if="!ctlIsModbus" class="mt-3">
|
OCPP is the exception and keeps it: a charging profile is not a
|
||||||
|
setting the charger reports, so there is no settings card to move
|
||||||
|
it to. Clearing it is OCPP's alone too — the cloud sets a ceiling
|
||||||
|
and has no message for "no ceiling". -->
|
||||||
|
<div v-if="!ctlReadsDevice" class="mt-3">
|
||||||
<label class="dh-label flex justify-between">
|
<label class="dh-label flex justify-between">
|
||||||
<span>{{ t("charging.control.limit") }}</span><span class="data text-body">{{ limitAmps }} A</span>
|
<span>{{ t("charging.control.limit") }}</span><span class="data text-body">{{ limitAmps }} A</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -2370,11 +2422,29 @@ onMounted(async () => {
|
|||||||
<section v-for="block in mqttSettingBlocks" :key="block.id" class="mt-2 rounded-control bg-sunken p-3">
|
<section v-for="block in mqttSettingBlocks" :key="block.id" class="mt-2 rounded-control bg-sunken p-3">
|
||||||
<h4 class="eyebrow">{{ t(`charging.modbus.${block.title}`) }}</h4>
|
<h4 class="eyebrow">{{ t(`charging.modbus.${block.title}`) }}</h4>
|
||||||
<div class="mt-2 flex flex-col gap-2">
|
<div class="mt-2 flex flex-col gap-2">
|
||||||
<div
|
<template v-for="f in block.fields" :key="f.key || f.from">
|
||||||
v-for="f in block.fields"
|
<!-- A slider needs the width, so its row stacks: the label and
|
||||||
:key="f.key || f.from"
|
the value it is at on one line, the track under them. -->
|
||||||
class="flex items-center justify-between gap-3"
|
<div v-if="f.type === 'slider'">
|
||||||
>
|
<label class="dh-label flex justify-between" :for="`set-${f.key}`">
|
||||||
|
<span>{{ t(`charging.modbus.${f.label}`) }}</span>
|
||||||
|
<span class="data text-body">{{ mqttDraft[f.key] }} {{ f.unit }}</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
:id="`set-${f.key}`"
|
||||||
|
v-model.number="mqttDraft[f.key]"
|
||||||
|
type="range"
|
||||||
|
:min="f.min"
|
||||||
|
:max="fieldMax(f)"
|
||||||
|
:step="f.step"
|
||||||
|
class="w-full accent-[var(--accent)]"
|
||||||
|
/>
|
||||||
|
<p v-if="f.hint" class="mt-1 text-[11px] text-muted">
|
||||||
|
{{ t(`charging.modbus.${f.hint}`, { amps: f.min }) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="flex items-center justify-between gap-3">
|
||||||
<label class="dh-label !mb-0 min-w-0 grow" :for="`set-${f.key || f.from}`">
|
<label class="dh-label !mb-0 min-w-0 grow" :for="`set-${f.key || f.from}`">
|
||||||
{{ t(`charging.modbus.${f.label}`) }}
|
{{ t(`charging.modbus.${f.label}`) }}
|
||||||
</label>
|
</label>
|
||||||
@@ -2433,6 +2503,7 @@ onMounted(async () => {
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p v-if="block.warning" class="mt-2 text-[11px]" style="color: var(--warning-600)">
|
<p v-if="block.warning" class="mt-2 text-[11px]" style="color: var(--warning-600)">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useRoute, useRouter } from "vue-router";
|
|||||||
import { api } from "../api";
|
import { api } from "../api";
|
||||||
import { state, isAdmin, logout, refreshProfile } from "../auth";
|
import { state, isAdmin, logout, refreshProfile } from "../auth";
|
||||||
import { prefs, applyProfilePrefs } from "../prefs";
|
import { prefs, applyProfilePrefs } from "../prefs";
|
||||||
import { formatDate, formatMoney } from "../lib/format.js";
|
import { formatDate, formatMoney, formatTime } from "../lib/format.js";
|
||||||
import { t, tSplit, TRANSLATED_LANGUAGES } from "../i18n";
|
import { t, tSplit, TRANSLATED_LANGUAGES } from "../i18n";
|
||||||
import { TAB_SURFACES, SETTINGS_TABS, defaultTabFor } from "../lib/tabs.js";
|
import { TAB_SURFACES, SETTINGS_TABS, defaultTabFor } from "../lib/tabs.js";
|
||||||
import { askConfirm } from "../lib/confirm.js";
|
import { askConfirm } from "../lib/confirm.js";
|
||||||
@@ -189,6 +189,13 @@ function saveDefaultTab(surface, key) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const dateFormatExample = computed(() => formatDate(new Date().toISOString()));
|
const dateFormatExample = computed(() => formatDate(new Date().toISOString()));
|
||||||
|
// Thirteen-something rather than now: an example at 09:00 reads the same in
|
||||||
|
// both conventions, which is the one time of day that cannot show the choice.
|
||||||
|
const timeFormatExample = computed(() => {
|
||||||
|
const d = new Date();
|
||||||
|
d.setHours(13, 45, 0, 0);
|
||||||
|
return formatTime(d);
|
||||||
|
});
|
||||||
const currencyExample = computed(() => formatMoney(1234.5));
|
const currencyExample = computed(() => formatMoney(1234.5));
|
||||||
|
|
||||||
// Language and region are two controls over the one stored BCP-47 locale, so
|
// Language and region are two controls over the one stored BCP-47 locale, so
|
||||||
@@ -1127,6 +1134,18 @@ onBeforeUnmount(() => {
|
|||||||
<p class="mt-1 text-xs text-muted">{{ t("settings.appearance.dateExample", { example: dateFormatExample }) }}</p>
|
<p class="mt-1 text-xs text-muted">{{ t("settings.appearance.dateExample", { example: dateFormatExample }) }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Beside the date rather than under the region, because it is the
|
||||||
|
same question asked about the other half of a timestamp. -->
|
||||||
|
<div>
|
||||||
|
<label class="dh-label">{{ t("settings.appearance.timeFormat") }}</label>
|
||||||
|
<select :value="prefs.timeFormat" class="dh-input" @change="saveAppearance({ timeFormat: $event.target.value })">
|
||||||
|
<option value="auto">{{ t("settings.appearance.timeAuto") }}</option>
|
||||||
|
<option value="24">{{ t("settings.appearance.time24") }}</option>
|
||||||
|
<option value="12">{{ t("settings.appearance.time12") }}</option>
|
||||||
|
</select>
|
||||||
|
<p class="mt-1 text-xs text-muted">{{ t("settings.appearance.timeExample", { example: timeFormatExample }) }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="dh-label">{{ t("settings.appearance.currency") }}</label>
|
<label class="dh-label">{{ t("settings.appearance.currency") }}</label>
|
||||||
<select :value="prefs.currency" class="dh-input" @change="saveAppearance({ currency: $event.target.value })">
|
<select :value="prefs.currency" class="dh-input" @change="saveAppearance({ currency: $event.target.value })">
|
||||||
|
|||||||
Reference in New Issue
Block a user