Migrate to Bootstrap 5, improve notifications, add supporter confetti (#954)

This commit is contained in:
Michael Geers 2021-05-02 13:37:57 +02:00 • committed by GitHub
parent 4eefe8f576
commit 380cd461b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 2354 additions and 1994 deletions

View file

@ -1,4 +1,4 @@
module.exports = {
stories: ["../assets/js/**/*.stories.mdx", "../assets/js/**/*.stories.@(js|jsx|ts|tsx)"],
addons: ["@storybook/addon-links", "@storybook/addon-essentials"],
addons: ["@storybook/addon-links", "@storybook/addon-essentials", "@storybook/addon-postcss"],
};

View file

@ -1,2 +1,4 @@
<link href="/css/bootstrap.min.css" rel="stylesheet" />
<script src="/js/bootstrap.min.js"></script>
<link href="/css/app.css" rel="stylesheet" />

View file

@ -31,6 +31,5 @@
</script>
<div id="app"></div>
<div id="toasts"></div>
</body>
</html>

View file

@ -1,11 +1,9 @@
import "../css/app.css";
import "bootstrap/dist/css/bootstrap.min.css";
import "popper.js";
import "bootstrap";
import Vue from "vue";
import axios from "axios";
import App from "./views/App";
import Toasts from "./components/Toasts";
import router from "./router";
import store from "./store";
@ -14,29 +12,21 @@ axios.defaults.baseURL =
loc.protocol + "//" + loc.hostname + (loc.port ? ":" + loc.port : "") + loc.pathname + "api";
axios.defaults.headers.post["Content-Type"] = "application/json";
window.toasts = new Vue({
el: "#toasts",
window.app = new Vue({
el: "#app",
router,
data: { store, notifications: [] },
render: function (h) {
return h(Toasts, { props: { items: this.items, count: this.count } });
return h(App, { props: { notifications: this.notifications } });
},
data: {
items: {},
count: 0,
},
name: "ToastsRoot",
methods: {
raise: function (msg) {
let found = false;
Object.keys(this.items).forEach(function (k) {
let m = this.items[k];
if (m.type == msg.type && m.message == msg.message) {
found = true;
}
}, this);
if (!found) {
msg.id = this.count++;
Vue.set(this.items, msg.id, msg);
}
console[msg.type](msg);
const withoutThisMsg = this.notifications.filter((m) => m.message !== msg.message);
this.notifications = [msg, ...withoutThisMsg];
},
clear: function () {
this.notifications = [];
},
error: function (msg) {
msg.type = "error";
@ -46,22 +36,11 @@ window.toasts = new Vue({
msg.type = "warn";
this.raise(msg);
},
remove: function (msg) {
Vue.delete(this.items, msg.id);
},
},
});
new Vue({
el: "#app",
router,
data: { store },
render: (h) => h(App),
});
window.setInterval(function () {
axios.get("health").catch(function (res) {
res.message = "Server unavailable";
window.toasts.error(res);
axios.get("health").catch(function () {
window.app.error({ message: "Server unavailable" });
});
}, 5000);

View file

@ -16,6 +16,7 @@ export const KeinUpdate = Template.bind({});
KeinUpdate.args = {
version: { installed: "0.40" },
};
export const UpdateVerfuegbar = Template.bind({});
UpdateVerfuegbar.args = {
version: {
@ -25,6 +26,15 @@ UpdateVerfuegbar.args = {
},
};
export const Sponsor = Template.bind({});
Sponsor.args = {
version: {
installed: "0.36",
available: "0.40",
},
sponsor: "naltatis",
};
export const Updater = Template.bind({});
Updater.args = {
version: {

View file

@ -1,18 +1,10 @@
<template>
<footer class="py-3 py-md-5 mt-3 mt-md-5 border-top">
<div class="container">
<div class="row">
<div class="col-6">
<Version v-bind="version" />
</div>
<div class="col-6 text-right">
<small class="text-black">
<a href="https://github.com/sponsors/andig" target="_blank" class="link">
<fa-icon icon="heart" class="icon mr-1"></fa-icon
><span class="d-none d-sm-inline">Projekt </span>unterstützen
</a>
</small>
</div>
<footer class="container">
<div class="py-3 py-md-5 mt-3 mt-md-5 border-top">
<div class="d-flex justify-content-between">
<Version v-bind="version" />
<!-- Please don't mess with the sponsor status. You risk loosing your mojo. -->
<Sponsor :sponsor="sponsor" />
</div>
</div>
</footer>
@ -20,21 +12,14 @@
<script>
import Version from "./Version";
import Sponsor from "./Sponsor";
export default {
name: "Footer",
components: { Version },
components: { Version, Sponsor },
props: {
version: Object,
sponsor: String,
},
};
</script>
<style scoped>
.icon {
color: #0fdd42;
}
.text-black a {
color: #18191a;
}
</style>

View file

@ -71,7 +71,7 @@ export default {
chargeCurrent: Number,
socCapacity: Number,
connectedDuration: Number,
chargeCurrents: Object,
chargeCurrents: Array,
chargeConfigured: Boolean,
chargeRemainingEnergy: Number,
},
@ -119,7 +119,7 @@ export default {
this.mode = response.data.mode;
}.bind(this)
)
.catch(window.toasts.error);
.catch(window.app.error);
},
setTargetSoC: function (soc) {
axios
@ -130,7 +130,7 @@ export default {
this.targetSoC = response.data.targetSoC;
}.bind(this)
)
.catch(window.toasts.error);
.catch(window.app.error);
},
},
destroyed: function () {

View file

@ -5,17 +5,17 @@
<div class="mb-2 value">
Leistung
<fa-icon
class="text-primary ml-1"
class="text-primary ms-1"
icon="temperature-low"
v-if="climater == 'heating'"
></fa-icon>
<fa-icon
class="text-primary ml-1"
class="text-primary ms-1"
icon="temperature-high"
v-if="climater == 'cooling'"
></fa-icon>
<fa-icon
class="text-primary ml-1"
class="text-primary ms-1"
icon="thermometer-half"
v-if="climater == 'on'"
></fa-icon>

View file

@ -1,20 +1,50 @@
<template>
<div>
<div class="mb-3">Modus</div>
<div class="btn-group btn-group-toggle bg-white shadow-none w-100">
<label class="btn btn-outline-primary first" :class="{ active: mode == 'off' }">
<input type="radio" value="off" @click="setTargetMode('off')" />Stop
</label>
<label class="btn btn-outline-primary" :class="{ active: mode == 'now' }">
<input type="radio" value="now" @click="setTargetMode('now')" />Sofort
</label>
<label class="btn btn-outline-primary" :class="{ active: mode == 'minpv' }">
<input type="radio" value="minpv" @click="setTargetMode('minpv')" />
<div class="btn-group w-100" role="group">
<input
id="mode_off"
type="radio"
name="mode"
class="btn-check"
value="off"
@click="setTargetMode('off')"
:checked="mode == 'off'"
/>
<label for="mode_off" class="btn btn-outline-primary"> Stop </label>
<input
id="mode_now"
type="radio"
name="mode"
class="btn-check"
value="now"
@click="setTargetMode('now')"
:checked="mode == 'now'"
/>
<label for="mode_now" class="btn btn-outline-primary"> Sofort </label>
<input
id="mode_minpv"
type="radio"
name="mode"
class="btn-check"
value="minpv"
@click="setTargetMode('minpv')"
:checked="mode == 'minpv'"
/>
<label for="mode_minpv" class="btn btn-outline-primary">
<span class="d-inline d-sm-none">Min</span>
<span class="d-none d-sm-inline">Min + PV</span>
</label>
<label class="btn btn-outline-primary" :class="{ active: mode == 'pv' }">
<input type="radio" value="pv" @click="setTargetMode('pv')" />
<input
id="mode_pv"
type="radio"
name="mode"
class="btn-check"
value="pv"
@click="setTargetMode('pv')"
:checked="mode == 'pv'"
/>
<label for="mode_pv" class="btn btn-outline-primary">
<span class="d-inline d-sm-none">PV</span>
<span class="d-none d-sm-inline">Nur PV</span>
</label>

View file

@ -0,0 +1,32 @@
import Notifications from "./Notifications.vue";
export default {
title: "Main/Notifications",
component: Notifications,
argTypes: {},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { Notifications },
template: '<Notifications v-bind="$props"></Notifications>',
});
export const Base = Template.bind({});
Base.args = {
notifications: [
{
message: "Server unavailable",
type: "error",
},
{
message: "charger out of sync: expected disabled, got enabled",
type: "warn",
},
{
message:
"Amet irure quis incididunt voluptate esse. Commodo ea sunt est ipsum tempor nisi laboris voluptate labore elit laborum. Ex irure commodo reprehenderit consequat consequat do ad tempor aliquip deserunt eu. Laboris minim nostrud quis nisi. Dolor occaecat reprehenderit velit dolore exercitation cupidatat et voluptate. Nulla pariatur deserunt esse minim nisi nisi nulla. Sit eiusmod do incididunt sint minim pariatur aute.",
type: "warn",
},
],
};

View file

@ -0,0 +1,91 @@
<template>
<div>
<button
v-show="iconVisible"
href="#"
data-bs-toggle="modal"
data-bs-target="#notificationModal"
class="btn btn-link text-decoration-none link-light text-nowrap"
>
<fa-icon :class="iconClass" icon="exclamation-triangle"></fa-icon>
</button>
<div
id="notificationModal"
class="modal fade"
tabindex="-1"
role="dialog"
aria-hidden="true"
>
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Meldungen</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<p
v-for="(notification, index) in notifications"
:key="index"
class="d-flex align-items-baseline"
>
<fa-icon
:class="{
'text-danger': notification.type === 'error',
'text-warning': notification.type === 'warn',
}"
class="flex-grow-0 d-block"
icon="exclamation-triangle"
></fa-icon>
<span class="flex-grow-1 px-2 py-1">{{ notification.message }}</span>
</p>
</div>
<div class="modal-footer">
<button
type="button"
data-bs-dismiss="modal"
aria-label="Close"
@click="clear"
class="btn btn-outline-secondary"
>
Meldungen entfernen
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import "../icons";
import formatter from "../mixins/formatter";
export default {
name: "Notifications",
props: {
notifications: Array,
},
computed: {
iconVisible: function () {
return this.notifications.length > 0;
},
iconClass: function () {
return this.notifications.find((m) => m.type === "error")
? "text-danger"
: "text-warning";
},
},
methods: {
clear: function () {
window.app.clear();
},
},
mixins: [formatter],
};
</script>

View file

@ -13,7 +13,7 @@
<template v-for="(loadpoint, id) in loadpoints">
<hr class="w-100 my-4" v-if="id > 0" :key="id + '_hr'" />
<Loadpoint
:key="loadpoint"
:key="id"
v-bind="loadpoint"
:single="loadpoints.length === 1"
:id="id"
@ -43,7 +43,7 @@ export default {
batteryConfigured: Boolean,
batteryPower: Number,
batterySoC: Number,
gridCurrents: Object,
gridCurrents: Array,
prioritySoC: Number,
},
components: { SiteDetails, Loadpoint },

View file

@ -0,0 +1,23 @@
import Sponsor from "./Sponsor.vue";
export default {
title: "Main/Footer/Sponsor",
component: Sponsor,
argTypes: {},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { Sponsor },
template: '<Sponsor v-bind="$props"></Sponsor>',
});
export const Standard = Template.bind({});
Standard.args = {
sponsor: undefined,
};
export const IsSponsor = Template.bind({});
IsSponsor.args = {
sponsor: "naltatis",
};

View file

@ -0,0 +1,83 @@
<template>
<div
ref="sponsor"
v-if="sponsor"
class="btn btn-link pe-0 text-decoration-none link-dark text-nowrap sponsor-button"
@click.stop.prevent="surprise"
>
Danke<span class="d-none d-sm-inline"> {{ sponsor }}</span>
<fa-icon :icon="['fas', 'heart']" class="icon ms-1"></fa-icon>
</div>
<a
v-else
href="https://github.com/sponsors/andig"
target="_blank"
class="btn btn-link pe-0 text-decoration-none link-dark text-nowrap support-button"
>
<span class="d-none d-sm-inline">Projekt </span>unterstützen
<fa-icon :icon="['far', 'heart']" class="icon ms-1 outline"></fa-icon>
<fa-icon :icon="['fas', 'heart']" class="icon ms-1 solid"></fa-icon>
</a>
</template>
<script>
import confetti from "canvas-confetti";
export default {
name: "sponsor",
props: {
sponsor: String,
},
methods: {
surprise: function () {
console.log(this.$refs.sponsor);
const { top, height, left, width } = this.$refs.sponsor.getBoundingClientRect();
const x = (left + width / 2) / window.innerWidth;
const y = (top + height / 2) / window.innerHeight;
const origin = { x, y };
confetti({
origin,
angle: 90 + Math.random() * 35,
particleCount: 75 + Math.random() * 50,
spread: 50 + Math.random() * 50,
drift: -0.5,
scalar: 1.3,
colors: [
"#0d6efd",
"#0fdd42",
"#408458",
"#4923BA",
"#5BC8EC",
"#C54482",
"#CC444A",
"#EE8437",
"#F7C144",
"#FFFD54",
],
});
},
},
};
</script>
<style scoped>
.icon {
color: #0fdd42;
display: inline-block;
}
.sponsor-button {
/* prevent double-tap zoom */
touch-action: none;
user-select: none;
}
.support-button .solid {
display: none;
}
.support-button:hover .solid {
display: inline-block;
}
.support-button:hover .outline {
display: none;
}
</style>

View file

@ -1,38 +0,0 @@
<template>
<div class="toast" data-delay="10000" :data-autohide="true">
<div class="toast-header">
<strong class="mr-auto" v-if="item.type != 'warn'"
><fa-icon class="text-danger" icon="exclamation-triangle"></fa-icon> Error</strong
>
<strong class="mr-auto" v-if="item.type == 'warn'"
><fa-icon class="text-warning" icon="exclamation-triangle"></fa-icon>
Warning</strong
>
<small v-if="item.status">HTTP {{ item.status }}</small>
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="toast-body">{{ item.message }}</div>
</div>
</template>
<script>
import "../icons";
import $ from "jquery";
export default {
name: "ToastMessage",
props: ["item"],
mounted: function () {
const id = "#message-id-" + this.item.id;
$(id).toast("show");
$(id).on(
"hidden.bs.toast",
function () {
window.toasts.remove(this.item);
}.bind(this)
);
},
};
</script>

View file

@ -1,22 +0,0 @@
import Toasts from "./Toasts.vue";
export default {
title: "Main/Toasts",
component: Toasts,
argTypes: {},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { Toasts },
template: '<Toasts v-bind="$props"></Toasts>',
});
export const Base = Template.bind({});
Base.args = {
items: {
id: 1,
message: "Evil warning",
type: "warn",
},
};

View file

@ -1,25 +0,0 @@
<template>
<div aria-atomic="true" style="position: absolute; top: 4rem; right: 0.5rem">
<!-- Position it -->
<ToastMessage
v-for="item in items"
:item="item"
:id="'message-id-' + item.id"
:key="item.id"
>
</ToastMessage>
</div>
</template>
<script>
import "../icons";
import ToastMessage from "./ToastMessage";
export default {
name: "Toasts",
components: { ToastMessage },
props: {
items: Object,
},
};
</script>

View file

@ -25,7 +25,7 @@
></div>
</div>
<div
v-if="hasVehicle && visibleTargetSoC"
v-if="connected && hasVehicle && visibleTargetSoC"
class="target"
:class="{ 'target--max': visibleTargetSoC === 100 }"
>
@ -161,7 +161,7 @@ export default {
color: var(--white);
}
.bg-light {
color: var(--dark);
color: var(--bs-gray-dark);
}
.target-label {
width: 3em;
@ -170,7 +170,7 @@ export default {
position: absolute;
top: calc((var(--label-height) + var(--thumb-overlap)) * -1);
text-align: center;
color: var(--dark);
color: var(--bs-gray-dark);
font-size: 1rem;
opacity: 1;
transition: opacity 0.2s ease 1s;
@ -189,6 +189,7 @@ export default {
}
/* Note: Safari,Chrome,Blink and Firefox specific styles need to be in separate definitions to work */
.target-slider::-webkit-slider-runnable-track {
position: relative;
background: transparent;
border: none;
height: 100%;
@ -209,7 +210,7 @@ export default {
padding: var(--label-height) var(--thumb-horizontal-padding) 0;
box-sizing: content-box;
background-clip: content-box;
background-color: var(--dark);
background-color: var(--bs-gray-dark);
cursor: grab;
border: none;
opacity: 1;
@ -220,10 +221,10 @@ export default {
top: calc(var(--label-height) * -1);
height: 100%;
width: var(--thumb-width);
padding: 0 var(--thumb-horizontal-padding);
padding: var(--label-height) var(--thumb-horizontal-padding) 0;
box-sizing: content-box;
background-clip: content-box;
background-color: var(--dark);
background-color: var(--bs-gray-dark);
cursor: grab;
border: none;
opacity: 1;

View file

@ -2,7 +2,7 @@
<div class="d-flex justify-content-between align-items-center">
<small class="text-secondary">
<span v-if="minSoCActive">
<fa-icon class="text-muted mr-1" icon="exclamation-circle"></fa-icon>
<fa-icon class="text-muted me-1" icon="exclamation-circle"></fa-icon>
Mindestladung bis {{ minSoC }}%
</span>
</small>
@ -15,7 +15,7 @@
}"
>
{{ targetTimeLabel() }}
<fa-icon class="ml-1" icon="clock"></fa-icon>
<fa-icon class="ms-1" icon="clock"></fa-icon>
</small>
</div>
</template>

View file

@ -1,7 +1,7 @@
import Version from "./Version.vue";
export default {
title: "Main/Version",
title: "Main/Footer/Version",
component: Version,
argTypes: {},
};

View file

@ -1,102 +1,96 @@
<template>
<div>
<small class="text-black">
<a href="#" @click.prevent="showModal" v-if="newVersionAvailable">
<fa-icon icon="gift" class="icon mr-1"></fa-icon>Update<span
class="d-none d-sm-inline"
>
verfügbar</span
>:
{{ available }}
</a>
<a :href="releaseNotesUrl(installed)" target="_blank" v-else>
Version {{ installed }}
</a>
</small>
<button
href="#"
data-bs-toggle="modal"
data-bs-target="#updateModal"
v-if="newVersionAvailable"
class="btn btn-link ps-0 text-decoration-none link-dark text-nowrap"
>
<fa-icon icon="gift" class="icon me-1"></fa-icon>
Update<span class="d-none d-sm-inline"> verfügbar</span>: {{ available }}
</button>
<a
:href="releaseNotesUrl(installed)"
target="_blank"
class="btn btn-link ps-0 text-decoration-none link-dark text-nowrap"
v-else
>
Version {{ installed }}
</a>
<transition name="fade">
<div id="updateModal" class="dialog" tabindex="-1" role="dialog" v-if="modalActive">
<div
class="modal-dialog modal-dialog-centered modal-dialog-scrollable"
role="document"
>
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title font-weight-bold">Update verfügbar</h4>
<div id="updateModal" class="modal fade" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Update verfügbar</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<div v-if="updateStarted">
<p>Nach der Aktualisierung wird evcc neu gestartet.</p>
<div class="progress my-3">
<div
class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar"
:style="{ width: uploadProgress + '%' }"
></div>
</div>
<p>{{ updateStatus }}{{ uploadMessage }}</p>
</div>
<div v-else>
<p>
<small>Aktuell installierte Version: {{ installed }}</small>
</p>
<div v-if="releaseNotes" v-html="releaseNotes"></div>
<p v-else>
Keine Releasenotes verfügbar. Mehr Informationen zur neuen Version
findest du
<a :href="releaseNotesUrl(available)">hier</a>.
</p>
</div>
</div>
<div class="modal-footer d-flex justify-content-between">
<button
type="button"
class="btn btn-outline-secondary"
:disabled="updateStarted"
data-bs-dismiss="modal"
>
Abbrechen
</button>
<div>
<button
type="button"
class="close"
class="btn btn-primary"
v-if="hasUpdater"
:disabled="updateStarted"
@click="closeModal"
@click="update"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div v-if="updateStarted">
<p>Nach der Aktualisierung wird evcc neu gestartet.</p>
<div class="progress my-3">
<div
class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar"
:style="{ width: uploadProgress + '%' }"
></div>
</div>
<p>{{ updateStatus }}{{ uploadMessage }}</p>
</div>
<div v-else>
<p>
<small>Aktuell installierte Version: {{ installed }}</small>
</p>
<div v-if="releaseNotes" v-html="releaseNotes"></div>
<p v-else>
Keine Releasenotes verfügbar. Mehr Informationen zur neuen
Version findest du
<a :href="releaseNotesUrl(available)">hier</a>.
</p>
</div>
</div>
<div class="modal-footer d-flex justify-content-between">
<button
type="button"
class="btn btn-outline-secondary"
:disabled="updateStarted"
@click="closeModal"
>
Abbrechen
</button>
<div>
<button
type="button"
class="btn btn-primary"
v-if="hasUpdater"
:disabled="updateStarted"
@click="update"
>
<span v-if="updateStarted">
<span
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
>
</span>
Akualisieren
<span v-if="updateStarted">
<span
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
>
</span>
<span v-else>Jetzt aktualisieren</span>
</button>
<a
:href="releaseNotesUrl(available)"
class="btn btn-primary"
v-else
>
Download
</a>
</div>
Akualisieren
</span>
<span v-else>Jetzt aktualisieren</span>
</button>
<a :href="releaseNotesUrl(available)" class="btn btn-primary" v-else>
Download
</a>
</div>
</div>
</div>
</div>
</transition>
</div>
</div>
</template>
@ -116,18 +110,11 @@ export default {
},
data: function () {
return {
modalActive: false,
updateStarted: false,
updateStatus: "",
};
},
methods: {
showModal: function () {
this.modalActive = true;
},
closeModal: function () {
this.modalActive = false;
},
update: async function () {
try {
await axios.post("update");
@ -155,28 +142,7 @@ export default {
</script>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.25s ease-in;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
.dialog {
position: fixed;
top: 0;
left: 0;
z-index: 1050;
width: 100%;
height: 100%;
overflow: hidden;
outline: 0;
}
.icon {
color: #0fdd42;
}
.text-black a {
color: #18191a;
}
</style>

View file

@ -18,7 +18,8 @@ import { faSun } from "@fortawesome/free-solid-svg-icons/faSun";
import { faTemperatureHigh } from "@fortawesome/free-solid-svg-icons/faTemperatureHigh";
import { faTemperatureLow } from "@fortawesome/free-solid-svg-icons/faTemperatureLow";
import { faThermometerHalf } from "@fortawesome/free-solid-svg-icons/faThermometerHalf";
import { faHeart } from "@fortawesome/free-solid-svg-icons/faHeart";
import { faHeart as farHeart } from "@fortawesome/free-regular-svg-icons/faHeart";
import { faHeart as fasHeart } from "@fortawesome/free-solid-svg-icons/faHeart";
import { faGift } from "@fortawesome/free-solid-svg-icons/faGift";
import { faBox } from "@fortawesome/free-solid-svg-icons/faBox";
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclamationCircle";
@ -40,7 +41,8 @@ library.add(
faTemperatureHigh,
faTemperatureLow,
faThermometerHalf,
faHeart,
farHeart,
fasHeart,
faGift,
faBox,
faExclamationCircle

View file

@ -24,8 +24,8 @@ const store = {
},
update: function (msg) {
Object.keys(msg).forEach(function (k) {
if (typeof window.toasts[k] === "function") {
window.toasts[k]({ message: msg[k] });
if (typeof window.app[k] === "function") {
window.app[k]({ message: msg[k] });
} else {
setProperty(store.state, k.split("."), msg[k]);
}

View file

@ -6,27 +6,36 @@
<a class="navbar-brand" href="https://github.com/andig/evcc">
<Logo class="logo"></Logo>
</a>
<button
class="navbar-toggler"
type="button"
data-toggle="collapse"
data-target="#navbarNavAltMarkup"
aria-controls="navbarNavAltMarkup"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
<div class="navbar-nav mr-auto"></div>
<div class="navbar-nav">
<a
class="nav-item nav-link"
href="https://github.com/andig/evcc/discussions"
target="_blank"
<div class="d-flex">
<div class="d-flex">
<Notifications :notifications="notifications" />
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarNavAltMarkup"
aria-controls="navbarNavAltMarkup"
aria-expanded="false"
aria-label="Toggle navigation"
>
Support
</a>
<span class="navbar-toggler-icon"></span>
</button>
</div>
<div
class="collapse navbar-collapse flex-lg-grow-0"
id="navbarNavAltMarkup"
>
<ul class="navbar-nav">
<li class="nav-item">
<a
class="nav-link"
href="https://github.com/andig/evcc/discussions"
target="_blank"
>
Support
</a>
</li>
</ul>
</div>
</div>
</div>
@ -36,7 +45,7 @@
class="flex-grow-1 d-flex flex-column justify-content-stretch"
></router-view>
</div>
<Footer :version="version"></Footer>
<Footer :version="version" :sponsor="sponsor"></Footer>
</div>
</template>
@ -44,11 +53,13 @@
import "../icons";
import Logo from "../components/Logo";
import Footer from "../components/Footer";
import Notifications from "../components/Notifications";
import store from "../store";
export default {
name: "App",
components: { Logo, Footer },
components: { Logo, Footer, Notifications },
data: function () {
return {
compact: false,
@ -80,7 +91,7 @@ export default {
var msg = JSON.parse(evt.data);
store.update(msg);
} catch (e) {
window.toasts.error(e, evt.data);
window.app.error(e, evt.data);
}
};
},
@ -96,6 +107,12 @@ export default {
uploadProgress: this.store.state.uploadProgress,
};
},
sponsor: function () {
return this.store.state.sponsor;
},
},
props: {
notifications: Array,
},
created: function () {
const urlParams = new URLSearchParams(window.location.search);

6
dist/css/chunk-vendors.0f1ada3b.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
dist/css/index.307488cf.css vendored Normal file
View file

@ -0,0 +1 @@
.progress-bar-animated{animation-direction:reverse}.btn.caption{opacity:1}.btn.first{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}code{font-size:87.5%;color:#e83e8c;word-wrap:break-word}.value,input[type=radio],label.btn{white-space:nowrap!important}.text-muted a,.text-muted a:hover{color:#6c757d!important;text-decoration:none}.bg-muted{opacity:.25}.icon[data-v-0f71c17c]{color:#0fdd42}.icon[data-v-60f66a8e]{color:#0fdd42;display:inline-block}.sponsor-button[data-v-60f66a8e]{touch-action:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.support-button .solid[data-v-60f66a8e]{display:none}.support-button:hover .solid[data-v-60f66a8e]{display:inline-block}.support-button:hover .outline[data-v-60f66a8e]{display:none}.logo[data-v-59c735a7]{width:85px}.app[data-v-59c735a7]{min-height:100vh}.btn[data-v-a92589b2]{flex-basis:0}.vehicle-soc[data-v-61c611fd]{--height:38px;--thumb-overlap:3px;--thumb-width:3px;--thumb-horizontal-padding:15px;--label-height:26px;position:relative;height:var(--height)}.progress[data-v-61c611fd]{height:100%;font-size:1rem}.progress-bar.bg-muted[data-v-61c611fd]{color:var(--white)}.bg-light[data-v-61c611fd]{color:var(--bs-gray-dark)}.target-label[data-v-61c611fd]{width:3em;margin-left:-1.5em;height:var(--label-height);position:absolute;top:calc(var(--label-height)*-1 + var(--thumb-overlap)*-1);text-align:center;color:var(--bs-gray-dark);font-size:1rem;opacity:1;transition:opacity .2s ease 1s}.target-slider[data-v-61c611fd]{-webkit-appearance:none;position:absolute;top:calc(var(--thumb-overlap)*-1);left:calc(var(--thumb-horizontal-padding)*-1);height:calc(100% + var(--thumb-overlap)*2);width:calc(100% + var(--thumb-horizontal-padding)*2);background:transparent}.target-slider[data-v-61c611fd]:focus{outline:none}.target-slider[data-v-61c611fd]::-webkit-slider-runnable-track{position:relative;background:transparent;border:none;height:100%;cursor:pointer}.target-slider[data-v-61c611fd]::-moz-range-track{background:transparent;border:none;height:100%;cursor:pointer}.target-slider[data-v-61c611fd]::-webkit-slider-thumb{-webkit-appearance:none;position:relative;top:calc(var(--label-height)*-1);height:100%;width:var(--thumb-width);padding:var(--label-height) var(--thumb-horizontal-padding) 0;box-sizing:content-box;background-clip:content-box;background-color:var(--bs-gray-dark);cursor:-webkit-grab;cursor:grab;border:none;opacity:1;-webkit-transition:opacity .2s ease 1s;transition:opacity .2s ease 1s}.target-slider[data-v-61c611fd]::-moz-range-thumb{position:relative;top:calc(var(--label-height)*-1);height:100%;width:var(--thumb-width);padding:var(--label-height) var(--thumb-horizontal-padding) 0;box-sizing:content-box;background-clip:content-box;background-color:var(--bs-gray-dark);cursor:grab;border:none;opacity:1;-moz-transition:opacity .2s ease 1s;transition:opacity .2s ease 1s}.target--max .target-label[data-v-61c611fd],.target--max .target-slider[data-v-61c611fd]::-webkit-slider-thumb{opacity:0}.target--max .target-label[data-v-61c611fd],.target--max .target-slider[data-v-61c611fd]::-moz-range-thumb{opacity:0}.target:hover .target-label[data-v-61c611fd],.target:hover .target-slider[data-v-61c611fd]::-webkit-slider-thumb{opacity:1;transition-delay:0s}.target:hover .target-label[data-v-61c611fd],.target:hover .target-slider[data-v-61c611fd]::-moz-range-thumb{opacity:1;transition-delay:0s}

View file

@ -1 +0,0 @@
.progress-bar-animated{animation-direction:reverse}.btn.caption{opacity:1}.btn.first{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}code{font-size:87.5%;color:#e83e8c;word-wrap:break-word}.value,input[type=radio],label.btn{white-space:nowrap!important}.text-muted a,.text-muted a:hover{color:#6c757d!important;text-decoration:none}.bg-muted{opacity:.25}.fade-enter-active[data-v-a6ded36e],.fade-leave-active[data-v-a6ded36e]{transition:opacity .25s ease-in}.fade-enter[data-v-a6ded36e],.fade-leave-to[data-v-a6ded36e]{opacity:0}.dialog[data-v-a6ded36e]{position:fixed;top:0;left:0;z-index:1050;width:100%;height:100%;overflow:hidden;outline:0}.icon[data-v-a6ded36e]{color:#0fdd42}.text-black a[data-v-a6ded36e]{color:#18191a}.icon[data-v-5f0f38d1]{color:#0fdd42}.text-black a[data-v-5f0f38d1]{color:#18191a}.logo[data-v-8dad761a]{width:85px}.app[data-v-8dad761a]{min-height:100vh}.btn[data-v-139a81de]{flex-basis:0}.vehicle-soc[data-v-2d786089]{--height:38px;--thumb-overlap:3px;--thumb-width:3px;--thumb-horizontal-padding:15px;--label-height:26px;position:relative;height:var(--height)}.progress[data-v-2d786089]{height:100%;font-size:1rem}.progress-bar.bg-muted[data-v-2d786089]{color:var(--white)}.bg-light[data-v-2d786089]{color:var(--dark)}.target-label[data-v-2d786089]{width:3em;margin-left:-1.5em;height:var(--label-height);position:absolute;top:calc(var(--label-height)*-1 + var(--thumb-overlap)*-1);text-align:center;color:var(--dark);font-size:1rem;opacity:1;transition:opacity .2s ease 1s}.target-slider[data-v-2d786089]{-webkit-appearance:none;position:absolute;top:calc(var(--thumb-overlap)*-1);left:calc(var(--thumb-horizontal-padding)*-1);height:calc(100% + var(--thumb-overlap)*2);width:calc(100% + var(--thumb-horizontal-padding)*2);background:transparent}.target-slider[data-v-2d786089]:focus{outline:none}.target-slider[data-v-2d786089]::-webkit-slider-runnable-track{background:transparent;border:none;height:100%;cursor:pointer}.target-slider[data-v-2d786089]::-moz-range-track{background:transparent;border:none;height:100%;cursor:pointer}.target-slider[data-v-2d786089]::-webkit-slider-thumb{-webkit-appearance:none;position:relative;top:calc(var(--label-height)*-1);height:100%;width:var(--thumb-width);padding:var(--label-height) var(--thumb-horizontal-padding) 0;box-sizing:content-box;background-clip:content-box;background-color:var(--dark);cursor:-webkit-grab;cursor:grab;border:none;opacity:1;-webkit-transition:opacity .2s ease 1s;transition:opacity .2s ease 1s}.target-slider[data-v-2d786089]::-moz-range-thumb{position:relative;top:calc(var(--label-height)*-1);height:100%;width:var(--thumb-width);padding:0 var(--thumb-horizontal-padding);box-sizing:content-box;background-clip:content-box;background-color:var(--dark);cursor:grab;border:none;opacity:1;-moz-transition:opacity .2s ease 1s;transition:opacity .2s ease 1s}.target--max .target-label[data-v-2d786089],.target--max .target-slider[data-v-2d786089]::-webkit-slider-thumb{opacity:0}.target--max .target-label[data-v-2d786089],.target--max .target-slider[data-v-2d786089]::-moz-range-thumb{opacity:0}.target:hover .target-label[data-v-2d786089],.target:hover .target-slider[data-v-2d786089]::-webkit-slider-thumb{opacity:1;transition-delay:0s}.target:hover .target-label[data-v-2d786089],.target:hover .target-slider[data-v-2d786089]::-moz-range-thumb{opacity:1;transition-delay:0s}

4
dist/index.html vendored
View file

@ -1,4 +1,4 @@
<!DOCTYPE html><html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="description" content="EV Charge Controller"><meta name="author" content="andig"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black"><link rel="apple-touch-icon" sizes="180x180" href="ico/apple-touch-icon.png?[[.Version]]"><link rel="icon" type="image/png" sizes="32x32" href="ico/favicon-32x32.png?[[.Version]]"><link rel="icon" type="image/png" sizes="16x16" href="ico/favicon-16x16.png?[[.Version]]"><link rel="manifest" href="ico/site.webmanifest"><link rel="mask-icon" href="ico/safari-pinned-tab.svg?[[.Version]]" color="#18191a"><link rel="shortcut icon" href="ico/favicon.ico?[[.Version]]"><meta name="apple-mobile-web-app-title" content="evcc"><meta name="application-name" content="evcc"><meta name="msapplication-TileColor" content="#18191a"><meta name="msapplication-config" content="ico/browserconfig.xml"><meta name="theme-color" content="#18191a"><title>evcc</title><link href="css/chunk-vendors.52eaf30c.css" rel="preload" as="style"><link href="css/index.65dbbf5e.css" rel="preload" as="style"><link href="js/chunk-vendors.35669cb4.js" rel="preload" as="script"><link href="js/index.f51648a0.js" rel="preload" as="script"><link href="css/chunk-vendors.52eaf30c.css" rel="stylesheet"><link href="css/index.65dbbf5e.css" rel="stylesheet"></head><body><script>window.evcc = {
<!DOCTYPE html><html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="description" content="EV Charge Controller"><meta name="author" content="andig"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black"><link rel="apple-touch-icon" sizes="180x180" href="ico/apple-touch-icon.png?[[.Version]]"><link rel="icon" type="image/png" sizes="32x32" href="ico/favicon-32x32.png?[[.Version]]"><link rel="icon" type="image/png" sizes="16x16" href="ico/favicon-16x16.png?[[.Version]]"><link rel="manifest" href="ico/site.webmanifest"><link rel="mask-icon" href="ico/safari-pinned-tab.svg?[[.Version]]" color="#18191a"><link rel="shortcut icon" href="ico/favicon.ico?[[.Version]]"><meta name="apple-mobile-web-app-title" content="evcc"><meta name="application-name" content="evcc"><meta name="msapplication-TileColor" content="#18191a"><meta name="msapplication-config" content="ico/browserconfig.xml"><meta name="theme-color" content="#18191a"><title>evcc</title><link href="css/chunk-vendors.0f1ada3b.css" rel="preload" as="style"><link href="css/index.307488cf.css" rel="preload" as="style"><link href="js/chunk-vendors.47c2aa5a.js" rel="preload" as="script"><link href="js/index.ab413d5d.js" rel="preload" as="script"><link href="css/chunk-vendors.0f1ada3b.css" rel="stylesheet"><link href="css/index.307488cf.css" rel="stylesheet"></head><body><script>window.evcc = {
version: "[[.Version]]",
configured: "[[.Configured]]",
};</script><div id="app"></div><div id="toasts"></div><script src="js/chunk-vendors.35669cb4.js"></script><script src="js/index.f51648a0.js"></script></body></html>
};</script><div id="app"></div><script src="js/chunk-vendors.47c2aa5a.js"></script><script src="js/index.ab413d5d.js"></script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

24
dist/js/chunk-vendors.47c2aa5a.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/js/chunk-vendors.47c2aa5a.js.map vendored Normal file

File diff suppressed because one or more lines are too long

2
dist/js/index.ab413d5d.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/js/index.ab413d5d.js.map vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

3439
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -13,21 +13,22 @@
"author": "naltatis & andig",
"license": "MIT",
"devDependencies": {
"@storybook/addon-actions": "^6.1.21",
"@storybook/addon-essentials": "^6.1.21",
"@storybook/addon-links": "^6.1.21",
"@storybook/vue": "^6.1.21",
"@storybook/addon-actions": "^6.2.9",
"@storybook/addon-essentials": "^6.2.9",
"@storybook/addon-links": "^6.2.9",
"@storybook/addon-postcss": "^2.0.0",
"@storybook/vue": "^6.2.9",
"@vue/cli-service": "^4.5.12",
"@vue/component-compiler-utils": "^3.2.0",
"@vue/eslint-config-standard": "^5.1.2",
"eslint": "^7.22.0",
"eslint": "^7.25.0",
"eslint-config-prettier": "^6.15.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-promise": "^4.3.1",
"eslint-plugin-standard": "^4.1.0",
"eslint-plugin-vue": "^7.7.0",
"eslint-plugin-vue": "^7.9.0",
"prettier": "^2.2.0",
"vue-template-compiler": "^2.6.12"
},
@ -41,12 +42,12 @@
"repository": "github:andig/evcc",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.35",
"@fortawesome/free-regular-svg-icons": "^5.15.3",
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"@fortawesome/vue-fontawesome": "^2.0.0",
"axios": "^0.21.0",
"bootstrap": "^4.6.0",
"jquery": "^3.6.0",
"popper.js": "^1.16.1",
"bootstrap": "^5.0.0-beta3",
"canvas-confetti": "^1.4.0",
"vue": "^2.6.12",
"vue-hot-reload-api": "^2.3.4",
"vue-router": "^3.5.1"