feat: Implemented public plugin system accessible from the store.
feat: Implemented external ryujinx integration plugin refactor: moved sdk types and schemas to own workspace package fix: Fixed emulator launch with no game
This commit is contained in:
parent
9051834ace
commit
38cb752552
124 changed files with 1918 additions and 1067 deletions
631
src/packages/gameflow-sdk/shared.ts
Normal file
631
src/packages/gameflow-sdk/shared.ts
Normal file
|
|
@ -0,0 +1,631 @@
|
|||
import * as z from "zod";
|
||||
|
||||
export const settingRegistry = z.registry<{
|
||||
dev?: boolean;
|
||||
}>();
|
||||
|
||||
export const SettingsSchema = z.object({
|
||||
rommAddress: z.url().optional(),
|
||||
rommUser: z.string().default('admin').optional(),
|
||||
windowSize: z.object({ width: z.number(), height: z.number() }).optional(),
|
||||
windowPosition: z.object({ x: z.number(), y: z.number() }).optional(),
|
||||
downloadPath: z.string(),
|
||||
launchInFullscreen: z.boolean().default(true),
|
||||
disabledPlugins: z.array(z.string()).default([]),
|
||||
emulatorResolution: z.enum(['720p', '1080p', '1440p', '4k']).default('720p'),
|
||||
emulatorWidescreen: z.boolean().default(true)
|
||||
}); export const LocalSettingsSchema = z.object({
|
||||
backgroundBlur: z.boolean().default(true).meta({ title: "Background Blur" }),
|
||||
backgroundAnimation: z.boolean().default(true).meta({ title: "Background Animation" }),
|
||||
theme: z.enum(['dark', 'light', 'auto']).default('auto').meta({ title: "Theme" }),
|
||||
soundEffects: z.boolean().default(true).meta({ title: "Sounds" }),
|
||||
soundEffectsVolume: z.number().min(0).max(100).default(50).meta({ title: "Sound Volume" }),
|
||||
hapticsEffects: z.boolean().default(true).meta({ title: "Haptics" }),
|
||||
showRouterDevOptions: z.boolean().default(false).meta({ title: "Show Router Options" }).register(settingRegistry, { dev: true }),
|
||||
showQueryDevOptions: z.boolean().default(false).meta({ title: "Show Query Options" }).register(settingRegistry, { dev: true }),
|
||||
useGameflowKeyboard: z.boolean().default(true).describe("Show the gameflow on screen keyboard when using a controller").meta({ title: "Use Gameflow Keyboard" }),
|
||||
autoKeybaord: z.boolean().default(true).describe("Open on screen keybaord automatically").meta({ title: "Auto Keyboard" })
|
||||
});
|
||||
export const GameListFilterSchema = z.object({
|
||||
platform_source: z.string().optional(),
|
||||
platform_slug: z.string().optional(),
|
||||
platform_id: z.coerce.number().optional(),
|
||||
collection_id: z.coerce.number().optional(),
|
||||
collection_source: z.string().optional(),
|
||||
limit: z.coerce.number().optional(),
|
||||
search: z.string().optional(),
|
||||
offset: z.coerce.number().optional(),
|
||||
source: z.string().optional(),
|
||||
localOnly: z.coerce.boolean().optional(),
|
||||
orderBy: z.literal(['added', 'activity', 'name', 'release']).optional(),
|
||||
age_ratings: z.union([z.string().array(), z.string().transform(v => [v])]).optional(),
|
||||
genres: z.union([z.string().array(), z.string().transform(v => [v])]).optional(),
|
||||
keywords: z.union([z.string().array(), z.string().transform(v => [v])]).optional(),
|
||||
});
|
||||
export const DownloadSourceSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
});
|
||||
export const RommLoginDataSchema = z.object({ hostname: z.url(), username: z.string(), password: z.string() });
|
||||
export type GameListFilterType = z.infer<typeof GameListFilterSchema>;
|
||||
export const DirSchema = z.object({ name: z.string(), parentPath: z.string(), isDirectory: z.boolean() });
|
||||
export type DirType = z.infer<typeof DirSchema>;
|
||||
export const CustomEmulatorSchema = z.record(z.string(), z.string());
|
||||
export const GithubManifestSchema = z.object({
|
||||
sha: z.hash('sha1'),
|
||||
url: z.url(),
|
||||
tree: z.array(z.object({
|
||||
path: z.string(),
|
||||
mode: z.string(),
|
||||
type: z.enum(['blob', 'tree']),
|
||||
sha: z.hash('sha1'),
|
||||
url: z.url()
|
||||
}))
|
||||
});
|
||||
export const StoreGameSaveSchema = z.object({
|
||||
cwd: z.string(),
|
||||
globs: z.string().array()
|
||||
});
|
||||
export const StoreDownloadSchema = z.discriminatedUnion('type', [
|
||||
z.object({
|
||||
type: z.literal('direct'),
|
||||
url: z.url(),
|
||||
name: z.string().optional(),
|
||||
system: z.string(),
|
||||
main: z.string().optional(),
|
||||
saves: z.record(z.string(), StoreGameSaveSchema).optional()
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("itch"),
|
||||
path: z.string(),
|
||||
name: z.string().optional(),
|
||||
system: z.string(),
|
||||
saves: z.record(z.string(), StoreGameSaveSchema).optional()
|
||||
})
|
||||
]);
|
||||
export const NewGameSchema = z.object({
|
||||
name: z.string(),
|
||||
summary: z.string(),
|
||||
genres: z.string().regex(/^$|^(\s*\S[^,]*)(\s*,\s*\S[^,]*)*\s*$/, {
|
||||
message: "Must be a comma-separated list",
|
||||
})
|
||||
});
|
||||
export const StoreGameSchema = z.object({
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
version: z.string(),
|
||||
homepage: z.string().optional(),
|
||||
keywords: z.string().array().optional(),
|
||||
genres: z.string().array().optional(),
|
||||
companies: z.string().array().optional(),
|
||||
screenshots: z.string().array().optional(),
|
||||
covers: z.string().array().optional(),
|
||||
igdb_id: z.number().optional(),
|
||||
ra_id: z.number().optional(),
|
||||
sgdb_id: z.number().optional(),
|
||||
first_release_date: z.union([z.number(), z.date()]).optional(),
|
||||
player_count: z.string().optional(),
|
||||
saves: z.record(z.string(), z.record(z.string(), StoreGameSaveSchema)).optional(),
|
||||
downloads: z.record(z.string(), StoreDownloadSchema)
|
||||
});
|
||||
export const EmulatorPackageSchema = z.object({
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
homepage: z.url(),
|
||||
logo: z.url(),
|
||||
type: z.enum(['emulator']),
|
||||
os: z.array(z.enum(['darwin', 'linux', 'win32', 'android'])),
|
||||
keywords: z.array(z.string()).optional(),
|
||||
downloads: z.record(z.string(), z.array(z.discriminatedUnion('type', [
|
||||
z.object({
|
||||
type: z.literal(['github', 'gitlab']),
|
||||
pattern: z.string(),
|
||||
path: z.string(),
|
||||
bin: z.string().optional()
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('direct'),
|
||||
url: z.url(),
|
||||
bin: z.string().optional()
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('scoop'),
|
||||
url: z.url(),
|
||||
bin: z.string().optional()
|
||||
})
|
||||
]))).optional(),
|
||||
systems: z.array(z.string()),
|
||||
bios: z.literal(["required", "optional"]).optional()
|
||||
});
|
||||
export const ScoopPackageSchema = z.object({
|
||||
version: z.string(),
|
||||
url: z.url().optional(),
|
||||
description: z.string(),
|
||||
bin: z.string().optional(),
|
||||
architecture: z.record(z.string(), z.object({
|
||||
url: z.url(),
|
||||
hash: z.string().optional(),
|
||||
extract_dir: z.string().optional()
|
||||
})).optional()
|
||||
});
|
||||
export const SystemInfoSchema = z.object({
|
||||
battery: z.object({
|
||||
percent: z.number(),
|
||||
isCharging: z.boolean(),
|
||||
acConnected: z.boolean(),
|
||||
hasBattery: z.boolean()
|
||||
}),
|
||||
wifiConnections: z.array(z.object({ signalLevel: z.number() })),
|
||||
bluetoothDevices: z.array(z.object({ connected: z.boolean() }))
|
||||
});
|
||||
export const GithubReleaseSchema = z.object({
|
||||
id: z.number(),
|
||||
tag_name: z.string().optional(),
|
||||
url: z.url(),
|
||||
body: z.string(),
|
||||
assets: z.array(z.object({
|
||||
name: z.string(),
|
||||
browser_download_url: z.url(),
|
||||
content_type: z.string().optional()
|
||||
}))
|
||||
});
|
||||
export const EmulatorDownloadInfoSchema = z.object({
|
||||
id: z.string(),
|
||||
version: z.string().optional(),
|
||||
url: z.url().optional(),
|
||||
description: z.string().optional(),
|
||||
downloadDate: z.coerce.date(),
|
||||
type: z.string()
|
||||
});
|
||||
export const PluginEntrySchema = z.object({
|
||||
downloads: z.object({
|
||||
monthly: z.number(),
|
||||
weekly: z.number()
|
||||
}),
|
||||
searchScore: z.number(),
|
||||
installed: z.boolean(),
|
||||
update: z.object({ from: z.string() }).optional(),
|
||||
package: z.object({
|
||||
name: z.string(),
|
||||
keywords: z.string().array(),
|
||||
version: z.string(),
|
||||
description: z.string().optional(),
|
||||
sanitized_name: z.string(),
|
||||
license: z.string().optional(),
|
||||
publisher: z.object({
|
||||
email: z.string(),
|
||||
username: z.string(),
|
||||
trustedPublisher: z.object({
|
||||
id: z.string(),
|
||||
oidcConfigId: z.string()
|
||||
}).optional()
|
||||
}),
|
||||
date: z.coerce.date(),
|
||||
links: z.object({
|
||||
homepage: z.string().optional(),
|
||||
repository: z.string().optional(),
|
||||
bugs: z.string().optional(),
|
||||
npm: z.url()
|
||||
})
|
||||
})
|
||||
});
|
||||
export const PluginBunDetailsSchema = z.object({
|
||||
name: z.string(),
|
||||
keywords: z.string().array(),
|
||||
version: z.string(),
|
||||
author: z.object({ name: z.string().optional() }).optional(),
|
||||
license: z.string().optional(),
|
||||
devDependencies: z.record(z.string(), z.string()).optional(),
|
||||
dependencies: z.record(z.string(), z.string()).optional(),
|
||||
maintainers: z.object({ name: z.string() }).array().optional(),
|
||||
dist: z.object({ unpackedSize: z.number() }),
|
||||
description: z.string().optional(),
|
||||
_npmUser: z.object({ name: z.string() }).optional()
|
||||
});
|
||||
export type EmulatorPackageType = z.infer<typeof EmulatorPackageSchema>;
|
||||
export type StoreGameType = z.infer<typeof StoreGameSchema>;
|
||||
export type StoreDownloadType = z.infer<typeof StoreDownloadSchema>;
|
||||
export type SettingsType = z.infer<typeof SettingsSchema>;
|
||||
export type LocalSettingsType = z.infer<typeof LocalSettingsSchema>;
|
||||
export const PlatformSchema = z.object({ slug: z.string() });
|
||||
export type SystemInfoType = z.infer<typeof SystemInfoSchema>;
|
||||
export type EmulatorDownloadInfoType = z.infer<typeof EmulatorDownloadInfoSchema>;
|
||||
export type DownloadSourceType = z.infer<typeof DownloadSourceSchema>;
|
||||
export type PluginEntryType = z.infer<typeof PluginEntrySchema>;
|
||||
export type PluginBunDetailsType = z.infer<typeof PluginBunDetailsSchema>;
|
||||
|
||||
export interface SaveFileChange
|
||||
{
|
||||
subPath: string | string[];
|
||||
isGlob?: true;
|
||||
cwd: string;
|
||||
shared: boolean;
|
||||
fixedSize?: boolean;
|
||||
}
|
||||
|
||||
export type EmulatorSourceType = 'custom' | 'store' | 'registry' | 'system' | 'static' | 'embedded';
|
||||
|
||||
export interface EmulatorSourceEntryType
|
||||
{
|
||||
binPath: string;
|
||||
rootPath?: string;
|
||||
type: EmulatorSourceType;
|
||||
exists: boolean;
|
||||
}
|
||||
|
||||
export interface FrontEndEmulator
|
||||
{
|
||||
name: string;
|
||||
source: string;
|
||||
logo: string;
|
||||
systems: EmulatorSystem[];
|
||||
description?: string;
|
||||
gameCount: number;
|
||||
validSources: EmulatorSourceEntryType[];
|
||||
integrations: EmulatorSupport[];
|
||||
}
|
||||
|
||||
export interface EmulatorSystem { id: string, romm_slug?: string, name: string, iconUrl: string; }
|
||||
|
||||
export interface FrontEndEmulatorDetailedDownload
|
||||
{
|
||||
name: string;
|
||||
type: string | undefined;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export interface FrontEndEmulatorDetailed extends FrontEndEmulator
|
||||
{
|
||||
homepage: string;
|
||||
description: string;
|
||||
downloads: FrontEndEmulatorDetailedDownload[];
|
||||
keywords?: string[];
|
||||
screenshots: string[];
|
||||
biosRequirement?: "required" | "optional";
|
||||
bios?: string[];
|
||||
storeDownloadInfo?: { hasUpdate: boolean; version?: string, type: string; description?: string; };
|
||||
}
|
||||
|
||||
export interface FrontEndGameTypeDetailedAchievement
|
||||
{
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
date?: Date;
|
||||
date_hardcode?: Date;
|
||||
badge_url?: string;
|
||||
display_order: number;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface FrontEndGameTypeDetailedEmulator extends FrontEndEmulator
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
export interface FrontEndGameTypeDetailed extends Exclude<FrontEndGameTypeWithIds, "metadata">
|
||||
{
|
||||
summary: string | null;
|
||||
fs_size_bytes: number | null;
|
||||
missing: boolean;
|
||||
local: boolean;
|
||||
version?: string | null;
|
||||
version_system?: string | null;
|
||||
version_source?: string | null;
|
||||
metadata: FrontEndGameMetadataDetailed,
|
||||
emulators?: FrontEndGameTypeDetailedEmulator[],
|
||||
achievements?: {
|
||||
unlocked: number;
|
||||
total: number;
|
||||
entires: FrontEndGameTypeDetailedAchievement[];
|
||||
};
|
||||
};
|
||||
|
||||
export interface Drive
|
||||
{
|
||||
parent: string | null;
|
||||
device: string;
|
||||
label: string;
|
||||
mountPoint: string | null;
|
||||
type: string;
|
||||
size: number;
|
||||
used: number;
|
||||
isRemovable: boolean;
|
||||
interfaceType: string | null;
|
||||
hasWriteAccess: boolean;
|
||||
hasReadAccess: boolean;
|
||||
}
|
||||
|
||||
export interface DownloadsDrive
|
||||
{
|
||||
device: string;
|
||||
label: string;
|
||||
mountPoint: string | null;
|
||||
isRemovable: boolean;
|
||||
size: number;
|
||||
used: number;
|
||||
isCurrentlyUsed: boolean;
|
||||
unusableReason: 'not_enough_space' | 'already_used' | null;
|
||||
}
|
||||
|
||||
export interface FrontendNotification
|
||||
{
|
||||
title?: string;
|
||||
message: string;
|
||||
type: 'success' | 'error' | 'info' | 'custom';
|
||||
icon?: "save" | "upload" | "clock";
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
export interface CommandEntry
|
||||
{
|
||||
/** The ID of the command. Could be just an index or a string */
|
||||
id: string | number;
|
||||
/** The front end label for the command. Mainly gotten from ES-DE list */
|
||||
label?: string;
|
||||
/** Compiled command to be executed */
|
||||
command: string | string[];
|
||||
/** Environment variables */
|
||||
env?: Record<string, string>,
|
||||
/** The path the spawned process will start at */
|
||||
startDir?: string;
|
||||
/** Is the command valid, for example does the executable exists */
|
||||
valid: boolean;
|
||||
/** Run the command as shell. Defaults is true */
|
||||
shell?: boolean;
|
||||
/** For what emulator is the command */
|
||||
emulator?: string;
|
||||
/** Where the emulator came from */
|
||||
emulatorSource?: EmulatorSourceType;
|
||||
/** Metadata for the command */
|
||||
metadata: {
|
||||
romPath?: string;
|
||||
emulatorBin?: string;
|
||||
/** The root directory of the emulator */
|
||||
emulatorDir?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface FrontEndId
|
||||
{
|
||||
id: string;
|
||||
source: string;
|
||||
}
|
||||
|
||||
// Stuff stored in the local sqlite metadata field
|
||||
export interface LocalGameMetadata
|
||||
{
|
||||
genres?: string[],
|
||||
companies?: string[],
|
||||
game_modes?: string[],
|
||||
age_ratings?: string[];
|
||||
player_count?: string;
|
||||
first_release_date?: number;
|
||||
average_rating?: number;
|
||||
}
|
||||
|
||||
export interface FrontEndPlatformType
|
||||
{
|
||||
id: FrontEndId;
|
||||
slug: string;
|
||||
name: string;
|
||||
family_name?: string | null;
|
||||
path_cover: string | null;
|
||||
game_count: number;
|
||||
updated_at: Date;
|
||||
hasLocal: boolean;
|
||||
paths_screenshots: string[];
|
||||
}
|
||||
|
||||
export interface FrontEndGameTypeWithIds extends FrontEndGameType
|
||||
{
|
||||
igdb_id: number | null;
|
||||
ra_id: number | null;
|
||||
}
|
||||
|
||||
export interface FrontEndFilterSets
|
||||
{
|
||||
age_ratings: Set<string>,
|
||||
player_counts: Set<string>,
|
||||
languages: Set<string>,
|
||||
companies: Set<string>,
|
||||
genres: Set<string>;
|
||||
}
|
||||
|
||||
export interface FrontEndFilterLists
|
||||
{
|
||||
age_ratings: string[],
|
||||
player_counts: string[],
|
||||
languages: string[],
|
||||
companies: string[],
|
||||
genres: string[];
|
||||
}
|
||||
|
||||
export interface FrontEndGameMetadata
|
||||
{
|
||||
first_release_date: Date | null;
|
||||
}
|
||||
|
||||
export interface FrontEndGameMetadataDetailed extends FrontEndGameMetadata
|
||||
{
|
||||
genres: string[],
|
||||
companies: string[],
|
||||
game_modes: string[],
|
||||
age_ratings: string[];
|
||||
player_count: string | null;
|
||||
average_rating: number | null;
|
||||
}
|
||||
|
||||
export interface FrontEndGameType
|
||||
{
|
||||
platform_display_name: string | null,
|
||||
path_platform_cover: string | null;
|
||||
id: FrontEndId,
|
||||
source: string | null,
|
||||
source_id: string | null,
|
||||
path_fs: string | null,
|
||||
path_covers: string[],
|
||||
last_played: Date | null,
|
||||
updated_at: Date,
|
||||
metadata: FrontEndGameMetadata,
|
||||
slug: string | null,
|
||||
name: string | null,
|
||||
platform_id: number | null,
|
||||
platform_slug: string | null,
|
||||
paths_screenshots: string[];
|
||||
};
|
||||
|
||||
export type GameStatusType = 'installed' | 'missing-emulator' | 'error' | 'install' | 'download' | 'extract' | 'playing' | 'queued';
|
||||
|
||||
export interface GameInstallProgress
|
||||
{
|
||||
progress?: number;
|
||||
status?: GameStatusType;
|
||||
details?: string;
|
||||
commands?: CommandEntry[];
|
||||
error?: any;
|
||||
}
|
||||
|
||||
export type JobStatus = 'completed' | 'error' | 'running' | 'queued' | 'aborted';
|
||||
export type GameInstallProgressEvent = 'refresh';
|
||||
|
||||
export interface FrontendPlugin
|
||||
{
|
||||
name: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
category: string;
|
||||
enabled: boolean;
|
||||
canDisable: boolean;
|
||||
canUninstall: boolean;
|
||||
source: PluginSourceType;
|
||||
hasSettings: boolean;
|
||||
version: string;
|
||||
icon?: string;
|
||||
update?: PluginUpdateCheck;
|
||||
}
|
||||
|
||||
export interface PluginUpdateCheck
|
||||
{
|
||||
current: string;
|
||||
new: string;
|
||||
}
|
||||
|
||||
export type PluginSourceType = "builtin" | "store";
|
||||
|
||||
export type KeysWithValueAssignableTo<T, Value> = {
|
||||
[K in keyof T]: Exclude<T[K], undefined> extends Value ? K : never;
|
||||
}[keyof T];
|
||||
|
||||
export interface DownloadInfo
|
||||
{
|
||||
id: string;
|
||||
screenshotUrls: string[];
|
||||
coverUrl: string;
|
||||
platform?: DownloadPlatform;
|
||||
slug?: string;
|
||||
path_fs?: string;
|
||||
main_glob?: string;
|
||||
summary?: string;
|
||||
name: string;
|
||||
last_played?: Date;
|
||||
igdb_id?: number;
|
||||
ra_id?: number;
|
||||
source_id: string;
|
||||
system_slug: string;
|
||||
extract_path?: string;
|
||||
metadata?: any;
|
||||
files: DownloadFileEntry[];
|
||||
auth?: string;
|
||||
version?: string;
|
||||
version_source?: string;
|
||||
version_system?: string;
|
||||
}
|
||||
|
||||
export interface DownloadPlatform
|
||||
{
|
||||
id: string;
|
||||
source: string;
|
||||
igdb_id?: number;
|
||||
igdb_slug?: string;
|
||||
ra_id?: number;
|
||||
moby_id?: number;
|
||||
slug: string;
|
||||
name: string;
|
||||
/** Like Sony or Nintendo */
|
||||
family_name?: string;
|
||||
}
|
||||
|
||||
export interface DownloadFileEntry
|
||||
{
|
||||
url: URL;
|
||||
/** The path of the file, excluding the name */
|
||||
file_path: string;
|
||||
/** Just the name of the file including the extension */
|
||||
file_name: string;
|
||||
/** Checksum of the file */
|
||||
sha1?: string;
|
||||
/** Size in bytes */
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export interface LocalDownloadFileEntry extends DownloadFileEntry
|
||||
{
|
||||
/** Exists on the file system */
|
||||
exists: boolean;
|
||||
/** Matches the checksum */
|
||||
matches: boolean;
|
||||
}
|
||||
|
||||
export interface FrontEndCollection
|
||||
{
|
||||
id: FrontEndId;
|
||||
name: string;
|
||||
description: string;
|
||||
path_platform_cover: string | null;
|
||||
game_count: number;
|
||||
}
|
||||
|
||||
export type EmulatorCapabilities = "saves" | "fullscreen" | "resolution" | "batch" | "states" | "config";
|
||||
|
||||
export interface EmulatorSupport
|
||||
{
|
||||
id: string;
|
||||
source?: EmulatorSourceEntryType;
|
||||
supportLevel?: "partial" | "full";
|
||||
capabilities?: EmulatorCapabilities[];
|
||||
}
|
||||
|
||||
export interface GameLookup
|
||||
{
|
||||
source: string;
|
||||
id: string;
|
||||
coverUrl: string | null | undefined;
|
||||
slug: string | null | undefined;
|
||||
screenshotUrls: string[];
|
||||
name: string;
|
||||
summary: string | null | undefined;
|
||||
genres: string[];
|
||||
companies: string[];
|
||||
game_modes: string[];
|
||||
age_ratings: string[];
|
||||
player_count: string | undefined;
|
||||
first_release_date: number | undefined;
|
||||
average_rating: number | undefined;
|
||||
keywords: string[];
|
||||
igdb_id: number | undefined;
|
||||
platforms: {
|
||||
id: number;
|
||||
name?: string | null;
|
||||
displayName: string;
|
||||
slug: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface AutoSaveChange
|
||||
{
|
||||
subPath: string;
|
||||
cwd: string;
|
||||
}
|
||||
|
||||
export type SaveSlots = Record<string, { cwd: string; }>;
|
||||
Loading…
Add table
Add a link
Reference in a new issue