diff --git a/src/app.css b/src/app.css index 3c723ff..99d7ca4 100644 --- a/src/app.css +++ b/src/app.css @@ -42,7 +42,7 @@ --background: 0.115 0 89.876; --foreground: 0.9461 0 0; - --border: 0.264 0 89.876; + --border: 0.2256 0 0; --input: 0.244 0 89.876; --ring: 0.4493 0.1953 294.69; diff --git a/src/lib/client/action/repositories/package.repository.ts b/src/lib/client/action/repositories/package.repository.ts index ac5de77..b6a3cbb 100644 --- a/src/lib/client/action/repositories/package.repository.ts +++ b/src/lib/client/action/repositories/package.repository.ts @@ -1,47 +1,42 @@ -import type { ComponentManifest, SystemManifest } from '$lib/server/project/package'; - import { BaseRepository } from '../base.repository'; import type { - AddComponentsActionInput, - AddSystemsActionInput, - ComponentPackageResult, + AssetPkg, + ComponentPkg, CreateComponentActionInput, CreateSystemActionInput, - GetComponentsManifestsActionInput, - GetSystemsManifestsActionInput, - SystemPackageResult, + InstallPackagesActionInput, + Package, + SearchInput, + SearchPackages, + SystemPkg, } from '../types'; export class ProjectPackageRepository extends BaseRepository { - addComponents(input: AddComponentsActionInput): Promise { - return this.run(`/actions/project/package?/add-components`, input); - } - - addSystems(input: AddSystemsActionInput): Promise { - return this.run(`/actions/project/package?/add-systems`, input); - } - - createComponent(input: CreateComponentActionInput): Promise { + createComponent(input: CreateComponentActionInput): Promise { return this.run(`/actions/project/package?/create-component`, input); } - createSystem(input: CreateSystemActionInput): Promise { + createSystem(input: CreateSystemActionInput): Promise { return this.run(`/actions/project/package?/create-system`, input); } - getComponentsManifests(input: GetComponentsManifestsActionInput): Promise { - return this.run(`/actions/project/package?/get-components-manifests`, input); + getComponents(): Promise { + return this.run(`/actions/project/package?/get-components`); } - getSystemsManifests(input: GetSystemsManifestsActionInput): Promise { - return this.run(`/actions/project/package?/get-systems-manifests`, input); + getSystems(): Promise { + return this.run(`/actions/project/package?/get-systems`); } - getComponents(): Promise { - return this.run(`/actions/project/package?/get-components`); + getAssets(): Promise { + return this.run(`/actions/project/package?/get-assets`); } - getSystems(): Promise { - return this.run(`/actions/project/package?/get-systems`); + installPackages(input: InstallPackagesActionInput): Promise { + return this.run(`/actions/project/package?/install-packages`, input); + } + + searchPackages(input: SearchInput): Promise { + return this.run(`/actions/project/package?/search-packages`, input); } } diff --git a/src/lib/client/action/types/package.type.ts b/src/lib/client/action/types/package.type.ts index 645db8c..8754b87 100644 --- a/src/lib/client/action/types/package.type.ts +++ b/src/lib/client/action/types/package.type.ts @@ -1,23 +1,30 @@ -import type { AddComponentBody } from '$lib/server/actions/project/package/add-components.action'; -import type { AddSystemBody } from '$lib/server/actions/project/package/add-systems.action'; import type { CreateComponentBody } from '$lib/server/actions/project/package/create-component.action'; import type { CreateSystemBody } from '$lib/server/actions/project/package/create-system.action'; -import type { GetComponentManifestBody } from '$lib/server/actions/project/package/get-components-manifests.action'; -import type { GetSystemManifestBody } from '$lib/server/actions/project/package/get-systems-manifests.action'; -import type { ComponentPackage, SystemPackage } from '$lib/server/project/package/package.type'; +import type { InstallPackagesBody } from '$lib/server/actions/project/package/install-packages.action'; +import type { SearchPackagesBody } from '$lib/server/actions/project/package/search-packages.action'; +import type { PaginateResult, Package as ServerPackage } from '$lib/server/api'; +import type { + AssetPackage, + ComponentPackage, + SystemPackage, +} from '$lib/server/project/package/package.type'; -export type ComponentPackageResult = ComponentPackage; +export type ComponentPkg = ComponentPackage; -export type SystemPackageResult = SystemPackage; +export type SystemPkg = SystemPackage; -export type AddComponentsActionInput = AddComponentBody; +export type AssetPkg = AssetPackage; -export type AddSystemsActionInput = AddSystemBody; +export type Package = ComponentPackage | SystemPackage; -export type GetComponentsManifestsActionInput = GetComponentManifestBody; - -export type GetSystemsManifestsActionInput = GetSystemManifestBody; +export type InstallPackagesActionInput = InstallPackagesBody; export type CreateComponentActionInput = CreateComponentBody; export type CreateSystemActionInput = CreateSystemBody; + +export type SearchInput = SearchPackagesBody; + +export type ApiPackage = ServerPackage; + +export type SearchPackages = PaginateResult; diff --git a/src/lib/client/ecs/asset/asset-handle.ts b/src/lib/client/ecs/asset/asset-handle.ts new file mode 100644 index 0000000..c89aae3 --- /dev/null +++ b/src/lib/client/ecs/asset/asset-handle.ts @@ -0,0 +1,40 @@ +import { type Writable, get, writable } from 'svelte/store'; + +import { resolveStore } from '../utils'; +import type { AssetManager } from './asset-manager'; +import type { Asset } from './asset.type'; + +const _storage = writable>>({}); + +export class AssetHandle { + private _manager: AssetManager; + private readonly _store: Writable; + public readonly id: string; + + static reset() { + _storage.set({}); + } + + constructor(manager: AssetManager, asset: Asset) { + this._manager = manager; + this.id = asset.id; + + this._store = resolveStore(_storage, this.id, asset); + } + + get store() { + return this._store; + } + + get data() { + return get(this._store); + } + + update(asset: Partial) { + this._store.set({ ...get(this._store), ...asset }); + } + + delete() { + return this._manager.delete(this.id); + } +} diff --git a/src/lib/client/ecs/asset/asset-manager.ts b/src/lib/client/ecs/asset/asset-manager.ts new file mode 100644 index 0000000..64267f0 --- /dev/null +++ b/src/lib/client/ecs/asset/asset-manager.ts @@ -0,0 +1,113 @@ +import { type Unsubscriber, get, writable } from 'svelte/store'; + +import type { AssetPkg } from '$lib/client/action'; +import { useProject } from '$lib/client/project'; + +import { assetTransformer, assetsTransformer } from '../transformers'; +import { resetSubscriptions } from '../utils'; +import { AssetHandle } from './asset-handle'; +import type { Asset } from './asset.type'; + +const _storage = writable([]); + +const _subscriptions = writable>({}); + +export class AssetManager { + static reset() { + _storage.set([]); + resetSubscriptions(_subscriptions); + } + + constructor(assets: Asset[]) { + _storage.set(assets); + } + + get store() { + return _storage; + } + + get data() { + return get(_storage); + } + + async createMany(files: File[]): Promise { + const { fs } = useProject(); + + await Promise.all(files.map(this.create.bind(this))); + + await this.sync(); + const dir = await fs.getDirectory(); + await dir.readdir(true); + } + + async create(file: File): Promise { + const { fs } = useProject(); + const assetDir = await fs.getDirectory('assets'); + const assetFile = await assetDir.getFile(file.name); + const stream = await assetFile.createWritable(); + await file.stream().pipeTo(stream); + await assetFile.sync(); + } + + add(asset: AssetPkg) { + this._add(assetTransformer(asset)); + } + + async sync() { + const { actions } = useProject(); + const assets = await actions.package.getAssets(); + _storage.set(assetsTransformer(assets)); + } + + get(id: string): AssetHandle { + const asset = get(_storage).find((asset) => asset.id === id); + if (!asset) throw new Error(`Asset with id ${id} not found`); + const handle = new AssetHandle(this, asset); + + this._subscribe(id, handle); + + return handle; + } + + async delete(id: string) { + const assets = get(_storage); + const asset = assets.find((c) => c.id === id); + + if (!asset) throw new Error(`System not found: ${id}`); + + _storage.set(assets.filter((c) => c.id !== id)); + const { fs } = useProject(); + const file = await fs.getFile(asset.path); + await file.delete(); + + const subscriptions = get(_subscriptions); + if (subscriptions[id]) { + subscriptions[id](); + subscriptions[id] = null; + _subscriptions.set(subscriptions); + } + } + + private _add(asset: Asset) { + const assets = get(_storage); + assets.push(asset); + _storage.set(assets); + } + + private _subscribe(id: string, handle: AssetHandle) { + setTimeout(() => { + const subscriptions = get(_subscriptions); + if (subscriptions[id]) return; + subscriptions[id] = handle.store.subscribe((asset) => this._update(id, asset)); + _subscriptions.set(subscriptions); + }, 0); + } + + private _update(id: string, asset: Asset) { + const assets = get(_storage); + const index = assets.findIndex((s) => s.id === id); + if (index === -1) return; + assets[index] = asset; + _storage.set(assets); + } +} diff --git a/src/lib/client/ecs/asset/asset.type.ts b/src/lib/client/ecs/asset/asset.type.ts new file mode 100644 index 0000000..81777b2 --- /dev/null +++ b/src/lib/client/ecs/asset/asset.type.ts @@ -0,0 +1,4 @@ +export interface Asset { + id: string; + path: string; +} diff --git a/src/lib/client/ecs/component/component-manager.ts b/src/lib/client/ecs/component/component-manager.ts index f993853..aa7db66 100644 --- a/src/lib/client/ecs/component/component-manager.ts +++ b/src/lib/client/ecs/component/component-manager.ts @@ -1,5 +1,6 @@ import { type Unsubscriber, get, writable } from 'svelte/store'; +import type { ComponentPkg } from '$lib/client/action'; import { useProject } from '$lib/client/project'; import { componentTransformer, componentsTransformer } from '../transformers'; @@ -37,13 +38,8 @@ export class ComponentManager { await dir.readdir(true); } - async import(names: [string, ...string[]]) { - const { actions, ecs, fs } = useProject(); - await actions.package.addComponents({ componentNames: names }); - await this.sync(); - await ecs.components.sync(); - const dir = await fs.getDirectory(); - await dir.readdir(true); + add(component: ComponentPkg) { + this._add(componentTransformer(component)); } async sync() { diff --git a/src/lib/client/ecs/ecs-handler.ts b/src/lib/client/ecs/ecs-handler.ts index f5a4eab..d69604c 100644 --- a/src/lib/client/ecs/ecs-handler.ts +++ b/src/lib/client/ecs/ecs-handler.ts @@ -1,4 +1,5 @@ import { + assetsTransformer, componentsTransformer, librariesTransformer, scenesTransformer, @@ -6,6 +7,8 @@ import { } from '$lib/client/ecs/transformers'; import type { Project } from '$lib/client/project'; +import { AssetHandle } from './asset/asset-handle'; +import { AssetManager } from './asset/asset-manager'; import { ComponentHandle } from './component/component-handle'; import { ComponentManager } from './component/component-manager'; import { LibraryHandle } from './library/library-handle'; @@ -25,11 +28,14 @@ export class ECSHandler { private readonly _project: Project; private _sceneManager: SceneManager | undefined; + private _assetManager: AssetManager | undefined; private _componentManager: ComponentManager | undefined; private _systemManager: SystemManager | undefined; private _libraryManager: LibraryManager | undefined; static reset() { + AssetHandle.reset(); + AssetManager.reset(); ComponentHandle.reset(); ComponentManager.reset(); SystemManager.reset(); @@ -57,11 +63,13 @@ export class ECSHandler { * When scenes will be implemented, this init must be changed accordingly */ + const assets = await this._project.actions.package.getAssets(); const components = await this._project.actions.package.getComponents(); const systems = await this._project.actions.package.getSystems(); // Cannot use save handler as it needs to use ecs handler to subscribe to ecs changes const save = await this._project.actions.save.get(); + this._assetManager = new AssetManager(assetsTransformer(assets)); this._componentManager = new ComponentManager(componentsTransformer(components)); this._systemManager = new SystemManager(systemsTransformer(systems)); this._libraryManager = new LibraryManager(librariesTransformer(save)); @@ -74,6 +82,11 @@ export class ECSHandler { return this._sceneManager; } + get assets(): AssetManager { + if (!this._assetManager) throw new Error('ECS handler not initialized'); + return this._assetManager; + } + get components(): ComponentManager { if (!this._componentManager) throw new Error('ECS handler not initialized'); return this._componentManager; diff --git a/src/lib/client/ecs/index.ts b/src/lib/client/ecs/index.ts index b1e94f8..bfdfd86 100644 --- a/src/lib/client/ecs/index.ts +++ b/src/lib/client/ecs/index.ts @@ -1,5 +1,9 @@ export { ECSHandler } from './ecs-handler'; +export type { AssetHandle } from './asset/asset-handle'; +export type { AssetManager } from './asset/asset-manager'; +export type { Asset } from './asset/asset.type'; + export type { ComponentHandle } from './component/component-handle'; export type { ComponentManager } from './component/component-manager'; export type { Component, ComponentParam } from './component/component.type'; diff --git a/src/lib/client/ecs/scene/entity/component/component-handle.ts b/src/lib/client/ecs/scene/entity/component/component-handle.ts index 27be6f6..2bc47d4 100644 --- a/src/lib/client/ecs/scene/entity/component/component-handle.ts +++ b/src/lib/client/ecs/scene/entity/component/component-handle.ts @@ -1,6 +1,6 @@ import { type Readable, get } from 'svelte/store'; -import type { Component } from '../../../component/component.type'; +import type { Component } from '../../../asset/asset.type'; import type { EntityComponentManager } from './component-manager'; import { ComponentParamManager } from './component-param-manager'; diff --git a/src/lib/client/ecs/scene/entity/component/component-param-handle.ts b/src/lib/client/ecs/scene/entity/component/component-param-handle.ts index a9b1c6b..91cc1f7 100644 --- a/src/lib/client/ecs/scene/entity/component/component-param-handle.ts +++ b/src/lib/client/ecs/scene/entity/component/component-param-handle.ts @@ -1,6 +1,6 @@ import { type Readable, type Unsubscriber, type Writable, get, writable } from 'svelte/store'; -import type { ComponentParam } from '../../../component/component.type'; +import type { ComponentParam } from '../../../asset/asset.type'; import { resetListeners, resolveStore } from '../../../utils'; import type { ComponentParamManager } from './component-param-manager'; diff --git a/src/lib/client/ecs/scene/entity/component/component-param-manager.ts b/src/lib/client/ecs/scene/entity/component/component-param-manager.ts index 89e9fc8..b343846 100644 --- a/src/lib/client/ecs/scene/entity/component/component-param-manager.ts +++ b/src/lib/client/ecs/scene/entity/component/component-param-manager.ts @@ -1,6 +1,6 @@ import { type Unsubscriber, type Writable, get, writable } from 'svelte/store'; -import type { ComponentParam } from '../../../component/component.type'; +import type { ComponentParam } from '../../../asset/asset.type'; import { resetListeners, resetSubscriptions } from '../../../utils'; import { resolveStore } from '../../../utils'; import type { EntityComponentHandle } from './component-handle'; diff --git a/src/lib/client/ecs/system/system-manager.ts b/src/lib/client/ecs/system/system-manager.ts index c5819c3..4a81cb4 100644 --- a/src/lib/client/ecs/system/system-manager.ts +++ b/src/lib/client/ecs/system/system-manager.ts @@ -1,5 +1,6 @@ import { type Unsubscriber, get, writable } from 'svelte/store'; +import type { SystemPkg } from '$lib/client/action'; import { useProject } from '$lib/client/project'; import { systemTransformer, systemsTransformer } from '../transformers'; @@ -36,13 +37,8 @@ export class SystemManager { await dir.readdir(true); } - async import(names: [string, ...string[]]) { - const { actions, ecs, fs } = useProject(); - await actions.package.addSystems({ systemNames: names }); - await this.sync(); - await ecs.components.sync(); - const dir = await fs.getDirectory(); - await dir.readdir(true); + add(system: SystemPkg) { + this._add(systemTransformer(system)); } async sync() { diff --git a/src/lib/client/ecs/transformers.ts b/src/lib/client/ecs/transformers.ts index 6505e5e..08b00df 100644 --- a/src/lib/client/ecs/transformers.ts +++ b/src/lib/client/ecs/transformers.ts @@ -1,27 +1,34 @@ -import type { Component, Library, Scene, System } from '$lib/client/ecs'; -import type { ComponentPackage, SystemPackage } from '$lib/server/project/package'; +import type { AssetPkg, ComponentPkg, SystemPkg } from '$lib/client/action'; +import type { Asset, Component, Library, Scene, System } from '$lib/client/ecs'; import type { Save, SaveLibrary } from '@utils/types'; -export const componentTransformer = (component: ComponentPackage): Component => ({ +export const componentTransformer = (component: ComponentPkg): Component => ({ id: component.manifest.id, name: component.manifest.name, path: component.save.path, params: component.manifest.params, }); -export const componentsTransformer = (components: ComponentPackage[]): Component[] => +export const componentsTransformer = (components: ComponentPkg[]): Component[] => components.map(componentTransformer); -export const systemTransformer = (system: SystemPackage): System => ({ +export const systemTransformer = (system: SystemPkg): System => ({ id: system.manifest.id, name: system.manifest.name, path: system.save.path, }); -export const systemsTransformer = (systems: SystemPackage[]): System[] => +export const systemsTransformer = (systems: SystemPkg[]): System[] => systems.map(systemTransformer); +export const assetTransformer = (asset: AssetPkg): Asset => ({ + id: asset.path, + path: asset.path, +}); + +export const assetsTransformer = (assets: AssetPkg[]): Asset[] => assets.map(assetTransformer); + export const libraryTransformer = (lib: SaveLibrary): Library => ({ id: lib.path, name: lib.name, diff --git a/src/lib/client/project/package-handler.ts b/src/lib/client/project/package-handler.ts index 52ac77f..cf38dd4 100644 --- a/src/lib/client/project/package-handler.ts +++ b/src/lib/client/project/package-handler.ts @@ -1,73 +1,20 @@ import type { Project } from '$lib/client/project'; -import type { ComponentManifest, SystemManifest } from '$lib/server/project/package'; export class PackageHandler { private readonly _project: Project; - private _componentsManifests: Map = new Map(); - private _systemsManifests: Map = new Map(); - constructor(project: Project) { this._project = project; } - async init() { - // Not working but not used currently - // if (this._project.save.save.components.length > 0) { - // this._componentsManifests = new Map( - // ( - // await this._project.actions.package.getComponentsManifests({ - // componentPaths: this._project.save.save.components.map((c) => c.path) as [ - // string, - // ...string[], - // ], - // }) - // ).map((e, index) => [this._project.save.save.components[index].name, e]), - // ); - // } - // if (this._project.save.save.systems.length > 0) { - // this._systemsManifests = new Map( - // ( - // await this._project.actions.package.getSystemsManifests({ - // systemPaths: this._project.save.save.systems.map((s) => s.path) as [ - // string, - // ...string[], - // ], - // }) - // ).map((e, index) => [this._project.save.save.systems[index].name, e]), - // ); - // } - } - - getComponentManifest(componentName: string): ComponentManifest | undefined { - return this._componentsManifests.get(componentName); - } - - getSystemManifest(systemName: string): SystemManifest | undefined { - return this._systemsManifests.get(systemName); - } - - async installComponent(name: string): Promise { - const newComponent = ( - await this._project.actions.package.addComponents({ componentNames: [name] }) - )[0]; + async installPackages(names: [string, ...string[]]): Promise { + await this._project.actions.package.installPackages({ names }); - this._project.save.save.components.push(newComponent.save); - this._componentsManifests.set(newComponent.save.name, newComponent.manifest); - } - - async installSystem(name: string): Promise { - const newSystem = (await this._project.actions.package.addSystems({ systemNames: [name] }))[0]; - - this._project.save.save.systems.push(newSystem.save); - this._systemsManifests.set(newSystem.save.name, newSystem.manifest); - } - - addComponentManifest(componentName: string, component: ComponentManifest) { - this._componentsManifests.set(componentName, component); - } + // installPackages does not return dependencies, so it's required to sync to fetch all new components/systems + await this._project.ecs.components.sync(); + await this._project.ecs.systems.sync(); - addSystemManifest(systemName: string, system: SystemManifest) { - this._systemsManifests.set(systemName, system); + const dir = await this._project.fs.getDirectory(); + await dir.readdir(true); } } diff --git a/src/lib/client/project/project.ts b/src/lib/client/project/project.ts index 537a6bb..177f0cc 100644 --- a/src/lib/client/project/project.ts +++ b/src/lib/client/project/project.ts @@ -32,7 +32,6 @@ export class Project { await this.fs.init(); await this.ecs.init(); await this.save.init(); - await this.packages.init(); this._inited = true; return this; } diff --git a/src/lib/client/sync-file-system/sfs-file.ts b/src/lib/client/sync-file-system/sfs-file.ts index 86cf35b..ad9a0ea 100644 --- a/src/lib/client/sync-file-system/sfs-file.ts +++ b/src/lib/client/sync-file-system/sfs-file.ts @@ -59,6 +59,11 @@ export class SfsFile { await this.sync(); } + async createWritable(): Promise { + await this._preWrite(); + return await this._cache!.createWritable(); + } + async sync(): Promise { if (!this._cache) return; const file = await this._cache.getFile(); diff --git a/src/lib/client/utils/file-system/file-system-file.ts b/src/lib/client/utils/file-system/file-system-file.ts index 5806181..0971258 100644 --- a/src/lib/client/utils/file-system/file-system-file.ts +++ b/src/lib/client/utils/file-system/file-system-file.ts @@ -133,6 +133,10 @@ export class FileSystemFile { return this.write(raw); } + async createWritable(): Promise { + return this.handle.createWritable(); + } + async getUrl(): Promise { if (URL_CACHE.has(this.handle.name)) URL.revokeObjectURL(URL_CACHE.get(this.handle.name)!); const file = await this.handle.getFile(); diff --git a/src/lib/components/Widget/ecs-tree/assets/asset.items.ts b/src/lib/components/Widget/ecs-tree/assets/asset.items.ts new file mode 100644 index 0000000..e040292 --- /dev/null +++ b/src/lib/components/Widget/ecs-tree/assets/asset.items.ts @@ -0,0 +1,11 @@ +import type { PackageItems } from '../types'; + +export const ASSET_ITEMS: PackageItems = { + icon: { + name: 'i-ic-baseline-landscape', + color: 'text-purple-400', + }, + canAdd: true, + canDelete: true, + canImport: true, +}; diff --git a/src/lib/components/Widget/ecs-tree/assets/assets-tab.svelte b/src/lib/components/Widget/ecs-tree/assets/assets-tab.svelte new file mode 100644 index 0000000..c0ba548 --- /dev/null +++ b/src/lib/components/Widget/ecs-tree/assets/assets-tab.svelte @@ -0,0 +1,12 @@ + + + diff --git a/src/lib/components/Widget/ecs-tree/assets/dialog-create-asset.svelte b/src/lib/components/Widget/ecs-tree/assets/dialog-create-asset.svelte new file mode 100644 index 0000000..cff16ef --- /dev/null +++ b/src/lib/components/Widget/ecs-tree/assets/dialog-create-asset.svelte @@ -0,0 +1,147 @@ + + + 1 ? `Import ${files.length} files` : 'Import'} + confirmDisabled={files.length === 0} + bind:open + onConfirm={handleConfirm} +> +
+ + +
inputEl?.click()} + onkeydown={(e) => (e.key === 'Enter' || e.key === ' ') && inputEl?.click()} + > + +

+ Drag & drop files here, or + browse +

+ +
+ + {#if files.length > 0} + +
+ {#each files as file, i (file.name + file.size)} +
+ + {file.name} + {formatSize(file.size)} + +
+ {/each} +
+
+ {/if} +
+
diff --git a/src/lib/components/Widget/ecs-tree/components/component.items.ts b/src/lib/components/Widget/ecs-tree/components/component.items.ts index 8d91cea..2f9c1aa 100644 --- a/src/lib/components/Widget/ecs-tree/components/component.items.ts +++ b/src/lib/components/Widget/ecs-tree/components/component.items.ts @@ -8,4 +8,7 @@ export const COMPONENT_ITEMS: PackageItems = { addTooltip: 'Add to selected entity', disableAddTooltipNotSelected: 'No entity selected', disableAddTooltipAlreadyAdded: 'The selected entity already had this component', + canAdd: true, + canDelete: true, + canImport: true, }; diff --git a/src/lib/components/Widget/ecs-tree/components/components-tab.svelte b/src/lib/components/Widget/ecs-tree/components/components-tab.svelte index de72152..8e769a0 100644 --- a/src/lib/components/Widget/ecs-tree/components/components-tab.svelte +++ b/src/lib/components/Widget/ecs-tree/components/components-tab.svelte @@ -1,9 +1,9 @@ - - +{#if type === 'asset'} + +{:else} + +{/if}
@@ -129,13 +141,18 @@ {/snippet} - {#if type !== 'library'} + {#if type === 'asset'} + (createOpen = true)}> + + Upload + + {:else if type !== 'library'} (createOpen = true)}> Create {/if} - (importOpen = true)}> + marketplace.open()}> Import diff --git a/src/lib/components/Widget/ecs-tree/systems/system.items.ts b/src/lib/components/Widget/ecs-tree/systems/system.items.ts index 876bff3..4f681e1 100644 --- a/src/lib/components/Widget/ecs-tree/systems/system.items.ts +++ b/src/lib/components/Widget/ecs-tree/systems/system.items.ts @@ -8,4 +8,7 @@ export const SYSTEM_ITEMS: PackageItems = { addTooltip: 'Add to active scene', disableAddTooltipNotSelected: 'No active scene', disableAddTooltipAlreadyAdded: 'The active scene already had this system', + canAdd: true, + canDelete: true, + canImport: true, }; diff --git a/src/lib/components/Widget/ecs-tree/types.ts b/src/lib/components/Widget/ecs-tree/types.ts index 802fa61..78c1a98 100644 --- a/src/lib/components/Widget/ecs-tree/types.ts +++ b/src/lib/components/Widget/ecs-tree/types.ts @@ -3,9 +3,13 @@ export interface PackageItems { name: string; color: string; }; - addTooltip: string | null; - disableAddTooltipNotSelected: string | null; - disableAddTooltipAlreadyAdded: string | null; + addTooltip?: string; + disableAddTooltipNotSelected?: string; + disableAddTooltipAlreadyAdded?: string; + + canAdd?: boolean; + canDelete?: boolean; + canImport?: boolean; } export type TreeEntity = { kind: 'entity'; id: string; scene?: string }; @@ -25,6 +29,6 @@ export interface EntityDragContext { export interface Package { id: string; - name: string; + name?: string; path?: string; } diff --git a/src/lib/components/Widget/ecs-tree/widget.svelte b/src/lib/components/Widget/ecs-tree/widget.svelte index 6924a9b..cc0f67d 100644 --- a/src/lib/components/Widget/ecs-tree/widget.svelte +++ b/src/lib/components/Widget/ecs-tree/widget.svelte @@ -1,6 +1,7 @@ + + Scenes Components Systems + Assets Libraries @@ -45,4 +51,7 @@ + + + diff --git a/src/lib/components/marketplace/index.ts b/src/lib/components/marketplace/index.ts new file mode 100644 index 0000000..2ec52b8 --- /dev/null +++ b/src/lib/components/marketplace/index.ts @@ -0,0 +1,3 @@ +export { default as MarketplaceDialog } from './marketplace-dialog.svelte'; +export { setMarketplaceContext, getMarketplaceContext } from './marketplace.context'; +export type { MarketplaceContext } from './marketplace.context'; diff --git a/src/lib/components/marketplace/marketplace-dialog.svelte b/src/lib/components/marketplace/marketplace-dialog.svelte new file mode 100644 index 0000000..f3f5d58 --- /dev/null +++ b/src/lib/components/marketplace/marketplace-dialog.svelte @@ -0,0 +1,149 @@ + + + + + +
+ Marketplace + +
+
+ +
+ (selectedPackage = pkg)} + {onQueue} + {onDequeue} + /> +
+ +
+
+ + +
+
diff --git a/src/lib/components/marketplace/marketplace-footer.svelte b/src/lib/components/marketplace/marketplace-footer.svelte new file mode 100644 index 0000000..05c9e11 --- /dev/null +++ b/src/lib/components/marketplace/marketplace-footer.svelte @@ -0,0 +1,72 @@ + + +{#if activeTab === 'marketplace'} +
+
+ {#each input as name (name)} + + {name} + + + {/each} + {#if input.length === 0} + Select packages to install + {/if} +
+
+ + +
+
+{:else} +
+ +
+{/if} diff --git a/src/lib/components/marketplace/marketplace-package-details.svelte b/src/lib/components/marketplace/marketplace-package-details.svelte new file mode 100644 index 0000000..df1b9a7 --- /dev/null +++ b/src/lib/components/marketplace/marketplace-package-details.svelte @@ -0,0 +1,89 @@ + + +{#if pkg} + +
+
+
+ {#if isInstalled(pkg)} + + {:else} + + {/if} +
+
+

{pkg.name}

+

{typeLabel(pkg.type)}

+
+ {#if activeTab === 'marketplace' && !isInstalled(pkg)} + {#if isQueued(pkg)} + + {:else} + + {/if} + {/if} +
+ + {#if pkg.description} +
+

+ Description +

+

{pkg.description}

+
+ {/if} + + {#if pkg.tags.length > 0} +
+

Tags

+
+ {#each pkg.tags as tag (tag)} + {tag} + {/each} +
+
+ {/if} + +
+

Type

+ {typeLabel(pkg.type)} +
+
+
+{:else} +
+ +

Select a package to view details

+
+{/if} diff --git a/src/lib/components/marketplace/marketplace-package-item.svelte b/src/lib/components/marketplace/marketplace-package-item.svelte new file mode 100644 index 0000000..4f379c6 --- /dev/null +++ b/src/lib/components/marketplace/marketplace-package-item.svelte @@ -0,0 +1,90 @@ + + +
e.key === 'Enter' && onSelect()} +> +
+ {#if installed} + + {:else} + + {/if} +
+
+

{pkg.name}

+

{typeLabel(pkg.type)}

+
+ + {#if installed} + + Installed + + {:else if onQueue || onDequeue} + {#if queued} + + {:else} + + {/if} + {/if} +
diff --git a/src/lib/components/marketplace/marketplace-package-list.svelte b/src/lib/components/marketplace/marketplace-package-list.svelte new file mode 100644 index 0000000..b5a2d40 --- /dev/null +++ b/src/lib/components/marketplace/marketplace-package-list.svelte @@ -0,0 +1,58 @@ + + + + {#if isLoading} +
+ +
+ {:else if packages.length === 0} +

{emptyMessage}

+ {:else} +
+ {#each packages as pkg (pkg.id)} + onSelect(pkg)} + onQueue={onQueue ? () => onQueue(pkg) : undefined} + onDequeue={onDequeue ? () => onDequeue(pkg.name) : undefined} + /> + {/each} +
+ {/if} +
diff --git a/src/lib/components/marketplace/marketplace-package-tabs.svelte b/src/lib/components/marketplace/marketplace-package-tabs.svelte new file mode 100644 index 0000000..9a43bef --- /dev/null +++ b/src/lib/components/marketplace/marketplace-package-tabs.svelte @@ -0,0 +1,94 @@ + + +
+ +
+ + Marketplace + Installed + +
+ +
+ { + refetch(); + onSelect(null); + }} + onClear={() => { + search = ''; + refetch(); + }} + /> +
+ + + + + + + + +
+
diff --git a/src/lib/components/marketplace/marketplace-search.svelte b/src/lib/components/marketplace/marketplace-search.svelte new file mode 100644 index 0000000..4b6df71 --- /dev/null +++ b/src/lib/components/marketplace/marketplace-search.svelte @@ -0,0 +1,40 @@ + + +
+ + + + {#if isFetching} + + {:else} + + {/if} + + {#if search} + + + + {/if} + +
diff --git a/src/lib/components/marketplace/marketplace.context.ts b/src/lib/components/marketplace/marketplace.context.ts new file mode 100644 index 0000000..77174f7 --- /dev/null +++ b/src/lib/components/marketplace/marketplace.context.ts @@ -0,0 +1,15 @@ +import { getContext, setContext } from 'svelte'; + +const MARKETPLACE_CONTEXT_KEY = 'marketplace'; + +export interface MarketplaceContext { + open: () => void; +} + +export function setMarketplaceContext(ctx: MarketplaceContext): void { + setContext(MARKETPLACE_CONTEXT_KEY, ctx); +} + +export function getMarketplaceContext(): MarketplaceContext { + return getContext(MARKETPLACE_CONTEXT_KEY); +} diff --git a/src/lib/server/actions/project/package/add-components.action.ts b/src/lib/server/actions/project/package/add-components.action.ts deleted file mode 100644 index 5ed7f75..0000000 --- a/src/lib/server/actions/project/package/add-components.action.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Expose } from 'class-transformer'; -import { IsNotEmpty, IsString } from 'class-validator'; - -import type { ComponentPackage } from '$lib/server/project/package/package.type'; - -import { useActionHandler } from '@utils-server/request-handler'; - -export class AddComponentBody { - @Expose() - @IsString({ each: true }) - @IsNotEmpty() - componentNames!: [string, ...string[]]; -} - -export const addComponentsProjectAction = useActionHandler( - async ({ body, project }): Promise => { - return await Promise.all( - body.componentNames.map((componentName) => - project.client.package.installComponent(componentName), - ), - ); - }, - { - body: AddComponentBody, - }, -); diff --git a/src/lib/server/actions/project/package/add-systems.action.ts b/src/lib/server/actions/project/package/add-systems.action.ts deleted file mode 100644 index 01d4c13..0000000 --- a/src/lib/server/actions/project/package/add-systems.action.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Expose } from 'class-transformer'; -import { IsNotEmpty, IsString } from 'class-validator'; - -import type { SystemPackage } from '$lib/server/project/package/package.type'; - -import { useActionHandler } from '@utils-server/request-handler'; - -export class AddSystemBody { - @Expose() - @IsString({ each: true }) - @IsNotEmpty() - systemNames!: [string, ...string[]]; -} - -export const addSystemsProjectAction = useActionHandler( - async ({ body, project }): Promise => { - return await Promise.all( - body.systemNames.map((systemName) => project.client.package.installSystem(systemName)), - ); - }, - { - body: AddSystemBody, - }, -); diff --git a/src/lib/server/actions/project/package/get-assets.action.ts b/src/lib/server/actions/project/package/get-assets.action.ts new file mode 100644 index 0000000..39aab7b --- /dev/null +++ b/src/lib/server/actions/project/package/get-assets.action.ts @@ -0,0 +1,7 @@ +import type { AssetPackage } from '$lib/server/project/package'; + +import { useActionHandler } from '@utils-server/request-handler'; + +export const getAssetsAction = useActionHandler(async ({ project }): Promise => { + return await project.client.package.getAssets(); +}); diff --git a/src/lib/server/actions/project/package/get-components-manifests.action.ts b/src/lib/server/actions/project/package/get-components-manifests.action.ts deleted file mode 100644 index 1806128..0000000 --- a/src/lib/server/actions/project/package/get-components-manifests.action.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Expose } from 'class-transformer'; -import { IsNotEmpty, IsString } from 'class-validator'; - -import type { ComponentManifest } from '$lib/server/project/package'; - -import { useActionHandler } from '@utils-server/request-handler'; - -export class GetComponentManifestBody { - @Expose() - @IsString({ each: true }) - @IsNotEmpty() - componentPaths!: [string, ...string[]]; -} - -export const getComponentsManifestsAction = useActionHandler( - async ({ body, project }): Promise => { - return await Promise.all( - body.componentPaths.map((path) => project.client.package.getComponentManifest(path)), - ); - }, - { - body: GetComponentManifestBody, - }, -); diff --git a/src/lib/server/actions/project/package/get-systems-manifests.action.ts b/src/lib/server/actions/project/package/get-systems-manifests.action.ts deleted file mode 100644 index 07c93e9..0000000 --- a/src/lib/server/actions/project/package/get-systems-manifests.action.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Expose } from 'class-transformer'; -import { IsNotEmpty, IsString } from 'class-validator'; - -import type { SystemManifest } from '$lib/server/project/package'; - -import { useActionHandler } from '@utils-server/request-handler'; - -export class GetSystemManifestBody { - @Expose() - @IsString({ each: true }) - @IsNotEmpty() - systemPaths!: [string, ...string[]]; -} - -export const getSystemsManifestsAction = useActionHandler( - async ({ body, project }): Promise => { - return await Promise.all( - body.systemPaths.map((path) => project.client.package.getSystemManifest(path)), - ); - }, - { - body: GetSystemManifestBody, - }, -); diff --git a/src/lib/server/actions/project/package/install-packages.action.ts b/src/lib/server/actions/project/package/install-packages.action.ts new file mode 100644 index 0000000..d699f77 --- /dev/null +++ b/src/lib/server/actions/project/package/install-packages.action.ts @@ -0,0 +1,22 @@ +import { Expose } from 'class-transformer'; +import { IsNotEmpty, IsString } from 'class-validator'; + +import type { ComponentPackage, SystemPackage } from '$lib/server/project/package/package.type'; + +import { useActionHandler } from '@utils-server/request-handler'; + +export class InstallPackagesBody { + @Expose() + @IsString({ each: true }) + @IsNotEmpty() + names!: [string, ...string[]]; +} + +export const installPackagesAction = useActionHandler( + async ({ body, project }): Promise<(ComponentPackage | SystemPackage)[]> => { + return await project.client.package.installPackages(body.names); + }, + { + body: InstallPackagesBody, + }, +); diff --git a/src/lib/server/actions/project/package/search-packages.action.ts b/src/lib/server/actions/project/package/search-packages.action.ts new file mode 100644 index 0000000..9a6f6a6 --- /dev/null +++ b/src/lib/server/actions/project/package/search-packages.action.ts @@ -0,0 +1,32 @@ +import { Expose } from 'class-transformer'; +import { IsNumber, IsOptional, IsString } from 'class-validator'; + +import type { Package, PaginateResult } from '$lib/server/api'; + +import { useActionHandler } from '@utils-server/request-handler'; + +export class SearchPackagesBody { + @Expose() + @IsString() + @IsOptional() + search?: string; + + @Expose() + @IsNumber() + @IsOptional() + page?: number; + + @Expose() + @IsNumber() + @IsOptional() + limit?: number; +} + +export const searchPackagesAction = useActionHandler( + async ({ body, api }): Promise> => { + return api.packages.getAll(body); + }, + { + body: SearchPackagesBody, + }, +); diff --git a/src/lib/server/api/client.ts b/src/lib/server/api/client.ts index 21bcb10..8ea5654 100644 --- a/src/lib/server/api/client.ts +++ b/src/lib/server/api/client.ts @@ -7,11 +7,13 @@ import { HttpClient } from '@utils/http'; import { useTokenMiddleware } from './middlewares/refresh-token.middleware'; import { AuthRepository } from './repositories/auth.repository'; +import { PackageRepository } from './repositories/package.repository'; import { ProjectRepository } from './repositories/projects.repository'; import { RegistryRepository } from './repositories/registry.repository'; export interface Api { auth: AuthRepository; + packages: PackageRepository; projects: ProjectRepository; registry: RegistryRepository; } @@ -28,6 +30,7 @@ export const getNoAuthApi = (): Api => { return { auth: new AuthRepository(client, isOnline), + packages: new PackageRepository(client, isOnline), registry: new RegistryRepository(client, isOnline), } as Api; }; @@ -43,6 +46,7 @@ export const getApi = (cookies: Cookies): Api => { }).useMiddlewares(useTokenMiddleware(cookies)); return { auth: new AuthRepository(client), + packages: new PackageRepository(client), projects: new ProjectRepository(client), registry: new RegistryRepository(client), }; diff --git a/src/lib/server/api/repositories/package.repository.ts b/src/lib/server/api/repositories/package.repository.ts new file mode 100644 index 0000000..14d8c56 --- /dev/null +++ b/src/lib/server/api/repositories/package.repository.ts @@ -0,0 +1,10 @@ +import { getUrl } from '@utils/http'; + +import { BaseRepository } from '../base.repository'; +import type { Package, PaginateQuery, PaginateResult, SearchQuery } from '../types'; + +export class PackageRepository extends BaseRepository { + getAll(query?: PaginateQuery & SearchQuery): Promise> { + return this.get(getUrl(`/packages`, query), { offline: true }); + } +} diff --git a/src/lib/server/api/types/index.ts b/src/lib/server/api/types/index.ts index 24d88b4..83f4894 100644 --- a/src/lib/server/api/types/index.ts +++ b/src/lib/server/api/types/index.ts @@ -1,3 +1,5 @@ export * from './auth.type'; +export * from './package.type'; +export * from './paginate.type'; export * from './project.type'; export * from './registry.type'; diff --git a/src/lib/server/api/types/package.type.ts b/src/lib/server/api/types/package.type.ts new file mode 100644 index 0000000..5600cc7 --- /dev/null +++ b/src/lib/server/api/types/package.type.ts @@ -0,0 +1,9 @@ +import type { PackageTypeEnum } from '$lib/server/project/package'; + +export interface Package { + id: string; + name: string; + type: PackageTypeEnum; + description: string | null; + tags: string[]; +} diff --git a/src/lib/server/api/types/paginate.type.ts b/src/lib/server/api/types/paginate.type.ts new file mode 100644 index 0000000..e8bcd6f --- /dev/null +++ b/src/lib/server/api/types/paginate.type.ts @@ -0,0 +1,16 @@ +export interface PaginateQuery { + page?: number; + limit?: number; +} + +export interface SearchQuery { + search?: string; +} + +export type PaginateResult = { + data: T[]; + total: number; + page: number; + limit: number; + totalPages: number; +}; diff --git a/src/lib/server/api/types/registry.type.ts b/src/lib/server/api/types/registry.type.ts index b90b67f..269cc85 100644 --- a/src/lib/server/api/types/registry.type.ts +++ b/src/lib/server/api/types/registry.type.ts @@ -1,6 +1,8 @@ +import type { PackageTypeEnum } from '$lib/server/project/package'; + export interface RegistryPackageType { name: string; - type: string; + type: PackageTypeEnum; description: string; tags: string[]; _file: string; diff --git a/src/lib/server/file-system/project-directory.ts b/src/lib/server/file-system/project-directory.ts index 3821cd9..5fc1598 100644 --- a/src/lib/server/file-system/project-directory.ts +++ b/src/lib/server/file-system/project-directory.ts @@ -35,17 +35,20 @@ export class ProjectDirectory { this._checkPathIsInsideProject(); } - read(recursive: boolean = false): DirectoryContent { + read(recursive: boolean = false, createIfNotExist: boolean = false): DirectoryContent { + if (createIfNotExist) { + if (!fs.existsSync(this.path)) this.create(); + } this._checkPathExists(); this._checkPathIsDir(); this._checkPathIsReadable(); return this._readDirContent(this.path, recursive); } - create(): void { + create(path: string = this.path): void { this._checkPathNotExists(); - fs.mkdirSync(this.path, { recursive: true }); + fs.mkdirSync(path, { recursive: true }); } delete(recursive: boolean = false): void { diff --git a/src/lib/server/project/package/index.ts b/src/lib/server/project/package/index.ts index defaadf..df8afa1 100644 --- a/src/lib/server/project/package/index.ts +++ b/src/lib/server/project/package/index.ts @@ -1 +1,2 @@ +export * from './package.enum'; export * from './package.type'; diff --git a/src/lib/server/project/package/manifest/manifest-handler.ts b/src/lib/server/project/package/manifest/manifest-handler.ts new file mode 100644 index 0000000..57ef9e1 --- /dev/null +++ b/src/lib/server/project/package/manifest/manifest-handler.ts @@ -0,0 +1,94 @@ +import { join } from 'path'; + +import { FileSystemError } from '$lib/server/file-system/file-system-error'; +import type { ProjectHandler } from '$lib/server/project'; + +import { PACKAGES_PATH, PACKAGES_PATH_FORMATTER } from '../package.const'; +import { PackageTypeEnum } from '../package.enum'; +import type { ManifestPackage, PackageType } from '../package.type'; +import { resolveManifest } from './manifest-resolver'; + +const resolveSave = ( + type: T, + name: string, + path: string, + manifest: PackageType['manifest'], +): PackageType['save'] => { + const base = { name, path }; + if (type === PackageTypeEnum.SYSTEM) return base; + return { + ...base, + paramsNames: (manifest as PackageType['manifest']).params.map( + ({ name }) => name, + ), + }; +}; + +export class ManifestHandler { + private readonly handler: ProjectHandler; + + constructor(handler: ProjectHandler) { + this.handler = handler; + } + + resolveFromName(type: T, rawName: string): PackageType { + return this.resolveFromFilename(type, PACKAGES_PATH_FORMATTER[type](rawName)); + } + + resolveFromFilename(type: T, filename: string): PackageType { + const path = join(PACKAGES_PATH[type], filename); + + return this.resolveFromPath(type, path); + } + + resolveFromPath(type: T, path: string): PackageType { + const manifest = this._resolveManifest(type, path); + + return { + manifest, + save: resolveSave(type, manifest.id, path, manifest), + } as PackageType; + } + + private _resolveManifest( + type: T, + path: string, + ): PackageType['manifest'] { + return this._findPackageManifest((p) => this._getPackageManifest(type, p), path); + } + + private _getPackageManifest( + type: T, + path: string, + ): PackageType['manifest'] { + const content = this.handler._rootFs.getFile(this._resolvePartPath(path)).read(); + return resolveManifest(type, content); + } + + private _findPackageManifest( + manifestGetter: (path: string) => PackageType['manifest'], + path: string, + ): PackageType['manifest'] { + const manifest = ['', '.ts', '.js'].reduce( + (result: PackageType['manifest'] | undefined, p) => { + if (result) return result; + + try { + return manifestGetter(path + p); + } catch (e) { + if (!(e instanceof FileSystemError)) throw e; + } + }, + undefined, + ); + + if (!manifest) { + throw new FileSystemError("Can't find package manifest"); + } + return manifest; + } + + private _resolvePartPath(path: string): string { + return join(this.handler._part, path); + } +} diff --git a/src/lib/server/project/package/manifest-resolver.ts b/src/lib/server/project/package/manifest/manifest-resolver.ts similarity index 85% rename from src/lib/server/project/package/manifest-resolver.ts rename to src/lib/server/project/package/manifest/manifest-resolver.ts index 40d7353..52a150f 100644 --- a/src/lib/server/project/package/manifest-resolver.ts +++ b/src/lib/server/project/package/manifest/manifest-resolver.ts @@ -1,6 +1,7 @@ import ts, { type Expression, type ObjectLiteralElementLike } from 'typescript'; -import { PackageTypeEnum } from './package.type'; +import { PackageTypeEnum } from '../package.enum'; +import type { ManifestPackage } from '../package.type'; export const MANIFEST_TITLES = { [PackageTypeEnum.COMPONENT]: 'EDITOR_COMPONENT_MANIFEST', @@ -71,14 +72,14 @@ const getManifestFromNode = (source: ts.VariableDeclaration | null): any | null return parseElement(init); }; -const parseManifest = (title: string, source: ts.SourceFile): any | null => { +const parseManifest = (type: ManifestPackage, source: ts.SourceFile): any | null => { const id = getName(source); - const manifest = getManifestFromNode(findManifestNode(title, source)); + const manifest = getManifestFromNode(findManifestNode(MANIFEST_TITLES[type], source)); if (!id || !manifest) return null; - return { id, ...manifest }; + return { id, type, ...manifest }; }; -export const resolveManifest = (type: PackageTypeEnum, content: string): any | null => { +export const resolveManifest = (type: ManifestPackage, content: string): any | null => { const source = ts.createSourceFile('tmp.ts', content, ts.ScriptTarget.ESNext, true); - return parseManifest(MANIFEST_TITLES[type], source); + return parseManifest(type, source); }; diff --git a/src/lib/server/project/package/manifest/manifest.type.ts b/src/lib/server/project/package/manifest/manifest.type.ts new file mode 100644 index 0000000..9df064b --- /dev/null +++ b/src/lib/server/project/package/manifest/manifest.type.ts @@ -0,0 +1,9 @@ +import type { EditorComponentManifest, EditorSystemManifest } from '@nanoforge-dev/ecs-lib'; + +import { type PackageTypeEnum } from '../package.enum'; + +export type ComponentManifest = EditorComponentManifest & { + id: string; + type: PackageTypeEnum.COMPONENT; +}; +export type SystemManifest = EditorSystemManifest & { id: string; type: PackageTypeEnum.SYSTEM }; diff --git a/src/lib/server/project/package/package-handler.ts b/src/lib/server/project/package/package-handler.ts index 9361f20..c619661 100644 --- a/src/lib/server/project/package/package-handler.ts +++ b/src/lib/server/project/package/package-handler.ts @@ -1,93 +1,48 @@ import { join } from 'path'; -import { FileSystemError } from '$lib/server/file-system/file-system-error'; import type { DirectoryContent } from '$lib/server/file-system/project-directory'; import type { ProjectHandler } from '$lib/server/project'; -import { formatFrom } from '@utils/format'; - -import { resolveManifest } from './manifest-resolver'; -import { - type ComponentManifest, - type ComponentPackage, - PackageTypeEnum, - type SystemManifest, - type SystemPackage, +import { ManifestHandler } from './manifest/manifest-handler'; +import { PACKAGES_PATH } from './package.const'; +import { PackageTypeEnum } from './package.enum'; +import type { + AssetPackage, + ComponentPackage, + CreatablePackage, + PackageType, + SystemPackage, } from './package.type'; export class PackageHandler { private readonly handler: ProjectHandler; + private readonly manifestHandler: ManifestHandler; constructor(handler: ProjectHandler) { this.handler = handler; + this.manifestHandler = new ManifestHandler(handler); } - async installComponent(name: string): Promise { - const rc = await this.handler._api.registry.getPackage(name); - if (rc.type !== 'component') throw new Error(`Can only add component: ${name} is a ${rc.type}`); - this.handler._cli.install([name], { server: this.handler._part === 'server' || undefined }); - - return this._getNewComponentPackage(rc.name, rc._file); - } - - async installSystem(name: string): Promise { - const rs = await this.handler._api.registry.getPackage(name); - if (rs.type !== 'system') throw new Error(`Can only add system: ${name} is a ${rs.type}`); - this.handler._cli.install([name], { server: this.handler._part === 'server' || undefined }); + async installPackages(names: [string, ...string[]]): Promise { + if (names.length === 0) return []; + this.handler._cli.install(names, { server: this.handler._part === 'server' || undefined }); - return this._getNewSystemPackage(rs.name, rs._file); + return Promise.all( + names.map(async (name) => { + const r = await this.handler._api.registry.getPackage(name); + return this._resolvesPackage(r.type, r._file); + }), + ); } - /** - * Create a new component in the project - * @beta function to be reworked - * - * @param {string} name - Name of the component - */ createComponent(name: string): ComponentPackage { this._createPackage(PackageTypeEnum.COMPONENT, name); - return this._getNewComponentPackage( - formatFrom.all(name).toPascal() + 'Component', - formatFrom.all(name).toKebab() + '.component', - ); + return this.manifestHandler.resolveFromName(PackageTypeEnum.COMPONENT, name); } - /** - * Create a new system in the project and update the save file - * @beta function to be reworked - * - * @param {string} name - Name of the system - */ createSystem(name: string): SystemPackage { this._createPackage(PackageTypeEnum.SYSTEM, name); - return this._getNewSystemPackage( - formatFrom.all(name).toCamel() + 'System', - formatFrom.all(name).toKebab() + '.system', - ); - } - - /** - * Get the manifest of the component - * @beta function to be reworked - * - * @param {string} path - Path from `/` - * - * @returns Manifest of the component - */ - getComponentManifest(path: string): ComponentManifest { - return this._getPackageManifest(PackageTypeEnum.COMPONENT, path); - } - - /** - * Get the manifest of the system - * @beta function to be reworked - * - * @param {string} path - Path from `/` - * - * @returns Manifest of the system - */ - getSystemManifest(path: string): SystemManifest { - return this._getPackageManifest(PackageTypeEnum.SYSTEM, path); + return this.manifestHandler.resolveFromName(PackageTypeEnum.SYSTEM, name); } async getComponents(): Promise { @@ -98,32 +53,27 @@ export class PackageHandler { return this._resolvesPackages(PackageTypeEnum.SYSTEM); } - private _resolvesPackages( - type: T, - ): (T extends PackageTypeEnum.COMPONENT ? ComponentPackage : SystemPackage)[] { - const basePath = type === PackageTypeEnum.COMPONENT ? './components' : './systems'; + async getAssets(): Promise { + return this._resolvesPackages(PackageTypeEnum.ASSET); + } + + private _resolvesPackages(type: T): PackageType[] { + const basePath = PACKAGES_PATH[type]; const dir = this.handler.fs.getDirectory(basePath); - const content = dir.read(true); + const content = dir.read(true, true); const paths = this._resolvesPackageFilesPathFromContent(content); - return paths.map((path) => this._resolvesPackage(type, join(basePath, path))); + return paths.map((path) => this._resolvesPackage(type, path, basePath)); } private _resolvesPackage( type: T, - path: string, - ): T extends PackageTypeEnum.COMPONENT ? ComponentPackage : SystemPackage { - const manifest = this._getPackageManifest(type, path); - const res: any = { - manifest, - save: { - name: manifest.id, - path, - }, - }; - if (type === PackageTypeEnum.COMPONENT) { - res.save.params = manifest.params.map(({ name }: { name: string }) => name); - } - return res; + filename: string, + basePath: string = PACKAGES_PATH[type], + ): PackageType { + const path = join(basePath, filename); + + if (type === PackageTypeEnum.ASSET) return { path } as PackageType; + return this.manifestHandler.resolveFromPath(type, path) as PackageType; } private _resolvesPackageFilesPathFromContent( @@ -139,59 +89,10 @@ export class PackageHandler { return [...files, ...directories.flat()]; } - private _getNewComponentPackage(name: string, fileName: string): ComponentPackage { - const path = `./components/${fileName}`; - - const manifest = this._findPackageManifest((p) => this.getComponentManifest(p), path); - - return { - manifest, - save: { - name, - path, - paramsNames: manifest.params.map(({ name }) => name), - }, - }; - } - - private _getNewSystemPackage(name: string, fileName: string): SystemPackage { - const path = `./systems/${fileName}`; - return { - manifest: this._findPackageManifest((p) => this.getSystemManifest(p), path), - save: { name, path }, - }; - } - - private _findPackageManifest(manifestGetter: (path: string) => T, path: string): T { - const manifest = ['', '.ts', '.js'].reduce((result: T | undefined, p) => { - if (result) return result; - - try { - return manifestGetter(path + p); - } catch (e) { - if (!(e instanceof FileSystemError)) throw e; - } - }, undefined); - - if (!manifest) { - throw new FileSystemError("Can't find package manifest"); - } - return manifest; - } - - private _createPackage(type: PackageTypeEnum, name: string): void { + private _createPackage(type: CreatablePackage, name: string): void { this.handler._cli.create(type, { name, server: this.handler._part === 'server' || undefined, }); } - - private _getPackageManifest(type: PackageTypeEnum, path: string): any { - const content = this.handler._rootFs.getFile(this._resolvePartPath(path)).read(); - return resolveManifest(type, content); - } - - private _resolvePartPath(path: string): string { - return join(this.handler._part, path); - } } diff --git a/src/lib/server/project/package/package-resolver.ts b/src/lib/server/project/package/package-resolver.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/lib/server/project/package/package.const.ts b/src/lib/server/project/package/package.const.ts new file mode 100644 index 0000000..489b279 --- /dev/null +++ b/src/lib/server/project/package/package.const.ts @@ -0,0 +1,21 @@ +import { formatFrom } from '@utils/format'; + +import { PackageTypeEnum } from './package.enum'; + +export const PACKAGES_PATH: Record = { + [PackageTypeEnum.COMPONENT]: 'components', + [PackageTypeEnum.SYSTEM]: 'systems', + [PackageTypeEnum.ASSET]: 'assets', +}; + +export const PACKAGES_NAME_FORMATTER: Record string> = { + [PackageTypeEnum.COMPONENT]: (n) => formatFrom.all(n).toPascal() + 'Component', + [PackageTypeEnum.SYSTEM]: (n) => formatFrom.all(n).toCamel() + 'System', + [PackageTypeEnum.ASSET]: (n) => n, +}; + +export const PACKAGES_PATH_FORMATTER: Record string> = { + [PackageTypeEnum.COMPONENT]: (n) => formatFrom.all(n).toKebab() + '.component', + [PackageTypeEnum.SYSTEM]: (n) => formatFrom.all(n).toKebab() + '.system', + [PackageTypeEnum.ASSET]: (n) => n, +}; diff --git a/src/lib/server/project/package/package.enum.ts b/src/lib/server/project/package/package.enum.ts new file mode 100644 index 0000000..393991f --- /dev/null +++ b/src/lib/server/project/package/package.enum.ts @@ -0,0 +1,5 @@ +export enum PackageTypeEnum { + COMPONENT = 'component', + SYSTEM = 'system', + ASSET = 'asset', +} diff --git a/src/lib/server/project/package/package.type.ts b/src/lib/server/project/package/package.type.ts index 3fdfcb3..9675fb3 100644 --- a/src/lib/server/project/package/package.type.ts +++ b/src/lib/server/project/package/package.type.ts @@ -1,14 +1,7 @@ -import type { EditorComponentManifest, EditorSystemManifest } from '@nanoforge-dev/ecs-lib'; - import type { SaveComponent, SaveSystem } from '@utils/types'; -export enum PackageTypeEnum { - COMPONENT = 'component', - SYSTEM = 'system', -} - -export type ComponentManifest = EditorComponentManifest & { id: string }; -export type SystemManifest = EditorSystemManifest & { id: string }; +import type { ComponentManifest, SystemManifest } from './manifest/manifest.type'; +import type { PackageTypeEnum } from './package.enum'; export interface ComponentPackage { manifest: ComponentManifest; @@ -19,3 +12,20 @@ export interface SystemPackage { manifest: SystemManifest; save: SaveSystem; } + +export interface AssetPackage { + path: string; +} + +export interface Packages { + [PackageTypeEnum.COMPONENT]: ComponentPackage; + [PackageTypeEnum.SYSTEM]: SystemPackage; + [PackageTypeEnum.ASSET]: AssetPackage; +} + +export type PackageType = T extends PackageTypeEnum + ? Packages[T] + : ComponentPackage | SystemPackage | AssetPackage; + +export type ManifestPackage = PackageTypeEnum.COMPONENT | PackageTypeEnum.SYSTEM; +export type CreatablePackage = PackageTypeEnum.COMPONENT | PackageTypeEnum.SYSTEM; diff --git a/src/lib/utils/http/index.ts b/src/lib/utils/http/index.ts index 2d6ee12..3fbc758 100644 --- a/src/lib/utils/http/index.ts +++ b/src/lib/utils/http/index.ts @@ -4,3 +4,4 @@ export { type MiddlewareParams, type RequestOptions, } from './client'; +export { getUrl } from './query'; diff --git a/src/lib/utils/http/query.ts b/src/lib/utils/http/query.ts new file mode 100644 index 0000000..43bc500 --- /dev/null +++ b/src/lib/utils/http/query.ts @@ -0,0 +1,5 @@ +export const getUrl = (url: string, query?: Record) => { + const entries = Object.entries(query ?? {}).filter(([, value]) => value !== undefined); + if (entries.length === 0) return url; + return `${url}?${new URLSearchParams(entries).toString()}`; +}; diff --git a/src/routes/actions/project/package/+page.server.ts b/src/routes/actions/project/package/+page.server.ts index 2650b05..078dcae 100644 --- a/src/routes/actions/project/package/+page.server.ts +++ b/src/routes/actions/project/package/+page.server.ts @@ -1,19 +1,17 @@ -import { addComponentsProjectAction } from '$lib/server/actions/project/package/add-components.action'; -import { addSystemsProjectAction } from '$lib/server/actions/project/package/add-systems.action'; import { createComponentProjectAction } from '$lib/server/actions/project/package/create-component.action'; import { createSystemProjectAction } from '$lib/server/actions/project/package/create-system.action'; -import { getComponentsManifestsAction } from '$lib/server/actions/project/package/get-components-manifests.action'; +import { getAssetsAction } from '$lib/server/actions/project/package/get-assets.action'; import { getComponentsAction } from '$lib/server/actions/project/package/get-components.action'; -import { getSystemsManifestsAction } from '$lib/server/actions/project/package/get-systems-manifests.action'; import { getSystemsAction } from '$lib/server/actions/project/package/get-systems.action'; +import { installPackagesAction } from '$lib/server/actions/project/package/install-packages.action'; +import { searchPackagesAction } from '$lib/server/actions/project/package/search-packages.action'; export const actions = { - 'add-components': addComponentsProjectAction, - 'add-systems': addSystemsProjectAction, 'create-component': createComponentProjectAction, 'create-system': createSystemProjectAction, - 'get-components-manifests': getComponentsManifestsAction, - 'get-systems-manifests': getSystemsManifestsAction, 'get-components': getComponentsAction, 'get-systems': getSystemsAction, + 'get-assets': getAssetsAction, + 'install-packages': installPackagesAction, + 'search-packages': searchPackagesAction, }; diff --git a/src/routes/dashboard/+page.svelte b/src/routes/dashboard/+page.svelte index 654732a..d337290 100644 --- a/src/routes/dashboard/+page.svelte +++ b/src/routes/dashboard/+page.svelte @@ -15,6 +15,10 @@ import { page } from '$app/state'; import { runSafe } from '@utils-client/error'; import { FullPageProjectSpinner } from '$lib/components/project-loader'; + import { MarketplaceDialog, setMarketplaceContext } from '$lib/components/marketplace'; + + let marketplaceOpen = $state(false); + setMarketplaceContext({ open: () => (marketplaceOpen = true) }); let tab = $derived($tabsStore.tabs.find((t) => t.id === $tabsStore.selectedTabId)); let Component = $derived(tab ? tabRegistry[tab.type]?.component : null); @@ -71,6 +75,7 @@ {#if loaded} +