@ -2,7 +2,7 @@
< html >
< head >
< meta charset = "UTF-8" >
< meta name= "viewport" content= "width=device-width, initial-scale=1.0, user-scalable=no ">
< meta content= "width=device-width, initial-scale=1.0, user-scalable=no " name = "viewport ">
< title > WhatWeight 1.3.9< / title >
< script src = "chart.min.js" > < / script >
@ -131,49 +131,58 @@ body.loaded {
< header style = "display: flex; align-items: center; margin: 10px 0 20px 5px;" >
< div class = "app-icon" > ️
< svg xmlns= "http://www.w3.org/2000/svg" width = "48" height = "48" viewBox = "0 0 24 24 ">
< svg height= "48" viewBox = "0 0 24 24" width = "48" xmlns = "http://www.w3.org/2000/svg ">
< g transform = "translate(5.04 5.04) scale(0.58)" >
< path
fill="var(--primary) "
d="M24,4c0,0.55 -0.45,1 -1,1h-1v1c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1V5h-1c-0.55,0 -1,-0.45 -1,-1c0,-0.55 0.45,-1 1,-1h1V2c0,-0.55 0.45,-1 1,-1s1,0.45 1,1v1h1C23.55,3 24,3.45 24,4zM21.52,8.95C21.83,9.91 22,10.94 22,12c0,5.52 -4.48,10 -10,10S2,17.52 2,12C2,6.48 6.48,2 12,2c1.5,0 2.92,0.34 4.2,0.94C16.08,3.27 16,3.62 16,4c0,1.35 0.9,2.5 2.13,2.87C18.5,8.1 19.65,9 21,9C21.18,9 21.35,8.98 21.52,8.95zM7,9.5C7,10.33 7.67,11 8.5,11S10,10.33 10,9.5S9.33,8 8.5,8S7,8.67 7,9.5zM16.31,14H7.69c-0.38,0 -0.63,0.42 -0.44,0.75C8.2,16.39 9.97,17.5 12,17.5s3.8,-1.11 4.75,-2.75C16.94,14.42 16.7,14 16.31,14zM17,9.5C17,8.67 16.33,8 15.5,8S14,8.67 14,9.5s0.67,1.5 1.5,1.5S17,10.33 17,9.5z "/>
d="M24,4c0,0.55 -0.45,1 -1,1h-1v1c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1V5h-1c-0.55,0 -1,-0.45 -1,-1c0,-0.55 0.45,-1 1,-1h1V2c0,-0.55 0.45,-1 1,-1s1,0.45 1,1v1h1C23.55,3 24,3.45 24,4zM21.52,8.95C21.83,9.91 22,10.94 22,12c0,5.52 -4.48,10 -10,10S2,17.52 2,12C2,6.48 6.48,2 12,2c1.5,0 2.92,0.34 4.2,0.94C16.08,3.27 16,3.62 16,4c0,1.35 0.9,2.5 2.13,2.87C18.5,8.1 19.65,9 21,9C21.18,9 21.35,8.98 21.52,8.95zM7,9.5C7,10.33 7.67,11 8.5,11S10,10.33 10,9.5S9.33,8 8.5,8S7,8.67 7,9.5zM16.31,14H7.69c-0.38,0 -0.63,0.42 -0.44,0.75C8.2,16.39 9.97,17.5 12,17.5s3.8,-1.11 4.75,-2.75C16.94,14.42 16.7,14 16.31,14zM17,9.5C17,8.67 16.33,8 15.5,8S14,8.67 14,9.5s0.67,1.5 1.5,1.5S17,10.33 17,9.5z "
fill="var(--primary) "/>
< / g >
< / svg >
< / div >
< b style = "font-size: 20px; flex-grow: 1;" > < span style = "color:var(--primary)" > [ < / span > < span data-i18n = "title-app" > What-Weight< / span > < span style = "color:var(--primary)" > ]< / span > < / b >
< b style = "font-size: 20px; flex-grow: 1;" > < span style = "color:var(--primary)" > [ < / span > < span
data-i18n="title-app">What-Weight< / span > < span style = "color:var(--primary)" > ]< / span > < / b >
< div id = "trendDisplay" style = "font-size: 10px; font-weight: 900; color: var(--sub);" > ...< / div >
< / header >
< div class = "card" >
< h2 data-i18n = "card-input-title" > Goal & Input< / h2 >
< div class = "goals" >
< div onclick = "setGoal('keep')" id = "g-keep" class = "goal-btn" data-i18n = "goal-keep" > MAINTAIN< / div >
< div onclick = "setGoal('lose')" id = "g-lose" class = "goal-btn" data-i18n = "goal-lose" > LOSE< / div >
< div onclick = "setGoal('gain')" id = "g-gain" class = "goal-btn" data-i18n = "goal-gain" > GAIN< / div >
< div class = "goal-btn" data-i18n = "goal-keep" id = "g-keep" onclick = "setGoal('keep')" > MAINTAIN
< / div >
< div class = "goal-btn" data-i18n = "goal-lose" id = "g-lose" onclick = "setGoal('lose')" > LOSE< / div >
< div class = "goal-btn" data-i18n = "goal-gain" id = "g-gain" onclick = "setGoal('gain')" > GAIN< / div >
< / div >
< div style = "display:grid; grid-template-columns: 1fr 1fr; gap: 10px;" >
< input type = "number" id = "targetWeight" data-i18n = "placeholder-target" placeholder = "Target kg" step = "0.1" >
< input type = "number" id = "wVal" data-i18n = "placeholder-weight" placeholder = "Weight kg" step = "0.1" inputmode = "decimal" oninput = "validate(this, 30, 250)" >
< input data-i18n = "placeholder-target" id = "targetWeight" placeholder = "Target kg"
step="0.1" type="number">
< input data-i18n = "placeholder-weight" id = "wVal" inputmode = "decimal" oninput = "validate(this, 30, 250)"
placeholder="Weight kg" step="0.1" type="number">
< / div >
< input type = "date" id = "wDate" >
< input id= "wDate" type = "d ate">
< details >
< summary > 📏 < span data-i18n = "measurements-title" > BODY MEASUREMENTS< / span > < / summary >
< div style = "display:grid; grid-template-columns: 1fr 1fr; gap: 10px;" >
< input type = "number" id = "mWaist" data-i18n = "placeholder-waist" placeholder = "Waist" step = "0.1" oninput = "validate(this, 30, 180)" >
< input type = "number" id = "mChest" data-i18n = "placeholder-chest" placeholder = "Chest" step = "0.1" oninput = "validate(this, 30, 180)" >
< input type = "number" id = "mHips" data-i18n = "placeholder-hips" placeholder = "Hips" step = "0.1" oninput = "validate(this, 30, 180)" >
< input type = "number" id = "mBicep" data-i18n = "placeholder-bicep" placeholder = "Bicep" step = "0.1" oninput = "validate(this, 10, 80)" >
< input data-i18n = "placeholder-waist" id = "mWaist" oninput = "validate(this, 30, 180)" placeholder = "Waist"
step="0.1" type="number">
< input data-i18n = "placeholder-chest" id = "mChest" oninput = "validate(this, 30, 180)" placeholder = "Chest"
step="0.1" type="number">
< input data-i18n = "placeholder-hips" id = "mHips" oninput = "validate(this, 30, 180)" placeholder = "Hips"
step="0.1" type="number">
< input data-i18n = "placeholder-bicep" id = "mBicep" oninput = "validate(this, 10, 80)" placeholder = "Bicep"
step="0.1" type="number">
< / div >
< / details >
< button onclick = "handleSaveClick()" class = "btn-main" data-i18n = "btn-save" id = "saveBtn" > SAVE< / button >
< button class = "btn-main" data-i18n = "btn-save" id = "saveBtn" onclick = "handleSaveClick()" > SAVE
< / button >
< / div >
< div class = "card" >
< h2 data-i18n = "chart-weight-title" > Weight Dynamics< / h2 >
< div class = "chart-container-fixed" >
< canvas id= "weightAxis" class= "y-axis-fixed "> < / canvas >
< div id= "wScroll" class= "scroll-container ">
< canvas class= "y-axis-fixed " id = "weightAxis "> < / canvas >
< div class= "scroll-container " id = "wScroll ">
< div id = "wWrapper" >
< canvas id = "weightChart" > < / canvas >
< / div >
@ -183,14 +192,18 @@ body.loaded {
< div class = "card" >
< h2 > < span data-i18n = "chart-measures-title" > Measurements< / span >
< select id = "measureType" style = "height:26px; font-size:10px; width:auto; border-radius:8px; margin:0; border:none; background:transparent; color:var(--primary);" onchange = "renderUI()" >
< option value = "waist" data-i18n = "placeholder-waist" > Waist< / option >
< option value = "chest" data-i18n = "placeholder-chest" > Chest< / option >
< option value = "hips" data-i18n = "placeholder-hips" > Hips< / option >
< option value = "bicep" data-i18n = "placeholder-bicep" > Bicep< / option >
< select id = "measureType"
onchange="renderUI()"
style="height:26px; font-size:10px; width:auto; border-radius:8px; margin:0; border:none; background:transparent; color:var(--primary);">
< option data-i18n = "placeholder-waist" value = "waist" > Waist< / option >
< option data-i18n = "placeholder-chest" value = "chest" > Chest< / option >
< option data-i18n = "placeholder-hips" value = "hips" > Hips< / option >
< option data-i18n = "placeholder-bicep" value = "bicep" > Bicep< / option >
< / select >
< / h2 >
< div class = "chart-wrapper" > < canvas id = "measureChart" > < / canvas > < / div >
< div class = "chart-wrapper" >
< canvas id = "measureChart" > < / canvas >
< / div >
< / div >
< div class = "card" id = "histBox" style = "display:none" >
@ -200,41 +213,57 @@ body.loaded {
< div class = "card" style = "margin-top: 20px;" >
< h3 style = "margin-top:0; margin-bottom: 15px; font-size: 16px; text-align: center;" > Данные< / h3 >
< h3 data-i18n = "data"
style="margin-top:0; margin-bottom: 15px; font-size: 16px; text-align: center;">Data< / h3 >
< div style = "display: flex; gap: 10px;" >
< button onclick= "exportToCSV()" class = "data-btn" > 📤 Экспорт < / button >
< button onclick= "showImportModal()" class = "data-btn" > 📥 Импорт < / button >
< button class= "data-btn" data-i18n = "export" onclick = "exportToCSV()" > 📤 Export < / button >
< button class= "data-btn" data-i18n = "import" onclick= "showImportModal()" > 📥 Import< / button >
< / div >
< / div >
< div id = "importModal" style = "display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.8); z-index:10000; padding:20px;" >
< div id = "importModal"
style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.8); z-index:10000; padding:20px;">
< div class = "card" style = "height:100%; display:flex; flex-direction:column;" >
< h3 style = "margin-top:0" > Вставьте содержимое CSV< / h3 >
< textarea id = "csvPasteArea" style = "flex:1; width:100%; background:var(--bg); color:var(--text); border:1px solid var(--sub); border-radius:12px; padding:10px; font-family:monospace; font-size:12px;" > < / textarea >
< h3 data-i18n = "insert-csv-data" style = "margin-top:0" > Insert CSV data< / h3 >
< textarea id = "csvPasteArea"
style="flex:1; width:100%; background:var(--bg); color:var(--text); border:1px solid var(--sub); border-radius:12px; padding:10px; font-family:monospace; font-size:12px;">< / textarea >
< div style = "display:flex; gap:10px; margin-top:15px;" >
< button onclick = "processPastedCSV()" class = "data-btn" style = "background:var(--primary); color:#000;" > Загрузить< / button >
< button onclick = "document.getElementById('importModal').style.display='none'" class = "data-btn" > Отмена< / button >
< button class = "data-btn" data-i18n = "btn-load-data"
onclick="processPastedCSV()" style="background:var(--primary); color:#000;">Load
< / button >
< button class = "data-btn"
data-i18n="btn-cancel" onclick="document.getElementById('importModal').style.display='none'">Cancel
< / button >
< / div >
< / div >
< / div >
< script >
let db = JSON.parse(localStorage.getItem('weight_tracker_data') || '{"entries":[], "config":{"goal":"keep", "target":0}}');
let wChart, mChart, wAxisChart; // Добавили wAxisChart
let confirmMode = false;
let translations = {}; // Будет заполнено через initLanguage
var db;
// Безопасная загрузка базы данных
try {
var savedData = localStorage.getItem('weight_tracker_data');
db = savedData ? JSON.parse(savedData) : { "entries": [], "config": { "goal": "keep", "target": 0 } };
} catch (e) {
console.error("Ошибка доступа к LocalStorage:", e);
// Резервный пустой объект, чтобы приложение не "упало"
db = { "entries": [], "config": { "goal": "keep", "target": 0 } };
}
var wChart, mChart, wAxisChart; // Добавили wAxisChart
var confirmMode = false;
var translations = {}; // Будет заполнено через initLanguage
async function initLanguage() {
let lang = 'ru';
var lang = 'ru';
if (window.Android & & window.Android.getLanguage) {
const fullLang = window.Android.getLanguage();
var fullLang = window.Android.getLanguage();
lang = fullLang.startsWith('ru') ? 'ru' : 'en';
}
try {
if (window.Android & & window.Android.getTranslations) {
const jsonString = window.Android.getTranslations(lang);
var jsonString = window.Android.getTranslations(lang);
translations = JSON.parse(jsonString);
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.getAttribute('data-i18n');
var key = el.getAttribute('data-i18n');
if (translations[key]) {
if (el.tagName === 'INPUT') el.placeholder = translations[key];
else el.textContent = translations[key];
@ -252,15 +281,16 @@ body.loaded {
document.querySelectorAll('.goal-btn').forEach(b => b.classList.remove('active'));
if(document.getElementById('g-' + db.config.goal)) document.getElementById('g-' + db.config.goal).classList.add('active');
document.getElementById('targetWeight').value = db.config.target || '';
const log = document.getElementById('log');
var unitKg = translations['unit_kg'] || 'kg';
var log = document.getElementById('log');
if (db.entries.length > 0) {
document.getElementById('histBox').style.display = 'block';
log.innerHTML = db.entries.slice().reverse().map(e => `
< div class = "hist-item" >
< div >
< div style = "font-size:11px; font-weight:bold; color:var(--sub)" > ${e.date.split('-').reverse().join('.')}< / div >
< div class = "hist-val" > ${e.weight || '?'} кг< / div >
< div class = "hist-val" > ${e.weight || '?'} ${unitKg}< / div >
< div style = "margin-top:4px" >
${['waist','chest','hips','bicep'].map(f => e[f] ? `< span class = "detail-tag" > ${translations[f] || f}: ${e[f]}< / span > ` : '').join('')}
< / div >
@ -274,40 +304,40 @@ body.loaded {
}
function drawCharts() {
const weights = db.entries
var weights = db.entries
.filter(e => e.weight)
.sort((a, b) => new Date(a.date) - new Date(b.date));
if (weights.length === 0) return;
const scrollContainer = document.getElementById('wScroll');
const wWrapper = document.getElementById('wWrapper');
const axisCanvas = document.getElementById('weightAxis');
const chartCanvas = document.getElementById('weightChart');
var scrollContainer = document.getElementById('wScroll');
var wWrapper = document.getElementById('wWrapper');
var axisCanvas = document.getElementById('weightAxis');
var chartCanvas = document.getElementById('weightChart');
const style = getComputedStyle(document.body);
const primaryColor = style.getPropertyValue('--primary').trim();
const textColor = style.getPropertyValue('--text').trim();
const subColor = style.getPropertyValue('--sub').trim();
var style = getComputedStyle(document.body);
var primaryColor = style.getPropertyValue('--primary').trim();
var textColor = style.getPropertyValue('--text').trim();
var subColor = style.getPropertyValue('--sub').trim();
// Цвета из твоей рабочей логики
const trendDown = '#4ade80'; // Зеленый (хорошо)
const trendUp = '#f87171'; // Красный (плохо)
const mintColor = primaryColor;
var trendDown = '#4ade80'; // Зеленый (хорошо)
var trendUp = '#f87171'; // Красный (плохо)
var mintColor = primaryColor;
// --- ТВОЯ РАБОЧАЯ ЛОГИКА ЦВЕТОВ ---
const pointColors = weights.map((e, idx) => {
const target = db.config.target || 0;
var pointColors = weights.map((e, idx) => {
var target = db.config.target || 0;
// 1. Режим УДЕРЖАНИЕ (keep)
if (db.config.goal === 'keep' & & target > 0) {
const diff = Math.abs(e.weight - target);
var diff = Math.abs(e.weight - target);
return diff > 0.4 ? trendUp : trendDown;
}
// Для режимов Снижение и Н а б о р сравниваем с предыдущим днем
if (idx === 0) return trendDown;
const dailyDiff = e.weight - weights[idx - 1].weight;
var dailyDiff = e.weight - weights[idx - 1].weight;
// 2. Режим СНИЖЕНИЕ (lose)
if (db.config.goal === 'lose') {
@ -323,13 +353,13 @@ function drawCharts() {
});
// Расчет границ шкалы (строго целые числа)
const targetVal = parseFloat(db.config.target) || 0;
const allValues = weights.map(e => e.weight);
var targetVal = parseFloat(db.config.target) || 0;
var allValues = weights.map(e => e.weight);
if (targetVal > 0) allValues.push(targetVal);
const minW = Math.floor(Math.min(...allValues) - 1);
const maxW = Math.ceil(Math.max(...allValues) + 1);
var minW = Math.floor(Math.min(...allValues) - 1);
var maxW = Math.ceil(Math.max(...allValues) + 1);
const commonYAxis = {
var commonYAxis = {
min: minW, max: maxW,
grid: { display: false, drawBorder: false },
ticks: { color: subColor, font: { size: 11, weight: 'bold' }, stepSize: 1, precision: 0, padding: 5 }
@ -356,7 +386,7 @@ function drawCharts() {
});
// Основной график
const chartWidth = Math.max(weights.length * 60, scrollContainer.clientWidth);
var chartWidth = Math.max(weights.length * 60, scrollContainer.clientWidth);
wWrapper.style.width = chartWidth + 'px';
chartCanvas.height = 240;
@ -388,18 +418,18 @@ function drawCharts() {
},
plugins: [{
afterDatasetsDraw: (chart) => {
const { ctx, chartArea: { right }, scales: { y } } = chart;
var { ctx, chartArea: { right }, scales: { y } } = chart;
ctx.save();
ctx.textAlign = 'center';
ctx.font = 'bold 11px sans-serif';
ctx.fillStyle = textColor;
chart.getDatasetMeta(0).data.forEach((point, index) => {
const val = chart.data.datasets[0].data[index];
var val = chart.data.datasets[0].data[index];
if (val) ctx.fillText(val, point.x, point.y - 12);
});
if (targetVal > 0) {
const yPos = y.getPixelForValue(targetVal);
var yPos = y.getPixelForValue(targetVal);
ctx.beginPath();
ctx.setLineDash([5, 5]);
ctx.strokeStyle = primaryColor + '66';
@ -419,9 +449,9 @@ function drawCharts() {
if (typeof renderMeasuresChart === 'function') renderMeasuresChart();
}
function renderMeasuresChart() {
const mType = document.getElementById('measureType').value;
const measures = db.entries.filter(e => e[mType]);
const mCtx = document.getElementById('measureChart').getContext('2d');
var mType = document.getElementById('measureType').value;
var measures = db.entries.filter(e => e[mType]);
var mCtx = document.getElementById('measureChart').getContext('2d');
if (mChart) mChart.destroy();
if (measures.length > 0) {
mChart = new Chart(mCtx, {
@ -437,12 +467,12 @@ function drawCharts() {
// Вспомогательные функции
function updateTrendDisplay() {
const entries = db.entries.filter(e => e.weight);
const el = document.getElementById('trendDisplay');
var entries = db.entries.filter(e => e.weight);
var el = document.getElementById('trendDisplay');
if (entries.length < 2 ) { el . innerText = translations['trend-analysis'] | | " . . . " ; return ; }
const last = entries[entries.length - 1];
const avg = entries.slice(-8, -1).reduce((s, e) => s + e.weight, 0) / Math.max(1, entries.slice(-8, -1).length);
const diff = last.weight - avg;
var last = entries[entries.length - 1];
var avg = entries.slice(-8, -1).reduce((s, e) => s + e.weight, 0) / Math.max(1, entries.slice(-8, -1).length);
var diff = last.weight - avg;
if (Math.abs(diff) < 0.1 ) { el . innerText = translations['trend-stable']; el . style . color = "var(--sub)" ; }
else if (diff > 0) { el.innerText = translations['trend-up']; el.style.color = "#f87171"; }
else { el.innerText = translations['trend-down']; el.style.color = "#4ade80"; }
@ -453,13 +483,13 @@ function drawCharts() {
function setGoal(g) { db.config.goal = g; saveDB(); renderUI(); }
function handleSaveClick() {
const date = document.getElementById('wDate').value;
var date = document.getElementById('wDate').value;
if (!date) return;
db.config.target = parseFloat(document.getElementById('targetWeight').value) || 0;
let entry = db.entries.find(e => e.date === date) || { date };
var entry = db.entries.find(e => e.date === date) || { date };
if (document.getElementById('wVal').value) entry.weight = parseFloat(document.getElementById('wVal').value);
['waist', 'chest', 'hips', 'bicep'].forEach(f => {
let v = document.getElementById('m' + f.charAt(0).toUpperCase() + f.slice(1)).value;
var v = document.getElementById('m' + f.charAt(0).toUpperCase() + f.slice(1)).value;
if (v) entry[f] = parseFloat(v);
});
if (!db.entries.find(e => e.date === date)) db.entries.push(entry);
@ -470,7 +500,7 @@ function drawCharts() {
function exportToCSV() {
if (!db.entries || db.entries.length === 0) return;
let csvContent = "date,weight,waist,chest,hips,bicep\n";
var csvContent = "date,weight,waist,chest,hips,bicep\n";
db.entries.forEach(e => {
csvContent += `${e.date},${e.weight || ''},${e.waist || ''},${e.chest || ''},${e.hips || ''},${e.bicep || ''}\n`;
});
@ -480,9 +510,9 @@ function exportToCSV() {
window.Android.exportCSV(csvContent, `weight_data_${new Date().toISOString().split('T')[0]}.csv`);
} else {
// Обычный браузерный способ (для тестов)
const blob = new Blob([csvContent], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
var blob = new Blob([csvContent], { type: 'text/csv' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'data.csv';
a.click();
@ -495,21 +525,21 @@ function showImportModal() {
}
function processPastedCSV() {
const text = document.getElementById('csvPasteArea').value;
var text = document.getElementById('csvPasteArea').value;
if (!text.trim()) return;
try {
const lines = text.split(/\r?\n/);
const dataLines = lines.slice(1); // Пропускаем заголовок (date,weight...)
var lines = text.split(/\r?\n/);
var dataLines = lines.slice(1); // Пропускаем заголовок (date,weight...)
let count = 0;
var count = 0;
dataLines.forEach(line => {
if (!line.trim()) return;
const [date, weight, waist, chest, hips, bicep] = line.split(',');
var [date, weight, waist, chest, hips, bicep] = line.split(',');
if (!date || date.length < 8 ) return ; / / Проверка на корректность даты
let entry = db.entries.find(item => item.date === date);
var entry = db.entries.find(item => item.date === date);
if (!entry) {
entry = { date };
db.entries.push(entry);