From 66668d85d603c5841d755a6023aa1925559fc6d4 Mon Sep 17 00:00:00 2001 From: 苏向夜 Date: Wed, 25 Feb 2026 01:32:51 +0800 Subject: chore(workspace): replace legacy codes --- packages/ui/src/models/auth.ts | 142 ++++++++++++++++++++++++++++++++++++ packages/ui/src/models/instances.ts | 135 ++++++++++++++++++++++++++++++++++ packages/ui/src/models/settings.ts | 75 +++++++++++++++++++ 3 files changed, 352 insertions(+) create mode 100644 packages/ui/src/models/auth.ts create mode 100644 packages/ui/src/models/instances.ts create mode 100644 packages/ui/src/models/settings.ts (limited to 'packages/ui/src/models') diff --git a/packages/ui/src/models/auth.ts b/packages/ui/src/models/auth.ts new file mode 100644 index 0000000..10b2a0d --- /dev/null +++ b/packages/ui/src/models/auth.ts @@ -0,0 +1,142 @@ +import { listen, type UnlistenFn } from "@tauri-apps/api/event"; +import { open } from "@tauri-apps/plugin-shell"; +import { Mutex } from "es-toolkit"; +import { toString as stringify } from "es-toolkit/compat"; +import { toast } from "sonner"; +import { create } from "zustand"; +import { + completeMicrosoftLogin, + getActiveAccount, + loginOffline, + logout, + startMicrosoftLogin, +} from "@/client"; +import type { Account, DeviceCodeResponse } from "@/types"; + +export interface AuthState { + account: Account | null; + loginMode: Account["type"] | null; + deviceCode: DeviceCodeResponse | null; + _pollingInterval: number | null; + _mutex: Mutex; + statusMessage: string | null; + _progressUnlisten: UnlistenFn | null; + + init: () => Promise; + setLoginMode: (mode: Account["type"] | null) => void; + loginOnline: (onSuccess?: () => void | Promise) => Promise; + _pollLoginStatus: ( + deviceCode: string, + onSuccess?: () => void | Promise, + ) => Promise; + cancelLoginOnline: () => Promise; + loginOffline: (username: string) => Promise; + logout: () => Promise; +} + +export const useAuthStore = create((set, get) => ({ + account: null, + loginMode: null, + deviceCode: null, + _pollingInterval: null, + statusMessage: null, + _progressUnlisten: null, + _mutex: new Mutex(), + + init: async () => { + try { + const account = await getActiveAccount(); + set({ account }); + } catch (error) { + console.error("Failed to initialize auth store:", error); + } + }, + setLoginMode: (mode) => set({ loginMode: mode }), + loginOnline: async (onSuccess) => { + const { _pollLoginStatus } = get(); + + set({ statusMessage: "Waiting for authorization..." }); + + try { + const unlisten = await listen("auth-progress", (event) => { + const message = event.payload; + console.log(message); + set({ statusMessage: stringify(message), _progressUnlisten: unlisten }); + }); + } catch (error) { + console.warn("Failed to attch auth-progress listener:", error); + toast.warning("Failed to attch auth-progress listener"); + } + + const deviceCode = await startMicrosoftLogin(); + navigator.clipboard?.writeText(deviceCode.userCode).catch((err) => { + console.error("Failed to copy to clipboard:", err); + }); + open(deviceCode.verificationUri).catch((err) => { + console.error("Failed to open browser:", err); + }); + const ms = Number(deviceCode.interval) * 1000; + const interval = setInterval(() => { + _pollLoginStatus(deviceCode.deviceCode, onSuccess); + }, ms); + set({ _pollingInterval: interval, deviceCode }); + }, + _pollLoginStatus: async (deviceCode, onSuccess) => { + const { _pollingInterval, _mutex: mutex, _progressUnlisten } = get(); + if (mutex.isLocked) return; + mutex.acquire(); + try { + const account = await completeMicrosoftLogin(deviceCode); + clearInterval(_pollingInterval ?? undefined); + _progressUnlisten?.(); + onSuccess?.(); + set({ account, loginMode: "microsoft" }); + } catch (error) { + if (error === "authorization_pending") { + console.log("Authorization pending..."); + } else { + console.error("Failed to poll login status:", error); + toast.error("Failed to poll login status"); + } + } finally { + mutex.release(); + } + }, + cancelLoginOnline: async () => { + const { account, logout, _pollingInterval, _progressUnlisten } = get(); + clearInterval(_pollingInterval ?? undefined); + _progressUnlisten?.(); + if (account) { + await logout(); + } + set({ + loginMode: null, + _pollingInterval: null, + statusMessage: null, + _progressUnlisten: null, + }); + }, + loginOffline: async (username: string) => { + const trimmedUsername = username.trim(); + if (trimmedUsername.length === 0) { + throw new Error("Username cannot be empty"); + } + + try { + const account = await loginOffline(trimmedUsername); + set({ account, loginMode: "offline" }); + } catch (error) { + console.error("Failed to login offline:", error); + toast.error("Failed to login offline"); + } + }, + logout: async () => { + try { + await logout(); + set({ account: null }); + } catch (error) { + console.error("Failed to logout:", error); + toast.error("Failed to logout"); + } + }, +})); diff --git a/packages/ui/src/models/instances.ts b/packages/ui/src/models/instances.ts new file mode 100644 index 0000000..f434c7c --- /dev/null +++ b/packages/ui/src/models/instances.ts @@ -0,0 +1,135 @@ +import { toast } from "sonner"; +import { create } from "zustand"; +import { + createInstance, + deleteInstance, + duplicateInstance, + getActiveInstance, + getInstance, + listInstances, + setActiveInstance, + updateInstance, +} from "@/client"; +import type { Instance } from "@/types"; + +interface InstancesState { + // State + instances: Instance[]; + activeInstance: Instance | null; + + // Actions + refresh: () => Promise; + create: (name: string) => Promise; + delete: (id: string) => Promise; + update: (instance: Instance) => Promise; + setActiveInstance: (instance: Instance) => Promise; + duplicate: (id: string, newName: string) => Promise; + getInstance: (id: string) => Promise; +} + +export const useInstancesStore = create((set, get) => ({ + // Initial state + instances: [], + activeInstance: null, + + // Actions + refresh: async () => { + const { setActiveInstance } = get(); + try { + const instances = await listInstances(); + const active = await getActiveInstance(); + + if (!active && instances.length > 0) { + // If no active instance but instances exist, set the first one as active + await setActiveInstance(instances[0]); + } + + set({ instances }); + } catch (e) { + console.error("Failed to load instances:", e); + toast.error("Error loading instances"); + } + }, + + create: async (name) => { + const { refresh } = get(); + try { + const instance = await createInstance(name); + await refresh(); + toast.success(`Instance "${name}" created successfully`); + return instance; + } catch (e) { + console.error("Failed to create instance:", e); + toast.error("Error creating instance"); + return null; + } + }, + + delete: async (id) => { + const { refresh, instances, activeInstance, setActiveInstance } = get(); + try { + await deleteInstance(id); + await refresh(); + + // If deleted instance was active, set another as active + if (activeInstance?.id === id) { + if (instances.length > 0) { + await setActiveInstance(instances[0]); + } else { + set({ activeInstance: null }); + } + } + + toast.success("Instance deleted successfully"); + } catch (e) { + console.error("Failed to delete instance:", e); + toast.error("Error deleting instance"); + } + }, + + update: async (instance) => { + const { refresh } = get(); + try { + await updateInstance(instance); + await refresh(); + toast.success("Instance updated successfully"); + } catch (e) { + console.error("Failed to update instance:", e); + toast.error("Error updating instance"); + } + }, + + setActiveInstance: async (instance) => { + try { + await setActiveInstance(instance.id); + set({ activeInstance: instance }); + toast.success("Active instance changed"); + } catch (e) { + console.error("Failed to set active instance:", e); + toast.error("Error setting active instance"); + } + }, + + duplicate: async (id, newName) => { + const { refresh } = get(); + try { + const instance = await duplicateInstance(id, newName); + await refresh(); + toast.success(`Instance duplicated as "${newName}"`); + return instance; + } catch (e) { + console.error("Failed to duplicate instance:", e); + toast.error("Error duplicating instance"); + return null; + } + }, + + getInstance: async (id) => { + try { + return await getInstance(id); + } catch (e) { + console.error("Failed to get instance:", e); + return null; + } + }, +})); diff --git a/packages/ui/src/models/settings.ts b/packages/ui/src/models/settings.ts new file mode 100644 index 0000000..9f4119c --- /dev/null +++ b/packages/ui/src/models/settings.ts @@ -0,0 +1,75 @@ +import { toast } from "sonner"; +import { create } from "zustand/react"; +import { getConfigPath, getSettings, saveSettings } from "@/client"; +import type { LauncherConfig } from "@/types"; + +export interface SettingsState { + config: LauncherConfig | null; + configPath: string | null; + + /* Theme getter */ + get theme(): string; + /* Apply theme to the document */ + applyTheme: (theme?: string) => void; + + /* Refresh settings from the backend */ + refresh: () => Promise; + /* Save settings to the backend */ + save: () => Promise; + /* Update settings in the backend */ + update: (config: LauncherConfig) => Promise; + /* Merge settings with the current config without saving */ + merge: (config: Partial) => void; +} + +export const useSettingsStore = create((set, get) => ({ + config: null, + configPath: null, + + get theme() { + const { config } = get(); + return config?.theme || "dark"; + }, + applyTheme: (theme?: string) => { + const { config } = get(); + if (!config) return; + if (!theme) theme = config.theme; + let themeValue = theme; + if (theme === "system") { + themeValue = window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "light"; + } + document.documentElement.classList.remove("light", "dark"); + document.documentElement.setAttribute("data-theme", themeValue); + document.documentElement.classList.add(themeValue); + set({ config: { ...config, theme } }); + }, + + refresh: async () => { + const { applyTheme } = get(); + try { + const settings = await getSettings(); + const path = await getConfigPath(); + set({ config: settings, configPath: path }); + applyTheme(settings.theme); + } catch (error) { + console.error("Failed to load settings:", error); + toast.error("Failed to load settings"); + } + }, + save: async () => { + const { config } = get(); + if (!config) return; + await saveSettings(config); + }, + update: async (config) => { + await saveSettings(config); + set({ config }); + }, + merge: (config) => { + const { config: currentConfig } = get(); + if (!currentConfig) throw new Error("Settings not loaded"); + set({ config: { ...currentConfig, ...config } }); + }, +})); -- cgit v1.2.3-70-g09d2