calorie-ai-android/web/src/DiaryPage.svelte
Daniel 9f660d50bc
Some checks failed
Android CI / build (push) Successful in 1m16s
Web CI / test (push) Failing after 1m46s
Android Release / release-apk (push) Successful in 1m16s
Refactor Android app to production grade with improved UI and sync
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>
2026-05-20 05:32:01 +02:00

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>