Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 0 additions & 115 deletions src/components/UpdateBanner.tsx

This file was deleted.

3 changes: 3 additions & 0 deletions src/components/browser/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { getLiveWorkspaceActiveTab } from "@/lib/workspaceTabs";
import { toggleBookmarkForActiveTab } from "@/lib/bookmarkAction";
import { resolveInput } from "@/lib/url";
import { focusAppWebview } from "@/services/browser";
import { UpdateButton } from "@/components/browser/UpdateButton";

/**
* Address-bar security indicator, derived purely from the URL scheme.
Expand Down Expand Up @@ -252,6 +253,8 @@ export function Toolbar({ onNavigate, onBack, onForward, onReload }: ToolbarProp
</div>
</div>

<UpdateButton />

{/* Bookmark button */}
<button
onClick={() => toggleBookmarkForActiveTab()}
Expand Down
37 changes: 37 additions & 0 deletions src/components/browser/UpdateButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Toolbar pill for an available update. Lives in the toolbar because anything
* drawn over the content area sits under the native tab webview and is never
* seen (see stores/updates.ts).
*/
import { Download, RotateCw } from "lucide-react";
import { useUpdatesStore } from "@/stores/updates";

export function UpdateButton() {
const { update, phase, pct, install, restart } = useUpdatesStore();
if (!update) return null;

const label =
phase === "downloading"
? `Updating ${pct}%`
: phase === "ready"
? "Restart to update"
: phase === "failed"
? "Update failed · retry"
: `Update to ${update.version}`;

return (
<button
onClick={() => void (phase === "ready" ? restart() : install())}
disabled={phase === "downloading"}
title={
phase === "failed"
? "The download or install failed. Click to try again, or get the installer from GitHub releases."
: `ZYNLEX ${update.version} is available`
}
className="h-7 px-2.5 mr-1 flex items-center gap-1.5 rounded-[4px] text-xs font-mono whitespace-nowrap bg-[var(--color-accent)] text-[var(--color-text-inverse)] hover:bg-[var(--color-accent-hover)] disabled:opacity-80 disabled:cursor-default transition-colors"
>
{phase === "ready" ? <RotateCw size={12} /> : <Download size={12} />}
{label}
</button>
);
}
7 changes: 5 additions & 2 deletions src/components/layout/RootLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { SettingsPanel } from "@/components/panels/SettingsPanel";
import { CommandPalette } from "@/components/CommandPalette";
import { ShortcutHelp } from "@/components/ShortcutHelp";
import { Toast } from "@/components/Toast";
import { UpdateBanner } from "@/components/UpdateBanner";
import { useUpdatesStore } from "@/stores/updates";
import { ErrorBoundary } from "@/components/ErrorBoundary";
import { ViewportSurface } from "@/components/panels/ViewportPanel";
import { useUIStore, useViewportMode } from "@/stores/ui";
Expand All @@ -35,6 +35,10 @@ type BridgeType = ReturnType<typeof useWebviewBridge>;
const IS_TAURI = typeof window !== "undefined" && "__TAURI_INTERNALS__" in window;

export function RootLayout() {
// One update check per launch; the result shows as a toolbar pill.
useEffect(() => {
void useUpdatesStore.getState().check();
}, []);
const settingsPanelOpen = useUIStore((s) => s.settingsPanelOpen);
const commandPaletteOpen = useUIStore((s) => s.commandPaletteOpen);
const shortcutHelpOpen = useUIStore((s) => s.shortcutHelpOpen);
Expand Down Expand Up @@ -155,7 +159,6 @@ export function RootLayout() {
{commandPaletteOpen && <CommandPalette />}
{shortcutHelpOpen && <ShortcutHelp />}
<Toast />
<UpdateBanner />
</div>
</ErrorBoundary>
);
Expand Down
30 changes: 30 additions & 0 deletions src/components/panels/SettingsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { X } from "lucide-react";
import { useSettingsStore } from "@/stores/settings";
import { useUIStore } from "@/stores/ui";
import { SEARCH_ENGINES, type SearchEngineId } from "@/lib/url";
import { useUpdatesStore } from "@/stores/updates";

function SectionHeader({ children }: { children: React.ReactNode }) {
return (
Expand Down Expand Up @@ -298,7 +299,36 @@ export function SettingsPanel() {
<div className="text-xs text-[var(--color-text-disabled)] mt-6 space-y-1">
<div>ZYNLEX Browser{version && ` v${version}`}</div>
<div>Open source · Zero telemetry · Zero accounts</div>
<UpdateCheck />
</div>
</div>
);
}

function UpdateCheck() {
const { update, phase, lastCheck, check } = useUpdatesStore();
const status =
phase === "checking"
? "Checking…"
: update
? `Version ${update.version} is available — use the Update button in the toolbar.`
: lastCheck === "up-to-date"
? "You're on the latest version."
: lastCheck === "error"
? "Couldn't reach GitHub to check for updates."
: "";
return (
<div className="pt-1 space-y-1">
<button
type="button"
onClick={() => void check()}
disabled={phase === "checking" || phase === "downloading"}
className="px-2 py-1 rounded border whitespace-nowrap text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] disabled:opacity-50"
style={{ background: "var(--color-elevated)", borderColor: "var(--color-border)" }}
>
Check for updates
</button>
{status && <div>{status}</div>}
</div>
);
}
70 changes: 70 additions & 0 deletions src/stores/updates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* App update state, shared by the toolbar pill and Settings.
*
* The prompt used to be a bottom-right banner in the main window's DOM — which
* is drawn *under* the native tab webview, so with any page open it was
* invisible and nobody ever saw an update. Everything here surfaces in the
* toolbar (never covered) or in Settings (which hides the webview while open).
*
* Checks on launch and when asked from Settings. Nothing is downloaded until
* the user clicks.
*/
import { create } from "zustand";
import { check as checkForUpdate, type Update } from "@tauri-apps/plugin-updater";
import { relaunch } from "@tauri-apps/plugin-process";

type Phase = "idle" | "checking" | "downloading" | "ready" | "failed";

interface UpdatesStore {
update: Update | null;
phase: Phase;
pct: number;
/** Outcome of the last manual check, shown in Settings. */
lastCheck: "none" | "up-to-date" | "available" | "error";
check: () => Promise<void>;
install: () => Promise<void>;
restart: () => Promise<void>;
}

const IS_TAURI = typeof window !== "undefined" && "__TAURI_INTERNALS__" in window;

export const useUpdatesStore = create<UpdatesStore>()((set, get) => ({
update: null,
phase: "idle",
pct: 0,
lastCheck: "none",

check: async () => {
if (!IS_TAURI || get().phase === "checking" || get().phase === "downloading") return;
set({ phase: "checking" });
try {
const update = await checkForUpdate();
set({ update, phase: "idle", lastCheck: update ? "available" : "up-to-date" });
} catch {
// Offline is normal; Settings shows the error, the toolbar stays quiet.
set({ phase: "idle", lastCheck: "error" });
}
},

install: async () => {
const { update } = get();
if (!update) return;
set({ phase: "downloading", pct: 0 });
let total = 0;
let got = 0;
try {
await update.downloadAndInstall((e) => {
if (e.event === "Started") total = e.data.contentLength ?? 0;
else if (e.event === "Progress") {
got += e.data.chunkLength;
if (total > 0) set({ pct: Math.round((got / total) * 100) });
}
});
set({ phase: "ready" });
} catch {
set({ phase: "failed" });
}
},

restart: () => relaunch(),
}));