247 lines
7.2 KiB
TypeScript
247 lines
7.2 KiB
TypeScript
import { rmSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
|
|
import { app } from "electron";
|
|
|
|
import { defaultPetScale, markOnboardingCompleted, normalizeFamiliarName, normalizePetScale, petScaleOptions, type PetScaleValue } from "./app-state-core.js";
|
|
import {
|
|
buildRecordedActivityAnalytics,
|
|
type FamiliarOSActivityRecord,
|
|
} from "./app-state-analytics.js";
|
|
import {
|
|
cloneState,
|
|
normalizeState,
|
|
normalizePosition,
|
|
type FamiliarOSStateV1,
|
|
type InstalledPetState,
|
|
} from "./app-state-normalization.js";
|
|
import { builtInPet } from "./built-in-familiar.js";
|
|
import type { Point } from "./display.js";
|
|
import {
|
|
normalizePreferences,
|
|
} from "./app-state-preferences.js";
|
|
import { publishPluginAgentActivity } from "./plugin-events-source.js";
|
|
import {
|
|
acquireStartupInstallLock,
|
|
migrateLegacyStateFilenames,
|
|
readStateJsonFile,
|
|
writeStateJsonFile,
|
|
} from "./app-state-storage.js";
|
|
export { defaultOpenApiChatEndpoint, normalizeOpenApiChatEndpoint } from "./app-state-preferences.js";
|
|
export type { FamiliarOSActivityRecord, FamiliarOSAnalyticsState } from "./app-state-analytics.js";
|
|
export type { FamiliarOSStateV1, InstalledPetState } from "./app-state-normalization.js";
|
|
export type { FamiliarOSPreferences } from "./app-state-preferences.js";
|
|
|
|
export { defaultPetScale, normalizeFamiliarName, normalizePetScale, petScaleOptions, type PetScaleValue };
|
|
|
|
const stateFileName = "familiaros-state.json";
|
|
let statePath: string | null = null;
|
|
let currentState: FamiliarOSStateV1 | null = null;
|
|
let startupInstallLockPath: string | null = null;
|
|
|
|
export function initializeAppState(): void {
|
|
const userDataPath = app.getPath("userData");
|
|
migrateLegacyStateFilenames(userDataPath);
|
|
startupInstallLockPath = acquireStartupInstallLock(userDataPath);
|
|
|
|
statePath = join(userDataPath, stateFileName);
|
|
const nextState = normalizeState(readStateJsonFile(statePath));
|
|
writeStateToDisk(nextState);
|
|
currentState = nextState;
|
|
console.log(`FamiliarOS state initialized at ${statePath}.`);
|
|
}
|
|
|
|
export function releaseStartupInstallLock(): void {
|
|
const lockPath = startupInstallLockPath;
|
|
startupInstallLockPath = null;
|
|
if (lockPath) rmSync(lockPath, { recursive: true, force: true });
|
|
}
|
|
|
|
export function getAppStateSnapshot(): FamiliarOSStateV1 {
|
|
return cloneState(getInitializedState());
|
|
}
|
|
|
|
export function updatePreferences(patch: Partial<FamiliarOSStateV1["preferences"]>): FamiliarOSStateV1 {
|
|
const state = getInitializedState();
|
|
const preferences = normalizePreferences({ ...state.preferences, ...patch });
|
|
|
|
const nextState = normalizeState({
|
|
...state,
|
|
preferences,
|
|
});
|
|
|
|
commitState(nextState);
|
|
return getAppStateSnapshot();
|
|
}
|
|
|
|
export function isOnboardingCompleted(): boolean {
|
|
return getInitializedState().preferences.onboardingCompleted;
|
|
}
|
|
|
|
export function completeOnboarding(): FamiliarOSStateV1 {
|
|
const state = getInitializedState();
|
|
const nextState = normalizeState(markOnboardingCompleted(state));
|
|
commitState(nextState);
|
|
return getAppStateSnapshot();
|
|
}
|
|
|
|
export function setDefaultPet(defaultPetId: string): FamiliarOSStateV1 {
|
|
const state = getInitializedState();
|
|
const targetPet = state.familiars.installed.find((familiar) => familiar.id === defaultPetId);
|
|
|
|
if (!targetPet) {
|
|
throw new Error(`Cannot set unknown familiar as default: ${defaultPetId}`);
|
|
}
|
|
|
|
if (targetPet.broken) {
|
|
throw new Error(`Cannot set broken familiar as default: ${defaultPetId}`);
|
|
}
|
|
|
|
const nextState = normalizeState({
|
|
...state,
|
|
preferences: {
|
|
...state.preferences,
|
|
defaultPetId,
|
|
},
|
|
});
|
|
|
|
commitState(nextState);
|
|
return getAppStateSnapshot();
|
|
}
|
|
|
|
export function setDefaultPetPosition(position: Point): FamiliarOSStateV1 {
|
|
const state = getInitializedState();
|
|
|
|
const nextState = normalizeState({
|
|
...state,
|
|
defaultPet: {
|
|
...state.defaultPet,
|
|
position: normalizePosition(position),
|
|
},
|
|
});
|
|
|
|
commitState(nextState);
|
|
return getAppStateSnapshot();
|
|
}
|
|
|
|
export function resetDefaultPetPosition(position: Point): FamiliarOSStateV1 {
|
|
return setDefaultPetPosition(position);
|
|
}
|
|
|
|
export function getDefaultPetPosition(): Point | undefined {
|
|
return getInitializedState().defaultPet.position;
|
|
}
|
|
|
|
export function recordFamiliarOSActivity(activity: FamiliarOSActivityRecord, now: number = Date.now()): FamiliarOSStateV1 {
|
|
publishPluginAgentActivity({ kind: activity.kind, reaction: activity.reaction });
|
|
const state = getInitializedState();
|
|
const nextState = normalizeState({
|
|
...state,
|
|
analytics: buildRecordedActivityAnalytics(state.analytics, activity, now),
|
|
});
|
|
|
|
commitState(nextState);
|
|
return getAppStateSnapshot();
|
|
}
|
|
|
|
export function installPetState(familiar: Omit<InstalledPetState, "builtIn" | "protected" | "installed">): FamiliarOSStateV1 {
|
|
const state = getInitializedState();
|
|
|
|
if (state.familiars.installed.some((installedPet) => installedPet.id === familiar.id)) {
|
|
throw new Error(`Familiar is already installed: ${familiar.id}`);
|
|
}
|
|
|
|
const nextState = normalizeState({
|
|
...state,
|
|
familiars: {
|
|
installed: [
|
|
...state.familiars.installed,
|
|
{
|
|
...familiar,
|
|
builtIn: false,
|
|
protected: false,
|
|
installed: true,
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
commitState(nextState);
|
|
return getAppStateSnapshot();
|
|
}
|
|
|
|
export function removePetState(petId: string): FamiliarOSStateV1 {
|
|
if (petId === builtInPet.id) {
|
|
throw new Error("Built-in familiar cannot be removed.");
|
|
}
|
|
|
|
const state = getInitializedState();
|
|
const existing = state.familiars.installed.find((familiar) => familiar.id === petId);
|
|
|
|
if (!existing) {
|
|
throw new Error(`Familiar is not installed: ${petId}`);
|
|
}
|
|
|
|
const nextDefaultPetId = state.preferences.defaultPetId === petId ? builtInPet.id : state.preferences.defaultPetId;
|
|
|
|
const nextState = normalizeState({
|
|
...state,
|
|
preferences: {
|
|
...state.preferences,
|
|
defaultPetId: nextDefaultPetId,
|
|
},
|
|
familiars: {
|
|
installed: state.familiars.installed.filter((familiar) => familiar.id !== petId),
|
|
},
|
|
});
|
|
|
|
commitState(nextState);
|
|
return getAppStateSnapshot();
|
|
}
|
|
|
|
export function markPetBroken(petId: string, brokenReason: string): FamiliarOSStateV1 {
|
|
const state = getInitializedState();
|
|
|
|
if (petId === builtInPet.id) {
|
|
return getAppStateSnapshot();
|
|
}
|
|
|
|
const nextState = normalizeState({
|
|
...state,
|
|
preferences: {
|
|
...state.preferences,
|
|
defaultPetId: state.preferences.defaultPetId === petId ? builtInPet.id : state.preferences.defaultPetId,
|
|
},
|
|
familiars: {
|
|
installed: state.familiars.installed.map((familiar) => familiar.id === petId ? { ...familiar, broken: true, brokenReason } : familiar),
|
|
},
|
|
});
|
|
|
|
commitState(nextState);
|
|
return getAppStateSnapshot();
|
|
}
|
|
|
|
export function getStateFilePath(): string {
|
|
if (!statePath) {
|
|
throw new Error("FamiliarOS app state has not been initialized.");
|
|
}
|
|
|
|
return statePath;
|
|
}
|
|
|
|
function getInitializedState(): FamiliarOSStateV1 {
|
|
if (!currentState) {
|
|
throw new Error("FamiliarOS app state has not been initialized.");
|
|
}
|
|
|
|
return currentState;
|
|
}
|
|
|
|
function commitState(nextState: FamiliarOSStateV1): void {
|
|
writeStateToDisk(nextState);
|
|
currentState = nextState;
|
|
}
|
|
|
|
function writeStateToDisk(state: FamiliarOSStateV1): void {
|
|
writeStateJsonFile(getStateFilePath(), state);
|
|
}
|