From 1e19ec98538e5f4419aa5cbfc69e58e5ae82d182 Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Sat, 15 Mar 2025 18:25:02 +1030 Subject: [PATCH 1/9] start working on ui to manage assets --- .vscode/launch.json | 48 ++++ example/basic/manifest.yaml | 63 +++--- src/cli/mod.rs | 2 +- src/core/commands/command_create.rs | 76 ++++++- src/core/project.rs | 56 +++++ ui/package-lock.json | 9 +- ui/package.json | 2 +- ui/src/App.tsx | 159 ++++++++++++- ui/src/api.ts | 14 +- ui/src/components/organisms/asset_tree.tsx | 117 ++++++++++ ui/src/components/ui/accordion.tsx | 2 +- ui/src/components/ui/context-menu.tsx | 249 +++++++++++++++++++++ ui/src/components/ui/dialog.tsx | 141 ++++++++++++ ui/src/components/ui/text-field.tsx | 152 +++++++++++++ ui/src/components/ui/tooltip.tsx | 35 +++ ui/src/index.tsx | 2 +- 16 files changed, 1073 insertions(+), 54 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 ui/src/components/organisms/asset_tree.tsx create mode 100644 ui/src/components/ui/context-menu.tsx create mode 100644 ui/src/components/ui/dialog.tsx create mode 100644 ui/src/components/ui/text-field.tsx create mode 100644 ui/src/components/ui/tooltip.tsx diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..3e0c0ac --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,48 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'conduct'", + "cargo": { + "args": [ + "build", + "--bin=conduct", + "--package=conduct" + ], + "filter": { + "name": "conduct", + "kind": "bin" + } + }, + "args": [ + "--project-dir", + "./example/basic" + ], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'conduct'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=conduct", + "--package=conduct" + ], + "filter": { + "name": "conduct", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/example/basic/manifest.yaml b/example/basic/manifest.yaml index be4d493..da003ee 100644 --- a/example/basic/manifest.yaml +++ b/example/basic/manifest.yaml @@ -4,18 +4,18 @@ display_name: Basic Project programs: blender: exports: + .animbake.abc: export_alembic.py .mesh.blend: export_blend_library.py .shadergraph.blend: export_blend_library.py - .animbake.abc: export_alembic.py imports: .animbake.abc: import_animbake_alembic.py .glb: import_glb_mesh.py - .shadergraph.blend: import_blend_shadergraph.py .mesh.blend: import_blend_library_mesh.py + .shadergraph.blend: import_blend_shadergraph.py inkscape: exports: - .png: export_png_512.py .2x.png: export_png_1024.py + .png: export_png_512.py departments: anim: @@ -23,23 +23,23 @@ departments: blender: exports: - .animbake.abc - light: - default_elements: - - default_light_rig + layout: programs: blender: + exports: + - .instancer.blend imports: - - .glb - - .mesh.blend - - .animbake.abc - - .shadergraph.blend - layout: + - .mesh.blend + light: + default_elements: + - default_light_rig programs: blender: - exports: - - .instancer.blend imports: - - .mesh.blend + - .glb + - .mesh.blend + - .animbake.abc + - .shadergraph.blend lookdev: programs: blender: @@ -57,32 +57,30 @@ departments: - .animbake.abc assets: + 2d: + icons: + - iconA: + departments: + design: + - vector + - 2x 3d: + environment: + - cubeWorldA: + departments: + layout: + - !depends(defaultCubeA;defaultCubeB) cubeInstancer prop: - $template: departments: + lookdev: + - shadergraph model: - !shot_local mesh - lod - test - lookdev: - - shadergraph - - defaultCubeA: - departments: {} - - defaultCubeB: - departments: {} - environment: - - cubeWorldA: - departments: - layout: - - !depends(defaultCubeA;defaultCubeB) cubeInstancer - 2d: - icons: - - iconA: - departments: - design: - - vector - - 2x + - defaultCubeA: {} + - defaultCubeB: {} shots: '103': @@ -104,3 +102,4 @@ shots: version_control: type: versioned_directories + seperate_shots_and_assets: true diff --git a/src/cli/mod.rs b/src/cli/mod.rs index a70ae1e..c386155 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -103,7 +103,7 @@ pub fn cli() -> CliResult { } None => CliResult::Success, }, - Err(_) => CliResult::Error("".to_string()), + Err(error) => CliResult::Error(error.to_string()), } } None => { diff --git a/src/core/commands/command_create.rs b/src/core/commands/command_create.rs index 6402627..b999d78 100644 --- a/src/core/commands/command_create.rs +++ b/src/core/commands/command_create.rs @@ -1,9 +1,12 @@ -use std::sync::RwLock; +use std::{collections::BTreeMap, sync::RwLock}; use clap::{command, Args}; use log::info; -use crate::core::project::Project; +use crate::core::{ + asset::{Asset, AssetEntry}, + project::Project, +}; use serde::{Deserialize, Serialize}; use super::{args::CommonArgs, error::CommandError, Command}; @@ -13,6 +16,9 @@ pub struct CreateArgs { #[command(flatten)] #[serde(flatten)] common: CommonArgs, + + #[arg(short, long)] + pub category: Option, } impl Command for CreateArgs { @@ -22,8 +28,72 @@ impl Command for CreateArgs { ) -> Result, CommandError> { info!("Returning result from command create!"); - project.read().unwrap().save(); + let mut p = project.write().unwrap(); + + match self.common.asset { + Some(asset) => match create_asset(&mut p, asset) { + Err(err) => return Err(err), + Ok(_) => (), + }, + None => (), + }; + + match self.category { + Some(category) => { + p.create_category_tree_from_path(&category); + } + None => (), + } + p.save(); Ok(None) } } + +fn create_asset( + p: &mut std::sync::RwLockWriteGuard<'_, Project>, + asset: String, +) -> Result<(), CommandError> { + let parts: Vec = asset.split("/").map(|x| x.to_string()).collect(); + let asset_name = parts.last().unwrap(); + match p.get_asset_by_name(asset_name.clone()) { + Some(existing) => { + return Err(CommandError::Message(format!( + "Asset '{}' already exists at '{}'", + asset_name, existing.1 + ))) + } + None => (), + } + let path = &parts[..(parts.len() - 1)]; + let category_path = path.join("/"); + p.create_category_tree_from_path(&category_path); + let category = p.get_mut_category_by_path(category_path); + + match category { + Some(category) => { + for child in category.children.iter() { + match child.1 { + AssetEntry::Asset(asset) => (), + AssetEntry::Category(asset_category) => { + return Err(CommandError::Message( + "Trying to add an asset to a category which contains other categories, this is not currently supported!" + .to_string(), + )) + } + } + } + + info!("Adding asset '{}' to category: '{}'", asset, category.name); + category.children.insert( + asset_name.clone(), + AssetEntry::Asset(Asset { + departments: BTreeMap::new(), + }), + ); + } + None => (), + } + + Ok(()) +} diff --git a/src/core/project.rs b/src/core/project.rs index 3d2af27..aaaede0 100644 --- a/src/core/project.rs +++ b/src/core/project.rs @@ -184,6 +184,62 @@ impl Project { None } + + pub fn get_mut_category_by_path(&mut self, path: String) -> Option<&mut AssetCategory> { + let parts = path.split('/'); + + let mut current = &mut self.assets; + + for part in parts.into_iter() { + trace!("Looking for part: {}", part); + let result = current.children.get_mut(part); + match result { + Some(result) => match result { + AssetEntry::Category(asset_category) => current = asset_category, + _ => panic!("Found unexpected asset while parsing category path"), + }, + None => return None, + } + } + + Some(current) + } + + pub fn create_category_tree_from_path(&mut self, path: &String) { + let parts: Vec = path.split('/').map(|x| x.to_string()).collect(); + let mut current = &mut self.assets; + + for i in 0..parts.len() { + let part = &parts[i]; + + let entry = current.children.get(part); + current = match entry { + Some(_) => match current.children.get_mut(part) { + Some(entry) => match entry { + AssetEntry::Asset(_) => panic!(), + AssetEntry::Category(asset_category) => asset_category, + }, + _ => panic!(), + }, + None => { + current.children.insert( + part.clone(), + AssetEntry::Category(AssetCategory { + name: part.clone(), + template: None, + children: BTreeMap::new(), + }), + ); + + let result = current.children.get_mut(part).unwrap(); + match result { + AssetEntry::Asset(_) => panic!(), + AssetEntry::Category(asset_category) => asset_category, + } + } + } + } + } } fn insert_assets_to_map<'a>( diff --git a/ui/package-lock.json b/ui/package-lock.json index 8188767..bb92e50 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "license": "MIT", "dependencies": { - "@kobalte/core": "^0.13.7", + "@kobalte/core": "^0.13.9", "@solidjs/router": "^0.15.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", @@ -783,9 +783,10 @@ } }, "node_modules/@kobalte/core": { - "version": "0.13.7", - "resolved": "https://registry.npmjs.org/@kobalte/core/-/core-0.13.7.tgz", - "integrity": "sha512-COhjWk1KnCkl3qMJDvdrOsvpTlJ9gMLdemkAn5SWfbPn/lxJYabejnNOk+b/ILGg7apzQycgbuo48qb8ppqsAg==", + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/@kobalte/core/-/core-0.13.9.tgz", + "integrity": "sha512-TkeSpgNy7I5k8jwjqT9CK3teAxN0aFb3yyL9ODb06JVYMwXIk+UKrizoAF1ahLUP85lKnxv44B4Y5cXkHShgqw==", + "license": "MIT", "dependencies": { "@floating-ui/dom": "^1.5.1", "@internationalized/date": "^3.4.0", diff --git a/ui/package.json b/ui/package.json index a09e179..e99e06c 100644 --- a/ui/package.json +++ b/ui/package.json @@ -20,7 +20,7 @@ "vite-plugin-solid": "^2.8.2" }, "dependencies": { - "@kobalte/core": "^0.13.7", + "@kobalte/core": "^0.13.9", "@solidjs/router": "^0.15.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", diff --git a/ui/src/App.tsx b/ui/src/App.tsx index ae09591..b6358dc 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,28 +1,171 @@ -import { createResource, type Component, Show, Switch, Match } from 'solid-js'; +import { createResource, type Component, Show, Switch, Match, createSignal } from 'solid-js'; -import { get, getSummary, doExport, doCreate } from './api'; +import { getSummary, getAssetTree, doCreate } from './api'; import { Button } from './components/ui/button'; import { ColorModeProvider } from '@kobalte/core/color-mode'; import { Separator } from './components/ui/separator'; -import { SummaryResponse } from './bindings/summary_response'; +import AssetTree from './components/organisms/asset_tree'; +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from './components/ui/dialog'; +import { ContextMenuCheckboxItem, ContextMenuGroupLabel, ContextMenuItem } from './components/ui/context-menu'; +import { TextField, TextFieldInput } from './components/ui/text-field'; +import { Callout, CalloutContent, CalloutTitle } from './components/ui/callout'; const App: Component = () => { const [info] = createResource(getSummary); + const [assets, { mutate, refetch }] = createResource(() => getAssetTree(null)); + + const [isCreateAssetDialogOpen, setOpenCreateAssetDialog] = createSignal(false); + + const [isCreateCategoryDialogOpen, setOpenCreateCategoryDialog] = createSignal(false); + const [category, setCategory] = createSignal(""); + const [newAssetName, setNewAssetname] = createSignal("") + + const [error, setError] = createSignal("") + + + const newAssetPath = () => { + let path = category() + if (path.length > 0) path += "/" + path += newAssetName() + return path + } + + const openCreateAssetDialog = (parent: string) => { + setOpenCreateAssetDialog(true) + setNewAssetname("") + setCategory(parent) + } + + const openCreateCategoryDialog = (parent: string) => { + setNewAssetname("") + setCategory(parent) + setOpenCreateCategoryDialog(true); + } + return ( -
+

{info()!.display_name}

{info()!.identifier}

-
- - - +
+ + ( + <> + console.log(path)}>Inspect + { + entry.type == "Category" && Object.entries(entry.children).every((e) => e[1]?.type != "Category") ? { openCreateAssetDialog(path) }} >Add Asset : <> + } + { + entry.type == "Category" && Object.entries(entry.children).every((e) => e[1]?.type == "Category") ? { openCreateCategoryDialog(path) }} >Add Subcategory : <> + } + + )}>
+
+ + +
+ + + + + {category()} + + Add an asset to the category + + +
+ + + +
+ {newAssetPath()} +
+
+ + + Warning + + {error()} + + + + + + +
+
+ + + + + + {category()} + + Add a new asset category + + +
+ + + +
+ {newAssetPath()} +
+
+ + + Warning + + {error()} + + + + + + +
+
+ + + ); diff --git a/ui/src/api.ts b/ui/src/api.ts index acbaa6e..1d2c18c 100644 --- a/ui/src/api.ts +++ b/ui/src/api.ts @@ -57,9 +57,17 @@ export async function doExport(department: string, asset: string, element: strin return await result.json() as SummaryResponse } -export async function doCreate(): Promise { - let result = await get("api/v1/command/create?asset=suzanneA&department=model") - return await result.json() as SummaryResponse +export async function doCreate(asset: string | null, category: string | null): Promise { + let result = await get("api/v1/command/create", { + "asset": asset, + "category": category + }) + + if (result.status == 200) { + return true + } else { + return await result.json() + } } export async function exitDialog(result: any) { diff --git a/ui/src/components/organisms/asset_tree.tsx b/ui/src/components/organisms/asset_tree.tsx new file mode 100644 index 0000000..da0681d --- /dev/null +++ b/ui/src/components/organisms/asset_tree.tsx @@ -0,0 +1,117 @@ +import { Component, createResource, createSignal, For, Resource, Show } from "solid-js"; +import { getAssetTree } from "~/api"; +import { ToggleGroup } from "../ui/toggle-group"; +import { Label } from "../ui/label"; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "../ui/accordion"; +import { AssetTreeCategory, AssetTreeEntry } from "~/bindings/bindings_gen"; +import { Checkbox } from "../ui/checkbox"; +import { ContextMenu } from "@kobalte/core/context-menu"; +import { ContextMenuContent, ContextMenuGroupLabel, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger } from "../ui/context-menu"; +import { Button } from "../ui/button"; + +import * as AccordionPrimitive from "@kobalte/core/accordion" +import { Tooltip } from "@kobalte/core/tooltip"; +import { TooltipContent, TooltipTrigger } from "../ui/tooltip"; + +export interface AssetTreeProps { + contextMenuBuilder?(name: string, entry: AssetTreeEntry): any + categoryContextMenuBuilder?(name: string): any + assets: Resource +} + +const AssetTree = (props: AssetTreeProps) => { + + let closedPaths: string[] = []; + let contextMenuBuilder = props.contextMenuBuilder; + + + const assetEntry: Component<{ entry_name: string, entry: AssetTreeEntry, current_path: string }> = (props) => { + let path = props.current_path + (props.current_path == "" ? props.entry_name : ("/" + props.entry_name)); + if (props.entry.type == "Asset") { + return ( +
+ + + + + + + + {contextMenuBuilder?.(path, props.entry)} + + + + +
+ ) + } + + return ( + + { + if (selection.includes(path)) { + let idx = closedPaths.indexOf(path) + if (idx != -1) { + closedPaths.splice(idx, 1); + } + } else { + closedPaths.push(path) + } + }} > + +
+ + + + + + + + + + + {contextMenuBuilder?.(path, props.entry)} + + +
+ + + {(item) => + assetEntry({ entry_name: item[0], entry: item[1]!, current_path: path }) + } + + +
+
+ ) + } + + + + return ( +
+ + + + {(item) => item[1]!.type == 'Asset' ? ( +
+ {item[0]} +
+ ) :
{ + assetEntry({ + entry_name: item[0]!, + entry: item[1]!, + current_path: "" + }) + } +
+ } +
+
+
+
+ ); + +} + +export default AssetTree \ No newline at end of file diff --git a/ui/src/components/ui/accordion.tsx b/ui/src/components/ui/accordion.tsx index 3099a95..0d1e1de 100644 --- a/ui/src/components/ui/accordion.tsx +++ b/ui/src/components/ui/accordion.tsx @@ -17,7 +17,7 @@ const AccordionItem = ( props: PolymorphicProps> ) => { const [local, others] = splitProps(props as AccordionItemProps, ["class"]) - return + return } type AccordionTriggerProps = diff --git a/ui/src/components/ui/context-menu.tsx b/ui/src/components/ui/context-menu.tsx new file mode 100644 index 0000000..0cf6c94 --- /dev/null +++ b/ui/src/components/ui/context-menu.tsx @@ -0,0 +1,249 @@ +import type { Component, ComponentProps, JSX, ValidComponent } from "solid-js" +import { splitProps } from "solid-js" + +import * as ContextMenuPrimitive from "@kobalte/core/context-menu" +import type { PolymorphicProps } from "@kobalte/core/polymorphic" + +import { cn } from "~/lib/utils" + +const ContextMenuTrigger = ContextMenuPrimitive.Trigger +const ContextMenuPortal = ContextMenuPrimitive.Portal +const ContextMenuSub = ContextMenuPrimitive.Sub +const ContextMenuGroup = ContextMenuPrimitive.Group +const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup + +const ContextMenu: Component = (props) => { + return +} + +type ContextMenuContentProps = + ContextMenuPrimitive.ContextMenuContentProps & { + class?: string | undefined + } + +const ContextMenuContent = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as ContextMenuContentProps, ["class"]) + return ( + + + + ) +} + +type ContextMenuItemProps = + ContextMenuPrimitive.ContextMenuItemProps & { + class?: string | undefined + } + +const ContextMenuItem = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as ContextMenuItemProps, ["class"]) + return ( + + ) +} + +const ContextMenuShortcut: Component> = (props) => { + const [local, others] = splitProps(props, ["class"]) + return +} + +type ContextMenuSeparatorProps = + ContextMenuPrimitive.ContextMenuSeparatorProps & { + class?: string | undefined + } + +const ContextMenuSeparator = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as ContextMenuSeparatorProps, ["class"]) + return ( + + ) +} + +type ContextMenuSubTriggerProps = + ContextMenuPrimitive.ContextMenuSubTriggerProps & { + class?: string | undefined + children?: JSX.Element + } + +const ContextMenuSubTrigger = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as ContextMenuSubTriggerProps, ["class", "children"]) + return ( + + {local.children} + + + + + ) +} + +type ContextMenuSubContentProps = + ContextMenuPrimitive.ContextMenuSubContentProps & { + class?: string | undefined + } + +const ContextMenuSubContent = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as ContextMenuSubContentProps, ["class"]) + return ( + + ) +} + +type ContextMenuCheckboxItemProps = + ContextMenuPrimitive.ContextMenuCheckboxItemProps & { + class?: string | undefined + children?: JSX.Element + } + +const ContextMenuCheckboxItem = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as ContextMenuCheckboxItemProps, ["class", "children"]) + return ( + + + + + + + + + {local.children} + + ) +} + +type ContextMenuGroupLabelProps = + ContextMenuPrimitive.ContextMenuGroupLabelProps & { + class?: string | undefined + } + +const ContextMenuGroupLabel = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as ContextMenuGroupLabelProps, ["class"]) + return ( + + ) +} + +type ContextMenuRadioItemProps = + ContextMenuPrimitive.ContextMenuRadioItemProps & { + class?: string | undefined + children?: JSX.Element + } + +const ContextMenuRadioItem = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as ContextMenuRadioItemProps, ["class", "children"]) + return ( + + + + + + + + + {local.children} + + ) +} + +export { + ContextMenu, + ContextMenuTrigger, + ContextMenuPortal, + ContextMenuContent, + ContextMenuItem, + ContextMenuShortcut, + ContextMenuSeparator, + ContextMenuSub, + ContextMenuSubTrigger, + ContextMenuSubContent, + ContextMenuCheckboxItem, + ContextMenuGroup, + ContextMenuGroupLabel, + ContextMenuRadioGroup, + ContextMenuRadioItem +} \ No newline at end of file diff --git a/ui/src/components/ui/dialog.tsx b/ui/src/components/ui/dialog.tsx new file mode 100644 index 0000000..a703e9c --- /dev/null +++ b/ui/src/components/ui/dialog.tsx @@ -0,0 +1,141 @@ +import type { Component, ComponentProps, JSX, ValidComponent } from "solid-js" +import { splitProps } from "solid-js" + +import * as DialogPrimitive from "@kobalte/core/dialog" +import type { PolymorphicProps } from "@kobalte/core/polymorphic" + +import { cn } from "~/lib/utils" + +const Dialog = DialogPrimitive.Root +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal: Component = (props) => { + const [, rest] = splitProps(props, ["children"]) + return ( + +
+ {props.children} +
+
+ ) +} + +type DialogOverlayProps = + DialogPrimitive.DialogOverlayProps & { class?: string | undefined } + +const DialogOverlay = ( + props: PolymorphicProps> +) => { + const [, rest] = splitProps(props as DialogOverlayProps, ["class"]) + return ( + + ) +} + +type DialogContentProps = + DialogPrimitive.DialogContentProps & { + class?: string | undefined + children?: JSX.Element + } + +const DialogContent = ( + props: PolymorphicProps> +) => { + const [, rest] = splitProps(props as DialogContentProps, ["class", "children"]) + return ( + + + + {props.children} + + + + + + Close + + + + ) +} + +const DialogHeader: Component> = (props) => { + const [, rest] = splitProps(props, ["class"]) + return ( +
+ ) +} + +const DialogFooter: Component> = (props) => { + const [, rest] = splitProps(props, ["class"]) + return ( +
+ ) +} + +type DialogTitleProps = DialogPrimitive.DialogTitleProps & { + class?: string | undefined +} + +const DialogTitle = ( + props: PolymorphicProps> +) => { + const [, rest] = splitProps(props as DialogTitleProps, ["class"]) + return ( + + ) +} + +type DialogDescriptionProps = + DialogPrimitive.DialogDescriptionProps & { + class?: string | undefined + } + +const DialogDescription = ( + props: PolymorphicProps> +) => { + const [, rest] = splitProps(props as DialogDescriptionProps, ["class"]) + return ( + + ) +} + +export { + Dialog, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription +} diff --git a/ui/src/components/ui/text-field.tsx b/ui/src/components/ui/text-field.tsx new file mode 100644 index 0000000..e5b821b --- /dev/null +++ b/ui/src/components/ui/text-field.tsx @@ -0,0 +1,152 @@ +import type { ValidComponent } from "solid-js" +import { mergeProps, splitProps } from "solid-js" + +import type { PolymorphicProps } from "@kobalte/core" +import * as TextFieldPrimitive from "@kobalte/core/text-field" +import { cva } from "class-variance-authority" + +import { cn } from "~/lib/utils" + +type TextFieldRootProps = + TextFieldPrimitive.TextFieldRootProps & { + class?: string | undefined + } + +const TextField = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as TextFieldRootProps, ["class"]) + return +} + +type TextFieldInputProps = + TextFieldPrimitive.TextFieldInputProps & { + class?: string | undefined + type?: + | "button" + | "checkbox" + | "color" + | "date" + | "datetime-local" + | "email" + | "file" + | "hidden" + | "image" + | "month" + | "number" + | "password" + | "radio" + | "range" + | "reset" + | "search" + | "submit" + | "tel" + | "text" + | "time" + | "url" + | "week" + } + +const TextFieldInput = ( + rawProps: PolymorphicProps> +) => { + const props = mergeProps[]>({ type: "text" }, rawProps) + const [local, others] = splitProps(props as TextFieldInputProps, ["type", "class"]) + return ( + + ) +} + +type TextFieldTextAreaProps = + TextFieldPrimitive.TextFieldTextAreaProps & { class?: string | undefined } + +const TextFieldTextArea = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as TextFieldTextAreaProps, ["class"]) + return ( + + ) +} + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", + { + variants: { + variant: { + label: "data-[invalid]:text-destructive", + description: "font-normal text-muted-foreground", + error: "text-xs text-destructive" + } + }, + defaultVariants: { + variant: "label" + } + } +) + +type TextFieldLabelProps = + TextFieldPrimitive.TextFieldLabelProps & { class?: string | undefined } + +const TextFieldLabel = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as TextFieldLabelProps, ["class"]) + return +} + +type TextFieldDescriptionProps = + TextFieldPrimitive.TextFieldDescriptionProps & { + class?: string | undefined + } + +const TextFieldDescription = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as TextFieldDescriptionProps, ["class"]) + return ( + + ) +} + +type TextFieldErrorMessageProps = + TextFieldPrimitive.TextFieldErrorMessageProps & { + class?: string | undefined + } + +const TextFieldErrorMessage = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as TextFieldErrorMessageProps, ["class"]) + return ( + + ) +} + +export { + TextField, + TextFieldInput, + TextFieldTextArea, + TextFieldLabel, + TextFieldDescription, + TextFieldErrorMessage +} diff --git a/ui/src/components/ui/tooltip.tsx b/ui/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..444a329 --- /dev/null +++ b/ui/src/components/ui/tooltip.tsx @@ -0,0 +1,35 @@ +import type { ValidComponent } from "solid-js" +import { splitProps, type Component } from "solid-js" + +import type { PolymorphicProps } from "@kobalte/core/polymorphic" +import * as TooltipPrimitive from "@kobalte/core/tooltip" + +import { cn } from "~/lib/utils" + +const TooltipTrigger = TooltipPrimitive.Trigger + +const Tooltip: Component = (props) => { + return +} + +type TooltipContentProps = + TooltipPrimitive.TooltipContentProps & { class?: string | undefined } + +const TooltipContent = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as TooltipContentProps, ["class"]) + return ( + + + + ) +} + +export { Tooltip, TooltipTrigger, TooltipContent } diff --git a/ui/src/index.tsx b/ui/src/index.tsx index 9b4c997..7a5319b 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -20,7 +20,7 @@ if (!(root instanceof HTMLElement)) { const rootComponent: Component = (props) => { return ( - + {props.children} ) From 5d660da73c0891a2efc9d5f878efc6a9b5bbae1a Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Sat, 22 Mar 2025 14:40:59 +1030 Subject: [PATCH 2/9] more work on ui --- example/basic/manifest.yaml | 7 + src/cli/mod.rs | 4 +- src/core/commands/command_create.rs | 22 +- src/core/commands/command_dialog.rs | 3 +- src/core/commands/command_export.rs | 3 +- src/core/commands/command_get_asset_tree.rs | 3 +- src/core/commands/command_list_assets.rs | 3 +- src/core/commands/command_list_elements.rs | 3 +- .../commands/command_list_export_formats.rs | 3 +- src/core/commands/command_list_shots.rs | 3 +- src/core/commands/command_load_assets.rs | 3 +- src/core/commands/command_save.rs | 30 ++ src/core/commands/command_setup.rs | 3 +- src/core/commands/command_summary.rs | 3 +- src/core/commands/mod.rs | 15 +- src/core/error.rs | 19 ++ src/core/mod.rs | 1 + src/core/project.rs | 12 +- src/gui/routes/command.rs | 5 +- ui/src/App.tsx | 64 +++- ui/src/api.ts | 6 + ui/src/components/organisms/asset_tree.tsx | 2 +- ui/src/components/ui/accordion.tsx | 4 +- ui/src/components/ui/menubar.tsx | 313 ++++++++++++++++++ 24 files changed, 493 insertions(+), 41 deletions(-) create mode 100644 src/core/commands/command_save.rs create mode 100644 src/core/error.rs create mode 100644 ui/src/components/ui/menubar.tsx diff --git a/example/basic/manifest.yaml b/example/basic/manifest.yaml index da003ee..f31c0bc 100644 --- a/example/basic/manifest.yaml +++ b/example/basic/manifest.yaml @@ -64,12 +64,19 @@ assets: design: - vector - 2x + - iconB: {} + sprites: + enemy: + - zombieA: {} + player: + - playerA: {} 3d: environment: - cubeWorldA: departments: layout: - !depends(defaultCubeA;defaultCubeB) cubeInstancer + - cubeWorldB: {} prop: - $template: departments: diff --git a/src/cli/mod.rs b/src/cli/mod.rs index c386155..b6d16ee 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -7,7 +7,7 @@ use clap::Parser; use log::*; pub use result::CliResult; -use crate::core::commands::{write_command_result, Command, CommandType}; +use crate::core::commands::{write_command_result, Command, CommandContext, CommandType}; #[derive(Debug, Parser)] #[command(name = "conduct")] @@ -93,7 +93,7 @@ pub fn cli() -> CliResult { match args.command { Some(command) => { info!("Running command: {:?}", command); - let result = CommandType::execute(command, &project); + let result = CommandType::execute(command, &project, CommandContext { is_cli: true }); match result { Ok(value) => match value { diff --git a/src/core/commands/command_create.rs b/src/core/commands/command_create.rs index b999d78..150438c 100644 --- a/src/core/commands/command_create.rs +++ b/src/core/commands/command_create.rs @@ -1,3 +1,4 @@ +use core::fmt; use std::{collections::BTreeMap, sync::RwLock}; use clap::{command, Args}; @@ -9,7 +10,7 @@ use crate::core::{ }; use serde::{Deserialize, Serialize}; -use super::{args::CommonArgs, error::CommandError, Command}; +use super::{args::CommonArgs, error::CommandError, Command, CommandContext}; #[derive(Debug, Args, Serialize, Deserialize)] pub struct CreateArgs { @@ -25,6 +26,7 @@ impl Command for CreateArgs { fn execute( self, project: &RwLock, + context: CommandContext, ) -> Result, CommandError> { info!("Returning result from command create!"); @@ -39,13 +41,17 @@ impl Command for CreateArgs { }; match self.category { - Some(category) => { - p.create_category_tree_from_path(&category); - } + Some(category) => match p.create_category_tree_from_path(&category) { + Some(err) => return Err(CommandError::Message(format!("{}", err).to_string())), + None => (), + }, None => (), } - p.save(); + if context.is_cli { + p.save(); + } + Ok(None) } } @@ -67,7 +73,11 @@ fn create_asset( } let path = &parts[..(parts.len() - 1)]; let category_path = path.join("/"); - p.create_category_tree_from_path(&category_path); + match p.create_category_tree_from_path(&category_path) { + Some(err) => return Err(CommandError::Message(format!("{}", err).to_string())), + None => (), + } + let category = p.get_mut_category_by_path(category_path); match category { diff --git a/src/core/commands/command_dialog.rs b/src/core/commands/command_dialog.rs index 08e1e16..91dc118 100644 --- a/src/core/commands/command_dialog.rs +++ b/src/core/commands/command_dialog.rs @@ -6,7 +6,7 @@ use query_string_builder::QueryString; use crate::{core::project::Project, gui}; use serde::{Deserialize, Serialize}; -use super::{error::CommandError, Command}; +use super::{error::CommandError, Command, CommandContext}; #[derive(Debug, Args, Serialize, Deserialize)] pub struct DialogArgs { @@ -26,6 +26,7 @@ impl Command for DialogArgs { fn execute( self, project: &RwLock, + _context: CommandContext, ) -> Result, CommandError> { let (_, argv) = argmap::parse(self.extras.iter()); log::debug!("Got extras: {:?}", argv); diff --git a/src/core/commands/command_export.rs b/src/core/commands/command_export.rs index 526aafb..1dce1e3 100644 --- a/src/core/commands/command_export.rs +++ b/src/core/commands/command_export.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use crate::core::{project::Project, version_control::VersionControl}; -use super::{args::CommonArgs, error::CommandError, Command}; +use super::{args::CommonArgs, error::CommandError, Command, CommandContext}; #[derive(Debug, Args, Serialize, Deserialize)] pub struct ExportArgs { @@ -26,6 +26,7 @@ impl Command for ExportArgs { fn execute( self, project: &RwLock, + _context: CommandContext, ) -> Result, CommandError> { info!("Exporting Asset!"); diff --git a/src/core/commands/command_get_asset_tree.rs b/src/core/commands/command_get_asset_tree.rs index 8342f32..091c629 100644 --- a/src/core/commands/command_get_asset_tree.rs +++ b/src/core/commands/command_get_asset_tree.rs @@ -10,7 +10,7 @@ use crate::core::{ }; use serde::{Deserialize, Serialize}; -use super::{args::CommonArgs, error::CommandError, Command}; +use super::{args::CommonArgs, error::CommandError, Command, CommandContext}; #[derive(Debug, Args, Serialize, Deserialize)] pub struct GetAssetTreeArgs { @@ -61,6 +61,7 @@ impl Command for GetAssetTreeArgs { fn execute( self, project: &RwLock, + _context: CommandContext, ) -> Result, CommandError> { let project = project.read().unwrap(); diff --git a/src/core/commands/command_list_assets.rs b/src/core/commands/command_list_assets.rs index 5a8eff5..2d284c6 100644 --- a/src/core/commands/command_list_assets.rs +++ b/src/core/commands/command_list_assets.rs @@ -6,7 +6,7 @@ use ts_rs::TS; use crate::core::{department::DepartmentFinder, project::Project}; use serde::{Deserialize, Serialize}; -use super::{args::CommonArgs, error::CommandError, Command}; +use super::{args::CommonArgs, error::CommandError, Command, CommandContext}; #[derive(Debug, Args, Serialize, Deserialize)] pub struct ListAssetsArgs { @@ -25,6 +25,7 @@ impl Command for ListAssetsArgs { fn execute( self, project: &RwLock, + _context: CommandContext, ) -> Result, CommandError> { let project = project.read().unwrap(); let assets = project.get_assets_flattened(); diff --git a/src/core/commands/command_list_elements.rs b/src/core/commands/command_list_elements.rs index ea2c7dd..e681618 100644 --- a/src/core/commands/command_list_elements.rs +++ b/src/core/commands/command_list_elements.rs @@ -10,7 +10,7 @@ use crate::core::{ }; use serde::{Deserialize, Serialize}; -use super::{args::CommonArgs, error::CommandError, Command}; +use super::{args::CommonArgs, error::CommandError, Command, CommandContext}; #[derive(Debug, Args, Serialize, Deserialize)] pub struct ListElementsArgs { @@ -38,6 +38,7 @@ impl Command for ListElementsArgs { fn execute( self, project: &RwLock, + _context: CommandContext, ) -> Result, CommandError> { if self.common.asset.is_none() { return Err(CommandError::InvalidArguments); diff --git a/src/core/commands/command_list_export_formats.rs b/src/core/commands/command_list_export_formats.rs index d78bc8a..28a56af 100644 --- a/src/core/commands/command_list_export_formats.rs +++ b/src/core/commands/command_list_export_formats.rs @@ -7,7 +7,7 @@ use ts_rs::TS; use crate::core::project::Project; use serde::{Deserialize, Serialize}; -use super::{args::CommonArgs, error::CommandError, Command}; +use super::{args::CommonArgs, error::CommandError, Command, CommandContext}; #[derive(Debug, Args, Serialize, Deserialize)] pub struct ListExportFormatsArgs { @@ -29,6 +29,7 @@ impl Command for ListExportFormatsArgs { fn execute( self, project: &RwLock, + _context: CommandContext, ) -> Result, CommandError> { if self.common.department.is_none() { return Err(CommandError::InvalidArguments); diff --git a/src/core/commands/command_list_shots.rs b/src/core/commands/command_list_shots.rs index 6ec106b..6c73bf8 100644 --- a/src/core/commands/command_list_shots.rs +++ b/src/core/commands/command_list_shots.rs @@ -6,7 +6,7 @@ use ts_rs::TS; use crate::core::project::Project; use serde::{Deserialize, Serialize}; -use super::{args::CommonArgs, error::CommandError, Command}; +use super::{args::CommonArgs, error::CommandError, Command, CommandContext}; use crate::core::shot::shot_resolver::ShotResolver; #[derive(Debug, Args, Serialize, Deserialize)] @@ -26,6 +26,7 @@ impl Command for ListShotsArgs { fn execute( self, project: &RwLock, + _context: CommandContext, ) -> Result, CommandError> { let project = project.read().unwrap(); diff --git a/src/core/commands/command_load_assets.rs b/src/core/commands/command_load_assets.rs index 2a1a2e1..f0361ee 100644 --- a/src/core/commands/command_load_assets.rs +++ b/src/core/commands/command_load_assets.rs @@ -10,7 +10,7 @@ use crate::core::{ }; use serde::{Deserialize, Serialize}; -use super::{args::CommonArgs, error::CommandError, Command}; +use super::{args::CommonArgs, error::CommandError, Command, CommandContext}; pub enum LoadReason { Requested, @@ -51,6 +51,7 @@ impl Command for LoadAssetsArgs { fn execute( self, project: &RwLock, + _context: CommandContext, ) -> Result, CommandError> { let project = project.read().unwrap(); diff --git a/src/core/commands/command_save.rs b/src/core/commands/command_save.rs new file mode 100644 index 0000000..dda3c7c --- /dev/null +++ b/src/core/commands/command_save.rs @@ -0,0 +1,30 @@ +use std::{collections::BTreeMap, sync::RwLock}; + +use clap::{command, Args}; +use log::info; + +use crate::core::{ + asset::{Asset, AssetEntry}, + project::Project, +}; +use serde::{Deserialize, Serialize}; + +use super::{args::CommonArgs, error::CommandError, Command, CommandContext}; + +#[derive(Debug, Args, Serialize, Deserialize)] +pub struct SaveArgs {} + +impl Command for SaveArgs { + fn execute( + self, + project: &RwLock, + context: CommandContext, + ) -> Result, CommandError> { + info!("Saving Project!"); + + let project = project.read().unwrap(); + project.save(); + + Ok(None) + } +} diff --git a/src/core/commands/command_setup.rs b/src/core/commands/command_setup.rs index fa0f2d9..9a7760e 100644 --- a/src/core/commands/command_setup.rs +++ b/src/core/commands/command_setup.rs @@ -7,7 +7,7 @@ use ts_rs::TS; use crate::core::{project::Project, shot::shot_resolver::ShotResolver}; use serde::{Deserialize, Serialize}; -use super::{args::CommonArgs, error::CommandError, Command}; +use super::{args::CommonArgs, error::CommandError, Command, CommandContext}; #[derive(Debug, Args, Serialize, Deserialize)] pub struct SetupArgs { @@ -37,6 +37,7 @@ impl Command for SetupArgs { fn execute( self, project: &RwLock, + _context: CommandContext, ) -> Result, CommandError> { if self.common.asset.is_none() || self.common.department.is_none() { return Err(CommandError::InvalidArguments); diff --git a/src/core/commands/command_summary.rs b/src/core/commands/command_summary.rs index 282fdfc..64ed0ea 100644 --- a/src/core/commands/command_summary.rs +++ b/src/core/commands/command_summary.rs @@ -8,7 +8,7 @@ use ts_rs::TS; use crate::core::project::Project; -use super::{error::CommandError, Command}; +use super::{error::CommandError, Command, CommandContext}; #[derive(Debug, Args, Serialize, Deserialize)] pub struct SummaryArgs {} @@ -26,6 +26,7 @@ impl Command for SummaryArgs { fn execute( self, project: &RwLock, + _context: CommandContext, ) -> Result, CommandError> { let project = project.read().unwrap(); diff --git a/src/core/commands/mod.rs b/src/core/commands/mod.rs index 60d029f..2b8f661 100644 --- a/src/core/commands/mod.rs +++ b/src/core/commands/mod.rs @@ -7,6 +7,7 @@ mod command_list_elements; mod command_list_export_formats; mod command_list_shots; mod command_load_assets; +mod command_save; mod command_setup; mod command_summary; mod error; @@ -32,6 +33,7 @@ use command_list_elements::ListElementsArgs; use command_list_export_formats::ListExportFormatsArgs; use command_list_shots::ListShotsArgs; use command_load_assets::LoadAssetsArgs; +use command_save::SaveArgs; use command_setup::SetupArgs; use command_summary::SummaryArgs; use log::{info, warn}; @@ -41,10 +43,17 @@ use enum_dispatch::enum_dispatch; use error::CommandError; use serde::{Deserialize, Serialize}; +pub struct CommandContext { + pub is_cli: bool, +} + #[enum_dispatch] pub trait Command { - fn execute(self, _project: &RwLock) - -> Result, CommandError>; + fn execute( + self, + _project: &RwLock, + context: CommandContext, + ) -> Result, CommandError>; } #[derive(Debug, Subcommand, Serialize, Deserialize)] @@ -82,6 +91,8 @@ pub enum CommandType { GetAssetTree(GetAssetTreeArgs), LoadAssets(LoadAssetsArgs), + + Save(SaveArgs), } pub fn write_command_result(result: serde_json::Value) { diff --git a/src/core/error.rs b/src/core/error.rs new file mode 100644 index 0000000..1b1d8cd --- /dev/null +++ b/src/core/error.rs @@ -0,0 +1,19 @@ +use std::{ + error::Error, + fmt::{self}, +}; + +#[derive(Debug)] +pub enum ProjectError { + Message(String), +} + +impl Error for ProjectError {} + +impl fmt::Display for ProjectError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ProjectError::Message(msg) => write!(f, "{}", msg), + } + } +} diff --git a/src/core/mod.rs b/src/core/mod.rs index bc6be8f..bdaad01 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -3,6 +3,7 @@ pub mod commands; pub mod context; pub mod department; pub mod element; +pub mod error; pub mod format; pub mod program; pub mod project; diff --git a/src/core/project.rs b/src/core/project.rs index aaaede0..b9a125f 100644 --- a/src/core/project.rs +++ b/src/core/project.rs @@ -11,6 +11,7 @@ use crate::core::{asset, format}; use super::asset::{Asset, AssetCategory, AssetEntry}; use super::department::{self, Department}; +use super::error::ProjectError; use super::program::{self, Program}; use super::version_control::VersionControlConfig; @@ -205,7 +206,7 @@ impl Project { Some(current) } - pub fn create_category_tree_from_path(&mut self, path: &String) { + pub fn create_category_tree_from_path(&mut self, path: &String) -> Option { let parts: Vec = path.split('/').map(|x| x.to_string()).collect(); let mut current = &mut self.assets; @@ -222,6 +223,13 @@ impl Project { _ => panic!(), }, None => { + for child in current.children.values().into_iter() { + match child { + AssetEntry::Asset(_) => return Some(ProjectError::Message("Cannot create a new category in a category which already contains assets".to_string())), + AssetEntry::Category(_) => continue, + } + } + current.children.insert( part.clone(), AssetEntry::Category(AssetCategory { @@ -239,6 +247,8 @@ impl Project { } } } + + return None; } } diff --git a/src/gui/routes/command.rs b/src/gui/routes/command.rs index 82f056c..9709dad 100644 --- a/src/gui/routes/command.rs +++ b/src/gui/routes/command.rs @@ -5,7 +5,7 @@ use url::Url; use wry::http::Request; use crate::{ - core::commands::{Command, CommandType}, + core::commands::{Command, CommandContext, CommandType}, gui::{ api_result::ApiResult, router::{ApiEntry, RequestContext}, @@ -67,7 +67,8 @@ fn do_command( match command { Ok(command) => { debug!("Got command: {:?}", command); - let command_result = CommandType::execute(command, &context.project); + let command_result = + CommandType::execute(command, &context.project, CommandContext { is_cli: false }); match command_result { Ok(value) => Some(ApiResult::Ok(value)), diff --git a/ui/src/App.tsx b/ui/src/App.tsx index b6358dc..acf1a3b 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,6 +1,6 @@ -import { createResource, type Component, Show, Switch, Match, createSignal } from 'solid-js'; +import { createResource, type Component, Show, Switch, Match, createSignal, For } from 'solid-js'; -import { getSummary, getAssetTree, doCreate } from './api'; +import { getSummary, getAssetTree, doCreate, get, saveChanges } from './api'; import { Button } from './components/ui/button'; import { ColorModeProvider } from '@kobalte/core/color-mode'; @@ -10,6 +10,14 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D import { ContextMenuCheckboxItem, ContextMenuGroupLabel, ContextMenuItem } from './components/ui/context-menu'; import { TextField, TextFieldInput } from './components/ui/text-field'; import { Callout, CalloutContent, CalloutTitle } from './components/ui/callout'; +import { Menubar, MenubarContent, MenubarItem, MenubarItemLabel, MenubarMenu, MenubarSeparator, MenubarShortcut, MenubarSub, MenubarSubContent, MenubarSubTrigger, MenubarTrigger } from './components/ui/menubar'; +import { comma } from 'postcss/lib/list'; + +interface CommandEntry { + command: string, + content: any, + label: string +} const App: Component = () => { const [info] = createResource(getSummary); @@ -23,7 +31,6 @@ const App: Component = () => { const [error, setError] = createSignal("") - const newAssetPath = () => { let path = category() if (path.length > 0) path += "/" @@ -34,11 +41,13 @@ const App: Component = () => { const openCreateAssetDialog = (parent: string) => { setOpenCreateAssetDialog(true) setNewAssetname("") + setError("") setCategory(parent) } const openCreateCategoryDialog = (parent: string) => { setNewAssetname("") + setError("") setCategory(parent) setOpenCreateCategoryDialog(true); } @@ -47,10 +56,42 @@ const App: Component = () => { return ( + +
+

