implements PR-05d assets activity progress and logs integration

This commit is contained in:
bQUARKz 2026-03-11 16:50:49 +00:00
parent 512e4eb49e
commit f7e0a4043d
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
10 changed files with 415 additions and 3 deletions

View File

@ -0,0 +1,19 @@
package p.studio.controls.shell;
import java.util.Objects;
public record StudioActivityEntry(
String source,
String message,
StudioActivityEntrySeverity severity,
boolean sticky) {
public StudioActivityEntry {
source = Objects.requireNonNull(source, "source").trim();
message = Objects.requireNonNull(message, "message").trim();
Objects.requireNonNull(severity, "severity");
if (source.isBlank() || message.isBlank()) {
throw new IllegalArgumentException("source and message must not be blank");
}
}
}

View File

@ -0,0 +1,8 @@
package p.studio.controls.shell;
public enum StudioActivityEntrySeverity {
INFO,
SUCCESS,
WARNING,
ERROR
}

View File

@ -0,0 +1,30 @@
package p.studio.controls.shell;
import p.studio.events.*;
import java.util.Optional;
public final class StudioActivityEventMapper {
private StudioActivityEventMapper() {
}
public static Optional<StudioActivityEntry> map(StudioEvent event) {
return switch (event) {
case StudioProjectOpenedEvent opened ->
Optional.of(new StudioActivityEntry("Studio", "Project opened: " + opened.project().name(), StudioActivityEntrySeverity.SUCCESS, false));
case StudioProjectLoadingStartedEvent started ->
Optional.of(new StudioActivityEntry("Studio", "Project loading started: " + started.project().name(), StudioActivityEntrySeverity.INFO, false));
case StudioProjectLoadingCompletedEvent completed ->
Optional.of(new StudioActivityEntry("Studio", "Project ready: " + completed.project().name(), StudioActivityEntrySeverity.SUCCESS, false));
case StudioProjectLoadingFailedEvent failed ->
Optional.of(new StudioActivityEntry("Studio", failed.message(), StudioActivityEntrySeverity.ERROR, true));
case StudioAssetsWorkspaceRefreshStartedEvent ignored ->
Optional.of(new StudioActivityEntry("Assets", "Asset scan started", StudioActivityEntrySeverity.INFO, false));
case StudioAssetsWorkspaceRefreshedEvent refreshed ->
Optional.of(new StudioActivityEntry("Assets", refreshed.assetCount() + " assets loaded", StudioActivityEntrySeverity.SUCCESS, false));
case StudioAssetsWorkspaceRefreshFailedEvent failed ->
Optional.of(new StudioActivityEntry("Assets", failed.message(), StudioActivityEntrySeverity.ERROR, true));
default -> Optional.empty();
};
}
}

View File

