Add Calendar tab for PST import and browsing imported events (T6.3)
This commit is contained in:
1 parent
f6be9645b8
commit
30edfa17d2
1 file changed
+103
-4
@@ -4,12 +4,37 @@
|
||||
// is fully demonstrable. Replace nothing here when Phase 9 lands — it just starts persisting.
|
||||
import { onMount } from "svelte";
|
||||
import { settings } from "../stores/settings.svelte";
|
||||
import { calendar } from "../stores/calendar.svelte";
|
||||
import ConsentNotice from "../components/ConsentNotice.svelte";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
import type { BackendId, SyncKind, SyncTargetConfig } from "../api";
|
||||
|
||||
let { onClose }: { onClose: () => void } = $props();
|
||||
|
||||
let section = $state<"recording" | "hardware" | "storage" | "sync">("recording");
|
||||
let section = $state<"recording" | "hardware" | "storage" | "calendar" | "sync">("recording");
|
||||
|
||||
// ---- Calendar / .pst import (T6.1/T6.2/T6.3, FR-CAL-1/2) ----
|
||||
let pstPath = $state("");
|
||||
let pstPassword = $state("");
|
||||
|
||||
async function pickPstFile() {
|
||||
const path = await open({ filters: [{ name: "Outlook data file", extensions: ["pst"] }] });
|
||||
if (typeof path === "string") pstPath = path;
|
||||
}
|
||||
async function doImportPst() {
|
||||
if (!pstPath) return;
|
||||
await calendar.importPst(pstPath, pstPassword || undefined);
|
||||
pstPassword = "";
|
||||
}
|
||||
function formatEventDate(unixSecs: number | null): string {
|
||||
if (!unixSecs) return "";
|
||||
return new Date(unixSecs * 1000).toLocaleString(undefined, {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Hardware + models (T3.6/T3.7/T3.9) ----
|
||||
function fmtBytes(mb: number): string {
|
||||
@@ -23,7 +48,10 @@
|
||||
let showConsent = $state(false);
|
||||
let testResult = $state<string | null>(null);
|
||||
|
||||
onMount(() => settings.load());
|
||||
onMount(() => {
|
||||
settings.load();
|
||||
calendar.load();
|
||||
});
|
||||
|
||||
// ---- Recording ----
|
||||
function onToggleDefaultRecord(e: Event) {
|
||||
@@ -109,6 +137,9 @@
|
||||
<button class:active={section === "storage"} onclick={() => (section = "storage")}
|
||||
>Storage</button
|
||||
>
|
||||
<button class:active={section === "calendar"} onclick={() => (section = "calendar")}
|
||||
>Calendar</button
|
||||
>
|
||||
<button class:active={section === "sync"} onclick={() => (section = "sync")}>Sync</button>
|
||||
</nav>
|
||||
<button class="close" onclick={onClose} aria-label="Close">✕</button>
|
||||
@@ -253,6 +284,58 @@
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
{:else if section === "calendar"}
|
||||
<section>
|
||||
<h3>Calendar & Outlook .pst</h3>
|
||||
<p class="muted">
|
||||
Import events and attendees from a local Outlook <code>.pst</code> backup — read-only, nothing
|
||||
is written back to the file. Nothing leaves this device.
|
||||
</p>
|
||||
<div class="grid">
|
||||
<label class="wide"
|
||||
>.pst file
|
||||
<div class="row">
|
||||
<input class="grow" bind:value={pstPath} placeholder="No file selected" readonly />
|
||||
<button onclick={pickPstFile}>Browse…</button>
|
||||
</div>
|
||||
</label>
|
||||
<label
|
||||
>Password <em>(rarely needed)</em>
|
||||
<input type="password" bind:value={pstPassword} placeholder="optional" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="primary" onclick={doImportPst} disabled={!pstPath || calendar.importing}>
|
||||
{calendar.importing ? "Importing…" : "Import"}
|
||||
</button>
|
||||
{#if calendar.importProgress}
|
||||
<span class="muted"
|
||||
>{calendar.importProgress.processed}/{calendar.importProgress.total} events</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{#if calendar.importError}
|
||||
<p class="error">
|
||||
Import failed: {calendar.importError} — the file itself is untouched; check the path and try
|
||||
again.
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<h4>Imported events</h4>
|
||||
{#if calendar.events.length === 0}
|
||||
<p class="muted">No events imported yet.</p>
|
||||
{:else}
|
||||
<ul class="events">
|
||||
{#each calendar.events as ev (ev.id)}
|
||||
<li>
|
||||
<span class="name">{ev.subject ?? "(untitled)"}</span>
|
||||
<span class="muted">{formatEventDate(ev.starts_at)}</span>
|
||||
{#if ev.organizer}<span class="muted">· {ev.organizer}</span>{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</section>
|
||||
{:else}
|
||||
<section>
|
||||
<h3>Sync & upload</h3>
|
||||
@@ -547,12 +630,14 @@
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
ul.targets,
|
||||
ul.models {
|
||||
ul.models,
|
||||
ul.events {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0.3rem 0;
|
||||
}
|
||||
ul.models li {
|
||||
ul.models li,
|
||||
ul.events li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
@@ -560,6 +645,20 @@
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
ul.events .name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.error {
|
||||
color: #e11d48;
|
||||
font-size: 0.85rem;
|
||||
margin: 0.4rem 0;
|
||||
}
|
||||
input.grow {
|
||||
flex: 1;
|
||||
}
|
||||
ul.models .name {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user