{info()!.display_name}

+

{info()!.identifier}

+
+ + + File + + { + saveChanges() + } + }>Save Changes + + + + Edit + + { + openCreateCategoryDialog("") + }}>Create Category + { + openCreateAssetDialog("") + }}>Create Asset + + + Undo + + + + + + + +
-

{info()!.display_name}

-

{info()!.identifier}

-
( @@ -66,15 +107,6 @@ const App: Component = () => { )}>
-
- - -
- @@ -110,6 +142,7 @@ const App: Component = () => { setOpenCreateAssetDialog(false) refetch() setNewAssetname("") + setError("") } else { setError(result['error']) } @@ -155,6 +188,7 @@ const App: Component = () => { setOpenCreateCategoryDialog(false) refetch() setNewAssetname("") + } else { setError(result['error']) } diff --git a/ui/src/api.ts b/ui/src/api.ts index 1d2c18c..e1fdeb1 100644 --- a/ui/src/api.ts +++ b/ui/src/api.ts @@ -118,6 +118,12 @@ export async function getAssetTree(department_filter: null | string = null): Pro return json as AssetTreeCategory } +export async function saveChanges(): Promise { + let result = await get("api/v1/command/save"); + let json = await result.json() + return json +} + export async function loadAssets(program: string, department: string, shot: null | string = null, assets: string[]): Promise { let result = await get("api/v1/command/load_assets", { "program": program, diff --git a/ui/src/components/organisms/asset_tree.tsx b/ui/src/components/organisms/asset_tree.tsx index da0681d..0b2a98e 100644 --- a/ui/src/components/organisms/asset_tree.tsx +++ b/ui/src/components/organisms/asset_tree.tsx @@ -63,7 +63,7 @@ const AssetTree = (props: AssetTreeProps) => { - + diff --git a/ui/src/components/ui/accordion.tsx b/ui/src/components/ui/accordion.tsx index 0d1e1de..958dce4 100644 --- a/ui/src/components/ui/accordion.tsx +++ b/ui/src/components/ui/accordion.tsx @@ -34,7 +34,7 @@ const AccordionTrigger = ( svg]:rotate-180", + "flex flex-1 items-center justify-between py-1 font-medium transition-all hover:underline [&[data-expanded]>svg]:rotate-90", local.class )} {...others} @@ -50,7 +50,7 @@ const AccordionTrigger = ( stroke-linejoin="round" class="size-4 shrink-0 transition-transform duration-200" > - + diff --git a/ui/src/components/ui/menubar.tsx b/ui/src/components/ui/menubar.tsx new file mode 100644 index 0000000..fc5179f --- /dev/null +++ b/ui/src/components/ui/menubar.tsx @@ -0,0 +1,313 @@ +import type { Component, ComponentProps, JSX, ValidComponent } from "solid-js" +import { splitProps } from "solid-js" + +import * as MenubarPrimitive from "@kobalte/core/menubar" +import type { PolymorphicProps } from "@kobalte/core/polymorphic" + +import { cn } from "~/lib/utils" + +const MenubarGroup = MenubarPrimitive.Group +const MenubarPortal = MenubarPrimitive.Portal +const MenubarSub = MenubarPrimitive.Sub +const MenubarRadioGroup = MenubarPrimitive.RadioGroup + +type MenubarRootProps = MenubarPrimitive.MenubarRootProps & { + class?: string | undefined +} + +const Menubar = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as MenubarRootProps, ["class"]) + return ( + + ) +} + +const MenubarMenu: Component = (props) => { + return +} + +type MenubarTriggerProps = + MenubarPrimitive.MenubarTriggerProps & { class?: string | undefined } + +const MenubarTrigger = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as MenubarTriggerProps, ["class"]) + return ( + + ) +} + +type MenubarContentProps = + MenubarPrimitive.MenubarContentProps & { class?: string | undefined } + +const MenubarContent = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as MenubarContentProps, ["class"]) + return ( + + + + ) +} + +type MenubarSubTriggerProps = + MenubarPrimitive.MenubarSubTriggerProps & { + class?: string | undefined + children?: JSX.Element + inset?: boolean + } + +const MenubarSubTrigger = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as MenubarSubTriggerProps, [ + "class", + "children", + "inset" + ]) + return ( + + {local.children} + + + + + ) +} + +type MenubarSubContentProps = + MenubarPrimitive.MenubarSubContentProps & { + class?: string | undefined + } + +const MenubarSubContent = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as MenubarSubContentProps, ["class"]) + return ( + + + + ) +} + +type MenubarItemProps = MenubarPrimitive.MenubarItemProps & { + class?: string | undefined + inset?: boolean +} + +const MenubarItem = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as MenubarItemProps, ["class", "inset"]) + return ( + + ) +} + +type MenubarCheckboxItemProps = + MenubarPrimitive.MenubarCheckboxItemProps & { + class?: string | undefined + children?: JSX.Element + } + +const MenubarCheckboxItem = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as MenubarCheckboxItemProps, ["class", "children"]) + return ( + + + + + + + + + {local.children} + + ) +} + +type MenubarRadioItemProps = + MenubarPrimitive.MenubarRadioItemProps & { + class?: string | undefined + children?: JSX.Element + } + +const MenubarRadioItem = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as MenubarRadioItemProps, ["class", "children"]) + return ( + + + + + + + + + {local.children} + + ) +} + +type MenubarItemLabelProps = + MenubarPrimitive.MenubarItemLabelProps & { + class?: string | undefined + inset?: boolean + } + +const MenubarItemLabel = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as MenubarItemLabelProps, ["class", "inset"]) + return ( + + ) +} + +type MenubarGroupLabelProps = + MenubarPrimitive.MenubarGroupLabelProps & { + class?: string | undefined + inset?: boolean + } + +const MenubarGroupLabel = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as MenubarGroupLabelProps, ["class", "inset"]) + return ( + + ) +} + +type MenubarSeparatorProps = + MenubarPrimitive.MenubarSeparatorProps & { class?: string | undefined } + +const MenubarSeparator = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as MenubarSeparatorProps, ["class"]) + return ( + + ) +} + +const MenubarShortcut: Component> = (props) => { + const [local, others] = splitProps(props, ["class"]) + return ( + + ) +} + +export { + Menubar, + MenubarMenu, + MenubarTrigger, + MenubarContent, + MenubarItem, + MenubarSeparator, + MenubarItemLabel, + MenubarGroupLabel, + MenubarCheckboxItem, + MenubarRadioGroup, + MenubarRadioItem, + MenubarPortal, + MenubarSubContent, + MenubarSubTrigger, + MenubarGroup, + MenubarSub, + MenubarShortcut +} From 8a16f141f0f4dac42fbb6d04a37b62ce390c1668 Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Sat, 22 Mar 2025 17:20:26 +1030 Subject: [PATCH 3/9] add asset element viewer --- bindings/ResolvedElementData.ts | 3 + bindings/ResolvedElementResult.ts | 5 + bindings/VersionControlFile.ts | 3 + src/core/commands/command_list_elements.rs | 17 +- src/core/commands/command_load_assets.rs | 12 + src/core/commands/command_resolve_elements.rs | 91 +++++ src/core/commands/mod.rs | 4 + src/core/element/element_resolver.rs | 14 +- src/core/element/resolved_element_data.rs | 5 +- src/core/version_control/common.rs | 2 +- src/core/version_control/mod.rs | 3 +- ui/src/App.tsx | 343 +++++++++++------- ui/src/api.ts | 9 +- ui/src/bindings/bindings_gen.ts | 9 +- ui/src/components/organisms/asset_tree.tsx | 6 +- 15 files changed, 377 insertions(+), 149 deletions(-) create mode 100644 bindings/ResolvedElementData.ts create mode 100644 bindings/ResolvedElementResult.ts create mode 100644 bindings/VersionControlFile.ts create mode 100644 src/core/commands/command_resolve_elements.rs diff --git a/bindings/ResolvedElementData.ts b/bindings/ResolvedElementData.ts new file mode 100644 index 0000000..8a306ef --- /dev/null +++ b/bindings/ResolvedElementData.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ResolvedElementData = { shot_local: boolean, dependencies: Array | null, asset: string | null, shot: string | null, owning_department: string | null, }; diff --git a/bindings/ResolvedElementResult.ts b/bindings/ResolvedElementResult.ts new file mode 100644 index 0000000..3ab9509 --- /dev/null +++ b/bindings/ResolvedElementResult.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ResolvedElementData } from "./ResolvedElementData"; +import type { VersionControlFile } from "./VersionControlFile"; + +export type ResolvedElementResult = { info: ResolvedElementData, versions: Array, }; diff --git a/bindings/VersionControlFile.ts b/bindings/VersionControlFile.ts new file mode 100644 index 0000000..39b865b --- /dev/null +++ b/bindings/VersionControlFile.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type VersionControlFile = { path: string, version: string, }; diff --git a/src/core/commands/command_list_elements.rs b/src/core/commands/command_list_elements.rs index e681618..096214b 100644 --- a/src/core/commands/command_list_elements.rs +++ b/src/core/commands/command_list_elements.rs @@ -55,13 +55,18 @@ impl Command for ListElementsArgs { }; let project = project.read().unwrap(); + + let asset = self.common.asset.unwrap(); + let asset = asset.split("/").last().unwrap(); + + let elements = project.get_elements(asset.to_string(), &context); + let elements = match elements { + Ok(elements) => elements, + Err(err) => return Err(CommandError::Message(format!("{}", err))), + }; + let mut result = ListElementsResult { - elements: project - .get_elements(self.common.asset.unwrap(), &context) - .keys() - .into_iter() - .map(|f| f.to_string()) - .collect(), + elements: elements.keys().into_iter().map(|f| f.to_string()).collect(), }; result.elements.sort(); diff --git a/src/core/commands/command_load_assets.rs b/src/core/commands/command_load_assets.rs index f0361ee..2cfa670 100644 --- a/src/core/commands/command_load_assets.rs +++ b/src/core/commands/command_load_assets.rs @@ -87,6 +87,12 @@ impl Command for LoadAssetsArgs { info!("----"); info!("Loading Asset: `{}`", asset); let elements = project.get_elements(asset.to_string(), &c); + + let elements = match elements { + Ok(elements) => elements, + Err(err) => return Err(CommandError::Message(format!("{}", err))), + }; + for element in elements.iter() { debug!("Resolved element: {:?}", element); } @@ -170,6 +176,12 @@ fn get_required_assets( let mut result = Vec::new(); for asset in asset_names.into_iter() { let elements = project.get_elements(asset.to_string(), context); + + let elements = match elements { + Ok(elements) => elements, + Err(_) => return vec![], + }; + for (element, data) in elements.iter() { match data.get_dependencies() { Some(dependencies) => { diff --git a/src/core/commands/command_resolve_elements.rs b/src/core/commands/command_resolve_elements.rs new file mode 100644 index 0000000..19b227f --- /dev/null +++ b/src/core/commands/command_resolve_elements.rs @@ -0,0 +1,91 @@ +use std::{ + collections::{BTreeMap, HashMap}, + sync::RwLock, +}; + +use argmap::new; +use clap::{command, Args}; +use ts_rs::TS; + +use crate::core::{ + context::{Context, ContextMode}, + element::{ + self, element_resolver::ElementResolver, resolved_element_data::ResolvedElementData, + }, + project::Project, + version_control::{VersionControl, VersionControlFile}, +}; +use serde::{Deserialize, Serialize}; + +use super::{args::CommonArgs, error::CommandError, Command, CommandContext}; + +#[derive(Debug, Args, Serialize, Deserialize)] +pub struct ResolveElementsArgs { + #[command(flatten)] + #[serde(flatten)] + common: CommonArgs, +} + +#[derive(Debug, Serialize, Deserialize, TS)] +pub struct ResolvedElementResult { + info: ResolvedElementData, + versions: Vec, +} + +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export, export_to = "../ui/src/bindings/bindings_gen.ts")] +pub struct ResolveElementsResult { + pub result: BTreeMap, +} + +impl Command for ResolveElementsArgs { + fn execute( + self, + project: &RwLock, + _context: CommandContext, + ) -> Result, CommandError> { + if self.common.asset.is_none() { + return Err(CommandError::InvalidArguments); + } + + let context = Context { + department: self.common.department, + shot: self.common.shot, + mode: ContextMode::Export, + }; + + let project = project.read().unwrap(); + + let asset = self.common.asset.unwrap(); + let asset = asset.split("/").last().unwrap(); + + let elements = project.get_elements(asset.to_string(), &context); + let elements = match elements { + Ok(elements) => elements, + Err(err) => return Err(CommandError::Message(format!("{}", err))), + }; + + let mut result: BTreeMap = BTreeMap::new(); + + for element in elements.iter() { + let files = VersionControl::get_element_files( + &project.version_control, + &project, + element.0.clone(), + element.1, + ); + + result.insert( + element.0.clone(), + ResolvedElementResult { + info: element.1.clone(), + versions: files.clone(), + }, + ); + } + + Ok(Some( + serde_json::to_value(ResolveElementsResult { result: result }).unwrap(), + )) + } +} diff --git a/src/core/commands/mod.rs b/src/core/commands/mod.rs index 2b8f661..8cecb7d 100644 --- a/src/core/commands/mod.rs +++ b/src/core/commands/mod.rs @@ -7,6 +7,7 @@ mod command_list_elements; mod command_list_export_formats; mod command_list_shots; mod command_load_assets; +mod command_resolve_elements; mod command_save; mod command_setup; mod command_summary; @@ -33,6 +34,7 @@ use command_list_elements::ListElementsArgs; use command_list_export_formats::ListExportFormatsArgs; use command_list_shots::ListShotsArgs; use command_load_assets::LoadAssetsArgs; +use command_resolve_elements::ResolveElementsArgs; use command_save::SaveArgs; use command_setup::SetupArgs; use command_summary::SummaryArgs; @@ -88,6 +90,8 @@ pub enum CommandType { /// List all shots ListShots(ListShotsArgs), + ResolveElements(ResolveElementsArgs), + GetAssetTree(GetAssetTreeArgs), LoadAssets(LoadAssetsArgs), diff --git a/src/core/element/element_resolver.rs b/src/core/element/element_resolver.rs index db603e8..15cf969 100644 --- a/src/core/element/element_resolver.rs +++ b/src/core/element/element_resolver.rs @@ -5,6 +5,7 @@ use log::{debug, trace, warn}; use crate::core::{ asset::Asset, context::{Context, ContextMode}, + error::ProjectError, project::Project, }; @@ -18,7 +19,7 @@ pub trait ElementResolver { &self, asset_name: String, context: &Context, - ) -> BTreeMap; + ) -> Result, ProjectError>; fn get_element( &self, @@ -42,7 +43,10 @@ impl ElementResolver for Project { ) -> Option { let result = self.get_elements(asset_name, context); - return result.get(&element_name).cloned(); + match result { + Ok(results) => results.get(&element_name).cloned(), + Err(_) => None, + } } // Resolve the list of elements for a given asset @@ -50,7 +54,7 @@ impl ElementResolver for Project { &self, asset_name: String, context: &Context, - ) -> BTreeMap { + ) -> Result, ProjectError> { let asset = self.get_asset_by_name(asset_name.clone()); trace!("Getting assets with context: {:#?}", context); @@ -59,7 +63,7 @@ impl ElementResolver for Project { Some(asset) => asset, None => { warn!("Asset {} does not exist", asset_name); - panic!() + return Err(ProjectError::Message("Asset does not exist".to_string())); } }; @@ -81,7 +85,7 @@ impl ElementResolver for Project { ); add_elements_from_department_default(&mut result, self, asset, context, element_data); - result + Ok(result) } } diff --git a/src/core/element/resolved_element_data.rs b/src/core/element/resolved_element_data.rs index 8e73c67..449ba78 100644 --- a/src/core/element/resolved_element_data.rs +++ b/src/core/element/resolved_element_data.rs @@ -1,4 +1,7 @@ -#[derive(Clone, Debug)] +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] pub struct ResolvedElementData { shot_local: bool, dependencies: Option>, diff --git a/src/core/version_control/common.rs b/src/core/version_control/common.rs index 80832c6..c1d82c9 100644 --- a/src/core/version_control/common.rs +++ b/src/core/version_control/common.rs @@ -76,7 +76,7 @@ pub fn resolve_element_path( trace!("Resolved shot: {}", shot) }, None => { - return Err(ExportError::Message("Tried to resolve the path of a shot_local element, but we are not in a shot context".into())) + return Err(ExportError::Message(format!("Tried to resolve the path of a shot_local element '{}', but we are not in a shot context", element_name).into())) }, } } diff --git a/src/core/version_control/mod.rs b/src/core/version_control/mod.rs index c977bca..1d5ae83 100644 --- a/src/core/version_control/mod.rs +++ b/src/core/version_control/mod.rs @@ -7,6 +7,7 @@ use direct::VersionControlConfigDirect; use enum_dispatch::enum_dispatch; use serde::{Deserialize, Serialize}; use symlink::VersionControlConfigSymlink; +use ts_rs::TS; use versioned_directories::VersionControlConfigVersionedDirectories; use super::{commands::ExportArgs, element::resolved_element_data::ResolvedElementData, project}; @@ -28,7 +29,7 @@ pub struct ExportResult { pub script: String, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, TS)] pub struct VersionControlFile { pub path: String, pub version: String, diff --git a/ui/src/App.tsx b/ui/src/App.tsx index acf1a3b..e8095bc 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,6 +1,6 @@ import { createResource, type Component, Show, Switch, Match, createSignal, For } from 'solid-js'; -import { getSummary, getAssetTree, doCreate, get, saveChanges } from './api'; +import { getSummary, getAssetTree, doCreate, get, saveChanges, listElements, resolveElements } from './api'; import { Button } from './components/ui/button'; import { ColorModeProvider } from '@kobalte/core/color-mode'; @@ -12,6 +12,11 @@ import { TextField, TextFieldInput } from './components/ui/text-field'; import { Callout, CalloutContent, CalloutTitle } from './components/ui/callout'; import { Menubar, MenubarContent, MenubarItem, MenubarItemLabel, MenubarMenu, MenubarSeparator, MenubarShortcut, MenubarSub, MenubarSubContent, MenubarSubTrigger, MenubarTrigger } from './components/ui/menubar'; import { comma } from 'postcss/lib/list'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './components/ui/card'; +import { Accordion } from '@kobalte/core/accordion'; +import { AccordionContent, AccordionItem, AccordionTrigger } from './components/ui/accordion'; +import { Label } from './components/ui/label'; +import { Tooltip, TooltipContent, TooltipTrigger } from './components/ui/tooltip'; interface CommandEntry { command: string, @@ -29,6 +34,10 @@ const App: Component = () => { const [category, setCategory] = createSignal(""); const [newAssetName, setNewAssetname] = createSignal("") + const [selectedPath, setSelectedPath] = createSignal("") + + const [resolvedElements] = createResource(selectedPath, resolveElements) + const [error, setError] = createSignal("") const newAssetPath = () => { @@ -56,150 +65,220 @@ const App: Component = () => { return ( - -
-

{info()!.display_name}

-

{info()!.identifier}

-
- - - File - - { - saveChanges() - } - }>Save Changes - - - - Edit - - { - openCreateCategoryDialog("") - }}>Create Category - { - openCreateAssetDialog("") - }}>Create Asset - - - Undo - - - - - - - -
-
-
- - ( - <> - console.log(path)}>Inspect - { - entry.type == "Category" && Object.entries(entry.children).every((e) => e[1]?.type != "Category") ? { openCreateAssetDialog(path) }} >Add Asset : <> - } - { - entry.type == "Category" && Object.entries(entry.children).every((e) => e[1]?.type == "Category") ? { openCreateCategoryDialog(path) }} >Add Subcategory : <> +
+ + +
+

{info()!.display_name}

+

{info()!.identifier}

+
+ + + File + + { + saveChanges() } - - )}> -
-
- - - - {category()} - - Add an asset to the category - - -
- - - -
- {newAssetPath()} + }>Save Changes + + + + Edit + + { + openCreateCategoryDialog("") + }}>Create Category + { + openCreateAssetDialog("") + }}>Create Asset + + + Undo + + + + + + + + +
+
+
+ + setSelectedPath(path)} assets={assets} contextMenuBuilder={(path, entry) => ( + <> + console.log(path)}>Inspect + { + entry.type == "Category" && Object.entries(entry.children).every((e) => e[1]?.type != "Category") ? { openCreateAssetDialog(path) }} >Add Asset : <> + } + { + entry.type == "Category" && Object.entries(entry.children).every((e) => e[1]?.type == "Category") ? { openCreateCategoryDialog(path) }} >Add Subcategory : <> + } + + )}>
- - - Warning - - {error()} - - + + + + + {selectedPath().split("/").slice(-1)[0]} + + + {selectedPath()} + + + + {(item) => { + let entry = resolvedElements()?.result[item[0]]! + let versions = entry.versions + + return + + { +
+ +
+ + + + + + + + + + +
+
+ } +
+ + + {(version) => + +
+
+ + + + + +
+
+ + } +
+ +
+
; + } + }
+
+
+
+ +
- -
+ + + + {category()} + + Add an asset to the category + + +
+ + + +
+ {newAssetPath()} +
+
+ + + Warning + + {error()} + + + + + - -
-
- - - - - - {category()} - - Add a new asset category - - -
- - - -
- {newAssetPath()} + }>Add Asset + + +
+ + + + + + {category()} + + Add a new asset category + + +
+ + + +
+ {newAssetPath()} +
-
- - - Warning - - {error()} - - - - - - - -
+ }>Add Category + + +
+
); diff --git a/ui/src/api.ts b/ui/src/api.ts index e1fdeb1..1233bf6 100644 --- a/ui/src/api.ts +++ b/ui/src/api.ts @@ -1,4 +1,4 @@ -import { AssetTreeCategory, ListAssetsResult, ListElementsResult, ListExportFormatsResult, ListShotsResult, SetupResult, SummaryResponse } from "./bindings/bindings_gen"; +import { AssetTreeCategory, ListAssetsResult, ListElementsResult, ListExportFormatsResult, ListShotsResult, ResolveElementsResult, SetupResult, SummaryResponse } from "./bindings/bindings_gen"; declare global { @@ -152,3 +152,10 @@ export async function listShots(): Promise { let result = await get("api/v1/command/list_shots") return await result.json() as ListShotsResult } + +export async function resolveElements(asset: string): Promise { + let result = await get("api/v1/command/resolve_elements", { + "asset": asset + }) + return await result.json() as ResolveElementsResult +} \ No newline at end of file diff --git a/ui/src/bindings/bindings_gen.ts b/ui/src/bindings/bindings_gen.ts index 13df221..fd9dc9a 100644 --- a/ui/src/bindings/bindings_gen.ts +++ b/ui/src/bindings/bindings_gen.ts @@ -1,4 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ResolvedElementResult } from "../../../bindings/ResolvedElementResult"; + +export type AssetLoadStep = { asset: string, element: string, script: string, file: string, file_type: string, version: string, }; export type AssetTreeCategory = { children: { [key in string]?: AssetTreeEntry }, }; @@ -12,6 +15,10 @@ export type ListExportFormatsResult = { formats: Array, }; export type ListShotsResult = { shots: Array, }; -export type SetupResult = { asset: string, department: string, path: string, file_name: string, shot: string | null, }; +export type LoadAssetsResult = { results: Array, }; + +export type ResolveElementsResult = { result: { [key in string]?: ResolvedElementResult }, }; + +export type SetupResult = { asset: string, department: string, folder: string, file_name: string, path: string, shot: string | null, }; export type SummaryResponse = { display_name: string, identifier: string, assets_flat: Array, departments: Array, }; diff --git a/ui/src/components/organisms/asset_tree.tsx b/ui/src/components/organisms/asset_tree.tsx index 0b2a98e..3907b14 100644 --- a/ui/src/components/organisms/asset_tree.tsx +++ b/ui/src/components/organisms/asset_tree.tsx @@ -16,6 +16,7 @@ import { TooltipContent, TooltipTrigger } from "../ui/tooltip"; export interface AssetTreeProps { contextMenuBuilder?(name: string, entry: AssetTreeEntry): any categoryContextMenuBuilder?(name: string): any + onPathClicked?(path: string): any assets: Resource } @@ -23,6 +24,7 @@ const AssetTree = (props: AssetTreeProps) => { let closedPaths: string[] = []; let contextMenuBuilder = props.contextMenuBuilder; + let onClick = props.onPathClicked; const assetEntry: Component<{ entry_name: string, entry: AssetTreeEntry, current_path: string }> = (props) => { @@ -32,7 +34,9 @@ const AssetTree = (props: AssetTreeProps) => {
- + From 45ddf1b0fb0785c3ede7e9215e68fe1cffc1958d Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Sun, 23 Mar 2025 14:13:17 +1030 Subject: [PATCH 4/9] implementing ingest workflow --- Cargo.lock | 54 +++- Cargo.toml | 1 + example/basic/manifest.yaml | 18 ++ .../basic/scripts/ingest/ingest_audio_file.js | 4 + .../ingest/impactGlass_light_000.ogg | Bin 0 -> 6526 bytes .../ingest/impactGlass_light_001.ogg | Bin 0 -> 6544 bytes .../ingest/impactGlass_light_002.ogg | Bin 0 -> 6562 bytes .../ingest/impactGlass_light_003.ogg | Bin 0 -> 6618 bytes .../ingest/impactGlass_light_004.ogg | Bin 0 -> 6641 bytes .../impactGlass_light_000 - License.txt | 23 ++ .../impactGlass_light_001 - License.txt | 23 ++ .../impactGlass_light_002 - License.txt | 23 ++ .../impactGlass_light_003 - License.txt | 23 ++ .../impactGlass_light_004 - License.txt | 23 ++ .../sound/impactGlassLight/ingest/sources.md | 5 + src/core/commands/command_ingest.rs | 227 ++++++++++++++ src/core/commands/command_setup.rs | 4 +- src/core/commands/mod.rs | 4 + src/gui/mod.rs | 48 ++- ui/src/App.tsx | 13 +- ui/src/api.ts | 15 +- ui/src/bindings/bindings_gen.ts | 2 + ui/src/index.tsx | 2 + ui/src/pages/dialogs/ingest.tsx | 279 ++++++++++++++++++ 24 files changed, 782 insertions(+), 9 deletions(-) create mode 100644 example/basic/scripts/ingest/ingest_audio_file.js create mode 100644 example/basic/setup/asset/sound/impactGlassLight/ingest/impactGlass_light_000.ogg create mode 100644 example/basic/setup/asset/sound/impactGlassLight/ingest/impactGlass_light_001.ogg create mode 100644 example/basic/setup/asset/sound/impactGlassLight/ingest/impactGlass_light_002.ogg create mode 100644 example/basic/setup/asset/sound/impactGlassLight/ingest/impactGlass_light_003.ogg create mode 100644 example/basic/setup/asset/sound/impactGlassLight/ingest/impactGlass_light_004.ogg create mode 100644 example/basic/setup/asset/sound/impactGlassLight/ingest/license/impactGlass_light_000 - License.txt create mode 100644 example/basic/setup/asset/sound/impactGlassLight/ingest/license/impactGlass_light_001 - License.txt create mode 100644 example/basic/setup/asset/sound/impactGlassLight/ingest/license/impactGlass_light_002 - License.txt create mode 100644 example/basic/setup/asset/sound/impactGlassLight/ingest/license/impactGlass_light_003 - License.txt create mode 100644 example/basic/setup/asset/sound/impactGlassLight/ingest/license/impactGlass_light_004 - License.txt create mode 100644 example/basic/setup/asset/sound/impactGlassLight/ingest/sources.md create mode 100644 src/core/commands/command_ingest.rs create mode 100644 ui/src/pages/dialogs/ingest.tsx diff --git a/Cargo.lock b/Cargo.lock index 8062d1a..c19d793 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "android-tzdata" @@ -349,6 +349,7 @@ dependencies = [ "similar", "stderrlog", "tao", + "time", "ts-rs", "url", "wry", @@ -461,6 +462,15 @@ dependencies = [ "syn 2.0.79", ] +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + [[package]] name = "derive_more" version = "0.99.18" @@ -1343,6 +1353,12 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-traits" version = "0.2.19" @@ -1373,6 +1389,15 @@ dependencies = [ "syn 2.0.79", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "objc" version = "0.2.7" @@ -1794,6 +1819,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.20" @@ -2370,6 +2401,27 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.3.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d9c75b47bdff86fa3334a3db91356b8d7d86a9b839dab7d0bdc5c3d3a077618" +dependencies = [ + "deranged", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + [[package]] name = "tinyvec" version = "1.8.0" diff --git a/Cargo.toml b/Cargo.toml index e77bac3..8531bb9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ serde_json = "1.0.132" serde_yaml = "0.9.34" stderrlog = "0.6.0" tao = "0.30.3" +time = { version = "0.3.40", features = ["local-offset"]} ts-rs = { version = "10.0.0", features = ["serde-compat"] } url = "2.5.2" wry = { version = "0.46.3", features = ["linux-body"] } diff --git a/example/basic/manifest.yaml b/example/basic/manifest.yaml index f31c0bc..4fa6155 100644 --- a/example/basic/manifest.yaml +++ b/example/basic/manifest.yaml @@ -12,6 +12,9 @@ programs: .glb: import_glb_mesh.py .mesh.blend: import_blend_library_mesh.py .shadergraph.blend: import_blend_shadergraph.py + ingest: + exports: + .ogg: ingest_audio_file.js inkscape: exports: .2x.png: export_png_1024.py @@ -55,6 +58,11 @@ departments: - .mesh.blend - .shadergraph.blend - .animbake.abc + sound: + programs: + ingest: + exports: + - .ogg assets: 2d: @@ -88,6 +96,16 @@ assets: - test - defaultCubeA: {} - defaultCubeB: {} + audio: + sfx: + impact: + - impactGlassLight: + departments: + sound: + - varA + - varB + - varC + - varD shots: '103': diff --git a/example/basic/scripts/ingest/ingest_audio_file.js b/example/basic/scripts/ingest/ingest_audio_file.js new file mode 100644 index 0000000..5f41837 --- /dev/null +++ b/example/basic/scripts/ingest/ingest_audio_file.js @@ -0,0 +1,4 @@ +(data) => { + console.log('Ingest running user script!') + console.log(data) +}; diff --git a/example/basic/setup/asset/sound/impactGlassLight/ingest/impactGlass_light_000.ogg b/example/basic/setup/asset/sound/impactGlassLight/ingest/impactGlass_light_000.ogg new file mode 100644 index 0000000000000000000000000000000000000000..d59cffe03da391e6e7b802185c7990f2f2504557 GIT binary patch literal 6526 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 B%uWCR literal 0 HcmV?d00001 diff --git a/example/basic/setup/asset/sound/impactGlassLight/ingest/impactGlass_light_001.ogg b/example/basic/setup/asset/sound/impactGlassLight/ingest/impactGlass_light_001.ogg new file mode 100644 index 0000000000000000000000000000000000000000..49535102febbbb0370e0c614109cc06808db7ee0 GIT binary patch literal 6544 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 literal 0 HcmV?d00001 diff --git a/example/basic/setup/asset/sound/impactGlassLight/ingest/impactGlass_light_002.ogg b/example/basic/setup/asset/sound/impactGlassLight/ingest/impactGlass_light_002.ogg new file mode 100644 index 0000000000000000000000000000000000000000..4ec4d868be35048578a02ec5b4a41045c21be843 GIT binary patch literal 6562 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 PD>OAQmd6A&wG#ggpt;Vp literal 0 HcmV?d00001 diff --git a/example/basic/setup/asset/sound/impactGlassLight/ingest/impactGlass_light_003.ogg b/example/basic/setup/asset/sound/impactGlassLight/ingest/impactGlass_light_003.ogg new file mode 100644 index 0000000000000000000000000000000000000000..3eef0704a9075d44c63e365f425f7db7df3498dc GIT binary patch literal 6618 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 CiTMlw literal 0 HcmV?d00001 diff --git a/example/basic/setup/asset/sound/impactGlassLight/ingest/license/impactGlass_light_000 - License.txt b/example/basic/setup/asset/sound/impactGlassLight/ingest/license/impactGlass_light_000 - License.txt new file mode 100644 index 0000000..4890fe5 --- /dev/null +++ b/example/basic/setup/asset/sound/impactGlassLight/ingest/license/impactGlass_light_000 - License.txt @@ -0,0 +1,23 @@ + + + Impact Sounds (1.0) + + Created/distributed by Kenney (www.kenney.nl) + Creation date: 19-12-2019 + + ------------------------------ + + License: (Creative Commons Zero, CC0) + http://creativecommons.org/publicdomain/zero/1.0/ + + This content is free to use in personal, educational and commercial projects. + Support us by crediting Kenney or www.kenney.nl (this is not mandatory) + + ------------------------------ + + Donate: http://support.kenney.nl + Request: http://request.kenney.nl + Patreon: http://patreon.com/kenney/ + + Follow on Twitter for updates: + http://twitter.com/KenneyNL \ No newline at end of file diff --git a/example/basic/setup/asset/sound/impactGlassLight/ingest/license/impactGlass_light_001 - License.txt b/example/basic/setup/asset/sound/impactGlassLight/ingest/license/impactGlass_light_001 - License.txt new file mode 100644 index 0000000..4890fe5 --- /dev/null +++ b/example/basic/setup/asset/sound/impactGlassLight/ingest/license/impactGlass_light_001 - License.txt @@ -0,0 +1,23 @@ + + + Impact Sounds (1.0) + + Created/distributed by Kenney (www.kenney.nl) + Creation date: 19-12-2019 + + ------------------------------ + + License: (Creative Commons Zero, CC0) + http://creativecommons.org/publicdomain/zero/1.0/ + + This content is free to use in personal, educational and commercial projects. + Support us by crediting Kenney or www.kenney.nl (this is not mandatory) + + ------------------------------ + + Donate: http://support.kenney.nl + Request: http://request.kenney.nl + Patreon: http://patreon.com/kenney/ + + Follow on Twitter for updates: + http://twitter.com/KenneyNL \ No newline at end of file diff --git a/example/basic/setup/asset/sound/impactGlassLight/ingest/license/impactGlass_light_002 - License.txt b/example/basic/setup/asset/sound/impactGlassLight/ingest/license/impactGlass_light_002 - License.txt new file mode 100644 index 0000000..4890fe5 --- /dev/null +++ b/example/basic/setup/asset/sound/impactGlassLight/ingest/license/impactGlass_light_002 - License.txt @@ -0,0 +1,23 @@ + + + Impact Sounds (1.0) + + Created/distributed by Kenney (www.kenney.nl) + Creation date: 19-12-2019 + + ------------------------------ + + License: (Creative Commons Zero, CC0) + http://creativecommons.org/publicdomain/zero/1.0/ + + This content is free to use in personal, educational and commercial projects. + Support us by crediting Kenney or www.kenney.nl (this is not mandatory) + + ------------------------------ + + Donate: http://support.kenney.nl + Request: http://request.kenney.nl + Patreon: http://patreon.com/kenney/ + + Follow on Twitter for updates: + http://twitter.com/KenneyNL \ No newline at end of file diff --git a/example/basic/setup/asset/sound/impactGlassLight/ingest/license/impactGlass_light_003 - License.txt b/example/basic/setup/asset/sound/impactGlassLight/ingest/license/impactGlass_light_003 - License.txt new file mode 100644 index 0000000..4890fe5 --- /dev/null +++ b/example/basic/setup/asset/sound/impactGlassLight/ingest/license/impactGlass_light_003 - License.txt @@ -0,0 +1,23 @@ + + + Impact Sounds (1.0) + + Created/distributed by Kenney (www.kenney.nl) + Creation date: 19-12-2019 + + ------------------------------ + + License: (Creative Commons Zero, CC0) + http://creativecommons.org/publicdomain/zero/1.0/ + + This content is free to use in personal, educational and commercial projects. + Support us by crediting Kenney or www.kenney.nl (this is not mandatory) + + ------------------------------ + + Donate: http://support.kenney.nl + Request: http://request.kenney.nl + Patreon: http://patreon.com/kenney/ + + Follow on Twitter for updates: + http://twitter.com/KenneyNL \ No newline at end of file diff --git a/example/basic/setup/asset/sound/impactGlassLight/ingest/license/impactGlass_light_004 - License.txt b/example/basic/setup/asset/sound/impactGlassLight/ingest/license/impactGlass_light_004 - License.txt new file mode 100644 index 0000000..4890fe5 --- /dev/null +++ b/example/basic/setup/asset/sound/impactGlassLight/ingest/license/impactGlass_light_004 - License.txt @@ -0,0 +1,23 @@ + + + Impact Sounds (1.0) + + Created/distributed by Kenney (www.kenney.nl) + Creation date: 19-12-2019 + + ------------------------------ + + License: (Creative Commons Zero, CC0) + http://creativecommons.org/publicdomain/zero/1.0/ + + This content is free to use in personal, educational and commercial projects. + Support us by crediting Kenney or www.kenney.nl (this is not mandatory) + + ------------------------------ + + Donate: http://support.kenney.nl + Request: http://request.kenney.nl + Patreon: http://patreon.com/kenney/ + + Follow on Twitter for updates: + http://twitter.com/KenneyNL \ No newline at end of file diff --git a/example/basic/setup/asset/sound/impactGlassLight/ingest/sources.md b/example/basic/setup/asset/sound/impactGlassLight/ingest/sources.md new file mode 100644 index 0000000..3901251 --- /dev/null +++ b/example/basic/setup/asset/sound/impactGlassLight/ingest/sources.md @@ -0,0 +1,5 @@ + - `impactGlass_light_000.ogg`: https://kenney.nl/assets/impact-sounds at 2025-03-23 14:12:15.092518611 +10:30:00 + - `impactGlass_light_001.ogg`: https://kenney.nl/assets/impact-sounds at 2025-03-23 14:12:15.220441588 +10:30:00 + - `impactGlass_light_002.ogg`: https://kenney.nl/assets/impact-sounds at 2025-03-23 14:12:15.346174524 +10:30:00 + - `impactGlass_light_003.ogg`: https://kenney.nl/assets/impact-sounds at 2025-03-23 14:12:15.471324395 +10:30:00 + - `impactGlass_light_004.ogg`: https://kenney.nl/assets/impact-sounds at 2025-03-23 14:12:15.597987283 +10:30:00 diff --git a/src/core/commands/command_ingest.rs b/src/core/commands/command_ingest.rs new file mode 100644 index 0000000..791c718 --- /dev/null +++ b/src/core/commands/command_ingest.rs @@ -0,0 +1,227 @@ +use std::{fmt::format, fs::OpenOptions, path::PathBuf, sync::RwLock, time::SystemTime}; + +use clap::{command, Args}; +use log::{error, info, warn}; +use time::OffsetDateTime; +use ts_rs::TS; + +use super::{ + args::CommonArgs, command_setup::SetupArgs, error::CommandError, Command, CommandContext, +}; +use crate::core::{program, project::Project, shot::shot_resolver::ShotResolver}; +use serde::{Deserialize, Serialize}; +use std::io::prelude::*; + +#[derive(Debug, Args, Serialize, Deserialize)] +pub struct IngestArgs { + #[command(flatten)] + #[serde(flatten)] + common: CommonArgs, + + #[arg(long)] + target_format: Option, + + #[clap(long)] + file: Option, + + #[clap(long)] + license: Option, + + #[clap(long)] + source: Option, +} + +#[derive(Debug, Args, Serialize, Deserialize, TS)] +#[ts(export, export_to = "../ui/src/bindings/bindings_gen.ts")] +pub struct IngestResult { + original_file: Option, + new_file: Option, + new_license_file: Option, + script: Option, +} + +impl Command for IngestArgs { + fn execute( + self, + project: &RwLock, + context: CommandContext, + ) -> Result, CommandError> { + if self.common.asset.is_none() || self.common.department.is_none() { + return Err(CommandError::InvalidArguments); + } + + let setup = SetupArgs { + common: self.common, + file_format: "".to_string(), + dry: true, + }; + + let result = Command::execute(setup, project, context); + + let folder = match result { + Ok(val) => match val { + Some(result) => match result { + serde_json::Value::Object(map) => Some( + map.get("folder") + .unwrap() + .clone() + .as_str() + .unwrap() + .to_string(), + ), + _ => None, + }, + None => None, + }, + Err(_) => None, + }; + + let mut result = IngestResult { + original_file: self.file.clone(), + new_file: None, + script: None, + new_license_file: None, + }; + + match folder { + Some(folder) => { + let mut path = PathBuf::from(folder); + path.push("ingest"); + + _ = std::fs::create_dir_all(&path); + + match self.file.clone() { + Some(file) => { + let mut file_path = path.clone(); + let original = PathBuf::from(file.clone()); + file_path.push(original.file_name().unwrap()); + + match std::fs::copy(&original, &file_path) { + Ok(_) => info!("Copied file to {}", file_path.to_str().unwrap()), + Err(err) => { + return Err(CommandError::Message(format!( + "Failed to copy file!: {:?}", + err + ))) + } + } + + result.new_file = Some(file_path.to_str().unwrap().to_string()); + + match self.source { + Some(source) => { + let mut sources_path = path.clone(); + sources_path.push("sources.md"); + + let mut file = OpenOptions::new() + .write(true) + .append(true) + .create(true) + .open(&sources_path) + .unwrap(); + + + let now = OffsetDateTime::now_local().unwrap(); + + if let Err(e) = writeln!(file, " - `{}`: {} at {}", original.file_name().unwrap().to_str().unwrap(), source, now) { + eprintln!("Couldn't write to file: {}", e); + } else { + info!("Wrote source down in {}", sources_path.to_str().unwrap()) + } + } + None => warn!("No source for this ingest has been specified! continuing regardless"), + } + } + None => (), + } + + match self.license { + Some(license) => { + let mut license_path = path.clone(); + license_path.push("license"); + _ = std::fs::create_dir_all(&license_path); + + let original = PathBuf::from(license.clone()); + let file_name = PathBuf::from(self.file.unwrap()); + let file_name = file_name.file_stem().unwrap().to_str().unwrap(); + let license_file_name = original.file_name().unwrap().to_str().unwrap(); + license_path.push(format!("{} - {}", file_name, license_file_name)); + + info!( + "Copying license from {} to: {}", + license, + license_path.to_str().unwrap() + ); + + match std::fs::copy(&original, &license_path) { + Ok(_) => { + info!("Copied license file to {}", license_path.to_str().unwrap()); + result.new_license_file = + Some(license_path.to_str().unwrap().to_string()); + } + Err(err) => { + return Err(CommandError::Message(format!( + "Failed to copy license file!: {:?}", + err + ))) + } + } + } + None => (), + } + + let project = project.read().unwrap(); + let program = project.programs.get("ingest"); + + let program = match program { + Some(program) => program, + None => { + return Err(CommandError::Message( + "No 'ingest' program specified".to_string(), + )) + } + }; + + match self.target_format { + Some(file_format) => { + let script = program.exports.get(&file_format); + + match script { + Some(script) => { + info!("Ingesting with script: {}", script); + + let mut script_path = project.get_root_directory(); + script_path.push("scripts"); + script_path.push("ingest"); + script_path.push(script); + + match std::fs::read_to_string(script_path) { + Ok(text) => { + result.script = Some(text); + } + Err(_) => { + warn!("Script file not found!"); + } + } + } + None => { + return Err(CommandError::Message(format!( + "No script has been specified for the format {}", + file_format + ))) + } + } + } + None => warn!("No target format was specified, so we cant run any script! continuing on regardless"), + } + } + None => { + return Err(CommandError::Message( + "Failed to get setup folder".to_string(), + )) + } + } + + return Ok(Some(serde_json::to_value(result).unwrap())); + } +} diff --git a/src/core/commands/command_setup.rs b/src/core/commands/command_setup.rs index 9a7760e..4d9770d 100644 --- a/src/core/commands/command_setup.rs +++ b/src/core/commands/command_setup.rs @@ -13,10 +13,10 @@ use super::{args::CommonArgs, error::CommandError, Command, CommandContext}; pub struct SetupArgs { #[command(flatten)] #[serde(flatten)] - common: CommonArgs, + pub common: CommonArgs, #[arg(short, long)] - file_format: String, + pub file_format: String, #[arg(long)] pub dry: bool, diff --git a/src/core/commands/mod.rs b/src/core/commands/mod.rs index 8cecb7d..9ede73b 100644 --- a/src/core/commands/mod.rs +++ b/src/core/commands/mod.rs @@ -2,6 +2,7 @@ mod command_create; mod command_dialog; mod command_export; mod command_get_asset_tree; +mod command_ingest; mod command_list_assets; mod command_list_elements; mod command_list_export_formats; @@ -29,6 +30,7 @@ pub use command_dialog::DialogOptions; pub use command_export::ExportArgs; use command_get_asset_tree::GetAssetTreeArgs; +use command_ingest::IngestArgs; use command_list_assets::ListAssetsArgs; use command_list_elements::ListElementsArgs; use command_list_export_formats::ListExportFormatsArgs; @@ -97,6 +99,8 @@ pub enum CommandType { LoadAssets(LoadAssetsArgs), Save(SaveArgs), + + Ingest(IngestArgs), } pub fn write_command_result(result: serde_json::Value) { diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 691dd73..6e28832 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -3,11 +3,15 @@ mod embedded_files; mod router; mod routes; -use std::sync::{Arc, RwLock}; +use std::{ + clone, + sync::{Arc, RwLock}, +}; -use log::{debug, info, trace}; +use log::{debug, info, trace, warn, Log}; use router::{ApiEntry, RequestContext}; use routes::register_routes; +use serde_json::json; use tao::{ dpi::{LogicalSize, PhysicalPosition, Position, Size}, event::{Event, WindowEvent}, @@ -15,7 +19,7 @@ use tao::{ window::WindowBuilder, }; -use wry::WebViewBuilder; +use wry::{WebView, WebViewBuilder}; use crate::core::commands::DialogOptions; @@ -66,6 +70,8 @@ pub fn gui(project: crate::core::project::Project, dialog_options: Option>> = Arc::new(RwLock::new(None)); + if let Some(options) = dialog_options { page += options.path.as_str(); title = options.title; @@ -97,10 +103,38 @@ pub fn gui(project: crate::core::project::Project, dialog_options: Option match webview_ref.clone().read() { + Ok(webview) => { + if webview.is_some() { + let _ = webview.as_ref().unwrap().evaluate_script( + &format!( + "window.postMessage({});", + json!({ + "type": "files_dropped", + "data": paths + }) + ) + .to_string(), + ); + } else { + warn!("Failed to get webview") + } + } + Err(_) => warn!("Failed to read"), + }, + _ => (), + } + + true + }) .with_asynchronous_custom_protocol( "conduct".into(), move |_webview_id, request, responder| { @@ -143,6 +177,12 @@ pub fn gui(project: crate::core::project::Project, dialog_options: Option { *control_flow = ControlFlow::Exit; } - _ => (), + event => {} }, Event::UserEvent(event) => match event { UserWindowEvent::Exit => { diff --git a/ui/src/App.tsx b/ui/src/App.tsx index e8095bc..de3b491 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -129,6 +129,8 @@ const App: Component = () => { {selectedPath()} + + {(item) => { @@ -179,6 +181,15 @@ const App: Component = () => { } } +
+ +
+
+ +
@@ -280,7 +291,7 @@ const App: Component = () => {
- + ); }; diff --git a/ui/src/api.ts b/ui/src/api.ts index 1233bf6..fd2f60b 100644 --- a/ui/src/api.ts +++ b/ui/src/api.ts @@ -1,4 +1,4 @@ -import { AssetTreeCategory, ListAssetsResult, ListElementsResult, ListExportFormatsResult, ListShotsResult, ResolveElementsResult, SetupResult, SummaryResponse } from "./bindings/bindings_gen"; +import { AssetTreeCategory, IngestResult, ListAssetsResult, ListElementsResult, ListExportFormatsResult, ListShotsResult, ResolveElementsResult, SetupResult, SummaryResponse } from "./bindings/bindings_gen"; declare global { @@ -158,4 +158,17 @@ export async function resolveElements(asset: string): Promise { + let result = await get("api/v1/command/ingest", { + "asset": asset, + "element": element, + "department": department, + "file": file, + "target_format": target_format, + "license": license, + "source": source + }) + return await result.json() as IngestResult } \ No newline at end of file diff --git a/ui/src/bindings/bindings_gen.ts b/ui/src/bindings/bindings_gen.ts index fd9dc9a..abc4ea6 100644 --- a/ui/src/bindings/bindings_gen.ts +++ b/ui/src/bindings/bindings_gen.ts @@ -7,6 +7,8 @@ export type AssetTreeCategory = { children: { [key in string]?: AssetTreeEntry } export type AssetTreeEntry = { "type": "Asset" } | { "type": "Category" } & AssetTreeCategory; +export type IngestResult = { original_file: string | null, new_file: string | null, new_license_file: string | null, script: string | null, }; + export type ListAssetsResult = { assets: Array, }; export type ListElementsResult = { elements: Array, }; diff --git a/ui/src/index.tsx b/ui/src/index.tsx index 7a5319b..d486e1f 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -9,6 +9,7 @@ import DialogLoadAsset from './pages/dialogs/load_asset'; import { ColorModeProvider } from '@kobalte/core/color-mode'; import { Component } from 'solid-js'; import DialogExport from './pages/dialogs/export'; +import DialogIngest from './pages/dialogs/ingest'; const root = document.getElementById('root'); @@ -31,4 +32,5 @@ render(() => ( + ), root!); diff --git a/ui/src/pages/dialogs/ingest.tsx b/ui/src/pages/dialogs/ingest.tsx new file mode 100644 index 0000000..02a9670 --- /dev/null +++ b/ui/src/pages/dialogs/ingest.tsx @@ -0,0 +1,279 @@ + +import { Combobox } from '@kobalte/core/*'; +import { useSearchParams } from '@solidjs/router'; +import { createResource, createSignal, For, Show, type Component } from 'solid-js'; +import { doIngest, getSummary, listElements, listExportFormats } from '~/api'; +import { Button } from '~/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '~/components/ui/card'; +import { Label } from '~/components/ui/label'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '~/components/ui/select'; +import { TextField, TextFieldInput } from '~/components/ui/text-field'; + +const DialogIngest: Component = () => { + const [files, setFiles] = createSignal([]) + const [selectedDepartment, setSelectedDepartment] = createSignal() + + const [info] = createResource(getSummary); + + const [license, setLicenseFiles] = createSignal([]) + + const [step, setStep] = createSignal("select_files") + + const [searchParams, setSearchParams] = useSearchParams(); + const targetAsset = () => searchParams.asset; + + const [elements] = createResource(() => listElements(targetAsset() as string, selectedDepartment())); + const [elementSelections, setElementSelections] = createSignal({}); + + const [formats] = createResource(selectedDepartment, (department) => listExportFormats(department, "ingest")); + const [formatSelections, setFormatSelections] = createSignal({}); + + const [source, setSource] = createSignal(""); + + const [isIngesting, setIsIngesting] = createSignal(false); + const [ingestStatus, setIngestStatus] = createSignal(""); + + + addEventListener("message", (event) => { + console.log("Received message!") + console.log(event) + + if (event.data.type == "files_dropped") { + + if (step() == "select_files") { + setFiles(event.data.data) + setStep("select_license") + } else if (step() == "select_license") { + setLicenseFiles([event.data.data[0]]) + } + + if (license().length > 0) { + setStep("add_source") + } + } + }); + + function onSourceChanged(value: string) { + setSource(value) + setStep("finish") + } + + + const delay = (ms: any) => new Promise(res => setTimeout(res, ms)); + + async function runIngest() { + setIsIngesting(true) + + for (var file of files()) { + try { + let element = elementSelections()[file] + let asset = targetAsset() as string + setIngestStatus(file + " -> " + asset + " " + "(" + element + ")") + let format = formatSelections()[file] + let dept = selectedDepartment() + let license_file = license()[0]; + let result = await doIngest(targetAsset() as string, element, dept!, file, format, license_file, source()) + + + if (result['script'] != null) { + setIngestStatus("Running user script") + let data = { + ...result, + "element": element, + "asset": asset, + "department": dept, + "format": format + } + + let obj = eval(result['script']) + + console.log(obj) + obj(data) + } + + + console.log(result) + setIngestStatus("Ingested: " + JSON.stringify(result)) + + } catch (error) { + setIngestStatus("Error: " + JSON.stringify(error)) + await delay(5000) + } + } + + setIngestStatus("Done!") + } + + return ( + +
+ +
+
+ + + Ingesting + {ingestStatus()} + + +
+
+
+ +
+
+ + + Ingest Files to {targetAsset()} + + + Drag and drop files to ingest + + + + + + + 0}> + + + Selected Files + + + + { + (e) => { + return
+
+ + +
+
+ + + + + +
+
+ } + } +
+ 0}> +
+ +
+
+
+
+
+ + + + + License + + Now, drag and drop the license associated with these files + + + + + { + (e) => { + return
{e}
+ } + } +
+
+
+ +
+
+ + + + + Source + + Please enter where these files were sourced from + + + + + + + + + + +
+ + + + Import + + + + + + +
+
+
+
+ ); +}; + + +export default DialogIngest; From 6ef7ae4a925431709faa3cbfcbce53d31de20325 Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Sun, 23 Mar 2025 14:21:56 +1030 Subject: [PATCH 5/9] add back button --- .../sound/impactGlassLight/ingest/sources.md | 1 + src/core/commands/command_ingest.rs | 5 ++--- ui/src/pages/dialogs/ingest.tsx | 22 ++++++++++++++++--- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/example/basic/setup/asset/sound/impactGlassLight/ingest/sources.md b/example/basic/setup/asset/sound/impactGlassLight/ingest/sources.md index 3901251..a03954a 100644 --- a/example/basic/setup/asset/sound/impactGlassLight/ingest/sources.md +++ b/example/basic/setup/asset/sound/impactGlassLight/ingest/sources.md @@ -3,3 +3,4 @@ - `impactGlass_light_002.ogg`: https://kenney.nl/assets/impact-sounds at 2025-03-23 14:12:15.346174524 +10:30:00 - `impactGlass_light_003.ogg`: https://kenney.nl/assets/impact-sounds at 2025-03-23 14:12:15.471324395 +10:30:00 - `impactGlass_light_004.ogg`: https://kenney.nl/assets/impact-sounds at 2025-03-23 14:12:15.597987283 +10:30:00 + - `impactGlass_light_001.ogg`: https://kenney.nl/assets/impact-sounds at 2025-03-23 14:19:38.710524974 +10:30:00 diff --git a/src/core/commands/command_ingest.rs b/src/core/commands/command_ingest.rs index 791c718..8169b25 100644 --- a/src/core/commands/command_ingest.rs +++ b/src/core/commands/command_ingest.rs @@ -119,8 +119,7 @@ impl Command for IngestArgs { .create(true) .open(&sources_path) .unwrap(); - - + let now = OffsetDateTime::now_local().unwrap(); if let Err(e) = writeln!(file, " - `{}`: {} at {}", original.file_name().unwrap().to_str().unwrap(), source, now) { @@ -144,7 +143,7 @@ impl Command for IngestArgs { let original = PathBuf::from(license.clone()); let file_name = PathBuf::from(self.file.unwrap()); let file_name = file_name.file_stem().unwrap().to_str().unwrap(); - let license_file_name = original.file_name().unwrap().to_str().unwrap(); + let license_file_name = original.file_name().unwrap().to_str().unwrap(); license_path.push(format!("{} - {}", file_name, license_file_name)); info!( diff --git a/ui/src/pages/dialogs/ingest.tsx b/ui/src/pages/dialogs/ingest.tsx index 02a9670..a54cb35 100644 --- a/ui/src/pages/dialogs/ingest.tsx +++ b/ui/src/pages/dialogs/ingest.tsx @@ -2,7 +2,7 @@ import { Combobox } from '@kobalte/core/*'; import { useSearchParams } from '@solidjs/router'; import { createResource, createSignal, For, Show, type Component } from 'solid-js'; -import { doIngest, getSummary, listElements, listExportFormats } from '~/api'; +import { doIngest, exitDialog, getSummary, listElements, listExportFormats } from '~/api'; import { Button } from '~/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '~/components/ui/card'; import { Label } from '~/components/ui/label'; @@ -31,6 +31,7 @@ const DialogIngest: Component = () => { const [source, setSource] = createSignal(""); const [isIngesting, setIsIngesting] = createSignal(false); + const [ingestFinished, setIsIngestFinished] = createSignal(false); const [ingestStatus, setIngestStatus] = createSignal(""); @@ -91,7 +92,6 @@ const DialogIngest: Component = () => { obj(data) } - console.log(result) setIngestStatus("Ingested: " + JSON.stringify(result)) @@ -101,7 +101,18 @@ const DialogIngest: Component = () => { } } - setIngestStatus("Done!") + setIsIngestFinished(true) + setIngestStatus("Ingest finished") + } + + function onFinished() { + console.log("test") + console.log(history.length) + if (history.length > 1) { + history.back() + } else { + exitDialog(null) + } } return ( @@ -115,6 +126,11 @@ const DialogIngest: Component = () => { Ingesting {ingestStatus()} + + + + +
From 498af314224fe87ea15d01646efaf84d6a68d509 Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Sun, 23 Mar 2025 14:55:48 +1030 Subject: [PATCH 6/9] allow running os commands, for automatic file conversions --- .../basic/scripts/ingest/ingest_audio_file.js | 15 ++++- src/gui/routes/mod.rs | 2 + src/gui/routes/os.rs | 64 +++++++++++++++++++ ui/api.js | 9 +++ ui/src/api.ts | 7 ++ ui/src/pages/dialogs/ingest.tsx | 2 +- 6 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 src/gui/routes/os.rs diff --git a/example/basic/scripts/ingest/ingest_audio_file.js b/example/basic/scripts/ingest/ingest_audio_file.js index 5f41837..a142d3f 100644 --- a/example/basic/scripts/ingest/ingest_audio_file.js +++ b/example/basic/scripts/ingest/ingest_audio_file.js @@ -1,4 +1,17 @@ -(data) => { +async (data) => { console.log('Ingest running user script!') console.log(data) + + var export_result = await conduct.api.doExport(data['department'], data['asset'], data['element'], data['shot'], 'ingest', data['format']) + console.log(export_result) + + var directory = export_result['directory'] + var name = export_result['recommended_file_name'] + var extension = export_result['file_format'] + + var ingest_file = data['new_file'] + var result = directory + "/" + name + extension + + let command = `ffmpeg -i '${ingest_file}' -c:a libvorbis '${result}'` + os.execute(command) }; diff --git a/src/gui/routes/mod.rs b/src/gui/routes/mod.rs index d68b7d3..acd7ec4 100644 --- a/src/gui/routes/mod.rs +++ b/src/gui/routes/mod.rs @@ -2,8 +2,10 @@ use super::router::ApiEntry; mod command; mod dialogue; +mod os; pub fn register_routes(router: &mut matchit::Router) { command::register_routes(router); dialogue::register_routes(router); + os::register_routes(router); } diff --git a/src/gui/routes/os.rs b/src/gui/routes/os.rs new file mode 100644 index 0000000..93975b7 --- /dev/null +++ b/src/gui/routes/os.rs @@ -0,0 +1,64 @@ +use std::process::Command; + +use log::{info, warn}; +use matchit::Params; +use serde_json::json; +use wry::http::{Method, Request}; + +use crate::{ + core::commands::write_command_result, + gui::{ + api_result::ApiResult, + router::{ApiEntry, RequestContext}, + }, +}; + +pub fn register_routes(router: &mut matchit::Router) { + router + .insert( + "/os/execute", + ApiEntry { + handler: execute, + threaded: true, + }, + ) + .unwrap(); +} + +fn execute( + request: &Request>, + _params: Params, + _context: RequestContext, +) -> Option { + if request.method() != Method::POST { + return Some(ApiResult::Error("Invalid http method".to_string())); + } + + let body = request.body(); + + let s = match std::str::from_utf8(body) { + Ok(v) => v, + Err(_) => return Some(ApiResult::Error("Invalid body".to_string())), + }; + + info!("Executing os command: {}", s); + + let output = if cfg!(target_os = "windows") { + Command::new("cmd").args(["/C", s]).output() + } else { + Command::new("sh").arg("-c").arg(s).output() + }; + + match output { + Ok(output) => { + info!("status: {}", output.status); + info!("stdout: {}", String::from_utf8_lossy(&output.stdout)); + info!("stderr: {}", String::from_utf8_lossy(&output.stderr)); + } + Err(_) => { + warn!("Failed to execute command! :(") + } + } + + Some(ApiResult::Ok(None)) +} diff --git a/ui/api.js b/ui/api.js index 00eddf7..8ff962e 100644 --- a/ui/api.js +++ b/ui/api.js @@ -12,3 +12,12 @@ window.conduct = { }); } }; + +window.os = { + execute: function (command) { + return fetch(`${base_path}/os/execute`, { + method: "POST", + body: command + }); + } +} diff --git a/ui/src/api.ts b/ui/src/api.ts index fd2f60b..9d03f90 100644 --- a/ui/src/api.ts +++ b/ui/src/api.ts @@ -6,6 +6,7 @@ declare global { conduct: { get: (path: string) => Promise, post: (path: string, body: string) => Promise + api: any } } } @@ -171,4 +172,10 @@ export async function doIngest(asset: string, element: string | null, department "source": source }) return await result.json() as IngestResult +} + +window.conduct.api = { + doExport, + doIngest, + listShots } \ No newline at end of file diff --git a/ui/src/pages/dialogs/ingest.tsx b/ui/src/pages/dialogs/ingest.tsx index a54cb35..2688924 100644 --- a/ui/src/pages/dialogs/ingest.tsx +++ b/ui/src/pages/dialogs/ingest.tsx @@ -89,7 +89,7 @@ const DialogIngest: Component = () => { let obj = eval(result['script']) console.log(obj) - obj(data) + await obj(data) } console.log(result) From 75ecf884b83517b341306d48b40bf963ee25aeae Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Sun, 23 Mar 2025 17:15:04 +1030 Subject: [PATCH 7/9] allow ingesting in to shot --- src/core/commands/command_ingest.rs | 2 +- ui/src/api.ts | 3 +- ui/src/pages/dialogs/ingest.tsx | 48 +++++++++++++++++++++++------ 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/src/core/commands/command_ingest.rs b/src/core/commands/command_ingest.rs index 8169b25..98c31a5 100644 --- a/src/core/commands/command_ingest.rs +++ b/src/core/commands/command_ingest.rs @@ -122,7 +122,7 @@ impl Command for IngestArgs { let now = OffsetDateTime::now_local().unwrap(); - if let Err(e) = writeln!(file, " - `{}`: {} at {}", original.file_name().unwrap().to_str().unwrap(), source, now) { + if let Err(e) = writeln!(file, " - `{}`: {} at {} ({})", original.file_name().unwrap().to_str().unwrap(), source, now, original.to_str().unwrap()) { eprintln!("Couldn't write to file: {}", e); } else { info!("Wrote source down in {}", sources_path.to_str().unwrap()) diff --git a/ui/src/api.ts b/ui/src/api.ts index 9d03f90..966e0c5 100644 --- a/ui/src/api.ts +++ b/ui/src/api.ts @@ -161,13 +161,14 @@ export async function resolveElements(asset: string): Promise { +export async function doIngest(asset: string, element: string | null, department: string, shot: string | null, file: string, target_format: string | null, license: string, source: string): Promise { let result = await get("api/v1/command/ingest", { "asset": asset, "element": element, "department": department, "file": file, "target_format": target_format, + "shot": shot, "license": license, "source": source }) diff --git a/ui/src/pages/dialogs/ingest.tsx b/ui/src/pages/dialogs/ingest.tsx index 2688924..acc8d7e 100644 --- a/ui/src/pages/dialogs/ingest.tsx +++ b/ui/src/pages/dialogs/ingest.tsx @@ -2,7 +2,7 @@ import { Combobox } from '@kobalte/core/*'; import { useSearchParams } from '@solidjs/router'; import { createResource, createSignal, For, Show, type Component } from 'solid-js'; -import { doIngest, exitDialog, getSummary, listElements, listExportFormats } from '~/api'; +import { doIngest, exitDialog, getSummary, listElements, listExportFormats, listShots } from '~/api'; import { Button } from '~/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '~/components/ui/card'; import { Label } from '~/components/ui/label'; @@ -12,8 +12,10 @@ import { TextField, TextFieldInput } from '~/components/ui/text-field'; const DialogIngest: Component = () => { const [files, setFiles] = createSignal([]) const [selectedDepartment, setSelectedDepartment] = createSignal() + const [selectedShot, setSelectedShot] = createSignal() const [info] = createResource(getSummary); + const [shots] = createResource(listShots); const [license, setLicenseFiles] = createSignal([]) @@ -22,12 +24,14 @@ const DialogIngest: Component = () => { const [searchParams, setSearchParams] = useSearchParams(); const targetAsset = () => searchParams.asset; - const [elements] = createResource(() => listElements(targetAsset() as string, selectedDepartment())); + const [elements] = createResource(selectedDepartment, (department) => listElements(targetAsset() as string, department)); const [elementSelections, setElementSelections] = createSignal({}); const [formats] = createResource(selectedDepartment, (department) => listExportFormats(department, "ingest")); const [formatSelections, setFormatSelections] = createSignal({}); + const [shotSelections, setShotSelections] = createSignal({}); + const [source, setSource] = createSignal(""); const [isIngesting, setIsIngesting] = createSignal(false); @@ -72,8 +76,9 @@ const DialogIngest: Component = () => { setIngestStatus(file + " -> " + asset + " " + "(" + element + ")") let format = formatSelections()[file] let dept = selectedDepartment() + let shot = shotSelections()[file] let license_file = license()[0]; - let result = await doIngest(targetAsset() as string, element, dept!, file, format, license_file, source()) + let result = await doIngest(targetAsset() as string, element, dept!, shot, file, format, license_file, source()) if (result['script'] != null) { @@ -83,6 +88,7 @@ const DialogIngest: Component = () => { "element": element, "asset": asset, "department": dept, + "shot": shot, "format": format } @@ -160,7 +166,7 @@ const DialogIngest: Component = () => { - 0}> + 0 && shots()}> Selected Files @@ -170,13 +176,13 @@ const DialogIngest: Component = () => { { (e) => { return
-
+
-
+
- { console.log(selected) let selections = { ...elementSelections() @@ -196,7 +202,7 @@ const DialogIngest: Component = () => { - + + +
} } 0}> -
+