implements PR-05c selected asset details contract and preview
This commit is contained in:
parent
17064aa505
commit
512e4eb49e
@ -94,6 +94,36 @@ public enum I18n {
|
||||
ASSETS_BADGE_ORPHAN("assets.badge.orphan"),
|
||||
ASSETS_BADGE_PRELOAD("assets.badge.preload"),
|
||||
ASSETS_BADGE_DIAGNOSTICS("assets.badge.diagnostics"),
|
||||
ASSETS_SECTION_SUMMARY("assets.section.summary"),
|
||||
ASSETS_SECTION_RUNTIME_CONTRACT("assets.section.runtimeContract"),
|
||||
ASSETS_SECTION_INPUTS_PREVIEW("assets.section.inputsPreview"),
|
||||
ASSETS_SECTION_DIAGNOSTICS("assets.section.diagnostics"),
|
||||
ASSETS_SECTION_ACTIONS("assets.section.actions"),
|
||||
ASSETS_ACTIONS_PRIMARY("assets.actions.primary"),
|
||||
ASSETS_ACTIONS_SENSITIVE("assets.actions.sensitive"),
|
||||
ASSETS_ACTION_DOCTOR("assets.action.doctor"),
|
||||
ASSETS_ACTION_BUILD("assets.action.build"),
|
||||
ASSETS_ACTION_ADOPT("assets.action.adopt"),
|
||||
ASSETS_ACTION_REGISTER("assets.action.register"),
|
||||
ASSETS_ACTION_FORGET("assets.action.forget"),
|
||||
ASSETS_ACTION_REMOVE("assets.action.remove"),
|
||||
ASSETS_LABEL_NAME("assets.label.name"),
|
||||
ASSETS_LABEL_STATE("assets.label.state"),
|
||||
ASSETS_LABEL_ASSET_ID("assets.label.assetId"),
|
||||
ASSETS_LABEL_TYPE("assets.label.type"),
|
||||
ASSETS_LABEL_LOCATION("assets.label.location"),
|
||||
ASSETS_LABEL_FORMAT("assets.label.format"),
|
||||
ASSETS_LABEL_CODEC("assets.label.codec"),
|
||||
ASSETS_LABEL_PRELOAD("assets.label.preload"),
|
||||
ASSETS_VALUE_YES("assets.value.yes"),
|
||||
ASSETS_VALUE_NO("assets.value.no"),
|
||||
ASSETS_INPUTS_EMPTY("assets.inputs.empty"),
|
||||
ASSETS_DIAGNOSTICS_EMPTY("assets.diagnostics.empty"),
|
||||
ASSETS_PREVIEW_EMPTY("assets.preview.empty"),
|
||||
ASSETS_PREVIEW_TEXT_ERROR("assets.preview.textError"),
|
||||
ASSETS_PREVIEW_IMAGE_ERROR("assets.preview.imageError"),
|
||||
ASSETS_PREVIEW_AUDIO_PLACEHOLDER("assets.preview.audioPlaceholder"),
|
||||
ASSETS_PREVIEW_GENERIC_PLACEHOLDER("assets.preview.genericPlaceholder"),
|
||||
ASSETS_SUMMARY_LOADING("assets.summary.loading"),
|
||||
ASSETS_SUMMARY_EMPTY("assets.summary.empty"),
|
||||
ASSETS_SUMMARY_READY("assets.summary.ready"),
|
||||
|
||||
@ -2,8 +2,12 @@ package p.studio.workspaces.assets;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.*;
|
||||
import p.studio.Container;
|
||||
import p.studio.events.*;
|
||||
@ -12,36 +16,47 @@ import p.studio.utilities.i18n.I18n;
|
||||
import p.studio.workspaces.Workspace;
|
||||
import p.studio.workspaces.WorkspaceId;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.EnumMap;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public final class AssetWorkspace implements Workspace {
|
||||
private final BorderPane root = new BorderPane();
|
||||
private final ProjectReference projectReference;
|
||||
private final AssetWorkspaceService assetWorkspaceService;
|
||||
private final StudioWorkspaceEventBus workspaceBus;
|
||||
|
||||
private final TextField searchField = new TextField();
|
||||
private final FlowPane filterBar = new FlowPane();
|
||||
private final Label navigatorStateLabel = new Label();
|
||||
private final VBox navigatorContent = new VBox(8);
|
||||
private final Label detailStateLabel = new Label();
|
||||
|
||||
private final VBox detailsContent = new VBox(12);
|
||||
private final Label workspaceSummaryLabel = new Label();
|
||||
|
||||
private final Map<AssetNavigatorFilter, ToggleButton> filterButtons = new EnumMap<>(AssetNavigatorFilter.class);
|
||||
private final EnumSet<AssetNavigatorFilter> activeFilters = EnumSet.noneOf(AssetNavigatorFilter.class);
|
||||
|
||||
private String searchQuery = "";
|
||||
private volatile AssetWorkspaceState state = AssetWorkspaceState.loading(null);
|
||||
private volatile AssetWorkspaceDetailsStatus detailsStatus = AssetWorkspaceDetailsStatus.EMPTY;
|
||||
private volatile AssetWorkspaceAssetDetails selectedAssetDetails;
|
||||
private volatile String detailsErrorMessage;
|
||||
private volatile Path selectedPreviewInput;
|
||||
private String searchQuery = "";
|
||||
|
||||
public AssetWorkspace(ProjectReference projectReference) {
|
||||
this(projectReference, new FileSystemAssetWorkspaceService());
|
||||
}
|
||||
|
||||
public AssetWorkspace(ProjectReference projectReference, AssetWorkspaceService assetWorkspaceService) {
|
||||
this.projectReference = projectReference;
|
||||
this.assetWorkspaceService = assetWorkspaceService;
|
||||
this.projectReference = Objects.requireNonNull(projectReference, "projectReference");
|
||||
this.assetWorkspaceService = Objects.requireNonNull(assetWorkspaceService, "assetWorkspaceService");
|
||||
this.workspaceBus = new StudioWorkspaceEventBus(WorkspaceId.ASSETS, Container.events());
|
||||
|
||||
root.getStyleClass().add("assets-workspace");
|
||||
@ -75,20 +90,22 @@ public final class AssetWorkspace implements Workspace {
|
||||
|
||||
private SplitPane buildLayout() {
|
||||
final VBox navigatorPane = new VBox(8);
|
||||
navigatorPane.getStyleClass().add("assets-workspace-pane");
|
||||
final Label navigatorTitle = new Label();
|
||||
navigatorTitle.textProperty().bind(Container.i18n().bind(I18n.ASSETS_NAVIGATOR_TITLE));
|
||||
navigatorTitle.getStyleClass().add("assets-workspace-pane-title");
|
||||
|
||||
searchField.setPromptText(Container.i18n().text(I18n.ASSETS_SEARCH_PROMPT));
|
||||
searchField.getStyleClass().add("assets-workspace-search");
|
||||
searchField.textProperty().addListener((ignored, oldValue, newValue) -> {
|
||||
final String previous = oldValue == null ? "" : oldValue;
|
||||
final String current = newValue == null ? "" : newValue;
|
||||
if (previous.equals(current)) {
|
||||
return;
|
||||
if (!previous.equals(current)) {
|
||||
searchQuery = current;
|
||||
renderState();
|
||||
}
|
||||
searchQuery = current;
|
||||
renderState();
|
||||
});
|
||||
|
||||
configureFilterBar();
|
||||
navigatorStateLabel.getStyleClass().add("assets-workspace-pane-body");
|
||||
navigatorContent.getStyleClass().add("assets-workspace-navigator-content");
|
||||
@ -96,19 +113,22 @@ public final class AssetWorkspace implements Workspace {
|
||||
navigatorScroll.setFitToWidth(true);
|
||||
navigatorScroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
navigatorScroll.getStyleClass().add("assets-workspace-navigator-scroll");
|
||||
navigatorPane.getStyleClass().add("assets-workspace-pane");
|
||||
navigatorPane.getChildren().addAll(navigatorTitle, searchField, filterBar, navigatorStateLabel, navigatorScroll);
|
||||
VBox.setVgrow(navigatorScroll, Priority.ALWAYS);
|
||||
|
||||
final VBox detailsPane = new VBox(8);
|
||||
final VBox detailsPane = new VBox(10);
|
||||
detailsPane.getStyleClass().add("assets-workspace-pane");
|
||||
final Label detailsTitle = new Label();
|
||||
detailsTitle.textProperty().bind(Container.i18n().bind(I18n.ASSETS_DETAILS_TITLE));
|
||||
detailsTitle.getStyleClass().add("assets-workspace-pane-title");
|
||||
workspaceSummaryLabel.getStyleClass().add("assets-workspace-summary");
|
||||
detailStateLabel.getStyleClass().add("assets-workspace-pane-body");
|
||||
detailsPane.getStyleClass().add("assets-workspace-pane");
|
||||
detailsPane.getChildren().addAll(detailsTitle, workspaceSummaryLabel, detailStateLabel);
|
||||
VBox.setVgrow(detailStateLabel, Priority.ALWAYS);
|
||||
detailsContent.getStyleClass().add("assets-workspace-details-content");
|
||||
final ScrollPane detailsScroll = new ScrollPane(detailsContent);
|
||||
detailsScroll.setFitToWidth(true);
|
||||
detailsScroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
detailsScroll.getStyleClass().add("assets-workspace-details-scroll");
|
||||
detailsPane.getChildren().addAll(detailsTitle, workspaceSummaryLabel, detailsScroll);
|
||||
VBox.setVgrow(detailsScroll, Priority.ALWAYS);
|
||||
|
||||
final SplitPane splitPane = new SplitPane(navigatorPane, detailsPane);
|
||||
splitPane.setDividerPositions(0.34);
|
||||
@ -145,6 +165,10 @@ public final class AssetWorkspace implements Workspace {
|
||||
|
||||
private void refresh() {
|
||||
state = AssetWorkspaceState.loading(state);
|
||||
detailsStatus = AssetWorkspaceDetailsStatus.EMPTY;
|
||||
selectedAssetDetails = null;
|
||||
detailsErrorMessage = null;
|
||||
selectedPreviewInput = null;
|
||||
renderState();
|
||||
workspaceBus.publish(new StudioAssetsWorkspaceRefreshStartedEvent(projectReference));
|
||||
|
||||
@ -153,6 +177,8 @@ public final class AssetWorkspace implements Workspace {
|
||||
.whenComplete((snapshot, throwable) -> Platform.runLater(() -> {
|
||||
if (throwable != null) {
|
||||
state = AssetWorkspaceState.error(state, rootCauseMessage(throwable));
|
||||
detailsStatus = AssetWorkspaceDetailsStatus.ERROR;
|
||||
detailsErrorMessage = state.errorMessage();
|
||||
renderState();
|
||||
workspaceBus.publish(new StudioAssetsWorkspaceRefreshFailedEvent(projectReference, state.errorMessage()));
|
||||
return;
|
||||
@ -161,33 +187,61 @@ public final class AssetWorkspace implements Workspace {
|
||||
state = AssetWorkspaceState.ready(snapshot.assets(), state.selectedKey());
|
||||
renderState();
|
||||
workspaceBus.publish(new StudioAssetsWorkspaceRefreshedEvent(projectReference, state.assets().size()));
|
||||
state.selectedAsset()
|
||||
.ifPresent(asset -> workspaceBus.publish(new StudioAssetsWorkspaceSelectionChangedEvent(projectReference, asset.selectionKey())));
|
||||
state.selectedAsset().ifPresent(asset -> {
|
||||
workspaceBus.publish(new StudioAssetsWorkspaceSelectionChangedEvent(projectReference, asset.selectionKey()));
|
||||
loadSelectedAssetDetails(asset.selectionKey());
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
private void loadSelectedAssetDetails(AssetWorkspaceSelectionKey selectionKey) {
|
||||
detailsStatus = AssetWorkspaceDetailsStatus.LOADING;
|
||||
selectedAssetDetails = null;
|
||||
detailsErrorMessage = null;
|
||||
selectedPreviewInput = null;
|
||||
renderState();
|
||||
|
||||
CompletableFuture
|
||||
.supplyAsync(() -> assetWorkspaceService.loadAssetDetails(projectReference, selectionKey))
|
||||
.whenComplete((details, throwable) -> Platform.runLater(() -> {
|
||||
if (!selectionKey.equals(state.selectedKey())) {
|
||||
return;
|
||||
}
|
||||
if (throwable != null) {
|
||||
detailsStatus = AssetWorkspaceDetailsStatus.ERROR;
|
||||
detailsErrorMessage = rootCauseMessage(throwable);
|
||||
selectedAssetDetails = null;
|
||||
renderState();
|
||||
return;
|
||||
}
|
||||
|
||||
selectedAssetDetails = details;
|
||||
detailsStatus = AssetWorkspaceDetailsStatus.READY;
|
||||
selectedPreviewInput = firstPreviewInput(details);
|
||||
renderState();
|
||||
}));
|
||||
}
|
||||
|
||||
private void renderState() {
|
||||
renderNavigator();
|
||||
renderDetails();
|
||||
}
|
||||
|
||||
private void renderNavigator() {
|
||||
switch (state.status()) {
|
||||
case LOADING -> {
|
||||
navigatorStateLabel.setText(Container.i18n().text(I18n.ASSETS_STATE_LOADING));
|
||||
navigatorContent.getChildren().setAll(createNavigatorMessage(Container.i18n().text(I18n.ASSETS_STATE_LOADING)));
|
||||
workspaceSummaryLabel.setText(Container.i18n().text(I18n.ASSETS_SUMMARY_LOADING));
|
||||
detailStateLabel.setText(Container.i18n().text(I18n.ASSETS_DETAILS_LOADING));
|
||||
}
|
||||
case EMPTY -> {
|
||||
navigatorStateLabel.setText(Container.i18n().text(I18n.ASSETS_STATE_EMPTY));
|
||||
navigatorContent.getChildren().setAll(createNavigatorMessage(Container.i18n().text(I18n.ASSETS_STATE_EMPTY)));
|
||||
workspaceSummaryLabel.setText(Container.i18n().text(I18n.ASSETS_SUMMARY_EMPTY));
|
||||
detailStateLabel.setText(Container.i18n().text(I18n.ASSETS_DETAILS_EMPTY));
|
||||
}
|
||||
case ERROR -> {
|
||||
navigatorStateLabel.setText(Container.i18n().text(I18n.ASSETS_STATE_ERROR) + "\n\n" + state.errorMessage());
|
||||
navigatorContent.getChildren().setAll(createNavigatorMessage(state.errorMessage()));
|
||||
workspaceSummaryLabel.setText(Container.i18n().text(I18n.ASSETS_SUMMARY_ERROR));
|
||||
detailStateLabel.setText(state.errorMessage());
|
||||
}
|
||||
case READY -> {
|
||||
final int assetCount = state.assets().size();
|
||||
final AssetNavigatorProjection projection = AssetNavigatorProjectionBuilder.build(
|
||||
state.assets(),
|
||||
assetsRoot(),
|
||||
@ -197,22 +251,297 @@ public final class AssetWorkspace implements Workspace {
|
||||
navigatorStateLabel.setText(Container.i18n().text(I18n.ASSETS_STATE_NO_RESULTS));
|
||||
navigatorContent.getChildren().setAll(createNavigatorMessage(Container.i18n().text(I18n.ASSETS_STATE_NO_RESULTS)));
|
||||
} else {
|
||||
navigatorStateLabel.setText(Container.i18n().format(I18n.ASSETS_STATE_READY, projection.visibleAssetCount(), assetCount));
|
||||
navigatorStateLabel.setText(Container.i18n().format(I18n.ASSETS_STATE_READY, projection.visibleAssetCount(), state.assets().size()));
|
||||
renderNavigatorProjection(projection);
|
||||
}
|
||||
workspaceSummaryLabel.setText(Container.i18n().format(I18n.ASSETS_SUMMARY_READY, assetCount));
|
||||
final String selectedDescription = state.selectedAsset()
|
||||
.map(asset -> Container.i18n().format(
|
||||
I18n.ASSETS_DETAILS_READY,
|
||||
asset.assetName(),
|
||||
asset.state().name().toLowerCase(),
|
||||
asset.assetRoot()))
|
||||
.orElseGet(() -> Container.i18n().text(I18n.ASSETS_DETAILS_NO_SELECTION));
|
||||
detailStateLabel.setText(selectedDescription);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void renderDetails() {
|
||||
detailsContent.getChildren().clear();
|
||||
switch (state.status()) {
|
||||
case LOADING -> {
|
||||
workspaceSummaryLabel.setText(Container.i18n().text(I18n.ASSETS_SUMMARY_LOADING));
|
||||
detailsContent.getChildren().add(createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_LOADING)));
|
||||
}
|
||||
case EMPTY -> {
|
||||
workspaceSummaryLabel.setText(Container.i18n().text(I18n.ASSETS_SUMMARY_EMPTY));
|
||||
detailsContent.getChildren().add(createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_EMPTY)));
|
||||
}
|
||||
case ERROR -> {
|
||||
workspaceSummaryLabel.setText(Container.i18n().text(I18n.ASSETS_SUMMARY_ERROR));
|
||||
detailsContent.getChildren().add(createSectionMessage(state.errorMessage()));
|
||||
}
|
||||
case READY -> {
|
||||
workspaceSummaryLabel.setText(Container.i18n().format(I18n.ASSETS_SUMMARY_READY, state.assets().size()));
|
||||
state.selectedAsset()
|
||||
.ifPresentOrElse(this::renderSelectedAssetDetails, () ->
|
||||
detailsContent.getChildren().add(createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_NO_SELECTION))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void renderSelectedAssetDetails(AssetWorkspaceAssetSummary summary) {
|
||||
detailsContent.getChildren().add(createSummarySection(summary));
|
||||
if (detailsStatus == AssetWorkspaceDetailsStatus.LOADING) {
|
||||
detailsContent.getChildren().add(createSection(
|
||||
Container.i18n().text(I18n.ASSETS_SECTION_RUNTIME_CONTRACT),
|
||||
createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_LOADING))));
|
||||
detailsContent.getChildren().add(createSection(
|
||||
Container.i18n().text(I18n.ASSETS_SECTION_INPUTS_PREVIEW),
|
||||
createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_LOADING))));
|
||||
detailsContent.getChildren().add(createSection(
|
||||
Container.i18n().text(I18n.ASSETS_SECTION_DIAGNOSTICS),
|
||||
createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_LOADING))));
|
||||
detailsContent.getChildren().add(createSection(
|
||||
Container.i18n().text(I18n.ASSETS_SECTION_ACTIONS),
|
||||
createActionsContent(summary)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (detailsStatus == AssetWorkspaceDetailsStatus.ERROR || selectedAssetDetails == null) {
|
||||
detailsContent.getChildren().add(createSection(
|
||||
Container.i18n().text(I18n.ASSETS_SECTION_RUNTIME_CONTRACT),
|
||||
createSectionMessage(Objects.requireNonNullElse(detailsErrorMessage, Container.i18n().text(I18n.ASSETS_DETAILS_EMPTY)))));
|
||||
detailsContent.getChildren().add(createSection(
|
||||
Container.i18n().text(I18n.ASSETS_SECTION_INPUTS_PREVIEW),
|
||||
createSectionMessage(Objects.requireNonNullElse(detailsErrorMessage, Container.i18n().text(I18n.ASSETS_DETAILS_EMPTY)))));
|
||||
detailsContent.getChildren().add(createSection(
|
||||
Container.i18n().text(I18n.ASSETS_SECTION_DIAGNOSTICS),
|
||||
createSectionMessage(Objects.requireNonNullElse(detailsErrorMessage, Container.i18n().text(I18n.ASSETS_DETAILS_EMPTY)))));
|
||||
detailsContent.getChildren().add(createSection(
|
||||
Container.i18n().text(I18n.ASSETS_SECTION_ACTIONS),
|
||||
createActionsContent(summary)));
|
||||
return;
|
||||
}
|
||||
|
||||
detailsContent.getChildren().add(createRuntimeContractSection(selectedAssetDetails));
|
||||
detailsContent.getChildren().add(createInputsPreviewSection(selectedAssetDetails));
|
||||
detailsContent.getChildren().add(createDiagnosticsSection(selectedAssetDetails));
|
||||
detailsContent.getChildren().add(createSection(
|
||||
Container.i18n().text(I18n.ASSETS_SECTION_ACTIONS),
|
||||
createActionsContent(summary)));
|
||||
}
|
||||
|
||||
private Node createSummarySection(AssetWorkspaceAssetSummary summary) {
|
||||
final VBox content = new VBox(8);
|
||||
content.getChildren().addAll(
|
||||
createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_NAME), summary.assetName()),
|
||||
createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_STATE), summary.state().name().toLowerCase()),
|
||||
createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_ASSET_ID), summary.assetId() == null ? "—" : String.valueOf(summary.assetId())),
|
||||
createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_TYPE), summary.assetFamily()),
|
||||
createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_LOCATION), summary.assetRoot().toString()));
|
||||
return createSection(Container.i18n().text(I18n.ASSETS_SECTION_SUMMARY), content);
|
||||
}
|
||||
|
||||
private Node createRuntimeContractSection(AssetWorkspaceAssetDetails details) {
|
||||
final VBox content = new VBox(8);
|
||||
content.getChildren().addAll(
|
||||
createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_FORMAT), details.outputFormat()),
|
||||
createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_CODEC), details.outputCodec()),
|
||||
createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_PRELOAD), yesNo(details.summary().preload())));
|
||||
return createSection(Container.i18n().text(I18n.ASSETS_SECTION_RUNTIME_CONTRACT), content);
|
||||
}
|
||||
|
||||
private Node createInputsPreviewSection(AssetWorkspaceAssetDetails details) {
|
||||
if (details.inputsByRole().isEmpty()) {
|
||||
return createSection(
|
||||
Container.i18n().text(I18n.ASSETS_SECTION_INPUTS_PREVIEW),
|
||||
createSectionMessage(Container.i18n().text(I18n.ASSETS_INPUTS_EMPTY)));
|
||||
}
|
||||
|
||||
if (selectedPreviewInput == null || !containsInput(details, selectedPreviewInput)) {
|
||||
selectedPreviewInput = firstPreviewInput(details);
|
||||
}
|
||||
|
||||
final VBox inputsList = new VBox(8);
|
||||
for (Map.Entry<String, List<Path>> entry : details.inputsByRole().entrySet()) {
|
||||
final VBox roleBox = new VBox(6);
|
||||
final Label roleLabel = new Label(entry.getKey());
|
||||
roleLabel.getStyleClass().add("assets-details-role-label");
|
||||
roleBox.getChildren().add(roleLabel);
|
||||
for (Path input : entry.getValue()) {
|
||||
final Button inputButton = new Button(input.getFileName().toString());
|
||||
inputButton.getStyleClass().add("assets-details-input-button");
|
||||
if (input.equals(selectedPreviewInput)) {
|
||||
inputButton.getStyleClass().add("assets-details-input-button-selected");
|
||||
}
|
||||
inputButton.setMaxWidth(Double.MAX_VALUE);
|
||||
inputButton.setOnAction(event -> {
|
||||
selectedPreviewInput = input;
|
||||
renderState();
|
||||
});
|
||||
roleBox.getChildren().add(inputButton);
|
||||
}
|
||||
inputsList.getChildren().add(roleBox);
|
||||
}
|
||||
|
||||
final Node previewPane = createPreviewPane(selectedPreviewInput);
|
||||
final SplitPane splitPane = new SplitPane(inputsList, previewPane);
|
||||
splitPane.setOrientation(Orientation.HORIZONTAL);
|
||||
splitPane.setDividerPositions(0.34);
|
||||
splitPane.getStyleClass().add("assets-details-input-preview-split");
|
||||
return createSection(Container.i18n().text(I18n.ASSETS_SECTION_INPUTS_PREVIEW), splitPane);
|
||||
}
|
||||
|
||||
private Node createDiagnosticsSection(AssetWorkspaceAssetDetails details) {
|
||||
if (details.diagnostics().isEmpty()) {
|
||||
return createSection(
|
||||
Container.i18n().text(I18n.ASSETS_SECTION_DIAGNOSTICS),
|
||||
createSectionMessage(Container.i18n().text(I18n.ASSETS_DIAGNOSTICS_EMPTY)));
|
||||
}
|
||||
|
||||
final VBox diagnosticsBox = new VBox(8);
|
||||
for (AssetWorkspaceDiagnostic diagnostic : details.diagnostics()) {
|
||||
final VBox card = new VBox(4);
|
||||
card.getStyleClass().add("assets-details-diagnostic-card");
|
||||
card.getStyleClass().add("assets-details-diagnostic-" + diagnostic.severity().name().toLowerCase());
|
||||
final Label severity = new Label(diagnostic.severity().name());
|
||||
severity.getStyleClass().add("assets-details-diagnostic-severity");
|
||||
final Label message = new Label(diagnostic.message());
|
||||
message.getStyleClass().add("assets-details-diagnostic-message");
|
||||
message.setWrapText(true);
|
||||
card.getChildren().addAll(severity, message);
|
||||
diagnosticsBox.getChildren().add(card);
|
||||
}
|
||||
return createSection(Container.i18n().text(I18n.ASSETS_SECTION_DIAGNOSTICS), diagnosticsBox);
|
||||
}
|
||||
|
||||
private Node createActionsContent(AssetWorkspaceAssetSummary summary) {
|
||||
final AssetWorkspaceActionSet actionSet = AssetWorkspaceActionSetBuilder.forAsset(summary);
|
||||
final VBox content = new VBox(12);
|
||||
|
||||
final VBox primaryBox = new VBox(8);
|
||||
final Label primaryLabel = new Label(Container.i18n().text(I18n.ASSETS_ACTIONS_PRIMARY));
|
||||
primaryLabel.getStyleClass().add("assets-details-subsection-title");
|
||||
primaryBox.getChildren().add(primaryLabel);
|
||||
final FlowPane primaryButtons = new FlowPane();
|
||||
primaryButtons.setHgap(8);
|
||||
primaryButtons.setVgap(8);
|
||||
for (AssetWorkspaceAction action : actionSet.primaryActions()) {
|
||||
primaryButtons.getChildren().add(createActionButton(action, false));
|
||||
}
|
||||
primaryBox.getChildren().add(primaryButtons);
|
||||
content.getChildren().add(primaryBox);
|
||||
|
||||
if (!actionSet.sensitiveActions().isEmpty()) {
|
||||
final VBox sensitiveBox = new VBox(8);
|
||||
final Label sensitiveLabel = new Label(Container.i18n().text(I18n.ASSETS_ACTIONS_SENSITIVE));
|
||||
sensitiveLabel.getStyleClass().add("assets-details-subsection-title");
|
||||
sensitiveBox.getChildren().add(sensitiveLabel);
|
||||
final FlowPane sensitiveButtons = new FlowPane();
|
||||
sensitiveButtons.setHgap(8);
|
||||
sensitiveButtons.setVgap(8);
|
||||
for (AssetWorkspaceAction action : actionSet.sensitiveActions()) {
|
||||
sensitiveButtons.getChildren().add(createActionButton(action, true));
|
||||
}
|
||||
sensitiveBox.getChildren().add(sensitiveButtons);
|
||||
content.getChildren().add(sensitiveBox);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
private Button createActionButton(AssetWorkspaceAction action, boolean sensitive) {
|
||||
final Button button = new Button(actionLabel(action));
|
||||
button.getStyleClass().add("assets-details-action-button");
|
||||
if (sensitive) {
|
||||
button.getStyleClass().add("assets-details-action-button-sensitive");
|
||||
} else {
|
||||
button.getStyleClass().add("assets-details-action-button-primary");
|
||||
}
|
||||
button.setDisable(true);
|
||||
return button;
|
||||
}
|
||||
|
||||
private String actionLabel(AssetWorkspaceAction action) {
|
||||
return switch (action) {
|
||||
case DOCTOR -> Container.i18n().text(I18n.ASSETS_ACTION_DOCTOR);
|
||||
case BUILD -> Container.i18n().text(I18n.ASSETS_ACTION_BUILD);
|
||||
case ADOPT -> Container.i18n().text(I18n.ASSETS_ACTION_ADOPT);
|
||||
case REGISTER -> Container.i18n().text(I18n.ASSETS_ACTION_REGISTER);
|
||||
case FORGET -> Container.i18n().text(I18n.ASSETS_ACTION_FORGET);
|
||||
case REMOVE -> Container.i18n().text(I18n.ASSETS_ACTION_REMOVE);
|
||||
};
|
||||
}
|
||||
|
||||
private Node createPreviewPane(Path input) {
|
||||
final VBox previewBox = new VBox(10);
|
||||
previewBox.getStyleClass().add("assets-details-preview-pane");
|
||||
if (input == null) {
|
||||
previewBox.getChildren().add(createSectionMessage(Container.i18n().text(I18n.ASSETS_PREVIEW_EMPTY)));
|
||||
return previewBox;
|
||||
}
|
||||
|
||||
final Label title = new Label(input.getFileName().toString());
|
||||
title.getStyleClass().add("assets-details-preview-title");
|
||||
previewBox.getChildren().add(title);
|
||||
|
||||
final String extension = extensionOf(input);
|
||||
if (isImage(extension)) {
|
||||
try {
|
||||
final Image image = new Image(input.toUri().toString(), true);
|
||||
final ImageView imageView = new ImageView(image);
|
||||
imageView.setPreserveRatio(true);
|
||||
imageView.setFitWidth(420);
|
||||
previewBox.getChildren().add(imageView);
|
||||
} catch (RuntimeException runtimeException) {
|
||||
previewBox.getChildren().add(createSectionMessage(Container.i18n().text(I18n.ASSETS_PREVIEW_IMAGE_ERROR)));
|
||||
}
|
||||
previewBox.getChildren().add(createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_LOCATION), input.toString()));
|
||||
return previewBox;
|
||||
}
|
||||
|
||||
if (isText(extension)) {
|
||||
final TextArea textArea = new TextArea(readPreviewText(input));
|
||||
textArea.setWrapText(true);
|
||||
textArea.setEditable(false);
|
||||
textArea.getStyleClass().add("assets-details-preview-text");
|
||||
previewBox.getChildren().add(textArea);
|
||||
return previewBox;
|
||||
}
|
||||
|
||||
if (isAudio(extension)) {
|
||||
previewBox.getChildren().add(createSectionMessage(Container.i18n().format(I18n.ASSETS_PREVIEW_AUDIO_PLACEHOLDER, input.getFileName().toString())));
|
||||
previewBox.getChildren().add(createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_LOCATION), input.toString()));
|
||||
return previewBox;
|
||||
}
|
||||
|
||||
previewBox.getChildren().add(createSectionMessage(Container.i18n().format(I18n.ASSETS_PREVIEW_GENERIC_PLACEHOLDER, input.getFileName().toString())));
|
||||
previewBox.getChildren().add(createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_LOCATION), input.toString()));
|
||||
return previewBox;
|
||||
}
|
||||
|
||||
private Node createSection(String title, Node content) {
|
||||
final VBox section = new VBox(10);
|
||||
section.getStyleClass().add("assets-details-section");
|
||||
final Label titleLabel = new Label(title);
|
||||
titleLabel.getStyleClass().add("assets-details-section-title");
|
||||
section.getChildren().addAll(titleLabel, content);
|
||||
return section;
|
||||
}
|
||||
|
||||
private Node createSectionMessage(String text) {
|
||||
final Label label = new Label(text);
|
||||
label.setWrapText(true);
|
||||
label.getStyleClass().add("assets-details-section-message");
|
||||
return label;
|
||||
}
|
||||
|
||||
private Node createKeyValueRow(String key, String value) {
|
||||
final HBox row = new HBox(12);
|
||||
row.setAlignment(Pos.TOP_LEFT);
|
||||
final Label keyLabel = new Label(key);
|
||||
keyLabel.getStyleClass().add("assets-details-key");
|
||||
final Label valueLabel = new Label(value);
|
||||
valueLabel.getStyleClass().add("assets-details-value");
|
||||
valueLabel.setWrapText(true);
|
||||
HBox.setHgrow(valueLabel, Priority.ALWAYS);
|
||||
row.getChildren().addAll(keyLabel, valueLabel);
|
||||
return row;
|
||||
}
|
||||
|
||||
private void renderNavigatorProjection(AssetNavigatorProjection projection) {
|
||||
navigatorContent.getChildren().clear();
|
||||
for (AssetNavigatorGroup group : projection.groups()) {
|
||||
@ -239,7 +568,7 @@ public final class AssetWorkspace implements Workspace {
|
||||
}
|
||||
|
||||
final HBox topLine = new HBox(8);
|
||||
topLine.setAlignment(javafx.geometry.Pos.CENTER_LEFT);
|
||||
topLine.setAlignment(Pos.CENTER_LEFT);
|
||||
final Label icon = new Label(assetIcon(asset));
|
||||
icon.getStyleClass().add("assets-workspace-asset-icon");
|
||||
final Label name = new Label(asset.assetName());
|
||||
@ -290,6 +619,65 @@ public final class AssetWorkspace implements Workspace {
|
||||
state = state.withSelection(selectionKey);
|
||||
renderState();
|
||||
workspaceBus.publish(new StudioAssetsWorkspaceSelectionChangedEvent(projectReference, selectionKey));
|
||||
loadSelectedAssetDetails(selectionKey);
|
||||
}
|
||||
|
||||
private Path firstPreviewInput(AssetWorkspaceAssetDetails details) {
|
||||
return details.inputsByRole().values().stream()
|
||||
.flatMap(List::stream)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private boolean containsInput(AssetWorkspaceAssetDetails details, Path input) {
|
||||
return details.inputsByRole().values().stream().flatMap(List::stream).anyMatch(input::equals);
|
||||
}
|
||||
|
||||
private String readPreviewText(Path input) {
|
||||
try {
|
||||
final String text = Files.readString(input);
|
||||
return text.length() > 4000 ? text.substring(0, 4000) + "\n\n…" : text;
|
||||
} catch (IOException ioException) {
|
||||
return Container.i18n().text(I18n.ASSETS_PREVIEW_TEXT_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private String extensionOf(Path input) {
|
||||
final String fileName = input.getFileName().toString();
|
||||
final int dot = fileName.lastIndexOf('.');
|
||||
if (dot < 0 || dot == fileName.length() - 1) {
|
||||
return "";
|
||||
}
|
||||
return fileName.substring(dot + 1).toLowerCase();
|
||||
}
|
||||
|
||||
private boolean isImage(String extension) {
|
||||
return extension.equals("png")
|
||||
|| extension.equals("jpg")
|
||||
|| extension.equals("jpeg")
|
||||
|| extension.equals("gif")
|
||||
|| extension.equals("bmp");
|
||||
}
|
||||
|
||||
private boolean isText(String extension) {
|
||||
return extension.equals("json")
|
||||
|| extension.equals("txt")
|
||||
|| extension.equals("pal")
|
||||
|| extension.equals("csv")
|
||||
|| extension.equals("yaml")
|
||||
|| extension.equals("yml")
|
||||
|| extension.equals("xml")
|
||||
|| extension.equals("pbs");
|
||||
}
|
||||
|
||||
private boolean isAudio(String extension) {
|
||||
return extension.equals("wav")
|
||||
|| extension.equals("ogg")
|
||||
|| extension.equals("mp3");
|
||||
}
|
||||
|
||||
private String yesNo(boolean value) {
|
||||
return value ? Container.i18n().text(I18n.ASSETS_VALUE_YES) : Container.i18n().text(I18n.ASSETS_VALUE_NO);
|
||||
}
|
||||
|
||||
private String assetIcon(AssetWorkspaceAssetSummary asset) {
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
package p.studio.workspaces.assets;
|
||||
|
||||
public enum AssetWorkspaceAction {
|
||||
DOCTOR,
|
||||
BUILD,
|
||||
ADOPT,
|
||||
REGISTER,
|
||||
FORGET,
|
||||
REMOVE
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package p.studio.workspaces.assets;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record AssetWorkspaceActionSet(
|
||||
List<AssetWorkspaceAction> primaryActions,
|
||||
List<AssetWorkspaceAction> sensitiveActions) {
|
||||
public AssetWorkspaceActionSet {
|
||||
primaryActions = List.copyOf(Objects.requireNonNull(primaryActions, "primaryActions"));
|
||||
sensitiveActions = List.copyOf(Objects.requireNonNull(sensitiveActions, "sensitiveActions"));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package p.studio.workspaces.assets;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class AssetWorkspaceActionSetBuilder {
|
||||
private AssetWorkspaceActionSetBuilder() {
|
||||
}
|
||||
|
||||
public static AssetWorkspaceActionSet forAsset(AssetWorkspaceAssetSummary summary) {
|
||||
Objects.requireNonNull(summary, "summary");
|
||||
return switch (summary.state()) {
|
||||
case MANAGED -> new AssetWorkspaceActionSet(
|
||||
List.of(AssetWorkspaceAction.DOCTOR, AssetWorkspaceAction.BUILD),
|
||||
List.of(AssetWorkspaceAction.FORGET, AssetWorkspaceAction.REMOVE));
|
||||
case ORPHAN -> new AssetWorkspaceActionSet(
|
||||
List.of(AssetWorkspaceAction.ADOPT, AssetWorkspaceAction.REGISTER),
|
||||
List.of());
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package p.studio.workspaces.assets;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public record AssetWorkspaceAssetDetails(
|
||||
AssetWorkspaceAssetSummary summary,
|
||||
String outputFormat,
|
||||
String outputCodec,
|
||||
Map<String, List<Path>> inputsByRole,
|
||||
List<AssetWorkspaceDiagnostic> diagnostics) {
|
||||
|
||||
public AssetWorkspaceAssetDetails {
|
||||
Objects.requireNonNull(summary, "summary");
|
||||
outputFormat = Objects.requireNonNullElse(outputFormat, "unknown");
|
||||
outputCodec = Objects.requireNonNullElse(outputCodec, "unknown");
|
||||
inputsByRole = Map.copyOf(Objects.requireNonNull(inputsByRole, "inputsByRole"));
|
||||
diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics"));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
package p.studio.workspaces.assets;
|
||||
|
||||
public enum AssetWorkspaceDetailsStatus {
|
||||
EMPTY,
|
||||
LOADING,
|
||||
READY,
|
||||
ERROR
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package p.studio.workspaces.assets;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record AssetWorkspaceDiagnostic(
|
||||
AssetWorkspaceDiagnosticSeverity severity,
|
||||
String message) {
|
||||
|
||||
public AssetWorkspaceDiagnostic {
|
||||
Objects.requireNonNull(severity, "severity");
|
||||
message = Objects.requireNonNull(message, "message").trim();
|
||||
if (message.isBlank()) {
|
||||
throw new IllegalArgumentException("message must not be blank");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package p.studio.workspaces.assets;
|
||||
|
||||
public enum AssetWorkspaceDiagnosticSeverity {
|
||||
BLOCKER,
|
||||
WARNING,
|
||||
HINT
|
||||
}
|
||||
@ -4,4 +4,6 @@ import p.studio.projects.ProjectReference;
|
||||
|
||||
public interface AssetWorkspaceService {
|
||||
AssetWorkspaceSnapshot loadWorkspace(ProjectReference projectReference);
|
||||
|
||||
AssetWorkspaceAssetDetails loadAssetDetails(ProjectReference projectReference, AssetWorkspaceSelectionKey selectionKey);
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package p.studio.workspaces.assets;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import p.studio.projects.ProjectReference;
|
||||
|
||||
@ -37,6 +38,33 @@ public final class FileSystemAssetWorkspaceService implements AssetWorkspaceServ
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AssetWorkspaceAssetDetails loadAssetDetails(ProjectReference projectReference, AssetWorkspaceSelectionKey selectionKey) {
|
||||
final Path projectRoot = Objects.requireNonNull(projectReference, "projectReference").rootPath();
|
||||
final Path assetsRoot = projectRoot.resolve("assets");
|
||||
final Map<Path, Integer> registryByRoot = readRegistry(assetsRoot);
|
||||
final Path assetRoot = resolveAssetRoot(selectionKey, assetsRoot, registryByRoot);
|
||||
final Path assetManifestPath = assetRoot.resolve("asset.json");
|
||||
try {
|
||||
final JsonNode root = MAPPER.readTree(assetManifestPath.toFile());
|
||||
final AssetWorkspaceAssetSummary summary = buildAssetSummary(assetManifestPath, registryByRoot);
|
||||
final String outputFormat = readText(root.path("output").path("format"), "unknown");
|
||||
final String outputCodec = readText(root.path("output").path("codec"), "unknown");
|
||||
final Map<String, List<Path>> inputsByRole = readInputs(root.path("inputs"), assetRoot);
|
||||
return new AssetWorkspaceAssetDetails(summary, outputFormat, outputCodec, inputsByRole, List.of());
|
||||
} catch (IOException ioException) {
|
||||
final AssetWorkspaceAssetSummary summary = buildAssetSummary(assetManifestPath, registryByRoot);
|
||||
return new AssetWorkspaceAssetDetails(
|
||||
summary,
|
||||
"unknown",
|
||||
"unknown",
|
||||
Map.of(),
|
||||
List.of(new AssetWorkspaceDiagnostic(
|
||||
AssetWorkspaceDiagnosticSeverity.BLOCKER,
|
||||
"Unable to read asset.json for the selected asset.")));
|
||||
}
|
||||
}
|
||||
|
||||
private AssetWorkspaceAssetSummary buildAssetSummary(Path assetManifestPath, Map<Path, Integer> registryByRoot) {
|
||||
final Path assetRoot = assetManifestPath.getParent().toAbsolutePath().normalize();
|
||||
try {
|
||||
@ -102,6 +130,51 @@ public final class FileSystemAssetWorkspaceService implements AssetWorkspaceServ
|
||||
}
|
||||
}
|
||||
|
||||
private Path resolveAssetRoot(
|
||||
AssetWorkspaceSelectionKey selectionKey,
|
||||
Path assetsRoot,
|
||||
Map<Path, Integer> registryByRoot) {
|
||||
return switch (selectionKey) {
|
||||
case AssetWorkspaceSelectionKey.ManagedAsset managedAsset -> registryByRoot.entrySet().stream()
|
||||
.filter(entry -> managedAsset.assetId() == entry.getValue())
|
||||
.map(Map.Entry::getKey)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("managed asset root not found for assetId " + managedAsset.assetId()));
|
||||
case AssetWorkspaceSelectionKey.OrphanAsset orphanAsset -> orphanAsset.assetRoot();
|
||||
};
|
||||
}
|
||||
|
||||
private Map<String, List<Path>> readInputs(JsonNode inputsNode, Path assetRoot) {
|
||||
if (inputsNode == null || !inputsNode.isObject()) {
|
||||
return Map.of();
|
||||
}
|
||||
final Map<String, List<Path>> result = new LinkedHashMap<>();
|
||||
final Iterator<Map.Entry<String, JsonNode>> fields = inputsNode.fields();
|
||||
while (fields.hasNext()) {
|
||||
final Map.Entry<String, JsonNode> entry = fields.next();
|
||||
if (!entry.getValue().isArray()) {
|
||||
continue;
|
||||
}
|
||||
final List<Path> resolvedInputs = new ArrayList<>();
|
||||
for (JsonNode inputNode : entry.getValue()) {
|
||||
if (!inputNode.isTextual()) {
|
||||
continue;
|
||||
}
|
||||
resolvedInputs.add(assetRoot.resolve(inputNode.asText()).toAbsolutePath().normalize());
|
||||
}
|
||||
result.put(entry.getKey(), List.copyOf(resolvedInputs));
|
||||
}
|
||||
return Map.copyOf(result);
|
||||
}
|
||||
|
||||
private String readText(JsonNode node, String fallback) {
|
||||
if (node == null || !node.isTextual()) {
|
||||
return fallback;
|
||||
}
|
||||
final String text = node.asText();
|
||||
return text == null || text.isBlank() ? fallback : text;
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private record AssetManifest(String name, String type, Preload preload) {
|
||||
}
|
||||
|
||||
@ -84,6 +84,36 @@ assets.badge.managed=Managed
|
||||
assets.badge.orphan=Orphan
|
||||
assets.badge.preload=Preload
|
||||
assets.badge.diagnostics=Diagnostics
|
||||
assets.section.summary=Summary
|
||||
assets.section.runtimeContract=Runtime Contract
|
||||
assets.section.inputsPreview=Inputs / Preview
|
||||
assets.section.diagnostics=Diagnostics
|
||||
assets.section.actions=Actions
|
||||
assets.actions.primary=Primary Actions
|
||||
assets.actions.sensitive=Sensitive Actions
|
||||
assets.action.doctor=Doctor
|
||||
assets.action.build=Build
|
||||
assets.action.adopt=Adopt
|
||||
assets.action.register=Register
|
||||
assets.action.forget=Forget
|
||||
assets.action.remove=Remove
|
||||
assets.label.name=Name
|
||||
assets.label.state=State
|
||||
assets.label.assetId=Asset ID
|
||||
assets.label.type=Type
|
||||
assets.label.location=Location
|
||||
assets.label.format=Format
|
||||
assets.label.codec=Codec
|
||||
assets.label.preload=Preload
|
||||
assets.value.yes=Yes
|
||||
assets.value.no=No
|
||||
assets.inputs.empty=No previewable inputs are currently declared for this asset.
|
||||
assets.diagnostics.empty=No diagnostics are currently attached to this asset.
|
||||
assets.preview.empty=Select an input to preview it here.
|
||||
assets.preview.textError=Unable to read this text-like input for preview.
|
||||
assets.preview.imageError=Unable to decode this image for preview.
|
||||
assets.preview.audioPlaceholder=Audio preview placeholder: {0}
|
||||
assets.preview.genericPlaceholder=Preview placeholder for {0}
|
||||
assets.summary.loading=Hydrating asset workspace state...
|
||||
assets.summary.empty=No assets are currently available.
|
||||
assets.summary.ready=Navigator ready with {0} assets.
|
||||
|
||||
@ -272,6 +272,151 @@
|
||||
-fx-padding: 10 0 0 0;
|
||||
}
|
||||
|
||||
.assets-workspace-details-scroll {
|
||||
-fx-background-color: transparent;
|
||||
-fx-fit-to-width: true;
|
||||
}
|
||||
|
||||
.assets-workspace-details-scroll > .viewport {
|
||||
-fx-background-color: transparent;
|
||||
}
|
||||
|
||||
.assets-workspace-details-content {
|
||||
-fx-padding: 4 0 4 0;
|
||||
}
|
||||
|
||||
.assets-details-section {
|
||||
-fx-background-color: #11151b;
|
||||
-fx-background-radius: 12;
|
||||
-fx-border-radius: 12;
|
||||
-fx-border-color: #26313d;
|
||||
-fx-padding: 12;
|
||||
}
|
||||
|
||||
.assets-details-section-title {
|
||||
-fx-text-fill: #f5f9ff;
|
||||
-fx-font-size: 14px;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.assets-details-subsection-title {
|
||||
-fx-text-fill: #b8d7f6;
|
||||
-fx-font-size: 12px;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.assets-details-section-message {
|
||||
-fx-text-fill: #cbd4de;
|
||||
-fx-font-size: 12px;
|
||||
}
|
||||
|
||||
.assets-details-key {
|
||||
-fx-text-fill: #8fa5bc;
|
||||
-fx-font-size: 11px;
|
||||
-fx-min-width: 88;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.assets-details-value {
|
||||
-fx-text-fill: #eef4fb;
|
||||
-fx-font-size: 12px;
|
||||
}
|
||||
|
||||
.assets-details-role-label {
|
||||
-fx-text-fill: #9fc3e7;
|
||||
-fx-font-size: 11px;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.assets-details-input-button {
|
||||
-fx-background-color: #17202a;
|
||||
-fx-text-fill: #e6eff8;
|
||||
-fx-background-radius: 8;
|
||||
-fx-border-radius: 8;
|
||||
-fx-border-color: #2f4053;
|
||||
-fx-alignment: center-left;
|
||||
}
|
||||
|
||||
.assets-details-input-button-selected {
|
||||
-fx-background-color: #224160;
|
||||
-fx-border-color: #4f8dc3;
|
||||
}
|
||||
|
||||
.assets-details-input-preview-split {
|
||||
-fx-background-color: transparent;
|
||||
}
|
||||
|
||||
.assets-details-preview-pane {
|
||||
-fx-background-color: #0f1318;
|
||||
-fx-background-radius: 10;
|
||||
-fx-padding: 10;
|
||||
-fx-spacing: 10;
|
||||
}
|
||||
|
||||
.assets-details-preview-title {
|
||||
-fx-text-fill: #f2f8ff;
|
||||
-fx-font-size: 13px;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.assets-details-preview-text {
|
||||
-fx-control-inner-background: #10161d;
|
||||
-fx-text-fill: #e4edf6;
|
||||
-fx-highlight-fill: #2c5e91;
|
||||
}
|
||||
|
||||
.assets-details-diagnostic-card {
|
||||
-fx-background-radius: 10;
|
||||
-fx-border-radius: 10;
|
||||
-fx-border-width: 1;
|
||||
-fx-padding: 10;
|
||||
}
|
||||
|
||||
.assets-details-diagnostic-blocker {
|
||||
-fx-background-color: #3d181b;
|
||||
-fx-border-color: #c15b64;
|
||||
}
|
||||
|
||||
.assets-details-diagnostic-warning {
|
||||
-fx-background-color: #3e3117;
|
||||
-fx-border-color: #c89b43;
|
||||
}
|
||||
|
||||
.assets-details-diagnostic-hint {
|
||||
-fx-background-color: #173241;
|
||||
-fx-border-color: #5f99c5;
|
||||
}
|
||||
|
||||
.assets-details-diagnostic-severity {
|
||||
-fx-text-fill: #ffffff;
|
||||
-fx-font-size: 11px;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.assets-details-diagnostic-message {
|
||||
-fx-text-fill: #eef4fb;
|
||||
-fx-font-size: 12px;
|
||||
}
|
||||
|
||||
.assets-details-action-button {
|
||||
-fx-background-radius: 10;
|
||||
-fx-border-radius: 10;
|
||||
-fx-border-width: 1;
|
||||
-fx-padding: 8 12 8 12;
|
||||
}
|
||||
|
||||
.assets-details-action-button-primary {
|
||||
-fx-background-color: #173322;
|
||||
-fx-border-color: #2f8f59;
|
||||
-fx-text-fill: #c7f8d7;
|
||||
}
|
||||
|
||||
.assets-details-action-button-sensitive {
|
||||
-fx-background-color: #3b191c;
|
||||
-fx-border-color: #c7606a;
|
||||
-fx-text-fill: #ffd6d9;
|
||||
}
|
||||
|
||||
.studio-project-launcher {
|
||||
-fx-background-color: linear-gradient(to bottom, #20242c, #14181d);
|
||||
}
|
||||
|
||||
@ -0,0 +1,42 @@
|
||||
package p.studio.workspaces.assets;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
final class AssetWorkspaceActionSetBuilderTest {
|
||||
@Test
|
||||
void managedAssetsExposeDoctorBuildAndSensitiveMutations() {
|
||||
final AssetWorkspaceActionSet actionSet = AssetWorkspaceActionSetBuilder.forAsset(new AssetWorkspaceAssetSummary(
|
||||
new AssetWorkspaceSelectionKey.ManagedAsset(42),
|
||||
"ui_atlas",
|
||||
AssetWorkspaceAssetState.MANAGED,
|
||||
42,
|
||||
"image_bank",
|
||||
Path.of("/tmp/assets/ui_atlas"),
|
||||
true,
|
||||
false));
|
||||
|
||||
assertEquals(List.of(AssetWorkspaceAction.DOCTOR, AssetWorkspaceAction.BUILD), actionSet.primaryActions());
|
||||
assertEquals(List.of(AssetWorkspaceAction.FORGET, AssetWorkspaceAction.REMOVE), actionSet.sensitiveActions());
|
||||
}
|
||||
|
||||
@Test
|
||||
void orphanAssetsExposeAdoptAndRegisterOnly() {
|
||||
final AssetWorkspaceActionSet actionSet = AssetWorkspaceActionSetBuilder.forAsset(new AssetWorkspaceAssetSummary(
|
||||
new AssetWorkspaceSelectionKey.OrphanAsset(Path.of("/tmp/assets/ui_sounds")),
|
||||
"ui_sounds",
|
||||
AssetWorkspaceAssetState.ORPHAN,
|
||||
null,
|
||||
"sound_bank",
|
||||
Path.of("/tmp/assets/ui_sounds"),
|
||||
false,
|
||||
false));
|
||||
|
||||
assertEquals(List.of(AssetWorkspaceAction.ADOPT, AssetWorkspaceAction.REGISTER), actionSet.primaryActions());
|
||||
assertEquals(List.of(), actionSet.sensitiveActions());
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,7 @@ import p.studio.projects.ProjectReference;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
@ -82,6 +83,57 @@ final class FileSystemAssetWorkspaceServiceTest {
|
||||
assertEquals(null, orphan.assetId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadsSelectedAssetDetailsIncludingContractAndInputs() throws Exception {
|
||||
final Path projectRoot = tempDir.resolve("main");
|
||||
final Path assetsRoot = projectRoot.resolve("assets");
|
||||
final Path managedRoot = assetsRoot.resolve("ui").resolve("atlas");
|
||||
Files.createDirectories(managedRoot.resolve("sprites"));
|
||||
Files.createDirectories(managedRoot.resolve("palettes"));
|
||||
Files.createDirectories(assetsRoot.resolve(".prometeu"));
|
||||
Files.writeString(managedRoot.resolve("sprites").resolve("confirm.png"), "fake-image");
|
||||
Files.writeString(managedRoot.resolve("palettes").resolve("ui_main.pal"), "00 11 22");
|
||||
Files.writeString(managedRoot.resolve("asset.json"), """
|
||||
{
|
||||
"name": "ui_atlas",
|
||||
"type": "image_bank",
|
||||
"inputs": {
|
||||
"sprites": ["sprites/confirm.png"],
|
||||
"palettes": ["palettes/ui_main.pal"]
|
||||
},
|
||||
"output": {
|
||||
"format": "TILES/indexed_v1",
|
||||
"codec": "RAW"
|
||||
},
|
||||
"preload": { "enabled": true }
|
||||
}
|
||||
""");
|
||||
Files.writeString(assetsRoot.resolve(".prometeu").resolve("index.json"), """
|
||||
{
|
||||
"schema_version": 1,
|
||||
"next_asset_id": 2,
|
||||
"assets": [
|
||||
{
|
||||
"asset_id": 1,
|
||||
"root": "ui/atlas"
|
||||
}
|
||||
]
|
||||
}
|
||||
""");
|
||||
|
||||
final FileSystemAssetWorkspaceService service = new FileSystemAssetWorkspaceService();
|
||||
|
||||
final AssetWorkspaceAssetDetails details = service.loadAssetDetails(
|
||||
project("Main", projectRoot),
|
||||
new AssetWorkspaceSelectionKey.ManagedAsset(1));
|
||||
|
||||
assertEquals("ui_atlas", details.summary().assetName());
|
||||
assertEquals("TILES/indexed_v1", details.outputFormat());
|
||||
assertEquals("RAW", details.outputCodec());
|
||||
assertEquals(List.of(managedRoot.resolve("sprites/confirm.png").toAbsolutePath().normalize()), details.inputsByRole().get("sprites"));
|
||||
assertEquals(List.of(managedRoot.resolve("palettes/ui_main.pal").toAbsolutePath().normalize()), details.inputsByRole().get("palettes"));
|
||||
}
|
||||
|
||||
private ProjectReference project(String name, Path root) {
|
||||
return new ProjectReference(name, "1.0.0", "pbs", 1, root);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user