From 9f460fa14b36f4d2222ca38e84c8a548a37dd64e Mon Sep 17 00:00:00 2001 From: elstua Date: Mon, 23 Feb 2026 20:40:51 +0000 Subject: [PATCH 1/4] Add hover preview cards for session notes in sidebar and tab bar Reusable SessionPreviewCard component that shows title, date, participants, and text snippet on hover. Appears to the right for sidebar items and below for tabs. Uses motion/react spring-based cursor following, warm-up delay reduction for consecutive hovers, and suppresses native WebKit tooltips. --- .../components/main/body/sessions/index.tsx | 39 +-- .../src/components/main/body/shared.tsx | 2 +- .../components/main/sidebar/timeline/item.tsx | 33 ++- .../src/components/session-preview-card.tsx | 239 ++++++++++++++++++ packages/ui/src/components/ui/hover-card.tsx | 96 ++++++- 5 files changed, 365 insertions(+), 44 deletions(-) create mode 100644 apps/desktop/src/components/session-preview-card.tsx diff --git a/apps/desktop/src/components/main/body/sessions/index.tsx b/apps/desktop/src/components/main/body/sessions/index.tsx index 70d381906c..5dd189d878 100644 --- a/apps/desktop/src/components/main/body/sessions/index.tsx +++ b/apps/desktop/src/components/main/body/sessions/index.tsx @@ -19,6 +19,7 @@ import { useTitleGeneration } from "../../../../hooks/useTitleGeneration"; import * as main from "../../../../store/tinybase/store/main"; import { useSessionTitle } from "../../../../store/zustand/live-title"; import { type Tab, useTabs } from "../../../../store/zustand/tabs"; +import { SessionPreviewCard } from "../../../session-preview-card"; import { StandardTabWrapper } from "../index"; import { type TabItem, TabItemBase } from "../shared"; import { CaretPositionProvider } from "./caret-position-context"; @@ -78,24 +79,26 @@ export const TabItemNote: TabItem> = ({ }, [isActive, stop, tab, handleCloseThis]); return ( - } - title={title || "Untitled"} - selected={tab.active} - active={isActive} - accent={isActive ? "red" : "neutral"} - finalizing={showSpinner} - pinned={tab.pinned} - tabIndex={tabIndex} - showCloseConfirmation={showCloseConfirmation} - onCloseConfirmationChange={handleCloseConfirmationChange} - handleCloseThis={handleCloseWithStop} - handleSelectThis={() => handleSelectThis(tab)} - handleCloseOthers={handleCloseOthers} - handleCloseAll={handleCloseAll} - handlePinThis={() => handlePinThis(tab)} - handleUnpinThis={() => handleUnpinThis(tab)} - /> + + } + title={title || "Untitled"} + selected={tab.active} + active={isActive} + accent={isActive ? "red" : "neutral"} + finalizing={showSpinner} + pinned={tab.pinned} + tabIndex={tabIndex} + showCloseConfirmation={showCloseConfirmation} + onCloseConfirmationChange={handleCloseConfirmationChange} + handleCloseThis={handleCloseWithStop} + handleSelectThis={() => handleSelectThis(tab)} + handleCloseOthers={handleCloseOthers} + handleCloseAll={handleCloseAll} + handlePinThis={() => handlePinThis(tab)} + handleUnpinThis={() => handleUnpinThis(tab)} + /> + ); }; diff --git a/apps/desktop/src/components/main/body/shared.tsx b/apps/desktop/src/components/main/body/shared.tsx index 90ca20eafb..75a5cae9b6 100644 --- a/apps/desktop/src/components/main/body/shared.tsx +++ b/apps/desktop/src/components/main/body/shared.tsx @@ -285,7 +285,7 @@ export function TabItemBase({ )} - {title} + {title} {showShortcut && (
diff --git a/apps/desktop/src/components/main/sidebar/timeline/item.tsx b/apps/desktop/src/components/main/sidebar/timeline/item.tsx index 121da8814f..06759e06f6 100644 --- a/apps/desktop/src/components/main/sidebar/timeline/item.tsx +++ b/apps/desktop/src/components/main/sidebar/timeline/item.tsx @@ -32,6 +32,7 @@ import { TimelinePrecision, } from "../../../../utils/timeline"; import { InteractiveButton } from "../../../interactive-button"; +import { SessionPreviewCard } from "../../../session-preview-card"; export const TimelineItemComponent = memo( ({ @@ -124,7 +125,7 @@ function ItemBase({
@@ -425,18 +426,24 @@ const SessionItem = memo( ); return ( - + + + ); }, ); diff --git a/apps/desktop/src/components/session-preview-card.tsx b/apps/desktop/src/components/session-preview-card.tsx new file mode 100644 index 0000000000..6a548bf158 --- /dev/null +++ b/apps/desktop/src/components/session-preview-card.tsx @@ -0,0 +1,239 @@ +import { useMotionValue, useSpring, useTransform } from "motion/react"; +import { useCallback, useMemo, useRef, useState } from "react"; + +import { + HoverCard, + HoverCardContent, + HoverCardTrigger, +} from "@hypr/ui/components/ui/hover-card"; +import { cn, format, safeParseDate } from "@hypr/utils"; + +import { extractPlainText } from "../contexts/search/engine/utils"; +import * as main from "../store/tinybase/store/main"; + +const MAX_PREVIEW_LENGTH = 200; +const FOLLOW_RANGE = 16; +const SPRING_CONFIG = { stiffness: 300, damping: 30, mass: 0.5 }; + +const OPEN_DELAY_COLD = 400; +const OPEN_DELAY_WARM = 0; +const WARMUP_COOLDOWN_MS = 600; + +let lastPreviewClosedAt = 0; + +function isWarmedUp() { + return Date.now() - lastPreviewClosedAt < WARMUP_COOLDOWN_MS; +} + +function markPreviewClosed() { + lastPreviewClosedAt = Date.now(); +} + +function useSessionPreviewData(sessionId: string) { + const title = + (main.UI.useCell("sessions", sessionId, "title", main.STORE_ID) as + | string + | undefined) || ""; + const rawMd = main.UI.useCell( + "sessions", + sessionId, + "raw_md", + main.STORE_ID, + ) as string | undefined; + const createdAt = main.UI.useCell( + "sessions", + sessionId, + "created_at", + main.STORE_ID, + ) as string | undefined; + const eventJson = main.UI.useCell( + "sessions", + sessionId, + "event_json", + main.STORE_ID, + ) as string | undefined; + + const participantMappingIds = main.UI.useSliceRowIds( + main.INDEXES.sessionParticipantsBySession, + sessionId, + main.STORE_ID, + ); + + const previewText = useMemo(() => { + const text = extractPlainText(rawMd); + if (!text) return ""; + return text.length > MAX_PREVIEW_LENGTH + ? text.slice(0, MAX_PREVIEW_LENGTH) + "…" + : text; + }, [rawMd]); + + const dateDisplay = useMemo(() => { + let timestamp = createdAt; + if (eventJson) { + try { + const event = JSON.parse(eventJson); + if (event?.started_at) timestamp = event.started_at; + } catch {} + } + const parsed = safeParseDate(timestamp); + if (!parsed) return ""; + return format(parsed, "MMM d, yyyy · h:mm a"); + }, [createdAt, eventJson]); + + return { title, previewText, dateDisplay, participantMappingIds }; +} + +function useCursorFollow(axis: "x" | "y") { + const triggerRef = useRef(null); + const normalized = useMotionValue(0.5); + + const offset = useSpring( + useTransform(normalized, [0, 1], [-FOLLOW_RANGE, FOLLOW_RANGE]), + SPRING_CONFIG, + ); + + const handleMouseMove = useCallback( + (e: React.MouseEvent) => { + const el = triggerRef.current; + if (!el) return; + const rect = el.getBoundingClientRect(); + const ratio = + axis === "y" + ? (e.clientY - rect.top) / rect.height + : (e.clientX - rect.left) / rect.width; + normalized.set(Math.max(0, Math.min(1, ratio))); + }, + [axis, normalized], + ); + + const handleMouseLeave = useCallback(() => { + normalized.set(0.5); + }, [normalized]); + + const style = axis === "y" ? { translateY: offset } : { translateX: offset }; + + return { triggerRef, handleMouseMove, handleMouseLeave, style }; +} + +function useParticipantNames(mappingIds: string[]) { + const allResults = main.UI.useResultTable( + main.QUERIES.sessionParticipantsWithDetails, + main.STORE_ID, + ); + + return useMemo(() => { + const names: string[] = []; + for (const id of mappingIds) { + const row = allResults[id]; + if (!row) continue; + const name = (row.human_name as string) || "Unknown"; + names.push(name); + } + return names; + }, [mappingIds, allResults]); +} + +const MAX_VISIBLE_PARTICIPANTS = 3; + +function ParticipantsList({ mappingIds }: { mappingIds: string[] }) { + const names = useParticipantNames(mappingIds); + + if (names.length === 0) return null; + + const visible = names.slice(0, MAX_VISIBLE_PARTICIPANTS); + const remaining = names.length - visible.length; + + return ( +
+ {visible.join(", ")} + {remaining > 0 && ( + and {remaining} more + )} +
+ ); +} + +export function SessionPreviewCard({ + sessionId, + side, + children, + enabled = true, +}: { + sessionId: string; + side: "right" | "bottom"; + children: React.ReactNode; + enabled?: boolean; +}) { + const { title, previewText, dateDisplay, participantMappingIds } = + useSessionPreviewData(sessionId); + + const followAxis = side === "right" ? "y" : "x"; + const { triggerRef, handleMouseMove, handleMouseLeave, style } = + useCursorFollow(followAxis); + + const [openDelay, setOpenDelay] = useState( + isWarmedUp() ? OPEN_DELAY_WARM : OPEN_DELAY_COLD, + ); + + const handleOpenChange = useCallback((open: boolean) => { + if (open) { + markPreviewClosed(); + } else { + markPreviewClosed(); + setOpenDelay(OPEN_DELAY_WARM); + } + }, []); + + const handleMouseEnter = useCallback(() => { + setOpenDelay(isWarmedUp() ? OPEN_DELAY_WARM : OPEN_DELAY_COLD); + }, []); + + const hasContent = title || previewText; + + if (!enabled || !hasContent) { + return <>{children}; + } + + return ( + + +
+ {children} +
+
+ +
+
{title || "Untitled"}
+ + {dateDisplay && ( +
{dateDisplay}
+ )} + + + + {previewText && ( +
+ {previewText} +
+ )} +
+
+
+ ); +} diff --git a/packages/ui/src/components/ui/hover-card.tsx b/packages/ui/src/components/ui/hover-card.tsx index d56855e941..47bf69f370 100644 --- a/packages/ui/src/components/ui/hover-card.tsx +++ b/packages/ui/src/components/ui/hover-card.tsx @@ -1,4 +1,5 @@ import * as HoverCardPrimitive from "@radix-ui/react-hover-card"; +import { motion, type MotionStyle } from "motion/react"; import * as React from "react"; import { cn } from "@hypr/utils"; @@ -8,19 +9,90 @@ const HoverCardTrigger = HoverCardPrimitive.Trigger; const HoverCardContent = React.forwardRef< React.ComponentRef, - React.ComponentPropsWithoutRef ->(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( - & { + followStyle?: MotionStyle; + } +>( + ( + { className, - ])} - {...props} - /> -)); + align = "center", + sideOffset = 4, + side = "bottom", + followStyle, + ...props + }, + ref, + ) => { + const getInitialPosition = () => { + switch (side) { + case "top": + return { y: 6 }; + case "bottom": + return { y: -6 }; + case "left": + return { x: 6 }; + case "right": + return { x: -6 }; + default: + return { y: -6 }; + } + }; + + const initialPosition = getInitialPosition(); + + return ( + + + + {props.children} + + + + ); + }, +); HoverCardContent.displayName = HoverCardPrimitive.Content.displayName; export { HoverCard, HoverCardContent, HoverCardTrigger }; From 84f8abdfde29bbf737bd3931dbc32f941de144a0 Mon Sep 17 00:00:00 2001 From: elstua Date: Tue, 24 Feb 2026 11:11:59 +0000 Subject: [PATCH 2/4] Show preview card for empty notes --- .../src/components/session-preview-card.tsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/apps/desktop/src/components/session-preview-card.tsx b/apps/desktop/src/components/session-preview-card.tsx index 6a548bf158..c991b06cd0 100644 --- a/apps/desktop/src/components/session-preview-card.tsx +++ b/apps/desktop/src/components/session-preview-card.tsx @@ -188,9 +188,7 @@ export function SessionPreviewCard({ setOpenDelay(isWarmedUp() ? OPEN_DELAY_WARM : OPEN_DELAY_COLD); }, []); - const hasContent = title || previewText; - - if (!enabled || !hasContent) { + if (!enabled) { return <>{children}; } @@ -216,19 +214,18 @@ export function SessionPreviewCard({ align="start" sideOffset={8} followStyle={style} - className={cn(["w-72 p-3", "pointer-events-none"])} + className={cn(["w-72 p-4", "pointer-events-none"])} > -
-
{title || "Untitled"}
- +
{dateDisplay && ( -
{dateDisplay}
+
{dateDisplay}
)} +
{title || "Untitled"}
{previewText && ( -
+
{previewText}
)} From 4b688bb8ad2b003b13aafbedea6d4e37b1be86c5 Mon Sep 17 00:00:00 2001 From: elstua Date: Tue, 24 Feb 2026 11:15:54 +0000 Subject: [PATCH 3/4] small adjustments for UI --- apps/desktop/src/components/session-preview-card.tsx | 2 +- packages/ui/src/components/ui/hover-card.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/components/session-preview-card.tsx b/apps/desktop/src/components/session-preview-card.tsx index c991b06cd0..2861c4bfa2 100644 --- a/apps/desktop/src/components/session-preview-card.tsx +++ b/apps/desktop/src/components/session-preview-card.tsx @@ -147,7 +147,7 @@ function ParticipantsList({ mappingIds }: { mappingIds: string[] }) {
{visible.join(", ")} {remaining > 0 && ( - and {remaining} more + and {remaining} more )}
); diff --git a/packages/ui/src/components/ui/hover-card.tsx b/packages/ui/src/components/ui/hover-card.tsx index 47bf69f370..f59bd94969 100644 --- a/packages/ui/src/components/ui/hover-card.tsx +++ b/packages/ui/src/components/ui/hover-card.tsx @@ -81,7 +81,7 @@ const HoverCardContent = React.forwardRef< }} style={followStyle} className={cn([ - "z-50 w-64 rounded-xl border bg-popover p-4 text-popover-foreground shadow-md outline-hidden", + "z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden", "origin-(--radix-hover-card-content-transform-origin)", className, ])} From 6f700defb1e03e0b6eeca33c48fe0429b4351af6 Mon Sep 17 00:00:00 2001 From: elstua Date: Wed, 25 Feb 2026 22:31:12 +0000 Subject: [PATCH 4/4] added update and recording icons --- Cargo.lock | 1 + apps/web/WEBSITE_PAGES_AUDIT.md | 542 ++++++++++++++++++++++++ plugins/listener/src/runtime.rs | 2 + plugins/tray/Cargo.toml | 1 + plugins/tray/icons/tray_default.png | Bin 3042 -> 1736 bytes plugins/tray/icons/tray_recording.png | Bin 4250 -> 0 bytes plugins/tray/icons/tray_recording_0.png | Bin 0 -> 2786 bytes plugins/tray/icons/tray_recording_1.png | Bin 0 -> 2797 bytes plugins/tray/icons/tray_recording_2.png | Bin 0 -> 2632 bytes plugins/tray/icons/tray_recording_3.png | Bin 0 -> 2800 bytes plugins/tray/icons/tray_update.png | Bin 0 -> 2481 bytes plugins/tray/src/ext.rs | 70 ++- plugins/tray/src/lib.rs | 5 + 13 files changed, 620 insertions(+), 1 deletion(-) create mode 100644 apps/web/WEBSITE_PAGES_AUDIT.md delete mode 100644 plugins/tray/icons/tray_recording.png create mode 100644 plugins/tray/icons/tray_recording_0.png create mode 100644 plugins/tray/icons/tray_recording_1.png create mode 100644 plugins/tray/icons/tray_recording_2.png create mode 100644 plugins/tray/icons/tray_recording_3.png create mode 100644 plugins/tray/icons/tray_update.png diff --git a/Cargo.lock b/Cargo.lock index 44d81d7138..2bc3c254d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18489,6 +18489,7 @@ dependencies = [ "tauri-plugin-updater2", "tauri-plugin-windows", "tauri-specta", + "tokio", "tracing", ] diff --git a/apps/web/WEBSITE_PAGES_AUDIT.md b/apps/web/WEBSITE_PAGES_AUDIT.md new file mode 100644 index 0000000000..7db27475ad --- /dev/null +++ b/apps/web/WEBSITE_PAGES_AUDIT.md @@ -0,0 +1,542 @@ +# Website Pages Audit + +## List 1: All Pages by Hierarchy Level + +### Level 1 -- Main Navigation (Header Menu) + +These pages are directly accessible from the header navigation on every page. + +| URL | Page Name | +|-----|-----------| +| `/` | Home | +| `/why-hyprnote/` | Why Hyprnote | +| `/pricing/` | Pricing | +| `/blog/` | Blog (index) | +| `/changelog/` | Changelog (index) | +| `/roadmap/` | Roadmap (index) | +| `/company-handbook/` | Company Handbook (redirects to first page) | +| `/opensource/` | Open Source | +| `/enterprise/` | Enterprise | +| `/product/ai-notetaking` | AI Notetaking | +| `/product/search` | Searchable Notes | +| `/product/markdown` | Markdown Files | +| `/product/flexible-ai` | Flexible AI | +| `/product/api` | API | +| `/solution/knowledge-workers` | For Knowledge Workers | +| `/gallery/templates` | Gallery - Templates view | + +### Level 1 -- Footer Navigation + +These pages appear in the footer on every page (some overlap with header). + +| URL | Page Name | Footer Column | +|-----|-----------|---------------| +| `/download/` | Download | Product | +| `/docs/` | Docs (redirects to first page) | Product | +| `/about/` | About Us | Company | +| `/jobs/` | Jobs | Company | +| `/brand/` | Brand | Company | +| `/press-kit/` | Press Kit | Company | +| `/gallery/` | Prompt Gallery | Resources | +| `/docs/faq` | FAQ (redirects to first FAQ page) | Resources | +| `/eval/` | AI Eval | Tools | +| `/file-transcription/` | Audio Transcription | Tools | +| `/oss-friends/` | OSS Navigator | Tools | +| `/legal/terms` | Terms | Brand section | +| `/legal/privacy` | Privacy | Brand section | +| `/auth/` | Get Started (sign up) | Brand section | +| `/vs/{random}` | Random comparison page | Resources (random) | +| `/solution/{random}` | Random solution page | Resources (random) | + +### Level 2 -- Linked from Specific Pages + +These pages are NOT in the header/footer but are linked from the body of other pages. + +| URL | Linked From | +|-----|-------------| +| `/founders/` | `/pricing`, `/enterprise`, `/download` | +| `/product/extensions` | `/product/ai-assistant` | +| `/product/self-hosting` | `/free`, `/product/local-ai` | +| `/product/ai-assistant` | `/` (home) | +| `/product/ai-notetaking/` | `/` (home), `/solution/*` pages, `/product/notepad` | +| `/templates/` | `/` (home), `/gallery`, `/vs/*`, `/integrations/*` | +| `/shortcuts/` | `/gallery` | +| `/privacy/` | `/security` | +| `/press-kit/app/` | `/press-kit` | +| `/free/` | *(needs verification -- may be linked from pricing or other)* | +| `/security/` | *(needs verification)* | +| `/download/apple-silicon` | `` component (platform-dependent) | +| `/download/windows` | `` component (platform-dependent) | +| `/download/linux-deb` | *(from download index page)* | +| `/download/linux-appimage` | *(from download index page)* | +| `/download/apple-intel` | *(from download index page)* | + +### Level 2 -- Content-Driven Pages (from list/index pages) + +These are individual content pages accessible from their parent list page. + +| URL Pattern | Parent Page | Count | +|-------------|-------------|-------| +| `/blog/{slug}` | `/blog` | 58 articles | +| `/changelog/{version}` | `/changelog` | 68 versions | +| `/docs/{section}/{page}` | `/docs` sidebar | ~45 pages | +| `/company-handbook/{section}/{page}` | `/company-handbook` sidebar | ~56 pages | +| `/vs/{slug}` | Footer (random), blog cross-links | 23 comparisons | +| `/integrations/{category}/{slug}` | *(no index page found)* | 12 pages | +| `/templates/{slug}` | `/templates`, `/gallery` | 17 templates | +| `/shortcuts/{slug}` | `/shortcuts`, `/gallery` | 6 shortcuts | +| `/roadmap/{slug}` | `/roadmap` | 12 items | +| `/legal/{slug}` | `/legal` | 4 documents | +| `/jobs/{slug}` | `/jobs` | 2 listings | +| `/gallery/{type}/{slug}` | `/gallery` | *(dynamic, from templates+shortcuts)* | +| `/k6-reports/{id}` | `/k6-reports` | *(dynamic, internal)* | + +### Level 3 -- Pages with No Incoming Links (Orphan Pages) + +These pages exist as routes but are NOT linked from the header, footer, or any other page body. + +| URL | Description | +|-----|-------------| +| `/product/bot` | Coming Soon page -- not linked from anywhere | +| `/product/memory` | Coming Soon page -- not linked from anywhere | +| `/product/notepad` | Has links TO other pages, but no pages link TO it | +| `/product/integrations` | Not linked from nav or other pages | +| `/product/local-ai` | Not linked from nav or other pages | +| `/product/mini-apps` | Not linked from nav or other pages | +| `/product/ai-assistant` | Only linked from home page (not nav) | +| `/solution/coaching` | Only reachable from footer random link | +| `/solution/consulting` | Only reachable from footer random link | +| `/solution/customer-success` | Not linked from anywhere | +| `/solution/engineering` | Not linked from anywhere | +| `/solution/field-engineering` | Not linked from anywhere | +| `/solution/government` | Not linked from anywhere | +| `/solution/healthcare` | Not linked from anywhere | +| `/solution/journalism` | Only reachable from footer random link | +| `/solution/legal` | Not linked from anywhere | +| `/solution/media` | Not linked from anywhere | +| `/solution/meeting` | Not linked from anywhere | +| `/solution/project-management` | Not linked from anywhere | +| `/solution/recruiting` | Only reachable from footer random link | +| `/solution/research` | Only reachable from footer random link | +| `/solution/sales` | Only reachable from footer random link | +| `/bounties/` | Redirect to GitHub -- no incoming links | +| `/contact/` | Redirect to mailto -- no incoming links | +| `/k6-reports/` | Internal tool -- no incoming links | + +### Utility / Redirect Pages (not real content pages) + +| URL | Purpose | +|-----|---------| +| `/bluesky` | Redirect to Bluesky profile | +| `/discord` | Redirect to Discord server | +| `/github` | Redirect to GitHub repo | +| `/linkedin` | Redirect to LinkedIn page | +| `/reddit` | Redirect to Reddit | +| `/x` | Redirect to Twitter/X | +| `/youtube` | Redirect to YouTube | +| `/bounties` | Redirect to GitHub | +| `/contact` | Redirect to email | +| `/founders` | Redirect to cal.com | +| `/callback/auth` | Auth callback handler | +| `/callback/signout` | Sign-out callback handler | +| `/reset-password` | Password reset form | +| `/update-password` | Password update form | + +### Authenticated Pages (require login) + +| URL | Purpose | +|-----|---------| +| `/app` | User dashboard | +| `/app/account` | Account settings | +| `/app/checkout` | Checkout flow | +| `/app/file-transcription` | File transcription (authenticated) | +| `/app/integration` | Integration management | + +### Admin Pages (require admin auth) + +| URL | Purpose | +|-----|---------| +| `/admin` | Admin dashboard | +| `/admin/collections` | Content management | +| `/admin/media` | Media management | +| `/admin/stars` | GitHub stars tracking | +| `/admin/crm` | CRM | +| `/admin/lead-finder` | Lead finder | +| `/admin/kanban` | Kanban board | + +--- + +## List 2: Pages with Very Little or No Content + +### No Content / Pure Redirects + +These pages render nothing -- they immediately redirect. + +| URL | What it does | +|-----|-------------| +| `/docs/` | Redirects to `/docs/about/hello-world` (11 lines) | +| `/company-handbook/` | Redirects to first handbook page (11 lines) | +| `/download/apple-silicon` | Redirects to external download URL (10 lines) | +| `/download/apple-intel` | Redirects to external download URL (10 lines) | +| `/download/windows` | Redirects to external download URL (11 lines) | +| `/download/linux-deb` | Redirects to external download URL (10 lines) | +| `/download/linux-appimage` | Redirects to external download URL (10 lines) | +| `/bounties` | Redirects to GitHub (external) | +| `/contact` | Redirects to mailto link | +| `/founders` | Redirects to cal.com | + +### Minimal Content (1 screen or a couple sentences) + +These pages have very little actual content -- typically just a title, one sentence, and a button. + +| URL | Lines | What's there | +|-----|-------|-------------| +| `/product/memory` | 53 | Title + "Coming Soon" badge + 1 sentence | +| `/product/flexible-ai` | 54 | Title + 1 sentence + "Download" button | +| `/product/markdown` | 54 | Title + 1 sentence + "Download" button | +| `/product/extensions` | 81 | "Coming Soon" + list of extension tags | +| `/product/api` | 93 | "Coming Soon" + mock terminal animation | +| `/legal/` (index) | 99 | Simple list of legal document links | +| `/k6-reports/` | 100 | Internal tool -- data table | +| `/product/integrations` | 124 | Hero section + grid of integration logos | +| `/jobs/` | 145 | Job listing cards (depends on MDX content -- currently 2 jobs) | +| `/product/bot` | 187 | "Coming Soon" + draggable meeting bot icons | +| `/product/search` | 200 | Hero + 1 feature section with mock search UI | +| `/changelog/` | 206 | List page -- content depends on MDX entries | + +### Product Pages Summary + +| URL | Content Level | Notes | +|-----|--------------|-------| +| `/product/ai-notetaking` | Full (2476 lines) | Interactive demos, multiple sections | +| `/product/mini-apps` | Full (638 lines) | Multiple sections with examples | +| `/product/self-hosting` | Full (524 lines) | Feature sections, comparisons | +| `/product/local-ai` | Full (479 lines) | Multiple feature sections | +| `/product/ai-assistant` | Full (465 lines) | Feature sections with examples | +| `/product/notepad` | Moderate (278 lines) | Mock window demo + features | +| `/product/search` | Moderate (200 lines) | Hero + mock search UI | +| `/product/bot` | Minimal (187 lines) | Coming Soon placeholder | +| `/product/integrations` | Minimal (124 lines) | Logo grid, no real content | +| `/product/api` | Minimal (93 lines) | Coming Soon placeholder | +| `/product/extensions` | Minimal (81 lines) | Coming Soon placeholder | +| `/product/flexible-ai` | Minimal (54 lines) | 1 sentence + button | +| `/product/markdown` | Minimal (54 lines) | 1 sentence + button | +| `/product/memory` | Minimal (53 lines) | 1 sentence + Coming Soon | + +### Solution Pages Summary + +All solution pages have `noindex, nofollow` meta tags. + +| URL | Content Level | Notes | +|-----|--------------|-------| +| `/solution/engineering` | Full (574 lines) | Unique layout with multiple sections | +| `/solution/meeting` | Moderate (378 lines) | Unique layout | +| `/solution/coaching` | Template (246 lines) | Hero + 6 cards + table + CTA | +| `/solution/consulting` | Template (246 lines) | Same template | +| `/solution/customer-success` | Template (~246 lines) | Same template | +| `/solution/field-engineering` | Template (~246 lines) | Same template | +| `/solution/government` | Template (~246 lines) | Same template | +| `/solution/healthcare` | Template (~246 lines) | Same template | +| `/solution/journalism` | Template (~246 lines) | Same template | +| `/solution/knowledge-workers` | Template (~246 lines) | Same template | +| `/solution/legal` | Template (~246 lines) | Same template | +| `/solution/media` | Template (~246 lines) | Same template | +| `/solution/project-management` | Template (~246 lines) | Same template | +| `/solution/recruiting` | Template (~246 lines) | Same template | +| `/solution/research` | Template (~246 lines) | Same template | +| `/solution/sales` | Template (~246 lines) | Same template | + +> Note: The 13 "template" solution pages all follow an identical structure with industry-specific copy swapped in. They have content, but it's formulaic (hero, 6 feature cards, comparison table, use cases, CTA). + +### Notes + +- All product and solution pages have `noindex, nofollow` robots meta tags +- Several handbook MDX files have duplicate slugs which could cause build issues +- The `/integrations/*` pages have no index/list page -- they're only reachable via direct URL or search +- `/oss-friends` renders 85 entries from MDX but has no individual detail pages + +--- + +## SEO & Performance Suggestions + +Based on the full codebase analysis (TanStack Start on Vite 7, deployed to Netlify, content via content-collections/MDX). + +### Critical Priority (High SEO Impact) + +#### 1. Fix the Domain Migration Leftovers (`hyprnote.com` → `char.com`) + +Several places in the code still reference the old domain `hyprnote.com`. Search engines see these as signals about where the "real" site lives, so they need to point to the current domain. + +| File | What's Wrong | +|------|-------------| +| `src/routes/__root.tsx` line 34 | `ai-sitemap` meta tag points to `https://hyprnote.com/llms.txt` | +| `src/routes/__root.tsx` line 39 | `og:url` is `https://hyprnote.com` | +| `src/routes/__root.tsx` line 51 | `twitter:url` is `https://hyprnote.com` | +| `src/routes/_view/blog/$slug.tsx` line 54 | Blog canonical URLs use `https://hyprnote.com/blog/...` | +| `src/routes/_view/blog/$slug.tsx` line 60 | Blog OG image fallback URL uses `https://hyprnote.com/og?...` | +| `public/llms.txt` | References `hyprnote.com` throughout | + +**What to do:** Find-and-replace `https://hyprnote.com` → `https://char.com` in all source files listed above. The Netlify 301 redirects handle visitors, but meta tags and canonical URLs should point to the canonical domain directly. + +#### 2. Add Canonical Tags to All Public Pages + +A "canonical tag" tells Google "this is the one true URL for this content" — it prevents duplicate-content issues (e.g. if someone links to your page with `?utm_source=...` query parameters, Google still knows which URL to rank). + +Currently only `/blog/$slug` pages have canonical tags. Every indexable public page should have one. + +**What to do:** Add a `` to the `head()` of every public route file. Consider creating a shared helper: + +```typescript +function canonicalUrl(path: string) { + return { tag: "link", attrs: { rel: "canonical", href: `https://char.com${path}` } }; +} +``` + +#### 3. Re-evaluate the Aggressive `noindex` Strategy + +Right now **44+ pages** are marked `noindex, nofollow` — meaning Google is told to completely ignore them. This includes pages that could bring organic traffic: + +| Pages | Status | Recommendation | +|-------|--------|----------------| +| `/product/ai-notetaking` (2476 lines, rich content) | noindex | **Should be indexed** — this is a flagship feature page | +| `/product/self-hosting` (524 lines) | noindex | **Should be indexed** — self-hosting is a differentiator | +| `/product/local-ai` (479 lines) | noindex | **Should be indexed** — privacy/local AI is a key selling point | +| `/product/ai-assistant` (465 lines) | noindex | **Should be indexed** | +| `/product/mini-apps` (638 lines) | noindex | **Should be indexed** | +| `/vs/*` (23 comparison pages) | noindex | **Should be indexed** — comparison pages are high-intent SEO gold | +| `/solution/engineering`, `/solution/meeting` | noindex | Consider indexing the ones with unique content | +| `/product/bot`, `/product/memory`, `/product/api`, `/product/extensions` | noindex | OK to keep noindex — these are "Coming Soon" stubs | +| Template solution pages (13 pages, same layout) | noindex | OK to keep noindex until content is differentiated | + +**What to do:** Remove `noindex, nofollow` from content-rich product pages and all `/vs/*` pages. Update `robots.txt` accordingly (remove the `Disallow: /product/` and `Disallow: /vs/` lines, or make them more specific). Add these routes to the sitemap. + +**Contradiction to fix:** `/solution/*` and `/vs/*` are currently prerendered (SSG) but also noindexed and blocked in robots.txt. You're spending build time generating HTML that Google is told to ignore. + +#### 4. Add Structured Data (JSON-LD / Schema.org) + +Structured data is invisible markup that helps Google understand *what* your content is (a product, an article, an FAQ, etc.). It can unlock "rich results" in search — like star ratings, FAQ dropdowns, breadcrumbs, and article cards. + +Currently: **no structured data anywhere on the site**. + +**What to add:** + +| Schema Type | Where | Why | +|-------------|-------|-----| +| `Organization` | Root layout (`__root.tsx`) | Tells Google about Char as a company (name, logo, social links) | +| `WebSite` with `SearchAction` | Homepage | Enables sitelinks search box in Google | +| `Article` | `/blog/$slug` pages | Rich article cards in search results (author, date, image) | +| `BreadcrumbList` | `/docs/*`, `/blog/*`, `/company-handbook/*` | Breadcrumb trail in search results | +| `SoftwareApplication` | `/download` or homepage | Product info for software (name, OS, price: "Free") | +| `FAQPage` | `/docs/faq` pages | FAQ dropdowns directly in search results | +| `Product` | `/pricing` | Product name, offers, pricing tiers | + +**Example for the root layout:** + +```json +{ + "@context": "https://schema.org", + "@type": "Organization", + "name": "Char", + "url": "https://char.com", + "logo": "https://char.com/api/images/hyprnote/og-image.jpg", + "sameAs": [ + "https://github.com/nichochar/hyprnote", + "https://x.com/getcharnotes", + "https://linkedin.com/company/char" + ] +} +``` + +--- + +### High Priority (Performance Impact) + +#### 5. Fix Font Loading (Currently Render-Blocking) + +Fonts are one of the biggest performance issues found. Two problems: + +**Problem A:** Google Fonts loaded via CSS `@import` in `styles.css` line 1. The `@import` method is "render-blocking" — the browser must download the CSS file from Google's server before it can show any text. This delays the First Contentful Paint (FCP). + +**What to do:** Replace the `@import` with `` and `` tags in the `` of `__root.tsx`. This lets the browser start fetching fonts earlier, in parallel with other resources: + +```html + + + +``` + +**Problem B:** The 7 self-hosted `@font-face` declarations (Redaction, SF Pro) in `styles.css` are missing `font-display: swap`. Without it, browsers may show invisible text while fonts load. + +**What to do:** Add `font-display: swap;` to every `@font-face` block. This tells the browser: "show fallback text immediately, then swap in the custom font when it loads." + +#### 6. Add Netlify Custom Headers (Security + Caching) + +The `netlify.toml` has no `[[headers]]` blocks. This means: +- No security headers (browsers don't know your security preferences) +- No cache-control hints for static assets (fonts, images, JS get default short caching) + +**What to add to `netlify.toml`:** + +```toml +[[headers]] + for = "/*" + [headers.values] + X-Frame-Options = "SAMEORIGIN" + X-Content-Type-Options = "nosniff" + Referrer-Policy = "strict-origin-when-cross-origin" + Permissions-Policy = "camera=(), microphone=(), geolocation=()" + +[[headers]] + for = "/fonts/*" + [headers.values] + Cache-Control = "public, max-age=31536000, immutable" + +[[headers]] + for = "/icons/*" + [headers.values] + Cache-Control = "public, max-age=31536000, immutable" + +[[headers]] + for = "/*.js" + [headers.values] + Cache-Control = "public, max-age=31536000, immutable" + +[[headers]] + for = "/*.css" + [headers.values] + Cache-Control = "public, max-age=31536000, immutable" +``` + +The `immutable` cache directive tells the browser: "this file will never change at this URL, so don't bother re-checking." Vite already puts content hashes in filenames, so this is safe. + +#### 7. Fix the `manifest.json` (Still Default Boilerplate) + +The web app manifest still says `"name": "Create TanStack App Sample"` — this is the default template value. While this file mainly affects PWA behavior and "Add to Home Screen," Google also reads it. + +**What to do:** + +```json +{ + "short_name": "Char", + "name": "Char - AI Notepad", + "icons": [ + { "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" } + ], + "start_url": "/", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} +``` + +Also: the manifest references `logo192.png` and `logo512.png` which don't exist in `public/`. Either add them or remove the references to avoid 404 errors. + +--- + +### Medium Priority (SEO Improvements) + +#### 8. Expand the Sitemap + +The sitemap currently includes ~18 static routes and dynamic blog/docs/changelog/gallery content — but it's missing several indexable pages: + +| Missing from Sitemap | Should Be Added | +|----------------------|----------------| +| `/why-hyprnote` | Yes | +| `/jobs/` and `/jobs/$slug` | Yes | +| `/templates/` and `/templates/$slug` | Yes | +| `/shortcuts/` and `/shortcuts/$slug` | Yes | +| `/product/*` (content-rich ones) | Yes, once noindex is removed | +| `/vs/$slug` | Yes, once noindex is removed | +| `/integrations/$category/$slug` | Yes, once noindex is removed | + +#### 9. Create Missing Index/List Pages + +Several content types have individual pages but no list/index page to browse them: + +| Content Type | Has Index? | Suggestion | +|--------------|-----------|------------| +| `/integrations/*` | No | Create `/integrations` index page — lists all 12 integration pages | +| `/templates/*` | Redirects to `/gallery` | Consider a dedicated `/templates` list page for SEO | +| `/shortcuts/*` | Redirects to `/gallery` | Consider a dedicated `/shortcuts` list page | +| `/vs/*` | No | Create `/vs` index page — "How Char compares to alternatives" | + +Index pages serve as "hub" pages for SEO — they link to all child pages in one place, which helps Google discover and rank them. + +#### 10. Add `og:url` Per-Page (Not Just Global) + +The global `og:url` in `__root.tsx` is set to the homepage for every page. When someone shares `/pricing` on social media, the OpenGraph URL still says `https://char.com` (actually still `https://hyprnote.com`). Each page should set its own `og:url` to its actual URL. + +#### 11. Expand Prerendering + +Currently only `/`, `/blog/*`, `/docs/*`, `/pricing`, `/solution/*`, and `/vs/*` are prerendered (turned into static HTML at build time). Other content-heavy pages like `/changelog/*`, `/gallery/*`, `/enterprise`, `/about`, `/download` are server-rendered on each request. + +Prerendering makes pages load faster (no server wait time) and is better for SEO (Google gets instant HTML). Consider adding to the prerender filter in `vite.config.ts`: + +```typescript +filter: ({ path }) => { + return ( + path === "/" || + path.startsWith("/blog") || + path.startsWith("/docs") || + path.startsWith("/pricing") || + path.startsWith("/solution") || + path.startsWith("/vs") || + path.startsWith("/changelog") || + path.startsWith("/gallery") || + path.startsWith("/about") || + path.startsWith("/enterprise") || + path.startsWith("/download") || + path.startsWith("/product") || + path.startsWith("/why-hyprnote") + ); +}, +``` + +--- + +### Lower Priority (Nice to Have) + +#### 12. Measure Core Web Vitals + +The `web-vitals` package is installed as a devDependency but not used anywhere in the codebase. Core Web Vitals (LCP, FID, CLS) are a Google ranking factor. + +**What to do:** Either integrate `web-vitals` to report to PostHog/Sentry, or use Netlify Analytics (which includes Web Vitals automatically). This gives real user data about how fast the site feels. + +#### 13. Add Favicon Variants and Apple Touch Icon + +Currently only `favicon.ico` exists. Modern browsers and devices expect: + +| Asset | Purpose | +|-------|---------| +| `apple-touch-icon.png` (180×180) | iOS home screen icon | +| `favicon-32x32.png` | Modern browsers tab icon | +| `favicon-16x16.png` | Smaller contexts | + +Add them to `public/` and reference them in `__root.tsx` head links. + +#### 14. Convert `.otf` Fonts to `.woff2` + +The self-hosted fonts (Redaction, SF Pro) are in `.otf` format. The `.woff2` format is ~30% smaller and specifically designed for web use. All modern browsers support it. + +**What to do:** Convert the 7 `.otf` files in `public/fonts/` to `.woff2` (using a tool like `fonttools` or an online converter), then update the `@font-face` `src` URLs in `styles.css`. + +#### 15. Fix Orphan Pages or Remove Them + +From the audit above, 25+ pages have no incoming links. Search engines can still find them via sitemap, but pages with no internal links get very little "link equity" (ranking power). + +**Options for each orphan:** +- **If the page is useful:** Add links to it from relevant pages (e.g. link `/product/integrations` from the footer or a product overview page) +- **If the page is a stub/placeholder:** Keep `noindex` and consider removing from the build entirely until real content exists +- **If the page is a duplicate:** Redirect it to the canonical version + +#### 16. Add `hreflang` If Internationalization Is Planned + +Currently no language variants exist — the site is English-only with ``. If there are plans for other languages, `hreflang` tags will be needed. No action required right now, but worth noting for future planning. + +#### 17. Reduce Third-Party Script Impact + +Two external scripts load on every public page: +- **Zendesk chat widget** (`ze-snippet`) — these are notoriously heavy (~200-400KB) +- **PostHog analytics** — relatively light but still adds to load time + +**What to do:** Consider lazy-loading Zendesk (load it only after user interaction or after a delay) instead of loading it immediately on page load. This can improve Time to Interactive significantly. diff --git a/plugins/listener/src/runtime.rs b/plugins/listener/src/runtime.rs index 6b64a3daaf..ba93cc196b 100644 --- a/plugins/listener/src/runtime.rs +++ b/plugins/listener/src/runtime.rs @@ -30,9 +30,11 @@ impl ListenerRuntime for TauriRuntime { match &event { hypr_listener_core::SessionLifecycleEvent::Active { .. } => { let _ = self.app.tray().set_start_disabled(true); + let _ = self.app.tray().set_recording(true); } hypr_listener_core::SessionLifecycleEvent::Inactive { .. } => { let _ = self.app.tray().set_start_disabled(false); + let _ = self.app.tray().set_recording(false); } hypr_listener_core::SessionLifecycleEvent::Finalizing { .. } => {} } diff --git a/plugins/tray/Cargo.toml b/plugins/tray/Cargo.toml index 206341e261..6804eb8837 100644 --- a/plugins/tray/Cargo.toml +++ b/plugins/tray/Cargo.toml @@ -27,6 +27,7 @@ tauri-plugin-windows = { workspace = true } serde_json = { workspace = true } specta = { workspace = true } +tokio = { workspace = true, features = ["time"] } tracing = { workspace = true } [target.'cfg(target_os = "macos")'.dependencies] diff --git a/plugins/tray/icons/tray_default.png b/plugins/tray/icons/tray_default.png index 42b602d640a4fc4c779152049745b212a9ea8703..e8171b7717f81a278a1cf9d70c96045c87597cc5 100644 GIT binary patch delta 1705 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 QlK=n!07*qoM6N<$f;Z%LYXATM diff --git a/plugins/tray/icons/tray_recording.png b/plugins/tray/icons/tray_recording.png deleted file mode 100644 index 05454f5f6d46f44043507eb78b2b72c130de40b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4250 zcmX9?1ymE<7pEP)fpl)8K|w&IV{CMY0?wd&(umaPIbw86f0Qs9X+&a-_!LkKQedQt zd~^xOf1dw6=e?W1cg~A*?>#T!u9-eF11|#w1qHLA0sJ1B|3e<706OyQ%*@12X7s)W z4+AJD7|AjtJ0(SaAvf7b8E{V@M$s_A|AVa1xapYcP*60dGF~{)QcwUn4BUb208edFh^ao|=?E8}~Cl-x3O&V361WM~sIMgVKQV@9iEfj{ zUV{Am=(fJTrB~XeUdX6g^t=nD<~b=&s9gf+&d0edo+_jjalA{?T{sTsVSXj?fgTxk z$4^aDGbir1%qZ&^bqqj_0>=tntbb)*GL|VU{NQxRZ6yf#!2e zhT0mvn}j_R<1PcEZ|2UizpbjH1#`<#e1)QD`La*nB`0o|OhYLlnw=XsfomXM|EoWj zTCcSi36}wz6CW=w(CF~>>`hlHFzI+TRcm+}Gw}QOZ|%s&A#6v+`QDbpNZ-Z2@rC+E zY-8V_-@G9(;6IdVYHA=m2%uqUXQV?W;`fhN$Ru?X>~4^(P4{7!dhX2b12k@lrZJlN z>d*O3g$TYw*7nV*W(W*m;&0EW#~{kNT;TKerL=ZaQxmX{GxA!wxaX6Q-|R*-01?sh z^=n$tQ~>0CG=h!9uverJ_Vsj!)P}?1_QKyoK_5zGMk154PBdRq;~|HlS`suM*C-3% zM_C#dd8(iG@S!*9KgP$$!KGc5nvE$jMiINkvXluxcpfDV+HS$~@!u#4x~BLmJwwI1 zH<4)Ko4fY6A`FW`@Vw|X3fxQ+iyn+czF}fw!n-=P_PBP@4HTLwbUg;3pe@fkEd^Oy zu9$4+|B;H@Z4KRX_wex0CPQb`r#tigQ83AIhW;RFIvvdTKIf9s;k7SjJa)!`Qe?g5 z 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 diff --git a/plugins/tray/icons/tray_recording_0.png b/plugins/tray/icons/tray_recording_0.png new file mode 100644 index 0000000000000000000000000000000000000000..afda9e2bcbf746e4684f1fbcc49d8976c897c10d GIT binary patch literal 2786 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 literal 0 HcmV?d00001 diff --git a/plugins/tray/icons/tray_recording_2.png b/plugins/tray/icons/tray_recording_2.png new file mode 100644 index 0000000000000000000000000000000000000000..7cf597765e450a5710b0091fd82586ba7d18e0ac GIT binary patch literal 2632 zcmV-O3b*x%P)005u}1^@s6i_d2*00009a7bBm000XU z000XU0RWnu7ytkO0drDELIAGL9O(c600d`2O+f$vv5yPlL*eI!7F;3<8z`VL z0vc%GT|)x`351al6nK%K0E7G*(@$@|vyEC&hXTF~_TEBPa`rh67-uZTB z&&&{ktXM9W_jCCl0gxIN-Z3thxLlEc696e;(VbmhM}&AY0gwV?y_-Bj0K{g^dN;X5 z$~ic6MGR;0<5Xh7Wzc z-sdPIVzmE`Z$qN5^5Ku> z$IrQZV#-zXyIZ)(#XQdC2$u)=JxBOUoF`zT&w7OgWkSsMcSb<|k9PSWw`mG}I?gl=#n z`9XBXw`E9bj^DA9MxtYeo58-wg}jT&WkjjPtEa80v-E~*ALi!a~OU}(*8C9 zc4IsTG@eh)@n-~ltXH@|d61<2Z2~-IjzheEldhY7geu3=dWBpDpO2Yi)b_`txOBLz zwKh7 zA(y8g$r+TAKR1O;2^eakb;rz!? zgaAvJ5-`$wg@vnv<|z@`3()*U%f+{)gkLd5Qrx?g@LjR2gi`{>Sg&Ax`ccsNk0R?% zNGVeSMq006O}(#SN<{9x;OwUan5mUxtXEh#UROUQB3l6#G$mlH^$NZATg#M)oFicN zQvw{T8n`%c%zUe2N<_{AJZg?PPYF0n<8eKDE8+Ynld}N3H0~^?1bEKxu;F_8k@fk9 z0*~TGFfhrqk1CT7Hh}=g;XA*hkLptI`+xuk62N+e>Qe9ffB**&$a;kZfp38ThcVT9 zg(_3;`+xvF31qzj>~{?a(1U>1D=Y|H0|N9Tg!Ky5rr!4f0Xi74s(}x%-whx@3xTay zSP=LQ5TKnQs~WiQO1-ZQ2(X8Ns~Y%F<-~Ir5a1Rj%gX0Rp6`)+=E6#R3Gl zB8K&LEC}>cy#OOxukbul@9k7Az=^9G_<;LA2en7QnAR&S2w16BfD>0W@IjVQ!!Dr? zYL0+YRyFY9Bf^H=PO21O!1*0EUNc8**lnRY0Y*qN-Nt*g5+FA0c2Hvk3^l*Q#_Q&Y z4Z9swCBQ`1E4*Qj*s$9{QQI_3A(8cVJWe-${(op3f8X-EhiKZmf!R<~1WaMQsrVZy zp%wxDlG0djDvp~{JT(YVhCoy_)&?q~U=0HNJ?TV66x1%jKhgHBt+ent6^>>B-bUMd zkXm?}Rs-EdqgjBGXrMLvN#uK)6rfdr_tC27tCfdH2^s|`*830Aiwf)Q&?rE;@4Jn3 zqQ-hVvfJ+Ow`cXCTI=m7b#<&)2Gsp_*oGT=&sJ%)7{*T7zJ%jI2^ zZt+T3?*@o~>7pO3cMA|8(Vr;|u-+|Tx6CvV5Z2p6?Gdn{_`rHC)Gojj_6zH^fCw0u zGq7GeHSZT3d-$cLbUQN@OU)6mp}3sn=yyQCO3ea{dH*N#I~D|-)Vg7H%;A@oDxKdk zhFT+FL-D|v-vDYAU@ZHsa%$xi5COx{60CPH)d?`-@C&SW1T{v$hQb5u9Z8J>41NFC z60CPLRS7WC@C&SW0<}fJhQb2tok(p03~9eD!Fnfy2sj>CZvd(hVBq1G1*|s=HATRN z!chH!LLsS1fT8aHV7&oB1RMqy6bei&0t~gZFXndyrWOI-LDPZtMj&SaUN*0XC}y7!Vx~fDH}f3>Fkhk4gdZeNsc$pM(0)64vsY1+5YAFEotPu-y!I;?j9soMXL<4cs35^fuQk;iPX-hrfO{~hvx?M>3bcg>FymVHygpQJ?a zca-na*8_eKYr2dlX8U^y(0mEG!S7PSZ6r?#H^VRcJuk~zepu6GBC*=vTYwv16hZ2#Hei?YAm&K>~}6B&N#5#J>O*`wiEaMSa9-1mX_ qm*(P)005u}1^@s6i_d2*00009a7bBm000XU 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 = Mutex::new(None); + pub struct Tray<'a, R: tauri::Runtime, M: tauri::Manager> { manager: &'a M, _runtime: std::marker::PhantomData R>, @@ -140,6 +157,57 @@ impl<'a, M: tauri::Manager> Tray<'a, tauri::Wry, M> { Ok(()) } + + pub fn set_recording(&self, recording: bool) -> Result<()> { + IS_RECORDING.store(recording, Ordering::SeqCst); + Self::refresh_icon(self.manager.app_handle()) + } + + pub fn set_update_available(&self, available: bool) -> Result<()> { + IS_UPDATE_AVAILABLE.store(available, Ordering::SeqCst); + Self::refresh_icon(self.manager.app_handle()) + } + + fn refresh_icon(app: &AppHandle) -> Result<()> { + { + let mut task = ANIMATION_TASK.lock().unwrap(); + if let Some(handle) = task.take() { + handle.abort(); + } + + if IS_RECORDING.load(Ordering::SeqCst) { + let app = app.clone(); + *task = Some(tauri::async_runtime::spawn(async move { + let mut interval = tokio::time::interval(std::time::Duration::from_millis(150)); + let mut frame = 0usize; + loop { + interval.tick().await; + if let Some(tray) = app.tray_by_id(TRAY_ID) { + if let Ok(image) = Image::from_bytes(RECORDING_FRAMES[frame]) { + let _ = tray.set_icon(Some(image)); + } + } + frame = (frame + 1) % RECORDING_FRAMES.len(); + } + })); + return Ok(()); + } + } + + let Some(tray) = app.tray_by_id(TRAY_ID) else { + return Ok(()); + }; + + let icon_bytes = if IS_UPDATE_AVAILABLE.load(Ordering::SeqCst) { + include_bytes!("../icons/tray_update.png").as_ref() + } else { + include_bytes!("../icons/tray_default.png").as_ref() + }; + + tray.set_icon(Some(Image::from_bytes(icon_bytes)?))?; + + Ok(()) + } } pub trait TrayPluginExt { diff --git a/plugins/tray/src/lib.rs b/plugins/tray/src/lib.rs index fd3d0d7292..1951ec56bd 100644 --- a/plugins/tray/src/lib.rs +++ b/plugins/tray/src/lib.rs @@ -19,11 +19,13 @@ pub fn init() -> tauri::plugin::TauriPlugin { } fn setup_update_listeners(app: &tauri::AppHandle) { + use ext::TrayPluginExt; use tauri_specta::Event; let handle = app.clone(); tauri_plugin_updater2::UpdateDownloadingEvent::listen(app, move |_event| { let _ = menu_items::TrayCheckUpdate::set_state(&handle, UpdateMenuState::Downloading); + let _ = handle.tray().set_update_available(true); }); let handle = app.clone(); @@ -32,16 +34,19 @@ fn setup_update_listeners(app: &tauri::AppHandle) { &handle, UpdateMenuState::RestartToApply(event.payload.version.clone()), ); + let _ = handle.tray().set_update_available(true); }); let handle = app.clone(); tauri_plugin_updater2::UpdateDownloadFailedEvent::listen(app, move |_event| { let _ = menu_items::TrayCheckUpdate::set_state(&handle, UpdateMenuState::CheckForUpdate); + let _ = handle.tray().set_update_available(false); }); let handle = app.clone(); tauri_plugin_updater2::UpdatedEvent::listen(app, move |_event| { let _ = menu_items::TrayCheckUpdate::set_state(&handle, UpdateMenuState::CheckForUpdate); + let _ = handle.tray().set_update_available(false); }); }