@ -0,0 +1,166 @@
package p.studio.controls.shell;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.ProgressBar;
import javafx.scene.layout.VBox;
import p.studio.Container;
import p.studio.controls.lifecycle.StudioControlLifecycle;
import p.studio.controls.lifecycle.StudioControlLifecycleSupport;
import p.studio.events.*;
import p.studio.utilities.events.EventSubscription;
import p.studio.utilities.i18n.I18n;
import java.util.ArrayList;
import java.util.List;
public final class StudioActivityFeedControl extends VBox implements StudioControlLifecycle {
private final ObservableList<StudioActivityEntry> entries = FXCollections.observableArrayList();
private final List<EventSubscription> subscriptions = new ArrayList<>();
private final Label progressLabel = new Label();
private final ProgressBar progressBar = new ProgressBar();
private final ListView<StudioActivityEntry> listView = new ListView<>(entries);
public StudioActivityFeedControl() {
StudioControlLifecycleSupport.install(this, this);
getStyleClass().add("studio-activity-feed");
setSpacing(10);
setPadding(new Insets(12));
progressLabel.setText(Container.i18n().text(I18n.ACTIVITY_PROGRESS_IDLE));
progressLabel.getStyleClass().add("studio-activity-progress-label");
progressBar.getStyleClass().add("studio-activity-progress-bar");
progressBar.setVisible(false);
progressBar.setManaged(false);
listView.getStyleClass().add("studio-activity-list");
listView.setCellFactory(ignored -> new ActivityCell());
getChildren().addAll(progressLabel, progressBar, listView);
}
@Override
public void subscribe() {
final StudioEventBus eventBus = Container.events();
subscriptions.add(eventBus.subscribe(StudioProjectOpenedEvent.class, this::onEvent));
subscriptions.add(eventBus.subscribe(StudioProjectLoadingStartedEvent.class, this::onEvent));
subscriptions.add(eventBus.subscribe(StudioProjectLoadingProgressEvent.class, this::onProjectLoadingProgress));
subscriptions.add(eventBus.subscribe(StudioProjectLoadingCompletedEvent.class, this::onProjectLoadingCompleted));
subscriptions.add(eventBus.subscribe(StudioProjectLoadingFailedEvent.class, this::onProjectLoadingFailed));
subscriptions.add(eventBus.subscribe(StudioAssetsWorkspaceRefreshStartedEvent.class, this::onAssetsRefreshStarted));
subscriptions.add(eventBus.subscribe(StudioAssetsWorkspaceRefreshedEvent.class, this::onAssetsRefreshFinished));
subscriptions.add(eventBus.subscribe(StudioAssetsWorkspaceRefreshFailedEvent.class, this::onAssetsRefreshFailed));
}
@Override
public void unsubscribe() {
subscriptions.forEach(EventSubscription::unsubscribe);
subscriptions.clear();
}
private void onEvent(StudioEvent event) {
StudioActivityEventMapper.map(event).ifPresent(this::appendEntry);
}
private void onProjectLoadingProgress(StudioProjectLoadingProgressEvent event) {
Platform.runLater(() -> {
progressLabel.setText(event.message());
progressBar.setVisible(true);
progressBar.setManaged(true);
if (event.indeterminate()) {
progressBar.setProgress(ProgressBar.INDETERMINATE_PROGRESS);
} else {
progressBar.setProgress(event.progress());
}
});
}
private void onProjectLoadingCompleted(StudioProjectLoadingCompletedEvent event) {
onEvent(event);
clearProgress();
}
private void onProjectLoadingFailed(StudioProjectLoadingFailedEvent event) {
onEvent(event);
clearProgress();
}
private void onAssetsRefreshStarted(StudioAssetsWorkspaceRefreshStartedEvent event) {
onEvent(event);
Platform.runLater(() -> {
progressLabel.setText(Container.i18n().text(I18n.ACTIVITY_PROGRESS_ASSETS_REFRESH));
progressBar.setVisible(true);
progressBar.setManaged(true);
progressBar.setProgress(ProgressBar.INDETERMINATE_PROGRESS);
});
}
private void onAssetsRefreshFinished(StudioAssetsWorkspaceRefreshedEvent event) {
onEvent(event);
clearProgress();
}
private void onAssetsRefreshFailed(StudioAssetsWorkspaceRefreshFailedEvent event) {
onEvent(event);
clearProgress();
}
private void clearProgress() {
Platform.runLater(() -> {
progressLabel.setText(Container.i18n().text(I18n.ACTIVITY_PROGRESS_IDLE));
progressBar.setVisible(false);
progressBar.setManaged(false);
});
}
private void appendEntry(StudioActivityEntry entry) {
Platform.runLater(() -> {
if (!entries.isEmpty()) {
final StudioActivityEntry last = entries.getLast();
if (last.source().equals(entry.source())
&& last.message().equals(entry.message())
&& last.severity() == entry.severity()) {
return;
}
}
entries.addFirst(entry);
while (entries.size() > 100) {
entries.removeLast();
}
});
}
private static final class ActivityCell extends ListCell<StudioActivityEntry> {
@Override
protected void updateItem(StudioActivityEntry item, boolean empty) {
super.updateItem(item, empty);
getStyleClass().removeAll(
"studio-activity-cell-info",
"studio-activity-cell-success",
"studio-activity-cell-warning",
"studio-activity-cell-error");
if (empty || item == null) {
setText(null);
setGraphic(null);
return;
}
final Label source = new Label(item.source());
source.getStyleClass().add("studio-activity-cell-source");
final Label message = new Label(item.message());
message.getStyleClass().add("studio-activity-cell-message");
message.setWrapText(true);
final VBox content = new VBox(4, source, message);
content.setAlignment(Pos.CENTER_LEFT);
setGraphic(content);
getStyleClass().add("studio-activity-cell-" + item.severity().name().toLowerCase());
}
}
}

