diff --git a/owlplug-client/src/main/java/com/owlplug/core/components/ApplicationDefaults.java b/owlplug-client/src/main/java/com/owlplug/core/components/ApplicationDefaults.java index 4047aa7f..2ee0761b 100644 --- a/owlplug-client/src/main/java/com/owlplug/core/components/ApplicationDefaults.java +++ b/owlplug-client/src/main/java/com/owlplug/core/components/ApplicationDefaults.java @@ -119,6 +119,7 @@ public class ApplicationDefaults { public static final String SYNC_FILE_STAT_KEY = "SYNC_FILE_STAT_KEY"; public static final String TELEMETRY_ENABLED_KEY = "TELEMETRY_ENABLED_KEY"; public static final String TELEMETRY_USER_ID_KEY = "TELEMETRY_USER_ID_KEY"; + public static final String LAST_PLUGIN_SCAN_DATE_KEY = "LAST_PLUGIN_SCAN_DATE_KEY"; /** * Creates a new ApplicationDefaults. diff --git a/owlplug-client/src/main/java/com/owlplug/core/controllers/HomeController.java b/owlplug-client/src/main/java/com/owlplug/core/controllers/HomeController.java new file mode 100644 index 00000000..2d0245db --- /dev/null +++ b/owlplug-client/src/main/java/com/owlplug/core/controllers/HomeController.java @@ -0,0 +1,370 @@ +/* OwlPlug + * Copyright (C) 2021 Arthur + * + * This file is part of OwlPlug. + * + * OwlPlug is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 + * as published by the Free Software Foundation. + * + * OwlPlug is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with OwlPlug. If not, see . + */ + +package com.owlplug.core.controllers; + +import com.owlplug.core.components.ApplicationDefaults; +import com.owlplug.core.utils.Async; +import com.owlplug.core.utils.FX; +import com.owlplug.core.utils.TimeUtils; +import com.owlplug.plugin.controllers.PluginsController; +import com.owlplug.plugin.events.PluginScanCompletedEvent; +import com.owlplug.plugin.events.PluginScanStartedEvent; +import com.owlplug.plugin.events.PluginUpdateEvent; +import com.owlplug.plugin.model.FileStat; +import com.owlplug.plugin.model.PluginFormat; +import com.owlplug.plugin.repositories.FileStatRepository; +import com.owlplug.plugin.repositories.PluginRepository; +import com.owlplug.plugin.services.PluginService; +import com.owlplug.project.model.LookupResult; +import com.owlplug.project.repositories.DawProjectRepository; +import com.owlplug.project.repositories.PluginLookupRepository; +import com.owlplug.project.services.ProjectService; +import java.util.Date; +import java.util.List; +import javafx.animation.RotateTransition; +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.util.Duration; +import javafx.scene.chart.BarChart; +import javafx.scene.chart.CategoryAxis; +import javafx.scene.chart.NumberAxis; +import javafx.scene.chart.XYChart; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.layout.HBox; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.scene.shape.Rectangle; +import org.apache.commons.io.FileUtils; +import org.kordamp.ikonli.javafx.FontIcon; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Controller; + +@Controller +public class HomeController extends BaseController { + + @Autowired + private PluginRepository pluginRepository; + @Autowired + private DawProjectRepository dawProjectRepository; + @Autowired + private PluginLookupRepository pluginLookupRepository; + @Autowired + private FileStatRepository fileStatRepository; + @Autowired + private PluginService pluginService; + @Autowired + private ProjectService projectService; + @Autowired + @Lazy + private MainController mainController; + @Autowired + @Lazy + private PluginsController pluginsController; + @Autowired + @Lazy + private OptionsController optionsController; + + @FXML + private Label pluginCountLabel; + @FXML + private Label vst2CountLabel; + @FXML + private Label vst3CountLabel; + @FXML + private Label auCountLabel; + @FXML + private Label lv2CountLabel; + @FXML + private Label projectCountLabel; + @FXML + private Label unresolvedPluginCountLabel; + @FXML + private Label disabledCountLabel; + @FXML + private Label pluginDirectoryCountLabel; + @FXML + private Label projectDirectoryCountLabel; + + @FXML + private HBox scanProgressPane; + @FXML + private FontIcon scanIcon; + + private RotateTransition scanRotation; + + @FXML + private VBox pluginTile; + @FXML + private VBox projectTile; + @FXML + private VBox pluginDirTile; + @FXML + private VBox projectDirTile; + + @FXML + private VBox fileSizeChartContainer; + @FXML + private Label fileSizeEmptyLabel; + + @FXML + private VBox setupPane; + @FXML + private VBox noPluginDirectorySuggestion; + @FXML + private VBox noPluginSuggestion; + @FXML + private VBox noProjectSuggestion; + + @FXML + private Label lastScanLabel; + + @FXML + private TextField pluginSearchField; + + @FXML + private Button scanPluginsButton; + @FXML + private Button exploreButton; + @FXML + private Button syncProjectsButton; + @FXML + private Button settingsButton; + @FXML + private Button setupPluginDirButton; + @FXML + private Button scanSuggestionButton; + @FXML + private Button setupProjectDirButton; + + private BarChart fileSizeChart; + + private final Async.Sequence refreshSequence = new Async.Sequence(); + + /** + * FXML initialize method. + */ + @FXML + public void initialize() { + pluginTile.setOnMouseClicked(e -> mainController.navigateToMainTab(MainController.PLUGINS_TAB_INDEX)); + projectTile.setOnMouseClicked(e -> mainController.navigateToMainTab(MainController.PROJECTS_TAB_INDEX)); + pluginDirTile.setOnMouseClicked(e -> { + mainController.navigateToMainTab(MainController.OPTIONS_TAB_INDEX); + optionsController.navigateToSection(OptionsController.SCAN_SECTION_INDEX); + }); + projectDirTile.setOnMouseClicked(e -> { + mainController.navigateToMainTab(MainController.OPTIONS_TAB_INDEX); + optionsController.navigateToSection(OptionsController.PROJECTS_SECTION_INDEX); + }); + + pluginSearchField.setOnAction(e -> { + mainController.navigateToMainTab(MainController.PLUGINS_TAB_INDEX); + pluginsController.setSearch(pluginSearchField.getText()); + }); + + scanPluginsButton.setOnAction(e -> pluginService.scanPlugins()); + exploreButton.setOnAction(e -> mainController.navigateToMainTab(MainController.EXPLORE_TAB_INDEX)); + syncProjectsButton.setOnAction(e -> projectService.syncProjects()); + settingsButton.setOnAction(e -> mainController.navigateToMainTab(MainController.OPTIONS_TAB_INDEX)); + + setupPluginDirButton.setOnAction(e -> mainController.navigateToMainTab(MainController.OPTIONS_TAB_INDEX)); + scanSuggestionButton.setOnAction(e -> pluginService.scanPlugins()); + setupProjectDirButton.setOnAction(e -> { + mainController.navigateToMainTab(MainController.OPTIONS_TAB_INDEX); + optionsController.navigateToSection(OptionsController.PROJECTS_SECTION_INDEX); + }); + + scanRotation = new RotateTransition(Duration.millis(700), scanIcon); + scanRotation.setByAngle(360); + scanRotation.setCycleCount(RotateTransition.INDEFINITE); + + initFileSizeChart(); + refresh(); + } + + private void initFileSizeChart() { + NumberAxis sizeAxis = new NumberAxis(); + sizeAxis.setTickLabelsVisible(false); + sizeAxis.setTickMarkVisible(false); + sizeAxis.setMinorTickVisible(false); + + CategoryAxis nameAxis = new CategoryAxis(); + + fileSizeChart = new BarChart<>(sizeAxis, nameAxis); + fileSizeChart.setAnimated(true); + fileSizeChart.setLegendVisible(false); + fileSizeChart.setCategoryGap(12); + fileSizeChart.setBarGap(0); + fileSizeChart.setMaxWidth(Double.MAX_VALUE); + fileSizeChart.setMaxHeight(Double.MAX_VALUE); + fileSizeChart.getStyleClass().add("dashboard-bar-chart"); + VBox.setVgrow(fileSizeChart, javafx.scene.layout.Priority.ALWAYS); + + fileSizeChartContainer.getChildren().add(fileSizeChart); + } + + /** + * Refreshes all dashboard statistics and updates setup suggestion visibility. + * Data fetching runs on a virtual thread; UI updates are applied on the FX thread. + */ + public void refresh() { + refreshSequence.supply(() -> new DashboardData( + pluginRepository.count(), + pluginRepository.countByFormat(PluginFormat.VST2), + pluginRepository.countByFormat(PluginFormat.VST3), + pluginRepository.countByFormat(PluginFormat.AU), + pluginRepository.countByFormat(PluginFormat.LV2), + dawProjectRepository.count(), + pluginLookupRepository.countByResult(LookupResult.MISSING), + pluginRepository.countByDisabledTrue(), + pluginService.getDirectoriesExplorationSet().size(), + projectService.getProjectDirectories().size(), + fileStatRepository.findTop5ByParentIsNullOrderByLengthDesc(), + getPreferences().getLong(ApplicationDefaults.LAST_PLUGIN_SCAN_DATE_KEY, 0L) + )).thenAccept(data -> FX.run(() -> { + lastScanLabel.setText(data.lastScanTimestamp() == 0L ? "Never scanned" + : "Last scan: " + TimeUtils.getHumanReadableDurationFrom(new Date(data.lastScanTimestamp()))); + + pluginCountLabel.setText(String.valueOf(data.totalPlugins())); + vst2CountLabel.setText(String.valueOf(data.vst2Count())); + vst3CountLabel.setText(String.valueOf(data.vst3Count())); + auCountLabel.setText(String.valueOf(data.auCount())); + lv2CountLabel.setText(String.valueOf(data.lv2Count())); + projectCountLabel.setText(String.valueOf(data.projectCount())); + unresolvedPluginCountLabel.setText(String.valueOf(data.unresolvedCount())); + disabledCountLabel.setText(String.valueOf(data.disabledCount())); + pluginDirectoryCountLabel.setText(String.valueOf(data.pluginDirectoryCount())); + projectDirectoryCountLabel.setText(String.valueOf(data.projectDirectoryCount())); + + final boolean hasPluginDirectories = data.pluginDirectoryCount() > 0; + final boolean hasProjectDirectories = data.projectDirectoryCount() > 0; + final boolean hasPlugins = data.totalPlugins() > 0; + + setNodeVisible(noPluginDirectorySuggestion, !hasPluginDirectories); + setNodeVisible(noPluginSuggestion, hasPluginDirectories && !hasPlugins); + setNodeVisible(noProjectSuggestion, !hasProjectDirectories); + setNodeVisible(setupPane, !hasPluginDirectories || !hasPlugins || !hasProjectDirectories); + + applyFileSizeChart(data.topStats()); + })); + } + + private void applyFileSizeChart(List topStats) { + final boolean hasData = !topStats.isEmpty(); + + fileSizeEmptyLabel.setVisible(!hasData); + fileSizeEmptyLabel.setManaged(!hasData); + fileSizeChart.setVisible(hasData); + fileSizeChart.setManaged(hasData); + + if (!hasData) { + return; + } + + fileSizeChart.getData().clear(); + + XYChart.Series series = new XYChart.Series<>(); + for (FileStat stat : topStats) { + final double mb = stat.getLength() / (1024.0 * 1024.0); + series.getData().add(new XYChart.Data<>(mb, stat.getName())); + } + fileSizeChart.getData().add(series); + + // Defer label and tooltip attachment until bars are laid out in the scene graph + Platform.runLater(() -> series.getData().forEach(data -> + topStats.stream() + .filter(fs -> fs.getName().equals(data.getYValue())) + .findFirst() + .ifPresent(fs -> { + final String sizeText = FileUtils.byteCountToDisplaySize(fs.getLength()); + Tooltip.install(data.getNode(), new Tooltip(data.getYValue() + " — " + sizeText)); + if (data.getNode() instanceof StackPane bar) { + Label label = new Label(sizeText); + label.getStyleClass().add("dashboard-bar-label"); + label.setMouseTransparent(true); + StackPane.setAlignment(label, Pos.CENTER_RIGHT); + StackPane.setMargin(label, new Insets(0, 6, 0, 0)); + bar.getChildren().add(label); + + // Clip the bar to exactly 20px, centered in the allocated slot + Rectangle clip = new Rectangle(); + clip.setArcWidth(4); + clip.setArcHeight(4); + clip.setHeight(20); + clip.widthProperty().bind(bar.widthProperty()); + clip.yProperty().bind(bar.heightProperty().subtract(20).divide(2)); + bar.setClip(clip); + } + }) + )); + } + + private record DashboardData( + long totalPlugins, + long vst2Count, + long vst3Count, + long auCount, + long lv2Count, + long projectCount, + long unresolvedCount, + long disabledCount, + long pluginDirectoryCount, + long projectDirectoryCount, + List topStats, + long lastScanTimestamp + ) { + } + + + private void setNodeVisible(VBox node, boolean visible) { + node.setVisible(visible); + node.setManaged(visible); + } + + @EventListener + private void handle(PluginScanStartedEvent event) { + FX.run(() -> { + scanProgressPane.setVisible(true); + scanProgressPane.setManaged(true); + scanRotation.play(); + }); + } + + @EventListener + private void handle(PluginScanCompletedEvent event) { + FX.run(() -> { + scanProgressPane.setVisible(false); + scanProgressPane.setManaged(false); + scanRotation.stop(); + refresh(); + }); + } + + @EventListener + private void handle(PluginUpdateEvent event) { + FX.run(this::refresh); + } + +} \ No newline at end of file diff --git a/owlplug-client/src/main/java/com/owlplug/core/controllers/MainController.java b/owlplug-client/src/main/java/com/owlplug/core/controllers/MainController.java index 07d4dd3a..6bea6769 100644 --- a/owlplug-client/src/main/java/com/owlplug/core/controllers/MainController.java +++ b/owlplug-client/src/main/java/com/owlplug/core/controllers/MainController.java @@ -110,6 +110,7 @@ public class MainController extends BaseController { @FXML private Button downloadUpdateButton; + public static int HOME_TAB_INDEX = 0; public static int PLUGINS_TAB_INDEX = 1; public static int EXPLORE_TAB_INDEX = 2; public static int PROJECTS_TAB_INDEX = 3; diff --git a/owlplug-client/src/main/java/com/owlplug/core/controllers/TaskBarController.java b/owlplug-client/src/main/java/com/owlplug/core/controllers/TaskBarController.java index ffacc3d5..da740cd5 100644 --- a/owlplug-client/src/main/java/com/owlplug/core/controllers/TaskBarController.java +++ b/owlplug-client/src/main/java/com/owlplug/core/controllers/TaskBarController.java @@ -15,10 +15,9 @@ * You should have received a copy of the GNU General Public License * along with OwlPlug. If not, see . */ - + package com.owlplug.core.controllers; -import com.owlplug.controls.Popup; import com.owlplug.core.components.TaskRunner; import com.owlplug.core.tasks.AbstractTask; import java.util.ArrayList; @@ -32,17 +31,15 @@ import javafx.beans.property.StringProperty; import javafx.concurrent.Worker.State; import javafx.fxml.FXML; +import javafx.geometry.Side; import javafx.scene.control.Button; +import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; -import javafx.scene.control.ListCell; -import javafx.scene.control.ListView; +import javafx.scene.control.MenuItem; import javafx.scene.control.ProgressBar; import javafx.scene.control.TextArea; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; -import javafx.scene.layout.Region; -import javafx.util.Callback; import javafx.util.Duration; +import org.kordamp.ikonli.javafx.FontIcon; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; @@ -62,69 +59,54 @@ public class TaskBarController extends BaseController { private Button logsButton; private final DoubleProperty progressProperty = new SimpleDoubleProperty(); - private final StringProperty taskNameProperty = new SimpleStringProperty(); private Timeline progressTimeline; - /** * FXML initialize. */ public void initialize() { - taskHistoryButton.setOnAction(e -> openTaskHistory()); resetErrorLog(); - progressProperty.addListener((obs, oldVal, newVal) -> { - updateProgress(newVal.doubleValue()); - }); + progressProperty.addListener((obs, oldVal, newVal) -> updateProgress(newVal.doubleValue())); taskLabel.textProperty().bind(taskNameProperty); + taskNameProperty.set("Application is ready"); } private void openTaskHistory() { + ArrayList tasks = new ArrayList<>(taskRunner.getTaskHistory()); + tasks.addAll(taskRunner.getPendingTasks()); + if (tasks.isEmpty()) { + return; + } - if (!taskRunner.getTaskHistory().isEmpty()) { - ListView list = new ListView<>(); - list.setPrefSize(Region.USE_COMPUTED_SIZE, Region.USE_COMPUTED_SIZE); - - ArrayList tasks = new ArrayList<>(taskRunner.getTaskHistory()); - tasks.addAll(taskRunner.getPendingTasks()); - list.getItems().addAll(tasks); - - list.setCellFactory(new Callback<>() { - @Override - public ListCell call(ListView param) { - return new ListCell<>() { - @Override - public void updateItem(AbstractTask item, boolean empty) { - super.updateItem(item, empty); - if (item != null && !empty) { - Image icon = getApplicationDefaults().taskPendingImage; - if (item.isRunning()) { - icon = getApplicationDefaults().taskRunningImage; - } else if (item.isDone()) { - icon = getApplicationDefaults().taskSuccessImage; - } - if (item.getState().equals(State.FAILED)) { - icon = getApplicationDefaults().taskFailImage; - setOnMouseClicked(e -> { - showErrorDialog(item.getException().getMessage(), item.getException().toString()); - }); - } - ImageView imageView = new ImageView(icon); - setGraphic(imageView); - setText(item.getName()); - } - } - }; - } - }); - - Popup popup = new Popup(list); - popup.show(taskHistoryButton, Popup.PopupVPosition.BOTTOM, Popup.PopupHPosition.RIGHT); + ContextMenu menu = new ContextMenu(); + for (AbstractTask task : tasks) { + MenuItem item = new MenuItem(task.getName(), buildTaskIcon(task)); + if (task.getState().equals(State.FAILED)) { + item.setOnAction(e -> showErrorDialog( + task.getException().getMessage(), task.getException().toString())); + } + menu.getItems().add(item); } + menu.show(taskHistoryButton, Side.TOP, 0, 0); + } + private FontIcon buildTaskIcon(AbstractTask task) { + FontIcon icon; + if (task.getState().equals(State.FAILED)) { + icon = new FontIcon("mdi2a-alert-circle-outline"); + } else if (task.isRunning()) { + icon = new FontIcon("mdi2s-sync"); + } else if (task.isDone()) { + icon = new FontIcon("mdi2c-check-circle-outline"); + } else { + icon = new FontIcon("mdi2c-clock-outline"); + } + icon.setIconSize(14); + return icon; } private void showErrorDialog(String title, String content) { @@ -134,7 +116,6 @@ private void showErrorDialog(String title, String content) { } public void setErrorLog(AbstractTask task, String title, String content, String summary) { - this.getTelemetryService().event("/Error/TaskExecution", p -> { p.put("taskName", task.getName()); p.put("error", title); @@ -143,9 +124,7 @@ public void setErrorLog(AbstractTask task, String title, String content, String taskProgressBar.getStyleClass().add("progress-bar-error"); logsButton.setVisible(true); logsButton.setManaged(true); - logsButton.setOnAction(e -> { - showErrorDialog(title, content); - }); + logsButton.setOnAction(e -> showErrorDialog(title, content)); } public void resetErrorLog() { @@ -154,7 +133,6 @@ public void resetErrorLog() { logsButton.setVisible(false); } - public StringProperty taskNameProperty() { return taskNameProperty; } @@ -206,6 +184,6 @@ private void updateProgress(double target) { progressTimeline = new Timeline(kf); progressTimeline.setOnFinished(ev -> progressTimeline = null); progressTimeline.play(); - } -} + +} \ No newline at end of file diff --git a/owlplug-client/src/main/java/com/owlplug/core/controllers/dialogs/WelcomeDialogController.java b/owlplug-client/src/main/java/com/owlplug/core/controllers/dialogs/WelcomeDialogController.java index 77746b3a..37fd0527 100644 --- a/owlplug-client/src/main/java/com/owlplug/core/controllers/dialogs/WelcomeDialogController.java +++ b/owlplug-client/src/main/java/com/owlplug/core/controllers/dialogs/WelcomeDialogController.java @@ -15,26 +15,27 @@ * You should have received a copy of the GNU General Public License * along with OwlPlug. If not, see . */ - + package com.owlplug.core.controllers.dialogs; +import atlantafx.base.controls.ToggleSwitch; import com.owlplug.controls.DialogLayout; import com.owlplug.core.components.ApplicationDefaults; import com.owlplug.core.components.LazyViewRegistry; import com.owlplug.core.controllers.OptionsController; -import com.owlplug.core.controllers.fragments.PluginPathFragmentController; -import com.owlplug.core.events.PreferencesChangedEvent; import com.owlplug.core.model.OperatingSystem; -import com.owlplug.core.utils.FX; import com.owlplug.plugin.components.PluginTaskFactory; import com.owlplug.plugin.model.PluginFormat; +import com.owlplug.plugin.ui.PluginFormatBadgeView; import javafx.fxml.FXML; +import javafx.geometry.Insets; +import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; -import org.kordamp.ikonli.javafx.FontIcon; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.event.EventListener; import org.springframework.stereotype.Controller; @Controller @@ -46,108 +47,104 @@ public class WelcomeDialogController extends AbstractDialogController { private PluginTaskFactory taskFactory; @Autowired private OptionsController optionsController; - @Autowired - private ListDirectoryDialogController listDirectoryDialogController; @FXML - private VBox pluginPathContainer; + private VBox stepWelcome; @FXML - private Button okButton; + private VBox stepFormats; + @FXML + private VBox formatTogglesContainer; + @FXML + private Button startButton; @FXML private Button cancelButton; - - private PluginPathFragmentController vst2PluginPathFragment; - private PluginPathFragmentController vst3PluginPathFragment; - private PluginPathFragmentController auPluginPathFragment; - private PluginPathFragmentController lv2PluginPathFragment; + @FXML + private Button backButton; + @FXML + private Button skipButton; + @FXML + private Button okButton; WelcomeDialogController() { - super(700, 300); + super(640, 480); this.setOverlayClose(false); } /** * FXML initialize. */ + @FXML public void initialize() { - - vst2PluginPathFragment = new PluginPathFragmentController(PluginFormat.VST2, - ApplicationDefaults.VST2_DISCOVERY_ENABLED_KEY, ApplicationDefaults.VST_DIRECTORY_KEY, - ApplicationDefaults.VST2_EXTRA_DIRECTORY_KEY, - this.getPreferences(), this.getApplicationDefaults(), - this.listDirectoryDialogController); - vst3PluginPathFragment = new PluginPathFragmentController(PluginFormat.VST3, - ApplicationDefaults.VST3_DISCOVERY_ENABLED_KEY, - ApplicationDefaults.VST3_DIRECTORY_KEY, - ApplicationDefaults.VST3_EXTRA_DIRECTORY_KEY, - this.getPreferences(), this.getApplicationDefaults(), - this.listDirectoryDialogController); - auPluginPathFragment = new PluginPathFragmentController(PluginFormat.AU, - ApplicationDefaults.AU_DISCOVERY_ENABLED_KEY, - ApplicationDefaults.AU_DIRECTORY_KEY, - ApplicationDefaults.AU_EXTRA_DIRECTORY_KEY, - this.getPreferences(), this.getApplicationDefaults(), - this.listDirectoryDialogController); - lv2PluginPathFragment = new PluginPathFragmentController(PluginFormat.LV2, - ApplicationDefaults.LV2_DISCOVERY_ENABLED_KEY, - ApplicationDefaults.LV2_DIRECTORY_KEY, - ApplicationDefaults.LV2_EXTRA_DIRECTORY_KEY, - this.getPreferences(), this.getApplicationDefaults(), - this.listDirectoryDialogController); - - pluginPathContainer.getChildren().add(vst2PluginPathFragment.getNode()); - pluginPathContainer.getChildren().add(vst3PluginPathFragment.getNode()); - pluginPathContainer.getChildren().add(auPluginPathFragment.getNode()); - pluginPathContainer.getChildren().add(lv2PluginPathFragment.getNode()); - + startButton.setOnAction(e -> showStep(2)); + cancelButton.setOnAction(e -> this.close()); + backButton.setOnAction(e -> showStep(1)); + skipButton.setOnAction(e -> this.close()); okButton.setOnAction(e -> { this.close(); optionsController.refreshView(); taskFactory.createPluginScanTask().schedule(); }); - cancelButton.setOnAction(e -> this.close()); - - refreshView(); - + buildFormatToggles(); } - public void refreshView() { - vst2PluginPathFragment.refresh(); - vst3PluginPathFragment.refresh(); - auPluginPathFragment.refresh(); - lv2PluginPathFragment.refresh(); + private void buildFormatToggles() { + formatTogglesContainer.getChildren().addAll( + buildFormatRow(PluginFormat.VST2, ApplicationDefaults.VST2_DISCOVERY_ENABLED_KEY, + "VST2", "Legacy format, broadly supported across DAWs"), + buildFormatRow(PluginFormat.VST3, ApplicationDefaults.VST3_DISCOVERY_ENABLED_KEY, + "VST3", "Modern Steinberg format — recommended"), + buildFormatRow(PluginFormat.AU, ApplicationDefaults.AU_DISCOVERY_ENABLED_KEY, + "AU", "Audio Units — macOS only"), + buildFormatRow(PluginFormat.LV2, ApplicationDefaults.LV2_DISCOVERY_ENABLED_KEY, + "LV2", "Open standard, primarily for Linux") + ); // Disable AU options for non MAC users if (!this.getApplicationDefaults().getRuntimePlatform() .getOperatingSystem().equals(OperatingSystem.MAC)) { - auPluginPathFragment.disable(); + formatTogglesContainer.getChildren().get(2).setDisable(true); } - } - @EventListener - private void handle(PreferencesChangedEvent event) { - FX.run(this::refreshView); + private HBox buildFormatRow(PluginFormat format, String enableKey, String name, String desc) { + HBox row = new HBox(14); + row.setAlignment(Pos.CENTER_LEFT); + row.setPadding(new Insets(10, 4, 10, 4)); + + PluginFormatBadgeView badge = new PluginFormatBadgeView( + format, this.getApplicationDefaults(), PluginFormatBadgeView.DisplayMode.DEFAULT); + + VBox labels = new VBox(2); + Label nameLabel = new Label(name); + nameLabel.getStyleClass().add("label-emphase"); + Label descLabel = new Label(desc); + descLabel.getStyleClass().add("label-disabled"); + labels.getChildren().addAll(nameLabel, descLabel); + HBox.setHgrow(labels, Priority.ALWAYS); + + ToggleSwitch toggle = new ToggleSwitch(); + toggle.setSelected(this.getPreferences().getBoolean(enableKey, false)); + toggle.selectedProperty().addListener((obs, old, v) -> + this.getPreferences().putBoolean(enableKey, v)); + + row.getChildren().addAll(badge, labels, toggle); + return row; } - @Override - protected void onDialogShow() { - this.refreshView(); + private void showStep(int step) { + boolean onStep1 = step == 1; + stepWelcome.setVisible(onStep1); + stepWelcome.setManaged(onStep1); + stepFormats.setVisible(!onStep1); + stepFormats.setManaged(!onStep1); } @Override protected DialogLayout getLayout() { - Label title = new Label("Owlplug is almost ready !"); - title.getStyleClass().add("heading-3"); - FontIcon icon = new FontIcon("mdi2r-rocket-launch-outline"); - icon.setIconSize(20); - title.setGraphic(icon); DialogLayout layout = new DialogLayout(); - layout.setHeading(title); layout.setBody(lazyViewRegistry.get(LazyViewRegistry.WELCOME_VIEW)); - return layout; } -} +} \ No newline at end of file diff --git a/owlplug-client/src/main/java/com/owlplug/core/services/OptionsService.java b/owlplug-client/src/main/java/com/owlplug/core/services/OptionsService.java index 494989a4..1e975c85 100644 --- a/owlplug-client/src/main/java/com/owlplug/core/services/OptionsService.java +++ b/owlplug-client/src/main/java/com/owlplug/core/services/OptionsService.java @@ -29,6 +29,7 @@ import com.owlplug.plugin.model.PluginFormat; import com.owlplug.plugin.repositories.FileStatRepository; import com.owlplug.plugin.repositories.PluginRepository; +import com.owlplug.project.repositories.DawProjectRepository; import jakarta.annotation.PostConstruct; import java.util.prefs.BackingStoreException; import org.slf4j.Logger; @@ -54,6 +55,8 @@ public class OptionsService extends BaseService { @Autowired private FileStatRepository fileStatRepository; @Autowired + private DawProjectRepository projectRepository; + @Autowired private ImageCache imageCache; @PostConstruct @@ -108,6 +111,7 @@ public void clearAllUserData() { packageRepository.deleteAll(); remoteSourceRepository.deleteAll(); fileStatRepository.deleteAll(); + projectRepository.deleteAll(); clearCache(); diff --git a/owlplug-client/src/main/java/com/owlplug/core/utils/TimeUtils.java b/owlplug-client/src/main/java/com/owlplug/core/utils/TimeUtils.java index bd42a682..a512834a 100644 --- a/owlplug-client/src/main/java/com/owlplug/core/utils/TimeUtils.java +++ b/owlplug-client/src/main/java/com/owlplug/core/utils/TimeUtils.java @@ -52,7 +52,7 @@ private static String toDuration(long duration) { } } if ("".contentEquals(res)) { - return "0 seconds ago"; + return "Just now"; } else { return res.toString(); } diff --git a/owlplug-client/src/main/java/com/owlplug/plugin/components/PluginTaskFactory.java b/owlplug-client/src/main/java/com/owlplug/plugin/components/PluginTaskFactory.java index 9c321a18..398cc93a 100644 --- a/owlplug-client/src/main/java/com/owlplug/plugin/components/PluginTaskFactory.java +++ b/owlplug-client/src/main/java/com/owlplug/plugin/components/PluginTaskFactory.java @@ -23,7 +23,8 @@ import com.owlplug.core.components.BaseTaskFactory; import com.owlplug.core.tasks.TaskExecutionContext; import com.owlplug.core.utils.FileUtils; -import com.owlplug.plugin.events.PluginScanEvent; +import com.owlplug.plugin.events.PluginScanCompletedEvent; +import com.owlplug.plugin.events.PluginScanStartedEvent; import com.owlplug.plugin.model.Plugin; import com.owlplug.plugin.repositories.FileStatRepository; import com.owlplug.plugin.repositories.PluginFootprintRepository; @@ -120,8 +121,11 @@ public TaskExecutionContext createPluginScanTask(String directoryScope, boolean symlinkRepository, nativeHostService); + scanTask.setOnRunning(e -> publisher.publishEvent(new PluginScanStartedEvent())); + scanTask.setOnSucceeded(scanEvent -> { - publisher.publishEvent(new PluginScanEvent()); + prefs.putLong(ApplicationDefaults.LAST_PLUGIN_SCAN_DATE_KEY, System.currentTimeMillis()); + publisher.publishEvent(new PluginScanCompletedEvent()); TaskExecutionContext lookupTask = projectTaskFactory.createLookupTask(); if (prefs.getBoolean(ApplicationDefaults.SYNC_FILE_STAT_KEY, true) @@ -137,6 +141,9 @@ public TaskExecutionContext createPluginScanTask(String directoryScope, boolean lookupTask.scheduleNow(); }); + scanTask.setOnFailed(scanEvent -> publisher.publishEvent(new PluginScanCompletedEvent())); + scanTask.setOnCancelled(scanEvent -> publisher.publishEvent(new PluginScanCompletedEvent())); + return create(scanTask); } diff --git a/owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginsController.java b/owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginsController.java index 45fcbb5b..f24863e5 100644 --- a/owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginsController.java +++ b/owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginsController.java @@ -25,7 +25,7 @@ import com.owlplug.plugin.controllers.dialogs.ExportDialogController; import com.owlplug.plugin.controllers.dialogs.NewLinkController; import com.owlplug.plugin.events.PluginRefreshEvent; -import com.owlplug.plugin.events.PluginScanEvent; +import com.owlplug.plugin.events.PluginScanCompletedEvent; import com.owlplug.plugin.events.PluginUpdateEvent; import com.owlplug.plugin.repositories.PluginRepository; import com.owlplug.plugin.services.PluginService; @@ -230,6 +230,10 @@ public void refresh() { tableController.refresh(); } + public void setSearch(String query) { + searchTextField.setText(query); + } + public void setInfoPaneDisplay(boolean display) { pluginInfoPane.setManaged(display); pluginInfoPane.setVisible(display); @@ -241,7 +245,7 @@ public void toggleInfoPaneDisplay() { } @EventListener - private void handle(PluginScanEvent event) { + private void handle(PluginScanCompletedEvent event) { FX.run(this::displayPlugins); } diff --git a/owlplug-client/src/main/java/com/owlplug/plugin/events/PluginScanEvent.java b/owlplug-client/src/main/java/com/owlplug/plugin/events/PluginScanCompletedEvent.java similarity index 94% rename from owlplug-client/src/main/java/com/owlplug/plugin/events/PluginScanEvent.java rename to owlplug-client/src/main/java/com/owlplug/plugin/events/PluginScanCompletedEvent.java index a0657c79..fea9bd61 100644 --- a/owlplug-client/src/main/java/com/owlplug/plugin/events/PluginScanEvent.java +++ b/owlplug-client/src/main/java/com/owlplug/plugin/events/PluginScanCompletedEvent.java @@ -21,5 +21,5 @@ /** * Plugin scan task has completed successfully. */ -public record PluginScanEvent() { +public record PluginScanCompletedEvent() { } diff --git a/owlplug-client/src/main/java/com/owlplug/plugin/events/PluginScanStartedEvent.java b/owlplug-client/src/main/java/com/owlplug/plugin/events/PluginScanStartedEvent.java new file mode 100644 index 00000000..511157c2 --- /dev/null +++ b/owlplug-client/src/main/java/com/owlplug/plugin/events/PluginScanStartedEvent.java @@ -0,0 +1,25 @@ +/* OwlPlug + * Copyright (C) 2021 Arthur + * + * This file is part of OwlPlug. + * + * OwlPlug is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 + * as published by the Free Software Foundation. + * + * OwlPlug is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with OwlPlug. If not, see . + */ + +package com.owlplug.plugin.events; + +/** + * Plugin scan task has started running. + */ +public record PluginScanStartedEvent() { +} diff --git a/owlplug-client/src/main/java/com/owlplug/plugin/repositories/FileStatRepository.java b/owlplug-client/src/main/java/com/owlplug/plugin/repositories/FileStatRepository.java index 4f7b089f..d61f1191 100644 --- a/owlplug-client/src/main/java/com/owlplug/plugin/repositories/FileStatRepository.java +++ b/owlplug-client/src/main/java/com/owlplug/plugin/repositories/FileStatRepository.java @@ -33,6 +33,8 @@ public interface FileStatRepository extends JpaRepository { List findByParentPathOrderByLengthDesc(String parentPath); + List findTop5ByParentIsNullOrderByLengthDesc(); + @Transactional @Modifying(clearAutomatically = true, flushAutomatically = true) @Query("delete from FileStat f where f.path=:path") diff --git a/owlplug-client/src/main/java/com/owlplug/plugin/repositories/PluginRepository.java b/owlplug-client/src/main/java/com/owlplug/plugin/repositories/PluginRepository.java index 70260c42..f6c3b8e3 100644 --- a/owlplug-client/src/main/java/com/owlplug/plugin/repositories/PluginRepository.java +++ b/owlplug-client/src/main/java/com/owlplug/plugin/repositories/PluginRepository.java @@ -49,8 +49,12 @@ static Specification hasComponentName(String name) { Plugin findByPath(String path); - + List findBySyncComplete(boolean syncComplete); + + long countByFormat(PluginFormat format); + + long countByDisabledTrue(); @Transactional void deleteByPathContainingIgnoreCase(String path); diff --git a/owlplug-client/src/main/java/com/owlplug/project/repositories/PluginLookupRepository.java b/owlplug-client/src/main/java/com/owlplug/project/repositories/PluginLookupRepository.java index 069ebbbc..2a7dffec 100644 --- a/owlplug-client/src/main/java/com/owlplug/project/repositories/PluginLookupRepository.java +++ b/owlplug-client/src/main/java/com/owlplug/project/repositories/PluginLookupRepository.java @@ -19,7 +19,11 @@ package com.owlplug.project.repositories; import com.owlplug.project.model.DawPluginLookup; +import com.owlplug.project.model.LookupResult; import org.springframework.data.repository.CrudRepository; public interface PluginLookupRepository extends CrudRepository { + + long countByResult(LookupResult result); + } diff --git a/owlplug-client/src/main/java/com/owlplug/project/services/ProjectService.java b/owlplug-client/src/main/java/com/owlplug/project/services/ProjectService.java index c2eb168b..28cccc22 100644 --- a/owlplug-client/src/main/java/com/owlplug/project/services/ProjectService.java +++ b/owlplug-client/src/main/java/com/owlplug/project/services/ProjectService.java @@ -18,10 +18,14 @@ package com.owlplug.project.services; +import com.owlplug.core.components.ApplicationDefaults; import com.owlplug.core.services.BaseService; import com.owlplug.project.components.ProjectTaskFactory; import com.owlplug.project.model.DawProject; import com.owlplug.project.repositories.DawProjectRepository; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -41,4 +45,8 @@ public Iterable getAllProjects() { return dawProjectRepository.findAll(); } + public Set getProjectDirectories() { + return new HashSet<>(this.getPreferences().getList(ApplicationDefaults.PROJECT_DIRECTORY_KEY)); + } + } diff --git a/owlplug-client/src/main/resources/fxml/HomeView.fxml b/owlplug-client/src/main/resources/fxml/HomeView.fxml new file mode 100644 index 00000000..c217f0e7 --- /dev/null +++ b/owlplug-client/src/main/resources/fxml/HomeView.fxml @@ -0,0 +1,227 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/owlplug-client/src/main/resources/fxml/MainView.fxml b/owlplug-client/src/main/resources/fxml/MainView.fxml index 49db49c0..308c2b57 100644 --- a/owlplug-client/src/main/resources/fxml/MainView.fxml +++ b/owlplug-client/src/main/resources/fxml/MainView.fxml @@ -26,7 +26,11 @@ - + + + + + @@ -69,9 +73,8 @@ - - + + diff --git a/owlplug-client/src/main/resources/fxml/TaskBarView.fxml b/owlplug-client/src/main/resources/fxml/TaskBarView.fxml index 2dca2248..dc24a53d 100644 --- a/owlplug-client/src/main/resources/fxml/TaskBarView.fxml +++ b/owlplug-client/src/main/resources/fxml/TaskBarView.fxml @@ -1,36 +1,39 @@ - - - - - + + + + + + + + - - - - - - - - - - - - - - + + + + + + + + + + + \ No newline at end of file diff --git a/owlplug-client/src/main/resources/fxml/dialogs/WelcomeView.fxml b/owlplug-client/src/main/resources/fxml/dialogs/WelcomeView.fxml index 3452e0f9..f02aa781 100644 --- a/owlplug-client/src/main/resources/fxml/dialogs/WelcomeView.fxml +++ b/owlplug-client/src/main/resources/fxml/dialogs/WelcomeView.fxml @@ -1,21 +1,89 @@ - + + + + + + - - - - - - - + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/owlplug-client/src/main/resources/owlplug.css b/owlplug-client/src/main/resources/owlplug.css index 7d6e13bd..56a3f8c8 100644 --- a/owlplug-client/src/main/resources/owlplug.css +++ b/owlplug-client/src/main/resources/owlplug.css @@ -327,10 +327,6 @@ Text { -fx-background-color: -color-bg-default; } -.progress-bar-error>.bar { - -fx-background-color: -color-danger-fg; -} - .progress-bar-link { dot_color: rgb(143, 143, 143); } @@ -384,11 +380,38 @@ ProgressIndicator { .task-bar-footer { -fx-background-color: -color-bg-inset; + -fx-border-color: -color-border-muted transparent transparent transparent; + -fx-border-width: 1px; +} + +.task-bar-progress { + -fx-padding: 0; + -fx-border-width: 0; + -fx-background-color: transparent; +} + +.task-bar-progress > .track { + -fx-background-color: transparent; + -fx-background-insets: 0; + -fx-background-radius: 0; +} + +.task-bar-progress > .bar { + -fx-background-color: -color-accent-fg; + -fx-background-insets: 0; + -fx-background-radius: 0; +} + +.task-bar-progress.progress-bar-error > .bar { + -fx-background-color: -color-danger-fg; } .pane-card { - -fx-padding: 10, 10, 10, 10; + -fx-padding: 14; + -fx-background-color: -color-bg-subtle; + -fx-background-radius: 8px; + -fx-border-radius: 8px; -fx-border-width: 1px; -fx-border-color: -color-border-default; } @@ -452,34 +475,58 @@ ProgressIndicator { .default-color0.chart-pie { -fx-pie-color: #116A7B; } +.default-color0.chart-bar { + -fx-bar-fill: #116A7B; +} .default-color1.chart-pie { -fx-pie-color: #85586F; } +.default-color1.chart-bar { + -fx-bar-fill: #85586F; +} .default-color2.chart-pie { -fx-pie-color: #7F8B52; } +.default-color2.chart-bar { + -fx-bar-fill: #7F8B52; +} .default-color3.chart-pie { -fx-pie-color: #FFDBA4; } +.default-color3.chart-bar { + -fx-bar-fill: #FFDBA4; +} .default-color4.chart-pie { -fx-pie-color: #47A992; } +.default-color4.chart-bar { + -fx-bar-fill: #47A992; +} .default-color5.chart-pie { -fx-pie-color: #C84B31; } +.default-color5.chart-bar { + -fx-bar-fill: #C84B31; +} .default-color6.chart-pie { -fx-pie-color: #BCCC9A; } +.default-color6.chart-bar { + -fx-bar-fill: #BCCC9A; +} .default-color7.chart-pie { -fx-pie-color: #968C83; } +.default-color7.chart-bar { + -fx-bar-fill: #968C83; +} .chart-pie { -fx-border-width: 0px; @@ -667,6 +714,103 @@ ProgressIndicator { -fx-background-color: transparent; } +/**************************************************************** + * + * Home Dashboard + * + * **************************************************************/ + +.dashboard-tile { + -fx-background-color: -color-bg-subtle; + -fx-background-radius: 8px; + -fx-border-radius: 8px; + -fx-border-color: -color-border-default; + -fx-border-width: 1px; + -fx-spacing: 4; + -fx-min-width: 90px; +} + +.dashboard-tile-clickable { + -fx-cursor: hand; +} + +.dashboard-tile-clickable:hover { + -fx-background-color: -color-bg-overlay; + -fx-border-color: -color-accent-fg; +} + +.dashboard-tile-value { + -fx-font-size: 32px; + -fx-font-weight: bold; + -fx-text-fill: -color-fg-emphasis; +} + +.dashboard-tile-value-small { + -fx-font-size: 22px; + -fx-font-weight: bold; + -fx-text-fill: -color-fg-default; +} + +.dashboard-tile-label { + -fx-font-size: 11px; + -fx-text-fill: -color-fg-muted; +} + +.dashboard-section-title { + -fx-font-size: 11px; + -fx-font-weight: bold; + -fx-text-fill: -color-fg-subtle; +} + +.dashboard-bar-chart { + -fx-background-color: transparent; + -fx-padding: 0; +} + +.dashboard-bar-chart .chart-plot-background { + -fx-background-color: transparent; +} + +.dashboard-bar-chart .chart-vertical-grid-lines { + -fx-stroke: transparent; +} + +.dashboard-bar-chart .chart-horizontal-grid-lines { + -fx-stroke: transparent; +} + +.dashboard-bar-chart .axis { + -fx-tick-label-fill: -color-fg-muted; +} + +.dashboard-bar-chart .axis-label { + -fx-text-fill: -color-fg-muted; +} + +.dashboard-bar-chart .axis-line { + visibility: hidden; +} + +.dashboard-bar-label { + -fx-font-size: 11px; + -fx-font-weight: bold; + -fx-text-fill: -color-fg-emphasis; +} + .no-padding { -fx-padding: 0; -} \ No newline at end of file +} + +.dashboard-scan-indicator { + -fx-background-color: -color-accent-subtle; + -fx-background-radius: 6px; + -fx-padding: 6 12 6 12; +} + +.dashboard-scan-indicator .ikonli-font-icon { + -fx-icon-color: -color-accent-fg; +} + +.dashboard-scan-indicator .label { + -fx-text-fill: -color-accent-fg; +}