chore: Typescript: Rewrite js (#21719)
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
This commit is contained in:
parent
cca4f254c2
commit
c7eedb9655
20 changed files with 512 additions and 478 deletions
|
|
@ -1,68 +0,0 @@
|
|||
import axios from "axios";
|
||||
import { openLoginModal } from "./components/Auth/auth";
|
||||
|
||||
const { protocol, hostname, port, pathname } = window.location;
|
||||
|
||||
const base = protocol + "//" + hostname + (port ? ":" + port : "") + pathname;
|
||||
|
||||
// override the way axios serializes arrays in query parameters (a=1&a=2&a=3 instead of a[]=1&a[]=2&a[]=3)
|
||||
function customParamsSerializer(params) {
|
||||
const queryString = Object.keys(params)
|
||||
.filter((key) => params[key] !== null)
|
||||
.map((key) => {
|
||||
const value = params[key];
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((v) => `${encodeURIComponent(key)}=${encodeURIComponent(v)}`).join("&");
|
||||
}
|
||||
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
||||
})
|
||||
.join("&");
|
||||
return queryString;
|
||||
}
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: base + "api/",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
paramsSerializer: customParamsSerializer,
|
||||
});
|
||||
|
||||
// global error handling
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
// handle unauthorized errors
|
||||
if (error.response?.status === 401) {
|
||||
openLoginModal();
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
const message = [`${error.message}.`];
|
||||
if (error.response?.data?.error) {
|
||||
message.push(`${error.response.data.error}.`);
|
||||
}
|
||||
if (error.config) {
|
||||
const method = error.config.method.toUpperCase();
|
||||
const url = error.request.responseURL;
|
||||
message.push(`${method} ${url}`);
|
||||
}
|
||||
window.app.raise({ message });
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default api;
|
||||
|
||||
export const i18n = axios.create({
|
||||
baseURL: base + "i18n/",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
export const allowClientError = {
|
||||
validateStatus(status) {
|
||||
return status >= 200 && status < 500;
|
||||
},
|
||||
};
|
||||
69
assets/js/api.ts
Normal file
69
assets/js/api.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import axios from "axios";
|
||||
import { openLoginModal } from "./components/Auth/auth";
|
||||
|
||||
const { protocol, hostname, port, pathname } = window.location;
|
||||
|
||||
const base = protocol + "//" + hostname + (port ? ":" + port : "") + pathname;
|
||||
|
||||
// override the way axios serializes arrays in query parameters (a=1&a=2&a=3 instead of a[]=1&a[]=2&a[]=3)
|
||||
function customParamsSerializer(params: { [key: string]: any }) {
|
||||
return Object.keys(params)
|
||||
.filter((key) => params[key] !== null)
|
||||
.map((key) => {
|
||||
const value = params[key];
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.map((v) => `${encodeURIComponent(key)}=${encodeURIComponent(v)}`)
|
||||
.join("&");
|
||||
}
|
||||
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
||||
})
|
||||
.join("&");
|
||||
}
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: base + "api/",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
paramsSerializer: customParamsSerializer,
|
||||
});
|
||||
|
||||
// global error handling
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
// handle unauthorized errors
|
||||
if (error.response?.status === 401) {
|
||||
openLoginModal();
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
const message = [`${error.message}.`];
|
||||
if (error.response?.data?.error) {
|
||||
message.push(`${error.response.data.error}.`);
|
||||
}
|
||||
if (error.config) {
|
||||
const method = error.config.method.toUpperCase();
|
||||
const url = error.request.responseURL;
|
||||
message.push(`${method} ${url}`);
|
||||
}
|
||||
window.app.raise({ message });
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default api;
|
||||
|
||||
export const i18n = axios.create({
|
||||
baseURL: base + "i18n/",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
export const allowClientError = {
|
||||
validateStatus(status: number) {
|
||||
return status >= 200 && status < 500;
|
||||
},
|
||||
};
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
import "../css/app.css";
|
||||
import { createApp, h } from "vue";
|
||||
import { VueHeadMixin, createHead } from "@unhead/vue/client";
|
||||
import App from "./views/App.vue";
|
||||
import setupRouter from "./router";
|
||||
import setupI18n from "./i18n";
|
||||
import featureflags from "./featureflags";
|
||||
import { watchThemeChanges } from "./theme";
|
||||
import { appDetection, sendToApp } from "./utils/native";
|
||||
|
||||
// lazy load smoothscroll polyfill. mainly for safari < 15.4
|
||||
if (!window.CSS.supports("scroll-behavior", "smooth")) {
|
||||
console.log("no native smoothscroll support. polyfilling...");
|
||||
import("smoothscroll-polyfill").then((module) => {
|
||||
module.polyfill();
|
||||
});
|
||||
}
|
||||
|
||||
const app = createApp({
|
||||
data() {
|
||||
return { notifications: [], offline: false };
|
||||
},
|
||||
watch: {
|
||||
offline(value) {
|
||||
console.log(`we are ${value ? "offline" : "online"}`);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
raise(msg) {
|
||||
if (this.offline) return;
|
||||
if (!msg.level) msg.level = "error";
|
||||
const now = new Date();
|
||||
const latestMsg = this.notifications[0];
|
||||
if (latestMsg && latestMsg.message === msg.message && latestMsg.lp === msg.lp) {
|
||||
latestMsg.count++;
|
||||
latestMsg.time = now;
|
||||
} else {
|
||||
this.notifications = [
|
||||
{
|
||||
...msg,
|
||||
count: 1,
|
||||
time: now,
|
||||
},
|
||||
...this.notifications,
|
||||
];
|
||||
}
|
||||
},
|
||||
clear() {
|
||||
this.notifications = [];
|
||||
},
|
||||
setOnline() {
|
||||
this.offline = false;
|
||||
sendToApp({ type: "online" });
|
||||
},
|
||||
setOffline() {
|
||||
this.offline = true;
|
||||
sendToApp({ type: "offline" });
|
||||
},
|
||||
},
|
||||
render() {
|
||||
return h(App, { notifications: this.notifications, offline: this.offline });
|
||||
},
|
||||
});
|
||||
|
||||
const i18n = setupI18n();
|
||||
const head = createHead();
|
||||
|
||||
app.use(i18n);
|
||||
app.use(setupRouter(i18n));
|
||||
app.use(featureflags);
|
||||
app.use(head);
|
||||
app.mixin(VueHeadMixin);
|
||||
window.app = app.mount("#app");
|
||||
|
||||
watchThemeChanges();
|
||||
appDetection();
|
||||
|
||||
if (window.evcc.customCss === "true") {
|
||||
const link = document.createElement("link");
|
||||
link.href = `./custom.css`;
|
||||
link.rel = "stylesheet";
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
87
assets/js/app.ts
Normal file
87
assets/js/app.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
import "../css/app.css";
|
||||
import { createApp, defineComponent, h } from "vue";
|
||||
import { VueHeadMixin, createHead } from "@unhead/vue/client";
|
||||
import App from "./views/App.vue";
|
||||
import setupRouter from "./router.ts";
|
||||
import setupI18n from "./i18n.ts";
|
||||
import featureflags from "./featureflags.ts";
|
||||
import { watchThemeChanges } from "./theme.ts";
|
||||
import { appDetection, sendToApp } from "./utils/native";
|
||||
import type { Notification } from "./types/evcc";
|
||||
|
||||
// lazy load smoothscroll polyfill. mainly for safari < 15.4
|
||||
if (!window.CSS.supports("scroll-behavior", "smooth")) {
|
||||
console.log("no native smoothscroll support. polyfilling...");
|
||||
import("smoothscroll-polyfill").then((module) => {
|
||||
module.polyfill();
|
||||
});
|
||||
}
|
||||
|
||||
const app = createApp(
|
||||
defineComponent({
|
||||
data() {
|
||||
return { notifications: [] as Notification[], offline: false };
|
||||
},
|
||||
watch: {
|
||||
offline(value) {
|
||||
console.log(`we are ${value ? "offline" : "online"}`);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
raise(msg: Notification) {
|
||||
if (this.offline) return;
|
||||
if (!msg.level) msg.level = "error";
|
||||
const now = new Date();
|
||||
const latestMsg = this.notifications[0];
|
||||
if (latestMsg && latestMsg.message === msg.message && latestMsg.lp === msg.lp) {
|
||||
latestMsg.count++;
|
||||
latestMsg.time = now;
|
||||
} else {
|
||||
this.notifications = [
|
||||
{
|
||||
...msg,
|
||||
count: 1,
|
||||
time: now,
|
||||
},
|
||||
...this.notifications,
|
||||
];
|
||||
}
|
||||
},
|
||||
clear() {
|
||||
this.notifications = [];
|
||||
},
|
||||
setOnline() {
|
||||
this.offline = false;
|
||||
sendToApp({ type: "online" });
|
||||
},
|
||||
setOffline() {
|
||||
this.offline = true;
|
||||
sendToApp({ type: "offline" });
|
||||
},
|
||||
},
|
||||
render() {
|
||||
return h(App, { notifications: this.notifications, offline: this.offline });
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const i18n = setupI18n();
|
||||
const head = createHead();
|
||||
|
||||
app.use(i18n);
|
||||
app.use(setupRouter(i18n.global));
|
||||
app.use(featureflags);
|
||||
app.use(head);
|
||||
app.mixin(VueHeadMixin);
|
||||
window.app = app.mount("#app");
|
||||
|
||||
watchThemeChanges();
|
||||
appDetection();
|
||||
|
||||
if (window.evcc.customCss === "true") {
|
||||
const link = document.createElement("link");
|
||||
link.href = `./custom.css`;
|
||||
link.rel = "stylesheet";
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
|
@ -71,7 +71,7 @@ export function getAndClearNextModal() {
|
|||
return nextModal;
|
||||
}
|
||||
|
||||
export function openLoginModal(nextUrl = null, nextModal = null) {
|
||||
export function openLoginModal(nextUrl: string | null = null, nextModal: Modal | null = null) {
|
||||
auth.nextUrl = nextUrl;
|
||||
auth.nextModal = nextModal;
|
||||
const modal = Modal.getOrCreateInstance(document.getElementById("loginModal") as HTMLElement);
|
||||
|
|
|
|||
|
|
@ -95,13 +95,18 @@
|
|||
import TelemetrySettings from "../TelemetrySettings.vue";
|
||||
import FormRow from "../Helper/FormRow.vue";
|
||||
import SelectGroup from "../Helper/SelectGroup.vue";
|
||||
import { getLocalePreference, setLocalePreference, LOCALES, removeLocalePreference } from "@/i18n";
|
||||
import { getThemePreference, setThemePreference, THEMES } from "@/theme";
|
||||
import {
|
||||
getLocalePreference,
|
||||
setLocalePreference,
|
||||
LOCALES,
|
||||
removeLocalePreference,
|
||||
} from "@/i18n.ts";
|
||||
import { getThemePreference, setThemePreference } from "@/theme.ts";
|
||||
import { getUnits, setUnits, UNITS, is12hFormat, set12hFormat } from "@/units";
|
||||
import { getHiddenFeatures, setHiddenFeatures } from "@/featureflags";
|
||||
import { getHiddenFeatures, setHiddenFeatures } from "@/featureflags.ts";
|
||||
import { isApp } from "@/utils/native";
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import type { Sponsor } from "@/types/evcc";
|
||||
import { THEME, type Sponsor } from "@/types/evcc";
|
||||
|
||||
const TIME_12H = "12";
|
||||
const TIME_24H = "24";
|
||||
|
|
@ -120,7 +125,7 @@ export default defineComponent({
|
|||
timeFormat: is12hFormat() ? TIME_12H : TIME_24H,
|
||||
hiddenFeatures: getHiddenFeatures(),
|
||||
fullscreenActive: false,
|
||||
THEMES,
|
||||
THEMES: Object.values(THEME),
|
||||
UNITS,
|
||||
TIME_FORMATS: [TIME_24H, TIME_12H],
|
||||
};
|
||||
|
|
@ -157,7 +162,8 @@ export default defineComponent({
|
|||
},
|
||||
language(value) {
|
||||
const i18n = this.$root?.$i18n;
|
||||
if (value) {
|
||||
if (!i18n) return;
|
||||
else if (value) {
|
||||
setLocalePreference(i18n, value);
|
||||
} else {
|
||||
removeLocalePreference(i18n);
|
||||
|
|
|
|||
|
|
@ -210,8 +210,8 @@ import LiveCommunity from "./LiveCommunity.vue";
|
|||
import TelemetrySettings from "../TelemetrySettings.vue";
|
||||
import CustomSelect from "../Helper/CustomSelect.vue";
|
||||
import settings from "@/settings";
|
||||
import api, { allowClientError } from "@/api";
|
||||
import { docsPrefix } from "@/i18n";
|
||||
import api, { allowClientError } from "@/api.ts";
|
||||
import { docsPrefix } from "@/i18n.ts";
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import type { CURRENCY, Rate, SelectOption, Sponsor as SponsorType } from "@/types/evcc";
|
||||
import type { Period } from "./types";
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
import settings from "./settings";
|
||||
|
||||
export function setHiddenFeatures(value) {
|
||||
settings.hiddenFeatures = value;
|
||||
}
|
||||
|
||||
export function getHiddenFeatures() {
|
||||
return settings.hiddenFeatures;
|
||||
}
|
||||
|
||||
export default {
|
||||
install: (app) => {
|
||||
app.config.globalProperties.$hiddenFeatures = getHiddenFeatures;
|
||||
},
|
||||
};
|
||||
16
assets/js/featureflags.ts
Normal file
16
assets/js/featureflags.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import type { App } from "vue";
|
||||
import settings from "./settings";
|
||||
|
||||
export function setHiddenFeatures(value: boolean) {
|
||||
settings.hiddenFeatures = value;
|
||||
}
|
||||
|
||||
export function getHiddenFeatures() {
|
||||
return settings.hiddenFeatures;
|
||||
}
|
||||
|
||||
export default {
|
||||
install: (app: App) => {
|
||||
app.config.globalProperties.$hiddenFeatures = getHiddenFeatures;
|
||||
},
|
||||
};
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
import { nextTick } from "vue";
|
||||
import { createI18n } from "vue-i18n";
|
||||
import en from "../../i18n/en.json";
|
||||
import { i18n as i18nApi } from "./api";
|
||||
import settings from "./settings";
|
||||
|
||||
// https://github.com/joker-x/languages.js/blob/master/languages.json
|
||||
export const LOCALES = {
|
||||
ar: ["Arabic", "العربية"],
|
||||
bg: ["Bulgarian", "Български"],
|
||||
ca: ["Catalan", "Català"],
|
||||
cs: ["Czech", "Česky"],
|
||||
da: ["Danish", "Dansk"],
|
||||
de: ["German", "Deutsch"],
|
||||
el: ["Greek", "Ελληνικά"],
|
||||
en: ["English", "English"],
|
||||
es: ["Spanish", "Español"],
|
||||
et: ["Estonian", "Eesti"],
|
||||
fi: ["Finnish", "Suomi"],
|
||||
fr: ["French", "Français"],
|
||||
hr: ["Croatian", "Hrvatski"],
|
||||
hu: ["Hungarian", "Magyar"],
|
||||
it: ["Italian", "Italiano"],
|
||||
lb: ["Luxembourgish", "Lëtzebuergesch"],
|
||||
lt: ["Lithuanian", "Lietuvių"],
|
||||
nl: ["Dutch", "Nederlands"],
|
||||
no: ["Norwegian", "Norsk"],
|
||||
pl: ["Polish", "Polski"],
|
||||
pt: ["Portuguese", "Português"],
|
||||
ro: ["Romanian", "Română"],
|
||||
ru: ["Russian", "Русский"],
|
||||
sk: ["Slovak", "Slovenčina"],
|
||||
sl: ["Slovenian", "Slovenščina"],
|
||||
sv: ["Swedish", "Svenska"],
|
||||
ta: ["Tamil", "தமிழ்"],
|
||||
tr: ["Turkish", "Türkçe"],
|
||||
uk: ["Ukrainian", "Українська"],
|
||||
"zh-Hans": ["Chinese (Simplified)", "简体中文"],
|
||||
};
|
||||
|
||||
export const DEFAULT_LOCALE = "en";
|
||||
const htmlLang = document.querySelector("html").getAttribute("lang");
|
||||
const DEFAULT_BROWSER_LOCALE = htmlLang?.length == 2 ? htmlLang : DEFAULT_LOCALE;
|
||||
|
||||
export function getLocalePreference() {
|
||||
return settings.locale;
|
||||
}
|
||||
|
||||
export function removeLocalePreference(i18n) {
|
||||
settings.locale = null;
|
||||
setI18nLanguage(i18n, DEFAULT_BROWSER_LOCALE);
|
||||
ensureCurrentLocaleMessages(i18n);
|
||||
}
|
||||
|
||||
export function setLocalePreference(i18n, locale) {
|
||||
if (!LOCALES[locale]) {
|
||||
console.error("unknown locale", locale);
|
||||
return;
|
||||
}
|
||||
settings.locale = locale;
|
||||
setI18nLanguage(i18n, locale);
|
||||
ensureCurrentLocaleMessages(i18n);
|
||||
}
|
||||
|
||||
function getLocale() {
|
||||
return getLocalePreference() || DEFAULT_BROWSER_LOCALE;
|
||||
}
|
||||
|
||||
export default function setupI18n() {
|
||||
const i18n = createI18n({
|
||||
legacy: true,
|
||||
silentFallbackWarn: true,
|
||||
silentTranslationWarn: true,
|
||||
locale: DEFAULT_LOCALE,
|
||||
fallbackLocale: DEFAULT_LOCALE,
|
||||
messages: { en },
|
||||
});
|
||||
setI18nLanguage(i18n.global, getLocale());
|
||||
return i18n;
|
||||
}
|
||||
|
||||
export function setI18nLanguage(i18n, locale) {
|
||||
i18n.locale = locale;
|
||||
document.querySelector("html").setAttribute("lang", locale);
|
||||
}
|
||||
|
||||
async function loadLocaleMessages(i18n, locale) {
|
||||
try {
|
||||
const response = await i18nApi.get(`${locale}.json`, { params: { v: window.evcc?.version } });
|
||||
i18n.setLocaleMessage(locale, response.data);
|
||||
} catch (e) {
|
||||
console.error(`unable to load translation for [${locale}]`, e);
|
||||
}
|
||||
|
||||
return nextTick();
|
||||
}
|
||||
|
||||
export async function ensureCurrentLocaleMessages(i18n) {
|
||||
const { locale } = i18n;
|
||||
if (!i18n.availableLocales.includes(locale)) {
|
||||
await loadLocaleMessages(i18n, locale);
|
||||
}
|
||||
}
|
||||
|
||||
export function docsPrefix() {
|
||||
const locale = getLocale();
|
||||
const path = locale === "de" ? "" : `/en`;
|
||||
return `https://docs.evcc.io${path}`;
|
||||
}
|
||||
113
assets/js/i18n.ts
Normal file
113
assets/js/i18n.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import { nextTick } from "vue";
|
||||
import { createI18n, type VueI18nInstance } from "vue-i18n";
|
||||
import en from "../../i18n/en.json";
|
||||
import { i18n as i18nApi } from "./api";
|
||||
import settings from "./settings";
|
||||
|
||||
// https://github.com/joker-x/languages.js/blob/master/languages.json
|
||||
export const LOCALES = {
|
||||
ar: ["Arabic", "العربية"],
|
||||
bg: ["Bulgarian", "Български"],
|
||||
ca: ["Catalan", "Català"],
|
||||
cs: ["Czech", "Česky"],
|
||||
da: ["Danish", "Dansk"],
|
||||
de: ["German", "Deutsch"],
|
||||
el: ["Greek", "Ελληνικά"],
|
||||
en: ["English", "English"],
|
||||
es: ["Spanish", "Español"],
|
||||
et: ["Estonian", "Eesti"],
|
||||
fi: ["Finnish", "Suomi"],
|
||||
fr: ["French", "Français"],
|
||||
hr: ["Croatian", "Hrvatski"],
|
||||
hu: ["Hungarian", "Magyar"],
|
||||
it: ["Italian", "Italiano"],
|
||||
lb: ["Luxembourgish", "Lëtzebuergesch"],
|
||||
lt: ["Lithuanian", "Lietuvių"],
|
||||
nl: ["Dutch", "Nederlands"],
|
||||
no: ["Norwegian", "Norsk"],
|
||||
pl: ["Polish", "Polski"],
|
||||
pt: ["Portuguese", "Português"],
|
||||
ro: ["Romanian", "Română"],
|
||||
ru: ["Russian", "Русский"],
|
||||
sk: ["Slovak", "Slovenčina"],
|
||||
sl: ["Slovenian", "Slovenščina"],
|
||||
sv: ["Swedish", "Svenska"],
|
||||
ta: ["Tamil", "தமிழ்"],
|
||||
tr: ["Turkish", "Türkçe"],
|
||||
uk: ["Ukrainian", "Українська"],
|
||||
"zh-Hans": ["Chinese (Simplified)", "简体中文"],
|
||||
};
|
||||
|
||||
export const DEFAULT_LOCALE = "en";
|
||||
const htmlLang = document.querySelector("html")?.getAttribute("lang");
|
||||
const DEFAULT_BROWSER_LOCALE = htmlLang?.length == 2 ? htmlLang : DEFAULT_LOCALE;
|
||||
|
||||
export function getLocalePreference() {
|
||||
return settings.locale;
|
||||
}
|
||||
|
||||
export function removeLocalePreference(i18n: VueI18nInstance) {
|
||||
settings.locale = null;
|
||||
setI18nLanguage(i18n, DEFAULT_BROWSER_LOCALE as typeof i18n.locale);
|
||||
ensureCurrentLocaleMessages(i18n);
|
||||
}
|
||||
|
||||
export function setLocalePreference(i18n: VueI18nInstance, locale: typeof i18n.locale) {
|
||||
if (!LOCALES.hasOwnProperty(locale)) {
|
||||
console.error("unknown locale", locale);
|
||||
return;
|
||||
}
|
||||
settings.locale = locale;
|
||||
setI18nLanguage(i18n, locale);
|
||||
ensureCurrentLocaleMessages(i18n);
|
||||
}
|
||||
|
||||
function getLocale() {
|
||||
return getLocalePreference() || DEFAULT_BROWSER_LOCALE;
|
||||
}
|
||||
|
||||
export default function setupI18n() {
|
||||
const i18n = createI18n({
|
||||
legacy: true,
|
||||
silentFallbackWarn: true,
|
||||
silentTranslationWarn: true,
|
||||
locale: DEFAULT_LOCALE,
|
||||
fallbackLocale: DEFAULT_LOCALE,
|
||||
messages: { "en-US": en },
|
||||
});
|
||||
setI18nLanguage(i18n.global, getLocale());
|
||||
return i18n;
|
||||
}
|
||||
|
||||
export function setI18nLanguage(i18n: VueI18nInstance, locale: typeof i18n.locale) {
|
||||
i18n.locale = locale;
|
||||
document.querySelector("html")?.setAttribute("lang", locale);
|
||||
}
|
||||
|
||||
async function loadLocaleMessages(i18n: VueI18nInstance, locale: typeof i18n.locale) {
|
||||
try {
|
||||
const response = await i18nApi.get(`${locale}.json`, {
|
||||
params: { v: window.evcc?.version },
|
||||
});
|
||||
if ("setLocaleMessage" in i18n) {
|
||||
i18n.setLocaleMessage(locale, response.data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`unable to load translation for [${locale}]`, e);
|
||||
}
|
||||
|
||||
return nextTick();
|
||||
}
|
||||
|
||||
export async function ensureCurrentLocaleMessages(i18n: VueI18nInstance) {
|
||||
const { locale } = i18n;
|
||||
if (!i18n.availableLocales.includes(locale)) {
|
||||
await loadLocaleMessages(i18n, locale);
|
||||
}
|
||||
}
|
||||
|
||||
export function docsPrefix() {
|
||||
const locale = getLocale();
|
||||
const path = locale === "de" ? "" : `/en`;
|
||||
return `https://docs.evcc.io${path}`;
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import { reactive } from "vue";
|
||||
import api from "./api";
|
||||
|
||||
const restart = reactive({
|
||||
restartNeeded: false,
|
||||
restarting: false,
|
||||
});
|
||||
|
||||
export async function performRestart() {
|
||||
try {
|
||||
await api.post("/system/shutdown");
|
||||
restart.restarting = true;
|
||||
} catch (e) {
|
||||
alert("Unabled to restart server.", e);
|
||||
}
|
||||
}
|
||||
|
||||
export function restartComplete() {
|
||||
restart.restarting = false;
|
||||
restart.restartNeeded = false;
|
||||
}
|
||||
|
||||
export default restart;
|
||||
23
assets/js/restart.ts
Normal file
23
assets/js/restart.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { reactive } from "vue";
|
||||
import api from "./api";
|
||||
|
||||
const restart = reactive({
|
||||
restartNeeded: false,
|
||||
restarting: false,
|
||||
});
|
||||
|
||||
export async function performRestart() {
|
||||
try {
|
||||
await api.post("/system/shutdown");
|
||||
restart.restarting = true;
|
||||
} catch (e) {
|
||||
alert(`Unable to restart server. ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function restartComplete() {
|
||||
restart.restarting = false;
|
||||
restart.restartNeeded = false;
|
||||
}
|
||||
|
||||
export default restart;
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
import { createRouter, createWebHashHistory } from "vue-router";
|
||||
import Modal from "bootstrap/js/dist/modal";
|
||||
import { ensureCurrentLocaleMessages } from "./i18n";
|
||||
import {
|
||||
openLoginModal,
|
||||
statusUnknown,
|
||||
updateAuthStatus,
|
||||
isLoggedIn,
|
||||
isConfigured,
|
||||
} from "./components/Auth/auth";
|
||||
|
||||
function hideAllModals() {
|
||||
[...document.querySelectorAll(".modal.show")].forEach((modal) => {
|
||||
// skip unclosable modals
|
||||
if (modal.getAttribute("data-bs-backdrop") === "static") return;
|
||||
|
||||
const modalInstance = Modal.getInstance(modal);
|
||||
if (modalInstance) {
|
||||
modalInstance.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureAuth(to) {
|
||||
await updateAuthStatus();
|
||||
if (!isConfigured()) {
|
||||
return false;
|
||||
}
|
||||
if (!isLoggedIn() && !statusUnknown()) {
|
||||
openLoginModal(to.path);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export default function setupRouter(i18n) {
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: "/",
|
||||
component: () => import("./views/Main.vue"),
|
||||
props: (route) => {
|
||||
const { lp } = route.query;
|
||||
return {
|
||||
selectedLoadpointIndex: lp ? parseInt(lp, 10) - 1 : undefined,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/config",
|
||||
component: () => import("./views/Config.vue"),
|
||||
beforeEnter: ensureAuth,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "/sessions",
|
||||
component: () => import("./views/Sessions.vue"),
|
||||
props: (route) => {
|
||||
const { month, year, loadpoint, vehicle, period } = route.query;
|
||||
return {
|
||||
month: month ? parseInt(month, 10) : undefined,
|
||||
year: year ? parseInt(year, 10) : undefined,
|
||||
period,
|
||||
loadpointFilter: loadpoint,
|
||||
vehicleFilter: vehicle,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/energy",
|
||||
component: () => import("./views/Energy.vue"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "/log",
|
||||
component: () => import("./views/Log.vue"),
|
||||
beforeEnter: ensureAuth,
|
||||
props: (route) => {
|
||||
const { areas, level } = route.query;
|
||||
return {
|
||||
areas: areas ? areas.split(",") : undefined,
|
||||
level,
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
router.beforeEach(async () => {
|
||||
await ensureCurrentLocaleMessages(i18n.global);
|
||||
return true;
|
||||
});
|
||||
router.afterEach(() => {
|
||||
hideAllModals();
|
||||
});
|
||||
return router;
|
||||
}
|
||||
102
assets/js/router.ts
Normal file
102
assets/js/router.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import {
|
||||
createRouter,
|
||||
createWebHashHistory,
|
||||
type RouteLocationNormalizedGeneric,
|
||||
} from "vue-router";
|
||||
import Modal from "bootstrap/js/dist/modal";
|
||||
import { ensureCurrentLocaleMessages } from "./i18n.ts";
|
||||
import {
|
||||
openLoginModal,
|
||||
statusUnknown,
|
||||
updateAuthStatus,
|
||||
isLoggedIn,
|
||||
isConfigured,
|
||||
} from "./components/Auth/auth";
|
||||
import type { VueI18nInstance } from "vue-i18n";
|
||||
|
||||
function hideAllModals() {
|
||||
[...document.querySelectorAll(".modal.show")].forEach((modal) => {
|
||||
// skip unclosable modals
|
||||
if (modal.getAttribute("data-bs-backdrop") === "static") return;
|
||||
|
||||
const modalInstance = Modal.getInstance(modal);
|
||||
if (modalInstance) {
|
||||
modalInstance.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureAuth(to: RouteLocationNormalizedGeneric) {
|
||||
await updateAuthStatus();
|
||||
if (!isConfigured()) {
|
||||
return false;
|
||||
}
|
||||
if (!isLoggedIn() && !statusUnknown()) {
|
||||
openLoginModal(to.path);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export default function setupRouter(i18n: VueI18nInstance) {
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: "/",
|
||||
component: () => import("./views/Main.vue"),
|
||||
props: (route) => {
|
||||
const { lp } = route.query;
|
||||
return {
|
||||
selectedLoadpointIndex: lp ? parseInt(lp as string, 10) - 1 : undefined,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/config",
|
||||
component: () => import("./views/Config.vue"),
|
||||
beforeEnter: ensureAuth,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "/sessions",
|
||||
component: () => import("./views/Sessions.vue"),
|
||||
props: (route) => {
|
||||
const { month, year, loadpoint, vehicle, period } = route.query;
|
||||
return {
|
||||
month: month ? parseInt(month as string, 10) : undefined,
|
||||
year: year ? parseInt(year as string, 10) : undefined,
|
||||
period,
|
||||
loadpointFilter: loadpoint,
|
||||
vehicleFilter: vehicle,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/energy",
|
||||
component: () => import("./views/Energy.vue"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "/log",
|
||||
component: () => import("./views/Log.vue"),
|
||||
beforeEnter: ensureAuth,
|
||||
props: (route) => {
|
||||
const { areas, level } = route.query;
|
||||
return {
|
||||
areas: typeof areas === "string" ? areas.split(",") : undefined,
|
||||
level: typeof level === "string" ? level : undefined,
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
router.beforeEach(async () => {
|
||||
await ensureCurrentLocaleMessages(i18n);
|
||||
return true;
|
||||
});
|
||||
router.afterEach(() => {
|
||||
hideAllModals();
|
||||
});
|
||||
return router;
|
||||
}
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
import settings from "./settings";
|
||||
|
||||
const darkModeMatcher = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)");
|
||||
|
||||
const THEME_AUTO = "auto";
|
||||
const THEME_LIGHT = "light";
|
||||
const THEME_DARK = "dark";
|
||||
export const THEMES = [THEME_AUTO, THEME_LIGHT, THEME_DARK];
|
||||
|
||||
export function getThemePreference() {
|
||||
const theme = settings.theme;
|
||||
if (THEMES.includes(theme)) {
|
||||
return theme;
|
||||
}
|
||||
return THEME_AUTO;
|
||||
}
|
||||
|
||||
export function setThemePreference(theme) {
|
||||
if (!THEMES.includes(theme)) {
|
||||
return;
|
||||
}
|
||||
settings.theme = theme;
|
||||
updateTheme();
|
||||
}
|
||||
|
||||
function setMetaThemeColor(theme) {
|
||||
const themeColors = { light: "#f3f3f7", dark: "#020318" };
|
||||
const $metaThemeColor = document.querySelector("meta[name=theme-color]");
|
||||
if ($metaThemeColor) {
|
||||
$metaThemeColor.setAttribute("content", themeColors[theme]);
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentTheme() {
|
||||
let theme = getThemePreference();
|
||||
if (theme === THEME_AUTO) {
|
||||
theme = darkModeMatcher?.matches ? THEME_DARK : THEME_LIGHT;
|
||||
}
|
||||
return theme;
|
||||
}
|
||||
|
||||
function updateTheme() {
|
||||
const theme = getCurrentTheme();
|
||||
|
||||
// update iOS title bar color
|
||||
setMetaThemeColor(theme);
|
||||
|
||||
// toggle the class on html root
|
||||
const $html = document.querySelector("html");
|
||||
$html.classList.add("no-transitions");
|
||||
$html.classList.toggle("dark", theme === THEME_DARK);
|
||||
window.setTimeout(function () {
|
||||
$html.classList.remove("no-transitions");
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function updateMetaThemeForBackdrop() {
|
||||
const $backdrop = document.querySelector("[data-bs-backdrop=true][aria-modal=true]");
|
||||
// dark if there is a backdrop, otherwise use the current theme
|
||||
const theme = $backdrop ? THEME_DARK : getCurrentTheme();
|
||||
setMetaThemeColor(theme);
|
||||
}
|
||||
|
||||
export function watchThemeChanges() {
|
||||
if (darkModeMatcher && darkModeMatcher.addEventListener) {
|
||||
darkModeMatcher.addEventListener("change", updateTheme);
|
||||
}
|
||||
updateTheme();
|
||||
|
||||
// listen for modal backdrops
|
||||
document.addEventListener("shown.bs.modal", updateMetaThemeForBackdrop);
|
||||
document.addEventListener("hidden.bs.modal", updateMetaThemeForBackdrop);
|
||||
}
|
||||
71
assets/js/theme.ts
Normal file
71
assets/js/theme.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import settings from "./settings";
|
||||
import { THEME } from "./types/evcc";
|
||||
|
||||
const darkModeMatcher = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)");
|
||||
|
||||
export function getThemePreference(): THEME {
|
||||
const theme = settings.theme;
|
||||
if (Object.values(THEME).includes(theme)) {
|
||||
return theme;
|
||||
}
|
||||
return THEME.AUTO;
|
||||
}
|
||||
|
||||
export function setThemePreference(theme: THEME) {
|
||||
if (!Object.values(THEME).includes(theme)) {
|
||||
return;
|
||||
}
|
||||
settings.theme = theme;
|
||||
updateTheme();
|
||||
}
|
||||
|
||||
function setMetaThemeColor(theme: Exclude<THEME, THEME.AUTO>) {
|
||||
const themeColors = { light: "#f3f3f7", dark: "#020318" };
|
||||
const $metaThemeColor = document.querySelector("meta[name=theme-color]");
|
||||
if ($metaThemeColor) {
|
||||
$metaThemeColor.setAttribute("content", themeColors[theme]);
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentTheme() {
|
||||
let theme = getThemePreference();
|
||||
if (theme === THEME.AUTO) {
|
||||
theme = darkModeMatcher?.matches ? THEME.DARK : THEME.LIGHT;
|
||||
}
|
||||
return theme;
|
||||
}
|
||||
|
||||
function updateTheme() {
|
||||
const theme = getCurrentTheme();
|
||||
|
||||
// update iOS title bar color
|
||||
setMetaThemeColor(theme);
|
||||
|
||||
// toggle the class on html root
|
||||
const $html = document.querySelector("html");
|
||||
if ($html) {
|
||||
$html.classList.add("no-transitions");
|
||||
$html.classList.toggle("dark", theme === THEME.DARK);
|
||||
window.setTimeout(function () {
|
||||
$html.classList.remove("no-transitions");
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
function updateMetaThemeForBackdrop() {
|
||||
const $backdrop = document.querySelector("[data-bs-backdrop=true][aria-modal=true]");
|
||||
// dark if there is a backdrop, otherwise use the current theme
|
||||
const theme = $backdrop ? THEME.DARK : getCurrentTheme();
|
||||
setMetaThemeColor(theme);
|
||||
}
|
||||
|
||||
export function watchThemeChanges() {
|
||||
if (darkModeMatcher && darkModeMatcher.addEventListener) {
|
||||
darkModeMatcher.addEventListener("change", updateTheme);
|
||||
}
|
||||
updateTheme();
|
||||
|
||||
// listen for modal backdrops
|
||||
document.addEventListener("shown.bs.modal", updateMetaThemeForBackdrop);
|
||||
document.addEventListener("hidden.bs.modal", updateMetaThemeForBackdrop);
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ declare global {
|
|||
evcc: {
|
||||
version: string;
|
||||
commit: string;
|
||||
customCss: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -48,6 +49,12 @@ export interface LoadpointCompact {
|
|||
chargerFeatureHeating: boolean;
|
||||
}
|
||||
|
||||
export enum THEME {
|
||||
AUTO = "auto",
|
||||
LIGHT = "light",
|
||||
DARK = "dark",
|
||||
}
|
||||
|
||||
export enum CURRENCY {
|
||||
AUD = "AUD",
|
||||
BGN = "BGN",
|
||||
|
|
|
|||
8
package-lock.json
generated
8
package-lock.json
generated
|
|
@ -69,6 +69,7 @@
|
|||
"@storybook/test": "^8.6.4",
|
||||
"@storybook/vue3": "^8.6.4",
|
||||
"@storybook/vue3-vite": "^8.6.4",
|
||||
"@types/smoothscroll-polyfill": "^0.3.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint-plugin-storybook": "^0.12.0",
|
||||
"prettier-plugin-sort-json": "^4.1.1",
|
||||
|
|
@ -3459,6 +3460,13 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/smoothscroll-polyfill": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/smoothscroll-polyfill/-/smoothscroll-polyfill-0.3.4.tgz",
|
||||
"integrity": "sha512-w1krF+TJh7Azz/2/bEN1tmyKoNt3B6bLCn6SfJErzhKAr1w1b06ePx79NXA+3lTw+DbqXN12nk/Ce1UQDVY8yA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"version": "9.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@
|
|||
"@storybook/test": "^8.6.4",
|
||||
"@storybook/vue3": "^8.6.4",
|
||||
"@storybook/vue3-vite": "^8.6.4",
|
||||
"@types/smoothscroll-polyfill": "^0.3.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint-plugin-storybook": "^0.12.0",
|
||||
"prettier-plugin-sort-json": "^4.1.1",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue