diff --git a/plugin/ActionBase.js b/plugin/ActionBase.js
new file mode 100644
index 0000000..933083c
--- /dev/null
+++ b/plugin/ActionBase.js
@@ -0,0 +1,61 @@
+class ActionBase {
+ constructor($UD, context) {
+ this.$UD = $UD;
+ this.context = context;
+ this.refreshTimer = null;
+ this.debounceTimer = 0;
+ }
+
+ onClear() {
+ if (this.refreshTimer) {
+ clearInterval(this.refreshTimer);
+ this.refreshTimer = null;
+ }
+ if (this.debounceTimer) {
+ clearTimeout(this.debounceTimer);
+ this.debounceTimer = 0;
+ }
+ }
+
+ startTimer(fetchFn, refreshRate) {
+ this.onClear();
+ fetchFn();
+ const duration = this.getRefreshDuration(refreshRate);
+ if (duration > 0) {
+ this.refreshTimer = setInterval(() => fetchFn(), duration * 60 * 1000);
+ }
+ }
+
+ debounce(fn, delay = 300) {
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
+ this.debounceTimer = setTimeout(() => fn(), delay);
+ }
+
+ getRefreshDuration(refreshRate) {
+ switch (refreshRate) {
+ case '1': return 1;
+ case '2': return 2;
+ case '3': return 5;
+ case '4': return 10;
+ case '5': return 30;
+ case '6': return 60;
+ default: return 0;
+ }
+ }
+
+ createCanvas() {
+ const canvas = document.createElement('canvas');
+ canvas.width = 196;
+ canvas.height = 196;
+ const ctx = canvas.getContext('2d');
+ ctx.fillStyle = '#000000';
+ ctx.fillRect(0, 0, 196, 196);
+ ctx.textBaseline = 'middle';
+ ctx.textAlign = 'center';
+ return { canvas, ctx };
+ }
+
+ setIcon(canvas) {
+ this.$UD.setBaseDataIcon(this.context, canvas.toDataURL('image/png'));
+ }
+}
diff --git a/plugin/actions/CopilotAction.js b/plugin/actions/CopilotAction.js
index 2196fd0..c5ebdcc 100644
--- a/plugin/actions/CopilotAction.js
+++ b/plugin/actions/CopilotAction.js
@@ -1,62 +1,42 @@
-class CopilotAction {
+class CopilotAction extends ActionBase {
constructor($UD, context) {
- this.$UD = $UD;
- this.context = context;
+ super($UD, context);
this.config = {
url: '',
refreshRate: '4',
};
- this.refreshTimer = null;
- this.debounceTimer = 0;
}
setActive() {}
- onClear() {
- if (this.refreshTimer) {
- clearInterval(this.refreshTimer);
- this.refreshTimer = null;
- }
- if (this.debounceTimer) {
- clearTimeout(this.debounceTimer);
- this.debounceTimer = 0;
- }
- }
-
setParams(jsn) {
this.config = Object.assign(this.config, (jsn && jsn.param) || {});
- this.start();
+ this.startTimer(() => this.fetchData(), this.config.refreshRate);
}
onRun() {
this.fetchData();
}
- start() {
- this.onClear();
- this.fetchData();
- const duration = this.getRefreshDuration();
- if (duration > 0) {
- this.refreshTimer = setInterval(() => this.fetchData(), duration * 60 * 1000);
- }
- }
-
- async fetchData() {
- if (this.debounceTimer) clearTimeout(this.debounceTimer);
- this.debounceTimer = setTimeout(async () => {
+ fetchData() {
+ this.debounce(async () => {
if (!this.config.url) {
- this.renderGauge(null);
+ this.renderGauge(null, false);
return;
}
try {
const response = await fetch(this.config.url);
+ if (!response.ok) {
+ this.renderGauge(null, true);
+ return;
+ }
const text = await response.text();
const value = this.parsePrometheus(text);
- this.renderGauge(value !== null ? parseFloat(value) : null);
+ this.renderGauge(value !== null ? parseFloat(value) : null, false);
} catch (e) {
- this.renderGauge(null);
+ this.renderGauge(null, true);
}
- }, 300);
+ });
}
parsePrometheus(text) {
@@ -66,7 +46,6 @@ class CopilotAction {
if (trimmed.startsWith('github_copilot_usage_percentage')) {
const closingBrace = trimmed.indexOf('}');
if (closingBrace !== -1) return trimmed.slice(closingBrace + 1).trim();
- // no labels
const parts = trimmed.split(/\s+/);
if (parts.length >= 2) return parts[1];
}
@@ -74,25 +53,18 @@ class CopilotAction {
return null;
}
- renderGauge(value) {
- const canvas = document.createElement('canvas');
- canvas.width = 196;
- canvas.height = 196;
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = '#000000';
- ctx.fillRect(0, 0, 196, 196);
+ renderGauge(value, isError) {
+ const { canvas, ctx } = this.createCanvas();
const cx = 98, cy = 100;
const r = 68;
const lineWidth = 14;
- // Gauge runs from 225° to 315° clockwise (270° sweep)
const startAngle = 135 * Math.PI / 180;
const totalSweep = 270 * Math.PI / 180;
- const greenEnd = startAngle + 0.50 * totalSweep; // 50%
- const yellowEnd = startAngle + 0.80 * totalSweep; // 80%
- const endAngle = startAngle + totalSweep; // 100%
+ const greenEnd = startAngle + 0.50 * totalSweep;
+ const yellowEnd = startAngle + 0.80 * totalSweep;
+ const endAngle = startAngle + totalSweep;
// Background track
ctx.beginPath();
@@ -106,7 +78,6 @@ class CopilotAction {
const pct = Math.max(0, Math.min(100, value)) / 100;
const valueAngle = startAngle + pct * totalSweep;
- // Green segment (0–50%)
if (pct > 0) {
ctx.beginPath();
ctx.arc(cx, cy, r, startAngle, Math.min(valueAngle, greenEnd), false);
@@ -115,8 +86,6 @@ class CopilotAction {
ctx.lineCap = 'round';
ctx.stroke();
}
-
- // Yellow segment (50–80%)
if (pct > 0.5) {
ctx.beginPath();
ctx.arc(cx, cy, r, greenEnd, Math.min(valueAngle, yellowEnd), false);
@@ -125,8 +94,6 @@ class CopilotAction {
ctx.lineCap = 'butt';
ctx.stroke();
}
-
- // Red segment (80–100%)
if (pct > 0.8) {
ctx.beginPath();
ctx.arc(cx, cy, r, yellowEnd, valueAngle, false);
@@ -136,21 +103,16 @@ class CopilotAction {
ctx.stroke();
}
- // Value text — centered, colored
const color = pct <= 0.5 ? '#4caf50' : pct <= 0.8 ? '#ffeb3b' : '#f44336';
const displayText = value.toFixed(1) + '%';
const fSize = displayText.length > 6 ? 28 : 34;
ctx.fillStyle = color;
ctx.font = `bold ${fSize}px "Source Han Sans SC"`;
- ctx.textBaseline = 'middle';
- ctx.textAlign = 'center';
ctx.fillText(displayText, cx, cy + 8);
} else {
- ctx.fillStyle = '#666666';
+ ctx.fillStyle = isError ? '#ff6b6b' : '#666666';
ctx.font = 'bold 28px "Source Han Sans SC"';
- ctx.textBaseline = 'middle';
- ctx.textAlign = 'center';
- ctx.fillText('N/A', cx, cy + 8);
+ ctx.fillText(isError ? 'ERR' : 'N/A', cx, cy + 8);
}
// Label — bottom
@@ -158,18 +120,6 @@ class CopilotAction {
ctx.font = 'bold 20px "Source Han Sans SC"';
ctx.fillText('Copilot', cx, 165);
- this.$UD.setBaseDataIcon(this.context, canvas.toDataURL('image/png'));
- }
-
- getRefreshDuration() {
- switch (this.config.refreshRate) {
- case '1': return 1;
- case '2': return 2;
- case '3': return 5;
- case '4': return 10;
- case '5': return 30;
- case '6': return 60;
- default: return 0;
- }
+ this.setIcon(canvas);
}
}
diff --git a/plugin/actions/GiteaAction.js b/plugin/actions/GiteaAction.js
index 5ccf6f9..377f8a4 100644
--- a/plugin/actions/GiteaAction.js
+++ b/plugin/actions/GiteaAction.js
@@ -1,7 +1,6 @@
-class GiteaAction {
+class GiteaAction extends ActionBase {
constructor($UD, context) {
- this.$UD = $UD;
- this.context = context;
+ super($UD, context);
this.config = {
url: '',
owner: '',
@@ -9,26 +8,13 @@ class GiteaAction {
token: '',
refreshRate: '4',
};
- this.refreshTimer = null;
- this.debounceTimer = 0;
}
setActive() {}
- onClear() {
- if (this.refreshTimer) {
- clearInterval(this.refreshTimer);
- this.refreshTimer = null;
- }
- if (this.debounceTimer) {
- clearTimeout(this.debounceTimer);
- this.debounceTimer = 0;
- }
- }
-
setParams(jsn) {
this.config = Object.assign(this.config, (jsn && jsn.param) || {});
- this.start();
+ this.startTimer(() => this.fetchRun(), this.config.refreshRate);
}
onRun() {
@@ -37,18 +23,8 @@ class GiteaAction {
}
}
- start() {
- this.onClear();
- this.fetchRun();
- const duration = this.getRefreshDuration();
- if (duration > 0) {
- this.refreshTimer = setInterval(() => this.fetchRun(), duration * 60 * 1000);
- }
- }
-
- async fetchRun() {
- if (this.debounceTimer) clearTimeout(this.debounceTimer);
- this.debounceTimer = setTimeout(async () => {
+ fetchRun() {
+ this.debounce(async () => {
if (!this.config.url || !this.config.owner || !this.config.repo) {
this.renderButton(null);
return;
@@ -69,20 +45,11 @@ class GiteaAction {
} catch (e) {
this.renderButton({ error: 'ERR' });
}
- }, 300);
+ });
}
renderButton(run) {
- const canvas = document.createElement('canvas');
- canvas.width = 196;
- canvas.height = 196;
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = '#000000';
- ctx.fillRect(0, 0, 196, 196);
-
- ctx.textBaseline = 'middle';
- ctx.textAlign = 'center';
+ const { canvas, ctx } = this.createCanvas();
// Owner — top
const ownerLabel = this.config.owner || '?';
@@ -135,18 +102,6 @@ class GiteaAction {
ctx.fillText(info.length > 16 ? info.slice(0, 15) + '…' : info, 98, 172);
}
- this.$UD.setBaseDataIcon(this.context, canvas.toDataURL('image/png'));
- }
-
- getRefreshDuration() {
- switch (this.config.refreshRate) {
- case '1': return 1;
- case '2': return 2;
- case '3': return 5;
- case '4': return 10;
- case '5': return 30;
- case '6': return 60;
- default: return 0;
- }
+ this.setIcon(canvas);
}
}
diff --git a/plugin/actions/PetrolAction.js b/plugin/actions/PetrolAction.js
index 84b9f9c..bf8aaab 100644
--- a/plugin/actions/PetrolAction.js
+++ b/plugin/actions/PetrolAction.js
@@ -1,15 +1,12 @@
-class PetrolAction {
+class PetrolAction extends ActionBase {
constructor($UD, context) {
- this.$UD = $UD;
- this.context = context;
+ super($UD, context);
this.config = {
url: '',
stationUuid: '',
fuelType: '',
refreshRate: '1',
};
- this.refreshTimer = null;
- this.debounceTimer = 0;
this.previousPrice = null;
this.lastDelta = null;
@@ -24,40 +21,18 @@ class PetrolAction {
setActive() {}
- onClear() {
- if (this.refreshTimer) {
- clearInterval(this.refreshTimer);
- this.refreshTimer = null;
- }
- if (this.debounceTimer) {
- clearTimeout(this.debounceTimer);
- this.debounceTimer = 0;
- }
- }
-
setParams(jsn) {
- const params = (jsn && jsn.param) || {};
- this.config = Object.assign(this.config, params);
+ this.config = Object.assign(this.config, (jsn && jsn.param) || {});
this.$UD.getSettings(this.context);
- this.start();
+ this.startTimer(() => this.fetchPrice(), this.config.refreshRate);
}
onRun() {
this.fetchPrice();
}
- start() {
- this.onClear();
- this.fetchPrice();
- const duration = this.getRefreshDuration();
- if (duration > 0) {
- this.refreshTimer = setInterval(() => this.fetchPrice(), duration * 60 * 1000);
- }
- }
-
- async fetchPrice() {
- if (this.debounceTimer) clearTimeout(this.debounceTimer);
- this.debounceTimer = setTimeout(async () => {
+ fetchPrice() {
+ this.debounce(async () => {
if (!this.config.url || !this.config.stationUuid || !this.config.fuelType) {
this.renderButton('?', null);
return;
@@ -73,7 +48,7 @@ class PetrolAction {
if (delta !== 0) this.lastDelta = delta;
}
this.previousPrice = current;
- this.renderButton(`${raw}€`, this.lastDelta);
+ this.renderButton(`${parseFloat(raw).toFixed(3)}€`, this.lastDelta);
this.$UD.setSettings({ ...this.config, previousPrice: this.previousPrice, lastDelta: this.lastDelta }, this.context);
} else {
this.renderButton('N/A', null);
@@ -81,7 +56,7 @@ class PetrolAction {
} catch (e) {
this.renderButton('ERR', null);
}
- }, 300);
+ });
}
parsePrometheus(text) {
@@ -97,55 +72,28 @@ class PetrolAction {
}
renderButton(priceText, delta) {
- const canvas = document.createElement('canvas');
- canvas.width = 196;
- canvas.height = 196;
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = '#000000';
- ctx.fillRect(0, 0, 196, 196);
-
- ctx.strokeStyle = '#000';
- ctx.lineWidth = 3;
- ctx.textBaseline = 'middle';
- ctx.textAlign = 'center';
+ const { canvas, ctx } = this.createCanvas();
// Fuel type — top, light blue
ctx.fillStyle = '#7ec8e3';
ctx.font = 'bold 28px "Source Han Sans SC"';
- ctx.strokeText(this.config.fuelType, 98, 45);
ctx.fillText(this.config.fuelType, 98, 45);
// Price — middle, yellow
const fSize = priceText.length > 6 ? 34 - priceText.length : 40;
ctx.fillStyle = '#f0c040';
ctx.font = `bold ${fSize}px "Source Han Sans SC"`;
- ctx.strokeText(priceText, 98, 105);
ctx.fillText(priceText, 98, 105);
// Delta — bottom
if (delta !== null) {
const arrow = delta > 0 ? '▲' : '▼';
const color = delta > 0 ? '#ff6b6b' : '#6bff6b';
- const deltaText = `${arrow} ${Math.abs(delta).toFixed(3)}`;
ctx.fillStyle = color;
ctx.font = '18px "Source Han Sans SC"';
- ctx.strokeText(deltaText, 98, 142);
- ctx.fillText(deltaText, 98, 142);
+ ctx.fillText(`${arrow} ${Math.abs(delta).toFixed(3)}`, 98, 142);
}
- this.$UD.setBaseDataIcon(this.context, canvas.toDataURL('image/png'));
- }
-
- getRefreshDuration() {
- switch (this.config.refreshRate) {
- case '1': return 1;
- case '2': return 2;
- case '3': return 5;
- case '4': return 10;
- case '5': return 30;
- case '6': return 60;
- default: return 0;
- }
+ this.setIcon(canvas);
}
}
diff --git a/plugin/app.html b/plugin/app.html
index 485affb..9121a62 100644
--- a/plugin/app.html
+++ b/plugin/app.html
@@ -11,6 +11,7 @@
+