diff --git a/apps/browser/package.json b/apps/browser/package.json index 9813ded..9ca6384 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -67,7 +67,12 @@ ], "afterPack": "scripts/evs-sign.js", "mac": { - "target": "dmg", + "target": [ + { + "target": "dmg", + "arch": ["x64", "arm64"] + } + ], "category": "public.app-category.developer-tools", "icon": "assets/icon.icns", "hardenedRuntime": true, diff --git a/apps/browser/src/main/html-generator.ts b/apps/browser/src/main/html-generator.ts new file mode 100644 index 0000000..7796b1c --- /dev/null +++ b/apps/browser/src/main/html-generator.ts @@ -0,0 +1,62 @@ +/** + * Generate HTML templates for renderer pages + */ + +interface HtmlOptions { + title: string; + themeColor: string; + scriptPath: string; + cssPath?: string; + queryParams?: Record; +} + +export function generateHtml(options: HtmlOptions): string { + const { title, themeColor, scriptPath, cssPath, queryParams } = options; + + // Inject query parameters as a global variable + const queryParamsScript = queryParams + ? `` + : ''; + + return ` + + + + + + ${title} + ${cssPath ? `` : ''} + ${queryParamsScript} + + +
+ + +`; +} + +export function generateBlankPageHtml(scriptPath: string, cssPath?: string): string { + return generateHtml({ + title: 'Blank Page', + themeColor: '#1c1c1e', + scriptPath, + cssPath, + }); +} + +export function generateErrorPageHtml( + scriptPath: string, + cssPath?: string, + queryParams?: Record +): string { + return generateHtml({ + title: 'Error', + themeColor: '#2d2d2d', + scriptPath, + cssPath, + queryParams, + }); +} diff --git a/apps/browser/src/main/ipc-handlers.ts b/apps/browser/src/main/ipc-handlers.ts index adac933..dc6f332 100644 --- a/apps/browser/src/main/ipc-handlers.ts +++ b/apps/browser/src/main/ipc-handlers.ts @@ -255,10 +255,13 @@ export class IPCHandlers { } if (this.state.webContentsView && !this.state.webContentsView.webContents.isDestroyed()) { const url = this.state.webContentsView.webContents.getURL(); + console.log("[IPC] webcontents-get-url raw URL:", url); // Return "/" for blank-page and error-page - if (url.includes("blank-page.html") || url.startsWith("data:text/html")) { + if (url.includes("blank-page-tab-") || url.includes("error-page-tab-")) { + console.log("[IPC] Returning / for temporary file"); return "/"; } + console.log("[IPC] Returning original URL:", url); return url; } return ""; @@ -272,11 +275,11 @@ export class IPCHandlers { if (this.state.webContentsView && !this.state.webContentsView.webContents.isDestroyed()) { const url = this.state.webContentsView.webContents.getURL(); // Return "Blank Page" for blank-page - if (url.includes("blank-page.html")) { + if (url.includes("blank-page-tab-")) { return "Blank Page"; } // Return actual title for error-page (it's set in the HTML) - if (url.startsWith("data:text/html")) { + if (url.includes("error-page-tab-")) { return this.state.webContentsView.webContents.getTitle(); } return this.state.webContentsView.webContents.getTitle(); diff --git a/apps/browser/src/main/tab-manager.ts b/apps/browser/src/main/tab-manager.ts index e81998e..f3f74de 100644 --- a/apps/browser/src/main/tab-manager.ts +++ b/apps/browser/src/main/tab-manager.ts @@ -13,6 +13,7 @@ import { logSecurityEvent, } from "./security"; import { ThemeColorCache } from "./theme-cache"; +import { generateBlankPageHtml, generateErrorPageHtml } from "./html-generator"; export class TabManager { private state: AppState; @@ -82,7 +83,21 @@ export class TabManager { if (!url || url.trim() === "") { // Load blank page for blank tabs const { app } = require("electron"); - const blankPagePath = path.join(app.getAppPath(), "assets", "blank-page.html"); + const distPath = path.join(app.getAppPath(), "dist-renderer"); + const scriptPath = path.join(distPath, "pages", "blank-page.js"); + const cssFile = this.findAssetFile(distPath, "blank-page", ".css"); + const cssPath = cssFile ? path.join(distPath, cssFile) : undefined; + + // Generate HTML dynamically with absolute paths + const html = generateBlankPageHtml(scriptPath, cssPath); + + // Write to temporary file + const tmpDir = path.join(app.getPath("temp"), "aka-browser"); + if (!fs.existsSync(tmpDir)) { + fs.mkdirSync(tmpDir, { recursive: true }); + } + const tmpHtmlPath = path.join(tmpDir, `blank-page-${tabId}.html`); + fs.writeFileSync(tmpHtmlPath, html, "utf-8"); // Immediately set blank-page theme color before loading const blankPageThemeColor = "#1c1c1e"; @@ -94,7 +109,8 @@ export class TabManager { ); } - view.webContents.loadFile(blankPagePath).catch((err) => { + // Load from temporary file + view.webContents.loadFile(tmpHtmlPath).catch((err) => { console.error("[TabManager] Failed to load blank page:", err); }); } else { @@ -673,22 +689,26 @@ export class TabManager { contents.on("did-navigate", (event: any, url: string) => { const tab = this.state.tabs.find((t) => t.id === tabId); + let displayUrl = url; + if (tab) { - // Set empty URL and "Blank Page" title for blank-page - if (url.includes("blank-page.html")) { - tab.url = ""; + // Set "/" URL and "Blank Page" title for blank-page + if (url.includes("blank-page-tab-")) { + tab.url = "/"; tab.title = "Blank Page"; - } else if (url.startsWith("data:text/html")) { + displayUrl = "/"; + } else if (url.includes("error-page-tab-")) { // Error page - set URL to "/" and use actual title tab.url = "/"; tab.title = contents.getTitle() || "Aka Browser cannot open the page"; + displayUrl = "/"; } else { tab.url = url; tab.title = contents.getTitle() || url; } } - this.state.mainWindow?.webContents.send("webcontents-did-navigate", url); + this.state.mainWindow?.webContents.send("webcontents-did-navigate", displayUrl); if (this.state.activeTabId === tabId && this.state.mainWindow) { this.state.mainWindow.webContents.send("tabs-updated", { @@ -705,15 +725,19 @@ export class TabManager { contents.on("did-navigate-in-page", (event: any, url: string) => { const tab = this.state.tabs.find((t) => t.id === tabId); + let displayUrl = url; + if (tab) { - // Set empty URL and "Blank Page" title for blank-page - if (url.includes("blank-page.html")) { - tab.url = ""; + // Set "/" URL and "Blank Page" title for blank-page + if (url.includes("blank-page-tab-")) { + tab.url = "/"; tab.title = "Blank Page"; - } else if (url.startsWith("data:text/html")) { + displayUrl = "/"; + } else if (url.includes("error-page-tab-")) { // Error page - set URL to "/" and use actual title tab.url = "/"; tab.title = contents.getTitle() || "Aka Browser cannot open the page"; + displayUrl = "/"; } else { tab.url = url; tab.title = contents.getTitle() || url; @@ -722,7 +746,7 @@ export class TabManager { this.state.mainWindow?.webContents.send( "webcontents-did-navigate-in-page", - url + displayUrl ); if (this.state.activeTabId === tabId && this.state.mainWindow) { @@ -758,81 +782,70 @@ export class TabManager { // Load error page with details // Use app.getAppPath() for correct path in both dev and production const { app } = require("electron"); - const errorPagePath = path.join( - app.getAppPath(), - "assets", - "error-page.html" - ); - - if (fs.existsSync(errorPagePath)) { - const statusText = this.getNetworkErrorText(errorCode, errorDescription); - - // Read the error page HTML and inject parameters - try { - let errorPageHtml = fs.readFileSync(errorPagePath, 'utf-8'); - - // Create query string for error details - const queryParams = new URLSearchParams({ - statusCode: Math.abs(errorCode).toString(), - statusText: statusText, - url: validatedURL, - method: 'GET', - timestamp: new Date().toISOString() - }); - - // Inject query parameters into the HTML by replacing the script section - errorPageHtml = errorPageHtml.replace( - 'window.location.search', - `"?${queryParams.toString()}"` - ); - - console.log(`[TabManager] Loading error page for error ${errorCode}`); - - // Use setTimeout with a longer delay to ensure the failed load is completely finished - setTimeout(() => { - if (!contents.isDestroyed()) { - console.log(`[TabManager] Attempting to load error page now`); - // Load as data URL to avoid file:// protocol restrictions - const dataUrl = `data:text/html;charset=utf-8,${encodeURIComponent(errorPageHtml)}`; - contents.loadURL(dataUrl).then(() => { - console.log(`[TabManager] Error page loaded successfully`); - - // Update tab info - const tab = this.state.tabs.find((t) => t.id === tabId); - if (tab) { - tab.url = "/"; - tab.title = "Aka Browser cannot open the page"; - } - - // Apply error-page theme color immediately - const errorPageThemeColor = "#2d2d2d"; - this.state.latestThemeColor = errorPageThemeColor; - if (this.state.mainWindow && !this.state.mainWindow.isDestroyed()) { - this.state.mainWindow.webContents.send( - "webcontents-theme-color-updated", - errorPageThemeColor - ); - } - }).catch((err) => { - console.error(`[TabManager] Failed to load error page:`, err); - }); - } else { - console.log(`[TabManager] Contents destroyed, cannot load error page`); - } - }, 100); - - // Notify renderer about the error - this.state.mainWindow?.webContents.send( - "webcontents-did-fail-load", - errorCode, - errorDescription - ); - } catch (err) { - console.error(`[TabManager] Failed to read error page:`, err); - } - } else { - console.error(`[TabManager] Error page not found at: ${errorPagePath}`); + const distPath = path.join(app.getAppPath(), "dist-renderer"); + const statusText = this.getNetworkErrorText(errorCode, errorDescription); + + // Create query params object for error details + const queryParamsObj = { + statusCode: Math.abs(errorCode).toString(), + statusText: statusText, + url: validatedURL, + }; + + // Generate HTML dynamically with absolute paths + const scriptPath = path.join(distPath, "pages", "error-page.js"); + const cssFile = this.findAssetFile(distPath, "error-page", ".css"); + const cssPath = cssFile ? path.join(distPath, cssFile) : undefined; + const html = generateErrorPageHtml(scriptPath, cssPath, queryParamsObj); + + // Write to temporary file + const tmpDir = path.join(app.getPath("temp"), "aka-browser"); + if (!fs.existsSync(tmpDir)) { + fs.mkdirSync(tmpDir, { recursive: true }); } + const tmpHtmlPath = path.join(tmpDir, `error-page-${tabId}.html`); + fs.writeFileSync(tmpHtmlPath, html, "utf-8"); + + console.log(`[TabManager] Loading error page for error ${errorCode}`); + + // Use setTimeout with a longer delay to ensure the failed load is completely finished + setTimeout(() => { + if (!contents.isDestroyed()) { + console.log(`[TabManager] Attempting to load error page now`); + // Load error page from temporary file + contents.loadFile(tmpHtmlPath).then(() => { + console.log(`[TabManager] Error page loaded successfully`); + + // Update tab info + const tab = this.state.tabs.find((t) => t.id === tabId); + if (tab) { + tab.url = "/"; + tab.title = "Aka Browser cannot open the page"; + } + + // Apply error-page theme color immediately + const errorPageThemeColor = "#2d2d2d"; + this.state.latestThemeColor = errorPageThemeColor; + if (this.state.mainWindow && !this.state.mainWindow.isDestroyed()) { + this.state.mainWindow.webContents.send( + "webcontents-theme-color-updated", + errorPageThemeColor + ); + } + }).catch((err) => { + console.error(`[TabManager] Failed to load error page:`, err); + }); + } else { + console.log(`[TabManager] Contents destroyed, cannot load error page`); + } + }, 100); + + // Notify renderer about the error + this.state.mainWindow?.webContents.send( + "webcontents-did-fail-load", + errorCode, + errorDescription + ); } ); @@ -871,40 +884,32 @@ export class TabManager { // Load error page with details // Use app.getAppPath() for correct path in both dev and production const { app } = require("electron"); - const errorPagePath = path.join( - app.getAppPath(), - "assets", - "error-page.html" - ); - - // Check if error page exists - if (fs.existsSync(errorPagePath)) { - const statusText = this.getStatusText(httpResponseCode); - - // Read the error page HTML and inject parameters - try { - let errorPageHtml = fs.readFileSync(errorPagePath, 'utf-8'); - - // Create query string for error details - const queryParams = new URLSearchParams({ - statusCode: httpResponseCode.toString(), - statusText: statusText, - url: originalURL, - method: requestMethod, - timestamp: new Date().toISOString() - }); - - // Inject query parameters into the HTML by replacing the script section - errorPageHtml = errorPageHtml.replace( - 'window.location.search', - `"?${queryParams.toString()}"` - ); - - // Load as data URL to avoid file:// protocol restrictions - const dataUrl = `data:text/html;charset=utf-8,${encodeURIComponent(errorPageHtml)}`; - - // Load the error page - contents.loadURL(dataUrl).then(() => { + const distPath = path.join(app.getAppPath(), "dist-renderer"); + const statusText = this.getStatusText(httpResponseCode); + + // Create query params object for error details + const queryParamsObj = { + statusCode: httpResponseCode.toString(), + statusText: statusText, + url: originalURL, + }; + + // Generate HTML dynamically with absolute paths + const scriptPath = path.join(distPath, "pages", "error-page.js"); + const cssFile = this.findAssetFile(distPath, "error-page", ".css"); + const cssPath = cssFile ? path.join(distPath, cssFile) : undefined; + const html = generateErrorPageHtml(scriptPath, cssPath, queryParamsObj); + + // Write to temporary file + const tmpDir = path.join(app.getPath("temp"), "aka-browser"); + if (!fs.existsSync(tmpDir)) { + fs.mkdirSync(tmpDir, { recursive: true }); + } + const tmpHtmlPath = path.join(tmpDir, `error-page-${tabId}.html`); + fs.writeFileSync(tmpHtmlPath, html, "utf-8"); + + // Load the error page from temporary file + contents.loadFile(tmpHtmlPath).then(() => { // Update tab info const tab = this.state.tabs.find((t) => t.id === tabId); if (tab) { @@ -925,17 +930,13 @@ export class TabManager { console.error("Failed to load error page:", err); }); - // Notify renderer about the error - this.state.mainWindow?.webContents.send( - "webcontents-http-error", - httpResponseCode, - statusText, - originalURL - ); - } catch (err) { - console.error("Failed to read error page:", err); - } - } + // Notify renderer about the error + this.state.mainWindow?.webContents.send( + "webcontents-http-error", + httpResponseCode, + statusText, + originalURL + ); } } ); @@ -1133,4 +1134,26 @@ export class TabManager { return statusTexts[statusCode] || "Unknown Error"; } + + /** + * Find asset file by name pattern in dist directory + */ + private findAssetFile(distPath: string, namePattern: string, extension: string): string | null { + try { + const assetsPath = path.join(distPath, "assets"); + if (!fs.existsSync(assetsPath)) { + return null; + } + + const files = fs.readdirSync(assetsPath); + const matchingFile = files.find( + (file) => file.includes(namePattern) && file.endsWith(extension) + ); + + return matchingFile ? `assets/${matchingFile}` : null; + } catch (error) { + console.error(`[TabManager] Failed to find asset file:`, error); + return null; + } + } } diff --git a/apps/browser/src/renderer/app.tsx b/apps/browser/src/renderer/app.tsx index 906e17a..d849a60 100644 --- a/apps/browser/src/renderer/app.tsx +++ b/apps/browser/src/renderer/app.tsx @@ -237,19 +237,31 @@ function App() { // Update page info const updatePageInfo = async () => { try { - const url = await window.electronAPI?.webContents.getURL(); + let url = await window.electronAPI?.webContents.getURL(); const title = await window.electronAPI?.webContents.getTitle(); + console.log("[App] Raw URL from IPC:", url); + + // Filter out temporary file paths for blank-page and error-page + if (url && (url.includes("blank-page-tab-") || url.includes("error-page-tab-"))) { + console.log("[App] Filtering temporary file path to /"); + url = "/"; + } + + console.log("[App] Final URL:", url); + setPageTitle(title || "Untitled"); - setCurrentUrl(url || "https://www.google.com"); + setCurrentUrl(url || "/"); - if (url) { + if (url && url !== "/") { try { const urlObj = new URL(url); setPageDomain(urlObj.hostname); } catch (e) { setPageDomain(url); } + } else { + setPageDomain(""); } } catch (err) { // Silently ignore errors during page transitions diff --git a/apps/browser/src/renderer/components/settings.tsx b/apps/browser/src/renderer/components/settings.tsx index 5538064..128237c 100644 --- a/apps/browser/src/renderer/components/settings.tsx +++ b/apps/browser/src/renderer/components/settings.tsx @@ -1,5 +1,6 @@ import { useState, useEffect } from "react"; -import { Info, Globe, ChevronRight, ChevronLeft, Star, Trash2, Plus, Edit2, X } from "lucide-react"; +import { Info, ChevronRight, ChevronLeft, Star, Trash2, Plus, Edit2, X } from "lucide-react"; +import appIcon from "../../../assets/icon.png"; interface SettingsProps { theme: "light" | "dark"; @@ -70,6 +71,7 @@ const defaultBookmarks: Bookmark[] = [ function Settings({ theme, orientation, onClose }: SettingsProps) { const [currentView, setCurrentView] = useState<"main" | "about" | "bookmarks">("main"); const [appVersion, setAppVersion] = useState("0.0.0"); + const [appIconPath, setAppIconPath] = useState(""); const [bookmarks, setBookmarks] = useState([]); const [hiddenDefaultBookmarks, setHiddenDefaultBookmarks] = useState>(new Set()); const [showBookmarkDialog, setShowBookmarkDialog] = useState(false); @@ -83,6 +85,9 @@ function Settings({ theme, orientation, onClose }: SettingsProps) { setAppVersion(version); }); + // Set app icon path from imported image + setAppIconPath(appIcon); + // Load hidden default bookmarks from localStorage try { const hidden = localStorage.getItem("hiddenDefaultBookmarks"); @@ -405,15 +410,20 @@ function Settings({ theme, orientation, onClose }: SettingsProps) {
{/* App Icon and Name */}
-
- +
+ {appIconPath ? ( + Aka Browser + ) : ( +
+ +
+ )}

+ + +); diff --git a/apps/browser/src/renderer/pages/blank-page.css b/apps/browser/src/renderer/pages/blank-page.css new file mode 100644 index 0000000..923de67 --- /dev/null +++ b/apps/browser/src/renderer/pages/blank-page.css @@ -0,0 +1,144 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, +body, +#root { + height: 100%; + width: 100%; +} + +body { + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", + Arial, sans-serif; + background: #1c1c1e; + color: #ffffff; +} + +#root { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px 20px; +} + +.bookmarks-container { + width: 100%; + max-width: 800px; + margin-top: 120px; +} + +.bookmarks-title { + font-size: 20px; + font-weight: 600; + color: #ffffff; + margin-bottom: 24px; + text-align: center; +} + +.bookmarks-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; + padding: 0 20px; + max-width: 100%; +} + +.bookmark-item { + display: flex; + flex-direction: column; + align-items: center; + text-decoration: none; + padding: 16px 12px; + border-radius: 16px; + background: rgba(255, 255, 255, 0.05); + transition: all 0.2s ease; + cursor: pointer; + min-width: 0; +} + +.bookmark-item:hover { + background: rgba(255, 255, 255, 0.1); + transform: translateY(-2px); +} + +.bookmark-icon { + width: 64px; + height: 64px; + border-radius: 14px; + display: flex; + align-items: center; + justify-content: center; + font-size: 32px; + margin-bottom: 12px; + overflow: hidden; + background: transparent; +} + +.bookmark-icon img { + width: 100%; + height: 100%; + object-fit: contain; +} + +.bookmark-title { + font-size: 13px; + font-weight: 500; + color: #ffffff; + text-align: center; + word-break: break-word; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + line-clamp: 2; + -webkit-box-orient: vertical; +} + +.bookmark-url { + font-size: 11px; + color: #8e8e93; + text-align: center; + margin-top: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 100%; +} + +.empty-state { + text-align: center; + padding: 60px 20px; + color: #8e8e93; +} + +.empty-state-icon { + font-size: 48px; + margin-bottom: 16px; + opacity: 0.5; +} + +.empty-state-text { + font-size: 15px; + line-height: 1.6; + text-wrap: balance; + word-break: keep-all; +} + +@media (max-width: 600px) { + .bookmarks-grid { + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: 16px; + } + + .bookmark-icon { + width: 56px; + height: 56px; + font-size: 28px; + } +} diff --git a/apps/browser/src/renderer/pages/blank-page.tsx b/apps/browser/src/renderer/pages/blank-page.tsx new file mode 100644 index 0000000..73656f3 --- /dev/null +++ b/apps/browser/src/renderer/pages/blank-page.tsx @@ -0,0 +1,203 @@ +/// +import { useEffect, useState } from 'react'; +import './blank-page.css'; + +interface Bookmark { + id: string; + title: string; + url: string; + favicon?: string; + displayUrl?: string; + createdAt?: number; + updatedAt?: number; +} + +const defaultBookmarks: Bookmark[] = [ + { + id: 'default-google', + title: 'Google', + url: 'https://www.google.com', + favicon: 'https://www.google.com/s2/favicons?domain=google.com&sz=128', + displayUrl: 'google.com', + }, + { + id: 'default-youtube', + title: 'YouTube', + url: 'https://www.youtube.com', + favicon: 'https://www.google.com/s2/favicons?domain=youtube.com&sz=128', + displayUrl: 'youtube.com', + }, + { + id: 'default-netflix', + title: 'Netflix', + url: 'https://www.netflix.com', + favicon: 'https://www.google.com/s2/favicons?domain=netflix.com&sz=128', + displayUrl: 'netflix.com', + }, + { + id: 'default-x', + title: 'X', + url: 'https://x.com', + favicon: 'https://www.google.com/s2/favicons?domain=x.com&sz=128', + displayUrl: 'x.com', + }, +]; + +function getHighResFavicon(url: string): string[] { + try { + const domain = new URL(url).origin; + return [ + `${domain}/apple-touch-icon.png`, + `${domain}/apple-touch-icon-precomposed.png`, + `https://www.google.com/s2/favicons?domain=${domain}&sz=128`, + `${domain}/favicon.ico`, + ]; + } catch { + return []; + } +} + +interface BookmarkItemProps { + bookmark: Bookmark; +} + +function BookmarkItem({ bookmark }: BookmarkItemProps) { + const [currentFaviconIndex, setCurrentFaviconIndex] = useState(0); + const [faviconSources, setFaviconSources] = useState([]); + const [showFallback, setShowFallback] = useState(false); + + useEffect(() => { + // Build favicon sources list based on whether bookmark has favicon + let sources: string[]; + if (bookmark.favicon) { + // Try high-res favicon first, fallback to provided favicon + sources = bookmark.id && bookmark.id.startsWith('default-') + ? [bookmark.favicon] // Use direct URL for default bookmarks + : getHighResFavicon(bookmark.url).concat([bookmark.favicon]); + } else { + // Try to get high-res favicon even if not provided + sources = getHighResFavicon(bookmark.url); + } + + setFaviconSources(sources); + setCurrentFaviconIndex(0); + setShowFallback(sources.length === 0); + }, [bookmark.favicon, bookmark.url, bookmark.id]); + + const handleClick = (e: React.MouseEvent) => { + e.preventDefault(); + window.location.href = bookmark.url; + }; + + const handleImageError = () => { + const nextIndex = currentFaviconIndex + 1; + if (nextIndex < faviconSources.length) { + setCurrentFaviconIndex(nextIndex); + } else { + // All sources failed, show first letter + setShowFallback(true); + } + }; + + const displayUrl = bookmark.displayUrl || (() => { + try { + const hostname = new URL(bookmark.url).hostname; + return hostname.replace(/^www\./, ''); + } catch { + return bookmark.url; + } + })(); + + return ( + +
+ {!showFallback && faviconSources.length > 0 ? ( + {bookmark.title} + ) : ( + bookmark.title.charAt(0).toUpperCase() + )} +
+
{bookmark.title}
+
{displayUrl}
+
+ ); +} + +export default function BlankPage() { + const [bookmarks, setBookmarks] = useState([]); + const [hiddenDefaults, setHiddenDefaults] = useState([]); + + const loadBookmarks = async () => { + try { + const userBookmarks = await window.electronAPI?.bookmarks?.getAll(); + + // Load hidden default bookmarks from localStorage + let hidden: string[] = []; + try { + const hiddenStr = localStorage.getItem('hiddenDefaultBookmarks'); + if (hiddenStr) { + hidden = JSON.parse(hiddenStr); + } + } catch (error) { + console.error('Failed to load hidden bookmarks:', error); + } + + setHiddenDefaults(hidden); + + // Filter out hidden default bookmarks + const visibleDefaults = defaultBookmarks.filter( + (bookmark) => !hidden.includes(bookmark.id) + ); + + // Combine user bookmarks and visible default bookmarks + const displayBookmarks = [...(userBookmarks || []), ...visibleDefaults]; + + console.log('[Start Page] User bookmarks:', userBookmarks?.length, userBookmarks); + console.log('[Start Page] Visible defaults:', visibleDefaults.length, visibleDefaults); + console.log('[Start Page] Total display bookmarks:', displayBookmarks.length, displayBookmarks); + + setBookmarks(displayBookmarks); + } catch (error) { + console.error('Failed to load bookmarks:', error); + } + }; + + useEffect(() => { + loadBookmarks(); + + // Listen for bookmark updates + if (window.electronAPI?.bookmarks?.onUpdate) { + const cleanup = window.electronAPI.bookmarks.onUpdate(() => { + loadBookmarks(); + }); + + return cleanup; + } + }, []); + + return ( +
+

Favorites

+ {bookmarks.length === 0 ? ( +
+
+
+ No favorites yet. +
+ Click the menu button to add your favorite sites. +
+
+ ) : ( +
+ {bookmarks.map((bookmark) => ( + + ))} +
+ )} +
+ ); +} diff --git a/apps/browser/src/renderer/pages/error-page-entry.tsx b/apps/browser/src/renderer/pages/error-page-entry.tsx new file mode 100644 index 0000000..5fda1f8 --- /dev/null +++ b/apps/browser/src/renderer/pages/error-page-entry.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import ErrorPage from './error-page'; + +createRoot(document.getElementById('root')!).render( + + + +); diff --git a/apps/browser/src/renderer/pages/error-page.css b/apps/browser/src/renderer/pages/error-page.css new file mode 100644 index 0000000..f40813c --- /dev/null +++ b/apps/browser/src/renderer/pages/error-page.css @@ -0,0 +1,59 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", + Arial, sans-serif; + background: #2d2d2d; + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + color: #b8b8b8; +} + +.error-container { + text-align: center; + max-width: 600px; +} + +.error-title { + font-size: 28px; + font-weight: 400; + margin-bottom: 20px; + color: #b8b8b8; + letter-spacing: -0.5px; + text-wrap: balance; + word-break: keep-all; +} + +.error-description { + font-size: 15px; + line-height: 1.6; + color: #8e8e8e; + margin-bottom: 10px; + text-wrap: balance; + word-break: keep-all; +} + +.error-url { + font-size: 13px; + color: #6e6e6e; + word-break: break-all; + margin-top: 5px; +} + +@media (max-width: 600px) { + .error-title { + font-size: 24px; + } + + .error-description { + font-size: 14px; + } +} diff --git a/apps/browser/src/renderer/pages/error-page.tsx b/apps/browser/src/renderer/pages/error-page.tsx new file mode 100644 index 0000000..d850bad --- /dev/null +++ b/apps/browser/src/renderer/pages/error-page.tsx @@ -0,0 +1,138 @@ +import { useEffect, useState } from 'react'; +import './error-page.css'; + +interface ErrorInfo { + statusCode: string; + statusText: string; + url: string; +} + +interface ErrorMessage { + title: string; + desc: string; +} + +function getErrorInfo(): ErrorInfo { + // Try to get params from injected global variable first + const injectedParams = (window as any).__QUERY_PARAMS__; + if (injectedParams) { + return { + statusCode: injectedParams.statusCode || 'UNKNOWN', + statusText: injectedParams.statusText || 'Unknown Error', + url: injectedParams.url || '', + }; + } + + // Fallback to URL search params (for backward compatibility) + const params = new URLSearchParams(window.location.search); + return { + statusCode: params.get('statusCode') || 'UNKNOWN', + statusText: params.get('statusText') || 'Unknown Error', + url: params.get('url') || window.location.href, + }; +} + +function getErrorMessage(statusCode: string, url: string): ErrorMessage { + const code = parseInt(statusCode); + let domain: string; + + try { + domain = new URL(url).hostname; + } catch { + domain = 'the server'; + } + + // Network errors + if (code === 105) { + return { + title: 'Aka Browser cannot open the page', + desc: `Aka Browser could not open the page "${domain}" because the server could not be found.`, + }; + } + if (code === 106) { + return { + title: 'Aka Browser cannot open the page', + desc: `Aka Browser could not open the page "${domain}" because your device is not connected to the internet.`, + }; + } + if (code === 102) { + return { + title: 'Aka Browser cannot open the page', + desc: `Aka Browser could not open the page "${domain}" because the server refused the connection.`, + }; + } + if (code === 7 || code === 118) { + return { + title: 'Aka Browser cannot open the page', + desc: `Aka Browser could not open the page "${domain}" because the server took too long to respond.`, + }; + } + if (code >= 100 && code < 200) { + return { + title: 'Aka Browser cannot open the page', + desc: `Aka Browser could not open the page "${domain}" because it could not connect to the server.`, + }; + } + if (code >= 200 && code < 300) { + return { + title: 'Aka Browser cannot open the page', + desc: `Aka Browser could not open the page "${domain}" because there is a problem with the website's security certificate.`, + }; + } + + // HTTP errors + if (code === 404) { + return { + title: 'Aka Browser cannot open the page', + desc: `Aka Browser could not open the page "${domain}" because the page could not be found.`, + }; + } + if (code >= 400 && code < 500) { + return { + title: 'Aka Browser cannot open the page', + desc: `Aka Browser could not open the page "${domain}" because of a client error.`, + }; + } + if (code >= 500 && code < 600) { + return { + title: 'Aka Browser cannot open the page', + desc: `Aka Browser could not open the page "${domain}" because the server encountered an error.`, + }; + } + + return { + title: 'Aka Browser cannot open the page', + desc: `Aka Browser could not open the page "${domain}".`, + }; +} + +export default function ErrorPage() { + const [errorInfo, setErrorInfo] = useState({ + statusCode: 'UNKNOWN', + statusText: 'Unknown Error', + url: '', + }); + const [errorMessage, setErrorMessage] = useState({ + title: 'Aka Browser cannot open the page', + desc: 'Aka Browser could not open the page.', + }); + + useEffect(() => { + const info = getErrorInfo(); + const message = getErrorMessage(info.statusCode, info.url); + + setErrorInfo(info); + setErrorMessage(message); + + // Update page title + document.title = message.title; + }, []); + + return ( +
+

{errorMessage.title}

+

{errorMessage.desc}

+

{decodeURIComponent(errorInfo.url)}

+
+ ); +} diff --git a/apps/browser/src/renderer/vite-env.d.ts b/apps/browser/src/renderer/vite-env.d.ts index dd246d8..13de2f1 100644 --- a/apps/browser/src/renderer/vite-env.d.ts +++ b/apps/browser/src/renderer/vite-env.d.ts @@ -1,2 +1,7 @@ /// /// + +declare module '*.png'; +declare module '*.jpg'; +declare module '*.jpeg'; +declare module '*.svg'; diff --git a/apps/browser/vite.config.ts b/apps/browser/vite.config.ts index ff9d61d..1dc0d89 100644 --- a/apps/browser/vite.config.ts +++ b/apps/browser/vite.config.ts @@ -10,6 +10,22 @@ export default defineConfig({ build: { outDir: path.join(__dirname, 'dist-renderer'), emptyOutDir: true, + rollupOptions: { + input: { + main: path.join(__dirname, 'src/renderer/index.html'), + 'pages/blank-page': path.join(__dirname, 'src/renderer/pages/blank-page-entry.tsx'), + 'pages/error-page': path.join(__dirname, 'src/renderer/pages/error-page-entry.tsx'), + }, + output: { + entryFileNames: (chunkInfo) => { + // Keep page entries in pages/ directory + if (chunkInfo.name.startsWith('pages/')) { + return '[name].js'; + } + return 'assets/[name]-[hash].js'; + }, + }, + }, }, server: { port: 5173,