feat(calculator): Unit-Converter mit 6 Kategorien
This commit is contained in:
@@ -508,6 +508,7 @@
|
|||||||
<script src="src/js/notes.js"></script>
|
<script src="src/js/notes.js"></script>
|
||||||
<script src="src/js/calculator.js"></script>
|
<script src="src/js/calculator.js"></script>
|
||||||
<script src="src/js/calc-scientific.js"></script>
|
<script src="src/js/calc-scientific.js"></script>
|
||||||
|
<script src="src/js/calc-converter.js"></script>
|
||||||
<script src="src/js/timer.js"></script>
|
<script src="src/js/timer.js"></script>
|
||||||
<script src="src/js/image-ref.js"></script>
|
<script src="src/js/image-ref.js"></script>
|
||||||
<script src="src/js/bookmark-import.js"></script>
|
<script src="src/js/bookmark-import.js"></script>
|
||||||
|
|||||||
@@ -1391,6 +1391,67 @@ body.show-desc .bm-desc { display: block; }
|
|||||||
padding: 2px 0;
|
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
|
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.b': 'Seite b',
|
||||||
'calculator.sci.field.value': 'Wert',
|
'calculator.sci.field.value': 'Wert',
|
||||||
'calculator.sci.field.percent': 'Prozent',
|
'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
|
||||||
'timer.title': 'Timer',
|
'timer.title': 'Timer',
|
||||||
@@ -423,6 +431,14 @@ const STRINGS = {
|
|||||||
'calculator.sci.field.b': 'Side b',
|
'calculator.sci.field.b': 'Side b',
|
||||||
'calculator.sci.field.value': 'Value',
|
'calculator.sci.field.value': 'Value',
|
||||||
'calculator.sci.field.percent': 'Percent',
|
'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
|
||||||
'timer.title': 'Timer',
|
'timer.title': 'Timer',
|
||||||
|
|||||||
Reference in New Issue
Block a user