View File

@ -117,6 +117,10 @@ public enum I18n {
ASSETS_LABEL_PRELOAD("assets.label.preload"), ASSETS_LABEL_PRELOAD("assets.label.preload"),
ASSETS_VALUE_YES("assets.value.yes"), ASSETS_VALUE_YES("assets.value.yes"),
ASSETS_VALUE_NO("assets.value.no"), ASSETS_VALUE_NO("assets.value.no"),
ASSETS_PROGRESS_IDLE("assets.progress.idle"),
ASSETS_PROGRESS_REFRESHING("assets.progress.refreshing"),
ASSETS_PROGRESS_LOADING_DETAILS("assets.progress.loadingDetails"),
ASSETS_LOGS_TITLE("assets.logs.title"),
ASSETS_INPUTS_EMPTY("assets.inputs.empty"), ASSETS_INPUTS_EMPTY("assets.inputs.empty"),
ASSETS_DIAGNOSTICS_EMPTY("assets.diagnostics.empty"), ASSETS_DIAGNOSTICS_EMPTY("assets.diagnostics.empty"),
ASSETS_PREVIEW_EMPTY("assets.preview.empty"), ASSETS_PREVIEW_EMPTY("assets.preview.empty"),
@ -124,6 +128,8 @@ public enum I18n {
ASSETS_PREVIEW_IMAGE_ERROR("assets.preview.imageError"), ASSETS_PREVIEW_IMAGE_ERROR("assets.preview.imageError"),
ASSETS_PREVIEW_AUDIO_PLACEHOLDER("assets.preview.audioPlaceholder"), ASSETS_PREVIEW_AUDIO_PLACEHOLDER("assets.preview.audioPlaceholder"),
ASSETS_PREVIEW_GENERIC_PLACEHOLDER("assets.preview.genericPlaceholder"), ASSETS_PREVIEW_GENERIC_PLACEHOLDER("assets.preview.genericPlaceholder"),
ACTIVITY_PROGRESS_IDLE("activity.progress.idle"),
ACTIVITY_PROGRESS_ASSETS_REFRESH("activity.progress.assetsRefresh"),
ASSETS_SUMMARY_LOADING("assets.summary.loading"), ASSETS_SUMMARY_LOADING("assets.summary.loading"),
ASSETS_SUMMARY_EMPTY("assets.summary.empty"), ASSETS_SUMMARY_EMPTY("assets.summary.empty"),
ASSETS_SUMMARY_READY("assets.summary.ready"), ASSETS_SUMMARY_READY("assets.summary.ready"),

View File

@ -43,7 +43,7 @@ public final class MainView extends BorderPane {
setRight(new StudioRightUtilityPanelControl( setRight(new StudioRightUtilityPanelControl(
runSurface, runSurface,
Container.i18n().bind(I18n.SHELL_ACTIVITY), Container.i18n().bind(I18n.SHELL_ACTIVITY),
StudioRightUtilityPanelControl.createPlaceholderContent(Container.i18n().bind(I18n.SHELL_ACTIVITY)))); new StudioActivityFeedControl()));
// default // default
workspaceRail.select(WorkspaceId.ASSETS); workspaceRail.select(WorkspaceId.ASSETS);

View File

@ -36,9 +36,12 @@ public final class AssetWorkspace implements Workspace {
private final FlowPane filterBar = new FlowPane(); private final FlowPane filterBar = new FlowPane();
private final Label navigatorStateLabel = new Label(); private final Label navigatorStateLabel = new Label();
private final VBox navigatorContent = new VBox(8); private final VBox navigatorContent = new VBox(8);
private final Label inlineProgressLabel = new Label();
private final ProgressBar inlineProgressBar = new ProgressBar();
private final VBox detailsContent = new VBox(12); private final VBox detailsContent = new VBox(12);
private final Label workspaceSummaryLabel = new Label(); private final Label workspaceSummaryLabel = new Label();
private final TextArea logsArea = new TextArea();
private final Map<AssetNavigatorFilter, ToggleButton> filterButtons = new EnumMap<>(AssetNavigatorFilter.class); private final Map<AssetNavigatorFilter, ToggleButton> filterButtons = new EnumMap<>(AssetNavigatorFilter.class);
private final EnumSet<AssetNavigatorFilter> activeFilters = EnumSet.noneOf(AssetNavigatorFilter.class); private final EnumSet<AssetNavigatorFilter> activeFilters = EnumSet.noneOf(AssetNavigatorFilter.class);
@ -61,6 +64,7 @@ public final class AssetWorkspace implements Workspace {
root.getStyleClass().add("assets-workspace"); root.getStyleClass().add("assets-workspace");
root.setCenter(buildLayout()); root.setCenter(buildLayout());
root.setBottom(buildLogsPane());
renderState(); renderState();
} }
@ -88,7 +92,15 @@ public final class AssetWorkspace implements Workspace {
return state; return state;
} }
private SplitPane buildLayout() { private VBox buildLayout() {
inlineProgressLabel.getStyleClass().add("assets-workspace-inline-progress-label");
inlineProgressLabel.setText(Container.i18n().text(I18n.ASSETS_PROGRESS_IDLE));
inlineProgressBar.getStyleClass().add("assets-workspace-inline-progress-bar");
inlineProgressBar.setVisible(false);
inlineProgressBar.setManaged(false);
final VBox topProgress = new VBox(6, inlineProgressLabel, inlineProgressBar);
topProgress.getStyleClass().add("assets-workspace-inline-progress");
final VBox navigatorPane = new VBox(8); final VBox navigatorPane = new VBox(8);
navigatorPane.getStyleClass().add("assets-workspace-pane"); navigatorPane.getStyleClass().add("assets-workspace-pane");
final Label navigatorTitle = new Label(); final Label navigatorTitle = new Label();
@ -133,7 +145,24 @@ public final class AssetWorkspace implements Workspace {
final SplitPane splitPane = new SplitPane(navigatorPane, detailsPane); final SplitPane splitPane = new SplitPane(navigatorPane, detailsPane);
splitPane.setDividerPositions(0.34); splitPane.setDividerPositions(0.34);
splitPane.getStyleClass().add("assets-workspace-split"); splitPane.getStyleClass().add("assets-workspace-split");
return splitPane; final VBox layout = new VBox(10, topProgress, splitPane);
VBox.setVgrow(splitPane, Priority.ALWAYS);
return layout;
}
private TitledPane buildLogsPane() {
logsArea.setEditable(false);
logsArea.setWrapText(true);
logsArea.setPrefRowCount(8);
logsArea.getStyleClass().add("assets-workspace-logs");
final TitledPane pane = new TitledPane();
pane.textProperty().bind(Container.i18n().bind(I18n.ASSETS_LOGS_TITLE));
pane.setContent(logsArea);
pane.setCollapsible(true);
pane.setExpanded(true);
pane.getStyleClass().add("assets-workspace-logs-pane");
return pane;
} }
private void configureFilterBar() { private void configureFilterBar() {
@ -169,6 +198,8 @@ public final class AssetWorkspace implements Workspace {
selectedAssetDetails = null; selectedAssetDetails = null;
detailsErrorMessage = null; detailsErrorMessage = null;
selectedPreviewInput = null; selectedPreviewInput = null;
setInlineProgress(Container.i18n().text(I18n.ASSETS_PROGRESS_REFRESHING), ProgressBar.INDETERMINATE_PROGRESS, true);
appendLog("Assets refresh started.");
renderState(); renderState();
workspaceBus.publish(new StudioAssetsWorkspaceRefreshStartedEvent(projectReference)); workspaceBus.publish(new StudioAssetsWorkspaceRefreshStartedEvent(projectReference));
@ -179,12 +210,16 @@ public final class AssetWorkspace implements Workspace {
state = AssetWorkspaceState.error(state, rootCauseMessage(throwable)); state = AssetWorkspaceState.error(state, rootCauseMessage(throwable));
detailsStatus = AssetWorkspaceDetailsStatus.ERROR; detailsStatus = AssetWorkspaceDetailsStatus.ERROR;
detailsErrorMessage = state.errorMessage(); detailsErrorMessage = state.errorMessage();
setInlineProgress(Container.i18n().text(I18n.ASSETS_PROGRESS_IDLE), 0, false);
appendLog("Assets refresh failed: " + state.errorMessage());
renderState(); renderState();
workspaceBus.publish(new StudioAssetsWorkspaceRefreshFailedEvent(projectReference, state.errorMessage())); workspaceBus.publish(new StudioAssetsWorkspaceRefreshFailedEvent(projectReference, state.errorMessage()));
return; return;
} }
state = AssetWorkspaceState.ready(snapshot.assets(), state.selectedKey()); state = AssetWorkspaceState.ready(snapshot.assets(), state.selectedKey());
setInlineProgress(Container.i18n().text(I18n.ASSETS_PROGRESS_IDLE), 0, false);
appendLog("Assets refresh completed: " + state.assets().size() + " assets.");
renderState(); renderState();
workspaceBus.publish(new StudioAssetsWorkspaceRefreshedEvent(projectReference, state.assets().size())); workspaceBus.publish(new StudioAssetsWorkspaceRefreshedEvent(projectReference, state.assets().size()));
state.selectedAsset().ifPresent(asset -> { state.selectedAsset().ifPresent(asset -> {
@ -199,6 +234,8 @@ public final class AssetWorkspace implements Workspace {
selectedAssetDetails = null; selectedAssetDetails = null;
detailsErrorMessage = null; detailsErrorMessage = null;
selectedPreviewInput = null; selectedPreviewInput = null;
setInlineProgress(Container.i18n().text(I18n.ASSETS_PROGRESS_LOADING_DETAILS), ProgressBar.INDETERMINATE_PROGRESS, true);
appendLog("Loading details for " + selectionKey.stableKey() + ".");
renderState(); renderState();
CompletableFuture CompletableFuture
@ -211,6 +248,8 @@ public final class AssetWorkspace implements Workspace {
detailsStatus = AssetWorkspaceDetailsStatus.ERROR; detailsStatus = AssetWorkspaceDetailsStatus.ERROR;
detailsErrorMessage = rootCauseMessage(throwable); detailsErrorMessage = rootCauseMessage(throwable);
selectedAssetDetails = null; selectedAssetDetails = null;
setInlineProgress(Container.i18n().text(I18n.ASSETS_PROGRESS_IDLE), 0, false);
appendLog("Asset details failed: " + detailsErrorMessage);
renderState(); renderState();
return; return;
} }
@ -218,6 +257,8 @@ public final class AssetWorkspace implements Workspace {
selectedAssetDetails = details; selectedAssetDetails = details;
detailsStatus = AssetWorkspaceDetailsStatus.READY; detailsStatus = AssetWorkspaceDetailsStatus.READY;
selectedPreviewInput = firstPreviewInput(details); selectedPreviewInput = firstPreviewInput(details);
setInlineProgress(Container.i18n().text(I18n.ASSETS_PROGRESS_IDLE), 0, false);
appendLog("Asset details ready for " + details.summary().assetName() + ".");
renderState(); renderState();
})); }));
} }
@ -617,6 +658,7 @@ public final class AssetWorkspace implements Workspace {
private void selectAsset(AssetWorkspaceSelectionKey selectionKey) { private void selectAsset(AssetWorkspaceSelectionKey selectionKey) {
state = state.withSelection(selectionKey); state = state.withSelection(selectionKey);
appendLog("Selected asset " + selectionKey.stableKey() + ".");
renderState(); renderState();
workspaceBus.publish(new StudioAssetsWorkspaceSelectionChangedEvent(projectReference, selectionKey)); workspaceBus.publish(new StudioAssetsWorkspaceSelectionChangedEvent(projectReference, selectionKey));
loadSelectedAssetDetails(selectionKey); loadSelectedAssetDetails(selectionKey);
@ -698,6 +740,17 @@ public final class AssetWorkspace implements Workspace {
return projectReference.rootPath().resolve("assets").toAbsolutePath().normalize(); return projectReference.rootPath().resolve("assets").toAbsolutePath().normalize();
} }
private void setInlineProgress(String message, double progress, boolean visible) {
inlineProgressLabel.setText(message);
inlineProgressBar.setVisible(visible);
inlineProgressBar.setManaged(visible);
inlineProgressBar.setProgress(progress);
}
private void appendLog(String message) {
logsArea.appendText(message + System.lineSeparator());
}
private String rootCauseMessage(Throwable throwable) { private String rootCauseMessage(Throwable throwable) {
Throwable current = throwable; Throwable current = throwable;
while (current.getCause() != null) { while (current.getCause() != null) {

View File

@ -107,6 +107,10 @@ assets.label.codec=Codec
assets.label.preload=Preload assets.label.preload=Preload
assets.value.yes=Yes assets.value.yes=Yes
assets.value.no=No assets.value.no=No
assets.progress.idle=Assets workspace idle.
assets.progress.refreshing=Refreshing assets...
assets.progress.loadingDetails=Loading selected asset details...
assets.logs.title=Logs
assets.inputs.empty=No previewable inputs are currently declared for this asset. assets.inputs.empty=No previewable inputs are currently declared for this asset.
assets.diagnostics.empty=No diagnostics are currently attached to 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.empty=Select an input to preview it here.
@ -114,6 +118,8 @@ assets.preview.textError=Unable to read this text-like input for preview.
assets.preview.imageError=Unable to decode this image for preview. assets.preview.imageError=Unable to decode this image for preview.
assets.preview.audioPlaceholder=Audio preview placeholder: {0} assets.preview.audioPlaceholder=Audio preview placeholder: {0}
assets.preview.genericPlaceholder=Preview placeholder for {0} assets.preview.genericPlaceholder=Preview placeholder for {0}
activity.progress.idle=No background work in progress.
activity.progress.assetsRefresh=Refreshing asset workspace...
assets.summary.loading=Hydrating asset workspace state... assets.summary.loading=Hydrating asset workspace state...
assets.summary.empty=No assets are currently available. assets.summary.empty=No assets are currently available.
assets.summary.ready=Navigator ready with {0} assets. assets.summary.ready=Navigator ready with {0} assets.

View File

@ -113,6 +113,56 @@
-fx-padding: 16; -fx-padding: 16;
} }
.studio-activity-feed {
-fx-background-color: transparent;
}
.studio-activity-progress-label {
-fx-text-fill: #d4e0ec;
-fx-font-size: 12px;
}
.studio-activity-progress-bar {
-fx-accent: #5cb6ff;
}
.studio-activity-list {
-fx-background-color: transparent;
-fx-control-inner-background: #12161b;
}
.studio-activity-list .list-cell {
-fx-background-color: transparent;
-fx-padding: 8;
}
.studio-activity-cell-source {
-fx-text-fill: #8db7e1;
-fx-font-size: 11px;
-fx-font-weight: bold;
}
.studio-activity-cell-message {
-fx-text-fill: #f1f6fb;
-fx-font-size: 12px;
}
.studio-activity-cell-info {
-fx-background-color: #16222f;
}
.studio-activity-cell-success {
-fx-background-color: #143020;
}
.studio-activity-cell-warning {
-fx-background-color: #3f3115;
}
.studio-activity-cell-error {
-fx-background-color: #40191d;
}
.assets-workspace { .assets-workspace {
-fx-background-color: #17191d; -fx-background-color: #17191d;
} }
@ -127,6 +177,19 @@
-fx-background-color: #1b1f25; -fx-background-color: #1b1f25;
} }
.assets-workspace-inline-progress {
-fx-padding: 16 18 0 18;
}
.assets-workspace-inline-progress-label {
-fx-text-fill: #d4dce5;
-fx-font-size: 12px;
}
.assets-workspace-inline-progress-bar {
-fx-accent: #5cb6ff;
}
.assets-workspace-pane-title { .assets-workspace-pane-title {
-fx-text-fill: #f3f7fb; -fx-text-fill: #f3f7fb;
-fx-font-size: 15px; -fx-font-size: 15px;
@ -417,6 +480,16 @@
-fx-text-fill: #ffd6d9; -fx-text-fill: #ffd6d9;
} }
.assets-workspace-logs-pane {
-fx-collapsible: true;
}
.assets-workspace-logs {
-fx-control-inner-background: #0f1318;
-fx-text-fill: #dbe4ed;
-fx-highlight-fill: #2c5e91;
}
.studio-project-launcher { .studio-project-launcher {
-fx-background-color: linear-gradient(to bottom, #20242c, #14181d); -fx-background-color: linear-gradient(to bottom, #20242c, #14181d);
} }

View File

@ -0,0 +1,51 @@
package p.studio.controls.shell;
import org.junit.jupiter.api.Test;
import p.studio.events.StudioAssetsWorkspaceRefreshFailedEvent;
import p.studio.events.StudioAssetsWorkspaceRefreshedEvent;
import p.studio.events.StudioProjectOpenedEvent;
import p.studio.projects.ProjectReference;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
final class StudioActivityEventMapperTest {
@Test
void mapsProjectOpenedToSuccessEntry() {
final StudioActivityEntry entry = StudioActivityEventMapper
.map(new StudioProjectOpenedEvent(project()))
.orElseThrow();
assertEquals("Studio", entry.source());
assertEquals(StudioActivityEntrySeverity.SUCCESS, entry.severity());
assertTrue(entry.message().contains("Project opened"));
}
@Test
void mapsAssetsRefreshedToSuccessEntry() {
final StudioActivityEntry entry = StudioActivityEventMapper
.map(new StudioAssetsWorkspaceRefreshedEvent(project(), 7))
.orElseThrow();
assertEquals("Assets", entry.source());
assertEquals(StudioActivityEntrySeverity.SUCCESS, entry.severity());
assertEquals("7 assets loaded", entry.message());
}
@Test
void mapsRefreshFailureToStickyErrorEntry() {
final StudioActivityEntry entry = StudioActivityEventMapper
.map(new StudioAssetsWorkspaceRefreshFailedEvent(project(), "Refresh failed"))
.orElseThrow();
assertEquals(StudioActivityEntrySeverity.ERROR, entry.severity());
assertTrue(entry.sticky());
assertEquals("Refresh failed", entry.message());
}
private ProjectReference project() {
return new ProjectReference("Main", "1.0.0", "pbs", 1, Path.of("/tmp/main"));
}
}