Navigation: add version update indicator (#29343)

This commit is contained in:
Michael Geers 2026-04-22 19:49:08 +02:00 • committed by GitHub
parent 1c7cdeb87a
commit 90fdd8ee6b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 85 additions and 18 deletions

View file

@ -1,5 +1,5 @@
<template>
<GenericModal id="aboutModal" :size="modalSize">
<GenericModal id="aboutModal" :size="modalSize" @opened="acknowledge">
<template #title>
<a :href="websiteUrl" target="_blank" rel="noopener noreferrer"
><Logo class="about-logo"
@ -132,8 +132,15 @@ import "@h2d2/shopicons/es/regular/gift";
import "@h2d2/shopicons/es/filled/heart";
import Logo from "./Footer/Logo.vue";
import api from "@/api";
import settings from "@/settings";
import { extractDomain } from "@/utils/extractDomain";
import { isDevelopment, isNightly, getReleaseName, shortCommit } from "@/utils/version";
import {
isDevelopment,
isNightly,
getReleaseName,
shortCommit,
isNewVersionAvailable,
} from "@/utils/version";
import { defineComponent } from "vue";
const GITHUB_REPO = "https://github.com/evcc-io/evcc";
@ -193,11 +200,7 @@ export default defineComponent({
return this.releaseNotes.replaceAll("<h2>Changelog</h2>", "");
},
newVersionAvailable() {
return (
this.availableVersion &&
!this.development &&
this.availableVersion !== this.installed
);
return isNewVersionAvailable(this.installed, this.availableVersion);
},
},
methods: {
@ -213,6 +216,10 @@ export default defineComponent({
releaseNotesUrl(version?: string) {
return `${GITHUB_REPO}/releases/tag/${version}`;
},
acknowledge() {
if (!this.newVersionAvailable) return;
settings.lastAcknowledgedVersion = this.availableVersion!;
},
},
});
</script>

View file

@ -36,6 +36,7 @@
:evopt="evopt"
:installed="installed"
:commit="commit"
:available-version="availableVersion"
/>
</div>
</nav>
@ -75,6 +76,7 @@ export default defineComponent({
evopt: { type: Object as PropType<EvOpt>, required: false },
installed: String,
commit: String,
availableVersion: String,
},
computed: {
hidden() {

View file

@ -19,6 +19,7 @@
:evopt="evopt"
:installed="installed"
:commit="commit"
:available-version="availableVersion"
@close="open = false"
/>
</template>
@ -31,6 +32,8 @@ import Item from "./Item.vue";
import MoreIcon from "../MaterialIcon/More.vue";
import MoreMenu from "./MoreMenu.vue";
import { isUserConfigError } from "@/utils/fatal";
import { isNewVersionAvailable, isNewVersionUnacknowledged } from "@/utils/version";
import settings from "@/settings";
import type { FatalError, Sponsor, EvOpt, AuthProviders } from "@/types/evcc";
export default defineComponent({
@ -46,6 +49,7 @@ export default defineComponent({
evopt: { type: Object as PropType<EvOpt>, required: false },
installed: String,
commit: String,
availableVersion: String,
},
data() {
return { open: false };
@ -69,14 +73,27 @@ export default defineComponent({
showConfigBadge() {
return this.sponsorExpires || isUserConfigError(this.fatal);
},
newVersionAvailable() {
return isNewVersionAvailable(this.installed, this.availableVersion);
},
showVersionBadge() {
return isNewVersionUnacknowledged(
this.installed,
this.availableVersion,
settings.lastAcknowledgedVersion
);
},
showRootBadge() {
return this.authorizationRequired || this.showConfigBadge;
return this.authorizationRequired || this.showConfigBadge || this.showVersionBadge;
},
badgeClass() {
if (this.fatal.length > 0) {
return "bg-danger";
}
return "bg-warning";
if (this.authorizationRequired || this.showConfigBadge) {
return "bg-warning";
}
return "bg-darker-green";
},
},
methods: {

View file

@ -11,8 +11,14 @@
class="dropdown-item d-flex align-items-center"
@click="openAboutModal"
>
<span v-if="showVersionBadge" class="circle-badge me-1 bg-darker-green"></span>
<span>evcc</span>
<span class="ms-2 text-muted small">{{ versionLabel }}</span>
<shopicon-regular-gift
v-if="newVersionAvailable"
size="s"
class="ms-2 text-gray-light gift-icon"
></shopicon-regular-gift>
</button>
<button v-if="isApp" type="button" class="dropdown-item" @click="openNativeSettings">
{{ $t("header.nativeSettings") }}
@ -65,9 +71,15 @@
<script lang="ts">
import Modal from "bootstrap/js/dist/modal";
import "@h2d2/shopicons/es/regular/gift";
import { logout, isLoggedIn } from "../Auth/auth";
import { isApp, sendToApp } from "@/utils/native";
import { getShortVersion } from "@/utils/version";
import {
getShortVersion,
isNewVersionAvailable,
isNewVersionUnacknowledged,
} from "@/utils/version";
import settings from "@/settings";
import { isUserConfigError } from "@/utils/fatal";
import { defineComponent, type PropType } from "vue";
import type { FatalError, Sponsor, EvOpt, AuthProviders } from "@/types/evcc";
@ -84,6 +96,7 @@ export default defineComponent({
evopt: { type: Object as PropType<EvOpt>, required: false },
installed: String,
commit: String,
availableVersion: String,
},
emits: ["close"],
data() {
@ -120,6 +133,16 @@ export default defineComponent({
versionLabel() {
return getShortVersion(this.installed || "", this.commit);
},
newVersionAvailable() {
return isNewVersionAvailable(this.installed, this.availableVersion);
},
showVersionBadge() {
return isNewVersionUnacknowledged(
this.installed,
this.availableVersion,
settings.lastAcknowledgedVersion
);
},
optimizeAvailable() {
return !!this.evopt && this.experimental;
},
@ -226,4 +249,9 @@ export default defineComponent({
color: var(--bs-primary);
border-left: 2px solid var(--bs-primary);
}
.gift-icon {
position: relative;
top: -2px;
}
</style>

View file

@ -40,7 +40,7 @@ import Modal from "bootstrap/js/dist/modal";
import "@h2d2/shopicons/es/regular/gift";
import "@h2d2/shopicons/es/regular/moonstars";
import Logo from "./Logo.vue";
import { isDevelopment } from "@/utils/version";
import { isNewVersionAvailable } from "@/utils/version";
import { defineComponent } from "vue";
export default defineComponent({
@ -56,11 +56,7 @@ export default defineComponent({
return `https://github.com/evcc-io/evcc/commit/${this.commit}`;
},
newVersionAvailable() {
return (
this.available &&
!isDevelopment(this.installed || "") &&
this.available !== this.installed
);
return isNewVersionAvailable(this.installed, this.available);
},
},
methods: {

View file

@ -27,13 +27,14 @@ const LAST_TARGET_TIME = "last_target_time";
const LAST_SOC_GOAL = "last_soc_goal";
const LAST_ENERGY_GOAL = "last_energy_goal";
const CONFIG_CARD_HEIGHTS = "config_card_heights";
const LAST_ACKNOWLEDGED_VERSION = "last_acknowledged_version";
function read(key: string) {
return window.localStorage[key];
}
function save(key: string) {
return (value: string | null) => {
return (value: string | null | undefined) => {
try {
if (value) {
window.localStorage[key] = value;
@ -132,6 +133,7 @@ export interface Settings {
lastSocGoal: number | undefined;
lastEnergyGoal: number | undefined;
cardHeights: Record<string, number>;
lastAcknowledgedVersion: string | undefined;
}
const settings: Settings = reactive({
@ -160,6 +162,7 @@ const settings: Settings = reactive({
lastSocGoal: readNumber(LAST_SOC_GOAL),
lastEnergyGoal: readNumber(LAST_ENERGY_GOAL),
cardHeights: readJSON(CONFIG_CARD_HEIGHTS),
lastAcknowledgedVersion: read(LAST_ACKNOWLEDGED_VERSION),
});
watch(() => settings.locale, save(SETTINGS_LOCALE));
@ -187,6 +190,7 @@ watch(() => settings.lastTargetTime, save(LAST_TARGET_TIME));
watch(() => settings.lastSocGoal, saveNumber(LAST_SOC_GOAL));
watch(() => settings.lastEnergyGoal, saveNumber(LAST_ENERGY_GOAL));
watch(() => settings.cardHeights, saveJSON(CONFIG_CARD_HEIGHTS), { deep: true });
watch(() => settings.lastAcknowledgedVersion, save(LAST_ACKNOWLEDGED_VERSION));
export default settings;

View file

@ -75,6 +75,7 @@ export interface State {
authProviders?: AuthProviders;
evopt?: EvOpt;
version?: string;
availableVersion?: string;
system?: string;
timezone?: string;
battery?: Battery;

View file

@ -19,5 +19,17 @@ export function shortCommit(commit?: string): string {
export function getShortVersion(version: string, commit?: string): string {
if (isDevelopment(version)) return "dev build";
if (isNightly(version, commit)) return `v${version} (${shortCommit(commit)})`;
return version;
return `v${version}`;
}
export function isNewVersionAvailable(installed?: string, available?: string): boolean {
return !!available && !isDevelopment(installed || "") && available !== installed;
}
export function isNewVersionUnacknowledged(
installed?: string,
available?: string,
acknowledged?: string
): boolean {
return isNewVersionAvailable(installed, available) && available !== acknowledged;
}