20260711存档01

This commit is contained in:
2026-07-11 23:33:44 +08:00
parent 9cb6a6e571
commit 2b19144352
56 changed files with 2156 additions and 627 deletions
+2
View File
@@ -12,6 +12,7 @@ import { Store } from "./pages/store";
import { Today } from "./pages/today";
import { PlayLink } from "./pages/play-link";
import { Setting } from "./pages/setting";
import { Gallery } from "./pages/gallery";
import { TaskQueue } from "./pages/task-queue";
import { Debug } from "./pages/debug";
import { SplashDebug } from "./pages/debug/splash-debug";
@@ -27,6 +28,7 @@ const pageMap = {
today: Today,
"play-link": PlayLink,
setting: Setting,
gallery: Gallery,
"task-queue": TaskQueue,
oobe: Oobe,
"oobe/about-info": OobeAboutInfo,
+132 -31
View File
@@ -1,59 +1,160 @@
import { ipcInvoke } from './ipc';
import { ipcInvoke, onIpcEvent } from './ipc';
export interface InstanceRuntime {
minecraft: string;
forge?: string;
neoForged?: string;
fabricLoader?: string;
quiltLoader?: string;
optifine?: string;
}
export interface InstanceConfig {
name: string;
mcVersion: string;
loaderType?: string;
loaderVersion?: string;
javaPath?: string;
memory?: { min?: string; max?: string };
createdAt: string;
author?: string;
description?: string;
runtime: InstanceRuntime;
java?: string;
minMemory?: number;
maxMemory?: number;
vmOptions?: string[];
mcOptions?: string[];
server?: { host: string; port?: number; name?: string };
showLog?: boolean;
hideLauncher?: boolean;
icon?: string;
creationDate: number;
lastAccessDate: number;
lastPlayedDate: number;
playtime: number;
}
export interface InstanceInfo {
name: string;
path: string;
config: InstanceConfig;
modCount?: number;
modCount: number;
resourcePackCount: number;
screenshotCount: number;
saveCount: number;
healthy: boolean;
issues: string[];
}
export interface InstallProgress {
stage: string;
current: number;
total: number;
message?: string;
}
export async function createInstance(
name: string,
gamePath: string,
mcVersion: string,
loaderType?: string,
loaderVersion?: string,
javaPath?: string,
memory?: { min?: string; max?: string }
runtime: InstanceRuntime,
options?: {
author?: string;
description?: string;
java?: string;
minMemory?: number;
maxMemory?: number;
vmOptions?: string[];
mcOptions?: string[];
}
): Promise<InstanceInfo> {
return ipcInvoke<InstanceInfo>('instance:create', {
name,
gamePath,
mcVersion,
loaderType,
loaderVersion,
javaPath,
memory,
runtime,
...options,
});
}
export async function listInstances(instancesPath: string): Promise<InstanceInfo[]> {
return ipcInvoke<InstanceInfo[]>('instance:list', { instancesPath });
export async function listInstances(gamePath: string): Promise<InstanceInfo[]> {
return ipcInvoke<InstanceInfo[]>('instance:list', { gamePath });
}
export async function deleteInstance(
name: string,
instancesPath: string
): Promise<{ deleted: string }> {
return ipcInvoke<{ deleted: string }>('instance:delete', {
name,
instancesPath,
});
export async function getInstanceInfo(name: string, gamePath: string): Promise<InstanceInfo> {
return ipcInvoke<InstanceInfo>('instance:info', { name, gamePath });
}
export async function getInstanceInfo(
export async function deleteInstance(name: string, gamePath: string): Promise<{ deleted: string }> {
return ipcInvoke<{ deleted: string }>('instance:delete', { name, gamePath });
}
export async function updateInstance(
name: string,
instancesPath: string
gamePath: string,
patch: Partial<Omit<InstanceConfig, 'name' | 'creationDate'>>
): Promise<InstanceInfo> {
return ipcInvoke<InstanceInfo>('instance:info', { name, instancesPath });
return ipcInvoke<InstanceInfo>('instance:update', { name, gamePath, patch });
}
export async function installInstance(
name: string,
gamePath: string
): Promise<{ requestId: string }> {
return ipcInvoke<{ requestId: string }>('instance:install', { name, gamePath });
}
export async function launchInstance(
name: string,
gamePath: string,
options: {
username: string;
uuid: string;
accessToken?: string;
javaPath?: string;
server?: { host: string; port?: number };
}
): Promise<{ requestId: string }> {
return ipcInvoke<{ requestId: string }>('instance:launch', { name, gamePath, ...options });
}
export async function diagnoseInstance(
name: string,
gamePath: string
): Promise<{ healthy: boolean; issues: string[] }> {
return ipcInvoke<{ healthy: boolean; issues: string[] }>('instance:diagnose', { name, gamePath });
}
export async function getMinecraftVersionList(type?: string) {
return ipcInvoke<{ versions: { id: string; type: string; url: string }[] }>('instance:version-list', { type });
}
export async function getForgeVersionList(mcVersion?: string) {
return ipcInvoke<{ versions: string[] | Record<string, string[]> }>('instance:forge-version-list', { mcVersion });
}
export async function getFabricVersionList(mcVersion?: string) {
return ipcInvoke<{ versions: string[] }>('instance:fabric-version-list', { mcVersion });
}
export async function getQuiltVersionList(mcVersion?: string) {
return ipcInvoke<{ versions: string[] }>('instance:quilt-version-list', { mcVersion });
}
// Event listeners
export function onInstallProgress(callback: (data: { requestId: string } & InstallProgress) => void) {
return onIpcEvent('instance:progress', callback);
}
export function onInstallComplete(callback: (data: { requestId: string; data: InstanceInfo }) => void) {
return onIpcEvent('instance:install-complete', callback);
}
export function onInstallError(callback: (data: { requestId: string; error: string }) => void) {
return onIpcEvent('instance:install-error', callback);
}
export function onLaunchEvent(callback: (data: { requestId: string; event: string; [key: string]: unknown }) => void) {
return onIpcEvent('instance:launch-event', callback);
}
export function onLaunchComplete(callback: (data: { requestId: string; data: { pid: number; version: string; username: string } }) => void) {
return onIpcEvent('instance:launch-complete', callback);
}
export function onLaunchError(callback: (data: { requestId: string; error: string }) => void) {
return onIpcEvent('instance:launch-error', callback);
}
+17
View File
@@ -0,0 +1,17 @@
import { useEffect } from "react";
import { toast } from "sonner";
import { BUILD_MODE } from "@/lib/mode";
import { VERSION } from "@/lib/version";
export function BetaWarning() {
useEffect(() => {
if (BUILD_MODE === "beta" || BUILD_MODE === "dev") {
toast.warning(`当前为 v${VERSION} BETA 测试版,不代表最终品质。`, {
duration: Infinity,
dismissible: true,
});
}
}, []);
return null;
}
+3 -4
View File
@@ -1,4 +1,5 @@
import { BUILD_MODE, LOGO_SVG } from "@/lib/mode";
import { VERSION } from "@/lib/version";
import { useUpdateStore } from "@/stores/updateStore";
import { relaunchApp } from "@/api/update";
import Silk from "@/components/silk/Silk";
@@ -17,8 +18,6 @@ const modeLabels: Record<string, string> = {
run: "正式版",
};
const VERSION = "0.1.0";
type UpdateState = "latest" | "hasUpdate" | "installed";
interface VersionCardProps {
@@ -50,12 +49,12 @@ export function VersionCard({
return (
<div
className={clsx(
"relative overflow-hidden rounded-xl border border-white/10",
"relative overflow-hidden rounded-xl border border-white/10 min-h-[200px]",
className,
)}
>
{/* Silk 背景 */}
<div className="absolute inset-0 z-0">
<div className="absolute inset-0 z-0" style={{ background: color }}>
<Silk speed={3} scale={1.2} color={color} noiseIntensity={1.2} rotation={0.3} />
</div>
+13 -2
View File
@@ -1,4 +1,4 @@
import { useEffect, useRef, useCallback } from "react";
import { useEffect, useRef, useCallback, useState } from "react";
import { useBackgroundStore } from "@/stores/backgroundStore";
import { useThemeStore } from "@/stores/themeStore";
import { useRouteStore } from "@/stores/routeStore";
@@ -11,9 +11,20 @@ export function BackgroundLayer() {
const route = useRouteStore((s) => s.current);
const forceDisableContentBlur = useDevStore((s) => s.forceDisableContentBlur);
const showContentBlur = route !== "home";
const [bgImage, setBgImage] = useState(image);
const bgRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (image && image !== DEFAULT_BG && !image.startsWith("data:")) {
(window as any).electronAPI?.getBackgroundDataUrl?.().then((dataUrl: string | null) => {
if (dataUrl) setBgImage(dataUrl);
});
return;
}
setBgImage(image);
}, [image]);
const handleMouseMove = useCallback(
(e: MouseEvent) => {
if (!parallax || !bgRef.current) return;
@@ -33,7 +44,7 @@ export function BackgroundLayer() {
return () => window.removeEventListener("mousemove", handleMouseMove);
}, [parallax, handleMouseMove]);
const bgUrl = image || DEFAULT_BG;
const bgUrl = bgImage || DEFAULT_BG;
const contentBlur = showContentBlur && !forceDisableContentBlur;
const getBackgroundStyle = (): React.CSSProperties => {
+9 -3
View File
@@ -74,17 +74,19 @@ interface SilkPlaneProps {
}
const SilkPlane = forwardRef(function SilkPlane({ uniforms }: SilkPlaneProps, ref: React.Ref<Mesh>) {
const { viewport } = useThree();
const { viewport, invalidate } = useThree();
useLayoutEffect(() => {
if (ref && typeof ref === "object" && ref.current) {
ref.current.scale.set(viewport.width, viewport.height, 1);
}
}, [ref, viewport]);
invalidate();
}, [ref, viewport, invalidate]);
useFrame((_, delta) => {
if (ref && typeof ref === "object" && ref.current) {
(ref.current.material as ShaderMaterial).uniforms.uTime.value += 0.1 * delta;
invalidate();
}
});
@@ -121,7 +123,11 @@ const Silk = ({ speed = 5, scale = 1, color = "#7B7481", noiseIntensity = 1.5, r
);
return (
<Canvas dpr={[1, 2]} frameloop="always">
<Canvas
dpr={[1, 2]}
style={{ position: "absolute", inset: 0, width: "100%", height: "100%", background: "black" }}
gl={{ antialias: true, alpha: false }}
>
<SilkPlane ref={meshRef} uniforms={uniforms} />
</Canvas>
);
+2 -2
View File
@@ -146,8 +146,8 @@ export function TitleBar({
style={{
WebkitAppRegion: "drag",
background: "var(--titlebar-bg)",
backdropFilter: "blur(16px)",
WebkitBackdropFilter: "blur(16px)",
backdropFilter: "blur(3px)",
WebkitBackdropFilter: "blur(3px)",
borderBottom: "1px solid var(--titlebar-border)",
userSelect: "none",
}}
+47
View File
@@ -0,0 +1,47 @@
import { useTheme } from "next-themes"
import { Toaster as Sonner, type ToasterProps } from "sonner"
import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from "lucide-react"
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
icons={{
success: (
<CircleCheckIcon className="size-4" />
),
info: (
<InfoIcon className="size-4" />
),
warning: (
<TriangleAlertIcon className="size-4" />
),
error: (
<OctagonXIcon className="size-4" />
),
loading: (
<Loader2Icon className="size-4 animate-spin" />
),
}}
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
"--border-radius": "var(--radius)",
} as React.CSSProperties
}
toastOptions={{
classNames: {
toast: "cn-toast",
},
}}
{...props}
/>
)
}
export { Toaster }
+5
View File
@@ -1,6 +1,7 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@import "sonner/dist/styles.css";
@import "@fontsource-variable/geist";
@import "@fontsource-variable/inter";
@@ -198,9 +199,11 @@
/* 内容区转场:前进方向 */
::view-transition-old(content) {
animation: 0.3s cubic-bezier(0.4, 0, 0.2, 1) both page-out-forward;
pointer-events: none;
}
::view-transition-new(content) {
animation: 0.3s cubic-bezier(0.4, 0, 0.2, 1) 0.05s both page-in-forward;
pointer-events: auto;
}
/* 设置子页面转场(已弃用 View Transitions,改用 keyframe */
@@ -208,10 +211,12 @@
/* 内容区转场:后退方向 */
:root[data-transition-dir="backward"] ::view-transition-old(content) {
animation-name: page-out-backward;
pointer-events: none;
}
:root[data-transition-dir="backward"] ::view-transition-new(content) {
animation-name: page-in-backward;
animation-delay: 0.05s;
pointer-events: auto;
}
@keyframes page-out-forward {
+13
View File
@@ -2,6 +2,8 @@ import { type ReactNode } from "react";
import { BackgroundLayer } from "@/components/background/BackgroundLayer";
import { SystemLayer } from "@/components/system/SystemLayer";
import { StartupPopup } from "@/components/StartupPopup";
import { BetaWarning } from "@/components/BetaWarning";
import { Toaster } from "sonner";
import { useA11yStore } from "@/stores/a11yStore";
import clsx from "clsx";
@@ -52,6 +54,17 @@ export function RootLayout({
{/* Startup popup — only when VITE_START_POP=true */}
<StartupPopup />
{/* Beta warning toast */}
<BetaWarning />
{/* Sonner toaster */}
<Toaster
position="bottom-right"
richColors
closeButton
duration={Infinity}
/>
</div>
);
}
+3
View File
@@ -0,0 +1,3 @@
declare const __APP_VERSION__: string;
export const VERSION: string = typeof __APP_VERSION__ !== "undefined" ? __APP_VERSION__ : "0.0.0";
+8
View File
@@ -0,0 +1,8 @@
export function Gallery() {
return (
<div className="p-6">
<h1 className="text-xl font-bold text-foreground mb-2"></h1>
<p className="text-sm text-muted-foreground"></p>
</div>
);
}
+23
View File
@@ -0,0 +1,23 @@
import { useInstanceStore } from "@/stores/instanceStore";
import { useRouteStore } from "@/stores/routeStore";
import clsx from "clsx";
export function InstanceTitle() {
const currentInstance = useInstanceStore((s) => s.currentInstance);
const navigate = useRouteStore((s) => s.navigate);
return (
<div
onClick={() => navigate("gallery")}
className={clsx(
"self-start pl-1 pr-1 py-1.5 rounded-lg cursor-pointer",
"text-5xl font-bold tracking-tight text-white",
"hover:bg-black/20",
"transition-colors duration-200",
"select-none",
)}
>
{currentInstance?.name ?? "选择一个实例"}
</div>
);
}
+1
View File
@@ -37,6 +37,7 @@ export function StartCard({ onSettingsClick }: StartCardProps) {
{/* Instance button */}
<button
onClick={() => navigate("gallery")}
className="flex items-center justify-center w-9 h-9 mr-0.5 text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-all duration-150 cursor-pointer shrink-0 rounded-full"
aria-label="选择实例"
>
+3 -1
View File
@@ -1,8 +1,10 @@
import { InstanceTitle } from "./InstanceTitle";
import { StartCard } from "./StartCard";
export function Home() {
return (
<div className="relative h-full flex flex-col justify-end items-start p-6">
<div className="relative h-full flex flex-col justify-end items-start p-6 gap-3">
<InstanceTitle />
<StartCard />
</div>
);
+1 -1
View File
@@ -29,7 +29,7 @@ const OFFICIAL_URL = "https://koring.app";
export function AboutSetting() {
const openLink = (url: string) => {
window.open(url, "_blank");
window.electronAPI?.openExternal(url);
};
return (
+3 -3
View File
@@ -17,7 +17,7 @@ function SettingRow({ label, desc, children }: { label: string; desc?: string; c
}
const openLink = (url: string) => {
window.open(url, "_blank");
window.electronAPI?.openExternal(url);
};
const licenses = [
@@ -50,9 +50,9 @@ export function CopyrightSetting() {
<div>
<h3 className="text-lg font-bold text-foreground mb-3"></h3>
<GlassCard>
<SettingRow label="MIT License" desc="Copyright © 2024 Koring Launcher Contributors">
<SettingRow label="LL-1.0 (LingkeLice 1.0)" desc="Copyright © Shenzhen Lingke Network Technology Co., Ltd.">
<button
onClick={() => openLink("https://opensource.org/licenses/MIT")}
onClick={() => openLink("https://support.lingke.ink/LL-1.0")}
className="inline-flex items-center gap-1.5 text-[13px] text-primary hover:underline"
>
@@ -110,14 +110,9 @@ export function ThemeBgSetting() {
const { image, opacity, setOpacity, blur, setBlur, setImage, reset } = useBackgroundStore();
const handlePickImage = async () => {
// In Electron, use the native file dialog via IPC
const result = await window.electronAPI?.invoke('dialog:openFile', {
filters: [
{ name: "图片", extensions: ["png", "jpg", "jpeg", "webp", "gif", "bmp"] },
],
}) as string | null;
if (result) {
setImage(result);
const dataUrl = await (window as any).electronAPI?.pickBackgroundImage();
if (dataUrl) {
setImage(dataUrl);
}
};
+1 -1
View File
@@ -65,7 +65,7 @@ export const useAuthStore = create<AuthState>((set) => ({
try {
const { state, authUrl } = await microsoftLoginStart(clientId);
set({ msAuthUrl: authUrl, msAuthState: state, loading: false });
window.open(authUrl, "_blank");
window.electronAPI?.openExternal(authUrl);
} catch (e: any) {
set({ error: e.message, loading: false });
}
+59 -33
View File
@@ -4,8 +4,11 @@ import {
listInstances,
deleteInstance,
getInstanceInfo,
installInstance,
launchInstance,
type InstanceInfo,
type InstanceRuntime,
} from "../api/instance";
import type { InstanceInfo } from "../api/instance";
interface InstanceState {
instances: InstanceInfo[];
@@ -13,18 +16,33 @@ interface InstanceState {
loading: boolean;
error: string | null;
fetchInstances: (instancesPath: string) => Promise<void>;
fetchInstances: (gamePath: string) => Promise<void>;
create: (
name: string,
gamePath: string,
mcVersion: string,
loaderType?: string,
loaderVersion?: string,
javaPath?: string,
memory?: { min?: string; max?: string }
runtime: InstanceRuntime,
options?: {
author?: string;
description?: string;
java?: string;
minMemory?: number;
maxMemory?: number;
}
) => Promise<void>;
remove: (name: string, instancesPath: string) => Promise<void>;
select: (name: string, instancesPath: string) => Promise<void>;
remove: (name: string, gamePath: string) => Promise<void>;
select: (name: string, gamePath: string) => Promise<void>;
install: (name: string, gamePath: string) => Promise<string>;
launch: (
name: string,
gamePath: string,
options: {
username: string;
uuid: string;
accessToken?: string;
javaPath?: string;
server?: { host: string; port?: number };
}
) => Promise<string>;
clearError: () => void;
}
@@ -34,36 +52,20 @@ export const useInstanceStore = create<InstanceState>((set) => ({
loading: false,
error: null,
fetchInstances: async (instancesPath: string) => {
fetchInstances: async (gamePath: string) => {
set({ loading: true, error: null });
try {
const instances = await listInstances(instancesPath);
const instances = await listInstances(gamePath);
set({ instances, loading: false });
} catch (e: any) {
set({ error: e.message, loading: false });
}
},
create: async (
name,
gamePath,
mcVersion,
loaderType?,
loaderVersion?,
javaPath?,
memory?
) => {
create: async (name, gamePath, runtime, options?) => {
set({ loading: true, error: null });
try {
const instance = await createInstance(
name,
gamePath,
mcVersion,
loaderType,
loaderVersion,
javaPath,
memory
);
const instance = await createInstance(name, gamePath, runtime, options);
set((state) => ({
instances: [...state.instances, instance],
loading: false,
@@ -73,10 +75,10 @@ export const useInstanceStore = create<InstanceState>((set) => ({
}
},
remove: async (name: string, instancesPath: string) => {
remove: async (name: string, gamePath: string) => {
set({ loading: true, error: null });
try {
await deleteInstance(name, instancesPath);
await deleteInstance(name, gamePath);
set((state) => ({
instances: state.instances.filter((i) => i.name !== name),
currentInstance:
@@ -88,15 +90,39 @@ export const useInstanceStore = create<InstanceState>((set) => ({
}
},
select: async (name: string, instancesPath: string) => {
select: async (name: string, gamePath: string) => {
set({ loading: true, error: null });
try {
const instance = await getInstanceInfo(name, instancesPath);
const instance = await getInstanceInfo(name, gamePath);
set({ currentInstance: instance, loading: false });
} catch (e: any) {
set({ error: e.message, loading: false });
}
},
install: async (name: string, gamePath: string) => {
set({ loading: true, error: null });
try {
const { requestId } = await installInstance(name, gamePath);
set({ loading: false });
return requestId;
} catch (e: any) {
set({ error: e.message, loading: false });
throw e;
}
},
launch: async (name, gamePath, options) => {
set({ loading: true, error: null });
try {
const { requestId } = await launchInstance(name, gamePath, options);
set({ loading: false });
return requestId;
} catch (e: any) {
set({ error: e.message, loading: false });
throw e;
}
},
clearError: () => set({ error: null }),
}));
+2
View File
@@ -6,6 +6,7 @@ export type RouteKey =
| "today"
| "play-link"
| "setting"
| "gallery"
| "task-queue"
| "oobe"
| "oobe/about-info"
@@ -29,6 +30,7 @@ interface RouteItem {
export const routes: RouteItem[] = [
{ key: "home", label: "首页", path: "/home" },
{ key: "gallery", label: "实例", path: "/gallery" },
{ key: "store", label: "资源", path: "/store" },
{ key: "today", label: "资讯", path: "/today" },
{ key: "play-link", label: "联机", path: "/play-link" },
+2
View File
@@ -10,6 +10,8 @@ interface ElectronAPI {
onResized: (callback: () => void) => () => void;
getTheme: () => Promise<'light' | 'dark' | 'system' | null>;
openExternal: (url: string) => Promise<void>;
}
declare global {