1354 lines
44 KiB
HTML
1354 lines
44 KiB
HTML
<!DOCTYPE html>
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<html lang="en" dir="ltr">
|
|
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<title>CALORIES</title>
|
|
|
|
<link rel="stylesheet" href="./css/client.css" />
|
|
|
|
<!-- Initialize theme BEFORE any components load -->
|
|
<script>
|
|
(function () {
|
|
const savedTheme = localStorage.getItem('theme');
|
|
if (savedTheme === 'dark') {
|
|
document.documentElement.classList.add('dark-mode');
|
|
if (document.body) {
|
|
document.body.classList.add('dark-mode');
|
|
}
|
|
}
|
|
})();
|
|
</script>
|
|
<link rel="shortcut icon" type="image/x-icon" href="./favicon/favicon-dots2.ico" />
|
|
|
|
<!-- SVG.js library (required by HamburgerMorphing) -->
|
|
<script src="./js/vendor/svg.min.js"></script>
|
|
|
|
<style media="screen">
|
|
#divBody {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
flex-direction: column;
|
|
padding: 20px;
|
|
font-family: var(--font-family);
|
|
overflow-y: auto;
|
|
}
|
|
|
|
#divQuickButtons {
|
|
display: flex;
|
|
flex-direction: row;
|
|
flex-wrap: wrap;
|
|
gap: 10px;
|
|
margin: 5px;
|
|
padding-bottom: 6px;
|
|
}
|
|
|
|
#divSearchContainer {
|
|
display: flex;
|
|
flex-wrap: nowrap;
|
|
flex-direction: row;
|
|
border: var(--border-width) solid var(--muted-color);
|
|
height: 2em;
|
|
margin-bottom: 10px;
|
|
background-color: var(--secondary-color);
|
|
border-radius: var(--border-radius);
|
|
}
|
|
|
|
#divSearch {
|
|
height: 2em;
|
|
padding: 0.2em;
|
|
width: 80%;
|
|
font-family: var(--font-family);
|
|
color: var(--primary-color);
|
|
background-color: var(--secondary-color);
|
|
}
|
|
|
|
#divGrams {
|
|
border-left: var(--border-width) solid var(--muted-color);
|
|
height: 2em;
|
|
padding: 0.2em;
|
|
width: 20%;
|
|
font-family: var(--font-family);
|
|
color: var(--primary-color);
|
|
background-color: var(--secondary-color);
|
|
}
|
|
|
|
#divFoodList {
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex-wrap: wrap;
|
|
height: 40vh;
|
|
overflow-y: scroll;
|
|
border: var(--border-width) solid var(--muted-color);
|
|
border-radius: var(--border-radius);
|
|
padding: 10px;
|
|
background-color: var(--secondary-color);
|
|
}
|
|
|
|
#divDiaryScroll {
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: space-between;
|
|
margin-top: 25px;
|
|
height: 2em;
|
|
font-size: 120%;
|
|
overflow-y: hidden;
|
|
color: var(--primary-color);
|
|
}
|
|
|
|
#divDiary {
|
|
height: 25vh;
|
|
border: var(--border-width) solid var(--muted-color);
|
|
border-radius: var(--border-radius);
|
|
padding: 10px;
|
|
background-color: var(--secondary-color);
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.divQuickButton {
|
|
border: 2px solid grey;
|
|
background-color: var(--secondary-color);
|
|
border-radius: 4px;
|
|
margin-left: 4px;
|
|
margin-right: 4px;
|
|
padding-left: 4px;
|
|
padding-right: 4px;
|
|
padding-top: 2px;
|
|
cursor: pointer;
|
|
}
|
|
.divQuickButton:hover {
|
|
background-color: red;
|
|
}
|
|
|
|
.divFood {
|
|
display: flex;
|
|
flex-direction: row;
|
|
flex-wrap: nowrap;
|
|
border: var(--border-width) solid var(--muted-color);
|
|
padding: 8px;
|
|
margin: 3px;
|
|
border-radius: var(--border-radius);
|
|
background-color: var(--secondary-color);
|
|
cursor: pointer;
|
|
font-family: var(--font-family);
|
|
}
|
|
.divFood:hover {
|
|
background-color: var(--button-hover-color);
|
|
}
|
|
|
|
.divFoodFlexLeft {
|
|
display: flex;
|
|
flex-direction: row;
|
|
flex-wrap: nowrap;
|
|
justify-content: flex-start;
|
|
align-items: center;
|
|
left: 0px;
|
|
width: 70%;
|
|
}
|
|
|
|
.divFoodFlexRight {
|
|
display: flex;
|
|
flex-direction: row-reverse;
|
|
flex-wrap: nowrap;
|
|
justify-content: flex-start;
|
|
align-items: center;
|
|
right: 0px;
|
|
width: 30%;
|
|
}
|
|
|
|
.divServing {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
background-color: var(--muted-color);
|
|
border: 1px solid var(--muted-color);
|
|
margin-bottom: 4px;
|
|
font-size: 75%;
|
|
margin-left: 4px;
|
|
margin-right: 4px;
|
|
}
|
|
.divServing:hover {
|
|
border: 1px solid black;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.divServingSelected {
|
|
border: 1px solid black;
|
|
background-color: var(--muted-color);
|
|
}
|
|
|
|
.divServingName {
|
|
border: 1px solid var(--muted-color);
|
|
margin-left: 4px;
|
|
margin-right: 2px;
|
|
}
|
|
|
|
.divServingButton {
|
|
border: 1px solid grey;
|
|
background-color: var(--secondary-color);
|
|
border-radius: 4px;
|
|
margin-left: 4px;
|
|
margin-right: 4px;
|
|
padding-left: 2px;
|
|
padding-right: 2px;
|
|
}
|
|
.divServingButton:hover {
|
|
background-color: red;
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* Sidenav food editing styles */
|
|
.divSideTitle {
|
|
font-weight: bold;
|
|
text-align: center;
|
|
color: var(--primary-color);
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.inDiv {
|
|
background-color: var(--secondary-color);
|
|
font-size: 85%;
|
|
border: var(--border-width) solid var(--muted-color);
|
|
min-height: 2em;
|
|
padding: 0.5em;
|
|
margin-bottom: 5px;
|
|
border-radius: var(--border-radius);
|
|
color: var(--primary-color);
|
|
font-family: var(--font-family);
|
|
height: 2.5em;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.outDiv {
|
|
font-size: 85%;
|
|
border: var(--border-width) solid var(--muted-color);
|
|
min-height: 2em;
|
|
padding: 0.5em;
|
|
margin-bottom: 5px;
|
|
overflow: auto;
|
|
height: 10em;
|
|
background-color: var(--secondary-color);
|
|
border-radius: var(--border-radius);
|
|
color: var(--primary-color);
|
|
font-family: var(--font-family);
|
|
}
|
|
|
|
.divButton {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
border: var(--border-width) solid var(--primary-color);
|
|
justify-content: center;
|
|
margin-top: 2em;
|
|
height: 4em;
|
|
border-radius: var(--border-radius);
|
|
background-color: var(--secondary-color);
|
|
cursor: pointer;
|
|
font-family: var(--font-family);
|
|
}
|
|
.divButton:hover {
|
|
background-color: var(--button-hover-color);
|
|
}
|
|
|
|
.tblDB {
|
|
font-family: var(--font-family);
|
|
border: var(--border-width) solid var(--muted-color);
|
|
border-collapse: collapse;
|
|
white-space: nowrap;
|
|
width: 100%;
|
|
font-size: 80%;
|
|
}
|
|
|
|
.tblDB th {
|
|
border: var(--border-width) solid var(--muted-color);
|
|
padding: 8px;
|
|
font-weight: bold;
|
|
vertical-align: center;
|
|
text-align: center;
|
|
background-color: var(--secondary-color);
|
|
color: var(--primary-color);
|
|
}
|
|
|
|
.tblDB td {
|
|
border: var(--border-width) solid var(--muted-color);
|
|
padding: 8px;
|
|
max-width: 400px;
|
|
overflow: hidden;
|
|
background-color: var(--secondary-color);
|
|
color: var(--primary-color);
|
|
}
|
|
|
|
.tblDB tr:hover {
|
|
background-color: var(--muted-color);
|
|
}
|
|
|
|
.divTool {
|
|
border: var(--border-width) solid var(--primary-color);
|
|
border-radius: var(--border-radius);
|
|
margin: 10px 0;
|
|
padding: 15px;
|
|
background-color: var(--secondary-color);
|
|
}
|
|
|
|
.AddFood, .EditFood {
|
|
width: 1.5em;
|
|
height: 1.5em;
|
|
cursor: pointer;
|
|
display: inline-block;
|
|
text-align: center;
|
|
line-height: 1.5em;
|
|
}
|
|
.AddFood:hover, .EditFood:hover {
|
|
background-color: green;
|
|
}
|
|
|
|
.DelFood {
|
|
width: 1.5em;
|
|
height: 1.5em;
|
|
display: inline-block;
|
|
cursor: pointer;
|
|
}
|
|
.DelFood:hover {
|
|
background-color: red;
|
|
}
|
|
|
|
.divQuickToggle {
|
|
border: 2px solid grey;
|
|
background-color: var(--secondary-color);
|
|
border-radius: 4px;
|
|
margin: 2px;
|
|
padding: 2px 4px;
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
display: inline-block;
|
|
}
|
|
.divQuickToggle:hover {
|
|
background-color: var(--muted-color);
|
|
}
|
|
.divQuickToggleActive {
|
|
border-color: var(--accent-color);
|
|
background-color: color-mix(in srgb, var(--accent-color) 15%, var(--secondary-color));
|
|
}
|
|
|
|
/* Sidenav layout - ensure relay section stays at bottom */
|
|
#divSideNav {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
#divSideNavBody {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 20px;
|
|
}
|
|
|
|
#divRelaySection {
|
|
flex-shrink: 0;
|
|
border-top: 1px solid var(--muted-color);
|
|
background-color: var(--secondary-color);
|
|
}
|
|
|
|
#divVersionBar {
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* Modal styles */
|
|
#divImportModal {
|
|
display: none;
|
|
position: fixed;
|
|
z-index: 100;
|
|
left: 0;
|
|
top: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
}
|
|
#divImportModalContent {
|
|
background-color: var(--secondary-color);
|
|
color: var(--primary-color);
|
|
margin: 15% auto;
|
|
padding: 20px;
|
|
border: 1px solid var(--primary-color);
|
|
width: 80%;
|
|
max-width: 400px;
|
|
text-align: center;
|
|
}
|
|
#divImportModalButtons {
|
|
margin-top: 20px;
|
|
display: flex;
|
|
gap: 10px;
|
|
justify-content: center;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<!-- Hamburger Button -->
|
|
<div id="divSvgHam" class="divHeaderButtons"></div>
|
|
|
|
<!-- Header -->
|
|
<div id="divHeader">
|
|
<div id="divHeaderFlexLeft"></div>
|
|
<div id="divHeaderFlexCenter">
|
|
<div class="divHeaderText">CALORIES</div>
|
|
</div>
|
|
<div id="divHeaderFlexRight"></div>
|
|
</div>
|
|
|
|
<!-- Body -->
|
|
<div id="divBody">
|
|
<div id="divQuickButtons"></div>
|
|
<div id="divSearchContainer">
|
|
<div id="divSearch" tabindex="1" contenteditable="true"></div>
|
|
<div id="divGrams" tabindex="2" contenteditable="true"></div>
|
|
</div>
|
|
<div id="divFoodList"></div>
|
|
<div id="divDiaryScroll"></div>
|
|
<div id="divDiary"></div>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div id="divFooter">
|
|
<div id="divFooterLeft" class="divFooterBox"></div>
|
|
<div id="divFooterCenter" class="divFooterBox"></div>
|
|
<div id="divFooterRight" class="divFooterBox"></div>
|
|
<div id="divFooterBalance" class="divFooterBox">0 sats</div>
|
|
</div>
|
|
|
|
<!-- Sidenav -->
|
|
<div id="divSideNav">
|
|
<div id="divSideNavHeader"></div>
|
|
<div id="divSideNavBody">
|
|
<!-- Food editing content will be loaded here -->
|
|
</div>
|
|
|
|
<div id="divAiSection" class="sidenavSection">
|
|
<div id="divAiSectionTitle" class="sidenavSectionTitle">AI</div>
|
|
<div id="divAiList" class="sidenavSectionList">
|
|
<div id="divAiProvidersList">No saved providers yet.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="divRelaySection">
|
|
<div id="divRelaySectionTitle">リレー</div>
|
|
<div id="divRelayList">Loading relays...</div>
|
|
</div>
|
|
<div id="divBlossomSection">
|
|
<div id="divBlossomSectionTitle">ブロッサム</div>
|
|
<div id="divBlossomList">Loading blossom servers...</div>
|
|
</div>
|
|
|
|
<div id="divVersionBar">
|
|
<span id="versionDisplay">loading...</span>
|
|
<div id="divVersionBarButtons">
|
|
<button id="themeToggleButton" title="Toggle Dark/Light Mode">
|
|
<div id="themeToggleHamburgerContainer"></div>
|
|
</button>
|
|
<button id="logoutButton" title="Logout">
|
|
<div id="logoutHamburgerContainer"></div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Import Modal -->
|
|
<div id="divImportModal">
|
|
<div id="divImportModalContent">
|
|
<h3>Food Database Not Found</h3>
|
|
<p>No food database found in Nostr.</p>
|
|
<p>Would you like to import the default food database?</p>
|
|
<div id="divImportModalButtons">
|
|
<div id="btnImportYes" class="btn divButton" style="flex: 1;">Yes, Import</div>
|
|
<div id="btnImportNo" class="btn divButton" style="flex: 1;">No</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="./nostr.bundle.js"></script>
|
|
<script src="/nostr-login-lite/nostr-lite.js"></script>
|
|
|
|
<script type="module">
|
|
import { initNDKPage, getPubkey, injectHeaderAvatar, subscribe, publishEvent, disconnect, encryptContent, decryptContent } from './js/init-ndk.mjs';
|
|
import { HamburgerMorphing } from "./hamburger_morphing/hamburger.mjs";
|
|
import { initFooterRelayStatus, updateFooterRelayStatus, initSidenavRelaySection, updateSidenavRelaySection, setRelayActivityState } from './js/relay-ui.mjs';
|
|
import { initBlossomSection, updateBlossomSection } from './js/blossom-ui.mjs';
|
|
|
|
import { initAiSectionWithLocalConfig } from './js/ai-ui.mjs';
|
|
import * as UT from "./js/utilities.mjs";
|
|
|
|
// Fetch version from JSON
|
|
const versionResponse = await fetch('./js/version.json');
|
|
const versionData = await versionResponse.json();
|
|
const VERSION = versionData.VERSION;
|
|
|
|
console.log(`[cal.html ${VERSION}] Loading...`);
|
|
|
|
/* ================================================================
|
|
GLOBAL VARIABLES
|
|
================================================================ */
|
|
let updateIntervalId = null;
|
|
let currentPubkey = null;
|
|
let hamburgerInstance = null;
|
|
let isNavOpen = false;
|
|
let logoutHamburger = null;
|
|
let themeToggleHamburger = null;
|
|
let isDarkMode = false;
|
|
|
|
// Calorie tracker data
|
|
let OBJ_FOODS = { rows: [] };
|
|
let OBJ_DIARY = { rows: [] };
|
|
let START_DATE = new Date();
|
|
let END_DATE = new Date();
|
|
START_DATE.setHours(0, 0, 0, 0);
|
|
END_DATE.setDate(START_DATE.getDate() + 1);
|
|
END_DATE.setHours(0, 0, 0, 0);
|
|
|
|
/* ================================================================
|
|
DOM VARIABLES
|
|
================================================================ */
|
|
const divBody = document.getElementById("divBody");
|
|
const divSideNav = document.getElementById("divSideNav");
|
|
const divSideNavBody = document.getElementById("divSideNavBody");
|
|
const divFooterCenter = document.getElementById("divFooterCenter");
|
|
const divFooterRight = document.getElementById("divFooterRight");
|
|
const divSearch = document.getElementById("divSearch");
|
|
const divGrams = document.getElementById("divGrams");
|
|
const divFoodList = document.getElementById("divFoodList");
|
|
const divDiary = document.getElementById("divDiary");
|
|
const divDiaryScroll = document.getElementById("divDiaryScroll");
|
|
const divQuickButtons = document.getElementById("divQuickButtons");
|
|
|
|
/* ================================================================
|
|
HAMBURGER MENU
|
|
================================================================ */
|
|
function initHamburgerMenu() {
|
|
hamburgerInstance = new HamburgerMorphing('#divSvgHam', {
|
|
foreground: 'var(--primary-color)',
|
|
background: 'var(--secondary-color)',
|
|
hover: 'var(--accent-color)'
|
|
});
|
|
hamburgerInstance.animateTo('burger');
|
|
}
|
|
|
|
function openNav() {
|
|
divSideNav.style.zIndex = 3;
|
|
divSideNav.style.width = "clamp(400px, 50vw, 600px)";
|
|
isNavOpen = true;
|
|
if (hamburgerInstance) hamburgerInstance.animateTo('arrow_left');
|
|
LoadSidenav();
|
|
|
|
if (!logoutHamburger) {
|
|
logoutHamburger = new HamburgerMorphing('#logoutHamburgerContainer', {
|
|
size: 24,
|
|
foreground: 'var(--primary-color)',
|
|
background: 'var(--secondary-color)',
|
|
hover: 'var(--accent-color)'
|
|
});
|
|
logoutHamburger.animateTo('x');
|
|
}
|
|
|
|
if (!themeToggleHamburger) {
|
|
themeToggleHamburger = new HamburgerMorphing('#themeToggleHamburgerContainer', {
|
|
size: 24,
|
|
foreground: 'var(--primary-color)',
|
|
background: 'var(--secondary-color)',
|
|
hover: 'var(--accent-color)'
|
|
});
|
|
const savedTheme = localStorage.getItem('theme');
|
|
isDarkMode = savedTheme === 'dark' || document.body.classList.contains('dark-mode');
|
|
themeToggleHamburger.animateTo(isDarkMode ? 'moon' : 'circle');
|
|
}
|
|
}
|
|
|
|
function closeNav() {
|
|
divSideNav.style.width = "0vw";
|
|
divSideNav.style.zIndex = -1;
|
|
isNavOpen = false;
|
|
if (hamburgerInstance) hamburgerInstance.animateTo('burger');
|
|
}
|
|
|
|
function toggleNav() {
|
|
isNavOpen ? closeNav() : openNav();
|
|
}
|
|
|
|
/* ================================================================
|
|
SIDENAV - FOOD EDITING
|
|
================================================================ */
|
|
const LoadSidenav = async () => {
|
|
let htmlOut = "";
|
|
|
|
htmlOut += `<div class="divTool">
|
|
<div class="divSideTitle">ADD FOOD</div>
|
|
<div>Name:</div>
|
|
<div id="inpFoodName" class="inDiv" contenteditable="true"></div>
|
|
<div style="display: flex; gap: 10px;">
|
|
<div style="flex: 1;">
|
|
<div>KCal:</div>
|
|
<div id="inpKcal" class="inDiv" contenteditable="true" inputmode="numeric"></div>
|
|
</div>
|
|
<div style="flex: 1;">
|
|
<div>Grams:</div>
|
|
<div id="inpGrams" class="inDiv" contenteditable="true" inputmode="numeric"></div>
|
|
</div>
|
|
</div>
|
|
<div>Servings (optional):</div>
|
|
<div style="display: flex; gap: 10px;">
|
|
<div style="flex: 1;"><div id="inpName1" class="inDiv" contenteditable="true" placeholder="Name"></div></div>
|
|
<div style="flex: 1;"><div id="inpGServ1" class="inDiv" contenteditable="true" inputmode="numeric" placeholder="Grams"></div></div>
|
|
</div>
|
|
<div style="display: flex; gap: 10px;">
|
|
<div style="flex: 1;"><div id="inpName2" class="inDiv" contenteditable="true" placeholder="Name"></div></div>
|
|
<div style="flex: 1;"><div id="inpGServ2" class="inDiv" contenteditable="true" inputmode="numeric" placeholder="Grams"></div></div>
|
|
</div>
|
|
<div style="display: flex; gap: 10px;">
|
|
<div style="flex: 1;"><div id="inpName3" class="inDiv" contenteditable="true" placeholder="Name"></div></div>
|
|
<div style="flex: 1;"><div id="inpGServ3" class="inDiv" contenteditable="true" inputmode="numeric" placeholder="Grams"></div></div>
|
|
</div>
|
|
<div id="btnNewFood" class="btn divButton">ADD FOOD</div>
|
|
</div>`;
|
|
|
|
// Food list table
|
|
htmlOut += `<table id="tblFoods" class="tblDB">
|
|
<tr>
|
|
<th></th>
|
|
<th>Food</th>
|
|
<th>Cal/g</th>
|
|
<th>Quick</th>
|
|
</tr>`;
|
|
|
|
for (let F of OBJ_FOODS.rows) {
|
|
let servingButtons = '';
|
|
for (let i = 0; i < F.Servings.length; i++) {
|
|
const serving = F.Servings[i];
|
|
const isActive = F.QuickButtons && F.QuickButtons.includes(i);
|
|
const buttonClass = isActive ? 'divQuickToggle divQuickToggleActive' : 'divQuickToggle';
|
|
servingButtons += `<div class="${buttonClass}" data-food-id="${F.id}" data-serving-index="${i}">${serving[0]}</div>`;
|
|
}
|
|
|
|
htmlOut += `<tr>
|
|
<td><span class="EditFood" data-food-id="${F.id}">E</span><span class="DelFood" data-id="${F.id}">X</span></td>
|
|
<td>${F.Food}</td>
|
|
<td>${F.CalPerGram}</td>
|
|
<td>${servingButtons}</td>
|
|
</tr>`;
|
|
}
|
|
htmlOut += `</table>`;
|
|
|
|
// Export/Import
|
|
htmlOut += `<div class="divTool">
|
|
<div class="divSideTitle">EXPORT/IMPORT</div>
|
|
<div id="expFood" class="outDiv"> </div>
|
|
<div id="btnCopyFood" class="btn divButton" style="margin-top:10px;height:2.5em;">Copy Food JSON</div>
|
|
<div id="inpFood" class="outDiv" contenteditable="true" style="margin-top:10px;"> </div>
|
|
<div id="btnSaveFood" class="btn divButton" style="margin-top:10px;height:2.5em;">Import Foods</div>
|
|
</div>`;
|
|
|
|
divSideNavBody.innerHTML = htmlOut;
|
|
|
|
// Update export area
|
|
document.getElementById("expFood").innerText = JSON.stringify(OBJ_FOODS, null, 2);
|
|
|
|
// Event listeners
|
|
document.getElementById("btnNewFood").addEventListener("click", SaveNewFood);
|
|
document.getElementById("btnCopyFood").addEventListener("click", () => copyToClipboard("expFood"));
|
|
document.getElementById("btnSaveFood").addEventListener("click", InitialSaveFood);
|
|
|
|
document.querySelectorAll(".DelFood").forEach(el => {
|
|
el.addEventListener("click", function() { DeleteFoodItem(this); });
|
|
});
|
|
|
|
document.querySelectorAll(".EditFood").forEach(el => {
|
|
el.addEventListener("click", function() { EditFood(this); });
|
|
});
|
|
|
|
document.querySelectorAll(".divQuickToggle").forEach(el => {
|
|
el.addEventListener("click", function() { ToggleQuickButton(this); });
|
|
});
|
|
};
|
|
|
|
/* ================================================================
|
|
CALORIE TRACKER FUNCTIONS
|
|
================================================================ */
|
|
const LoadQuickButtons = async () => {
|
|
divQuickButtons.innerHTML = '';
|
|
|
|
for (const food of OBJ_FOODS.rows) {
|
|
if (food.QuickButtons && food.QuickButtons.length > 0) {
|
|
for (const servingIndex of food.QuickButtons) {
|
|
if (servingIndex < food.Servings.length) {
|
|
const serving = food.Servings[servingIndex];
|
|
let divServing = document.createElement("div");
|
|
divServing.setAttribute("class", "divQuickButton");
|
|
divServing.dataset.Food = food.Food;
|
|
divServing.dataset.Grams = serving[1];
|
|
divServing.dataset.KCals = (food.CalPerGram * serving[1]).toFixed(0);
|
|
divServing.innerHTML = `${food.Food} ${serving[0]}`;
|
|
divQuickButtons.appendChild(divServing);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Default buttons if none exist
|
|
if (divQuickButtons.children.length === 0) {
|
|
const defaults = [
|
|
{ Food: "Cream, Heavy", Grams: 10, KCals: 34, Label: "Heavy Cream 10g" },
|
|
{ Food: "Egg", Grams: 45, KCals: 71, Label: "Large Egg" },
|
|
{ Food: "Sausage, Andouille", Grams: 85, KCals: 200, Label: "Sausage Link" },
|
|
{ Food: "Chicken, Breast", Grams: 50, KCals: 86, Label: "Chicken 50g" },
|
|
{ Food: "Beef, Ribeye", Grams: 50, KCals: 88, Label: "Steak 50g" },
|
|
{ Food: "Butter", Grams: 5, KCals: 36, Label: "Butter 5g" },
|
|
];
|
|
defaults.forEach(d => {
|
|
let divServing = document.createElement("div");
|
|
divServing.setAttribute("class", "divQuickButton");
|
|
divServing.dataset.Food = d.Food;
|
|
divServing.dataset.Grams = d.Grams;
|
|
divServing.dataset.KCals = d.KCals;
|
|
divServing.innerHTML = d.Label;
|
|
divQuickButtons.appendChild(divServing);
|
|
});
|
|
}
|
|
|
|
document.querySelectorAll(".divQuickButton").forEach(el => {
|
|
el.addEventListener("click", function() { AddDiaryItem(this); });
|
|
});
|
|
};
|
|
|
|
const DisplayFoodList = async () => {
|
|
divFoodList.innerHTML = "";
|
|
let SortArr = await UT.arrOfObjSort(OBJ_FOODS.rows, 'Food');
|
|
|
|
for (const [row, Each] of SortArr.entries()) {
|
|
if (Each.Selected > 0) {
|
|
let divFood = document.createElement("div");
|
|
divFood.setAttribute("class", Each.Selected == 2 ? "divServing divServingSelected" : "divServing");
|
|
divFood.dataset.row = row;
|
|
|
|
let divFoodName = document.createElement("div");
|
|
divFoodName.setAttribute("class", "divServingName");
|
|
divFoodName.innerHTML = `${Each.Food}`;
|
|
divFood.appendChild(divFoodName);
|
|
|
|
for (const Serv of Each.Servings) {
|
|
let divServing = document.createElement("div");
|
|
divServing.setAttribute("class", "divServingButton");
|
|
divServing.dataset.Food = Each.Food;
|
|
divServing.dataset.Grams = Serv[1];
|
|
divServing.dataset.KCals = (Each.CalPerGram * Serv[1]).toFixed(0);
|
|
divServing.innerHTML = `${Serv[0]}`;
|
|
divFood.appendChild(divServing);
|
|
}
|
|
divFoodList.appendChild(divFood);
|
|
}
|
|
}
|
|
|
|
document.querySelectorAll(".divServingButton").forEach(el => {
|
|
el.addEventListener("click", function() { AddDiaryItem(this); });
|
|
});
|
|
|
|
document.querySelectorAll(".divServing").forEach(el => {
|
|
el.addEventListener("click", function(e) {
|
|
if (e.target.classList.contains('divServing')) {
|
|
SelectFood(this);
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
const FilterFoodList = async (FoodFilter = "") => {
|
|
let OneSelected = false;
|
|
for (const [row, Each] of OBJ_FOODS.rows.entries()) {
|
|
if (FoodFilter == "") {
|
|
Each.Selected = 1;
|
|
} else if (Each.Food.toLowerCase().startsWith(FoodFilter.toLowerCase())) {
|
|
Each.Selected = OneSelected ? 1 : 2;
|
|
OneSelected = true;
|
|
} else {
|
|
Each.Selected = 0;
|
|
}
|
|
}
|
|
DisplayFoodList();
|
|
};
|
|
|
|
const SelectFood = async (t) => {
|
|
let Row = t.dataset.row;
|
|
for (const [row, Each] of OBJ_FOODS.rows.entries()) {
|
|
if (row == Row) {
|
|
Each.Selected = 2;
|
|
divSearch.innerText = Each.Food;
|
|
} else {
|
|
if (Each.Selected == 2) Each.Selected = 1;
|
|
}
|
|
}
|
|
DisplayFoodList();
|
|
};
|
|
|
|
const UpdateFoodDiary = async () => {
|
|
divDiary.innerHTML = "";
|
|
let TotalCal = 0;
|
|
|
|
for (const [row, Each] of OBJ_DIARY.rows.entries()) {
|
|
if (Each.Date >= START_DATE.toISOString() && Each.Date < END_DATE.toISOString()) {
|
|
TotalCal += Each.KCals;
|
|
|
|
let divFood = document.createElement("div");
|
|
divFood.setAttribute("class", "divFood");
|
|
|
|
let divFoodLeft = document.createElement("div");
|
|
divFoodLeft.setAttribute("class", "divFoodFlexLeft");
|
|
|
|
let divFoodRight = document.createElement("div");
|
|
divFoodRight.setAttribute("class", "divFoodFlexRight");
|
|
|
|
let svgX = await UT.svgIcon("X");
|
|
svgX.setAttribute("class", "svgIcon");
|
|
svgX.style.cursor = "pointer";
|
|
svgX.addEventListener("click", function() { DeleteDiaryItem(row); });
|
|
divFoodLeft.appendChild(svgX);
|
|
|
|
let divFoodName = document.createTextNode(`${Each.Food} ${Each.Grams}g `);
|
|
divFoodLeft.appendChild(divFoodName);
|
|
|
|
let divFoodCal = document.createTextNode(`${Each.KCals} KCal`);
|
|
divFoodRight.appendChild(divFoodCal);
|
|
|
|
divFood.appendChild(divFoodLeft);
|
|
divFood.appendChild(divFoodRight);
|
|
divDiary.appendChild(divFood);
|
|
}
|
|
}
|
|
|
|
// Total
|
|
let divFood = document.createElement("div");
|
|
divFood.setAttribute("class", "divFood");
|
|
let divFoodLeft = document.createElement("div");
|
|
divFoodLeft.setAttribute("class", "divFoodFlexLeft");
|
|
let divFoodRight = document.createElement("div");
|
|
divFoodRight.setAttribute("class", "divFoodFlexRight");
|
|
let divFoodCal = document.createTextNode(`${TotalCal} KCal`);
|
|
divFoodRight.appendChild(divFoodCal);
|
|
divFood.appendChild(divFoodLeft);
|
|
divFood.appendChild(divFoodRight);
|
|
divDiary.appendChild(divFood);
|
|
};
|
|
|
|
const LoadDiaryScroll = async () => {
|
|
divDiaryScroll.innerHTML = "";
|
|
let svgLeft = await UT.svgIcon("carrot_left");
|
|
svgLeft.setAttribute("class", "svgIconMedium");
|
|
svgLeft.style.cursor = "pointer";
|
|
svgLeft.addEventListener("click", function() { IncrementDate(-1); });
|
|
divDiaryScroll.appendChild(svgLeft);
|
|
|
|
let txtDate = document.createTextNode(START_DATE.toLocaleDateString());
|
|
divDiaryScroll.appendChild(txtDate);
|
|
|
|
let svgRight = await UT.svgIcon("carrot_right");
|
|
svgRight.setAttribute("class", "svgIconMedium");
|
|
svgRight.style.cursor = "pointer";
|
|
svgRight.addEventListener("click", function() { IncrementDate(1); });
|
|
divDiaryScroll.appendChild(svgRight);
|
|
};
|
|
|
|
const IncrementDate = async (incDays) => {
|
|
START_DATE.setDate(START_DATE.getDate() + incDays);
|
|
END_DATE.setDate(END_DATE.getDate() + incDays);
|
|
LoadDiaryScroll();
|
|
UpdateFoodDiary();
|
|
};
|
|
|
|
const DeleteDiaryItem = async (row) => {
|
|
OBJ_DIARY.rows.splice(row, 1);
|
|
await saveToStorage();
|
|
UpdateFoodDiary();
|
|
};
|
|
|
|
const AddDiaryItem = async (t) => {
|
|
let AddItem = true;
|
|
for (let Each of OBJ_DIARY.rows) {
|
|
if (Each.Food === t.dataset.Food && Each.Date >= START_DATE.toISOString() && Each.Date < END_DATE.toISOString()) {
|
|
let KcalPerG = Each.KCals / Each.Grams;
|
|
Each.Grams += Number(t.dataset.Grams);
|
|
Each.KCals += Number((Number(t.dataset.Grams) * KcalPerG).toFixed(0));
|
|
AddItem = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (AddItem) {
|
|
let objDiaryEntry = {};
|
|
let Cur_id = (await UT.numMax_id(OBJ_DIARY.rows)) + 1;
|
|
objDiaryEntry.id = Cur_id;
|
|
objDiaryEntry.Date = START_DATE.toISOString();
|
|
objDiaryEntry.Food = t.dataset.Food;
|
|
objDiaryEntry.Grams = Number(t.dataset.Grams);
|
|
objDiaryEntry.KCals = Number(t.dataset.KCals);
|
|
OBJ_DIARY.rows.push(objDiaryEntry);
|
|
}
|
|
|
|
await saveToStorage();
|
|
UpdateFoodDiary();
|
|
};
|
|
|
|
/* ================================================================
|
|
NOSTR STORAGE FUNCTIONS (Kind 30078)
|
|
|
|
These functions store calorie data on Nostr using kind 30078
|
|
application-specific data events with NIP-04 encryption.
|
|
|
|
Data is stored in one object event:
|
|
- "calorie": { foods: {...}, diary: {...} }
|
|
|
|
On first load, if no data exists on Nostr, a popup asks the user
|
|
to import the default food database from food.json.
|
|
================================================================ */
|
|
|
|
// Pending 30078 loads waiting for EOSE
|
|
let pending30078Loads = new Map();
|
|
|
|
// Set up global event listener for 30078 events
|
|
window.addEventListener('ndkEvent', (e) => {
|
|
const evt = e.detail;
|
|
if (evt.kind !== 30078 || evt.pubkey !== currentPubkey) return;
|
|
|
|
// Extract d tag
|
|
const dTag = evt.tags.find(t => t[0] === 'd')?.[1];
|
|
if (!dTag) return;
|
|
|
|
// Check if we're waiting for this load
|
|
const pending = pending30078Loads.get(dTag);
|
|
if (pending) {
|
|
// Keep the most recent event
|
|
if (!pending.event || evt.created_at > pending.event.created_at) {
|
|
pending.event = evt;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Handle EOSE events - worker sends { subId } not { data }
|
|
window.addEventListener('ndkEose', (e) => {
|
|
console.log('[cal.html] ndkEose received:', e.detail);
|
|
const subId = e.detail?.subId;
|
|
if (!subId) {
|
|
console.log('[cal.html] ndkEose: no subId, ignoring');
|
|
return;
|
|
}
|
|
|
|
// Check pending loads
|
|
for (const [dTag, pending] of pending30078Loads) {
|
|
if (pending.subId === subId) {
|
|
if (pending.event) {
|
|
// Decrypt and resolve
|
|
(async () => {
|
|
try {
|
|
const pubkey = await getPubkey();
|
|
const decryptedResp = await decryptContent(pubkey, pending.event.content, 'auto');
|
|
const decrypted = String(decryptedResp?.plaintext || '');
|
|
const obj = JSON.parse(decrypted);
|
|
console.log(`[cal.html] Loaded ${dTag} from Nostr:`, obj);
|
|
pending.resolve({ found: true, data: obj });
|
|
} catch (error) {
|
|
console.error(`[cal.html] Failed to decrypt ${dTag}:`, error);
|
|
pending.resolve({ found: false, error: error.message });
|
|
}
|
|
})();
|
|
} else {
|
|
console.log(`[cal.html] No ${dTag} found in Nostr`);
|
|
pending.resolve({ found: false });
|
|
}
|
|
pending30078Loads.delete(dTag);
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Save object to Nostr kind 30078
|
|
const saveObj30078 = async (name, obj) => {
|
|
console.log(`[cal.html] Saving ${name} to Nostr...`);
|
|
try {
|
|
const pubkey = await getPubkey();
|
|
const encryptedResp = await encryptContent(pubkey, JSON.stringify(obj), 'nip44');
|
|
const encrypted = String(encryptedResp?.ciphertext || '');
|
|
|
|
const event = {
|
|
kind: 30078,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: [['d', name]],
|
|
content: encrypted
|
|
};
|
|
|
|
const result = await publishEvent(event);
|
|
console.log(`[cal.html] Saved ${name} to Nostr:`, result);
|
|
return result;
|
|
} catch (error) {
|
|
console.error(`[cal.html] Failed to save ${name}:`, error);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
// Load object from Nostr kind 30078
|
|
const loadObj30078 = async (name) => {
|
|
console.log(`[cal.html] Loading ${name} from Nostr...`);
|
|
|
|
return new Promise((resolve) => {
|
|
// Create subscription
|
|
const sub = subscribe(
|
|
{ kinds: [30078], '#d': [name], authors: [currentPubkey] },
|
|
{ closeOnEose: true, cacheUsage: 'CACHE_FIRST' }
|
|
);
|
|
|
|
// Store pending load
|
|
pending30078Loads.set(name, {
|
|
subId: sub.subId,
|
|
event: null,
|
|
resolve: resolve
|
|
});
|
|
|
|
// Timeout after 5 seconds
|
|
setTimeout(() => {
|
|
const pending = pending30078Loads.get(name);
|
|
if (pending) {
|
|
pending30078Loads.delete(name);
|
|
sub.close();
|
|
resolve({ found: false, timeout: true });
|
|
}
|
|
}, 5000);
|
|
});
|
|
};
|
|
|
|
// Show import modal
|
|
const showImportModal = () => {
|
|
return new Promise((resolve) => {
|
|
const modal = document.getElementById('divImportModal');
|
|
const btnYes = document.getElementById('btnImportYes');
|
|
const btnNo = document.getElementById('btnImportNo');
|
|
|
|
modal.style.display = 'block';
|
|
|
|
const handleYes = () => {
|
|
modal.style.display = 'none';
|
|
btnYes.removeEventListener('click', handleYes);
|
|
btnNo.removeEventListener('click', handleNo);
|
|
resolve(true);
|
|
};
|
|
|
|
const handleNo = () => {
|
|
modal.style.display = 'none';
|
|
btnYes.removeEventListener('click', handleYes);
|
|
btnNo.removeEventListener('click', handleNo);
|
|
resolve(false);
|
|
};
|
|
|
|
btnYes.addEventListener('click', handleYes);
|
|
btnNo.addEventListener('click', handleNo);
|
|
});
|
|
};
|
|
|
|
// Load default food.json
|
|
const loadDefaultFoods = async () => {
|
|
console.log('[cal.html] Loading default food.json...');
|
|
try {
|
|
const response = await fetch('./food.json');
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
const data = await response.json();
|
|
console.log('[cal.html] Loaded default foods:', data);
|
|
return data;
|
|
} catch (error) {
|
|
console.error('[cal.html] Failed to load food.json:', error);
|
|
return { rows: [] };
|
|
}
|
|
};
|
|
|
|
// Initialize data - try Nostr first, fallback to import
|
|
const initializeData = async () => {
|
|
console.log('[cal.html] Initializing data...');
|
|
|
|
// Preferred v2 layout: single d:calorie event
|
|
const combinedResult = await loadObj30078('calorie');
|
|
if (combinedResult.found) {
|
|
const payload = combinedResult.data || {};
|
|
OBJ_FOODS = payload?.foods && typeof payload.foods === 'object' ? payload.foods : { rows: [] };
|
|
OBJ_DIARY = payload?.diary && typeof payload.diary === 'object' ? payload.diary : { rows: [] };
|
|
console.log('[cal.html] Using combined calorie payload from Nostr');
|
|
return;
|
|
}
|
|
|
|
// Legacy fallback: read old split d-tags, then migrate to combined payload.
|
|
const foodsResult = await loadObj30078('calorie_foods');
|
|
if (foodsResult.found) {
|
|
OBJ_FOODS = foodsResult.data;
|
|
console.log('[cal.html] Using legacy foods from Nostr');
|
|
} else {
|
|
const shouldImport = await showImportModal();
|
|
if (shouldImport) {
|
|
OBJ_FOODS = await loadDefaultFoods();
|
|
} else {
|
|
OBJ_FOODS = { rows: [] };
|
|
}
|
|
}
|
|
|
|
const diaryResult = await loadObj30078('calorie_diary');
|
|
if (diaryResult.found) {
|
|
OBJ_DIARY = diaryResult.data;
|
|
console.log('[cal.html] Using legacy diary from Nostr');
|
|
} else {
|
|
OBJ_DIARY = { rows: [] };
|
|
}
|
|
|
|
// One-time migration write to v2 combined event.
|
|
await saveObj30078('calorie', { foods: OBJ_FOODS, diary: OBJ_DIARY });
|
|
};
|
|
|
|
// Wrapper functions for compatibility
|
|
const saveToStorage = async () => {
|
|
await saveObj30078('calorie', { foods: OBJ_FOODS, diary: OBJ_DIARY });
|
|
};
|
|
|
|
const loadFromStorage = () => {
|
|
// No-op - data loaded via initializeData
|
|
console.log('[cal.html] loadFromStorage called (no-op, use initializeData)');
|
|
};
|
|
|
|
/* ================================================================
|
|
FOOD MANAGEMENT
|
|
================================================================ */
|
|
const SaveNewFood = async () => {
|
|
let Food = document.getElementById("inpFoodName").innerText.trim();
|
|
let Kcal = Number(document.getElementById("inpKcal").innerText.trim());
|
|
let Grams = Number(document.getElementById("inpGrams").innerText.trim());
|
|
|
|
if (!Food || !Kcal || !Grams) {
|
|
alert("Please fill in food name, calories, and grams");
|
|
return;
|
|
}
|
|
|
|
let objNewFood = {};
|
|
objNewFood.id = Date.now();
|
|
objNewFood.Food = Food;
|
|
objNewFood.Selected = 0;
|
|
objNewFood.CalPerGram = Number((Kcal / Grams).toFixed(3));
|
|
objNewFood.Servings = [["100g", 100]];
|
|
objNewFood.QuickButtons = [];
|
|
|
|
// Add optional servings
|
|
for (let i = 1; i <= 3; i++) {
|
|
let Name = document.getElementById(`inpName${i}`).innerText.trim();
|
|
let Gram = Number(document.getElementById(`inpGServ${i}`).innerText.trim());
|
|
if (Name.length > 0 && Gram > 0) {
|
|
objNewFood.Servings.push([Name, Gram]);
|
|
}
|
|
}
|
|
|
|
OBJ_FOODS.rows.push(objNewFood);
|
|
await saveToStorage();
|
|
await FilterFoodList();
|
|
DisplayFoodList();
|
|
LoadQuickButtons();
|
|
LoadSidenav();
|
|
};
|
|
|
|
const DeleteFoodItem = async (t) => {
|
|
for (const [i, Each] of OBJ_FOODS.rows.entries()) {
|
|
if (Number(Each.id) == Number(t.dataset.id)) {
|
|
OBJ_FOODS.rows.splice(i, 1);
|
|
await saveToStorage();
|
|
DisplayFoodList();
|
|
LoadQuickButtons();
|
|
LoadSidenav();
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
const ToggleQuickButton = async (t) => {
|
|
const foodId = t.dataset.foodId;
|
|
const servingIndex = parseInt(t.dataset.servingIndex);
|
|
|
|
for (const food of OBJ_FOODS.rows) {
|
|
if (food.id == foodId) {
|
|
if (!food.QuickButtons) food.QuickButtons = [];
|
|
const index = food.QuickButtons.indexOf(servingIndex);
|
|
if (index > -1) {
|
|
food.QuickButtons.splice(index, 1);
|
|
} else {
|
|
food.QuickButtons.push(servingIndex);
|
|
}
|
|
await saveToStorage();
|
|
LoadQuickButtons();
|
|
LoadSidenav();
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
const EditFood = async (t) => {
|
|
// Simple edit - just alert for now, can be expanded
|
|
alert("Edit functionality - click Quick buttons to toggle quick add");
|
|
};
|
|
|
|
const copyToClipboard = async (elementId) => {
|
|
const element = document.getElementById(elementId);
|
|
try {
|
|
await navigator.clipboard.writeText(element.innerText);
|
|
console.log("JSON copied to clipboard");
|
|
} catch (err) {
|
|
console.error("Failed to copy:", err);
|
|
}
|
|
};
|
|
|
|
const InitialSaveFood = async () => {
|
|
try {
|
|
let objF = JSON.parse(document.getElementById("inpFood").innerText.trim());
|
|
OBJ_FOODS = objF;
|
|
await saveToStorage();
|
|
FilterFoodList();
|
|
DisplayFoodList();
|
|
LoadQuickButtons();
|
|
LoadSidenav();
|
|
} catch (e) {
|
|
alert("Invalid JSON");
|
|
}
|
|
};
|
|
|
|
/* ================================================================
|
|
SEARCH
|
|
================================================================ */
|
|
const SearchBarKeyDown = async (evt, t) => {
|
|
if (evt.key.length == 1) {
|
|
let FullWord = (t.innerText + evt.key).trim().toLowerCase();
|
|
FilterFoodList(FullWord);
|
|
} else if (evt.key == "Backspace") {
|
|
let FullWord = t.innerText.trim().toLowerCase().slice(0, -1);
|
|
FilterFoodList(FullWord);
|
|
}
|
|
};
|
|
|
|
const NewDiaryLostFocus = async () => {
|
|
if (!Number.isNaN(divGrams.innerText)) {
|
|
for (const [row, Each] of OBJ_FOODS.rows.entries()) {
|
|
if (Each.Selected == 2) {
|
|
let AddItem = true;
|
|
for (let F of OBJ_DIARY.rows) {
|
|
if (F.Food === Each.Food && F.Date >= START_DATE.toISOString() && F.Date < END_DATE.toISOString()) {
|
|
let KcalPerG = Number(F.KCals / F.Grams);
|
|
F.Grams += Number(divGrams.innerText);
|
|
F.KCals += Number((Number(divGrams.innerText) * KcalPerG).toFixed(0));
|
|
AddItem = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (AddItem) {
|
|
let objDiaryEntry = {};
|
|
let Cur_id = (await UT.numMax_id(OBJ_DIARY.rows)) + 1;
|
|
objDiaryEntry.id = Cur_id;
|
|
objDiaryEntry.Date = START_DATE.toISOString();
|
|
objDiaryEntry.Food = Each.Food;
|
|
objDiaryEntry.Grams = Number(divGrams.innerText);
|
|
objDiaryEntry.KCals = Number((Each.CalPerGram * Number(divGrams.innerText)).toFixed(0));
|
|
OBJ_DIARY.rows.push(objDiaryEntry);
|
|
}
|
|
|
|
await saveToStorage();
|
|
UpdateFoodDiary();
|
|
divGrams.innerText = "";
|
|
divSearch.innerText = "";
|
|
FilterFoodList();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/* ================================================================
|
|
FOOTER & LOGOUT
|
|
================================================================ */
|
|
const UpdateFooter = async () => {
|
|
try {
|
|
await updateFooterRelayStatus();
|
|
await updateSidenavRelaySection();
|
|
await updateBlossomSection();
|
|
divFooterCenter.innerHTML = '';
|
|
divFooterRight.innerHTML = '';
|
|
} catch (error) {
|
|
console.error('[cal.html] Error updating footer:', error);
|
|
}
|
|
};
|
|
|
|
const Logout = async () => {
|
|
if (updateIntervalId) {
|
|
clearInterval(updateIntervalId);
|
|
updateIntervalId = null;
|
|
}
|
|
disconnect();
|
|
if (window.NOSTR_LOGIN_LITE?.logout) {
|
|
await window.NOSTR_LOGIN_LITE.logout();
|
|
}
|
|
localStorage.clear();
|
|
sessionStorage.clear();
|
|
location.reload(true);
|
|
};
|
|
|
|
/* ================================================================
|
|
INITIALIZATION
|
|
================================================================ */
|
|
(async function main() {
|
|
try {
|
|
initHamburgerMenu();
|
|
document.getElementById('divSvgHam').addEventListener('click', toggleNav);
|
|
|
|
document.getElementById('themeToggleButton')?.addEventListener('click', () => {
|
|
isDarkMode = !isDarkMode;
|
|
localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
|
|
document.documentElement.classList.toggle('dark-mode', isDarkMode);
|
|
document.body.classList.toggle('dark-mode', isDarkMode);
|
|
if (themeToggleHamburger) {
|
|
themeToggleHamburger.animateTo(isDarkMode ? 'moon' : 'circle');
|
|
}
|
|
});
|
|
|
|
document.getElementById('logoutButton')?.addEventListener('click', Logout);
|
|
|
|
await initNDKPage();
|
|
currentPubkey = await getPubkey();
|
|
await injectHeaderAvatar(currentPubkey);
|
|
console.log('[cal.html] Authenticated as:', currentPubkey);
|
|
|
|
initFooterRelayStatus();
|
|
initSidenavRelaySection();
|
|
|
|
await initBlossomSection();
|
|
initAiSectionWithLocalConfig();
|
|
|
|
// Load calorie data from Nostr (or import default)
|
|
await initializeData();
|
|
|
|
// Set up search
|
|
divSearch.addEventListener("keydown", function() { SearchBarKeyDown(event, this); });
|
|
divGrams.addEventListener("blur", function() { NewDiaryLostFocus(); });
|
|
|
|
// Initialize display
|
|
LoadQuickButtons();
|
|
await FilterFoodList();
|
|
DisplayFoodList();
|
|
LoadDiaryScroll();
|
|
UpdateFoodDiary();
|
|
|
|
// Start update loop
|
|
updateIntervalId = setInterval(UpdateFooter, 1000);
|
|
document.getElementById('versionDisplay').textContent = VERSION;
|
|
|
|
|
|
console.log('[cal.html] Initialization complete');
|
|
} catch (error) {
|
|
console.error('[cal.html] Initialization failed:', error);
|
|
}
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|