Android: - Add adaptive app icon (blue background, white flame foreground) - Add android:icon and roundIcon references to manifest - Add windowSoftInputMode=adjustResize for keyboard handling - Dark mode support via isSystemInDarkTheme() in Theme.kt - Bump versionCode 1→2, versionName 1.0→1.1, named APK output - Merge Trash into Diary as a tab (was a 6th bottom-nav item) - Add sync refresh button in top bar with loading indicator - Add CalorieGoalBar progress indicator on dashboard - Add calorieTarget to ServerSettings, Settings screen, and API - Add clearTrash() to ApiRepository (server route was unused) - Fix onCancelEdit to also clear selected image state - AiClient.kt: remove old pre-server AiClient class (replaced by ApiRepository) - LogMealScreen: side-by-side field rows, DropdownField for meal type Web server: - Add calorieTarget to settings defaults and allowed field list - Remove dead handleRequest() function (never called, Express routes it) CI: - Fix APK paths in android.yml and release.yml for renamed output Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
106 lines
5.7 KiB
Svelte
106 lines
5.7 KiB
Svelte
<script>
|
|
import Field from './Field.svelte';
|
|
|
|
export let mealTypes = [];
|
|
export let diaryRows = [];
|
|
export let diaryDays = [];
|
|
export let diarySearch;
|
|
export let diaryType;
|
|
export let diaryDate;
|
|
export let selectedEntryIds = [];
|
|
export let trash = [];
|
|
export let editEntry;
|
|
export let toggleSelected;
|
|
export let moveEntriesToTrash;
|
|
export let restoreEntry;
|
|
export let trashMode = false;
|
|
|
|
$: selectedRows = diaryRows.filter(entry => selectedEntryIds.includes(entry.id));
|
|
$: oneSelected = selectedRows.length === 1;
|
|
</script>
|
|
|
|
<section class="space-y-4">
|
|
<div>
|
|
<h2 class="text-xl font-bold">{trashMode ? 'Trash' : 'Diary'}</h2>
|
|
<p class="text-sm text-slate-500">{trashMode ? 'Restore meals moved out of the diary.' : 'Search, filter, edit, or remove meals.'}</p>
|
|
</div>
|
|
|
|
{#if trashMode}
|
|
<div id="trashEntries" class="grid gap-2">
|
|
{#if trash.length}
|
|
{#each trash as entry}
|
|
<article class="entry card p-4">
|
|
<div class="flex flex-col justify-between gap-2 sm:flex-row sm:items-center">
|
|
<div class="min-w-0">
|
|
<div class="font-semibold text-slate-800">{entry.mealName || entry.description}</div>
|
|
<div class="text-sm text-slate-500">{entry.date} {entry.time} · {entry.mealType}</div>
|
|
<div class="text-sm text-slate-400">P {entry.proteinGrams}g · C {entry.carbsGrams}g · F {entry.fatGrams}g · {entry.calories} kcal</div>
|
|
</div>
|
|
<button class="btn-secondary shrink-0" type="button" on:click={() => restoreEntry(entry.id)}>Restore</button>
|
|
</div>
|
|
</article>
|
|
{/each}
|
|
{:else}
|
|
<div class="empty-state">Trash is empty.</div>
|
|
{/if}
|
|
</div>
|
|
|
|
{:else}
|
|
<section class="card">
|
|
<div class="grid gap-3 p-4 md:grid-cols-[1fr_180px_170px]">
|
|
<Field label="Search"><input id="diarySearch" class="input" bind:value={diarySearch} placeholder="Search meals, notes, groups..."></Field>
|
|
<Field label="Meal type"><select id="diaryTypeFilter" class="input" bind:value={diaryType}><option value="">All types</option>{#each mealTypes as type}<option>{type}</option>{/each}</select></Field>
|
|
<Field label="Date"><input id="diaryDateFilter" class="input" type="date" bind:value={diaryDate}></Field>
|
|
</div>
|
|
{#if selectedEntryIds.length}
|
|
<div class="flex flex-col gap-2 border-t border-slate-200 bg-blue-50 px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
|
|
<div class="text-sm font-semibold text-blue-900">{selectedEntryIds.length} selected</div>
|
|
<div class="flex flex-col gap-2 sm:flex-row">
|
|
{#if oneSelected}<button class="btn-secondary" type="button" on:click={() => editEntry(selectedRows[0])}>Edit selected</button>{/if}
|
|
<button class="rounded-lg bg-red-600 px-4 py-2 text-sm font-bold text-white hover:bg-red-700" type="button" on:click={() => moveEntriesToTrash(selectedEntryIds)}>Move selected to trash</button>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</section>
|
|
|
|
<div id="entries" class="grid gap-3">
|
|
{#if diaryRows.length}
|
|
{#each diaryDays as day}
|
|
<section class="card">
|
|
<div class="flex items-center justify-between border-b border-slate-100 bg-slate-50 px-4 py-2.5">
|
|
<h3 class="text-sm font-bold text-slate-700">{day}</h3>
|
|
<button class="rounded text-xs font-semibold text-slate-400 hover:text-blue-600" type="button" on:click={() => diaryDate = day}>Filter day</button>
|
|
</div>
|
|
<div class="divide-y divide-slate-100">
|
|
{#each diaryRows.filter(entry => entry.date === day) as entry}
|
|
<article class="entry px-4 py-3">
|
|
<div class="flex items-start gap-3">
|
|
<input type="checkbox" class="mt-0.5 h-4 w-4 shrink-0 rounded border-slate-300 accent-blue-600" checked={selectedEntryIds.includes(entry.id)} on:change={() => toggleSelected(entry.id)}>
|
|
<div class="min-w-0 flex-1">
|
|
<div class="flex items-start justify-between gap-2">
|
|
<div class="min-w-0">
|
|
<span class="font-semibold text-slate-800">{entry.mealName || entry.description}</span>
|
|
<span class="ml-2 text-sm font-bold text-slate-600">{entry.calories} kcal</span>
|
|
</div>
|
|
<div class="flex shrink-0 gap-1">
|
|
<button class="rounded px-2 py-0.5 text-xs font-semibold text-blue-600 hover:bg-blue-50" type="button" on:click={() => editEntry(entry)}>Edit</button>
|
|
<button class="rounded px-2 py-0.5 text-xs font-semibold text-red-500 hover:bg-red-50" type="button" on:click={() => moveEntriesToTrash([entry.id])}>Delete</button>
|
|
</div>
|
|
</div>
|
|
<div class="text-sm text-slate-500">{entry.date} {entry.time} · {entry.mealType}</div>
|
|
<div class="mt-0.5 text-xs text-slate-400">P {entry.proteinGrams}g · C {entry.carbsGrams}g · F {entry.fatGrams}g · Fruit {Number(entry.fruitServings || 0).toFixed(1)} · Veg {Number(entry.vegetableServings || 0).toFixed(1)}</div>
|
|
{#if entry.description}<div class="mt-1 truncate text-xs text-slate-400">{entry.description}</div>{/if}
|
|
{#if entry.notes}<div class="text-xs text-slate-400">{entry.notes}</div>{/if}
|
|
</div>
|
|
</div>
|
|
</article>
|
|
{/each}
|
|
</div>
|
|
</section>
|
|
{/each}
|
|
{:else}
|
|
<div class="empty-state">No meals match the current filters.</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</section>
|