mirror of
https://github.com/dream-pep/koring-launcher.git
synced 2026-09-12 05:45:18 +08:00
Add crash monitor, OOBE flow & factory-reset
Introduce crash monitoring and an OOBE onboarding flow. Adds a crash logger (koring-crash.log), crash-monitor handler/window, preload-crash bridge, crash UI (crash.html, src/crash.tsx, pages/crash) and a debug page. Main process now sets up crash listeners, startup checks (first-launch, .minecraft), and a config reset/factory-reset that removes Koring.yml, koring-auth.json and background cache and can relaunch the app. Exposes config preloading to renderer, adds OOBE pages/layout, confirm dialog store/UI, trims Silk renderer settings, bumps version to 1.0.1 and adds motion dependency, and registers crash.html in Vite. Various related type and config updates included.
This commit is contained in:
@@ -1,7 +1,2 @@
|
|||||||
VITE_BUILD_MODE=beta
|
VITE_BUILD_MODE=beta
|
||||||
VITE_APP_ICON=/beta.png
|
VITE_APP_ICON=/beta.png
|
||||||
|
|
||||||
VITE_START_POP=true
|
|
||||||
VITE_START_POP_TITLE="你正在使用 UI 预览版本"
|
|
||||||
VITE_START_POP_INFO="此版本是专门用于进行检测是否有 UI 问题的版本,因此功能将会缺失"
|
|
||||||
VITE_START_POP_BOUTTON="我已了解"
|
|
||||||
@@ -1,7 +1,2 @@
|
|||||||
VITE_BUILD_MODE=dev
|
VITE_BUILD_MODE=dev
|
||||||
VITE_APP_ICON=/dev.png
|
VITE_APP_ICON=/dev.png
|
||||||
|
|
||||||
VITE_START_POP=true
|
|
||||||
VITE_START_POP_TITLE="你正在使用 UI 预览版本"
|
|
||||||
VITE_START_POP_INFO="此版本是专门用于进行检测是否有UI问题的版本,因此功能将会缺失"
|
|
||||||
VITE_START_POP_BOUTTON="我已了解"
|
|
||||||
@@ -1,7 +1,2 @@
|
|||||||
VITE_BUILD_MODE=run
|
VITE_BUILD_MODE=run
|
||||||
VITE_APP_ICON=/run.png
|
VITE_APP_ICON=/run.png
|
||||||
|
|
||||||
VITE_START_POP=false
|
|
||||||
VITE_START_POP_TITLE="你正在使用 UI 预览版本"
|
|
||||||
VITE_START_POP_INFO="此版本是专门用于进行检测是否有UI问题的版本,因此功能将会缺失"
|
|
||||||
VITE_START_POP_BOUTTON="我已了解"
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
# Crash Monitor Implementation Plan
|
|
||||||
|
|
||||||
## 1. Architecture Overview
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────┐
|
|
||||||
│ Main Process │
|
|
||||||
│ ┌──────────────────────────────────────────────────┐ │
|
|
||||||
│ │ Crash Logger (file-based, survives crashes) │ │
|
|
||||||
│ │ - ipcMain.on('renderer-error') → write to file │ │
|
|
||||||
│ │ - process.on('uncaughtException') → write to file │ │
|
|
||||||
│ │ - app.on('render-process-gone') → write to file │ │
|
|
||||||
│ │ - app.on('child-process-gone') → write to file │ │
|
|
||||||
│ └──────────────────────────────────────────────────┘ │
|
|
||||||
│ ┌──────────────────────────────┐ │
|
|
||||||
│ │ UtilityProcess (monitor) │ ← isolated process │
|
|
||||||
│ │ - Watches crash log file │ survives window │
|
|
||||||
│ │ - Sends crash events via │ crashes │
|
|
||||||
│ │ MessagePort to main │ │
|
|
||||||
│ └──────────────────────────────┘ │
|
|
||||||
│ ┌──────────────────────────────┐ │
|
|
||||||
│ │ Crash Monitor Window │ ← separate window │
|
|
||||||
│ │ - Shows crash dialogs │ custom UI │
|
|
||||||
│ │ - Log viewer window │ │
|
|
||||||
│ └──────────────────────────────┘ │
|
|
||||||
└─────────────────────────────────────────────────────────┘
|
|
||||||
↕ IPC (contextBridge)
|
|
||||||
┌─────────────────────────────────────────────────────────┐
|
|
||||||
│ Renderer (Main Window) │
|
|
||||||
│ - window.onerror → ipcRenderer.send('renderer-error') │
|
|
||||||
│ - unhandledrejection → ipcRenderer.send(...) │
|
|
||||||
│ - webContents 'render-process-gone' → recover │
|
|
||||||
└─────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2. Implementation Steps
|
|
||||||
|
|
||||||
### Step 1: Create Crash Logger Module
|
|
||||||
**File:** `electron/core/crash-logger.ts`
|
|
||||||
- Initialize crash log path (`koring-crash.log` next to executable)
|
|
||||||
- Write crash events synchronously to survive crashes
|
|
||||||
- Buffer recent events for breadcrumb trail
|
|
||||||
- Log rotation (keep last 1000 lines)
|
|
||||||
|
|
||||||
### Step 2: Create Crash Monitor Window
|
|
||||||
**File:** `electron/handlers/crash-monitor.ts`
|
|
||||||
- Separate BrowserWindow (hidden by default)
|
|
||||||
- Listens for crash events from main process
|
|
||||||
- Shows custom crash dialog UI
|
|
||||||
- Can be opened from developer tools
|
|
||||||
|
|
||||||
### Step 3: Create Log Viewer Window
|
|
||||||
**File:** `electron/handlers/log-viewer.ts`
|
|
||||||
- Separate BrowserWindow for viewing logs
|
|
||||||
- Real-time log streaming via IPC
|
|
||||||
- Filter by log level (error, warn, info)
|
|
||||||
- Export logs functionality
|
|
||||||
|
|
||||||
### Step 4: Create Crash Dialog UI
|
|
||||||
**File:** `src/components/crash/CrashDialog.tsx`
|
|
||||||
- Custom styled crash dialog
|
|
||||||
- Shows error details, stack trace
|
|
||||||
- Options: Restart, View Logs, Close
|
|
||||||
- Uses existing UI components (shadcn/ui)
|
|
||||||
|
|
||||||
### Step 5: Create Log Viewer UI
|
|
||||||
**File:** `src/components/crash/LogViewer.tsx`
|
|
||||||
- Log list with syntax highlighting
|
|
||||||
- Search/filter functionality
|
|
||||||
- Real-time updates
|
|
||||||
|
|
||||||
### Step 6: Update Main Process
|
|
||||||
**File:** `electron/main.ts`
|
|
||||||
- Initialize crash logger early
|
|
||||||
- Set up crash event handlers
|
|
||||||
- Create crash monitor window
|
|
||||||
|
|
||||||
### Step 7: Update Preload Script
|
|
||||||
**File:** `electron/preload.ts`
|
|
||||||
- Add renderer error capture
|
|
||||||
- Expose crash-related IPC methods
|
|
||||||
|
|
||||||
## 3. Key Features
|
|
||||||
|
|
||||||
1. **Crash Detection:**
|
|
||||||
- Renderer crashes (`render-process-gone`)
|
|
||||||
- Main process errors (`uncaughtException`, `unhandledRejection`)
|
|
||||||
- GPU/utility crashes (`child-process-gone`)
|
|
||||||
- Unresponsive detection (`unresponsive` event)
|
|
||||||
|
|
||||||
2. **Crash Dialog:**
|
|
||||||
- Custom styled UI matching launcher theme
|
|
||||||
- Error message and stack trace display
|
|
||||||
- One-click restart option
|
|
||||||
- View logs option
|
|
||||||
|
|
||||||
3. **Log Viewer:**
|
|
||||||
- Can be opened from developer tools
|
|
||||||
- Real-time log streaming
|
|
||||||
- Search and filter capabilities
|
|
||||||
- Export functionality
|
|
||||||
|
|
||||||
4. **Process Isolation:**
|
|
||||||
- Crash monitor runs in separate window
|
|
||||||
- Main window crashes don't affect monitor
|
|
||||||
- Monitor can restart main window
|
|
||||||
|
|
||||||
## 4. Files to Create/Modify
|
|
||||||
|
|
||||||
**New Files:**
|
|
||||||
- `electron/core/crash-logger.ts`
|
|
||||||
- `electron/handlers/crash-monitor.ts`
|
|
||||||
- `electron/handlers/log-viewer.ts`
|
|
||||||
- `src/components/crash/CrashDialog.tsx`
|
|
||||||
- `src/components/crash/LogViewer.tsx`
|
|
||||||
- `src/pages/crash/index.tsx`
|
|
||||||
|
|
||||||
**Modified Files:**
|
|
||||||
- `electron/main.ts` - Add crash monitoring initialization
|
|
||||||
- `electron/preload.ts` - Add error capture and IPC methods
|
|
||||||
- `src/types/electron.d.ts` - Add new IPC method types
|
|
||||||
|
|
||||||
## 5. UI Design
|
|
||||||
|
|
||||||
The crash dialog and log viewer will use the existing design system:
|
|
||||||
- Glass effects with backdrop-filter
|
|
||||||
- Dark theme compatible
|
|
||||||
- Consistent spacing and typography
|
|
||||||
- Uses shadcn/ui components where appropriate
|
|
||||||
|
|
||||||
## 6. Testing Plan
|
|
||||||
|
|
||||||
1. Test crash detection by triggering intentional errors
|
|
||||||
2. Verify crash dialog appears correctly
|
|
||||||
3. Test log viewer functionality
|
|
||||||
4. Verify main window can be restarted from crash dialog
|
|
||||||
5. Test log export functionality
|
|
||||||
|
|
||||||
## 7. Questions for User
|
|
||||||
|
|
||||||
1. What specific crash scenarios should we prioritize?
|
|
||||||
2. Do you want the log viewer always accessible or only in dev mode?
|
|
||||||
3. Should crash reports be sent to a server or just stored locally?
|
|
||||||
4. Any specific UI preferences for the crash dialog beyond the existing design system?
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
oobe: false
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Koring Launcher - Crash</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/crash.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+11
-1
@@ -7,7 +7,7 @@ const { app } = electron;
|
|||||||
const CONFIG_FILE = 'Koring.yml';
|
const CONFIG_FILE = 'Koring.yml';
|
||||||
const CURRENT_VERSION = 1;
|
const CURRENT_VERSION = 1;
|
||||||
|
|
||||||
function configPath(): string {
|
export function configPath(): string {
|
||||||
if (app.isPackaged) {
|
if (app.isPackaged) {
|
||||||
return path.join(path.dirname(app.getPath('exe')), CONFIG_FILE);
|
return path.join(path.dirname(app.getPath('exe')), CONFIG_FILE);
|
||||||
}
|
}
|
||||||
@@ -76,6 +76,7 @@ export interface NetworkConfig {
|
|||||||
|
|
||||||
export interface AppConfig {
|
export interface AppConfig {
|
||||||
version: number;
|
version: number;
|
||||||
|
oobe: boolean;
|
||||||
theme: ThemeConfig;
|
theme: ThemeConfig;
|
||||||
a11y: A11yConfig;
|
a11y: A11yConfig;
|
||||||
background: BackgroundConfig;
|
background: BackgroundConfig;
|
||||||
@@ -88,6 +89,7 @@ export interface AppConfig {
|
|||||||
|
|
||||||
const DEFAULTS: AppConfig = {
|
const DEFAULTS: AppConfig = {
|
||||||
version: CURRENT_VERSION,
|
version: CURRENT_VERSION,
|
||||||
|
oobe: true,
|
||||||
theme: { darkMode: 'auto', parallax: true },
|
theme: { darkMode: 'auto', parallax: true },
|
||||||
a11y: { reduceMotion: false, reduceTransparency: false, highContrast: false, contentBlurOpacity: 50 },
|
a11y: { reduceMotion: false, reduceTransparency: false, highContrast: false, contentBlurOpacity: 50 },
|
||||||
background: { bgType: 'image', image: '/background.png', blur: 0, opacity: 100 },
|
background: { bgType: 'image', image: '/background.png', blur: 0, opacity: 100 },
|
||||||
@@ -129,6 +131,14 @@ function diffValue(full: unknown, defaultVal: unknown): unknown {
|
|||||||
return Object.keys(result).length === 0 ? undefined : result;
|
return Object.keys(result).length === 0 ? undefined : result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function configExists(): boolean {
|
||||||
|
try {
|
||||||
|
return fs.existsSync(configPath());
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function loadConfig(): AppConfig {
|
export function loadConfig(): AppConfig {
|
||||||
const filePath = configPath();
|
const filePath = configPath();
|
||||||
if (!fs.existsSync(filePath)) {
|
if (!fs.existsSync(filePath)) {
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
"use strict";
|
||||||
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||||
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||||
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||||
|
}
|
||||||
|
Object.defineProperty(o, k2, desc);
|
||||||
|
}) : (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
o[k2] = m[k];
|
||||||
|
}));
|
||||||
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||||
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||||
|
}) : function(o, v) {
|
||||||
|
o["default"] = v;
|
||||||
|
});
|
||||||
|
var __importStar = (this && this.__importStar) || (function () {
|
||||||
|
var ownKeys = function(o) {
|
||||||
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||||
|
var ar = [];
|
||||||
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||||
|
return ar;
|
||||||
|
};
|
||||||
|
return ownKeys(o);
|
||||||
|
};
|
||||||
|
return function (mod) {
|
||||||
|
if (mod && mod.__esModule) return mod;
|
||||||
|
var result = {};
|
||||||
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||||
|
__setModuleDefault(result, mod);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.writeCrashLog = writeCrashLog;
|
||||||
|
exports.readCrashLog = readCrashLog;
|
||||||
|
exports.clearCrashLog = clearCrashLog;
|
||||||
|
const fs = __importStar(require("fs"));
|
||||||
|
const path = __importStar(require("path"));
|
||||||
|
const electron_1 = __importDefault(require("electron"));
|
||||||
|
const { app } = electron_1.default;
|
||||||
|
const LOG_FILE = 'koring-crash.log';
|
||||||
|
const MAX_LINES = 1000;
|
||||||
|
function logPath() {
|
||||||
|
if (app.isPackaged) {
|
||||||
|
return path.join(path.dirname(app.getPath('exe')), LOG_FILE);
|
||||||
|
}
|
||||||
|
return path.join(__dirname, '..', LOG_FILE);
|
||||||
|
}
|
||||||
|
function trimFile(filePath) {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(filePath))
|
||||||
|
return;
|
||||||
|
const lines = fs.readFileSync(filePath, 'utf-8').split('\n').filter(Boolean);
|
||||||
|
if (lines.length > MAX_LINES) {
|
||||||
|
fs.writeFileSync(filePath, lines.slice(-MAX_LINES).join('\n') + '\n', 'utf-8');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
function writeCrashLog(entry) {
|
||||||
|
const filePath = logPath();
|
||||||
|
try {
|
||||||
|
const line = JSON.stringify(entry) + '\n';
|
||||||
|
fs.appendFileSync(filePath, line, 'utf-8');
|
||||||
|
trimFile(filePath);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
function readCrashLog() {
|
||||||
|
const filePath = logPath();
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(filePath))
|
||||||
|
return '';
|
||||||
|
return fs.readFileSync(filePath, 'utf-8');
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function clearCrashLog() {
|
||||||
|
const filePath = logPath();
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
fs.writeFileSync(filePath, '', 'utf-8');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import electron from 'electron';
|
||||||
|
const { app } = electron;
|
||||||
|
|
||||||
|
const LOG_FILE = 'koring-crash.log';
|
||||||
|
const MAX_LINES = 1000;
|
||||||
|
|
||||||
|
function logPath(): string {
|
||||||
|
if (app.isPackaged) {
|
||||||
|
return path.join(path.dirname(app.getPath('exe')), LOG_FILE);
|
||||||
|
}
|
||||||
|
return path.join(__dirname, '..', LOG_FILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CrashEntry {
|
||||||
|
timestamp: string;
|
||||||
|
type: 'renderer-gone' | 'unresponsive' | 'uncaught-exception' | 'unhandled-rejection' | 'child-process-gone' | 'test';
|
||||||
|
message: string;
|
||||||
|
stack?: string;
|
||||||
|
details?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function trimFile(filePath: string): void {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(filePath)) return;
|
||||||
|
const lines = fs.readFileSync(filePath, 'utf-8').split('\n').filter(Boolean);
|
||||||
|
if (lines.length > MAX_LINES) {
|
||||||
|
fs.writeFileSync(filePath, lines.slice(-MAX_LINES).join('\n') + '\n', 'utf-8');
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeCrashLog(entry: CrashEntry): void {
|
||||||
|
const filePath = logPath();
|
||||||
|
try {
|
||||||
|
const line = JSON.stringify(entry) + '\n';
|
||||||
|
fs.appendFileSync(filePath, line, 'utf-8');
|
||||||
|
trimFile(filePath);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readCrashLog(): string {
|
||||||
|
const filePath = logPath();
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(filePath)) return '';
|
||||||
|
return fs.readFileSync(filePath, 'utf-8');
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearCrashLog(): void {
|
||||||
|
const filePath = logPath();
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
fs.writeFileSync(filePath, '', 'utf-8');
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
@@ -1,11 +1,45 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||||
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||||
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||||
|
}
|
||||||
|
Object.defineProperty(o, k2, desc);
|
||||||
|
}) : (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
o[k2] = m[k];
|
||||||
|
}));
|
||||||
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||||
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||||
|
}) : function(o, v) {
|
||||||
|
o["default"] = v;
|
||||||
|
});
|
||||||
|
var __importStar = (this && this.__importStar) || (function () {
|
||||||
|
var ownKeys = function(o) {
|
||||||
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||||
|
var ar = [];
|
||||||
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||||
|
return ar;
|
||||||
|
};
|
||||||
|
return ownKeys(o);
|
||||||
|
};
|
||||||
|
return function (mod) {
|
||||||
|
if (mod && mod.__esModule) return mod;
|
||||||
|
var result = {};
|
||||||
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||||
|
__setModuleDefault(result, mod);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
})();
|
||||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
};
|
};
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.registerConfigHandlers = registerConfigHandlers;
|
exports.registerConfigHandlers = registerConfigHandlers;
|
||||||
const electron_1 = __importDefault(require("electron"));
|
const electron_1 = __importDefault(require("electron"));
|
||||||
const { ipcMain } = electron_1.default;
|
const { ipcMain, app } = electron_1.default;
|
||||||
|
const fs = __importStar(require("fs"));
|
||||||
const config_1 = require("../config");
|
const config_1 = require("../config");
|
||||||
function registerConfigHandlers() {
|
function registerConfigHandlers() {
|
||||||
ipcMain.handle('config:get', () => {
|
ipcMain.handle('config:get', () => {
|
||||||
@@ -26,4 +60,18 @@ function registerConfigHandlers() {
|
|||||||
return { success: false, data: null, error: String(e) };
|
return { success: false, data: null, error: String(e) };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
ipcMain.handle('config:reset', () => {
|
||||||
|
try {
|
||||||
|
const filePath = (0, config_1.configPath)();
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
}
|
||||||
|
app.relaunch();
|
||||||
|
app.exit(0);
|
||||||
|
return { success: true, data: null, error: null };
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
return { success: false, data: null, error: String(e) };
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import electron from 'electron';
|
import electron from 'electron';
|
||||||
const { ipcMain } = electron;
|
const { ipcMain, app } = electron;
|
||||||
import { loadConfig, saveConfig, type AppConfig } from '../config';
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { loadConfig, saveConfig, type AppConfig, configPath } from '../config';
|
||||||
|
|
||||||
export function registerConfigHandlers() {
|
export function registerConfigHandlers() {
|
||||||
ipcMain.handle('config:get', () => {
|
ipcMain.handle('config:get', () => {
|
||||||
@@ -20,4 +22,18 @@ export function registerConfigHandlers() {
|
|||||||
return { success: false, data: null, error: String(e) };
|
return { success: false, data: null, error: String(e) };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('config:reset', () => {
|
||||||
|
try {
|
||||||
|
const filePath = configPath();
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
}
|
||||||
|
app.relaunch();
|
||||||
|
app.exit(0);
|
||||||
|
return { success: true, data: null, error: null };
|
||||||
|
} catch (e: unknown) {
|
||||||
|
return { success: false, data: null, error: String(e) };
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
"use strict";
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.setupCrashListeners = setupCrashListeners;
|
||||||
|
exports.registerCrashHandlers = registerCrashHandlers;
|
||||||
|
exports.testCrashDialog = testCrashDialog;
|
||||||
|
const electron_1 = __importDefault(require("electron"));
|
||||||
|
const path_1 = __importDefault(require("path"));
|
||||||
|
const fs_1 = __importDefault(require("fs"));
|
||||||
|
const crash_logger_1 = require("../core/crash-logger");
|
||||||
|
const { app, ipcMain, BrowserWindow } = electron_1.default;
|
||||||
|
const isDev = !app.isPackaged;
|
||||||
|
let crashWin = null;
|
||||||
|
function getIconPath() {
|
||||||
|
return path_1.default.join(__dirname, '../../build/icon.ico');
|
||||||
|
}
|
||||||
|
function createCrashWindow() {
|
||||||
|
const iconPath = getIconPath();
|
||||||
|
const win = new BrowserWindow({
|
||||||
|
width: 600,
|
||||||
|
height: 460,
|
||||||
|
minWidth: 500,
|
||||||
|
maxWidth: 700,
|
||||||
|
minHeight: 460,
|
||||||
|
maxHeight: 460,
|
||||||
|
frame: false,
|
||||||
|
transparent: false,
|
||||||
|
resizable: false,
|
||||||
|
show: false,
|
||||||
|
icon: iconPath,
|
||||||
|
webPreferences: {
|
||||||
|
preload: path_1.default.join(__dirname, '../preload-crash.js'),
|
||||||
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true,
|
||||||
|
sandbox: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (isDev) {
|
||||||
|
win.loadURL('http://localhost:1420/crash.html');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
win.loadFile(path_1.default.join(__dirname, '../../dist/crash.html'));
|
||||||
|
}
|
||||||
|
return win;
|
||||||
|
}
|
||||||
|
function sendToCrashWindow(data) {
|
||||||
|
if (!crashWin || crashWin.isDestroyed()) {
|
||||||
|
crashWin = createCrashWindow();
|
||||||
|
}
|
||||||
|
const send = () => {
|
||||||
|
crashWin?.show();
|
||||||
|
crashWin?.focus();
|
||||||
|
crashWin?.webContents.send('crash:show', {
|
||||||
|
type: data.type,
|
||||||
|
message: data.message,
|
||||||
|
timestamp: data.timestamp,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
if (crashWin.webContents.isLoading()) {
|
||||||
|
crashWin.webContents.once('did-finish-load', send);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
send();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function setupCrashListeners(mainWindow) {
|
||||||
|
mainWindow.webContents.on('render-process-gone', (_event, details) => {
|
||||||
|
const entry = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
type: 'renderer-gone',
|
||||||
|
message: `渲染进程崩溃: ${details.reason} (退出码: ${details.exitCode})`,
|
||||||
|
details: details,
|
||||||
|
};
|
||||||
|
(0, crash_logger_1.writeCrashLog)(entry);
|
||||||
|
sendToCrashWindow(entry);
|
||||||
|
});
|
||||||
|
mainWindow.on('unresponsive', () => {
|
||||||
|
const entry = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
type: 'unresponsive',
|
||||||
|
message: '渲染进程无响应',
|
||||||
|
};
|
||||||
|
(0, crash_logger_1.writeCrashLog)(entry);
|
||||||
|
sendToCrashWindow(entry);
|
||||||
|
});
|
||||||
|
// Inject devtools crash tools when devtools opens
|
||||||
|
if (isDev) {
|
||||||
|
mainWindow.webContents.on('devtools-opened', () => {
|
||||||
|
const js = `
|
||||||
|
(function() {
|
||||||
|
if (window.__crashToolsLoaded) return;
|
||||||
|
window.__crashToolsLoaded = true;
|
||||||
|
|
||||||
|
console.log('%c[崩溃工具] 已加载', 'color: #f59e0b; font-weight: bold; font-size: 14px;');
|
||||||
|
console.log('%c可用命令:', 'color: #3b82f6; font-weight: bold;');
|
||||||
|
console.log('%c crash.simulate() %c— 模拟渲染进程崩溃', 'color: #ef4444; font-weight: bold;', 'color: inherit;');
|
||||||
|
console.log('%c crash.testDialog() %c— 测试崩溃弹窗', 'color: #ef4444; font-weight: bold;', 'color: inherit;');
|
||||||
|
console.log('%c crash.readLog() %c— 读取崩溃日志', 'color: #ef4444; font-weight: bold;', 'color: inherit;');
|
||||||
|
console.log('%c crash.factoryReset()%c— 强还原配置', 'color: #ef4444; font-weight: bold;', 'color: inherit;');
|
||||||
|
console.log('%c crash.restart() %c— 重启应用', 'color: #ef4444; font-weight: bold;', 'color: inherit;');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
window.crash = {
|
||||||
|
simulate: function() { window.electronAPI?.simulateCrash(); },
|
||||||
|
testDialog: function() { window.electronAPI?.testCrashDialog(); },
|
||||||
|
readLog: function() { return window.electronAPI?.invoke('crash:readLog'); },
|
||||||
|
factoryReset: function() { return window.electronAPI?.invoke('crash:factoryReset'); },
|
||||||
|
restart: function() { return window.electronAPI?.invoke('crash:restart'); },
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
`;
|
||||||
|
mainWindow.webContents.executeJavaScript(js);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function registerCrashHandlers() {
|
||||||
|
// Close crash window
|
||||||
|
ipcMain.handle('crash:closeWindow', () => {
|
||||||
|
if (crashWin && !crashWin.isDestroyed()) {
|
||||||
|
crashWin.close();
|
||||||
|
crashWin = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Read crash log
|
||||||
|
ipcMain.handle('crash:readLog', () => {
|
||||||
|
return (0, crash_logger_1.readCrashLog)();
|
||||||
|
});
|
||||||
|
// Simulate crash — forcefully crash the renderer (devtools only)
|
||||||
|
ipcMain.handle('crash:simulate', (_event) => {
|
||||||
|
const senderWebContents = electron_1.default.webContents.getAllWebContents().find((wc) => wc.id === _event.sender.id);
|
||||||
|
if (senderWebContents) {
|
||||||
|
senderWebContents.forcefullyCrashRenderer();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Test crash dialog — show crash window without actually crashing
|
||||||
|
ipcMain.handle('crash:testDialog', () => {
|
||||||
|
testCrashDialog();
|
||||||
|
});
|
||||||
|
// Factory reset
|
||||||
|
ipcMain.handle('crash:factoryReset', () => {
|
||||||
|
const dataPath = app.isPackaged
|
||||||
|
? path_1.default.dirname(app.getPath('exe'))
|
||||||
|
: path_1.default.join(__dirname, '../..');
|
||||||
|
// Delete config
|
||||||
|
try {
|
||||||
|
const configPath = path_1.default.join(dataPath, 'Koring.yml');
|
||||||
|
if (fs_1.default.existsSync(configPath))
|
||||||
|
fs_1.default.unlinkSync(configPath);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
// Delete auth
|
||||||
|
try {
|
||||||
|
const authPath = path_1.default.join(dataPath, 'koring-auth.json');
|
||||||
|
if (fs_1.default.existsSync(authPath))
|
||||||
|
fs_1.default.unlinkSync(authPath);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
// Delete background cache in userData
|
||||||
|
try {
|
||||||
|
const bgPath = path_1.default.join(app.getPath('userData'), 'background.png');
|
||||||
|
if (fs_1.default.existsSync(bgPath))
|
||||||
|
fs_1.default.unlinkSync(bgPath);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
// Delete crash log
|
||||||
|
(0, crash_logger_1.clearCrashLog)();
|
||||||
|
return { success: true };
|
||||||
|
});
|
||||||
|
// Restart app
|
||||||
|
ipcMain.handle('crash:restart', () => {
|
||||||
|
app.relaunch();
|
||||||
|
app.exit(0);
|
||||||
|
});
|
||||||
|
// Main process error handlers
|
||||||
|
process.on('uncaughtException', (error) => {
|
||||||
|
const entry = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
type: 'uncaught-exception',
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
};
|
||||||
|
(0, crash_logger_1.writeCrashLog)(entry);
|
||||||
|
sendToCrashWindow(entry);
|
||||||
|
});
|
||||||
|
process.on('unhandledRejection', (reason) => {
|
||||||
|
const entry = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
type: 'unhandled-rejection',
|
||||||
|
message: String(reason),
|
||||||
|
stack: reason instanceof Error ? reason.stack : undefined,
|
||||||
|
};
|
||||||
|
(0, crash_logger_1.writeCrashLog)(entry);
|
||||||
|
sendToCrashWindow(entry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Called from devtools to test crash dialog
|
||||||
|
function testCrashDialog() {
|
||||||
|
const entry = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
type: 'test',
|
||||||
|
message: '这是一个测试崩溃弹窗',
|
||||||
|
};
|
||||||
|
sendToCrashWindow(entry);
|
||||||
|
}
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
import electron from 'electron';
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { writeCrashLog, readCrashLog, clearCrashLog, type CrashEntry } from '../core/crash-logger';
|
||||||
|
|
||||||
|
const { app, ipcMain, BrowserWindow } = electron;
|
||||||
|
|
||||||
|
const isDev = !app.isPackaged;
|
||||||
|
|
||||||
|
let crashWin: electron.BrowserWindow | null = null;
|
||||||
|
|
||||||
|
function getIconPath(): string {
|
||||||
|
return path.join(__dirname, '../../build/icon.ico');
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCrashWindow(): electron.BrowserWindow {
|
||||||
|
const iconPath = getIconPath();
|
||||||
|
|
||||||
|
const win = new BrowserWindow({
|
||||||
|
width: 600,
|
||||||
|
height: 460,
|
||||||
|
minWidth: 500,
|
||||||
|
maxWidth: 700,
|
||||||
|
minHeight: 460,
|
||||||
|
maxHeight: 460,
|
||||||
|
frame: false,
|
||||||
|
transparent: false,
|
||||||
|
resizable: false,
|
||||||
|
show: false,
|
||||||
|
icon: iconPath,
|
||||||
|
webPreferences: {
|
||||||
|
preload: path.join(__dirname, '../preload-crash.js'),
|
||||||
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true,
|
||||||
|
sandbox: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isDev) {
|
||||||
|
win.loadURL('http://localhost:1420/crash.html');
|
||||||
|
} else {
|
||||||
|
win.loadFile(path.join(__dirname, '../../dist/crash.html'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return win;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendToCrashWindow(data: CrashEntry) {
|
||||||
|
if (!crashWin || crashWin.isDestroyed()) {
|
||||||
|
crashWin = createCrashWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
const send = () => {
|
||||||
|
crashWin?.show();
|
||||||
|
crashWin?.focus();
|
||||||
|
crashWin?.webContents.send('crash:show', {
|
||||||
|
type: data.type,
|
||||||
|
message: data.message,
|
||||||
|
timestamp: data.timestamp,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (crashWin.webContents.isLoading()) {
|
||||||
|
crashWin.webContents.once('did-finish-load', send);
|
||||||
|
} else {
|
||||||
|
send();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setupCrashListeners(mainWindow: electron.BrowserWindow) {
|
||||||
|
mainWindow.webContents.on('render-process-gone', (_event: any, details: any) => {
|
||||||
|
const entry: CrashEntry = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
type: 'renderer-gone',
|
||||||
|
message: `渲染进程崩溃: ${details.reason} (退出码: ${details.exitCode})`,
|
||||||
|
details: details as unknown as Record<string, unknown>,
|
||||||
|
};
|
||||||
|
writeCrashLog(entry);
|
||||||
|
sendToCrashWindow(entry);
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWindow.on('unresponsive', () => {
|
||||||
|
const entry: CrashEntry = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
type: 'unresponsive',
|
||||||
|
message: '渲染进程无响应',
|
||||||
|
};
|
||||||
|
writeCrashLog(entry);
|
||||||
|
sendToCrashWindow(entry);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Inject devtools crash tools when devtools opens
|
||||||
|
if (isDev) {
|
||||||
|
mainWindow.webContents.on('devtools-opened', () => {
|
||||||
|
const js = `
|
||||||
|
(function() {
|
||||||
|
if (window.__crashToolsLoaded) return;
|
||||||
|
window.__crashToolsLoaded = true;
|
||||||
|
|
||||||
|
console.log('%c[崩溃工具] 已加载', 'color: #f59e0b; font-weight: bold; font-size: 14px;');
|
||||||
|
console.log('%c可用命令:', 'color: #3b82f6; font-weight: bold;');
|
||||||
|
console.log('%c crash.simulate() %c— 模拟渲染进程崩溃', 'color: #ef4444; font-weight: bold;', 'color: inherit;');
|
||||||
|
console.log('%c crash.testDialog() %c— 测试崩溃弹窗', 'color: #ef4444; font-weight: bold;', 'color: inherit;');
|
||||||
|
console.log('%c crash.readLog() %c— 读取崩溃日志', 'color: #ef4444; font-weight: bold;', 'color: inherit;');
|
||||||
|
console.log('%c crash.factoryReset()%c— 强还原配置', 'color: #ef4444; font-weight: bold;', 'color: inherit;');
|
||||||
|
console.log('%c crash.restart() %c— 重启应用', 'color: #ef4444; font-weight: bold;', 'color: inherit;');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
window.crash = {
|
||||||
|
simulate: function() { window.electronAPI?.simulateCrash(); },
|
||||||
|
testDialog: function() { window.electronAPI?.testCrashDialog(); },
|
||||||
|
readLog: function() { return window.electronAPI?.invoke('crash:readLog'); },
|
||||||
|
factoryReset: function() { return window.electronAPI?.invoke('crash:factoryReset'); },
|
||||||
|
restart: function() { return window.electronAPI?.invoke('crash:restart'); },
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
`;
|
||||||
|
mainWindow.webContents.executeJavaScript(js);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerCrashHandlers() {
|
||||||
|
// Close crash window
|
||||||
|
ipcMain.handle('crash:closeWindow', () => {
|
||||||
|
if (crashWin && !crashWin.isDestroyed()) {
|
||||||
|
crashWin.close();
|
||||||
|
crashWin = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Read crash log
|
||||||
|
ipcMain.handle('crash:readLog', () => {
|
||||||
|
return readCrashLog();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Simulate crash — forcefully crash the renderer (devtools only)
|
||||||
|
ipcMain.handle('crash:simulate', (_event) => {
|
||||||
|
const senderWebContents = electron.webContents.getAllWebContents().find(
|
||||||
|
(wc) => wc.id === _event.sender.id
|
||||||
|
);
|
||||||
|
if (senderWebContents) {
|
||||||
|
senderWebContents.forcefullyCrashRenderer();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test crash dialog — show crash window without actually crashing
|
||||||
|
ipcMain.handle('crash:testDialog', () => {
|
||||||
|
testCrashDialog();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Factory reset
|
||||||
|
ipcMain.handle('crash:factoryReset', () => {
|
||||||
|
const dataPath = app.isPackaged
|
||||||
|
? path.dirname(app.getPath('exe'))
|
||||||
|
: path.join(__dirname, '../..');
|
||||||
|
|
||||||
|
// Delete config
|
||||||
|
try {
|
||||||
|
const configPath = path.join(dataPath, 'Koring.yml');
|
||||||
|
if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
// Delete auth
|
||||||
|
try {
|
||||||
|
const authPath = path.join(dataPath, 'koring-auth.json');
|
||||||
|
if (fs.existsSync(authPath)) fs.unlinkSync(authPath);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
// Delete background cache in userData
|
||||||
|
try {
|
||||||
|
const bgPath = path.join(app.getPath('userData'), 'background.png');
|
||||||
|
if (fs.existsSync(bgPath)) fs.unlinkSync(bgPath);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
// Delete crash log
|
||||||
|
clearCrashLog();
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restart app
|
||||||
|
ipcMain.handle('crash:restart', () => {
|
||||||
|
app.relaunch();
|
||||||
|
app.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Main process error handlers
|
||||||
|
process.on('uncaughtException', (error) => {
|
||||||
|
const entry: CrashEntry = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
type: 'uncaught-exception',
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
};
|
||||||
|
writeCrashLog(entry);
|
||||||
|
sendToCrashWindow(entry);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('unhandledRejection', (reason) => {
|
||||||
|
const entry: CrashEntry = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
type: 'unhandled-rejection',
|
||||||
|
message: String(reason),
|
||||||
|
stack: reason instanceof Error ? reason.stack : undefined,
|
||||||
|
};
|
||||||
|
writeCrashLog(entry);
|
||||||
|
sendToCrashWindow(entry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called from devtools to test crash dialog
|
||||||
|
export function testCrashDialog() {
|
||||||
|
const entry: CrashEntry = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
type: 'test',
|
||||||
|
message: '这是一个测试崩溃弹窗',
|
||||||
|
};
|
||||||
|
sendToCrashWindow(entry);
|
||||||
|
}
|
||||||
+45
-2
@@ -1,5 +1,6 @@
|
|||||||
import electron from 'electron';
|
import electron from 'electron';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
import { registerConfigHandlers } from './handlers/config';
|
import { registerConfigHandlers } from './handlers/config';
|
||||||
import { registerAuthHandlers } from './handlers/auth';
|
import { registerAuthHandlers } from './handlers/auth';
|
||||||
import { registerInstallHandlers } from './handlers/install';
|
import { registerInstallHandlers } from './handlers/install';
|
||||||
@@ -10,17 +11,47 @@ import { registerBackgroundHandlers } from './handlers/background';
|
|||||||
import { registerTaskHandlers } from './handlers/task';
|
import { registerTaskHandlers } from './handlers/task';
|
||||||
import { registerSystemHandlers } from './handlers/system';
|
import { registerSystemHandlers } from './handlers/system';
|
||||||
import { registerWindowHandlers } from './handlers/window';
|
import { registerWindowHandlers } from './handlers/window';
|
||||||
|
import { registerCrashHandlers, setupCrashListeners, testCrashDialog } from './handlers/crash-monitor';
|
||||||
|
import { loadConfig, saveConfig, configExists } from './config';
|
||||||
|
|
||||||
const { app } = electron;
|
const { app } = electron;
|
||||||
|
|
||||||
const isDev = !app.isPackaged;
|
const isDev = !app.isPackaged;
|
||||||
|
|
||||||
// Mutable ref — handlers always read from this
|
// GPU acceleration flags
|
||||||
|
app.commandLine.appendSwitch('enable-gpu-rasterization');
|
||||||
|
app.commandLine.appendSwitch('enable-zero-copy');
|
||||||
|
|
||||||
const win: { mainWindow: electron.BrowserWindow | null; splashWindow: electron.BrowserWindow | null } = {
|
const win: { mainWindow: electron.BrowserWindow | null; splashWindow: electron.BrowserWindow | null } = {
|
||||||
mainWindow: null,
|
mainWindow: null,
|
||||||
splashWindow: null,
|
splashWindow: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Startup checks: .minecraft dir + config file + first launch detection
|
||||||
|
function runStartupChecks(): { isFirstLaunch: boolean; config: ReturnType<typeof loadConfig> } {
|
||||||
|
const dataPath = app.isPackaged
|
||||||
|
? path.dirname(app.getPath('exe'))
|
||||||
|
: path.join(__dirname, '..');
|
||||||
|
|
||||||
|
// 1. Ensure .minecraft directory exists
|
||||||
|
const minecraftDir = path.join(dataPath, '.minecraft');
|
||||||
|
if (!fs.existsSync(minecraftDir)) {
|
||||||
|
fs.mkdirSync(minecraftDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check if config exists, if not create with defaults
|
||||||
|
const hasConfig = configExists();
|
||||||
|
const isFirstLaunch = !hasConfig;
|
||||||
|
|
||||||
|
// 3. Load (or create) config
|
||||||
|
const config = loadConfig();
|
||||||
|
if (isFirstLaunch) {
|
||||||
|
saveConfig(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isFirstLaunch, config };
|
||||||
|
}
|
||||||
|
|
||||||
function createSplashWindow(): electron.BrowserWindow {
|
function createSplashWindow(): electron.BrowserWindow {
|
||||||
const iconPath = isDev
|
const iconPath = isDev
|
||||||
? path.join(__dirname, '../build/icon.ico')
|
? path.join(__dirname, '../build/icon.ico')
|
||||||
@@ -101,18 +132,27 @@ function registerAllHandlers() {
|
|||||||
registerTaskHandlers(win);
|
registerTaskHandlers(win);
|
||||||
registerSystemHandlers();
|
registerSystemHandlers();
|
||||||
registerWindowHandlers(win);
|
registerWindowHandlers(win);
|
||||||
|
registerCrashHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
registerAllHandlers();
|
registerAllHandlers();
|
||||||
|
|
||||||
|
// Run startup checks before creating windows
|
||||||
|
const { isFirstLaunch, config } = runStartupChecks();
|
||||||
|
|
||||||
// 1. Show splash immediately
|
// 1. Show splash immediately
|
||||||
win.splashWindow = createSplashWindow();
|
win.splashWindow = createSplashWindow();
|
||||||
|
|
||||||
// 2. Create main window in background
|
// 2. Create main window in background
|
||||||
win.mainWindow = createMainWindow();
|
win.mainWindow = createMainWindow();
|
||||||
|
|
||||||
// 3. When main window finishes loading, wait a minimum time then transition
|
// 3. Preload config into renderer before it renders
|
||||||
|
win.mainWindow.webContents.on('did-finish-load', () => {
|
||||||
|
win.mainWindow?.webContents.send('config:preload', { config, isFirstLaunch });
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. When main window finishes loading, wait a minimum time then transition
|
||||||
let mainReady = false;
|
let mainReady = false;
|
||||||
let splashMinTimeDone = false;
|
let splashMinTimeDone = false;
|
||||||
|
|
||||||
@@ -134,6 +174,9 @@ app.whenReady().then(() => {
|
|||||||
tryTransition();
|
tryTransition();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Setup crash listeners on main window
|
||||||
|
setupCrashListeners(win.mainWindow);
|
||||||
|
|
||||||
// Minimum splash display time (1.5s)
|
// Minimum splash display time (1.5s)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
splashMinTimeDone = true;
|
splashMinTimeDone = true;
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import electron from 'electron';
|
||||||
|
const { contextBridge, ipcRenderer } = electron;
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
|
invoke: (channel: string, ...args: unknown[]) =>
|
||||||
|
ipcRenderer.invoke(channel, ...args),
|
||||||
|
|
||||||
|
on: (channel: string, callback: (...args: unknown[]) => void) => {
|
||||||
|
const handler = (_event: Electron.IpcRendererEvent, ...args: unknown[]) => callback(...args);
|
||||||
|
ipcRenderer.on(channel, handler);
|
||||||
|
return () => ipcRenderer.removeListener(channel, handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
send: (channel: string, ...args: unknown[]) =>
|
||||||
|
ipcRenderer.send(channel, ...args),
|
||||||
|
|
||||||
|
minimize: () => ipcRenderer.invoke('window:minimize'),
|
||||||
|
maximize: () => ipcRenderer.invoke('window:maximize'),
|
||||||
|
close: () => ipcRenderer.invoke('crash:closeWindow'),
|
||||||
|
isMaximized: () => ipcRenderer.invoke('window:isMaximized'),
|
||||||
|
onResized: (callback: () => void) => {
|
||||||
|
const handler = () => callback();
|
||||||
|
ipcRenderer.on('window:resized', handler);
|
||||||
|
return () => ipcRenderer.removeListener('window:resized', handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
getTheme: () => ipcRenderer.invoke('window:getTheme'),
|
||||||
|
openExternal: (url: string) => ipcRenderer.invoke('shell:openExternal', url),
|
||||||
|
});
|
||||||
@@ -51,6 +51,13 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
// Theme
|
// Theme
|
||||||
getTheme: () => ipcRenderer.invoke('window:getTheme'),
|
getTheme: () => ipcRenderer.invoke('window:getTheme'),
|
||||||
|
|
||||||
|
// Config preloading
|
||||||
|
onConfigPreload: (callback: (data: { config: unknown; isFirstLaunch: boolean }) => void) => {
|
||||||
|
const handler = (_event: Electron.IpcRendererEvent, data: { config: unknown; isFirstLaunch: boolean }) => callback(data);
|
||||||
|
ipcRenderer.on('config:preload', handler);
|
||||||
|
return () => ipcRenderer.removeListener('config:preload', handler);
|
||||||
|
},
|
||||||
|
|
||||||
// Background image — pick file, copy to userData, return base64 data URL
|
// Background image — pick file, copy to userData, return base64 data URL
|
||||||
pickBackgroundImage: async (): Promise<string | null> => {
|
pickBackgroundImage: async (): Promise<string | null> => {
|
||||||
const result = await ipcRenderer.invoke('dialog:openFile', {
|
const result = await ipcRenderer.invoke('dialog:openFile', {
|
||||||
@@ -73,4 +80,11 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
|
|
||||||
// Open external URL in system browser
|
// Open external URL in system browser
|
||||||
openExternal: (url: string) => ipcRenderer.invoke('shell:openExternal', url),
|
openExternal: (url: string) => ipcRenderer.invoke('shell:openExternal', url),
|
||||||
|
|
||||||
|
// Crash monitoring — devtools only
|
||||||
|
simulateCrash: () => ipcRenderer.invoke('crash:simulate'),
|
||||||
|
testCrashDialog: () => ipcRenderer.invoke('crash:testDialog'),
|
||||||
|
|
||||||
|
// Config reset
|
||||||
|
resetConfig: () => ipcRenderer.invoke('config:reset'),
|
||||||
});
|
});
|
||||||
|
|||||||
+2
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "koring-launcher",
|
"name": "koring-launcher",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"description": "Koring Launcher - Minecraft launcher built with Electron + React",
|
"description": "Koring Launcher - Minecraft launcher built with Electron + React",
|
||||||
"author": "Shenzhen Lingke Network Technology Co., Ltd.",
|
"author": "Shenzhen Lingke Network Technology Co., Ltd.",
|
||||||
"license": "LL-1.0",
|
"license": "LL-1.0",
|
||||||
@@ -43,6 +43,7 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"lucide-react": "^1.21.0",
|
"lucide-react": "^1.21.0",
|
||||||
|
"motion": "^12.42.2",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
|
|||||||
Generated
+60
@@ -41,6 +41,9 @@ importers:
|
|||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^1.21.0
|
specifier: ^1.21.0
|
||||||
version: 1.21.0(react@19.2.7)
|
version: 1.21.0(react@19.2.7)
|
||||||
|
motion:
|
||||||
|
specifier: ^12.42.2
|
||||||
|
version: 12.42.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
next-themes:
|
next-themes:
|
||||||
specifier: ^0.4.6
|
specifier: ^0.4.6
|
||||||
version: 0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
@@ -1937,6 +1940,20 @@ packages:
|
|||||||
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
framer-motion@12.42.2:
|
||||||
|
resolution: {integrity: sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@emotion/is-prop-valid': '*'
|
||||||
|
react: ^18.0.0 || ^19.0.0
|
||||||
|
react-dom: ^18.0.0 || ^19.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@emotion/is-prop-valid':
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
|
||||||
fresh@2.0.0:
|
fresh@2.0.0:
|
||||||
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
|
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -2644,6 +2661,26 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
motion-dom@12.42.2:
|
||||||
|
resolution: {integrity: sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA==}
|
||||||
|
|
||||||
|
motion-utils@12.39.0:
|
||||||
|
resolution: {integrity: sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==}
|
||||||
|
|
||||||
|
motion@12.42.2:
|
||||||
|
resolution: {integrity: sha512-Atvv11yUKIid41cVrRBDVX5m8tF8kNpExRSlbpt6APClhDjtwQssgFHhQzejxw7/7YYbjHSPKBVbHo05BuJT5Q==}
|
||||||
|
peerDependencies:
|
||||||
|
'@emotion/is-prop-valid': '*'
|
||||||
|
react: ^18.0.0 || ^19.0.0
|
||||||
|
react-dom: ^18.0.0 || ^19.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@emotion/is-prop-valid':
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
|
||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
@@ -5492,6 +5529,15 @@ snapshots:
|
|||||||
|
|
||||||
forwarded@0.2.0: {}
|
forwarded@0.2.0: {}
|
||||||
|
|
||||||
|
framer-motion@12.42.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
||||||
|
dependencies:
|
||||||
|
motion-dom: 12.42.2
|
||||||
|
motion-utils: 12.39.0
|
||||||
|
tslib: 2.8.1
|
||||||
|
optionalDependencies:
|
||||||
|
react: 19.2.7
|
||||||
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
|
|
||||||
fresh@2.0.0: {}
|
fresh@2.0.0: {}
|
||||||
|
|
||||||
fs-constants@1.0.0: {}
|
fs-constants@1.0.0: {}
|
||||||
@@ -6132,6 +6178,20 @@ snapshots:
|
|||||||
|
|
||||||
mkdirp@1.0.4: {}
|
mkdirp@1.0.4: {}
|
||||||
|
|
||||||
|
motion-dom@12.42.2:
|
||||||
|
dependencies:
|
||||||
|
motion-utils: 12.39.0
|
||||||
|
|
||||||
|
motion-utils@12.39.0: {}
|
||||||
|
|
||||||
|
motion@12.42.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
||||||
|
dependencies:
|
||||||
|
framer-motion: 12.42.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
|
tslib: 2.8.1
|
||||||
|
optionalDependencies:
|
||||||
|
react: 19.2.7
|
||||||
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
nanoid@3.3.13: {}
|
nanoid@3.3.13: {}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 216 KiB |
@@ -0,0 +1,20 @@
|
|||||||
|
# v1.0.1 2607120117 beta 更新内容
|
||||||
|
|
||||||
|
## 新增
|
||||||
|
- 全新启动器界面设计
|
||||||
|
- 实例管理系统
|
||||||
|
- 启动动画(Silk Shader)
|
||||||
|
- 自定义背景图片
|
||||||
|
- 深色/浅色主题切换
|
||||||
|
- 多语言支持界面
|
||||||
|
- 崩溃监控与日志系统
|
||||||
|
- 任务队列管理
|
||||||
|
|
||||||
|
## 优化
|
||||||
|
- GPU 加速渲染
|
||||||
|
- 配置文件持久化(YAML 格式)
|
||||||
|
- 窗口控制自定义实现
|
||||||
|
- 首次启动引导流程
|
||||||
|
|
||||||
|
## 修复
|
||||||
|
- 修复已知问题
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
Koring APP Beta 测试协议
|
||||||
|
|
||||||
|
更新日期:2026年6月23日
|
||||||
|
|
||||||
|
感谢您参与 Koring Launcher Beta 测试!在参与测试前,请您仔细阅读本协议。
|
||||||
|
|
||||||
|
一、测试目的
|
||||||
|
1.1 Beta 测试旨在收集用户反馈,发现并修复软件中的问题,提升产品质量。
|
||||||
|
1.2 Beta 版本可能包含尚未完善的的功能和未修复的 Bug。
|
||||||
|
|
||||||
|
二、测试风险
|
||||||
|
2.1 Beta 版本不稳定,可能导致数据丢失或游戏异常。
|
||||||
|
2.2 您应自行备份重要数据,我们不对 Beta 测试期间的数据丢失负责。
|
||||||
|
2.3 Beta 版本可能与某些模组或配置不兼容。
|
||||||
|
|
||||||
|
三、反馈义务
|
||||||
|
3.1 您同意在使用 Beta 版本期间,如发现问题及时向我们反馈。
|
||||||
|
3.2 您的反馈将帮助我们改进产品,我们可能会在产品中引用您的反馈内容。
|
||||||
|
|
||||||
|
四、保密义务
|
||||||
|
4.1 您不得公开传播 Beta 版本的截图、视频或未公开的功能信息。
|
||||||
|
4.2 未经授权,您不得将 Beta 版本分享给他人。
|
||||||
|
|
||||||
|
五、协议终止
|
||||||
|
5.1 我们有权随时终止 Beta 测试项目。
|
||||||
|
5.2 您可以随时退出 Beta 测试计划。
|
||||||
|
|
||||||
|
六、联系方式
|
||||||
|
如您对本协议有任何疑问,请联系我们:
|
||||||
|
邮箱:koring-app-beta@lenjing.email
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
Koring Team 产品用户协议
|
||||||
|
|
||||||
|
更新日期:2026年7月4日
|
||||||
|
生效日期:2026年6月1日
|
||||||
|
|
||||||
|
欢迎您使用 Koring Launcher!在使用我们的产品和服务之前,请您仔细阅读本协议。
|
||||||
|
|
||||||
|
一、服务条款的接受
|
||||||
|
1.1 本协议是您与深圳领科网络科技有限公司(以下简称"我们")之间关于使用 Koring Launcher 软件及相关服务所订立的协议。
|
||||||
|
1.2 您一旦注册、登录、使用本软件及相关服务,即视为您已充分理解并同意本协议的全部内容。
|
||||||
|
|
||||||
|
二、服务内容
|
||||||
|
2.1 Koring Launcher 是一款 Minecraft 启动器,为您提供游戏版本管理、模组管理、实例管理等功能。
|
||||||
|
2.2 我们保留随时变更、暂停或终止部分或全部服务的权利。
|
||||||
|
|
||||||
|
三、用户行为规范
|
||||||
|
3.1 您在使用本软件时,应遵守中华人民共和国相关法律法规。
|
||||||
|
3.2 您不得利用本软件从事任何违法违规活动。
|
||||||
|
3.3 您应妥善保管您的账户信息,因您的原因导致的账户安全问题,我们不承担责任。
|
||||||
|
|
||||||
|
四、知识产权
|
||||||
|
4.1 本软件的所有知识产权归深圳领科网络科技有限公司所有。
|
||||||
|
4.2 未经授权,您不得复制、修改、传播本软件的任何部分。
|
||||||
|
|
||||||
|
五、免责声明
|
||||||
|
5.1 本软件按"现状"提供服务,我们不对其适用性、准确性做出任何明示或暗示的保证。
|
||||||
|
5.2 对于因不可抗力或非我们可控的原因导致的服务中断或数据丢失,我们不承担责任。
|
||||||
|
|
||||||
|
六、协议修改
|
||||||
|
6.1 我们有权根据需要不时修改本协议,并在本软件中公布。
|
||||||
|
6.2 修改后的协议一经公布即替代原来的协议。您应定期查阅本协议。
|
||||||
|
|
||||||
|
七、联系方式
|
||||||
|
如您对本协议有任何疑问,请联系我们:
|
||||||
|
邮箱:support-koring-app@lenjing.email
|
||||||
+23
-1
@@ -7,6 +7,7 @@ import { syncThemeFromConfig } from "./stores/themeStore";
|
|||||||
import { syncA11yFromConfig } from "./stores/a11yStore";
|
import { syncA11yFromConfig } from "./stores/a11yStore";
|
||||||
import { syncBackgroundFromConfig } from "./stores/backgroundStore";
|
import { syncBackgroundFromConfig } from "./stores/backgroundStore";
|
||||||
import { useAuthStore } from "./stores/authStore";
|
import { useAuthStore } from "./stores/authStore";
|
||||||
|
import type { AppConfig } from "./api/config";
|
||||||
import { Home } from "./pages/home";
|
import { Home } from "./pages/home";
|
||||||
import { Store } from "./pages/store";
|
import { Store } from "./pages/store";
|
||||||
import { Today } from "./pages/today";
|
import { Today } from "./pages/today";
|
||||||
@@ -19,7 +20,13 @@ import { SplashDebug } from "./pages/debug/splash-debug";
|
|||||||
import { DisplayDebug } from "./pages/debug/display-debug";
|
import { DisplayDebug } from "./pages/debug/display-debug";
|
||||||
import { VersionCardDebug } from "./pages/debug/version-card-debug";
|
import { VersionCardDebug } from "./pages/debug/version-card-debug";
|
||||||
import { TaskDebug } from "./pages/debug/task-debug";
|
import { TaskDebug } from "./pages/debug/task-debug";
|
||||||
|
import { CrashDebug } from "./pages/debug/crash-debug";
|
||||||
import { Oobe } from "./pages/oobe";
|
import { Oobe } from "./pages/oobe";
|
||||||
|
import { OobeLanguage } from "./pages/oobe/step-language";
|
||||||
|
import { OobeAgreement } from "./pages/oobe/step-agreement";
|
||||||
|
import { OobeVersion } from "./pages/oobe/step-version";
|
||||||
|
import { OobeBetaTest } from "./pages/oobe/step-beta-test";
|
||||||
|
import { OobeFinish } from "./pages/oobe/step-finish";
|
||||||
import { OobeAboutInfo } from "./pages/oobe/about-info";
|
import { OobeAboutInfo } from "./pages/oobe/about-info";
|
||||||
|
|
||||||
const pageMap = {
|
const pageMap = {
|
||||||
@@ -31,12 +38,18 @@ const pageMap = {
|
|||||||
gallery: Gallery,
|
gallery: Gallery,
|
||||||
"task-queue": TaskQueue,
|
"task-queue": TaskQueue,
|
||||||
oobe: Oobe,
|
oobe: Oobe,
|
||||||
|
"oobe/language": OobeLanguage,
|
||||||
|
"oobe/agreement": OobeAgreement,
|
||||||
|
"oobe/version": OobeVersion,
|
||||||
|
"oobe/beta-test": OobeBetaTest,
|
||||||
|
"oobe/finish": OobeFinish,
|
||||||
"oobe/about-info": OobeAboutInfo,
|
"oobe/about-info": OobeAboutInfo,
|
||||||
debug: Debug,
|
debug: Debug,
|
||||||
"debug-splash": SplashDebug,
|
"debug-splash": SplashDebug,
|
||||||
"debug-display": DisplayDebug,
|
"debug-display": DisplayDebug,
|
||||||
"debug-version-card": VersionCardDebug,
|
"debug-version-card": VersionCardDebug,
|
||||||
"debug-task": TaskDebug,
|
"debug-task": TaskDebug,
|
||||||
|
"debug-crash": CrashDebug,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
@@ -45,12 +58,21 @@ function App() {
|
|||||||
const Page = pageMap[current];
|
const Page = pageMap[current];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
useConfigStore.getState().init().then(() => {
|
// Listen for preloaded config from main process
|
||||||
|
const unsub = window.electronAPI?.onConfigPreload((data) => {
|
||||||
|
const { config, isFirstLaunch } = data;
|
||||||
|
useConfigStore.getState().applyPreloaded(config as AppConfig, isFirstLaunch);
|
||||||
syncThemeFromConfig();
|
syncThemeFromConfig();
|
||||||
syncA11yFromConfig();
|
syncA11yFromConfig();
|
||||||
syncBackgroundFromConfig();
|
syncBackgroundFromConfig();
|
||||||
useAuthStore.getState().initFromRegistry();
|
useAuthStore.getState().initFromRegistry();
|
||||||
|
// Navigate to OOBE on first launch or if oobe not completed
|
||||||
|
if (isFirstLaunch || config.oobe) {
|
||||||
|
useRouteStore.getState().navigate("oobe");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return () => { unsub?.(); };
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export interface NetworkConfig {
|
|||||||
|
|
||||||
export interface AppConfig {
|
export interface AppConfig {
|
||||||
version: number;
|
version: number;
|
||||||
|
oobe: boolean;
|
||||||
theme: ThemeConfig;
|
theme: ThemeConfig;
|
||||||
a11y: A11yConfig;
|
a11y: A11yConfig;
|
||||||
background: BackgroundConfig;
|
background: BackgroundConfig;
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import { useEffect } from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { BUILD_MODE } from "@/lib/mode";
|
|
||||||
import { VERSION } from "@/lib/version";
|
|
||||||
|
|
||||||
export function BetaWarning() {
|
|
||||||
useEffect(() => {
|
|
||||||
if (BUILD_MODE === "beta" || BUILD_MODE === "dev") {
|
|
||||||
toast.warning(`当前为 v${VERSION} BETA 测试版,不代表最终品质。`, {
|
|
||||||
duration: Infinity,
|
|
||||||
dismissible: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import { useState, useEffect } from "react";
|
|
||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogAction,
|
|
||||||
} from "@/components/ui/alert-dialog";
|
|
||||||
import { AlertTriangle } from "lucide-react";
|
|
||||||
|
|
||||||
const enabled = import.meta.env.VITE_START_POP === "true";
|
|
||||||
const title = import.meta.env.VITE_START_POP_TITLE ?? "";
|
|
||||||
const info = import.meta.env.VITE_START_POP_INFO ?? "";
|
|
||||||
const buttonText = import.meta.env.VITE_START_POP_BOUTTON ?? "确定";
|
|
||||||
|
|
||||||
export function StartupPopup() {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (enabled) {
|
|
||||||
setOpen(true);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!enabled) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AlertDialog open={open} onOpenChange={setOpen}>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<div className="mb-2 inline-flex size-10 items-center justify-center rounded-md bg-amber-500/10 sm:group-data-[size=default]/alert-dialog-content:row-span-2">
|
|
||||||
<AlertTriangle className="size-5 text-amber-500" />
|
|
||||||
</div>
|
|
||||||
<AlertDialogTitle>{title}</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>{info}</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogAction onClick={() => setOpen(false)}>
|
|
||||||
{buttonText}
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -124,9 +124,10 @@ const Silk = ({ speed = 5, scale = 1, color = "#7B7481", noiseIntensity = 1.5, r
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Canvas
|
<Canvas
|
||||||
dpr={[1, 2]}
|
dpr={[1, 1.5]}
|
||||||
|
frameloop="demand"
|
||||||
style={{ position: "absolute", inset: 0, width: "100%", height: "100%", background: "black" }}
|
style={{ position: "absolute", inset: 0, width: "100%", height: "100%", background: "black" }}
|
||||||
gl={{ antialias: true, alpha: false }}
|
gl={{ antialias: false, alpha: false }}
|
||||||
>
|
>
|
||||||
<SilkPlane ref={meshRef} uniforms={uniforms} />
|
<SilkPlane ref={meshRef} uniforms={uniforms} />
|
||||||
</Canvas>
|
</Canvas>
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { motion } from "motion/react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const initialProps = {
|
||||||
|
pathLength: 0,
|
||||||
|
opacity: 0,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
const animateProps = {
|
||||||
|
pathLength: 1,
|
||||||
|
opacity: 1,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
type Props = React.ComponentProps<typeof motion.svg> & {
|
||||||
|
speed?: number
|
||||||
|
onAnimationComplete?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function AppleHelloEnglishEffect({ className, speed = 1, onAnimationComplete, ...props }: Props) {
|
||||||
|
const calc = (x: number) => x * speed
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.svg
|
||||||
|
className={cn("h-20", className)}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
fill="none"
|
||||||
|
initial={{ opacity: 1 }}
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="14.8883"
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
viewBox="0 0 638 200"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...(props as any)}
|
||||||
|
>
|
||||||
|
<title>hello</title>
|
||||||
|
|
||||||
|
{/* h1 */}
|
||||||
|
<motion.path
|
||||||
|
animate={animateProps}
|
||||||
|
d="M8.69214 166.553C36.2393 151.239 61.3409 131.548 89.8191 98.0295C109.203 75.1488 119.625 49.0228 120.122 31.0026C120.37 17.6036 113.836 7.43883 101.759 7.43883C88.3598 7.43883 79.9231 17.6036 74.7122 40.9363C69.005 66.5793 64.7866 96.0036 54.1166 190.356"
|
||||||
|
initial={initialProps}
|
||||||
|
style={{ strokeLinecap: "round" }}
|
||||||
|
transition={{
|
||||||
|
duration: calc(0.8),
|
||||||
|
ease: "easeInOut",
|
||||||
|
opacity: { duration: 0.4 },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* h2, ello */}
|
||||||
|
<motion.path
|
||||||
|
animate={animateProps}
|
||||||
|
d="M55.1624 181.135C60.6251 133.114 81.4118 98.0479 107.963 98.0479C123.844 98.0479 133.937 110.703 131.071 128.817C129.457 139.487 127.587 150.405 125.408 163.06C122.869 178.941 130.128 191.348 152.122 191.348C184.197 191.348 219.189 173.523 237.097 145.915C243.198 136.509 245.68 128.073 245.928 119.884C246.176 104.996 237.739 93.8296 222.851 93.8296C203.992 93.8296 189.6 115.17 189.6 142.465C189.6 171.745 205.481 192.341 239.208 192.341C285.066 192.341 335.86 137.292 359.199 75.8585C365.788 58.513 368.26 42.4065 368.26 31.1512C368.26 17.8057 364.042 7.55823 352.131 7.55823C340.469 7.55823 332.777 16.6141 325.829 30.9129C317.688 47.4967 311.667 71.4162 309.203 98.4549C303 166.301 316.896 191.348 349.936 191.348C390 191.348 434.542 135.534 457.286 75.6686C463.803 58.513 466.275 42.4065 466.275 31.1512C466.275 17.8057 462.057 7.55823 450.146 7.55823C438.484 7.55823 430.792 16.6141 423.844 30.9129C415.703 47.4967 409.682 71.4162 407.218 98.4549C401.015 166.301 414.911 191.348 444.416 191.348C473.874 191.348 489.877 165.67 499.471 138.402C508.955 111.447 520.618 94.8221 544.935 94.8221C565.035 94.8221 580.916 109.71 580.916 137.75C580.916 168.768 560.792 192.093 535.362 192.341C512.984 192.589 498.285 174.475 499.774 147.179C501.511 116.907 519.873 94.8221 543.943 94.8221C557.839 94.8221 569.51 100.999 578.682 107.725C603.549 125.866 622.709 114.656 630.047 96.7186"
|
||||||
|
initial={initialProps}
|
||||||
|
onAnimationComplete={onAnimationComplete}
|
||||||
|
style={{ strokeLinecap: "round" }}
|
||||||
|
transition={{
|
||||||
|
duration: calc(2.8),
|
||||||
|
ease: "easeInOut",
|
||||||
|
delay: calc(0.7),
|
||||||
|
opacity: { duration: 0.7, delay: calc(0.7) },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { AppleHelloEnglishEffect}
|
||||||
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useConfirmDialogStore } from "@/stores/confirmDialogStore";
|
||||||
|
|
||||||
|
export function ConfirmDialog() {
|
||||||
|
const { open, title, description, confirmLabel, countdown: initCountdown, showCountdown, onConfirm, closeDialog } =
|
||||||
|
useConfirmDialogStore();
|
||||||
|
const [countdown, setCountdown] = useState(initCountdown);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
setCountdown(initCountdown);
|
||||||
|
}, [open, initCountdown]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open || !showCountdown || countdown <= 0) return;
|
||||||
|
const timer = setTimeout(() => setCountdown((c) => c - 1), 1000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [open, showCountdown, countdown]);
|
||||||
|
|
||||||
|
if (!open) return null;
|
||||||
|
|
||||||
|
const canConfirm = !showCountdown || countdown <= 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-black/50">
|
||||||
|
<div className="glass-card w-[380px] p-6 space-y-4">
|
||||||
|
<h3 className="text-base font-bold text-foreground">{title}</h3>
|
||||||
|
<p className="text-[13px] text-muted-foreground leading-relaxed">{description}</p>
|
||||||
|
<div className="flex justify-end gap-2 pt-2">
|
||||||
|
<button
|
||||||
|
onClick={closeDialog}
|
||||||
|
className="px-4 py-1.5 rounded-md text-[13px] font-medium bg-foreground/[0.06] hover:bg-foreground/[0.12] text-foreground/60 hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => { closeDialog(); onConfirm(); }}
|
||||||
|
disabled={!canConfirm}
|
||||||
|
className="px-4 py-1.5 rounded-md text-[13px] font-medium bg-red-500/15 text-red-600 dark:text-red-400 hover:bg-red-500/25 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{showCountdown && countdown > 0 ? `${confirmLabel} (${countdown}s)` : confirmLabel}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import { CrashPage } from "./pages/crash";
|
||||||
|
import "./index.css";
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<CrashPage />
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import { type ReactNode } from "react";
|
import { type ReactNode } from "react";
|
||||||
import { BackgroundLayer } from "@/components/background/BackgroundLayer";
|
import { BackgroundLayer } from "@/components/background/BackgroundLayer";
|
||||||
import { SystemLayer } from "@/components/system/SystemLayer";
|
import { SystemLayer } from "@/components/system/SystemLayer";
|
||||||
import { StartupPopup } from "@/components/StartupPopup";
|
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
|
||||||
import { BetaWarning } from "@/components/BetaWarning";
|
|
||||||
import { Toaster } from "sonner";
|
import { Toaster } from "sonner";
|
||||||
import { useA11yStore } from "@/stores/a11yStore";
|
import { useA11yStore } from "@/stores/a11yStore";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
@@ -52,11 +51,8 @@ export function RootLayout({
|
|||||||
showClose={showClose}
|
showClose={showClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Startup popup — only when VITE_START_POP=true */}
|
{/* Global confirm dialog */}
|
||||||
<StartupPopup />
|
<ConfirmDialog />
|
||||||
|
|
||||||
{/* Beta warning toast */}
|
|
||||||
<BetaWarning />
|
|
||||||
|
|
||||||
{/* Sonner toaster */}
|
{/* Sonner toaster */}
|
||||||
<Toaster
|
<Toaster
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { VERSION } from "@/lib/version";
|
||||||
|
|
||||||
|
type CrashInfo = {
|
||||||
|
type: string;
|
||||||
|
message: string;
|
||||||
|
timestamp: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CrashPage() {
|
||||||
|
const [crashInfo, setCrashInfo] = useState<CrashInfo | null>(null);
|
||||||
|
const [copying, setCopying] = useState(false);
|
||||||
|
const [copyDone, setCopyDone] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsub = window.electronAPI?.on("crash:show", (...args: unknown[]) => {
|
||||||
|
const info = args[0] as CrashInfo;
|
||||||
|
setCrashInfo(info);
|
||||||
|
});
|
||||||
|
return () => { unsub?.(); };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
window.electronAPI?.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopyLog = async () => {
|
||||||
|
setCopying(true);
|
||||||
|
try {
|
||||||
|
const log = await window.electronAPI?.invoke("crash:readLog") as string;
|
||||||
|
if (log) {
|
||||||
|
await navigator.clipboard.writeText(log);
|
||||||
|
setCopyDone(true);
|
||||||
|
setTimeout(() => setCopyDone(false), 2000);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
} finally {
|
||||||
|
setCopying(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFactoryReset = async () => {
|
||||||
|
await window.electronAPI?.invoke("crash:factoryReset");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRestart = async () => {
|
||||||
|
await window.electronAPI?.invoke("crash:restart");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-screen h-screen flex flex-col overflow-hidden bg-background text-foreground select-none">
|
||||||
|
{/* 顶栏 — 只有关闭按钮 */}
|
||||||
|
<div
|
||||||
|
className="h-[40px] flex items-center shrink-0 relative z-10"
|
||||||
|
style={{
|
||||||
|
WebkitAppRegion: "drag" as React.CSSProperties["WebkitAppRegion"],
|
||||||
|
background: "var(--titlebar-bg, rgba(255,255,255,0.03))",
|
||||||
|
backdropFilter: "blur(3px)",
|
||||||
|
WebkitBackdropFilter: "blur(3px)",
|
||||||
|
borderBottom: "1px solid var(--titlebar-border, rgba(255,255,255,0.06))",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center pl-3 shrink-0" style={{ WebkitAppRegion: "no-drag" as React.CSSProperties["WebkitAppRegion"] }}>
|
||||||
|
<span className="text-sm font-semibold tracking-wide text-foreground/80">
|
||||||
|
Koring Launcher
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center shrink-0 justify-end ml-auto pr-2" style={{ WebkitAppRegion: "no-drag" as React.CSSProperties["WebkitAppRegion"] }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleClose}
|
||||||
|
className="flex items-center justify-center w-[25px] h-[25px] rounded transition-colors cursor-default hover:bg-red-500 text-black/70 dark:text-white/70 hover:text-white"
|
||||||
|
>
|
||||||
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||||
|
<line x1="2" y1="2" x2="10" y2="10" />
|
||||||
|
<line x1="10" y1="2" x2="2" y2="10" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 崩溃内容 */}
|
||||||
|
<div className="flex-1 flex items-center justify-center p-8">
|
||||||
|
<div className="w-full max-w-[520px] text-center space-y-6">
|
||||||
|
{/* 图标 */}
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<div className="w-16 h-16 rounded-full bg-red-500/10 flex items-center justify-center">
|
||||||
|
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className="text-red-500">
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<line x1="12" y1="8" x2="12" y2="12" />
|
||||||
|
<line x1="12" y1="16" x2="12.01" y2="16" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 标题 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h1 className="text-xl font-bold text-foreground">
|
||||||
|
程序发生崩溃
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
我们已记录此错误,接下来您想做什么?
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{/* 说明文字 */}
|
||||||
|
<p className="text-sm text-muted-foreground leading-relaxed text-left">
|
||||||
|
您可能是在一些奇怪的地方触发了内容导致崩溃,我们已准备好日志,你可以直接复制将其发送给支持人员,你也可以点击下方的重启按钮再次启动,如果还是不行您可以点击强还原配置按钮进行还原。(注意,还原只会清除启动器数据,并不会影响实例,请放心使用)
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* 按钮组 */}
|
||||||
|
<div className="flex items-center justify-center gap-3 pt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleCopyLog}
|
||||||
|
disabled={copying}
|
||||||
|
className="px-4 py-2 text-sm font-medium rounded-lg border border-border bg-background hover:bg-muted transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{copyDone ? "已复制" : copying ? "复制中..." : "复制日志"}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleFactoryReset}
|
||||||
|
className="px-4 py-2 text-sm font-medium rounded-lg border border-border bg-background hover:bg-muted transition-colors"
|
||||||
|
>
|
||||||
|
强还原配置
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleRestart}
|
||||||
|
className="px-4 py-2 text-sm font-medium rounded-lg bg-primary text-primary-foreground hover:bg-primary/80 transition-colors"
|
||||||
|
>
|
||||||
|
重启
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { AlertTriangle, TestTube, RotateCcw, Copy, Trash2 } from "lucide-react";
|
||||||
|
|
||||||
|
export function CrashDebug() {
|
||||||
|
const [log, setLog] = useState("");
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [status, setStatus] = useState("");
|
||||||
|
|
||||||
|
const handleSimulateCrash = async () => {
|
||||||
|
setStatus("正在模拟崩溃...");
|
||||||
|
await window.electronAPI?.simulateCrash();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTestDialog = async () => {
|
||||||
|
setStatus("正在打开崩溃弹窗...");
|
||||||
|
await window.electronAPI?.testCrashDialog();
|
||||||
|
setStatus("崩溃弹窗已打开");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReadLog = async () => {
|
||||||
|
const result = await window.electronAPI?.invoke("crash:readLog") as string;
|
||||||
|
setLog(result || "(空)");
|
||||||
|
setStatus("日志已加载");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopyLog = async () => {
|
||||||
|
if (!log) return;
|
||||||
|
await navigator.clipboard.writeText(log);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFactoryReset = async () => {
|
||||||
|
if (!confirm("确定要执行强还原配置吗?这将删除 Koring.yml、koring-auth.json 和背景缓存,但不会影响实例。")) return;
|
||||||
|
setStatus("正在还原...");
|
||||||
|
await window.electronAPI?.invoke("crash:factoryReset");
|
||||||
|
setStatus("还原完成");
|
||||||
|
};
|
||||||
|
|
||||||
|
const btnBase = "flex items-center gap-2 px-4 py-2.5 rounded-lg text-sm font-medium transition-all duration-150 cursor-pointer active:scale-[0.97]";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold text-foreground mb-1">崩溃测试</h2>
|
||||||
|
<p className="text-sm text-muted-foreground mb-6">测试崩溃检测、崩溃弹窗与恢复功能</p>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 模拟崩溃 */}
|
||||||
|
<section className="glass-card p-5 space-y-3">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<AlertTriangle className="w-4 h-4 text-red-500" />
|
||||||
|
<h3 className="text-sm font-semibold text-foreground">模拟渲染进程崩溃</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-[13px] text-muted-foreground">
|
||||||
|
调用 <code className="px-1.5 py-0.5 rounded bg-foreground/[0.06] text-foreground/80 text-xs">forcefullyCrashRenderer()</code> 强制销毁渲染进程,触发 <code className="px-1.5 py-0.5 rounded bg-foreground/[0.06] text-foreground/80 text-xs">render-process-gone</code> 事件,崩溃弹窗应自动弹出。
|
||||||
|
</p>
|
||||||
|
<button onClick={handleSimulateCrash} className={`${btnBase} bg-red-500/10 text-red-600 dark:text-red-400 hover:bg-red-500/20`}>
|
||||||
|
<AlertTriangle className="w-4 h-4" />
|
||||||
|
模拟崩溃
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* 测试崩溃弹窗 */}
|
||||||
|
<section className="glass-card p-5 space-y-3">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<TestTube className="w-4 h-4 text-amber-500" />
|
||||||
|
<h3 className="text-sm font-semibold text-foreground">测试崩溃弹窗</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-[13px] text-muted-foreground">
|
||||||
|
不会真正崩溃,直接弹出崩溃弹窗 UI,用于验证窗口样式、按钮功能是否正常。
|
||||||
|
</p>
|
||||||
|
<button onClick={handleTestDialog} className={`${btnBase} bg-amber-500/10 text-amber-600 dark:text-amber-400 hover:bg-amber-500/20`}>
|
||||||
|
<TestTube className="w-4 h-4" />
|
||||||
|
测试崩溃弹窗
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* 崩溃日志 */}
|
||||||
|
<section className="glass-card p-5 space-y-3">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<Copy className="w-4 h-4 text-blue-500" />
|
||||||
|
<h3 className="text-sm font-semibold text-foreground">崩溃日志</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-[13px] text-muted-foreground">
|
||||||
|
读取 <code className="px-1.5 py-0.5 rounded bg-foreground/[0.06] text-foreground/80 text-xs">koring-crash.log</code> 文件内容,可复制发送给开发人员。
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button onClick={handleReadLog} className={`${btnBase} bg-blue-500/10 text-blue-600 dark:text-blue-400 hover:bg-blue-500/20`}>
|
||||||
|
<Copy className="w-4 h-4" />
|
||||||
|
读取日志
|
||||||
|
</button>
|
||||||
|
<button onClick={handleCopyLog} disabled={!log} className={`${btnBase} bg-blue-500/10 text-blue-600 dark:text-blue-400 hover:bg-blue-500/20 disabled:opacity-40`}>
|
||||||
|
<Copy className="w-4 h-4" />
|
||||||
|
{copied ? "已复制" : "复制到剪贴板"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{log && (
|
||||||
|
<pre className="mt-2 p-3 rounded-lg bg-foreground/[0.03] border border-border/50 text-xs text-foreground/70 overflow-auto max-h-40 font-mono">
|
||||||
|
{log}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* 强还原配置 */}
|
||||||
|
<section className="glass-card p-5 space-y-3">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<Trash2 className="w-4 h-4 text-orange-500" />
|
||||||
|
<h3 className="text-sm font-semibold text-foreground">强还原配置</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-[13px] text-muted-foreground">
|
||||||
|
删除 <code className="px-1.5 py-0.5 rounded bg-foreground/[0.06] text-foreground/80 text-xs">Koring.yml</code>、
|
||||||
|
<code className="px-1.5 py-0.5 rounded bg-foreground/[0.06] text-foreground/80 text-xs">koring-auth.json</code> 和背景缓存。
|
||||||
|
<strong className="text-foreground/80"> 不会影响实例数据。</strong>
|
||||||
|
</p>
|
||||||
|
<button onClick={handleFactoryReset} className={`${btnBase} bg-orange-500/10 text-orange-600 dark:text-orange-400 hover:bg-orange-500/20`}>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
强还原配置
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* 状态 */}
|
||||||
|
{status && (
|
||||||
|
<p className="text-xs text-muted-foreground/60">{status}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,15 @@
|
|||||||
import { useRouteStore } from "@/stores/routeStore";
|
import { useRouteStore } from "@/stores/routeStore";
|
||||||
import { Monitor, Paintbrush, CreditCard, ListTodo, ChevronRight, FlaskConical, Rocket } from "lucide-react";
|
import { Monitor, Paintbrush, CreditCard, ListTodo, ChevronRight, FlaskConical, Rocket, AlertTriangle } from "lucide-react";
|
||||||
|
|
||||||
const debugPages = [
|
const debugPages = [
|
||||||
|
{
|
||||||
|
key: "debug-crash" as const,
|
||||||
|
icon: AlertTriangle,
|
||||||
|
title: "崩溃测试",
|
||||||
|
desc: "模拟崩溃、测试崩溃弹窗、查看崩溃日志与强还原配置",
|
||||||
|
color: "text-red-500",
|
||||||
|
bg: "bg-red-500/10",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "debug-splash" as const,
|
key: "debug-splash" as const,
|
||||||
icon: Monitor,
|
icon: Monitor,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
Clock,
|
Clock,
|
||||||
Loader2,
|
Loader2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { VERSION } from "@/lib/version";
|
||||||
|
|
||||||
interface InfoItem {
|
interface InfoItem {
|
||||||
icon: typeof Package;
|
icon: typeof Package;
|
||||||
@@ -35,7 +36,7 @@ export function OobeAboutInfo() {
|
|||||||
|
|
||||||
const items: InfoItem[] = systemInfo && localeInfo
|
const items: InfoItem[] = systemInfo && localeInfo
|
||||||
? [
|
? [
|
||||||
{ icon: Package, label: "App Version", value: `v${systemInfo.app_version}`, color: "text-primary", bg: "bg-primary/10" },
|
{ icon: Package, label: "App Version", value: `v${VERSION}`, color: "text-primary", bg: "bg-primary/10" },
|
||||||
{ icon: Cpu, label: "BIOS ID", value: systemInfo.bios_id, color: "text-blue-500", bg: "bg-blue-500/10" },
|
{ icon: Cpu, label: "BIOS ID", value: systemInfo.bios_id, color: "text-blue-500", bg: "bg-blue-500/10" },
|
||||||
{ icon: Monitor, label: "OS Name", value: `${systemInfo.os_name} (${systemInfo.os_version})`, color: "text-purple-500", bg: "bg-purple-500/10" },
|
{ icon: Monitor, label: "OS Name", value: `${systemInfo.os_name} (${systemInfo.os_version})`, color: "text-purple-500", bg: "bg-purple-500/10" },
|
||||||
{ icon: Globe, label: "Region", value: localeInfo.region, color: "text-green-500", bg: "bg-green-500/10" },
|
{ icon: Globe, label: "Region", value: localeInfo.region, color: "text-green-500", bg: "bg-green-500/10" },
|
||||||
|
|||||||
+53
-18
@@ -1,29 +1,64 @@
|
|||||||
import { useRouteStore } from "@/stores/routeStore";
|
import { useRouteStore } from "@/stores/routeStore";
|
||||||
import { Rocket, ChevronRight } from "lucide-react";
|
import { useEffect, useState, useRef } from "react";
|
||||||
|
|
||||||
export function Oobe() {
|
const words = ["Hello.", "你好。", "こんにちは。", "안녕하세요。", "Bonjour."];
|
||||||
const goBack = useRouteStore((s) => s.goBack);
|
|
||||||
|
function TypewriterText() {
|
||||||
|
const [wordIndex, setWordIndex] = useState(0);
|
||||||
|
const [displayed, setDisplayed] = useState("");
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
const timerRef = useRef<ReturnType<typeof setTimeout>>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const word = words[wordIndex];
|
||||||
|
|
||||||
|
if (!isDeleting) {
|
||||||
|
if (displayed.length < word.length) {
|
||||||
|
timerRef.current = setTimeout(() => {
|
||||||
|
setDisplayed(word.slice(0, displayed.length + 1));
|
||||||
|
}, 120);
|
||||||
|
} else {
|
||||||
|
timerRef.current = setTimeout(() => setIsDeleting(true), 1800);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (displayed.length > 0) {
|
||||||
|
timerRef.current = setTimeout(() => {
|
||||||
|
setDisplayed(displayed.slice(0, -1));
|
||||||
|
}, 60);
|
||||||
|
} else {
|
||||||
|
setIsDeleting(false);
|
||||||
|
setWordIndex((i) => (i + 1) % words.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => clearTimeout(timerRef.current);
|
||||||
|
}, [displayed, isDeleting, wordIndex]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col items-center justify-center px-8">
|
<span className="inline-block min-w-[200px] text-center">
|
||||||
<div className="flex flex-col items-center text-center max-w-md">
|
{displayed}
|
||||||
<div className="p-4 rounded-2xl bg-primary/10 mb-6">
|
<span className="inline-block w-[2px] h-[1em] bg-foreground/60 ml-0.5 align-middle animate-pulse" />
|
||||||
<Rocket className="w-10 h-10 text-primary" />
|
</span>
|
||||||
</div>
|
);
|
||||||
|
}
|
||||||
|
|
||||||
<h1 className="text-2xl font-bold text-foreground mb-2">
|
export function Oobe() {
|
||||||
欢迎使用 Koring Launcher
|
const navigate = useRouteStore((s) => s.navigate);
|
||||||
</h1>
|
|
||||||
<p className="text-sm text-muted-foreground mb-8 leading-relaxed">
|
|
||||||
这是 OOBE(开箱体验)页面。在这里可以引导用户完成初始设置。
|
|
||||||
</p>
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-col items-center justify-center relative">
|
||||||
|
{/* 中心动画文字 */}
|
||||||
|
<div className="text-5xl font-bold tracking-tight text-foreground">
|
||||||
|
<TypewriterText />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 下方按钮 */}
|
||||||
|
<div className="absolute bottom-12">
|
||||||
<button
|
<button
|
||||||
onClick={goBack}
|
onClick={() => navigate("oobe/language")}
|
||||||
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-lg bg-foreground/[0.06] hover:bg-foreground/[0.1] text-sm font-medium transition-colors"
|
className="w-12 h-12 rounded-full bg-foreground/[0.06] hover:bg-foreground/[0.12] flex items-center justify-center text-foreground/60 hover:text-foreground transition-all duration-200 text-xl"
|
||||||
>
|
>
|
||||||
返回调试
|
→
|
||||||
<ChevronRight className="w-4 h-4" />
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { type ReactNode } from "react";
|
||||||
|
|
||||||
|
interface OobeLayoutProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OobeLayout({ children }: OobeLayoutProps) {
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-col items-center justify-center relative">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
interface NextButtonProps {
|
||||||
|
onClick: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NextButton({ onClick, disabled = false }: NextButtonProps) {
|
||||||
|
return (
|
||||||
|
<div className="absolute bottom-12">
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={disabled}
|
||||||
|
className="w-12 h-12 rounded-full bg-foreground/[0.06] hover:bg-foreground/[0.12] flex items-center justify-center text-foreground/60 hover:text-foreground transition-all duration-200 text-xl disabled:opacity-30 disabled:cursor-not-allowed disabled:hover:bg-foreground/[0.06] disabled:hover:text-foreground/60"
|
||||||
|
>
|
||||||
|
→
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useRouteStore } from "@/stores/routeStore";
|
||||||
|
import { OobeLayout } from "./layout";
|
||||||
|
import { NextButton } from "./next-button";
|
||||||
|
|
||||||
|
export function OobeAgreement() {
|
||||||
|
const navigate = useRouteStore((s) => s.navigate);
|
||||||
|
const [text, setText] = useState("");
|
||||||
|
const [checked, setChecked] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch(`${import.meta.env.BASE_URL}protocol-user.txt`)
|
||||||
|
.then((r) => r.text())
|
||||||
|
.then(setText)
|
||||||
|
.catch(() => setText("无法加载协议内容"));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OobeLayout>
|
||||||
|
<div className="w-full max-w-lg flex flex-col items-center gap-4 px-6">
|
||||||
|
{/* 标题 */}
|
||||||
|
<div className="text-center space-y-1">
|
||||||
|
<h2 className="text-lg font-bold text-foreground">Koring Team 产品用户协议</h2>
|
||||||
|
<p className="text-xs text-muted-foreground">您需要同意才可以继续</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 协议内容 */}
|
||||||
|
<div className="w-full h-[300px] rounded-xl bg-foreground/[0.03] border border-border/50 p-4 overflow-y-auto">
|
||||||
|
<pre className="text-xs text-foreground/70 whitespace-pre-wrap font-sans leading-relaxed">
|
||||||
|
{text}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 勾选框 */}
|
||||||
|
<label className="flex items-start gap-2.5 cursor-pointer select-none group">
|
||||||
|
<div className="relative mt-0.5">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={checked}
|
||||||
|
onChange={(e) => setChecked(e.target.checked)}
|
||||||
|
className="sr-only peer"
|
||||||
|
/>
|
||||||
|
<div className="w-4 h-4 rounded border border-border/60 bg-foreground/[0.03] peer-checked:bg-primary peer-checked:border-primary transition-colors flex items-center justify-center">
|
||||||
|
{checked && (
|
||||||
|
<svg width="10" height="10" viewBox="0 0 12 12" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<polyline points="2 6 5 9 10 3" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-muted-foreground leading-relaxed group-hover:text-foreground/70 transition-colors">
|
||||||
|
我已详细阅读此协议,且同意其内容并签署此协议。
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<NextButton onClick={() => navigate("oobe/version")} disabled={!checked} />
|
||||||
|
</OobeLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useRouteStore } from "@/stores/routeStore";
|
||||||
|
import { OobeLayout } from "./layout";
|
||||||
|
import { NextButton } from "./next-button";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
|
||||||
|
export function OobeBetaTest() {
|
||||||
|
const navigate = useRouteStore((s) => s.navigate);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [text, setText] = useState("");
|
||||||
|
const [checked, setChecked] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
fetch(`${import.meta.env.BASE_URL}protocol-beta.txt`)
|
||||||
|
.then((r) => r.text())
|
||||||
|
.then(setText)
|
||||||
|
.catch(() => setText("无法加载协议内容"))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, 1500);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<OobeLayout>
|
||||||
|
<div className="flex flex-col items-center gap-3">
|
||||||
|
<Loader2 className="w-6 h-6 animate-spin text-foreground/40" />
|
||||||
|
<span className="text-sm text-muted-foreground">正在确认版本信息,并激活...</span>
|
||||||
|
</div>
|
||||||
|
</OobeLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OobeLayout>
|
||||||
|
<div className="w-full max-w-lg flex flex-col items-center gap-4 px-6">
|
||||||
|
{/* 标题 */}
|
||||||
|
<div className="text-center space-y-1">
|
||||||
|
<h2 className="text-lg font-bold text-foreground">Koring APP Beta 测试协议</h2>
|
||||||
|
<p className="text-xs text-muted-foreground">您需要同意才可以继续</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 协议内容 */}
|
||||||
|
<div className="w-full h-[300px] rounded-xl bg-foreground/[0.03] border border-border/50 p-4 overflow-y-auto">
|
||||||
|
<pre className="text-xs text-foreground/70 whitespace-pre-wrap font-sans leading-relaxed">
|
||||||
|
{text}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 勾选框 */}
|
||||||
|
<label className="flex items-start gap-2.5 cursor-pointer select-none group">
|
||||||
|
<div className="relative mt-0.5">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={checked}
|
||||||
|
onChange={(e) => setChecked(e.target.checked)}
|
||||||
|
className="sr-only peer"
|
||||||
|
/>
|
||||||
|
<div className="w-4 h-4 rounded border border-border/60 bg-foreground/[0.03] peer-checked:bg-primary peer-checked:border-primary transition-colors flex items-center justify-center">
|
||||||
|
{checked && (
|
||||||
|
<svg width="10" height="10" viewBox="0 0 12 12" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<polyline points="2 6 5 9 10 3" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-muted-foreground leading-relaxed group-hover:text-foreground/70 transition-colors">
|
||||||
|
我已详细阅读并了解此 测试 协议,且 同意其内容 并 签署此协议。
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<NextButton onClick={() => navigate("oobe/finish")} disabled={!checked} />
|
||||||
|
</OobeLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { useRouteStore } from "@/stores/routeStore";
|
||||||
|
import { useConfigStore } from "@/stores/configStore";
|
||||||
|
import { OobeLayout } from "./layout";
|
||||||
|
import { AppleHelloEnglishEffect } from "@/components/ui/apple-hello-effect";
|
||||||
|
|
||||||
|
export function OobeFinish() {
|
||||||
|
const navigate = useRouteStore((s) => s.navigate);
|
||||||
|
const setOobe = useConfigStore((s) => s.setOobe);
|
||||||
|
|
||||||
|
const handleFinish = () => {
|
||||||
|
setOobe(false);
|
||||||
|
navigate("home");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OobeLayout>
|
||||||
|
<div className="flex flex-col items-center gap-6">
|
||||||
|
<AppleHelloEnglishEffect className="text-foreground" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute bottom-12">
|
||||||
|
<button
|
||||||
|
onClick={handleFinish}
|
||||||
|
className="h-12 px-6 rounded-full bg-foreground/[0.06] hover:bg-foreground/[0.12] flex items-center justify-center text-foreground/60 hover:text-foreground transition-all duration-200 text-sm font-medium"
|
||||||
|
>
|
||||||
|
前往首页
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</OobeLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useRouteStore } from "@/stores/routeStore";
|
||||||
|
import { OobeLayout } from "./layout";
|
||||||
|
import { NextButton } from "./next-button";
|
||||||
|
import { Check } from "lucide-react";
|
||||||
|
|
||||||
|
const languages = [
|
||||||
|
{ key: "zh-CN", label: "简体中文", available: true },
|
||||||
|
{ key: "zh-TW", label: "繁体中文", available: false },
|
||||||
|
{ key: "en", label: "English", available: false },
|
||||||
|
{ key: "ja", label: "日本語", available: false },
|
||||||
|
{ key: "ko", label: "한국어", available: false },
|
||||||
|
{ key: "lzh", label: "文言文(中国)", available: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function OobeLanguage() {
|
||||||
|
const navigate = useRouteStore((s) => s.navigate);
|
||||||
|
const [selected, setSelected] = useState("zh-CN");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OobeLayout>
|
||||||
|
<div className="w-full max-w-sm space-y-3">
|
||||||
|
{languages.map((lang) => (
|
||||||
|
<button
|
||||||
|
key={lang.key}
|
||||||
|
disabled={!lang.available}
|
||||||
|
onClick={() => lang.available && setSelected(lang.key)}
|
||||||
|
className={[
|
||||||
|
"w-full flex items-center justify-between px-4 py-3 rounded-xl text-sm font-medium transition-all duration-200",
|
||||||
|
lang.available
|
||||||
|
? selected === lang.key
|
||||||
|
? "bg-foreground/[0.08] text-foreground ring-1 ring-foreground/10"
|
||||||
|
: "bg-foreground/[0.03] text-foreground/60 hover:bg-foreground/[0.06] hover:text-foreground/80"
|
||||||
|
: "bg-foreground/[0.02] text-foreground/25 cursor-not-allowed",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
<span>{lang.label}</span>
|
||||||
|
{!lang.available && (
|
||||||
|
<span className="text-[11px] text-foreground/20">即将推出</span>
|
||||||
|
)}
|
||||||
|
{selected === lang.key && lang.available && (
|
||||||
|
<Check className="w-4 h-4 text-foreground/60" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<NextButton onClick={() => navigate("oobe/agreement")} />
|
||||||
|
</OobeLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useRouteStore } from "@/stores/routeStore";
|
||||||
|
import { OobeLayout } from "./layout";
|
||||||
|
import { NextButton } from "./next-button";
|
||||||
|
import { VERSION } from "@/lib/version";
|
||||||
|
import { BUILD_MODE } from "@/lib/mode";
|
||||||
|
|
||||||
|
export function OobeVersion() {
|
||||||
|
const navigate = useRouteStore((s) => s.navigate);
|
||||||
|
const [changelog, setChangelog] = useState("");
|
||||||
|
|
||||||
|
const isTestBuild = BUILD_MODE === "dev" || BUILD_MODE === "beta";
|
||||||
|
const badgeLabel = BUILD_MODE === "dev" ? "DEV" : BUILD_MODE === "beta" ? "BETA" : null;
|
||||||
|
|
||||||
|
const nextRoute = isTestBuild ? "oobe/beta-test" : "oobe/finish";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch(`${import.meta.env.BASE_URL}changelog-${VERSION}.txt`)
|
||||||
|
.then((r) => r.text())
|
||||||
|
.then(setChangelog)
|
||||||
|
.catch(() => setChangelog("暂无更新内容"));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OobeLayout>
|
||||||
|
<div className="w-full max-w-lg flex flex-col items-center gap-4 px-6">
|
||||||
|
{/* 标题 */}
|
||||||
|
<h2 className="text-lg font-bold text-foreground">核对您的版本信息</h2>
|
||||||
|
|
||||||
|
{/* 版本号 + Badge */}
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<span className="text-3xl font-bold tracking-tight text-foreground">
|
||||||
|
v{VERSION}
|
||||||
|
</span>
|
||||||
|
{badgeLabel && (
|
||||||
|
<span
|
||||||
|
className={[
|
||||||
|
"text-[11px] font-bold px-2 py-0.5 rounded-full leading-none",
|
||||||
|
BUILD_MODE === "dev"
|
||||||
|
? "bg-amber-500/15 text-amber-600 dark:text-amber-400"
|
||||||
|
: "bg-emerald-500/15 text-emerald-600 dark:text-emerald-400",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
{badgeLabel}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 测试版警告 */}
|
||||||
|
{isTestBuild && (
|
||||||
|
<p className="text-xs text-muted-foreground text-center max-w-sm leading-relaxed">
|
||||||
|
您正在使用测试版本,它并不稳定,不建议用于正式游戏体验,具体内容请以发行版本为准。
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 更新内容 */}
|
||||||
|
<div className="w-full h-[260px] rounded-xl bg-foreground/[0.03] border border-border/50 p-4 overflow-y-auto">
|
||||||
|
<pre className="text-xs text-foreground/70 whitespace-pre-wrap font-sans leading-relaxed">
|
||||||
|
{changelog}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<NextButton onClick={() => navigate(nextRoute)} />
|
||||||
|
</OobeLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { VersionCard } from "@/components/VersionCard";
|
import { VersionCard } from "@/components/VersionCard";
|
||||||
import { BUILD_MODE } from "@/lib/mode";
|
import { BUILD_MODE } from "@/lib/mode";
|
||||||
import { ExternalLink, GitFork } from "lucide-react";
|
import { ExternalLink, GitFork, RotateCcw } from "lucide-react";
|
||||||
|
import { useConfirmDialogStore } from "@/stores/confirmDialogStore";
|
||||||
|
|
||||||
function GlassCard({ children }: { children: React.ReactNode }) {
|
function GlassCard({ children }: { children: React.ReactNode }) {
|
||||||
return <div className="glass-card px-5 py-4">{children}</div>;
|
return <div className="glass-card px-5 py-4">{children}</div>;
|
||||||
@@ -28,10 +29,22 @@ const GITHUB_URL = "https://github.com/koring-launcher/koring-launcher";
|
|||||||
const OFFICIAL_URL = "https://koring.app";
|
const OFFICIAL_URL = "https://koring.app";
|
||||||
|
|
||||||
export function AboutSetting() {
|
export function AboutSetting() {
|
||||||
|
const openDialog = useConfirmDialogStore((s) => s.openDialog);
|
||||||
|
|
||||||
const openLink = (url: string) => {
|
const openLink = (url: string) => {
|
||||||
window.electronAPI?.openExternal(url);
|
window.electronAPI?.openExternal(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleResetClick = () => {
|
||||||
|
openDialog({
|
||||||
|
title: "您确定要还原所有配置吗?",
|
||||||
|
description: "您还原后,您的实例将会保留,但是所有个性化配置将全部丢失,并且需要重新进行激活",
|
||||||
|
confirmLabel: "确认还原",
|
||||||
|
countdown: 5,
|
||||||
|
onConfirm: () => window.electronAPI?.resetConfig(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-foreground mb-1">关于</h2>
|
<h2 className="text-xl font-bold text-foreground mb-1">关于</h2>
|
||||||
@@ -92,6 +105,22 @@ export function AboutSetting() {
|
|||||||
</GlassCard>
|
</GlassCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 危险操作 */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-bold text-foreground mb-3">危险操作</h3>
|
||||||
|
<GlassCard>
|
||||||
|
<SettingRow label="还原所有设置" desc="删除所有配置文件并重启应用">
|
||||||
|
<button
|
||||||
|
onClick={handleResetClick}
|
||||||
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md text-[13px] font-medium bg-red-500/10 text-red-600 dark:text-red-400 hover:bg-red-500/20 transition-colors"
|
||||||
|
>
|
||||||
|
<RotateCcw className="w-4 h-4" />
|
||||||
|
还原
|
||||||
|
</button>
|
||||||
|
</SettingRow>
|
||||||
|
</GlassCard>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+58
-27
@@ -14,22 +14,23 @@ import {
|
|||||||
} from "@/api/config";
|
} from "@/api/config";
|
||||||
import { DEFAULT_BG } from "@/lib/mode";
|
import { DEFAULT_BG } from "@/lib/mode";
|
||||||
|
|
||||||
// TODO: re-enable when ready
|
let saveTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
// let saveTimer: ReturnType<typeof setTimeout> | null = null;
|
function debouncedSave(config: AppConfig) {
|
||||||
// function debouncedSave(config: AppConfig) {
|
if (saveTimer) clearTimeout(saveTimer);
|
||||||
// if (saveTimer) clearTimeout(saveTimer);
|
saveTimer = setTimeout(() => {
|
||||||
// saveTimer = setTimeout(() => {
|
saveConfig(config).catch((e) => {
|
||||||
// saveConfig(config).catch((e) => {
|
console.error("[config] save failed:", e);
|
||||||
// console.error("[config] save failed:", e);
|
});
|
||||||
// });
|
}, 300);
|
||||||
// }, 300);
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
interface ConfigState {
|
interface ConfigState {
|
||||||
config: AppConfig;
|
config: AppConfig;
|
||||||
loaded: boolean;
|
loaded: boolean;
|
||||||
|
isFirstLaunch: boolean;
|
||||||
|
|
||||||
init: () => Promise<void>;
|
init: () => Promise<void>;
|
||||||
|
applyPreloaded: (config: AppConfig, isFirstLaunch: boolean) => void;
|
||||||
setTheme: (patch: Partial<ThemeConfig>) => void;
|
setTheme: (patch: Partial<ThemeConfig>) => void;
|
||||||
setA11y: (patch: Partial<A11yConfig>) => void;
|
setA11y: (patch: Partial<A11yConfig>) => void;
|
||||||
setBackground: (patch: Partial<BackgroundConfig>) => void;
|
setBackground: (patch: Partial<BackgroundConfig>) => void;
|
||||||
@@ -38,10 +39,12 @@ interface ConfigState {
|
|||||||
setAdvanced: (patch: Partial<AdvancedConfig>) => void;
|
setAdvanced: (patch: Partial<AdvancedConfig>) => void;
|
||||||
setDownload: (patch: Partial<DownloadConfig>) => void;
|
setDownload: (patch: Partial<DownloadConfig>) => void;
|
||||||
setNetwork: (patch: Partial<NetworkConfig>) => void;
|
setNetwork: (patch: Partial<NetworkConfig>) => void;
|
||||||
|
setOobe: (value: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_CONFIG: AppConfig = {
|
const DEFAULT_CONFIG: AppConfig = {
|
||||||
version: 1,
|
version: 1,
|
||||||
|
oobe: true,
|
||||||
theme: { darkMode: "auto", parallax: true },
|
theme: { darkMode: "auto", parallax: true },
|
||||||
a11y: { reduceMotion: false, reduceTransparency: false, highContrast: false, contentBlurOpacity: 50 },
|
a11y: { reduceMotion: false, reduceTransparency: false, highContrast: false, contentBlurOpacity: 50 },
|
||||||
background: { bgType: "image", image: DEFAULT_BG, blur: 0, opacity: 100 },
|
background: { bgType: "image", image: DEFAULT_BG, blur: 0, opacity: 100 },
|
||||||
@@ -55,56 +58,84 @@ const DEFAULT_CONFIG: AppConfig = {
|
|||||||
export const useConfigStore = create<ConfigState>((set, get) => ({
|
export const useConfigStore = create<ConfigState>((set, get) => ({
|
||||||
config: DEFAULT_CONFIG,
|
config: DEFAULT_CONFIG,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
|
isFirstLaunch: false,
|
||||||
|
|
||||||
|
applyPreloaded: (config, isFirstLaunch) => {
|
||||||
|
set({ config, isFirstLaunch, loaded: true });
|
||||||
|
},
|
||||||
|
|
||||||
init: async () => {
|
init: async () => {
|
||||||
// TODO: re-enable when ready — load from Rust/Koring.yml
|
// If already preloaded, skip IPC call
|
||||||
// try {
|
if (get().loaded) return;
|
||||||
// const config = await getConfig();
|
try {
|
||||||
// set({ config, loaded: true });
|
const config = await getConfig();
|
||||||
// } catch (e) {
|
set({ config, loaded: true });
|
||||||
// console.error("[config] init failed, using defaults:", e);
|
} catch (e) {
|
||||||
// set({ loaded: true });
|
console.error("[config] init failed, using defaults:", e);
|
||||||
// }
|
set({ loaded: true });
|
||||||
set({ loaded: true });
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setTheme: (patch) => {
|
setTheme: (patch) => {
|
||||||
const { config } = get();
|
const { config } = get();
|
||||||
set({ config: { ...config, theme: { ...config.theme, ...patch } } });
|
const next = { ...config, theme: { ...config.theme, ...patch } };
|
||||||
|
set({ config: next });
|
||||||
|
debouncedSave(next);
|
||||||
},
|
},
|
||||||
|
|
||||||
setA11y: (patch) => {
|
setA11y: (patch) => {
|
||||||
const { config } = get();
|
const { config } = get();
|
||||||
set({ config: { ...config, a11y: { ...config.a11y, ...patch } } });
|
const next = { ...config, a11y: { ...config.a11y, ...patch } };
|
||||||
|
set({ config: next });
|
||||||
|
debouncedSave(next);
|
||||||
},
|
},
|
||||||
|
|
||||||
setBackground: (patch) => {
|
setBackground: (patch) => {
|
||||||
const { config } = get();
|
const { config } = get();
|
||||||
set({ config: { ...config, background: { ...config.background, ...patch } } });
|
const next = { ...config, background: { ...config.background, ...patch } };
|
||||||
|
set({ config: next });
|
||||||
|
debouncedSave(next);
|
||||||
},
|
},
|
||||||
|
|
||||||
setGame: (patch) => {
|
setGame: (patch) => {
|
||||||
const { config } = get();
|
const { config } = get();
|
||||||
set({ config: { ...config, game: { ...config.game, ...patch } } });
|
const next = { ...config, game: { ...config.game, ...patch } };
|
||||||
|
set({ config: next });
|
||||||
|
debouncedSave(next);
|
||||||
},
|
},
|
||||||
|
|
||||||
setJava: (patch) => {
|
setJava: (patch) => {
|
||||||
const { config } = get();
|
const { config } = get();
|
||||||
set({ config: { ...config, java: { ...config.java, ...patch } } });
|
const next = { ...config, java: { ...config.java, ...patch } };
|
||||||
|
set({ config: next });
|
||||||
|
debouncedSave(next);
|
||||||
},
|
},
|
||||||
|
|
||||||
setAdvanced: (patch) => {
|
setAdvanced: (patch) => {
|
||||||
const { config } = get();
|
const { config } = get();
|
||||||
set({ config: { ...config, advanced: { ...config.advanced, ...patch } } });
|
const next = { ...config, advanced: { ...config.advanced, ...patch } };
|
||||||
|
set({ config: next });
|
||||||
|
debouncedSave(next);
|
||||||
},
|
},
|
||||||
|
|
||||||
setDownload: (patch) => {
|
setDownload: (patch) => {
|
||||||
const { config } = get();
|
const { config } = get();
|
||||||
set({ config: { ...config, download: { ...config.download, ...patch } } });
|
const next = { ...config, download: { ...config.download, ...patch } };
|
||||||
|
set({ config: next });
|
||||||
|
debouncedSave(next);
|
||||||
},
|
},
|
||||||
|
|
||||||
setNetwork: (patch) => {
|
setNetwork: (patch) => {
|
||||||
const { config } = get();
|
const { config } = get();
|
||||||
set({ config: { ...config, network: { ...config.network, ...patch } } });
|
const next = { ...config, network: { ...config.network, ...patch } };
|
||||||
|
set({ config: next });
|
||||||
|
debouncedSave(next);
|
||||||
|
},
|
||||||
|
|
||||||
|
setOobe: (value) => {
|
||||||
|
const { config } = get();
|
||||||
|
const next = { ...config, oobe: value };
|
||||||
|
set({ config: next });
|
||||||
|
debouncedSave(next);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
|
||||||
|
interface ConfirmDialogState {
|
||||||
|
open: boolean;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
confirmLabel: string;
|
||||||
|
countdown: number;
|
||||||
|
showCountdown: boolean;
|
||||||
|
onConfirm: () => void;
|
||||||
|
openDialog: (opts: {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
confirmLabel?: string;
|
||||||
|
countdown?: number;
|
||||||
|
onConfirm: () => void;
|
||||||
|
}) => void;
|
||||||
|
closeDialog: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useConfirmDialogStore = create<ConfirmDialogState>((set) => ({
|
||||||
|
open: false,
|
||||||
|
title: "",
|
||||||
|
description: "",
|
||||||
|
confirmLabel: "确认",
|
||||||
|
countdown: 0,
|
||||||
|
showCountdown: false,
|
||||||
|
onConfirm: () => {},
|
||||||
|
openDialog: (opts) =>
|
||||||
|
set({
|
||||||
|
open: true,
|
||||||
|
title: opts.title,
|
||||||
|
description: opts.description,
|
||||||
|
confirmLabel: opts.confirmLabel ?? "确认",
|
||||||
|
countdown: opts.countdown ?? 0,
|
||||||
|
showCountdown: (opts.countdown ?? 0) > 0,
|
||||||
|
onConfirm: opts.onConfirm,
|
||||||
|
}),
|
||||||
|
closeDialog: () => set({ open: false }),
|
||||||
|
}));
|
||||||
@@ -9,12 +9,18 @@ export type RouteKey =
|
|||||||
| "gallery"
|
| "gallery"
|
||||||
| "task-queue"
|
| "task-queue"
|
||||||
| "oobe"
|
| "oobe"
|
||||||
|
| "oobe/language"
|
||||||
|
| "oobe/agreement"
|
||||||
|
| "oobe/version"
|
||||||
|
| "oobe/beta-test"
|
||||||
|
| "oobe/finish"
|
||||||
| "oobe/about-info"
|
| "oobe/about-info"
|
||||||
| "debug"
|
| "debug"
|
||||||
| "debug-splash"
|
| "debug-splash"
|
||||||
| "debug-display"
|
| "debug-display"
|
||||||
| "debug-version-card"
|
| "debug-version-card"
|
||||||
| "debug-task";
|
| "debug-task"
|
||||||
|
| "debug-crash";
|
||||||
|
|
||||||
export type TitleBarMode = "default" | "sub" | "window" | "oobe";
|
export type TitleBarMode = "default" | "sub" | "window" | "oobe";
|
||||||
|
|
||||||
@@ -41,6 +47,11 @@ export const allRoutes: RouteItem[] = [
|
|||||||
...routes,
|
...routes,
|
||||||
{ key: "task-queue", label: "任务队列", path: "/task-queue", hidden: true },
|
{ key: "task-queue", label: "任务队列", path: "/task-queue", hidden: true },
|
||||||
{ key: "oobe", label: "OOBE", path: "/oobe", hidden: true },
|
{ key: "oobe", label: "OOBE", path: "/oobe", hidden: true },
|
||||||
|
{ key: "oobe/language", label: "语言设置", path: "/oobe/language", hidden: true },
|
||||||
|
{ key: "oobe/agreement", label: "同意协议", path: "/oobe/agreement", hidden: true },
|
||||||
|
{ key: "oobe/version", label: "当前版本", path: "/oobe/version", hidden: true },
|
||||||
|
{ key: "oobe/beta-test", label: "测试协议", path: "/oobe/beta-test", hidden: true },
|
||||||
|
{ key: "oobe/finish", label: "完成", path: "/oobe/finish", hidden: true },
|
||||||
{ key: "oobe/about-info", label: "关于信息", path: "/oobe/about-info", hidden: true, backable: true },
|
{ key: "oobe/about-info", label: "关于信息", path: "/oobe/about-info", hidden: true, backable: true },
|
||||||
{ key: "debug", label: "调试", path: "/debug", hidden: true },
|
{ key: "debug", label: "调试", path: "/debug", hidden: true },
|
||||||
{ key: "debug-splash", label: "启动动画调试", path: "/debug/splash", hidden: true },
|
{ key: "debug-splash", label: "启动动画调试", path: "/debug/splash", hidden: true },
|
||||||
@@ -52,7 +63,7 @@ export const allRoutes: RouteItem[] = [
|
|||||||
const topLevelKeys = new Set(routes.map((r) => r.key));
|
const topLevelKeys = new Set(routes.map((r) => r.key));
|
||||||
|
|
||||||
function getRouteTitleBarMode(key: RouteKey): TitleBarMode {
|
function getRouteTitleBarMode(key: RouteKey): TitleBarMode {
|
||||||
if (key === "oobe") return "oobe";
|
if (key === "oobe" || key.startsWith("oobe/")) return "oobe";
|
||||||
return topLevelKeys.has(key) ? "default" : "sub";
|
return topLevelKeys.has(key) ? "default" : "sub";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+9
@@ -11,7 +11,16 @@ interface ElectronAPI {
|
|||||||
|
|
||||||
getTheme: () => Promise<'light' | 'dark' | 'system' | null>;
|
getTheme: () => Promise<'light' | 'dark' | 'system' | null>;
|
||||||
|
|
||||||
|
onConfigPreload: (callback: (data: { config: unknown; isFirstLaunch: boolean }) => void) => () => void;
|
||||||
|
|
||||||
openExternal: (url: string) => Promise<void>;
|
openExternal: (url: string) => Promise<void>;
|
||||||
|
|
||||||
|
// Crash monitoring
|
||||||
|
simulateCrash: () => Promise<void>;
|
||||||
|
testCrashDialog: () => Promise<void>;
|
||||||
|
|
||||||
|
// Config reset
|
||||||
|
resetConfig: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
Vendored
+1
-6
@@ -1,11 +1,6 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {}
|
||||||
readonly VITE_START_POP: string;
|
|
||||||
readonly VITE_START_POP_TITLE: string;
|
|
||||||
readonly VITE_START_POP_INFO: string;
|
|
||||||
readonly VITE_START_POP_BOUTTON: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
readonly env: ImportMetaEnv;
|
readonly env: ImportMetaEnv;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export default defineConfig(async () => ({
|
|||||||
input: {
|
input: {
|
||||||
main: path.resolve(__dirname, "index.html"),
|
main: path.resolve(__dirname, "index.html"),
|
||||||
splash: path.resolve(__dirname, "splash.html"),
|
splash: path.resolve(__dirname, "splash.html"),
|
||||||
|
crash: path.resolve(__dirname, "crash.html"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user