feat(calculator): Unit-Converter mit 6 Kategorien
This commit is contained in:
@@ -1391,6 +1391,67 @@ body.show-desc .bm-desc { display: block; }
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
/* Calculator Converter Mode */
|
||||
.calc-conv-select {
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
background: rgba(0,0,0,0.3);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-primary);
|
||||
font-size: 13px;
|
||||
font-family: 'Rajdhani', sans-serif;
|
||||
}
|
||||
.calc-conv-row {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
.calc-conv-input {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
background: rgba(0,0,0,0.3);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-primary);
|
||||
font-size: 16px;
|
||||
font-family: 'Rajdhani', monospace;
|
||||
}
|
||||
.calc-conv-input:read-only {
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
.calc-conv-unit {
|
||||
width: 80px;
|
||||
padding: 4px 6px;
|
||||
background: rgba(0,0,0,0.3);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-primary);
|
||||
font-size: 12px;
|
||||
font-family: 'Rajdhani', sans-serif;
|
||||
}
|
||||
.calc-conv-swap {
|
||||
align-self: center;
|
||||
width: 36px;
|
||||
height: 28px;
|
||||
background: rgba(255,255,255,0.04);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--accent);
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.calc-conv-swap:hover {
|
||||
background: var(--accent-dim);
|
||||
}
|
||||
.calc-conv-ref {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
padding: 4px 0;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
TIMER WIDGET
|
||||
============================================ */
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
/* =============================================
|
||||
HELLION NEWTAB — calc-converter.js
|
||||
Unit-Converter Modus für Calculator Widget
|
||||
============================================= */
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const CATEGORIES = {
|
||||
length: {
|
||||
titleKey: 'calculator.conv.cat.length',
|
||||
baseUnit: 'm',
|
||||
units: {
|
||||
mm: { toBase: v => v / 1000, fromBase: v => v * 1000 },
|
||||
cm: { toBase: v => v / 100, fromBase: v => v * 100 },
|
||||
m: { toBase: v => v, fromBase: v => v },
|
||||
km: { toBase: v => v * 1000, fromBase: v => v / 1000 },
|
||||
in: { toBase: v => v * 0.0254, fromBase: v => v / 0.0254 },
|
||||
ft: { toBase: v => v * 0.3048, fromBase: v => v / 0.3048 },
|
||||
yd: { toBase: v => v * 0.9144, fromBase: v => v / 0.9144 },
|
||||
mi: { toBase: v => v * 1609.344, fromBase: v => v / 1609.344 }
|
||||
}
|
||||
},
|
||||
weight: {
|
||||
titleKey: 'calculator.conv.cat.weight',
|
||||
baseUnit: 'g',
|
||||
units: {
|
||||
mg: { toBase: v => v / 1000, fromBase: v => v * 1000 },
|
||||
g: { toBase: v => v, fromBase: v => v },
|
||||
kg: { toBase: v => v * 1000, fromBase: v => v / 1000 },
|
||||
t: { toBase: v => v * 1000000, fromBase: v => v / 1000000 },
|
||||
oz: { toBase: v => v * 28.3495, fromBase: v => v / 28.3495 },
|
||||
lb: { toBase: v => v * 453.592, fromBase: v => v / 453.592 }
|
||||
}
|
||||
},
|
||||
temperature: {
|
||||
titleKey: 'calculator.conv.cat.temperature',
|
||||
baseUnit: null,
|
||||
units: { '\u00B0C': null, '\u00B0F': null, 'K': null },
|
||||
convert(value, from, to) {
|
||||
if (from === to) return value;
|
||||
const key = from + '_' + to;
|
||||
const conversions = {
|
||||
'\u00B0C_\u00B0F': v => (v * 9 / 5) + 32,
|
||||
'\u00B0C_K': v => v + 273.15,
|
||||
'\u00B0F_\u00B0C': v => (v - 32) * 5 / 9,
|
||||
'\u00B0F_K': v => (v - 32) * 5 / 9 + 273.15,
|
||||
'K_\u00B0C': v => v - 273.15,
|
||||
'K_\u00B0F': v => (v - 273.15) * 9 / 5 + 32
|
||||
};
|
||||
const fn = conversions[key];
|
||||
return fn ? fn(value) : null;
|
||||
}
|
||||
},
|
||||
volume: {
|
||||
titleKey: 'calculator.conv.cat.volume',
|
||||
baseUnit: 'ml',
|
||||
units: {
|
||||
ml: { toBase: v => v, fromBase: v => v },
|
||||
L: { toBase: v => v * 1000, fromBase: v => v / 1000 },
|
||||
'm\u00B3':{ toBase: v => v * 1000000, fromBase: v => v / 1000000 },
|
||||
'gal(US)':{ toBase: v => v * 3785.41, fromBase: v => v / 3785.41 },
|
||||
'gal(UK)':{ toBase: v => v * 4546.09, fromBase: v => v / 4546.09 },
|
||||
'ft\u00B3':{ toBase: v => v * 28316.8, fromBase: v => v / 28316.8 }
|
||||
}
|
||||
},
|
||||
speed: {
|
||||
titleKey: 'calculator.conv.cat.speed',
|
||||
baseUnit: 'm/s',
|
||||
units: {
|
||||
'm/s': { toBase: v => v, fromBase: v => v },
|
||||
'km/h': { toBase: v => v / 3.6, fromBase: v => v * 3.6 },
|
||||
'mph': { toBase: v => v * 0.44704, fromBase: v => v / 0.44704 },
|
||||
'kn': { toBase: v => v * 0.514444, fromBase: v => v / 0.514444 }
|
||||
}
|
||||
},
|
||||
area: {
|
||||
titleKey: 'calculator.conv.cat.area',
|
||||
baseUnit: 'm\u00B2',
|
||||
units: {
|
||||
'mm\u00B2': { toBase: v => v / 1000000, fromBase: v => v * 1000000 },
|
||||
'cm\u00B2': { toBase: v => v / 10000, fromBase: v => v * 10000 },
|
||||
'm\u00B2': { toBase: v => v, fromBase: v => v },
|
||||
'km\u00B2': { toBase: v => v * 1000000, fromBase: v => v / 1000000 },
|
||||
'ha': { toBase: v => v * 10000, fromBase: v => v / 10000 },
|
||||
'acre': { toBase: v => v * 4046.86, fromBase: v => v / 4046.86 },
|
||||
'ft\u00B2': { toBase: v => v * 0.092903, fromBase: v => v / 0.092903 },
|
||||
'in\u00B2': { toBase: v => v * 0.00064516, fromBase: v => v / 0.00064516 }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const CATEGORY_ORDER = ['length', 'weight', 'temperature', 'volume', 'speed', 'area'];
|
||||
|
||||
let _currentCategory = 'length';
|
||||
let _fromUnit = 'cm';
|
||||
let _toUnit = 'in';
|
||||
let _fromInput = null;
|
||||
let _toInput = null;
|
||||
let _refEl = null;
|
||||
|
||||
/**
|
||||
* Converts a value from one unit to another within the current category.
|
||||
* @param {number} value
|
||||
* @param {string} from
|
||||
* @param {string} to
|
||||
* @returns {number|null}
|
||||
*/
|
||||
function convert(value, from, to) {
|
||||
const cat = CATEGORIES[_currentCategory];
|
||||
if (!cat) return null;
|
||||
if (cat.convert) return cat.convert(value, from, to);
|
||||
const fromDef = cat.units[from];
|
||||
const toDef = cat.units[to];
|
||||
if (!fromDef || !toDef) return null;
|
||||
const base = fromDef.toBase(value);
|
||||
return toDef.fromBase(base);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculates the output field and reference lines based on current input.
|
||||
*/
|
||||
function recalc() {
|
||||
if (!_fromInput || !_toInput) return;
|
||||
const val = parseFloat(_fromInput.value);
|
||||
if (isNaN(val)) {
|
||||
_toInput.value = '';
|
||||
updateReference();
|
||||
return;
|
||||
}
|
||||
const result = convert(val, _fromUnit, _toUnit);
|
||||
if (result === null) {
|
||||
_toInput.value = '';
|
||||
} else {
|
||||
_toInput.value = Calculator._formatResult(result);
|
||||
}
|
||||
updateReference();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the reference conversion lines below the inputs.
|
||||
*/
|
||||
function updateReference() {
|
||||
if (!_refEl) return;
|
||||
_refEl.textContent = '';
|
||||
const r1 = convert(1, _fromUnit, _toUnit);
|
||||
const r2 = convert(1, _toUnit, _fromUnit);
|
||||
if (r1 !== null) {
|
||||
const line1 = document.createElement('div');
|
||||
line1.textContent = '1 ' + _fromUnit + ' = ' + Calculator._formatResult(r1) + ' ' + _toUnit;
|
||||
_refEl.appendChild(line1);
|
||||
}
|
||||
if (r2 !== null) {
|
||||
const line2 = document.createElement('div');
|
||||
line2.textContent = '1 ' + _toUnit + ' = ' + Calculator._formatResult(r2) + ' ' + _fromUnit;
|
||||
_refEl.appendChild(line2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates a unit <select> element with options for the current category.
|
||||
* @param {HTMLSelectElement} selectEl
|
||||
* @param {string} selectedUnit
|
||||
*/
|
||||
function populateUnitSelect(selectEl, selectedUnit) {
|
||||
while (selectEl.firstChild) {
|
||||
selectEl.removeChild(selectEl.firstChild);
|
||||
}
|
||||
const cat = CATEGORIES[_currentCategory];
|
||||
if (!cat) return;
|
||||
const units = Object.keys(cat.units);
|
||||
units.forEach(unit => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = unit;
|
||||
opt.textContent = unit;
|
||||
if (unit === selectedUnit) opt.selected = true;
|
||||
selectEl.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns sensible default from/to units for a given category key.
|
||||
* @param {string} catKey
|
||||
* @returns {{ from: string, to: string }}
|
||||
*/
|
||||
function getDefaultUnits(catKey) {
|
||||
const defaults = {
|
||||
length: { from: 'cm', to: 'in' },
|
||||
weight: { from: 'kg', to: 'lb' },
|
||||
temperature: { from: '\u00B0C', to: '\u00B0F' },
|
||||
volume: { from: 'L', to: 'gal(US)' },
|
||||
speed: { from: 'km/h', to: 'mph' },
|
||||
area: { from: 'm\u00B2', to: 'ft\u00B2' }
|
||||
};
|
||||
return defaults[catKey] || { from: Object.keys(CATEGORIES[catKey].units)[0], to: Object.keys(CATEGORIES[catKey].units)[1] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads persisted converter state from storage.
|
||||
*/
|
||||
async function loadState() {
|
||||
const data = await Store.get(Calculator.STORAGE_KEY);
|
||||
if (data && data.calculator && data.calculator.converter) {
|
||||
const s = data.calculator.converter;
|
||||
if (s.lastCategory && CATEGORIES[s.lastCategory]) _currentCategory = s.lastCategory;
|
||||
if (s.fromUnit) _fromUnit = s.fromUnit;
|
||||
if (s.toUnit) _toUnit = s.toUnit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists current converter state to storage (read-before-write).
|
||||
*/
|
||||
async function saveState() {
|
||||
const data = await Store.get(Calculator.STORAGE_KEY) || {};
|
||||
if (!data.calculator) data.calculator = {};
|
||||
data.calculator.converter = {
|
||||
lastCategory: _currentCategory,
|
||||
fromUnit: _fromUnit,
|
||||
toUnit: _toUnit
|
||||
};
|
||||
await Store.set(Calculator.STORAGE_KEY, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the converter UI and appends it to the widget body element.
|
||||
* @param {HTMLElement} bodyEl
|
||||
*/
|
||||
function buildUI(bodyEl) {
|
||||
const catSelect = document.createElement('select');
|
||||
catSelect.className = 'calc-conv-select';
|
||||
|
||||
CATEGORY_ORDER.forEach(catKey => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = catKey;
|
||||
opt.textContent = t(CATEGORIES[catKey].titleKey);
|
||||
if (catKey === _currentCategory) opt.selected = true;
|
||||
catSelect.appendChild(opt);
|
||||
});
|
||||
|
||||
const fromRow = document.createElement('div');
|
||||
fromRow.className = 'calc-conv-row';
|
||||
|
||||
_fromInput = document.createElement('input');
|
||||
_fromInput.type = 'number';
|
||||
_fromInput.className = 'calc-conv-input';
|
||||
_fromInput.placeholder = '0';
|
||||
_fromInput.step = 'any';
|
||||
|
||||
const fromSelect = document.createElement('select');
|
||||
fromSelect.className = 'calc-conv-unit';
|
||||
populateUnitSelect(fromSelect, _fromUnit);
|
||||
|
||||
fromRow.append(_fromInput, fromSelect);
|
||||
|
||||
const swapBtn = document.createElement('button');
|
||||
swapBtn.type = 'button';
|
||||
swapBtn.className = 'calc-conv-swap';
|
||||
swapBtn.textContent = '\u21C5';
|
||||
swapBtn.title = t('calculator.conv.swap');
|
||||
|
||||
const toRow = document.createElement('div');
|
||||
toRow.className = 'calc-conv-row';
|
||||
|
||||
_toInput = document.createElement('input');
|
||||
_toInput.type = 'text';
|
||||
_toInput.className = 'calc-conv-input';
|
||||
_toInput.readOnly = true;
|
||||
_toInput.placeholder = '0';
|
||||
|
||||
const toSelect = document.createElement('select');
|
||||
toSelect.className = 'calc-conv-unit';
|
||||
populateUnitSelect(toSelect, _toUnit);
|
||||
|
||||
toRow.append(_toInput, toSelect);
|
||||
|
||||
_refEl = document.createElement('div');
|
||||
_refEl.className = 'calc-conv-ref';
|
||||
|
||||
_fromInput.addEventListener('input', () => recalc());
|
||||
fromSelect.addEventListener('change', () => {
|
||||
_fromUnit = fromSelect.value;
|
||||
recalc();
|
||||
saveState();
|
||||
});
|
||||
toSelect.addEventListener('change', () => {
|
||||
_toUnit = toSelect.value;
|
||||
recalc();
|
||||
saveState();
|
||||
});
|
||||
swapBtn.addEventListener('click', () => {
|
||||
const tmpUnit = _fromUnit;
|
||||
_fromUnit = _toUnit;
|
||||
_toUnit = tmpUnit;
|
||||
populateUnitSelect(fromSelect, _fromUnit);
|
||||
populateUnitSelect(toSelect, _toUnit);
|
||||
const currentVal = _toInput.value;
|
||||
if (currentVal) {
|
||||
_fromInput.value = currentVal;
|
||||
}
|
||||
recalc();
|
||||
saveState();
|
||||
});
|
||||
catSelect.addEventListener('change', () => {
|
||||
_currentCategory = catSelect.value;
|
||||
const defaults = getDefaultUnits(_currentCategory);
|
||||
_fromUnit = defaults.from;
|
||||
_toUnit = defaults.to;
|
||||
populateUnitSelect(fromSelect, _fromUnit);
|
||||
populateUnitSelect(toSelect, _toUnit);
|
||||
_fromInput.value = '';
|
||||
_toInput.value = '';
|
||||
updateReference();
|
||||
saveState();
|
||||
});
|
||||
|
||||
bodyEl.append(catSelect, fromRow, swapBtn, toRow, _refEl);
|
||||
updateReference();
|
||||
}
|
||||
|
||||
Calculator.registerMode('converter', {
|
||||
label: '⚖️',
|
||||
shortName: 'Unit',
|
||||
titleKey: 'calculator.tab.converter',
|
||||
|
||||
render(bodyEl) {
|
||||
bodyEl.style.padding = '8px';
|
||||
bodyEl.style.display = 'flex';
|
||||
bodyEl.style.flexDirection = 'column';
|
||||
bodyEl.style.gap = '8px';
|
||||
|
||||
loadState().then(() => {
|
||||
buildUI(bodyEl);
|
||||
});
|
||||
},
|
||||
|
||||
destroy() {
|
||||
_fromInput = null;
|
||||
_toInput = null;
|
||||
_refEl = null;
|
||||
saveState();
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -99,6 +99,14 @@ const STRINGS = {
|
||||
'calculator.sci.field.b': 'Seite b',
|
||||
'calculator.sci.field.value': 'Wert',
|
||||
'calculator.sci.field.percent': 'Prozent',
|
||||
'calculator.tab.converter': 'Umrechner',
|
||||
'calculator.conv.swap': 'Einheiten tauschen',
|
||||
'calculator.conv.cat.length': 'Länge',
|
||||
'calculator.conv.cat.weight': 'Gewicht',
|
||||
'calculator.conv.cat.temperature': 'Temperatur',
|
||||
'calculator.conv.cat.volume': 'Volumen',
|
||||
'calculator.conv.cat.speed': 'Geschwindigkeit',
|
||||
'calculator.conv.cat.area': 'Fläche',
|
||||
|
||||
// Timer
|
||||
'timer.title': 'Timer',
|
||||
@@ -423,6 +431,14 @@ const STRINGS = {
|
||||
'calculator.sci.field.b': 'Side b',
|
||||
'calculator.sci.field.value': 'Value',
|
||||
'calculator.sci.field.percent': 'Percent',
|
||||
'calculator.tab.converter': 'Converter',
|
||||
'calculator.conv.swap': 'Swap units',
|
||||
'calculator.conv.cat.length': 'Length',
|
||||
'calculator.conv.cat.weight': 'Weight',
|
||||
'calculator.conv.cat.temperature': 'Temperature',
|
||||
'calculator.conv.cat.volume': 'Volume',
|
||||
'calculator.conv.cat.speed': 'Speed',
|
||||
'calculator.conv.cat.area': 'Area',
|
||||
|
||||
// Timer
|
||||
'timer.title': 'Timer',
|
||||
|
||||
Reference in New Issue
Block a user