diff --git a/README.md b/README.md index e5662a5..573cf8a 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ **Your companion browser for Netflix, Twitter(X), and everything in between.** +🌐 **[Visit our website](https://browser.aka.page)** | 📚 **[Technical Wiki](https://deepwiki.com/hmmhmmhm/aka-browser)** | 🚀 **Currently in Beta** — Stable Release coming in November! + aka-browser screenshot [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) @@ -27,70 +29,51 @@ Think of it as your **always-on-top companion** for those moments when you need ### Perfect For -- 🍿 **Watching Netflix with subtitles** → PiP mode loses subtitles, aka-browser keeps them -- 🐦 **Following Twitter/X** → Keep your timeline visible while working -- 📺 **Monitoring streams** → Twitch, YouTube Live always in view -- 📖 **Following tutorials** → Step-by-step guides alongside your code -- 💬 **Chat windows** → Discord, Slack, or any web chat always accessible -- 🎵 **Music controls** → Spotify, YouTube Music at your fingertips +- **Watching Netflix with subtitles** → PiP mode loses subtitles, aka-browser keeps them +- **Following Twitter/X** → Keep your timeline visible while working +- **Monitoring streams** → Twitch, YouTube Live always in view +- **Following tutorials** → Step-by-step guides alongside your code +- **Chat windows** → Discord, Slack, or any web chat always accessible +- **Music controls** → Spotify, YouTube Music at your fingertips ### Why Not Just Use Your Main Browser? -- 🪟 **Always on top** → Never gets buried under other windows -- 📱 **Compact & elegant** → Beautiful iPhone frame that doesn't clutter your screen -- 🎯 **Purpose-built** → Lightweight, fast, and distraction-free -- 🎬 **DRM-ready** → Full Widevine support for streaming services -- ⚡ **Instant access** → Lives in your menu bar, launches immediately +- **Always on top** → Never gets buried under other windows +- **Compact & elegant** → Beautiful iPhone frame that doesn't clutter your screen +- **Purpose-built** → Lightweight, fast, and distraction-free +- **DRM-ready** → Full Widevine support for streaming services +- **Instant access** → Lives in your menu bar, launches immediately ## ✨ Key Features - - - - - - - - - -
- ### 🖥️ **Browser Essentials** -- 📑 **Multi-tab browsing** with visual switcher -- 🖼️ **Tab previews** via auto-screenshots -- 👆 **Trackpad gestures** for navigation -- 🎨 **Dynamic theme colors** with LRU cache -- 🤖 **Smart user agent** switching (mobile/desktop) - - +- **Multi-tab browsing** with visual switcher +- **Tab previews** via auto-screenshots +- **Trackpad gestures** for navigation +- **Dynamic theme colors** with LRU cache +- **Smart user agent** switching (mobile/desktop) ### 🎬 **DRM Content Ready** -- 🍿 **Netflix, Disney+, Prime Video** support -- 🔐 **Widevine CDM** integration -- ✍️ **Castlabs EVS** signed for production -- 📦 **Packaged builds** for DRM validation - -
+- **Netflix, Disney+, Prime Video** support +- **Widevine CDM** integration +- **Castlabs EVS** signed for production +- **Packaged builds** for DRM validation ### 🎨 **Beautiful Interface** -- 📱 **iPhone 15 Pro frame** with Dynamic Island -- ⚛️ **React 18** + Vite + TailwindCSS -- 🌓 **System theme** detection (light/dark) -- ✨ **Smooth animations** with optimized rendering - - +- **iPhone 15 Pro frame** with Dynamic Island +- **React 18** + Vite + TailwindCSS +- **System theme** detection (light/dark) +- **Smooth animations** with optimized rendering ### 🛠️ **Developer Tools** -- 🔍 **Chrome DevTools** (Cmd+Option+I) -- 🎯 **Element inspector** via right-click -- 🔗 **URL bar** with title/domain display -- 🖥️ **System tray** with always-on-top - -
+- **Chrome DevTools** (Cmd+Option+I) +- **Element inspector** via right-click +- **URL bar** with title/domain display +- **System tray** with always-on-top ## 🚀 Quick Start diff --git a/apps/browser/.gitignore b/apps/browser/.gitignore index 077bd14..46a4faa 100644 --- a/apps/browser/.gitignore +++ b/apps/browser/.gitignore @@ -10,3 +10,7 @@ release # Compiled scripts (TypeScript sources in scripts-src/) scripts/*.js + +# Environment variables (contains sensitive data) +.env.local +.env.*.local diff --git a/apps/browser/CODESIGN_SETUP.md b/apps/browser/CODESIGN_SETUP.md new file mode 100644 index 0000000..772bf08 --- /dev/null +++ b/apps/browser/CODESIGN_SETUP.md @@ -0,0 +1,212 @@ +# Code Signing and Notarization Setup Guide + +This document explains how to set up code signing and notarization for distributing aka-browser on macOS. + +## 1. Create Developer ID Application Certificate + +### 1-1. Generate CSR (Certificate Signing Request) + +1. Open **Keychain Access** app +2. Menu: **Keychain Access > Certificate Assistant > Request a Certificate from a Certificate Authority...** +3. Enter the following information: + - User Email Address: Your Apple Developer account email + - Common Name: Your name + - CA Email Address: Leave empty + - Select **"Saved to disk"** + - Check **"Let me specify key pair information"** +4. Choose save location (e.g., `~/Desktop/CertificateSigningRequest.certSigningRequest`) +5. Key Size: **2048 bits**, Algorithm: **RSA** +6. Click **Continue** + +### 1-2. Issue Certificate from Apple Developer Website + +1. Visit https://developer.apple.com/account/resources/certificates/list +2. Click **"+"** button +3. Select **"Developer ID Application"** (for distribution outside Mac App Store) +4. Click **Continue** +5. Upload the CSR file created above +6. Click **Continue** +7. Download certificate (`.cer` file) +8. **Double-click** the downloaded file to install it in Keychain + +### 1-3. Verify Certificate + +Verify the certificate is installed by running this command in Terminal: + +```bash +security find-identity -v -p codesigning +``` + +You should see output like: +``` +1) XXXXXX "Developer ID Application: Your Name (TEAM_ID)" +``` + +## 2. Generate App-Specific Password + +An App-Specific Password is required for notarization. + +1. Visit https://appleid.apple.com/account/manage +2. **Sign in** (Two-factor authentication required) +3. Click **App-Specific Passwords** in the **Security** section +4. Click **"+"** button +5. Enter a name (e.g., "aka-browser notarization") +6. Copy the generated password (e.g., `abcd-efgh-ijkl-mnop`) +7. **Save it securely** (you won't be able to see it again) + +## 3. Find Your Team ID + +1. Visit https://developer.apple.com/account +2. Find your **Team ID** in the **Membership Details** section (e.g., `AB12CD34EF`) + +## 4. Set Environment Variables + +### 4-1. Method 1: .env File (Recommended) + +Create a `.env.local` file in the project root: + +```bash +# /Users/hm/Documents/GitHub/aka-browser/apps/browser/.env.local +APPLE_ID=your-apple-id@example.com +APPLE_APP_SPECIFIC_PASSWORD=abcd-efgh-ijkl-mnop +APPLE_TEAM_ID=AB12CD34EF +``` + +**Warning**: Add `.env.local` to `.gitignore` to prevent committing it to Git! + +### 4-2. Method 2: System Environment Variables + +Add to `~/.zshrc` or `~/.bash_profile`: + +```bash +export APPLE_ID="your-apple-id@example.com" +export APPLE_APP_SPECIFIC_PASSWORD="abcd-efgh-ijkl-mnop" +export APPLE_TEAM_ID="AB12CD34EF" +``` + +After saving: +```bash +source ~/.zshrc +``` + +### 4-3. Method 3: Specify During Build + +```bash +APPLE_ID="your@email.com" \ +APPLE_APP_SPECIFIC_PASSWORD="abcd-efgh-ijkl-mnop" \ +APPLE_TEAM_ID="AB12CD34EF" \ +pnpm run package +``` + +## 5. Build and Notarize + +### 5-1. Install Dependencies + +```bash +cd /Users/hm/Documents/GitHub/aka-browser/apps/browser +pnpm install +``` + +### 5-2. Compile Build Scripts + +```bash +pnpm run build:scripts +``` + +### 5-3. Build and Notarize App + +```bash +pnpm run package +``` + +Build process: +1. ✅ Build app +2. ✅ EVS signing (Widevine CDM) +3. ✅ Code signing (Developer ID Application) +4. ✅ Notarization (upload to Apple servers) +5. ✅ DMG creation + +### 5-4. Verify Notarization + +After the build completes, verify notarization status with: + +```bash +spctl -a -vv -t install release/mac-arm64/aka-browser.app +``` + +Successful output: +``` +release/mac-arm64/aka-browser.app: accepted +source=Notarized Developer ID +``` + +## 6. Troubleshooting + +### Cannot Find Certificate + +``` +Error: Cannot find identity matching "Developer ID Application" +``` + +**Solution**: +1. Verify the certificate is installed in Keychain +2. Check if the certificate has expired +3. Run `security find-identity -v -p codesigning` to verify + +### Notarization Failed + +``` +Error: Notarization failed +``` + +**Solution**: +1. Verify Apple ID, App-Specific Password, and Team ID are correct +2. Ensure two-factor authentication is enabled +3. Check if App-Specific Password is valid (it may have expired) + +### Check Notarization Logs + +```bash +xcrun notarytool log --apple-id "your@email.com" \ + --password "abcd-efgh-ijkl-mnop" \ + --team-id "AB12CD34EF" \ + +``` + +## 7. Distribution + +Notarized DMG files are created in the `release/` directory: + +- `release/aka-browser-0.1.0-arm64.dmg` (Apple Silicon) +- `release/aka-browser-0.1.0-x64.dmg` (Intel) +- `release/aka-browser-0.1.0-universal.dmg` (Universal) + +You can upload these files to the internet for distribution. Users won't see the "damaged" message when downloading and installing. + +## 8. Security Precautions + +- ❌ **NEVER commit to Git**: + - App-Specific Password + - `.env.local` file + - Certificate files (`.p12`, `.cer`) + +- ✅ **Store securely**: + - Use password managers like 1Password or Bitwarden + - Use encrypted channels when sharing with team members + +## 9. CI/CD Setup (GitHub Actions) + +Add the following variables to GitHub Secrets: + +- `APPLE_ID` +- `APPLE_APP_SPECIFIC_PASSWORD` +- `APPLE_TEAM_ID` +- `CSC_LINK` (certificate `.p12` file encoded in base64) +- `CSC_KEY_PASSWORD` (certificate password) + +For detailed setup, refer to the electron-builder documentation: +https://www.electron.build/code-signing + +--- + +**Questions**: Please create an issue if you encounter any problems. diff --git a/apps/browser/CODESIGN_START.md b/apps/browser/CODESIGN_START.md new file mode 100644 index 0000000..48eae34 --- /dev/null +++ b/apps/browser/CODESIGN_START.md @@ -0,0 +1,106 @@ +# Quick Start Guide + +## 🚀 Get Started Now + +### 1. Issue Developer ID Application Certificate + +**Takes 5 minutes** + +1. https://developer.apple.com/account/resources/certificates/list +2. Click "+" button → Select "Developer ID Application" +3. Upload CSR file (generate using method below) +4. Download and double-click to install + +**How to generate CSR:** +``` +Keychain Access app → Menu → Keychain Access → Certificate Assistant → Request a Certificate from a Certificate Authority... +→ Enter email → Select "Saved to disk" → Save +``` + +### 2. Generate App-Specific Password + +**Takes 2 minutes** + +1. https://appleid.apple.com/account/manage +2. Security → App-Specific Passwords → "+" button +3. Enter name (e.g., "aka-browser") → Generate +4. **Copy password** (you won't be able to see it again!) + +### 3. Find Your Team ID + +**Takes 1 minute** + +1. https://developer.apple.com/account +2. Membership Details → Copy Team ID (e.g., `AB12CD34EF`) + +### 4. Set Environment Variables + +Create a `.env.local` file in the project root: + +```bash +# /Users/hm/Documents/GitHub/aka-browser/apps/browser/.env.local +APPLE_ID=your-apple-id@example.com +APPLE_APP_SPECIFIC_PASSWORD=abcd-efgh-ijkl-mnop +APPLE_TEAM_ID=AB12CD34EF +``` + +**Or** run directly in terminal: + +```bash +export APPLE_ID="your-apple-id@example.com" +export APPLE_APP_SPECIFIC_PASSWORD="abcd-efgh-ijkl-mnop" +export APPLE_TEAM_ID="AB12CD34EF" +``` + +### 5. Build and Notarize + +```bash +cd /Users/hm/Documents/GitHub/aka-browser/apps/browser +pnpm run package +``` + +**Build process (approx. 5-10 minutes):** +1. ✅ Build app +2. ✅ EVS signing (Widevine) +3. ✅ Code signing +4. ✅ Notarization (upload to Apple servers) +5. ✅ DMG creation + +### 6. Done! + +Generated file: `release/aka-browser-0.1.0-arm64.dmg` + +You can now upload this file to the internet for distribution. +Users won't see the **"damaged"** message when downloading! ✨ + +--- + +## 🔍 Verify Notarization + +```bash +spctl -a -vv -t install release/mac-arm64/aka-browser.app +``` + +On success: +``` +accepted +source=Notarized Developer ID +``` + +--- + +## ❓ Troubleshooting + +### Cannot Find Certificate +```bash +security find-identity -v -p codesigning +``` +→ Check if "Developer ID Application" certificate exists + +### Notarization Failed +- Verify Apple ID, Password, and Team ID are correct +- Ensure two-factor authentication is enabled + +--- + +**For detailed instructions**: See `CODESIGN_SETUP.md` diff --git a/apps/browser/assets/blank-page.html b/apps/browser/assets/blank-page.html deleted file mode 100644 index f366b60..0000000 --- a/apps/browser/assets/blank-page.html +++ /dev/null @@ -1,353 +0,0 @@ - - - - - - - Blank Page - - - -
-

Favorites

-
- -
- -
- - - - diff --git a/apps/browser/assets/error-page.html b/apps/browser/assets/error-page.html deleted file mode 100644 index 893eed6..0000000 --- a/apps/browser/assets/error-page.html +++ /dev/null @@ -1,189 +0,0 @@ - - - - - - - Error - - - -
-

- Aka Browser cannot open the page -

-

- Aka Browser could not open the page because the server could not be - found. -

-

-
- - - - diff --git a/apps/browser/package.json b/apps/browser/package.json index 9ca6384..2179953 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -32,6 +32,7 @@ "author": "hmmhmmhm", "license": "MIT", "devDependencies": { + "@electron/notarize": "^2.5.0", "@tailwindcss/vite": "^4.1.14", "@types/node": "^20.11.0", "@types/react": "^19.2.2", @@ -39,6 +40,7 @@ "@vitejs/plugin-react": "^5.0.4", "concurrently": "^8.2.2", "cross-env": "^10.1.0", + "dotenv": "^17.2.3", "electron": "github:castlabs/electron-releases#v38.0.0+wvcus", "electron-builder": "^26.0.12", "tailwindcss": "^4.1.14", @@ -66,11 +68,15 @@ "!node_modules/**/*" ], "afterPack": "scripts/evs-sign.js", + "afterSign": "scripts/notarize.js", "mac": { "target": [ { "target": "dmg", - "arch": ["x64", "arm64"] + "arch": [ + "x64", + "arm64" + ] } ], "category": "public.app-category.developer-tools", @@ -78,8 +84,7 @@ "hardenedRuntime": true, "gatekeeperAssess": false, "entitlements": "build/entitlements.mac.plist", - "entitlementsInherit": "build/entitlements.mac.plist", - "identity": null + "entitlementsInherit": "build/entitlements.mac.plist" }, "win": { "target": "nsis", diff --git a/apps/browser/scripts-src/notarize.ts b/apps/browser/scripts-src/notarize.ts new file mode 100644 index 0000000..eaa5fa9 --- /dev/null +++ b/apps/browser/scripts-src/notarize.ts @@ -0,0 +1,44 @@ +import { notarize } from '@electron/notarize'; +import * as path from 'path'; +import * as dotenv from 'dotenv'; + +// Load .env.local file +dotenv.config({ path: path.join(__dirname, '..', '.env.local') }); + +export default async function notarizing(context: any) { + const { electronPlatformName, appOutDir } = context; + + // Only notarize on macOS + if (electronPlatformName !== 'darwin') { + return; + } + + // Check environment variables + const appleId = process.env.APPLE_ID; + const appleIdPassword = process.env.APPLE_APP_SPECIFIC_PASSWORD; + const teamId = process.env.APPLE_TEAM_ID; + + if (!appleId || !appleIdPassword || !teamId) { + console.warn('⚠️ Skipping notarization. Please set environment variables:'); + console.warn(' APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, APPLE_TEAM_ID'); + return; + } + + const appName = context.packager.appInfo.productFilename; + const appPath = path.join(appOutDir, `${appName}.app`); + + console.log(`🔐 Starting notarization: ${appPath}`); + + try { + await notarize({ + appPath, + appleId, + appleIdPassword, + teamId, + }); + console.log('✅ Notarization completed!'); + } catch (error) { + console.error('❌ Notarization failed:', error); + throw error; + } +} diff --git a/apps/browser/scripts/notarize.d.ts b/apps/browser/scripts/notarize.d.ts new file mode 100644 index 0000000..c04f366 --- /dev/null +++ b/apps/browser/scripts/notarize.d.ts @@ -0,0 +1 @@ +export default function notarizing(context: any): Promise; diff --git a/apps/browser/src/main/html-generator.ts b/apps/browser/src/main/html-generator.ts index 7796b1c..5b4662d 100644 --- a/apps/browser/src/main/html-generator.ts +++ b/apps/browser/src/main/html-generator.ts @@ -8,16 +8,20 @@ interface HtmlOptions { scriptPath: string; cssPath?: string; queryParams?: Record; + isDev?: boolean; } export function generateHtml(options: HtmlOptions): string { - const { title, themeColor, scriptPath, cssPath, queryParams } = options; + const { title, themeColor, scriptPath, cssPath, queryParams, isDev } = options; // Inject query parameters as a global variable const queryParamsScript = queryParams ? `` : ''; + // In dev mode, use Vite's @vite/client for HMR + const viteClient = isDev ? '' : ''; + return ` @@ -30,6 +34,7 @@ export function generateHtml(options: HtmlOptions): string { ${title} ${cssPath ? `` : ''} ${queryParamsScript} + ${viteClient}
@@ -38,19 +43,21 @@ export function generateHtml(options: HtmlOptions): string { `; } -export function generateBlankPageHtml(scriptPath: string, cssPath?: string): string { +export function generateBlankPageHtml(scriptPath: string, cssPath?: string, isDev: boolean = false): string { return generateHtml({ title: 'Blank Page', themeColor: '#1c1c1e', scriptPath, cssPath, + isDev, }); } export function generateErrorPageHtml( scriptPath: string, cssPath?: string, - queryParams?: Record + queryParams?: Record, + isDev: boolean = false ): string { return generateHtml({ title: 'Error', @@ -58,5 +65,6 @@ export function generateErrorPageHtml( scriptPath, cssPath, queryParams, + isDev, }); } diff --git a/apps/browser/src/main/tab-manager.ts b/apps/browser/src/main/tab-manager.ts index f3f74de..cbbff32 100644 --- a/apps/browser/src/main/tab-manager.ts +++ b/apps/browser/src/main/tab-manager.ts @@ -36,11 +36,13 @@ export class TabManager { console.log("[TabManager] Creating tab with preload:", webviewPreloadPath); console.log("[TabManager] Preload exists:", hasWebviewPreload); + const isDev = process.env.NODE_ENV === "development"; + const view = new WebContentsView({ webPreferences: { nodeIntegration: false, contextIsolation: true, - webSecurity: true, + webSecurity: !isDev, // Disable webSecurity in dev mode to allow loading from Vite dev server allowRunningInsecureContent: false, sandbox: false, // Widevine requires sandbox: false partition: "persist:main", @@ -81,24 +83,6 @@ export class TabManager { // Load URL or blank page if (!url || url.trim() === "") { - // Load blank page for blank tabs - const { app } = require("electron"); - 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"; this.state.latestThemeColor = blankPageThemeColor; @@ -109,10 +93,65 @@ export class TabManager { ); } - // Load from temporary file - view.webContents.loadFile(tmpHtmlPath).catch((err) => { - console.error("[TabManager] Failed to load blank page:", err); - }); + // Load blank page for blank tabs + const { app } = require("electron"); + + if (isDev) { + // In dev mode, use temporary file with Vite dev server URLs + const devHtml = ` + + + + + + Blank Page + + + + +
+ + +`; + + // 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, devHtml, "utf-8"); + + // Load from temporary file (file:// protocol with webSecurity disabled allows loading from http://) + view.webContents.loadFile(tmpHtmlPath).catch((err) => { + console.error("[TabManager] Failed to load blank page:", err); + }); + } else { + // In production, generate HTML and load from temporary file + const distPath = path.join(app.getAppPath(), "dist-renderer"); + const scriptPath = path.join(distPath, "pages", "blank-page.js"); + + const html = generateBlankPageHtml(scriptPath, undefined, false); + + // 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"); + + // Load from temporary file + view.webContents.loadFile(tmpHtmlPath).catch((err) => { + console.error("[TabManager] Failed to load blank page:", err); + }); + } } else { const sanitized = sanitizeUrl(url); if (isValidUrl(sanitized)) { @@ -780,31 +819,9 @@ export class TabManager { ); // Load error page with details - // Use app.getAppPath() for correct path in both dev and production const { app } = require("electron"); - 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"); + const isDev = process.env.NODE_ENV === "development"; console.log(`[TabManager] Loading error page for error ${errorCode}`); @@ -812,8 +829,49 @@ export class TabManager { setTimeout(() => { if (!contents.isDestroyed()) { console.log(`[TabManager] Attempting to load error page now`); - // Load error page from temporary file - contents.loadFile(tmpHtmlPath).then(() => { + + // Create query params object for error details + const queryParamsObj = { + statusCode: Math.abs(errorCode).toString(), + statusText: statusText, + url: validatedURL, + }; + + if (isDev) { + // In dev mode, use temporary file with Vite dev server URLs + const devHtml = ` + + + + + + Error + + + + + +
+ + +`; + + // 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, devHtml, "utf-8"); + + // Load from temporary file (file:// protocol with webSecurity disabled allows loading from http://) + contents.loadFile(tmpHtmlPath).then(() => { console.log(`[TabManager] Error page loaded successfully`); // Update tab info @@ -836,9 +894,47 @@ export class TabManager { console.error(`[TabManager] Failed to load error page:`, err); }); } else { - console.log(`[TabManager] Contents destroyed, cannot load error page`); + // In production, generate HTML and load from temporary file + const distPath = path.join(app.getAppPath(), "dist-renderer"); + const scriptPath = path.join(distPath, "pages", "error-page.js"); + const html = generateErrorPageHtml(scriptPath, undefined, queryParamsObj, false); + + // 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 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); + }); } - }, 100); + } else { + console.log(`[TabManager] Contents destroyed, cannot load error page`); + } + }, 100); // Notify renderer about the error this.state.mainWindow?.webContents.send( @@ -882,10 +978,9 @@ export class TabManager { ); // Load error page with details - // Use app.getAppPath() for correct path in both dev and production const { app } = require("electron"); - const distPath = path.join(app.getAppPath(), "dist-renderer"); const statusText = this.getStatusText(httpResponseCode); + const isDev = process.env.NODE_ENV === "development"; // Create query params object for error details const queryParamsObj = { @@ -894,41 +989,96 @@ export class TabManager { 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 }); + if (isDev) { + // In dev mode, use temporary file with Vite dev server URLs + const devHtml = ` + + + + + + Error + + + + + +
+ + +`; + + // 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-http-${tabId}.html`); + fs.writeFileSync(tmpHtmlPath, devHtml, "utf-8"); + + // Load from temporary file (file:// protocol with webSecurity disabled allows loading from http://) + contents.loadFile(tmpHtmlPath).then(() => { + // 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("Failed to load error page from data URL:", err); + }); + } else { + // In production, generate HTML and load from temporary file + const distPath = path.join(app.getAppPath(), "dist-renderer"); + const scriptPath = path.join(distPath, "pages", "error-page.js"); + const html = generateErrorPageHtml(scriptPath, undefined, queryParamsObj, false); + + // 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) { + 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("Failed to load error page:", err); + }); } - 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) { - 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("Failed to load error page:", err); - }); // Notify renderer about the error this.state.mainWindow?.webContents.send( @@ -1135,25 +1285,4 @@ 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/pages/blank-page.css b/apps/browser/src/renderer/pages/blank-page.css deleted file mode 100644 index 923de67..0000000 --- a/apps/browser/src/renderer/pages/blank-page.css +++ /dev/null @@ -1,144 +0,0 @@ -* { - 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 index 73656f3..888a2ea 100644 --- a/apps/browser/src/renderer/pages/blank-page.tsx +++ b/apps/browser/src/renderer/pages/blank-page.tsx @@ -1,6 +1,5 @@ /// import { useEffect, useState } from 'react'; -import './blank-page.css'; interface Bookmark { id: string; @@ -180,24 +179,172 @@ export default function BlankPage() { }, []); return ( -
-

Favorites

- {bookmarks.length === 0 ? ( -
-
-
- No favorites yet. -
- Click the menu button to add your favorite sites. + <> + +
+

Favorites

+ {bookmarks.length === 0 ? ( +
+
+
+ No favorites yet. +
+ Click the menu button to add your favorite sites. +
-
- ) : ( -
- {bookmarks.map((bookmark) => ( - - ))} -
- )} -
+ ) : ( +
+ {bookmarks.map((bookmark) => ( + + ))} +
+ )} +
+ ); } diff --git a/apps/browser/src/renderer/pages/error-page.css b/apps/browser/src/renderer/pages/error-page.css deleted file mode 100644 index f40813c..0000000 --- a/apps/browser/src/renderer/pages/error-page.css +++ /dev/null @@ -1,59 +0,0 @@ -* { - 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 index d850bad..2abb551 100644 --- a/apps/browser/src/renderer/pages/error-page.tsx +++ b/apps/browser/src/renderer/pages/error-page.tsx @@ -1,5 +1,4 @@ import { useEffect, useState } from 'react'; -import './error-page.css'; interface ErrorInfo { statusCode: string; @@ -129,10 +128,73 @@ export default function ErrorPage() { }, []); return ( -
-

{errorMessage.title}

-

{errorMessage.desc}

-

{decodeURIComponent(errorInfo.url)}

-
+ <> + +
+

{errorMessage.title}

+

{errorMessage.desc}

+

{decodeURIComponent(errorInfo.url)}

+
+ ); } diff --git a/apps/browser/vite.config.ts b/apps/browser/vite.config.ts index 1dc0d89..c2d0c63 100644 --- a/apps/browser/vite.config.ts +++ b/apps/browser/vite.config.ts @@ -29,5 +29,11 @@ export default defineConfig({ }, server: { port: 5173, + cors: true, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', + 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization', + }, }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55d8485..cb27c4f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,6 +30,9 @@ importers: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) devDependencies: + '@electron/notarize': + specifier: ^2.5.0 + version: 2.5.0 '@tailwindcss/vite': specifier: ^4.1.14 version: 4.1.14(vite@7.1.11(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.2)) @@ -51,6 +54,9 @@ importers: cross-env: specifier: ^10.1.0 version: 10.1.0 + dotenv: + specifier: ^17.2.3 + version: 17.2.3 electron: specifier: github:castlabs/electron-releases#v38.0.0+wvcus version: https://codeload.github.com/castlabs/electron-releases/tar.gz/678b5c7761825c5af936f5c67a9101f3fc6ab750 @@ -1095,6 +1101,10 @@ packages: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} + dotenv@17.2.3: + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + engines: {node: '>=12'} + dotenv@9.0.2: resolution: {integrity: sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==} engines: {node: '>=10'} @@ -2455,7 +2465,7 @@ snapshots: '@babel/parser': 7.28.4 '@babel/template': 7.27.2 '@babel/types': 7.28.4 - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -3513,6 +3523,8 @@ snapshots: dotenv@16.6.1: {} + dotenv@17.2.3: {} + dotenv@9.0.2: {} dunder-proto@1.0.1: