-
Notifications
You must be signed in to change notification settings - Fork 6
feat: Complete system integration with navigation and profile management #76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| import { protocol } from "electron"; | ||
| import { existsSync } from "fs"; | ||
| import { readFile } from "fs/promises"; | ||
| import { parse } from "path"; | ||
| import { createLogger } from "@vibe/shared-types"; | ||
|
|
||
| const logger = createLogger("protocol-handler"); | ||
|
|
||
| type BufferLoader = ( | ||
| filepath: string, | ||
| params: Record<string, any>, | ||
| ) => Promise<Buffer>; | ||
|
|
||
| /** | ||
| * PDF to Image conversion function | ||
| * Converts PDF files to JPEG images using PDF.js and Canvas | ||
| */ | ||
| async function pdfToImage( | ||
| filepath: string, | ||
| _params: Record<string, any>, | ||
| ): Promise<Buffer> { | ||
| try { | ||
| const content = await readFile(filepath); | ||
|
|
||
| // Use require for external dependencies to avoid TypeScript issues | ||
| // eslint-disable-next-line @typescript-eslint/no-require-imports | ||
| const pdfjsLib = require("pdfjs-dist"); | ||
| // eslint-disable-next-line @typescript-eslint/no-require-imports | ||
| const canvas = require("canvas"); | ||
|
|
||
| // Initialize PDF.js | ||
|
|
||
| pdfjsLib.GlobalWorkerOptions.workerSrc = require.resolve( | ||
| "pdfjs-dist/build/pdf.worker.js", | ||
| ); | ||
|
|
||
| // Load PDF document | ||
| const pdfDoc = await pdfjsLib.getDocument({ data: content }).promise; | ||
| const page = await pdfDoc.getPage(1); // Get first page | ||
|
|
||
| // Get page viewport | ||
| const viewport = page.getViewport({ scale: 1.5 }); | ||
|
|
||
| // Create canvas | ||
| const canvasElement = canvas.createCanvas(viewport.width, viewport.height); | ||
| const context = canvasElement.getContext("2d"); | ||
|
|
||
| // Render page to canvas | ||
| const renderContext = { | ||
| canvasContext: context, | ||
| viewport: viewport, | ||
| }; | ||
|
|
||
| await page.render(renderContext).promise; | ||
|
|
||
| // Convert canvas to buffer | ||
| const buffer = canvasElement.toBuffer("image/jpeg", { quality: 0.8 }); | ||
|
|
||
| return buffer; | ||
| } catch (reason) { | ||
| logger.error("PDF conversion failed:", reason); | ||
|
|
||
| // Return placeholder image as fallback | ||
| const placeholderImage = Buffer.from( | ||
| "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", | ||
| "base64", | ||
| ); | ||
| return placeholderImage; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Register the global img:// protocol handler | ||
| * This protocol handles local file serving with PDF conversion support | ||
| */ | ||
| export function registerImgProtocol(): void { | ||
| protocol.handle("img", async request => { | ||
| const url_string = request.url; | ||
| const url = new URL(url_string); | ||
| let loader: BufferLoader | undefined = undefined; | ||
|
|
||
| // Extract filepath from img:// URL | ||
| const filepath = url_string.substring("img://".length); | ||
|
|
||
| if (existsSync(filepath)) { | ||
| const { ext } = parse(filepath); | ||
| let blobType: string | undefined = undefined; | ||
|
|
||
| // Determine file type and loader | ||
| switch (ext.toLowerCase()) { | ||
| case ".jpg": | ||
| case ".jpeg": | ||
| blobType = "image/jpeg"; | ||
| break; | ||
| case ".png": | ||
| blobType = "image/png"; | ||
| break; | ||
| case ".svg": | ||
| blobType = "image/svg+xml"; | ||
| break; | ||
| case ".pdf": | ||
| loader = pdfToImage; | ||
| blobType = "image/jpeg"; | ||
| break; | ||
| } | ||
|
|
||
| // Load file content | ||
| const imageBuffer = loader | ||
| ? await loader(filepath, { ...url.searchParams, mimeType: blobType }) | ||
| : await readFile(filepath); | ||
|
|
||
| // Create response | ||
| const blob = new Blob([imageBuffer], { | ||
| type: blobType || "application/octet-stream", | ||
| }); | ||
| return new Response(blob, { | ||
| status: 200, | ||
| headers: { "Content-Type": blob.type }, | ||
| }); | ||
| } | ||
|
|
||
| // File not found | ||
| return new Response(null, { status: 404 }); | ||
| }); | ||
|
|
||
| logger.info("✓ Registered img:// protocol handler globally"); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| import { ipcMain, IpcMainInvokeEvent } from "electron"; | ||
| import { createLogger } from "@vibe/shared-types"; | ||
|
|
||
| const logger = createLogger("tray-control"); | ||
|
|
||
| // Reference to the main process tray variable | ||
| let mainTray: Electron.Tray | null = null; | ||
|
|
||
| // Function to set the main tray reference | ||
| export function setMainTray(tray: Electron.Tray | null) { | ||
| mainTray = tray; | ||
| } | ||
|
|
||
| /** | ||
| * Tray control IPC handlers | ||
| */ | ||
|
|
||
| ipcMain.handle("tray:create", async (_event: IpcMainInvokeEvent) => { | ||
| try { | ||
| if (mainTray) { | ||
| logger.info("Tray already exists"); | ||
| return true; | ||
| } | ||
|
|
||
| // Import tray creation logic from main process | ||
| const { createTray } = await import("../../tray-manager"); | ||
| mainTray = await createTray(); | ||
|
|
||
| logger.info("Tray created successfully"); | ||
| return true; | ||
| } catch (error) { | ||
| logger.error("Failed to create tray", { error }); | ||
| return false; | ||
| } | ||
| }); | ||
|
|
||
| ipcMain.handle("tray:destroy", async (_event: IpcMainInvokeEvent) => { | ||
| try { | ||
| if (!mainTray) { | ||
| logger.info("Tray does not exist"); | ||
| return true; | ||
| } | ||
|
|
||
| mainTray.destroy(); | ||
| mainTray = null; | ||
|
|
||
| logger.info("Tray destroyed successfully"); | ||
| return true; | ||
| } catch (error) { | ||
| logger.error("Failed to destroy tray", { error }); | ||
| return false; | ||
| } | ||
| }); | ||
|
|
||
| ipcMain.handle("tray:is-visible", async (_event: IpcMainInvokeEvent) => { | ||
| return mainTray !== null; | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import { Notification, type NotificationConstructorOptions } from "electron"; | ||
|
|
||
| export function createNotification({ | ||
| click, | ||
| action, | ||
| ...options | ||
| }: NotificationConstructorOptions & { | ||
| click?: () => void; | ||
| action?: (index: number) => void; | ||
| }) { | ||
| if (!Notification.isSupported()) { | ||
| return; | ||
| } | ||
|
|
||
| const notification = new Notification({ | ||
| silent: true, | ||
| ...options, | ||
| }); | ||
|
|
||
| if (click) { | ||
| notification.once("click", click); | ||
| } | ||
|
|
||
| if (action) { | ||
| notification.once("action", (_event, index) => { | ||
| action?.(index); | ||
| }); | ||
| } | ||
|
|
||
| notification.show(); | ||
|
|
||
| return notification; | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,86 @@ | ||||||
| import { ipcMain } from "electron"; | ||||||
| import { useUserProfileStore } from "@/store/user-profile-store"; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix the module import path to resolve compilation error. The TypeScript compiler cannot resolve the module path Verify the path alias configuration or use a relative import: -import { useUserProfileStore } from "@/store/user-profile-store";
+import { useUserProfileStore } from "../../store/user-profile-store";📝 Committable suggestion
Suggested change
🧰 Tools🪛 GitHub Actions: CI[error] 2-2: TypeScript error TS2307: Cannot find module '@/store/user-profile-store' or its corresponding type declarations. 🤖 Prompt for AI Agents |
||||||
| import { createLogger } from "@vibe/shared-types"; | ||||||
|
|
||||||
| const logger = createLogger("top-sites"); | ||||||
|
|
||||||
| export function registerTopSitesHandlers(): void { | ||||||
| ipcMain.handle("profile:get-top-sites", async (_, limit: number = 3) => { | ||||||
| try { | ||||||
| const userProfileStore = useUserProfileStore.getState(); | ||||||
| const activeProfile = userProfileStore.getActiveProfile(); | ||||||
|
|
||||||
| if (!activeProfile) { | ||||||
| return { success: false, sites: [] }; | ||||||
| } | ||||||
|
|
||||||
| // Get navigation history | ||||||
| const history = activeProfile.navigationHistory || []; | ||||||
|
|
||||||
| // Count visits per domain | ||||||
| const siteVisits = new Map< | ||||||
| string, | ||||||
| { | ||||||
| url: string; | ||||||
| title: string; | ||||||
| visitCount: number; | ||||||
| lastVisit: number; | ||||||
| } | ||||||
| >(); | ||||||
|
|
||||||
| history.forEach(entry => { | ||||||
| try { | ||||||
| const url = new URL(entry.url); | ||||||
| const domain = url.hostname; | ||||||
|
|
||||||
| const existing = siteVisits.get(domain); | ||||||
| if (existing) { | ||||||
| existing.visitCount++; | ||||||
| existing.lastVisit = Math.max(existing.lastVisit, entry.timestamp); | ||||||
| // Update title if the new one is better (not empty) | ||||||
| if (entry.title && entry.title.trim()) { | ||||||
| existing.title = entry.title; | ||||||
| } | ||||||
| } else { | ||||||
| siteVisits.set(domain, { | ||||||
| url: entry.url, | ||||||
| title: entry.title || domain, | ||||||
| visitCount: 1, | ||||||
| lastVisit: entry.timestamp, | ||||||
| }); | ||||||
| } | ||||||
| } catch { | ||||||
| // Skip invalid URLs | ||||||
| logger.debug("Skipping invalid URL:", entry.url); | ||||||
| } | ||||||
| }); | ||||||
|
|
||||||
| // Sort by visit count and get top sites | ||||||
| const topSites = Array.from(siteVisits.values()) | ||||||
| .sort((a, b) => { | ||||||
| // First sort by visit count | ||||||
| if (b.visitCount !== a.visitCount) { | ||||||
| return b.visitCount - a.visitCount; | ||||||
| } | ||||||
| // Then by last visit time | ||||||
| return b.lastVisit - a.lastVisit; | ||||||
| }) | ||||||
| .slice(0, limit) | ||||||
| .map(site => ({ | ||||||
| url: site.url, | ||||||
| title: site.title, | ||||||
| visitCount: site.visitCount, | ||||||
| // TODO: Add favicon support | ||||||
| favicon: undefined, | ||||||
| })); | ||||||
|
|
||||||
| return { | ||||||
| success: true, | ||||||
| sites: topSites, | ||||||
| }; | ||||||
| } catch (error) { | ||||||
| logger.error("Failed to get top sites:", error); | ||||||
| return { success: false, sites: [] }; | ||||||
| } | ||||||
| }); | ||||||
| } | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical security issue: Add path validation to prevent directory traversal attacks.
The filepath is extracted directly from the URL without any validation. This could allow malicious actors to access files outside the intended directory using path traversal techniques (e.g.,
img://../../../../etc/passwd).Add path validation:
🤖 Prompt for AI Agents