feat: What's New screen + updater progress, plus light-theme fixes

Post-update UX for the next release (0.1.2):

- What's New: after a version change, greet the user with a toast that opens
  an in-app release-notes screen (collapsible Added/Changed/Fixed sections)
  sourced from the bundled CHANGELOG.md, reopenable from Settings -> Updates.
  Backed by a last_seen_version settings file (get/set_last_seen_version
  commands) that distinguishes upgrades from fresh installs.
- Updater: the download/install progress toast now reappears when an update is
  started from the title-bar indicator or Settings after the prompt was
  dismissed; Settings -> Updates gains a real progress bar with a percentage.
- Light theme: fix the recurring subtle-light breakage. Neutral surfaces now
  rely on the CSS-variable remap instead of light-theme:bg-white (which forced
  surfaces dark because --color-white is remapped dark); the green action
  buttons drop the broken light-theme:hover:bg-emerald-200 (remapped dark,
  unreadable on hover) for an override-free emerald-500 tint that auto-themes,
  across the updater, What's New, and onboarding.
- Debug panel: add What's New triggers (toast / modal / reset).
This commit is contained in:
2026-06-23 21:33:08 +01:00
parent 1a95e31f78
commit c878970180
14 changed files with 537 additions and 8 deletions
+50 -1
View File
@@ -6,6 +6,7 @@ import { getVersion } from "@tauri-apps/api/app";
import { check, Update } from "@tauri-apps/plugin-updater";
import { relaunch } from "@tauri-apps/plugin-process";
import { notifyTaskComplete } from "./notifications";
import { getChangelogForVersion } from "./changelog";
// Per-folder debounce timers for batching notifications.
// Keyed as `${folderId}:embedding` or `${folderId}:tagging`.
@@ -378,6 +379,11 @@ interface GalleryState {
updateProgress: number | null; // 0..1 download progress, null while size unknown
updateError: string | null;
updateDismissed: boolean;
// "What's New" greeting after a version change. `whatsNewToast` holds the
// version to advertise in the corner toast (null = hidden); `whatsNewOpen`
// controls the full changelog modal.
whatsNewOpen: boolean;
whatsNewToast: string | null;
ffmpegStatus: FfmpegStatus;
ffmpegProgress: { downloaded_bytes: number; total_bytes: number } | null;
@@ -479,6 +485,10 @@ interface GalleryState {
checkForUpdates: (options?: { quiet?: boolean }) => Promise<void>;
installUpdate: () => Promise<void>;
dismissUpdate: () => void;
initWhatsNew: () => Promise<void>;
openWhatsNew: () => void;
closeWhatsNew: () => void;
dismissWhatsNewToast: () => void;
loadFfmpegStatus: () => Promise<void>;
retryFfmpegDownload: () => Promise<void>;
loadOnboardingCompleted: () => Promise<void>;
@@ -810,6 +820,8 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
updateProgress: null,
updateError: null,
updateDismissed: false,
whatsNewOpen: false,
whatsNewToast: null,
ffmpegStatus: "unknown",
ffmpegProgress: null,
@@ -1801,7 +1813,10 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
const update = pendingUpdate;
if (!update || get().updateStatus !== "available") return;
set({ updateStatus: "downloading", updateProgress: null, updateError: null });
// Clearing the dismissed flag re-surfaces the progress toast: the user may
// have clicked "Later" on the prompt and then triggered the install from the
// title-bar button or Settings, and they should still see download progress.
set({ updateStatus: "downloading", updateProgress: null, updateError: null, updateDismissed: false });
try {
let contentLength: number | null = null;
let downloaded = 0;
@@ -1830,6 +1845,40 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
dismissUpdate: () => set({ updateDismissed: true }),
initWhatsNew: async () => {
try {
const current = await getVersion();
const lastSeen = (await invoke<string | null>("get_last_seen_version")) || null;
// Already greeted this version — nothing to do, and no need to rewrite.
if (lastSeen === current) return;
let shouldShow: boolean;
if (lastSeen) {
// A recorded earlier version means this launch is a genuine upgrade.
shouldShow = true;
} else {
// No record yet. Fresh installs are covered by the welcome tour, so only
// greet users who have already completed onboarding (i.e. upgraded into
// this feature) rather than someone opening the app for the first time.
shouldShow = await invoke<boolean>("get_onboarding_completed").catch(() => false);
}
// Only surface the prompt if we actually have notes for this version.
if (shouldShow && getChangelogForVersion(current)) {
set({ whatsNewToast: current });
}
await invoke("set_last_seen_version", { version: current }).catch(() => {});
} catch {
// Non-fatal: the greeting is a nicety, never block startup on it.
}
},
openWhatsNew: () => set({ whatsNewOpen: true, whatsNewToast: null }),
closeWhatsNew: () => set({ whatsNewOpen: false }),
dismissWhatsNewToast: () => set({ whatsNewToast: null }),
loadFfmpegStatus: async () => {
try {
const status = await invoke<{ installed: boolean; downloading: boolean; failed: boolean }>(