mirror of
https://github.com/dream-pep/koring-launcher.git
synced 2026-09-12 05:45:18 +08:00
Add Koring auth flow and settings UI refactor
Implements Koring OIDC device-auth flow and integrates it into the app: core auth (TS/JS), IPC handlers, renderer API, KoringLogin React component, and a zustand koringAuth store. Handlers support device-code request, polling, refresh, get-user and logout and persist user info to existing auth/config. Also refactors many settings pages to use shared Setting components (SettingCard, SettingRow, SectionTitle, PageHeader) and replaces custom controls with @heroui/react primitives (Avatar, Button, Switch, RadioGroup, Slider, Input, TextArea, EmptyState). Adds OOBE login/welcome pages, a setting login route, installer header generator script, and a sample koring-auth.json for development.
This commit is contained in:
@@ -0,0 +1,175 @@
|
|||||||
|
"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;
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.requestDeviceCode = requestDeviceCode;
|
||||||
|
exports.pollForTokenOnce = pollForTokenOnce;
|
||||||
|
exports.refreshAccessToken = refreshAccessToken;
|
||||||
|
exports.decodeIdToken = decodeIdToken;
|
||||||
|
exports.saveKoringAuth = saveKoringAuth;
|
||||||
|
exports.readKoringAuth = readKoringAuth;
|
||||||
|
exports.deleteKoringAuth = deleteKoringAuth;
|
||||||
|
const https = __importStar(require("https"));
|
||||||
|
const auth_1 = require("../auth");
|
||||||
|
const CLIENT_ID = '547qe8ky1pr69f08b71kj';
|
||||||
|
const DEVICE_AUTH_URL = 'https://oac.lingke.ink/oidc/device/auth';
|
||||||
|
const TOKEN_URL = 'https://oac.lingke.ink/oidc/token';
|
||||||
|
const SCOPE = 'openid offline_access profile';
|
||||||
|
function postForm(url, data) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const params = new URLSearchParams(data);
|
||||||
|
const body = params.toString().replace(/\+/g, '%20');
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
console.log(`[koring-auth] POST ${url}`);
|
||||||
|
console.log(`[koring-auth] body: ${body}`);
|
||||||
|
const req = https.request({
|
||||||
|
hostname: urlObj.hostname,
|
||||||
|
port: urlObj.port || 443,
|
||||||
|
path: urlObj.pathname,
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'Content-Length': Buffer.byteLength(body),
|
||||||
|
},
|
||||||
|
}, (res) => {
|
||||||
|
let raw = '';
|
||||||
|
res.on('data', (chunk) => (raw += chunk));
|
||||||
|
res.on('end', () => {
|
||||||
|
console.log(`[koring-auth] response (${res.statusCode}): ${raw}`);
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(raw));
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
reject(new Error(`Invalid response: ${raw}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on('error', reject);
|
||||||
|
req.write(body);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function parseJwt(token) {
|
||||||
|
try {
|
||||||
|
const payload = token.split('.')[1];
|
||||||
|
const decoded = Buffer.from(payload, 'base64url').toString('utf-8');
|
||||||
|
return JSON.parse(decoded);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function requestDeviceCode() {
|
||||||
|
const res = await postForm(DEVICE_AUTH_URL, {
|
||||||
|
client_id: CLIENT_ID,
|
||||||
|
scope: SCOPE,
|
||||||
|
});
|
||||||
|
if (res.error)
|
||||||
|
throw new Error(res.error_description || res.error);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
async function pollForTokenOnce(device_code) {
|
||||||
|
const res = await postForm(TOKEN_URL, {
|
||||||
|
client_id: CLIENT_ID,
|
||||||
|
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
||||||
|
device_code,
|
||||||
|
});
|
||||||
|
if (res.access_token) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
if (res.error === 'expired_token' || res.error === 'access_denied') {
|
||||||
|
throw new Error(res.error);
|
||||||
|
}
|
||||||
|
// authorization_pending or slow_down — throw so caller can retry
|
||||||
|
throw new Error(res.error || 'authorization_pending');
|
||||||
|
}
|
||||||
|
async function refreshAccessToken(refresh_token) {
|
||||||
|
const res = await postForm(TOKEN_URL, {
|
||||||
|
client_id: CLIENT_ID,
|
||||||
|
grant_type: 'refresh_token',
|
||||||
|
refresh_token,
|
||||||
|
});
|
||||||
|
if (res.error)
|
||||||
|
throw new Error(res.error_description || res.error);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
function decodeIdToken(id_token) {
|
||||||
|
const payload = parseJwt(id_token);
|
||||||
|
return {
|
||||||
|
sub: payload.sub || '',
|
||||||
|
name: payload.name || '',
|
||||||
|
username: payload.username || payload.name || '',
|
||||||
|
email: payload.email || '',
|
||||||
|
picture: payload.picture || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function saveKoringAuth(tokenRes) {
|
||||||
|
const user = decodeIdToken(tokenRes.id_token);
|
||||||
|
const auth = {
|
||||||
|
user,
|
||||||
|
access_token: tokenRes.access_token,
|
||||||
|
refresh_token: tokenRes.refresh_token,
|
||||||
|
id_token: tokenRes.id_token,
|
||||||
|
expires_at: Date.now() + tokenRes.expires_in * 1000,
|
||||||
|
};
|
||||||
|
// Reuse existing auth file for storage
|
||||||
|
(0, auth_1.writeAuth)({
|
||||||
|
username: user.username,
|
||||||
|
uuid: user.sub,
|
||||||
|
accessToken: auth.access_token,
|
||||||
|
refreshToken: auth.refresh_token,
|
||||||
|
xboxProfile: JSON.stringify(user),
|
||||||
|
});
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
function readKoringAuth() {
|
||||||
|
const auth = (0, auth_1.readAuth)();
|
||||||
|
if (!auth.username || !auth.refreshToken)
|
||||||
|
return null;
|
||||||
|
let user = { sub: '', name: '', username: '', email: '', picture: '' };
|
||||||
|
try {
|
||||||
|
user = JSON.parse(auth.xboxProfile || '{}');
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
access_token: auth.accessToken,
|
||||||
|
refresh_token: auth.refreshToken,
|
||||||
|
id_token: '',
|
||||||
|
expires_at: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function deleteKoringAuth() {
|
||||||
|
(0, auth_1.writeAuth)({ username: '', uuid: '', accessToken: '', refreshToken: '', xboxProfile: '' });
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import * as https from 'https';
|
||||||
|
import { readAuth, writeAuth } from '../auth';
|
||||||
|
|
||||||
|
const CLIENT_ID = '547qe8ky1pr69f08b71kj';
|
||||||
|
const DEVICE_AUTH_URL = 'https://oac.lingke.ink/oidc/device/auth';
|
||||||
|
const TOKEN_URL = 'https://oac.lingke.ink/oidc/token';
|
||||||
|
const SCOPE = 'openid offline_access profile';
|
||||||
|
|
||||||
|
export interface DeviceAuthResponse {
|
||||||
|
device_code: string;
|
||||||
|
user_code: string;
|
||||||
|
verification_uri: string;
|
||||||
|
verification_uri_complete: string;
|
||||||
|
expires_in: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TokenResponse {
|
||||||
|
access_token: string;
|
||||||
|
id_token: string;
|
||||||
|
refresh_token: string;
|
||||||
|
token_type: string;
|
||||||
|
expires_in: number;
|
||||||
|
scope: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KoringUser {
|
||||||
|
sub: string;
|
||||||
|
name: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
picture: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function postForm(url: string, data: Record<string, string>): Promise<any> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const params = new URLSearchParams(data);
|
||||||
|
const body = params.toString().replace(/\+/g, '%20');
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
console.log(`[koring-auth] POST ${url}`);
|
||||||
|
console.log(`[koring-auth] body: ${body}`);
|
||||||
|
const req = https.request(
|
||||||
|
{
|
||||||
|
hostname: urlObj.hostname,
|
||||||
|
port: urlObj.port || 443,
|
||||||
|
path: urlObj.pathname,
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'Content-Length': Buffer.byteLength(body),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
(res) => {
|
||||||
|
let raw = '';
|
||||||
|
res.on('data', (chunk) => (raw += chunk));
|
||||||
|
res.on('end', () => {
|
||||||
|
console.log(`[koring-auth] response (${res.statusCode}): ${raw}`);
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(raw));
|
||||||
|
} catch {
|
||||||
|
reject(new Error(`Invalid response: ${raw}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
req.on('error', reject);
|
||||||
|
req.write(body);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJwt(token: string): Record<string, unknown> {
|
||||||
|
try {
|
||||||
|
const payload = token.split('.')[1];
|
||||||
|
const decoded = Buffer.from(payload, 'base64url').toString('utf-8');
|
||||||
|
return JSON.parse(decoded);
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function requestDeviceCode(): Promise<DeviceAuthResponse> {
|
||||||
|
const res = await postForm(DEVICE_AUTH_URL, {
|
||||||
|
client_id: CLIENT_ID,
|
||||||
|
scope: SCOPE,
|
||||||
|
});
|
||||||
|
if (res.error) throw new Error(res.error_description || res.error);
|
||||||
|
return res as DeviceAuthResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function pollForTokenOnce(
|
||||||
|
device_code: string
|
||||||
|
): Promise<TokenResponse> {
|
||||||
|
const res = await postForm(TOKEN_URL, {
|
||||||
|
client_id: CLIENT_ID,
|
||||||
|
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
||||||
|
device_code,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.access_token) {
|
||||||
|
return res as TokenResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.error === 'expired_token' || res.error === 'access_denied') {
|
||||||
|
throw new Error(res.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// authorization_pending or slow_down — throw so caller can retry
|
||||||
|
throw new Error(res.error || 'authorization_pending');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function refreshAccessToken(refresh_token: string): Promise<TokenResponse> {
|
||||||
|
const res = await postForm(TOKEN_URL, {
|
||||||
|
client_id: CLIENT_ID,
|
||||||
|
grant_type: 'refresh_token',
|
||||||
|
refresh_token,
|
||||||
|
});
|
||||||
|
if (res.error) throw new Error(res.error_description || res.error);
|
||||||
|
return res as TokenResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decodeIdToken(id_token: string): KoringUser {
|
||||||
|
const payload = parseJwt(id_token);
|
||||||
|
return {
|
||||||
|
sub: (payload.sub as string) || '',
|
||||||
|
name: (payload.name as string) || '',
|
||||||
|
username: (payload.username as string) || (payload.name as string) || '',
|
||||||
|
email: (payload.email as string) || '',
|
||||||
|
picture: (payload.picture as string) || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StoredKoringAuth {
|
||||||
|
user: KoringUser;
|
||||||
|
access_token: string;
|
||||||
|
refresh_token: string;
|
||||||
|
id_token: string;
|
||||||
|
expires_at: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveKoringAuth(tokenRes: TokenResponse): KoringUser {
|
||||||
|
const user = decodeIdToken(tokenRes.id_token);
|
||||||
|
const auth: StoredKoringAuth = {
|
||||||
|
user,
|
||||||
|
access_token: tokenRes.access_token,
|
||||||
|
refresh_token: tokenRes.refresh_token,
|
||||||
|
id_token: tokenRes.id_token,
|
||||||
|
expires_at: Date.now() + tokenRes.expires_in * 1000,
|
||||||
|
};
|
||||||
|
// Reuse existing auth file for storage
|
||||||
|
writeAuth({
|
||||||
|
username: user.username,
|
||||||
|
uuid: user.sub,
|
||||||
|
accessToken: auth.access_token,
|
||||||
|
refreshToken: auth.refresh_token,
|
||||||
|
xboxProfile: JSON.stringify(user),
|
||||||
|
});
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readKoringAuth(): StoredKoringAuth | null {
|
||||||
|
const auth = readAuth();
|
||||||
|
if (!auth.username || !auth.refreshToken) return null;
|
||||||
|
let user: KoringUser = { sub: '', name: '', username: '', email: '', picture: '' };
|
||||||
|
try {
|
||||||
|
user = JSON.parse(auth.xboxProfile || '{}') as KoringUser;
|
||||||
|
} catch {}
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
access_token: auth.accessToken,
|
||||||
|
refresh_token: auth.refreshToken,
|
||||||
|
id_token: '',
|
||||||
|
expires_at: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteKoringAuth(): void {
|
||||||
|
writeAuth({ username: '', uuid: '', accessToken: '', refreshToken: '', xboxProfile: '' });
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
"use strict";
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.registerKoringAuthHandlers = registerKoringAuthHandlers;
|
||||||
|
const electron_1 = __importDefault(require("electron"));
|
||||||
|
const { ipcMain } = electron_1.default;
|
||||||
|
const koring_auth_1 = require("../core/koring-auth");
|
||||||
|
const config_1 = require("../config");
|
||||||
|
function registerKoringAuthHandlers() {
|
||||||
|
ipcMain.handle('koring-auth:request-device-code', async () => {
|
||||||
|
try {
|
||||||
|
const result = await (0, koring_auth_1.requestDeviceCode)();
|
||||||
|
return { success: true, data: result, error: null };
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
return { success: false, data: null, error: String(e) };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ipcMain.handle('koring-auth:poll-token', async (_event, deviceCode) => {
|
||||||
|
try {
|
||||||
|
const result = await (0, koring_auth_1.pollForTokenOnce)(deviceCode);
|
||||||
|
const user = (0, koring_auth_1.saveKoringAuth)(result);
|
||||||
|
// 同时写入配置文件
|
||||||
|
try {
|
||||||
|
const config = (0, config_1.loadConfig)();
|
||||||
|
config.koringUser = {
|
||||||
|
sub: user.sub,
|
||||||
|
name: user.name,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
picture: user.picture,
|
||||||
|
accessToken: result.access_token,
|
||||||
|
refreshToken: result.refresh_token,
|
||||||
|
};
|
||||||
|
(0, config_1.saveConfig)(config);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error('[koring-auth] failed to save user to config:', e);
|
||||||
|
}
|
||||||
|
return { success: true, data: { user }, error: null };
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
|
return { success: false, data: null, error: msg };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ipcMain.handle('koring-auth:refresh', async () => {
|
||||||
|
try {
|
||||||
|
const stored = (0, koring_auth_1.readKoringAuth)();
|
||||||
|
if (!stored?.refresh_token)
|
||||||
|
throw new Error('No refresh token');
|
||||||
|
const result = await (0, koring_auth_1.refreshAccessToken)(stored.refresh_token);
|
||||||
|
const user = (0, koring_auth_1.saveKoringAuth)(result);
|
||||||
|
// 同步到配置文件
|
||||||
|
try {
|
||||||
|
const config = (0, config_1.loadConfig)();
|
||||||
|
config.koringUser = {
|
||||||
|
sub: user.sub,
|
||||||
|
name: user.name,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
picture: user.picture,
|
||||||
|
accessToken: result.access_token,
|
||||||
|
refreshToken: result.refresh_token,
|
||||||
|
};
|
||||||
|
(0, config_1.saveConfig)(config);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return { success: true, data: { user }, error: null };
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
return { success: false, data: null, error: String(e) };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ipcMain.handle('koring-auth:get-user', () => {
|
||||||
|
try {
|
||||||
|
const stored = (0, koring_auth_1.readKoringAuth)();
|
||||||
|
// 也从配置文件读取
|
||||||
|
if (!stored?.user?.sub) {
|
||||||
|
try {
|
||||||
|
const config = (0, config_1.loadConfig)();
|
||||||
|
const ku = config.koringUser;
|
||||||
|
if (ku?.sub) {
|
||||||
|
return { success: true, data: { user: ku, access_token: '', refresh_token: '', id_token: '', expires_at: 0 }, error: null };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
return { success: true, data: stored, error: null };
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
return { success: false, data: null, error: String(e) };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ipcMain.handle('koring-auth:logout', () => {
|
||||||
|
try {
|
||||||
|
(0, koring_auth_1.deleteKoringAuth)();
|
||||||
|
// 清除配置文件中的用户数据
|
||||||
|
try {
|
||||||
|
const config = (0, config_1.loadConfig)();
|
||||||
|
delete config.koringUser;
|
||||||
|
(0, config_1.saveConfig)(config);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return { success: true, data: null, error: null };
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
return { success: false, data: null, error: String(e) };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import electron from 'electron';
|
||||||
|
const { ipcMain } = electron;
|
||||||
|
import {
|
||||||
|
requestDeviceCode,
|
||||||
|
pollForTokenOnce,
|
||||||
|
refreshAccessToken,
|
||||||
|
saveKoringAuth,
|
||||||
|
readKoringAuth,
|
||||||
|
deleteKoringAuth,
|
||||||
|
} from '../core/koring-auth';
|
||||||
|
import { loadConfig, saveConfig } from '../config';
|
||||||
|
|
||||||
|
export function registerKoringAuthHandlers() {
|
||||||
|
ipcMain.handle('koring-auth:request-device-code', async () => {
|
||||||
|
try {
|
||||||
|
const result = await requestDeviceCode();
|
||||||
|
return { success: true, data: result, error: null };
|
||||||
|
} catch (e: unknown) {
|
||||||
|
return { success: false, data: null, error: String(e) };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('koring-auth:poll-token', async (_event, deviceCode: string) => {
|
||||||
|
try {
|
||||||
|
const result = await pollForTokenOnce(deviceCode);
|
||||||
|
const user = saveKoringAuth(result);
|
||||||
|
|
||||||
|
// 同时写入配置文件
|
||||||
|
try {
|
||||||
|
const config = loadConfig();
|
||||||
|
(config as any).koringUser = {
|
||||||
|
sub: user.sub,
|
||||||
|
name: user.name,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
picture: user.picture,
|
||||||
|
accessToken: result.access_token,
|
||||||
|
refreshToken: result.refresh_token,
|
||||||
|
};
|
||||||
|
saveConfig(config);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[koring-auth] failed to save user to config:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, data: { user }, error: null };
|
||||||
|
} catch (e: unknown) {
|
||||||
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
|
return { success: false, data: null, error: msg };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('koring-auth:refresh', async () => {
|
||||||
|
try {
|
||||||
|
const stored = readKoringAuth();
|
||||||
|
if (!stored?.refresh_token) throw new Error('No refresh token');
|
||||||
|
const result = await refreshAccessToken(stored.refresh_token);
|
||||||
|
const user = saveKoringAuth(result);
|
||||||
|
|
||||||
|
// 同步到配置文件
|
||||||
|
try {
|
||||||
|
const config = loadConfig();
|
||||||
|
(config as any).koringUser = {
|
||||||
|
sub: user.sub,
|
||||||
|
name: user.name,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
picture: user.picture,
|
||||||
|
accessToken: result.access_token,
|
||||||
|
refreshToken: result.refresh_token,
|
||||||
|
};
|
||||||
|
saveConfig(config);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return { success: true, data: { user }, error: null };
|
||||||
|
} catch (e: unknown) {
|
||||||
|
return { success: false, data: null, error: String(e) };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('koring-auth:get-user', () => {
|
||||||
|
try {
|
||||||
|
const stored = readKoringAuth();
|
||||||
|
// 也从配置文件读取
|
||||||
|
if (!stored?.user?.sub) {
|
||||||
|
try {
|
||||||
|
const config = loadConfig();
|
||||||
|
const ku = (config as any).koringUser;
|
||||||
|
if (ku?.sub) {
|
||||||
|
return { success: true, data: { user: ku, access_token: '', refresh_token: '', id_token: '', expires_at: 0 }, error: null };
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
return { success: true, data: stored, error: null };
|
||||||
|
} catch (e: unknown) {
|
||||||
|
return { success: false, data: null, error: String(e) };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('koring-auth:logout', () => {
|
||||||
|
try {
|
||||||
|
deleteKoringAuth();
|
||||||
|
// 清除配置文件中的用户数据
|
||||||
|
try {
|
||||||
|
const config = loadConfig();
|
||||||
|
delete (config as any).koringUser;
|
||||||
|
saveConfig(config);
|
||||||
|
} catch {}
|
||||||
|
return { success: true, data: null, error: null };
|
||||||
|
} catch (e: unknown) {
|
||||||
|
return { success: false, data: null, error: String(e) };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"username": "dream_pep",
|
||||||
|
"uuid": "rqkzikchwvjt",
|
||||||
|
"accessToken": "Nsk5O07OxX_oDQRFHWdIh6oDNHB8WxleTsAi3yAUtCL",
|
||||||
|
"refreshToken": "rFvsd9kmvyiBYNLID8A5xmeWTqF-Efselc21ug3cGiy",
|
||||||
|
"xboxProfile": "{\"sub\":\"rqkzikchwvjt\",\"name\":\"周逸\",\"username\":\"dream_pep\",\"email\":\"\",\"picture\":\"\"}"
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// Run: node scripts/gen-installer-header.js
|
||||||
|
const { createCanvas } = require('canvas');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const c = createCanvas(493, 58);
|
||||||
|
const ctx = c.getContext('2d');
|
||||||
|
|
||||||
|
const g = ctx.createLinearGradient(0, 0, 493, 0);
|
||||||
|
g.addColorStop(0, '#1a1a2e');
|
||||||
|
g.addColorStop(1, '#16213e');
|
||||||
|
ctx.fillStyle = g;
|
||||||
|
ctx.fillRect(0, 0, 493, 58);
|
||||||
|
|
||||||
|
ctx.fillStyle = '#ffffff';
|
||||||
|
ctx.font = 'bold 24px sans-serif';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.fillText('Koring Launcher', 246, 29);
|
||||||
|
|
||||||
|
const buf = c.toBuffer('image/png');
|
||||||
|
const out = path.resolve(__dirname, '..', 'build', 'installer-header.png');
|
||||||
|
fs.writeFileSync(out, buf);
|
||||||
|
console.log('Created:', out);
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { ipcInvoke } from './ipc';
|
||||||
|
|
||||||
|
export interface DeviceAuthResponse {
|
||||||
|
device_code: string;
|
||||||
|
user_code: string;
|
||||||
|
verification_uri: string;
|
||||||
|
verification_uri_complete: string;
|
||||||
|
expires_in: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KoringUser {
|
||||||
|
sub: string;
|
||||||
|
name: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
picture: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KoringAuthData {
|
||||||
|
user: KoringUser;
|
||||||
|
access_token: string;
|
||||||
|
refresh_token: string;
|
||||||
|
id_token: string;
|
||||||
|
expires_at: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function requestDeviceCode(): Promise<DeviceAuthResponse> {
|
||||||
|
return ipcInvoke<DeviceAuthResponse>('koring-auth:request-device-code');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function pollForToken(
|
||||||
|
device_code: string
|
||||||
|
): Promise<{ user: KoringUser }> {
|
||||||
|
return ipcInvoke<{ user: KoringUser }>('koring-auth:poll-token', device_code);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function refreshKoringToken(): Promise<{ user: KoringUser }> {
|
||||||
|
return ipcInvoke<{ user: KoringUser }>('koring-auth:refresh');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getKoringUser(): Promise<KoringAuthData | null> {
|
||||||
|
return ipcInvoke<KoringAuthData | null>('koring-auth:get-user');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function logoutKoring(): Promise<void> {
|
||||||
|
return ipcInvoke<void>('koring-auth:logout');
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
import { useState, useEffect, useRef, useCallback } from "react";
|
||||||
|
import { QRCodeSVG } from "qrcode.react";
|
||||||
|
import {
|
||||||
|
requestDeviceCode,
|
||||||
|
pollForToken,
|
||||||
|
type KoringUser,
|
||||||
|
} from "@/api/koring-auth";
|
||||||
|
import { useKoringAuthStore } from "@/stores/koringAuthStore";
|
||||||
|
import { Loader2, CheckCircle2, Copy, ExternalLink } from "lucide-react";
|
||||||
|
|
||||||
|
type Step = "loading" | "scan" | "polling" | "success" | "error";
|
||||||
|
|
||||||
|
interface KoringLoginProps {
|
||||||
|
onLoginSuccess?: (user: KoringUser) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function KoringLogin({ onLoginSuccess }: KoringLoginProps) {
|
||||||
|
const [step, setStep] = useState<Step>("loading");
|
||||||
|
const [userCode, setUserCode] = useState("");
|
||||||
|
const [verifyUri, setVerifyUri] = useState("");
|
||||||
|
const [verifyUriComplete, setVerifyUriComplete] = useState("");
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
|
const setUser = useKoringAuthStore((s) => s.setUser);
|
||||||
|
|
||||||
|
const cleanup = useCallback(() => {
|
||||||
|
if (pollRef.current) {
|
||||||
|
clearInterval(pollRef.current);
|
||||||
|
pollRef.current = null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => cleanup, [cleanup]);
|
||||||
|
|
||||||
|
// Auto-start on mount
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const res = await requestDeviceCode();
|
||||||
|
if (cancelled) return;
|
||||||
|
setUserCode(res.user_code);
|
||||||
|
setVerifyUri(res.verification_uri);
|
||||||
|
setVerifyUriComplete(res.verification_uri_complete);
|
||||||
|
setStep("scan");
|
||||||
|
startPolling(res.device_code, res.expires_in, cancelled);
|
||||||
|
} catch (e: any) {
|
||||||
|
if (!cancelled) {
|
||||||
|
setError(e.message || "请求设备码失败");
|
||||||
|
setStep("error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return () => { cancelled = true; cleanup(); };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const startPolling = (deviceCode: string, expires: number, cancelled: boolean) => {
|
||||||
|
setStep("polling");
|
||||||
|
const startTime = Date.now();
|
||||||
|
pollRef.current = setInterval(async () => {
|
||||||
|
if (cancelled) { cleanup(); return; }
|
||||||
|
if (Date.now() - startTime > expires * 1000) {
|
||||||
|
cleanup();
|
||||||
|
setError("二维码已过期,请重新获取");
|
||||||
|
setStep("error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await pollForToken(deviceCode);
|
||||||
|
if (cancelled) return;
|
||||||
|
cleanup();
|
||||||
|
setUser(result.user);
|
||||||
|
setStep("success");
|
||||||
|
onLoginSuccess?.(result.user);
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.message !== "authorization_pending" && e.message !== "slow_down") {
|
||||||
|
if (!cancelled) {
|
||||||
|
cleanup();
|
||||||
|
setError(e.message || "验证失败");
|
||||||
|
setStep("error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const retry = async () => {
|
||||||
|
setStep("loading");
|
||||||
|
setError("");
|
||||||
|
try {
|
||||||
|
const res = await requestDeviceCode();
|
||||||
|
setUserCode(res.user_code);
|
||||||
|
setVerifyUri(res.verification_uri);
|
||||||
|
setVerifyUriComplete(res.verification_uri_complete);
|
||||||
|
setStep("scan");
|
||||||
|
startPolling(res.device_code, res.expires_in, false);
|
||||||
|
} catch (e: any) {
|
||||||
|
setError(e.message || "请求设备码失败");
|
||||||
|
setStep("error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyCode = () => {
|
||||||
|
navigator.clipboard.writeText(userCode);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openVerify = () => {
|
||||||
|
window.electronAPI?.openExternal(verifyUri);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (step === "loading") {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center gap-3 py-6">
|
||||||
|
<Loader2 className="w-6 h-6 animate-spin text-foreground/40" />
|
||||||
|
<span className="text-sm text-muted-foreground">正在获取设备码...</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step === "error") {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center gap-3 py-4">
|
||||||
|
<p className="text-sm text-red-500">{error}</p>
|
||||||
|
<button
|
||||||
|
onClick={retry}
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
{/* QR Code */}
|
||||||
|
<div className="p-3 bg-white rounded-xl">
|
||||||
|
<QRCodeSVG value={verifyUriComplete} size={160} level="M" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 提示文字 */}
|
||||||
|
<p className="text-sm text-muted-foreground text-center leading-relaxed">
|
||||||
|
请使用任意二维码扫描器打开
|
||||||
|
<br />
|
||||||
|
或访问以下链接并输入验证码
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* 验证链接 */}
|
||||||
|
<button
|
||||||
|
onClick={openVerify}
|
||||||
|
className="inline-flex items-center gap-1.5 text-[13px] text-primary hover:underline"
|
||||||
|
>
|
||||||
|
{verifyUri}
|
||||||
|
<ExternalLink className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* 用户码 */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-2xl font-mono font-bold tracking-[0.3em] text-foreground">
|
||||||
|
{userCode}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={copyCode}
|
||||||
|
className="p-1.5 rounded-md hover:bg-foreground/[0.06] transition-colors"
|
||||||
|
title="复制验证码"
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<CheckCircle2 className="w-4 h-4 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<Copy className="w-4 h-4 text-foreground/40" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 轮询状态 */}
|
||||||
|
{step === "polling" && (
|
||||||
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
|
<Loader2 className="w-3 h-3 animate-spin" />
|
||||||
|
等待验证中...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === "success" && (
|
||||||
|
<div className="flex items-center gap-2 text-sm text-green-600 dark:text-green-400">
|
||||||
|
<CheckCircle2 className="w-4 h-4" />
|
||||||
|
登录成功
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
export function PageHeader({ title, desc }: { title: string; desc: string }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h2 className="text-xl font-bold text-foreground mb-1">{title}</h2>
|
||||||
|
<p className="text-sm text-muted-foreground mb-6">{desc}</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SectionTitle({ children }: { children: React.ReactNode }) {
|
||||||
|
return <h3 className="text-lg font-bold text-foreground mb-3">{children}</h3>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Card } from "@heroui/react";
|
||||||
|
|
||||||
|
export function SettingCard({ children, className }: { children: React.ReactNode; className?: string }) {
|
||||||
|
return (
|
||||||
|
<Card variant="transparent" className={`glass-card px-5 py-4 ${className ?? ""}`}>
|
||||||
|
{children}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export function SettingRow({ label, desc, children }: { label: string; desc?: string; children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-medium text-foreground">{label}</p>
|
||||||
|
{desc && <p className="text-[13px] text-muted-foreground mt-0.5">{desc}</p>}
|
||||||
|
</div>
|
||||||
|
<div className="shrink-0">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { SettingCard } from "./SettingCard";
|
||||||
|
export { SettingRow } from "./SettingRow";
|
||||||
|
export { PageHeader, SectionTitle } from "./SectionTitle";
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { useState, useEffect, useCallback } from "react";
|
||||||
|
import { useRouteStore } from "@/stores/routeStore";
|
||||||
|
import { useKoringAuthStore } from "@/stores/koringAuthStore";
|
||||||
|
import { OobeLayout } from "./layout";
|
||||||
|
import { NextButton } from "./next-button";
|
||||||
|
import { KoringLogin } from "@/components/KoringLogin";
|
||||||
|
|
||||||
|
export function OobeLogin() {
|
||||||
|
const navigate = useRouteStore((s) => s.navigate);
|
||||||
|
const user = useKoringAuthStore((s) => s.user);
|
||||||
|
const [showSkip, setShowSkip] = useState(false);
|
||||||
|
const [justLoggedIn, setJustLoggedIn] = useState(false);
|
||||||
|
|
||||||
|
// 10秒后显示"我暂时不需要"
|
||||||
|
useEffect(() => {
|
||||||
|
const t = setTimeout(() => setShowSkip(true), 10000);
|
||||||
|
return () => clearTimeout(t);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSuccess = useCallback(() => {
|
||||||
|
setJustLoggedIn(true);
|
||||||
|
setTimeout(() => navigate("oobe/agreement"), 1500);
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OobeLayout>
|
||||||
|
<div className="w-full max-w-md flex flex-col items-center gap-6 px-6">
|
||||||
|
{/* 标题 */}
|
||||||
|
<div className="text-center space-y-1">
|
||||||
|
<h2 className="text-lg font-bold text-foreground">登录 Koring 账户</h2>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
扫码登录以同步数据、皮肤与个人配置
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 登录组件 — 自动显示 QR */}
|
||||||
|
<KoringLogin onLoginSuccess={handleSuccess} />
|
||||||
|
|
||||||
|
{/* 已登录提示 */}
|
||||||
|
{justLoggedIn && (
|
||||||
|
<p className="text-xs text-green-600 dark:text-green-400">
|
||||||
|
登录成功,正在跳转...
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 下一步按钮 — 需要登录才能点击 */}
|
||||||
|
<NextButton onClick={() => navigate("oobe/agreement")} disabled={!user && !justLoggedIn} />
|
||||||
|
|
||||||
|
{/* 跳过按钮 — 10秒后显示 */}
|
||||||
|
{showSkip && !user && !justLoggedIn && (
|
||||||
|
<div className="absolute bottom-6">
|
||||||
|
<button
|
||||||
|
onClick={() => navigate("oobe/agreement")}
|
||||||
|
className="text-[12px] text-foreground/30 hover:text-foreground/50 transition-colors"
|
||||||
|
>
|
||||||
|
我暂时不需要
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</OobeLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useRouteStore } from "@/stores/routeStore";
|
||||||
|
import { useKoringAuthStore } from "@/stores/koringAuthStore";
|
||||||
|
import { OobeLayout } from "./layout";
|
||||||
|
import { NextButton } from "./next-button";
|
||||||
|
|
||||||
|
export function OobeWelcome() {
|
||||||
|
const navigate = useRouteStore((s) => s.navigate);
|
||||||
|
const user = useKoringAuthStore((s) => s.user);
|
||||||
|
const [showBtn, setShowBtn] = useState(false);
|
||||||
|
const [animClass, setAnimClass] = useState("scale-90 opacity-0");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setAnimClass("scale-100 opacity-100");
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const t = setTimeout(() => setShowBtn(true), 1500);
|
||||||
|
return () => clearTimeout(t);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const displayName = user?.name || user?.username || "用户";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OobeLayout>
|
||||||
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
<h2
|
||||||
|
className={`text-4xl font-bold text-foreground transition-all duration-700 ease-out ${animClass}`}
|
||||||
|
>
|
||||||
|
欢迎回来,{displayName}。
|
||||||
|
</h2>
|
||||||
|
{user?.sub && (
|
||||||
|
<p className="text-xs text-muted-foreground font-mono">
|
||||||
|
UUID: {user.sub}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showBtn && <NextButton onClick={() => navigate("oobe/version")} />}
|
||||||
|
</OobeLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,21 +1,6 @@
|
|||||||
import { useConfigStore } from "@/stores/configStore";
|
import { useConfigStore } from "@/stores/configStore";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch, RadioGroup, Radio, Input, TextArea } from "@heroui/react";
|
||||||
|
import { SettingCard, SettingRow, PageHeader, SectionTitle } from "@/components/setting";
|
||||||
function GlassCard({ children }: { children: React.ReactNode }) {
|
|
||||||
return <div className="glass-card px-5 py-4">{children}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SettingRow({ label, desc, children }: { label: string; desc?: string; children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between gap-4">
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<p className="text-sm font-medium text-foreground">{label}</p>
|
|
||||||
{desc && <p className="text-[13px] text-muted-foreground mt-0.5">{desc}</p>}
|
|
||||||
</div>
|
|
||||||
<div className="shrink-0">{children}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const launcherBehavior = [
|
const launcherBehavior = [
|
||||||
{ value: "close", label: "关闭启动器" },
|
{ value: "close", label: "关闭启动器" },
|
||||||
@@ -35,125 +20,127 @@ export function AdvancedSetting() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-foreground mb-1">高级设置</h2>
|
<PageHeader title="高级设置" desc="游戏高级启动参数、调试选项与实验性功能" />
|
||||||
<p className="text-sm text-muted-foreground mb-6">游戏高级启动参数、调试选项与实验性功能</p>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* ===== 启动行为 ===== */}
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-foreground mb-3">启动行为</h3>
|
<SectionTitle>启动行为</SectionTitle>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<GlassCard>
|
<SettingCard>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-sm font-medium text-foreground">启动后启动器行为</p>
|
<p className="text-sm font-medium text-foreground">启动后启动器行为</p>
|
||||||
<p className="text-[13px] text-muted-foreground">游戏启动后启动器的处理方式</p>
|
<p className="text-[13px] text-muted-foreground">游戏启动后启动器的处理方式</p>
|
||||||
<div className="space-y-2 mt-2">
|
<RadioGroup
|
||||||
|
value={adv.afterLaunch}
|
||||||
|
onValueChange={(v) => setAdvanced({ afterLaunch: v })}
|
||||||
|
className="mt-2 space-y-2"
|
||||||
|
>
|
||||||
{launcherBehavior.map((opt) => (
|
{launcherBehavior.map((opt) => (
|
||||||
<label key={opt.value} className="flex items-center gap-2 cursor-pointer">
|
<Radio key={opt.value} value={opt.value}>
|
||||||
<input type="radio" name="afterLaunch" checked={adv.afterLaunch === opt.value} onChange={() => setAdvanced({ afterLaunch: opt.value })} className="accent-primary" />
|
<Radio.Content>{opt.label}</Radio.Content>
|
||||||
<span className="text-sm text-foreground">{opt.label}</span>
|
</Radio>
|
||||||
</label>
|
|
||||||
))}
|
))}
|
||||||
|
</RadioGroup>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</SettingCard>
|
||||||
</GlassCard>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ===== 窗口设置 ===== */}
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-foreground mb-3">窗口设置</h3>
|
<SectionTitle>窗口设置</SectionTitle>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<GlassCard>
|
<SettingCard>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-sm font-medium text-foreground">窗口大小</p>
|
<p className="text-sm font-medium text-foreground">窗口大小</p>
|
||||||
<div className="space-y-2">
|
<RadioGroup
|
||||||
|
value={adv.winMode}
|
||||||
|
onValueChange={(v) => setAdvanced({ winMode: v })}
|
||||||
|
className="space-y-2"
|
||||||
|
>
|
||||||
{windowSize.map((opt) => (
|
{windowSize.map((opt) => (
|
||||||
<label key={opt.value} className="flex items-center gap-2 cursor-pointer">
|
<Radio key={opt.value} value={opt.value}>
|
||||||
<input type="radio" name="winMode" checked={adv.winMode === opt.value} onChange={() => setAdvanced({ winMode: opt.value })} className="accent-primary" />
|
<Radio.Content>{opt.label}</Radio.Content>
|
||||||
<span className="text-sm text-foreground">{opt.label}</span>
|
</Radio>
|
||||||
</label>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</RadioGroup>
|
||||||
</div>
|
</div>
|
||||||
{adv.winMode === "custom" && (
|
{adv.winMode === "custom" && (
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-[13px] text-muted-foreground">宽</span>
|
<span className="text-[13px] text-muted-foreground">宽</span>
|
||||||
<input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={adv.customWidth}
|
value={String(adv.customWidth)}
|
||||||
onChange={(e) => setAdvanced({ customWidth: Number(e.target.value) })}
|
onChange={(e) => setAdvanced({ customWidth: Number(e.target.value) })}
|
||||||
className="w-20 h-8 px-2 rounded-md border border-input bg-background text-sm text-center font-mono focus:outline-none focus:ring-1 focus:ring-ring"
|
className="w-20"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-muted-foreground">×</span>
|
<span className="text-muted-foreground">×</span>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-[13px] text-muted-foreground">高</span>
|
<span className="text-[13px] text-muted-foreground">高</span>
|
||||||
<input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={adv.customHeight}
|
value={String(adv.customHeight)}
|
||||||
onChange={(e) => setAdvanced({ customHeight: Number(e.target.value) })}
|
onChange={(e) => setAdvanced({ customHeight: Number(e.target.value) })}
|
||||||
className="w-20 h-8 px-2 rounded-md border border-input bg-background text-sm text-center font-mono focus:outline-none focus:ring-1 focus:ring-ring"
|
className="w-20"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</GlassCard>
|
</SettingCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ===== 游戏参数 ===== */}
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-foreground mb-3">游戏参数</h3>
|
<SectionTitle>游戏参数</SectionTitle>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<GlassCard>
|
<SettingCard>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-sm font-medium text-foreground">额外游戏启动参数</p>
|
<p className="text-sm font-medium text-foreground">额外游戏启动参数</p>
|
||||||
<p className="text-[13px] text-muted-foreground">附加到游戏启动命令末尾的参数</p>
|
<p className="text-[13px] text-muted-foreground">附加到游戏启动命令末尾的参数</p>
|
||||||
<input
|
<Input
|
||||||
type="text"
|
|
||||||
value={adv.gameArgs}
|
value={adv.gameArgs}
|
||||||
onChange={(e) => setAdvanced({ gameArgs: e.target.value })}
|
onChange={(e) => setAdvanced({ gameArgs: e.target.value })}
|
||||||
placeholder="可选,例如 --demo"
|
placeholder="可选,例如 --demo"
|
||||||
className="w-full h-8 px-3 rounded-md border border-input bg-background text-sm font-mono placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
|
fullWidth
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</GlassCard>
|
</SettingCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ===== 启动命令 ===== */}
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-foreground mb-3">启动命令</h3>
|
<SectionTitle>启动命令</SectionTitle>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<GlassCard>
|
<SettingCard>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-sm font-medium text-foreground">启动前执行命令</p>
|
<p className="text-sm font-medium text-foreground">启动前执行命令</p>
|
||||||
<p className="text-[13px] text-muted-foreground">游戏启动前自动执行的命令或程序路径</p>
|
<p className="text-[13px] text-muted-foreground">游戏启动前自动执行的命令或程序路径</p>
|
||||||
<input
|
<Input
|
||||||
type="text"
|
|
||||||
value={adv.preLaunchCmd}
|
value={adv.preLaunchCmd}
|
||||||
onChange={(e) => setAdvanced({ preLaunchCmd: e.target.value })}
|
onChange={(e) => setAdvanced({ preLaunchCmd: e.target.value })}
|
||||||
placeholder="可选,例如 D:\scripts\pre-launch.bat"
|
placeholder="可选,例如 D:\scripts\pre-launch.bat"
|
||||||
className="w-full h-8 px-3 rounded-md border border-input bg-background text-sm font-mono placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
|
fullWidth
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</GlassCard>
|
</SettingCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ===== 调试 ===== */}
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-foreground mb-3">调试</h3>
|
<SectionTitle>调试</SectionTitle>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<GlassCard>
|
<SettingCard>
|
||||||
<SettingRow label="调试模式" desc="启用后将在控制台输出详细日志,可能影响性能">
|
<SettingRow label="调试模式" desc="启用后将在控制台输出详细日志,可能影响性能">
|
||||||
<Switch checked={adv.debugMode} onCheckedChange={(v) => setAdvanced({ debugMode: v })} />
|
<Switch isSelected={adv.debugMode} onValueChange={(v) => setAdvanced({ debugMode: v })}>
|
||||||
|
<Switch.Control>
|
||||||
|
<Switch.Thumb />
|
||||||
|
</Switch.Control>
|
||||||
|
</Switch>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
</GlassCard>
|
</SettingCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
|
import { EmptyState } from "@heroui/react";
|
||||||
|
import { Gamepad2 } from "lucide-react";
|
||||||
|
import { PageHeader } from "@/components/setting";
|
||||||
|
|
||||||
export function GameAccountSetting() {
|
export function GameAccountSetting() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-foreground mb-1">游戏账户&档案</h2>
|
<PageHeader title="游戏账户&档案" desc="管理 Minecraft 游戏内账户、正版验证与游戏档案配置" />
|
||||||
<p className="text-sm text-muted-foreground mb-6">管理 Minecraft 游戏内账户、正版验证与游戏档案配置</p>
|
<EmptyState className="py-16">
|
||||||
|
<Gamepad2 className="w-10 h-10 text-muted-foreground/30" />
|
||||||
|
<p className="text-sm text-muted-foreground mt-3">该功能正在开发中</p>
|
||||||
|
</EmptyState>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,26 @@
|
|||||||
import { useConfigStore } from "@/stores/configStore";
|
import { useConfigStore } from "@/stores/configStore";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button, Input } from "@heroui/react";
|
||||||
import { RefreshCw } from "lucide-react";
|
import { RefreshCw } from "lucide-react";
|
||||||
|
import { SettingCard, PageHeader, SectionTitle } from "@/components/setting";
|
||||||
function GlassCard({ children }: { children: React.ReactNode }) {
|
|
||||||
return <div className="glass-card px-5 py-4">{children}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function PathInput({ label, desc, value, onChange }: { label: string; desc: string; value: string; onChange: (v: string) => void }) {
|
function PathInput({ label, desc, value, onChange }: { label: string; desc: string; value: string; onChange: (v: string) => void }) {
|
||||||
return (
|
return (
|
||||||
<GlassCard>
|
<SettingCard>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-foreground">{label}</p>
|
<p className="text-sm font-medium text-foreground">{label}</p>
|
||||||
<p className="text-[13px] text-muted-foreground mt-0.5">{desc}</p>
|
<p className="text-[13px] text-muted-foreground mt-0.5">{desc}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<Input
|
||||||
type="text"
|
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
className="flex-1 h-8 px-3 rounded-md border border-input bg-background text-sm font-mono placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
|
fullWidth
|
||||||
/>
|
/>
|
||||||
<Button size="sm" variant="outline">浏览</Button>
|
<Button size="sm" variant="outline">浏览</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</GlassCard>
|
</SettingCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,13 +30,11 @@ export function GameDirSetting() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-foreground mb-1">游戏目录</h2>
|
<PageHeader title="游戏目录" desc="设置 Minecraft 游戏安装路径、存档位置与资源包目录" />
|
||||||
<p className="text-sm text-muted-foreground mb-6">设置 Minecraft 游戏安装路径、存档位置与资源包目录</p>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* ===== 游戏路径 ===== */}
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-foreground mb-3">游戏路径</h3>
|
<SectionTitle>游戏路径</SectionTitle>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<PathInput
|
<PathInput
|
||||||
label="游戏安装根目录"
|
label="游戏安装根目录"
|
||||||
@@ -48,7 +42,7 @@ export function GameDirSetting() {
|
|||||||
value={game.gameDir}
|
value={game.gameDir}
|
||||||
onChange={(v) => setGame({ gameDir: v })}
|
onChange={(v) => setGame({ gameDir: v })}
|
||||||
/>
|
/>
|
||||||
<GlassCard>
|
<SettingCard>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-foreground">已安装版本</p>
|
<p className="text-sm font-medium text-foreground">已安装版本</p>
|
||||||
@@ -59,13 +53,12 @@ export function GameDirSetting() {
|
|||||||
扫描
|
扫描
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</GlassCard>
|
</SettingCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ===== 资源包 ===== */}
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-foreground mb-3">资源包目录</h3>
|
<SectionTitle>资源包目录</SectionTitle>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<PathInput
|
<PathInput
|
||||||
label="资源包路径"
|
label="资源包路径"
|
||||||
@@ -76,9 +69,8 @@ export function GameDirSetting() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ===== 存档 ===== */}
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-foreground mb-3">存档目录</h3>
|
<SectionTitle>存档目录</SectionTitle>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<PathInput
|
<PathInput
|
||||||
label="存档路径"
|
label="存档路径"
|
||||||
|
|||||||
@@ -1,23 +1,7 @@
|
|||||||
import { useConfigStore } from "@/stores/configStore";
|
import { useConfigStore } from "@/stores/configStore";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button, Slider, RadioGroup, Radio, Input, TextArea } from "@heroui/react";
|
||||||
import { Slider } from "@/components/ui/slider";
|
|
||||||
import { Cpu, FolderSearch } from "lucide-react";
|
import { Cpu, FolderSearch } from "lucide-react";
|
||||||
|
import { SettingCard, SettingRow, PageHeader, SectionTitle } from "@/components/setting";
|
||||||
function GlassCard({ children }: { children: React.ReactNode }) {
|
|
||||||
return <div className="glass-card px-5 py-4">{children}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SettingRow({ label, desc, children }: { label: string; desc?: string; children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between gap-4">
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<p className="text-sm font-medium text-foreground">{label}</p>
|
|
||||||
{desc && <p className="text-[13px] text-muted-foreground mt-0.5">{desc}</p>}
|
|
||||||
</div>
|
|
||||||
<div className="shrink-0">{children}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface JavaInfo {
|
interface JavaInfo {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -42,25 +26,23 @@ export function JavaMemSetting() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-foreground mb-1">Java 虚拟机与内存</h2>
|
<PageHeader title="Java 虚拟机与内存" desc="配置 Java 运行环境路径、JVM 参数与游戏内存分配" />
|
||||||
<p className="text-sm text-muted-foreground mb-6">配置 Java 运行环境路径、JVM 参数与游戏内存分配</p>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* ===== Java 环境 ===== */}
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-foreground mb-3">Java 环境</h3>
|
<SectionTitle>Java 环境</SectionTitle>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<GlassCard>
|
<SettingCard>
|
||||||
<SettingRow label="自动检测" desc="扫描系统中已安装的 Java 版本">
|
<SettingRow label="自动检测" desc="扫描系统中已安装的 Java 版本">
|
||||||
<Button size="sm" variant="outline">
|
<Button size="sm" variant="outline">
|
||||||
<FolderSearch className="w-3.5 h-3.5 mr-1.5" />
|
<FolderSearch className="w-3.5 h-3.5 mr-1.5" />
|
||||||
检测
|
检测
|
||||||
</Button>
|
</Button>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
</GlassCard>
|
</SettingCard>
|
||||||
|
|
||||||
{mockJavaList.map((j) => (
|
{mockJavaList.map((j) => (
|
||||||
<GlassCard key={j.path}>
|
<SettingCard key={j.path}>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Cpu className="w-4 h-4 text-muted-foreground shrink-0" />
|
<Cpu className="w-4 h-4 text-muted-foreground shrink-0" />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
@@ -71,47 +53,47 @@ export function JavaMemSetting() {
|
|||||||
{j.path}
|
{j.path}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button size="sm" variant="outline" className="shrink-0" onClick={() => setJava({ javaPath: j.path })}>
|
<Button size="sm" variant="outline" className="shrink-0" onPress={() => setJava({ javaPath: j.path })}>
|
||||||
使用
|
使用
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</GlassCard>
|
</SettingCard>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<GlassCard>
|
<SettingCard>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-sm font-medium text-foreground">手动指定路径</p>
|
<p className="text-sm font-medium text-foreground">手动指定路径</p>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<Input
|
||||||
type="text"
|
|
||||||
value={java.javaPath}
|
value={java.javaPath}
|
||||||
onChange={(e) => setJava({ javaPath: e.target.value })}
|
onChange={(e) => setJava({ javaPath: e.target.value })}
|
||||||
placeholder="输入 javaw.exe 完整路径"
|
placeholder="输入 javaw.exe 完整路径"
|
||||||
className="flex-1 h-8 px-3 rounded-md border border-input bg-background text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
|
fullWidth
|
||||||
/>
|
/>
|
||||||
<Button size="sm" variant="outline">浏览</Button>
|
<Button size="sm" variant="outline">浏览</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</GlassCard>
|
</SettingCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ===== 内存分配 ===== */}
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-foreground mb-3">内存分配</h3>
|
<SectionTitle>内存分配</SectionTitle>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<GlassCard>
|
<SettingCard>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center gap-4">
|
<RadioGroup
|
||||||
<label className="flex items-center gap-2 cursor-pointer">
|
value={java.memMode}
|
||||||
<input type="radio" name="mem" checked={java.memMode === "auto"} onChange={() => setJava({ memMode: "auto" })} className="accent-primary" />
|
onValueChange={(v) => setJava({ memMode: v })}
|
||||||
<span className="text-sm text-foreground">自动配置</span>
|
className="flex items-center gap-4"
|
||||||
</label>
|
>
|
||||||
<label className="flex items-center gap-2 cursor-pointer">
|
<Radio value="auto">
|
||||||
<input type="radio" name="mem" checked={java.memMode === "custom"} onChange={() => setJava({ memMode: "custom" })} className="accent-primary" />
|
<Radio.Content>自动配置</Radio.Content>
|
||||||
<span className="text-sm text-foreground">自定义</span>
|
</Radio>
|
||||||
</label>
|
<Radio value="custom">
|
||||||
</div>
|
<Radio.Content>自定义</Radio.Content>
|
||||||
|
</Radio>
|
||||||
|
</RadioGroup>
|
||||||
{java.memMode === "custom" && (
|
{java.memMode === "custom" && (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-1.5">
|
<div className="flex items-center justify-between mb-1.5">
|
||||||
@@ -119,12 +101,17 @@ export function JavaMemSetting() {
|
|||||||
<span className="text-[13px] text-muted-foreground tabular-nums">{java.memGB} GB</span>
|
<span className="text-[13px] text-muted-foreground tabular-nums">{java.memGB} GB</span>
|
||||||
</div>
|
</div>
|
||||||
<Slider
|
<Slider
|
||||||
value={[java.memGB]}
|
value={java.memGB}
|
||||||
onValueChange={(v) => setJava({ memGB: Array.isArray(v) ? v[0] : v })}
|
onChange={(v) => setJava({ memGB: typeof v === "number" ? v : v[0] })}
|
||||||
min={1}
|
minValue={1}
|
||||||
max={16}
|
maxValue={16}
|
||||||
step={1}
|
step={1}
|
||||||
/>
|
>
|
||||||
|
<Slider.Track>
|
||||||
|
<Slider.Fill />
|
||||||
|
<Slider.Thumb />
|
||||||
|
</Slider.Track>
|
||||||
|
</Slider>
|
||||||
<div className="flex justify-between mt-1">
|
<div className="flex justify-between mt-1">
|
||||||
<span className="text-[11px] text-muted-foreground/50">1 GB</span>
|
<span className="text-[11px] text-muted-foreground/50">1 GB</span>
|
||||||
<span className="text-[11px] text-muted-foreground/50">16 GB</span>
|
<span className="text-[11px] text-muted-foreground/50">16 GB</span>
|
||||||
@@ -132,47 +119,48 @@ export function JavaMemSetting() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</GlassCard>
|
</SettingCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ===== JVM 参数 ===== */}
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-foreground mb-3">JVM 参数</h3>
|
<SectionTitle>JVM 参数</SectionTitle>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<GlassCard>
|
<SettingCard>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-sm font-medium text-foreground">额外 JVM 启动参数</p>
|
<p className="text-sm font-medium text-foreground">额外 JVM 启动参数</p>
|
||||||
<p className="text-[13px] text-muted-foreground">每行一个参数,例如 -XX:+UseZGC</p>
|
<p className="text-[13px] text-muted-foreground">每行一个参数,例如 -XX:+UseZGC</p>
|
||||||
<textarea
|
<TextArea
|
||||||
value={java.jvmArgs}
|
value={java.jvmArgs}
|
||||||
onChange={(e) => setJava({ jvmArgs: e.target.value })}
|
onChange={(e) => setJava({ jvmArgs: e.target.value })}
|
||||||
placeholder="可选,留空使用默认参数"
|
placeholder="可选,留空使用默认参数"
|
||||||
rows={3}
|
rows={3}
|
||||||
className="w-full px-3 py-2 rounded-md border border-input bg-background text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring resize-none font-mono"
|
fullWidth
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</GlassCard>
|
</SettingCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ===== 垃圾回收 ===== */}
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-foreground mb-3">垃圾回收</h3>
|
<SectionTitle>垃圾回收</SectionTitle>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<GlassCard>
|
<SettingCard>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-sm font-medium text-foreground">GC 算法</p>
|
<p className="text-sm font-medium text-foreground">GC 算法</p>
|
||||||
<div className="space-y-2">
|
<RadioGroup
|
||||||
|
value={java.gc}
|
||||||
|
onValueChange={(v) => setJava({ gc: v })}
|
||||||
|
className="space-y-2"
|
||||||
|
>
|
||||||
{gcOptions.map((opt) => (
|
{gcOptions.map((opt) => (
|
||||||
<label key={opt.value} className="flex items-center gap-2 cursor-pointer">
|
<Radio key={opt.value} value={opt.value}>
|
||||||
<input type="radio" name="gc" checked={java.gc === opt.value} onChange={() => setJava({ gc: opt.value })} className="accent-primary" />
|
<Radio.Content>{opt.label}</Radio.Content>
|
||||||
<span className="text-sm text-foreground">{opt.label}</span>
|
</Radio>
|
||||||
</label>
|
|
||||||
))}
|
))}
|
||||||
|
</RadioGroup>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</SettingCard>
|
||||||
</GlassCard>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,22 +2,8 @@ import { VersionCard } from "@/components/VersionCard";
|
|||||||
import { BUILD_MODE } from "@/lib/mode";
|
import { BUILD_MODE } from "@/lib/mode";
|
||||||
import { ExternalLink, GitFork, RotateCcw } from "lucide-react";
|
import { ExternalLink, GitFork, RotateCcw } from "lucide-react";
|
||||||
import { useConfirmDialogStore } from "@/stores/confirmDialogStore";
|
import { useConfirmDialogStore } from "@/stores/confirmDialogStore";
|
||||||
|
import { Button, Link } from "@heroui/react";
|
||||||
function GlassCard({ children }: { children: React.ReactNode }) {
|
import { SettingCard, SettingRow, PageHeader, SectionTitle } from "@/components/setting";
|
||||||
return <div className="glass-card px-5 py-4">{children}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SettingRow({ label, desc, children }: { label: string; desc?: string; children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between gap-4">
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<p className="text-sm font-medium text-foreground">{label}</p>
|
|
||||||
{desc && <p className="text-[13px] text-muted-foreground mt-0.5">{desc}</p>}
|
|
||||||
</div>
|
|
||||||
<div className="shrink-0">{children}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const modeLabels: Record<string, string> = {
|
const modeLabels: Record<string, string> = {
|
||||||
dev: "开发版",
|
dev: "开发版",
|
||||||
@@ -47,79 +33,65 @@ export function AboutSetting() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-foreground mb-1">关于</h2>
|
<PageHeader title="关于" desc="查看版本信息、更新状态与项目相关链接" />
|
||||||
<p className="text-sm text-muted-foreground mb-6">查看版本信息、更新状态与项目相关链接</p>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* 版本卡片 */}
|
|
||||||
<VersionCard />
|
<VersionCard />
|
||||||
|
|
||||||
{/* 项目信息 */}
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-foreground mb-3">项目信息</h3>
|
<SectionTitle>项目信息</SectionTitle>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<GlassCard>
|
<SettingCard>
|
||||||
<SettingRow label="应用名称" desc="Koring Launcher">
|
<SettingRow label="应用名称" desc="Koring Launcher">
|
||||||
<span className="text-[13px] text-muted-foreground">Lingke Network 提供技术支持</span>
|
<span className="text-[13px] text-muted-foreground">Lingke Network 提供技术支持</span>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
</GlassCard>
|
</SettingCard>
|
||||||
<GlassCard>
|
<SettingCard>
|
||||||
<SettingRow label="构建模式">
|
<SettingRow label="构建模式">
|
||||||
<span className="text-[13px] text-muted-foreground">{modeLabels[BUILD_MODE] ?? BUILD_MODE}</span>
|
<span className="text-[13px] text-muted-foreground">{modeLabels[BUILD_MODE] ?? BUILD_MODE}</span>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
</GlassCard>
|
</SettingCard>
|
||||||
<GlassCard>
|
<SettingCard>
|
||||||
<SettingRow label="技术栈" desc="Electron + React 19 + TypeScript + @xmcl">
|
<SettingRow label="技术栈" desc="Electron + React 19 + TypeScript + @xmcl">
|
||||||
<span className="text-[13px] text-muted-foreground">Node.js</span>
|
<span className="text-[13px] text-muted-foreground">Node.js</span>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
</GlassCard>
|
</SettingCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 链接 */}
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-foreground mb-3">相关链接</h3>
|
<SectionTitle>相关链接</SectionTitle>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<GlassCard>
|
<SettingCard>
|
||||||
<SettingRow label="GitHub 仓库" desc="查看源代码、提交 Issue">
|
<SettingRow label="GitHub 仓库" desc="查看源代码、提交 Issue">
|
||||||
<button
|
<Link onPress={() => openLink(GITHUB_URL)}>
|
||||||
onClick={() => openLink(GITHUB_URL)}
|
|
||||||
className="inline-flex items-center gap-1.5 text-[13px] text-primary hover:underline"
|
|
||||||
>
|
|
||||||
<GitFork className="w-4 h-4" />
|
<GitFork className="w-4 h-4" />
|
||||||
打开
|
打开
|
||||||
<ExternalLink className="w-3 h-3" />
|
<ExternalLink className="w-3 h-3" />
|
||||||
</button>
|
</Link>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
</GlassCard>
|
</SettingCard>
|
||||||
<GlassCard>
|
<SettingCard>
|
||||||
<SettingRow label="官方网站" desc="了解更多功能与文档">
|
<SettingRow label="官方网站" desc="了解更多功能与文档">
|
||||||
<button
|
<Link onPress={() => openLink(OFFICIAL_URL)}>
|
||||||
onClick={() => openLink(OFFICIAL_URL)}
|
|
||||||
className="inline-flex items-center gap-1.5 text-[13px] text-primary hover:underline"
|
|
||||||
>
|
|
||||||
访问
|
访问
|
||||||
<ExternalLink className="w-3 h-3" />
|
<ExternalLink className="w-3 h-3" />
|
||||||
</button>
|
</Link>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
</GlassCard>
|
</SettingCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 危险操作 */}
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-foreground mb-3">危险操作</h3>
|
<SectionTitle>危险操作</SectionTitle>
|
||||||
<GlassCard>
|
<SettingCard>
|
||||||
<SettingRow label="还原所有设置" desc="删除所有配置文件并重启应用">
|
<SettingRow label="还原所有设置" desc="删除所有配置文件并重启应用">
|
||||||
<button
|
<Button variant="danger" size="sm" onPress={handleResetClick}>
|
||||||
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" />
|
<RotateCcw className="w-4 h-4" />
|
||||||
还原
|
还原
|
||||||
</button>
|
</Button>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
</GlassCard>
|
</SettingCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ import { useEffect } from "react";
|
|||||||
import { useKoringAuthStore } from "@/stores/koringAuthStore";
|
import { useKoringAuthStore } from "@/stores/koringAuthStore";
|
||||||
import { useRouteStore } from "@/stores/routeStore";
|
import { useRouteStore } from "@/stores/routeStore";
|
||||||
import { UserCircle, LogOut, Loader2, ChevronRight } from "lucide-react";
|
import { UserCircle, LogOut, Loader2, ChevronRight } from "lucide-react";
|
||||||
|
import { Button, Avatar } from "@heroui/react";
|
||||||
function GlassCard({ children }: { children: React.ReactNode }) {
|
import { SettingCard, PageHeader, SectionTitle } from "@/components/setting";
|
||||||
return <div className="glass-card px-5 py-4">{children}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AccountSetting() {
|
export function AccountSetting() {
|
||||||
const { user, loading, initFromDisk, logout } = useKoringAuthStore();
|
const { user, loading, initFromDisk, logout } = useKoringAuthStore();
|
||||||
@@ -17,21 +15,22 @@ export function AccountSetting() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-foreground mb-1">Koring 账户</h2>
|
<PageHeader title="Koring 账户" desc="管理 Koring 工作室账户,用于同步数据、皮肤与个人配置" />
|
||||||
<p className="text-sm text-muted-foreground mb-6">管理 Koring 工作室账户,用于同步数据、皮肤与个人配置</p>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* 账户信息 */}
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-foreground mb-3">账户信息</h3>
|
<SectionTitle>账户信息</SectionTitle>
|
||||||
<button
|
<SettingCard
|
||||||
|
className={
|
||||||
|
!user && !loading
|
||||||
|
? "cursor-pointer hover:bg-foreground/[0.04] transition-all duration-200"
|
||||||
|
: "cursor-default"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!user && !loading) navigate("setting/login");
|
if (!user && !loading) navigate("setting/login");
|
||||||
}}
|
}}
|
||||||
className={[
|
|
||||||
"glass-card px-5 py-4 w-full text-left transition-all duration-200",
|
|
||||||
!user && !loading ? "cursor-pointer hover:bg-foreground/[0.04]" : "cursor-default",
|
|
||||||
].join(" ")}
|
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex items-center gap-2 py-2">
|
<div className="flex items-center gap-2 py-2">
|
||||||
@@ -40,13 +39,15 @@ export function AccountSetting() {
|
|||||||
</div>
|
</div>
|
||||||
) : user ? (
|
) : user ? (
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="w-14 h-14 rounded-full bg-foreground/[0.06] flex items-center justify-center shrink-0 overflow-hidden">
|
<Avatar size="lg">
|
||||||
{user.picture ? (
|
{user.picture ? (
|
||||||
<img src={user.picture} alt="" className="w-full h-full object-cover" />
|
<Avatar.Image src={user.picture} alt="" />
|
||||||
) : (
|
) : (
|
||||||
|
<Avatar.Fallback>
|
||||||
<UserCircle className="w-8 h-8 text-foreground/40" />
|
<UserCircle className="w-8 h-8 text-foreground/40" />
|
||||||
|
</Avatar.Fallback>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Avatar>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-base font-medium text-foreground truncate">
|
<p className="text-base font-medium text-foreground truncate">
|
||||||
{user.name || user.username}
|
{user.name || user.username}
|
||||||
@@ -69,29 +70,30 @@ export function AccountSetting() {
|
|||||||
<ChevronRight className="w-4 h-4 text-foreground/20 shrink-0" />
|
<ChevronRight className="w-4 h-4 text-foreground/20 shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</button>
|
</div>
|
||||||
|
</SettingCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 退出登录 */}
|
|
||||||
{user && (
|
{user && (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-foreground mb-3">账户操作</h3>
|
<SectionTitle>账户操作</SectionTitle>
|
||||||
<GlassCard>
|
<SettingCard>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-foreground">退出登录</p>
|
<p className="text-sm font-medium text-foreground">退出登录</p>
|
||||||
<p className="text-[13px] text-muted-foreground mt-0.5">退出当前账户,数据将保留在本地</p>
|
<p className="text-[13px] text-muted-foreground mt-0.5">退出当前账户,数据将保留在本地</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<Button
|
||||||
onClick={logout}
|
variant="danger-soft"
|
||||||
disabled={loading}
|
size="sm"
|
||||||
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 disabled:opacity-40"
|
isDisabled={loading}
|
||||||
|
onPress={logout}
|
||||||
>
|
>
|
||||||
<LogOut className="w-3.5 h-3.5" />
|
<LogOut className="w-3.5 h-3.5" />
|
||||||
退出
|
退出
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</GlassCard>
|
</SettingCard>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,20 +1,6 @@
|
|||||||
import { ExternalLink } from "lucide-react";
|
import { ExternalLink } from "lucide-react";
|
||||||
|
import { Link } from "@heroui/react";
|
||||||
function GlassCard({ children }: { children: React.ReactNode }) {
|
import { SettingCard, SettingRow, PageHeader, SectionTitle } from "@/components/setting";
|
||||||
return <div className="glass-card px-5 py-4">{children}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SettingRow({ label, desc, children }: { label: string; desc?: string; children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between gap-4">
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<p className="text-sm font-medium text-foreground">{label}</p>
|
|
||||||
{desc && <p className="text-[13px] text-muted-foreground mt-0.5">{desc}</p>}
|
|
||||||
</div>
|
|
||||||
<div className="shrink-0">{children}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const openLink = (url: string) => {
|
const openLink = (url: string) => {
|
||||||
window.electronAPI?.openExternal(url);
|
window.electronAPI?.openExternal(url);
|
||||||
@@ -28,7 +14,7 @@ const licenses = [
|
|||||||
{ name: "@xmcl/user", license: "MIT", url: "https://github.com/VoxelCogs/xmcl" },
|
{ name: "@xmcl/user", license: "MIT", url: "https://github.com/VoxelCogs/xmcl" },
|
||||||
{ name: "Zustand", license: "MIT", url: "https://github.com/pmndrs/zustand" },
|
{ name: "Zustand", license: "MIT", url: "https://github.com/pmndrs/zustand" },
|
||||||
{ name: "Tailwind CSS", license: "MIT", url: "https://github.com/tailwindlabs/tailwindcss" },
|
{ name: "Tailwind CSS", license: "MIT", url: "https://github.com/tailwindlabs/tailwindcss" },
|
||||||
{ name: "shadcn/ui", license: "MIT", url: "https://github.com/shadcn-ui/ui" },
|
{ name: "HeroUI", license: "MIT", url: "https://github.com/heroui-inc/heroui" },
|
||||||
{ name: "Vite", license: "MIT", url: "https://github.com/vitejs/vite" },
|
{ name: "Vite", license: "MIT", url: "https://github.com/vitejs/vite" },
|
||||||
{ name: "Lucide React", license: "ISC", url: "https://github.com/lucide-icons/lucide" },
|
{ name: "Lucide React", license: "ISC", url: "https://github.com/lucide-icons/lucide" },
|
||||||
];
|
];
|
||||||
@@ -42,55 +28,45 @@ const fonts = [
|
|||||||
export function CopyrightSetting() {
|
export function CopyrightSetting() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-foreground mb-1">版权</h2>
|
<PageHeader title="版权" desc="开源协议、依赖项目授权与字体版权信息" />
|
||||||
<p className="text-sm text-muted-foreground mb-6">开源协议、依赖项目授权与字体版权信息</p>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* 项目协议 */}
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-foreground mb-3">项目协议</h3>
|
<SectionTitle>项目协议</SectionTitle>
|
||||||
<GlassCard>
|
<SettingCard>
|
||||||
<SettingRow label="LL-1.0 (LingkeLice 1.0)" desc="Copyright © Shenzhen Lingke Network Technology Co., Ltd.">
|
<SettingRow label="LL-1.0 (LingkeLice 1.0)" desc="Copyright © Shenzhen Lingke Network Technology Co., Ltd.">
|
||||||
<button
|
<Link onPress={() => openLink("https://support.lingke.ink/LL-1.0")}>
|
||||||
onClick={() => openLink("https://support.lingke.ink/LL-1.0")}
|
|
||||||
className="inline-flex items-center gap-1.5 text-[13px] text-primary hover:underline"
|
|
||||||
>
|
|
||||||
查看
|
查看
|
||||||
<ExternalLink className="w-3 h-3" />
|
<ExternalLink className="w-3 h-3" />
|
||||||
</button>
|
</Link>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
</GlassCard>
|
</SettingCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 开源依赖 */}
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-foreground mb-3">开源依赖</h3>
|
<SectionTitle>开源依赖</SectionTitle>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{licenses.map((dep) => (
|
{licenses.map((dep) => (
|
||||||
<GlassCard key={dep.name}>
|
<SettingCard key={dep.name}>
|
||||||
<SettingRow label={dep.name} desc={dep.license}>
|
<SettingRow label={dep.name} desc={dep.license}>
|
||||||
<button
|
<Link onPress={() => openLink(dep.url)}>
|
||||||
onClick={() => openLink(dep.url)}
|
|
||||||
className="inline-flex items-center gap-1 text-[13px] text-primary hover:underline"
|
|
||||||
>
|
|
||||||
<ExternalLink className="w-3 h-3" />
|
<ExternalLink className="w-3 h-3" />
|
||||||
</button>
|
</Link>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
</GlassCard>
|
</SettingCard>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 字体 */}
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-foreground mb-3">字体授权</h3>
|
<SectionTitle>字体授权</SectionTitle>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{fonts.map((font) => (
|
{fonts.map((font) => (
|
||||||
<GlassCard key={font.name}>
|
<SettingCard key={font.name}>
|
||||||
<SettingRow label={font.name} desc={font.license}>
|
<SettingRow label={font.name} desc={font.license}>
|
||||||
<span className="text-[12px] text-muted-foreground">{font.note}</span>
|
<span className="text-[12px] text-muted-foreground">{font.note}</span>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
</GlassCard>
|
</SettingCard>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useKoringAuthStore } from "@/stores/koringAuthStore";
|
import { useKoringAuthStore } from "@/stores/koringAuthStore";
|
||||||
import { useRouteStore } from "@/stores/routeStore";
|
|
||||||
import { BUILD_MODE } from "@/lib/mode";
|
import { BUILD_MODE } from "@/lib/mode";
|
||||||
import { useUpdateStore } from "@/stores/updateStore";
|
import { useUpdateStore } from "@/stores/updateStore";
|
||||||
import {
|
import {
|
||||||
@@ -11,11 +10,10 @@ import {
|
|||||||
Cpu,
|
Cpu,
|
||||||
Info,
|
Info,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
|
Search,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { Avatar } from "@heroui/react";
|
||||||
function GlassCard({ children, className }: { children: React.ReactNode; className?: string }) {
|
import { SettingCard, PageHeader, SectionTitle } from "@/components/setting";
|
||||||
return <div className={`glass-card px-5 py-4 ${className ?? ""}`}>{children}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ShortcutItem {
|
interface ShortcutItem {
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
@@ -68,31 +66,27 @@ export function HomeSetting({ onNavigate }: HomeSettingProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-foreground mb-1">主页</h2>
|
<PageHeader title="主页" desc="自定义启动器主页的显示内容与常用设置的快捷入口" />
|
||||||
<p className="text-sm text-muted-foreground mb-6">自定义启动器主页的显示内容与常用设置的快捷入口</p>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* ===== 搜索栏占位 ===== */}
|
<SettingCard className="flex items-center gap-3 px-4 py-3">
|
||||||
<div className="glass-card flex items-center gap-3 px-4 py-3">
|
<Search className="w-4 h-4 text-muted-foreground shrink-0" />
|
||||||
<svg className="w-4 h-4 text-muted-foreground shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
||||||
<circle cx="11" cy="11" r="8" />
|
|
||||||
<path d="m21 21-4.3-4.3" />
|
|
||||||
</svg>
|
|
||||||
<span className="text-sm text-muted-foreground">查找设置</span>
|
<span className="text-sm text-muted-foreground">查找设置</span>
|
||||||
</div>
|
</SettingCard>
|
||||||
|
|
||||||
{/* ===== 快捷状态 ===== */}
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-foreground mb-3">快速概览</h3>
|
<SectionTitle>快速概览</SectionTitle>
|
||||||
<GlassCard>
|
<SettingCard>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="w-12 h-12 rounded-full bg-foreground/[0.06] flex items-center justify-center shrink-0 overflow-hidden">
|
<Avatar size="lg">
|
||||||
{user?.picture ? (
|
{user?.picture ? (
|
||||||
<img src={user.picture} alt="" className="w-full h-full object-cover" />
|
<Avatar.Image src={user.picture} alt="" />
|
||||||
) : (
|
) : (
|
||||||
|
<Avatar.Fallback>
|
||||||
<UserCircle className="w-7 h-7 text-foreground/40" />
|
<UserCircle className="w-7 h-7 text-foreground/40" />
|
||||||
|
</Avatar.Fallback>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Avatar>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-sm font-medium text-foreground truncate">
|
<p className="text-sm font-medium text-foreground truncate">
|
||||||
{user ? (user.name || user.username) : "未登录"}
|
{user ? (user.name || user.username) : "未登录"}
|
||||||
@@ -106,12 +100,11 @@ export function HomeSetting({ onNavigate }: HomeSettingProps) {
|
|||||||
{update ? " · 有更新" : ""}
|
{update ? " · 有更新" : ""}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</GlassCard>
|
</SettingCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ===== 快捷入口 ===== */}
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-foreground mb-3">常用设置</h3>
|
<SectionTitle>常用设置</SectionTitle>
|
||||||
<div className="grid grid-cols-1 gap-2">
|
<div className="grid grid-cols-1 gap-2">
|
||||||
{shortcuts.map((s) => (
|
{shortcuts.map((s) => (
|
||||||
<ShortcutTile key={s.navKey} {...s} onClick={onNavigate} />
|
<ShortcutTile key={s.navKey} {...s} onClick={onNavigate} />
|
||||||
|
|||||||
@@ -133,12 +133,13 @@ export function Setting() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full">
|
<div className="flex h-full">
|
||||||
{/* 侧边栏 — 无动画 */}
|
{/* 侧边栏 */}
|
||||||
<aside className="scroll-area w-[240px] shrink-0 h-full overflow-y-auto py-5 pl-5 pr-2">
|
<aside className="scroll-area w-[220px] shrink-0 h-full overflow-y-auto py-5 pl-4 pr-2">
|
||||||
<nav className="space-y-5">
|
<nav className="space-y-6">
|
||||||
{menuData.map((group) => (
|
{menuData.map((group, gi) => (
|
||||||
<div key={group.title}>
|
<div key={group.title}>
|
||||||
<h3 className="text-[11px] font-semibold uppercase tracking-wider text-foreground/30 mb-2 px-2">
|
{gi > 0 && <div className="mx-3 mb-3 border-t border-border/40" />}
|
||||||
|
<h3 className="text-[11px] font-semibold uppercase tracking-wider text-foreground/40 mb-2 px-3">
|
||||||
{group.title}
|
{group.title}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
@@ -149,12 +150,15 @@ export function Setting() {
|
|||||||
key={item.key}
|
key={item.key}
|
||||||
onClick={() => handleItemClick(item)}
|
onClick={() => handleItemClick(item)}
|
||||||
className={[
|
className={[
|
||||||
"w-full flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-[14px] transition-all duration-150",
|
"relative w-full flex items-center gap-2.5 px-3 py-2 rounded-md text-[13.5px] transition-all duration-150",
|
||||||
active
|
active
|
||||||
? "bg-foreground/[0.08] text-foreground font-medium shadow-sm"
|
? "text-foreground font-medium"
|
||||||
: "text-foreground/50 hover:text-foreground/80 hover:bg-foreground/[0.04]",
|
: "text-foreground/50 hover:text-foreground/80 hover:bg-foreground/[0.04]",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
|
{active && (
|
||||||
|
<span className="absolute left-0 top-1/2 -translate-y-1/2 w-[3px] h-[16px] rounded-full bg-primary" />
|
||||||
|
)}
|
||||||
<span className={active ? "text-foreground/80" : "text-foreground/35"}>
|
<span className={active ? "text-foreground/80" : "text-foreground/35"}>
|
||||||
{item.icon}
|
{item.icon}
|
||||||
</span>
|
</span>
|
||||||
@@ -168,7 +172,7 @@ export function Setting() {
|
|||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* 内容区 — 仅内容有 fade 动画 */}
|
{/* 内容区 */}
|
||||||
<main className="scroll-area flex-1 h-full overflow-y-auto p-8">
|
<main className="scroll-area flex-1 h-full overflow-y-auto p-8">
|
||||||
<div key={animKey} className="setting-page-enter">
|
<div key={animKey} className="setting-page-enter">
|
||||||
{current?.component}
|
{current?.component}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import { useRouteStore } from "@/stores/routeStore";
|
||||||
|
import { KoringLogin } from "@/components/KoringLogin";
|
||||||
|
|
||||||
|
export function SettingLogin() {
|
||||||
|
const goBack = useRouteStore((s) => s.goBack);
|
||||||
|
|
||||||
|
const handleSuccess = useCallback(() => {
|
||||||
|
setTimeout(() => goBack(), 1500);
|
||||||
|
}, [goBack]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-col items-center justify-center p-8">
|
||||||
|
<div className="w-full max-w-sm flex flex-col items-center gap-6">
|
||||||
|
<div className="text-center space-y-1">
|
||||||
|
<h2 className="text-lg font-bold text-foreground">扫码登录</h2>
|
||||||
|
<p className="text-xs text-muted-foreground">使用任意二维码扫描器扫描下方二维码</p>
|
||||||
|
</div>
|
||||||
|
<KoringLogin onLoginSuccess={handleSuccess} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
import { useConfigStore } from "@/stores/configStore";
|
import { useConfigStore } from "@/stores/configStore";
|
||||||
import { Slider } from "@/components/ui/slider";
|
import { Slider, RadioGroup, Radio, Input } from "@heroui/react";
|
||||||
|
import { SettingCard, PageHeader, SectionTitle } from "@/components/setting";
|
||||||
function GlassCard({ children }: { children: React.ReactNode }) {
|
|
||||||
return <div className="glass-card px-5 py-4">{children}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const downloadSources = [
|
const downloadSources = [
|
||||||
{ value: "mirror", label: "尽量使用镜像源(推荐国内用户)" },
|
{ value: "mirror", label: "尽量使用镜像源(推荐国内用户)" },
|
||||||
@@ -17,51 +14,54 @@ export function DownloadSetting() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-foreground mb-1">下载</h2>
|
<PageHeader title="下载" desc="配置游戏资源下载源、并发数与存储路径" />
|
||||||
<p className="text-sm text-muted-foreground mb-6">配置游戏资源下载源、并发数与存储路径</p>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* ===== 下载源 ===== */}
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-foreground mb-3">下载源</h3>
|
<SectionTitle>下载源</SectionTitle>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<GlassCard>
|
<SettingCard>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-sm font-medium text-foreground">文件下载源</p>
|
<p className="text-sm font-medium text-foreground">文件下载源</p>
|
||||||
<p className="text-[13px] text-muted-foreground">游戏文件(jar、lib)的下载来源</p>
|
<p className="text-[13px] text-muted-foreground">游戏文件(jar、lib)的下载来源</p>
|
||||||
<div className="space-y-2 mt-2">
|
<RadioGroup
|
||||||
|
value={dl.fileSource}
|
||||||
|
onValueChange={(v) => setDownload({ fileSource: v })}
|
||||||
|
className="mt-2 space-y-2"
|
||||||
|
>
|
||||||
{downloadSources.map((opt) => (
|
{downloadSources.map((opt) => (
|
||||||
<label key={opt.value} className="flex items-center gap-2 cursor-pointer">
|
<Radio key={opt.value} value={opt.value}>
|
||||||
<input type="radio" name="fileSource" checked={dl.fileSource === opt.value} onChange={() => setDownload({ fileSource: opt.value })} className="accent-primary" />
|
<Radio.Content>{opt.label}</Radio.Content>
|
||||||
<span className="text-sm text-foreground">{opt.label}</span>
|
</Radio>
|
||||||
</label>
|
|
||||||
))}
|
))}
|
||||||
|
</RadioGroup>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</SettingCard>
|
||||||
</GlassCard>
|
|
||||||
|
|
||||||
<GlassCard>
|
<SettingCard>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-sm font-medium text-foreground">版本列表源</p>
|
<p className="text-sm font-medium text-foreground">版本列表源</p>
|
||||||
<p className="text-[13px] text-muted-foreground">获取可用游戏版本列表的来源</p>
|
<p className="text-[13px] text-muted-foreground">获取可用游戏版本列表的来源</p>
|
||||||
<div className="space-y-2 mt-2">
|
<RadioGroup
|
||||||
|
value={dl.versionSource}
|
||||||
|
onValueChange={(v) => setDownload({ versionSource: v })}
|
||||||
|
className="mt-2 space-y-2"
|
||||||
|
>
|
||||||
{downloadSources.map((opt) => (
|
{downloadSources.map((opt) => (
|
||||||
<label key={opt.value} className="flex items-center gap-2 cursor-pointer">
|
<Radio key={opt.value} value={opt.value}>
|
||||||
<input type="radio" name="versionSource" checked={dl.versionSource === opt.value} onChange={() => setDownload({ versionSource: opt.value })} className="accent-primary" />
|
<Radio.Content>{opt.label}</Radio.Content>
|
||||||
<span className="text-sm text-foreground">{opt.label}</span>
|
</Radio>
|
||||||
</label>
|
|
||||||
))}
|
))}
|
||||||
|
</RadioGroup>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</SettingCard>
|
||||||
</GlassCard>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ===== 并发控制 ===== */}
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-foreground mb-3">并发控制</h3>
|
<SectionTitle>并发控制</SectionTitle>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<GlassCard>
|
<SettingCard>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-1.5">
|
<div className="flex items-center justify-between mb-1.5">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
@@ -71,41 +71,44 @@ export function DownloadSetting() {
|
|||||||
<span className="text-[13px] text-muted-foreground tabular-nums shrink-0 ml-4">{dl.threads}</span>
|
<span className="text-[13px] text-muted-foreground tabular-nums shrink-0 ml-4">{dl.threads}</span>
|
||||||
</div>
|
</div>
|
||||||
<Slider
|
<Slider
|
||||||
value={[dl.threads]}
|
value={dl.threads}
|
||||||
onValueChange={(v) => setDownload({ threads: Array.isArray(v) ? v[0] : v })}
|
onChange={(v) => setDownload({ threads: typeof v === "number" ? v : v[0] })}
|
||||||
min={1}
|
minValue={1}
|
||||||
max={64}
|
maxValue={64}
|
||||||
step={1}
|
step={1}
|
||||||
/>
|
>
|
||||||
|
<Slider.Track>
|
||||||
|
<Slider.Fill />
|
||||||
|
<Slider.Thumb />
|
||||||
|
</Slider.Track>
|
||||||
|
</Slider>
|
||||||
<div className="flex justify-between mt-1">
|
<div className="flex justify-between mt-1">
|
||||||
<span className="text-[11px] text-muted-foreground/50">1</span>
|
<span className="text-[11px] text-muted-foreground/50">1</span>
|
||||||
<span className="text-[11px] text-muted-foreground/50">64</span>
|
<span className="text-[11px] text-muted-foreground/50">64</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</GlassCard>
|
</SettingCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ===== 速度限制 ===== */}
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-foreground mb-3">速度限制</h3>
|
<SectionTitle>速度限制</SectionTitle>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<GlassCard>
|
<SettingCard>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-sm font-medium text-foreground">下载速度上限</p>
|
<p className="text-sm font-medium text-foreground">下载速度上限</p>
|
||||||
<p className="text-[13px] text-muted-foreground">单位 KB/s,0 表示不限速</p>
|
<p className="text-[13px] text-muted-foreground">单位 KB/s,0 表示不限速</p>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={dl.speedLimit}
|
value={String(dl.speedLimit)}
|
||||||
onChange={(e) => setDownload({ speedLimit: Number(e.target.value) })}
|
onChange={(e) => setDownload({ speedLimit: Number(e.target.value) })}
|
||||||
min={0}
|
className="w-28"
|
||||||
className="w-28 h-8 px-3 rounded-md border border-input bg-background text-sm font-mono placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
|
|
||||||
/>
|
/>
|
||||||
<span className="text-[13px] text-muted-foreground">KB/s</span>
|
<span className="text-[13px] text-muted-foreground">KB/s</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</GlassCard>
|
</SettingCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
|
import { EmptyState } from "@heroui/react";
|
||||||
|
import { Globe } from "lucide-react";
|
||||||
|
import { PageHeader } from "@/components/setting";
|
||||||
|
|
||||||
export function EtherOnlineSetting() {
|
export function EtherOnlineSetting() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-foreground mb-1">以太联机</h2>
|
<PageHeader title="以太联机" desc="配置以太联机服务,实现多人局域网或远程联机" />
|
||||||
<p className="text-sm text-muted-foreground mb-6">配置以太联机服务,实现多人局域网或远程联机</p>
|
<EmptyState className="py-16">
|
||||||
|
<Globe className="w-10 h-10 text-muted-foreground/30" />
|
||||||
|
<p className="text-sm text-muted-foreground mt-3">该功能正在开发中</p>
|
||||||
|
</EmptyState>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,8 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useConfigStore } from "@/stores/configStore";
|
import { useConfigStore } from "@/stores/configStore";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch, Input } from "@heroui/react";
|
||||||
import { ShieldCheck } from "lucide-react";
|
import { ShieldCheck } from "lucide-react";
|
||||||
|
import { SettingCard, SettingRow, PageHeader, SectionTitle } from "@/components/setting";
|
||||||
function GlassCard({ children }: { children: React.ReactNode }) {
|
|
||||||
return <div className="glass-card px-5 py-4">{children}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SettingRow({ label, desc, children }: { label: string; desc?: string; children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between gap-4">
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<p className="text-sm font-medium text-foreground">{label}</p>
|
|
||||||
{desc && <p className="text-[13px] text-muted-foreground mt-0.5">{desc}</p>}
|
|
||||||
</div>
|
|
||||||
<div className="shrink-0">{children}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SecurityIdSetting() {
|
export function SecurityIdSetting() {
|
||||||
const enabled = useConfigStore((s) => s.config.network.securityId.enabled);
|
const enabled = useConfigStore((s) => s.config.network.securityId.enabled);
|
||||||
@@ -34,39 +19,41 @@ export function SecurityIdSetting() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-foreground mb-1">安全识别服务</h2>
|
<PageHeader title="安全识别服务" desc="管理账户安全验证、设备识别与登录保护" />
|
||||||
<p className="text-sm text-muted-foreground mb-6">管理账户安全验证、设备识别与登录保护</p>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-foreground mb-3">认证服务</h3>
|
<SectionTitle>认证服务</SectionTitle>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<GlassCard>
|
<SettingCard>
|
||||||
<SettingRow
|
<SettingRow
|
||||||
label="启用第三方认证"
|
label="启用第三方认证"
|
||||||
desc="使用自定义认证服务器替代 Microsoft 认证(适用于离线服务器)"
|
desc="使用自定义认证服务器替代 Microsoft 认证(适用于离线服务器)"
|
||||||
>
|
>
|
||||||
<Switch checked={enabled} onCheckedChange={handleToggle} />
|
<Switch isSelected={enabled} onValueChange={handleToggle}>
|
||||||
|
<Switch.Control>
|
||||||
|
<Switch.Thumb />
|
||||||
|
</Switch.Control>
|
||||||
|
</Switch>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
</GlassCard>
|
</SettingCard>
|
||||||
|
|
||||||
{enabled && (
|
{enabled && (
|
||||||
<GlassCard>
|
<SettingCard>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-sm font-medium text-foreground">认证服务器 URL</p>
|
<p className="text-sm font-medium text-foreground">认证服务器 URL</p>
|
||||||
<p className="text-[13px] text-muted-foreground">输入第三方认证服务的地址</p>
|
<p className="text-[13px] text-muted-foreground">输入第三方认证服务的地址</p>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ShieldCheck className="w-4 h-4 text-muted-foreground shrink-0" />
|
<ShieldCheck className="w-4 h-4 text-muted-foreground shrink-0" />
|
||||||
<input
|
<Input
|
||||||
type="text"
|
|
||||||
value={authUrl}
|
value={authUrl}
|
||||||
onChange={handleUrlChange}
|
onChange={handleUrlChange}
|
||||||
placeholder="https://auth.example.com"
|
placeholder="https://auth.example.com"
|
||||||
className="flex-1 h-8 px-3 rounded-md border border-input bg-background text-sm font-mono placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
|
fullWidth
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</GlassCard>
|
</SettingCard>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
|
import { EmptyState } from "@heroui/react";
|
||||||
|
import { Network } from "lucide-react";
|
||||||
|
import { PageHeader } from "@/components/setting";
|
||||||
|
|
||||||
export function TawaOnlineSetting() {
|
export function TawaOnlineSetting() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-foreground mb-1">陶瓦联机</h2>
|
<PageHeader title="陶瓦联机" desc="配置陶瓦联机服务,通过中继服务器进行多人游戏" />
|
||||||
<p className="text-sm text-muted-foreground mb-6">配置陶瓦联机服务,通过中继服务器进行多人游戏</p>
|
<EmptyState className="py-16">
|
||||||
|
<Network className="w-10 h-10 text-muted-foreground/30" />
|
||||||
|
<p className="text-sm text-muted-foreground mt-3">该功能正在开发中</p>
|
||||||
|
</EmptyState>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
|
import { EmptyState } from "@heroui/react";
|
||||||
|
import { MessageSquareHeart } from "lucide-react";
|
||||||
|
import { PageHeader } from "@/components/setting";
|
||||||
|
|
||||||
export function FeedbackSetting() {
|
export function FeedbackSetting() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-foreground mb-1">服务与反馈</h2>
|
<PageHeader title="服务与反馈" desc="提交问题反馈、功能建议与联系开发团队" />
|
||||||
<p className="text-sm text-muted-foreground mb-6">提交问题反馈、功能建议与联系开发团队</p>
|
<EmptyState className="py-16">
|
||||||
|
<MessageSquareHeart className="w-10 h-10 text-muted-foreground/30" />
|
||||||
|
<p className="text-sm text-muted-foreground mt-3">该功能正在开发中</p>
|
||||||
|
</EmptyState>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
|
import { EmptyState } from "@heroui/react";
|
||||||
|
import { Heart } from "lucide-react";
|
||||||
|
import { PageHeader } from "@/components/setting";
|
||||||
|
|
||||||
export function SponsorSetting() {
|
export function SponsorSetting() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-foreground mb-1">赞助我们</h2>
|
<PageHeader title="赞助我们" desc="如果 Koring Launcher 对你有帮助,欢迎赞助支持开发" />
|
||||||
<p className="text-sm text-muted-foreground mb-6">如果 Koring Launcher 对你有帮助,欢迎赞助支持开发</p>
|
<EmptyState className="py-16">
|
||||||
|
<Heart className="w-10 h-10 text-muted-foreground/30" />
|
||||||
|
<p className="text-sm text-muted-foreground mt-3">该功能正在开发中</p>
|
||||||
|
</EmptyState>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +1,47 @@
|
|||||||
import { useA11yStore } from "@/stores/a11yStore";
|
import { useA11yStore } from "@/stores/a11yStore";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@heroui/react";
|
||||||
|
import { SettingCard, SettingRow, PageHeader, SectionTitle } from "@/components/setting";
|
||||||
function GlassCard({ children }: { children: React.ReactNode }) {
|
|
||||||
return <div className="glass-card px-5 py-4">{children}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SettingRow({ label, desc, children }: { label: string; desc: string; children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between gap-4">
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<p className="text-sm font-medium text-foreground">{label}</p>
|
|
||||||
<p className="text-[13px] text-muted-foreground mt-0.5">{desc}</p>
|
|
||||||
</div>
|
|
||||||
<div className="shrink-0">{children}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function A11ySetting() {
|
export function A11ySetting() {
|
||||||
const { reduceMotion, setReduceMotion, reduceTransparency, setReduceTransparency, highContrast, setHighContrast } = useA11yStore();
|
const { reduceMotion, setReduceMotion, reduceTransparency, setReduceTransparency, highContrast, setHighContrast } = useA11yStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-foreground mb-1">辅助功能</h2>
|
<PageHeader title="辅助功能" desc="调整动画、透明度与对比度以改善使用体验" />
|
||||||
<p className="text-sm text-muted-foreground mb-6">调整动画、透明度与对比度以改善使用体验</p>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-foreground mb-3">显示</h3>
|
<SectionTitle>显示</SectionTitle>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<GlassCard>
|
<SettingCard>
|
||||||
<SettingRow label="减少动画" desc="关闭页面切换动画和背景动效">
|
<SettingRow label="减少动画" desc="关闭页面切换动画和背景动效">
|
||||||
<Switch checked={reduceMotion} onCheckedChange={setReduceMotion} />
|
<Switch isSelected={reduceMotion} onValueChange={setReduceMotion}>
|
||||||
|
<Switch.Control>
|
||||||
|
<Switch.Thumb />
|
||||||
|
</Switch.Control>
|
||||||
|
</Switch>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
</GlassCard>
|
</SettingCard>
|
||||||
|
|
||||||
<GlassCard>
|
<SettingCard>
|
||||||
<SettingRow label="减少透明度" desc="将磨砂玻璃效果替换为纯色背景,提升可读性">
|
<SettingRow label="减少透明度" desc="将磨砂玻璃效果替换为纯色背景,提升可读性">
|
||||||
<Switch checked={reduceTransparency} onCheckedChange={setReduceTransparency} />
|
<Switch isSelected={reduceTransparency} onValueChange={setReduceTransparency}>
|
||||||
|
<Switch.Control>
|
||||||
|
<Switch.Thumb />
|
||||||
|
</Switch.Control>
|
||||||
|
</Switch>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
</GlassCard>
|
</SettingCard>
|
||||||
|
|
||||||
<GlassCard>
|
<SettingCard>
|
||||||
<SettingRow label="高对比度" desc="增强文字与背景的对比度,改善可读性">
|
<SettingRow label="高对比度" desc="增强文字与背景的对比度,改善可读性">
|
||||||
<Switch checked={highContrast} onCheckedChange={setHighContrast} />
|
<Switch isSelected={highContrast} onValueChange={setHighContrast}>
|
||||||
|
<Switch.Control>
|
||||||
|
<Switch.Thumb />
|
||||||
|
</Switch.Control>
|
||||||
|
</Switch>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
</GlassCard>
|
</SettingCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
|
import { EmptyState } from "@heroui/react";
|
||||||
|
import { Languages } from "lucide-react";
|
||||||
|
import { PageHeader } from "@/components/setting";
|
||||||
|
|
||||||
export function LangSetting() {
|
export function LangSetting() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-foreground mb-1">语言</h2>
|
<PageHeader title="语言" desc="选择启动器界面的显示语言与地区偏好" />
|
||||||
<p className="text-sm text-muted-foreground mb-6">选择启动器界面的显示语言与地区偏好</p>
|
<EmptyState className="py-16">
|
||||||
|
<Languages className="w-10 h-10 text-muted-foreground/30" />
|
||||||
|
<p className="text-sm text-muted-foreground mt-3">该功能正在开发中</p>
|
||||||
|
</EmptyState>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,9 @@
|
|||||||
import { useThemeStore, type DarkMode } from "@/stores/themeStore";
|
import { useThemeStore, type DarkMode } from "@/stores/themeStore";
|
||||||
import { useBackgroundStore } from "@/stores/backgroundStore";
|
import { useBackgroundStore } from "@/stores/backgroundStore";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch, Button, Slider } from "@heroui/react";
|
||||||
import { Slider } from "@/components/ui/slider";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { DEFAULT_BG } from "@/lib/mode";
|
import { DEFAULT_BG } from "@/lib/mode";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { SettingCard, SettingRow, PageHeader, SectionTitle } from "@/components/setting";
|
||||||
function GlassCard({ children }: { children: React.ReactNode }) {
|
|
||||||
return <div className="glass-card px-5 py-4">{children}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SettingRow({ label, desc, children }: { label: string; desc?: string; children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between gap-4">
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<p className="text-sm font-medium text-foreground">{label}</p>
|
|
||||||
{desc && <p className="text-[13px] text-muted-foreground mt-0.5">{desc}</p>}
|
|
||||||
</div>
|
|
||||||
<div className="shrink-0">{children}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ======== 深色模式预览卡片 ======== */
|
|
||||||
|
|
||||||
function ThemePreviewCard({ mode, selected, onClick }: { mode: DarkMode; selected: boolean; onClick: () => void }) {
|
function ThemePreviewCard({ mode, selected, onClick }: { mode: DarkMode; selected: boolean; onClick: () => void }) {
|
||||||
const isDark = mode === "dark";
|
const isDark = mode === "dark";
|
||||||
@@ -43,7 +24,6 @@ function ThemePreviewCard({ mode, selected, onClick }: { mode: DarkMode; selecte
|
|||||||
>
|
>
|
||||||
<div className="relative w-[140px] h-[96px] rounded overflow-hidden">
|
<div className="relative w-[140px] h-[96px] rounded overflow-hidden">
|
||||||
{isAuto ? (
|
{isAuto ? (
|
||||||
/* 跟随系统 — 左右分屏 */
|
|
||||||
<div className="flex w-full h-full">
|
<div className="flex w-full h-full">
|
||||||
<div className="w-1/2 h-full bg-white flex flex-col">
|
<div className="w-1/2 h-full bg-white flex flex-col">
|
||||||
<div className="h-[6px] bg-gray-200 flex items-center px-1 gap-[2px]">
|
<div className="h-[6px] bg-gray-200 flex items-center px-1 gap-[2px]">
|
||||||
@@ -69,7 +49,6 @@ function ThemePreviewCard({ mode, selected, onClick }: { mode: DarkMode; selecte
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : isDark ? (
|
) : isDark ? (
|
||||||
/* 深色模式 */
|
|
||||||
<div className="w-full h-full bg-[#1c1c1e] flex flex-col">
|
<div className="w-full h-full bg-[#1c1c1e] flex flex-col">
|
||||||
<div className="h-[6px] bg-[#2c2c2e] flex items-center px-1 gap-[2px]">
|
<div className="h-[6px] bg-[#2c2c2e] flex items-center px-1 gap-[2px]">
|
||||||
<div className="w-[3px] h-[3px] rounded-full bg-red-400" />
|
<div className="w-[3px] h-[3px] rounded-full bg-red-400" />
|
||||||
@@ -83,7 +62,6 @@ function ThemePreviewCard({ mode, selected, onClick }: { mode: DarkMode; selecte
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
/* 浅色模式 */
|
|
||||||
<div className="w-full h-full bg-white flex flex-col">
|
<div className="w-full h-full bg-white flex flex-col">
|
||||||
<div className="h-[6px] bg-gray-200 flex items-center px-1 gap-[2px]">
|
<div className="h-[6px] bg-gray-200 flex items-center px-1 gap-[2px]">
|
||||||
<div className="w-[3px] h-[3px] rounded-full bg-red-400" />
|
<div className="w-[3px] h-[3px] rounded-full bg-red-400" />
|
||||||
@@ -103,8 +81,6 @@ function ThemePreviewCard({ mode, selected, onClick }: { mode: DarkMode; selecte
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ======== 页面 ======== */
|
|
||||||
|
|
||||||
export function ThemeBgSetting() {
|
export function ThemeBgSetting() {
|
||||||
const { darkMode, setDarkMode, parallax, setParallax } = useThemeStore();
|
const { darkMode, setDarkMode, parallax, setParallax } = useThemeStore();
|
||||||
const { image, opacity, setOpacity, blur, setBlur, setImage, reset } = useBackgroundStore();
|
const { image, opacity, setOpacity, blur, setBlur, setImage, reset } = useBackgroundStore();
|
||||||
@@ -122,29 +98,26 @@ export function ThemeBgSetting() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-foreground mb-1">主题与背景</h2>
|
<PageHeader title="主题与背景" desc="切换深色模式、更换背景图片与调整视觉效果" />
|
||||||
<p className="text-sm text-muted-foreground mb-6">切换深色模式、更换背景图片与调整视觉效果</p>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* ===== 深色模式 ===== */}
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-foreground mb-3">深色模式</h3>
|
<SectionTitle>深色模式</SectionTitle>
|
||||||
<GlassCard>
|
<SettingCard>
|
||||||
<div className="flex items-center justify-center gap-4 py-2">
|
<div className="flex items-center justify-center gap-4 py-2">
|
||||||
<ThemePreviewCard mode="light" selected={darkMode === "light"} onClick={() => setDarkMode("light")} />
|
<ThemePreviewCard mode="light" selected={darkMode === "light"} onClick={() => setDarkMode("light")} />
|
||||||
<ThemePreviewCard mode="auto" selected={darkMode === "auto"} onClick={() => setDarkMode("auto")} />
|
<ThemePreviewCard mode="auto" selected={darkMode === "auto"} onClick={() => setDarkMode("auto")} />
|
||||||
<ThemePreviewCard mode="dark" selected={darkMode === "dark"} onClick={() => setDarkMode("dark")} />
|
<ThemePreviewCard mode="dark" selected={darkMode === "dark"} onClick={() => setDarkMode("dark")} />
|
||||||
</div>
|
</div>
|
||||||
</GlassCard>
|
</SettingCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ===== 背景 ===== */}
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-foreground mb-3">背景</h3>
|
<SectionTitle>背景</SectionTitle>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<GlassCard>
|
<SettingCard>
|
||||||
<SettingRow label="背景图片" desc="更换启动器背景图片">
|
<SettingRow label="背景图片" desc="更换启动器背景图片">
|
||||||
<Button variant="outline" size="sm" onClick={handlePickImage}>
|
<Button variant="outline" size="sm" onPress={handlePickImage}>
|
||||||
选择图片
|
选择图片
|
||||||
</Button>
|
</Button>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
@@ -157,50 +130,61 @@ export function ThemeBgSetting() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</GlassCard>
|
</SettingCard>
|
||||||
|
|
||||||
<GlassCard>
|
<SettingCard>
|
||||||
<SettingRow label="背景模糊" desc={`对背景图施加高斯模糊,当前 ${blur}px`}>
|
<SettingRow label="背景模糊" desc={`对背景图施加高斯模糊,当前 ${blur}px`}>
|
||||||
<Slider
|
<Slider
|
||||||
className="w-[180px]"
|
className="w-[180px]"
|
||||||
value={[blur]}
|
value={blur}
|
||||||
min={0}
|
minValue={0}
|
||||||
max={20}
|
maxValue={20}
|
||||||
step={1}
|
step={1}
|
||||||
onValueChange={(v) => setBlur(Array.isArray(v) ? v[0] : v)}
|
onChange={(v) => setBlur(typeof v === "number" ? v : v[0])}
|
||||||
/>
|
>
|
||||||
|
<Slider.Track>
|
||||||
|
<Slider.Fill />
|
||||||
|
<Slider.Thumb />
|
||||||
|
</Slider.Track>
|
||||||
|
</Slider>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
</GlassCard>
|
</SettingCard>
|
||||||
|
|
||||||
<GlassCard>
|
<SettingCard>
|
||||||
<SettingRow label="背景不透明度" desc={`控制背景图的可见程度,当前 ${Math.round(opacity * 100)}%`}>
|
<SettingRow label="背景不透明度" desc={`控制背景图的可见程度,当前 ${Math.round(opacity * 100)}%`}>
|
||||||
<Slider
|
<Slider
|
||||||
className="w-[180px]"
|
className="w-[180px]"
|
||||||
value={[opacity * 100]}
|
value={opacity * 100}
|
||||||
min={0}
|
minValue={0}
|
||||||
max={100}
|
maxValue={100}
|
||||||
step={1}
|
step={1}
|
||||||
onValueChange={(v) => setOpacity((Array.isArray(v) ? v[0] : v) / 100)}
|
onChange={(v) => setOpacity((typeof v === "number" ? v : v[0]) / 100)}
|
||||||
/>
|
|
||||||
</SettingRow>
|
|
||||||
</GlassCard>
|
|
||||||
|
|
||||||
<GlassCard>
|
|
||||||
<SettingRow
|
|
||||||
label="背景图片视差"
|
|
||||||
desc="背景图片随窗口滚动产生视差位移"
|
|
||||||
>
|
>
|
||||||
<Switch checked={parallax} onCheckedChange={setParallax} />
|
<Slider.Track>
|
||||||
|
<Slider.Fill />
|
||||||
|
<Slider.Thumb />
|
||||||
|
</Slider.Track>
|
||||||
|
</Slider>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
</GlassCard>
|
</SettingCard>
|
||||||
|
|
||||||
<GlassCard>
|
<SettingCard>
|
||||||
|
<SettingRow label="背景图片视差" desc="背景图片随窗口滚动产生视差位移">
|
||||||
|
<Switch isSelected={parallax} onValueChange={setParallax}>
|
||||||
|
<Switch.Control>
|
||||||
|
<Switch.Thumb />
|
||||||
|
</Switch.Control>
|
||||||
|
</Switch>
|
||||||
|
</SettingRow>
|
||||||
|
</SettingCard>
|
||||||
|
|
||||||
|
<SettingCard>
|
||||||
<SettingRow label="恢复默认" desc="重置所有背景设置为初始状态">
|
<SettingRow label="恢复默认" desc="重置所有背景设置为初始状态">
|
||||||
<Button variant="destructive" size="sm" onClick={handleReset}>
|
<Button variant="danger" size="sm" onPress={handleReset}>
|
||||||
恢复默认
|
恢复默认
|
||||||
</Button>
|
</Button>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
</GlassCard>
|
</SettingCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
|
import { EmptyState } from "@heroui/react";
|
||||||
|
import { Monitor } from "lucide-react";
|
||||||
|
import { PageHeader } from "@/components/setting";
|
||||||
|
|
||||||
export function UiSetting() {
|
export function UiSetting() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-foreground mb-1">主界面</h2>
|
<PageHeader title="主界面" desc="自定义启动器主界面的布局、模块显示与交互方式" />
|
||||||
<p className="text-sm text-muted-foreground mb-6">自定义启动器主界面的布局、模块显示与交互方式</p>
|
<EmptyState className="py-16">
|
||||||
|
<Monitor className="w-10 h-10 text-muted-foreground/30" />
|
||||||
|
<p className="text-sm text-muted-foreground mt-3">该功能正在开发中</p>
|
||||||
|
</EmptyState>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import {
|
||||||
|
getKoringUser,
|
||||||
|
logoutKoring,
|
||||||
|
type KoringUser,
|
||||||
|
type KoringAuthData,
|
||||||
|
} from "../api/koring-auth";
|
||||||
|
|
||||||
|
interface KoringAuthState {
|
||||||
|
user: KoringUser | null;
|
||||||
|
authData: KoringAuthData | null;
|
||||||
|
loading: boolean;
|
||||||
|
|
||||||
|
initFromDisk: () => Promise<void>;
|
||||||
|
setUser: (user: KoringUser) => void;
|
||||||
|
logout: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useKoringAuthStore = create<KoringAuthState>((set) => ({
|
||||||
|
user: null,
|
||||||
|
authData: null,
|
||||||
|
loading: false,
|
||||||
|
|
||||||
|
initFromDisk: async () => {
|
||||||
|
// 如果已经有用户数据,跳过重复读取
|
||||||
|
if (get().user) return;
|
||||||
|
set({ loading: true });
|
||||||
|
try {
|
||||||
|
const data = await getKoringUser();
|
||||||
|
if (data?.user?.sub) {
|
||||||
|
set({ user: data.user, authData: data, loading: false });
|
||||||
|
} else {
|
||||||
|
set({ loading: false });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
set({ loading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setUser: (user) => set({ user }),
|
||||||
|
|
||||||
|
logout: async () => {
|
||||||
|
set({ user: null, authData: null });
|
||||||
|
await logoutKoring().catch(() => {});
|
||||||
|
},
|
||||||
|
}));
|
||||||
Reference in New Issue
Block a user