From 4248a29e3ca624164a106d107f4058ee9cf759be Mon Sep 17 00:00:00 2001 From: Arthur Poiret Date: Sun, 26 Apr 2026 17:35:42 +0200 Subject: [PATCH] feat: add app home overview tab --- .../core/controllers/HomeController.java | 291 ++++++++++++++++++ .../core/controllers/MainController.java | 4 + .../plugin/controllers/PluginsController.java | 4 + .../repositories/FileStatRepository.java | 2 + .../plugin/repositories/PluginRepository.java | 6 +- .../repositories/PluginLookupRepository.java | 4 + .../project/services/ProjectService.java | 8 + .../src/main/resources/fxml/HomeView.fxml | 276 +++++++++++++++++ .../src/main/resources/fxml/MainView.fxml | 7 +- owlplug-client/src/main/resources/owlplug.css | 104 +++++++ 10 files changed, 701 insertions(+), 5 deletions(-) create mode 100644 owlplug-client/src/main/java/com/owlplug/core/controllers/HomeController.java create mode 100644 owlplug-client/src/main/resources/fxml/HomeView.fxml 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..8d53bc0e --- /dev/null +++ b/owlplug-client/src/main/java/com/owlplug/core/controllers/HomeController.java @@ -0,0 +1,291 @@ +/* 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.controllers.dialogs.ListDirectoryDialogController; +import com.owlplug.core.utils.FX; +import com.owlplug.plugin.controllers.PluginsController; +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.List; +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +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.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.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 + private ListDirectoryDialogController listDirectoryDialogController; + + @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 VBox pluginTile; + @FXML + private VBox projectTile; + + @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 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; + + /** + * FXML initialize method. + */ + @FXML + public void initialize() { + pluginTile.setOnMouseClicked(e -> mainController.selectMainTab(MainController.PLUGINS_TAB_INDEX)); + projectTile.setOnMouseClicked(e -> mainController.selectMainTab(MainController.PROJECTS_TAB_INDEX)); + + pluginSearchField.setOnAction(e -> { + mainController.selectMainTab(MainController.PLUGINS_TAB_INDEX); + pluginsController.setSearch(pluginSearchField.getText()); + }); + + scanPluginsButton.setOnAction(e -> pluginService.scanPlugins()); + exploreButton.setOnAction(e -> mainController.selectMainTab(MainController.EXPLORE_TAB_INDEX)); + syncProjectsButton.setOnAction(e -> projectService.syncProjects()); + settingsButton.setOnAction(e -> mainController.selectMainTab(MainController.OPTIONS_TAB_INDEX)); + + setupPluginDirButton.setOnAction(e -> mainController.selectMainTab(MainController.OPTIONS_TAB_INDEX)); + scanSuggestionButton.setOnAction(e -> pluginService.scanPlugins()); + setupProjectDirButton.setOnAction(e -> { + listDirectoryDialogController.configure(ApplicationDefaults.PROJECT_DIRECTORY_KEY); + listDirectoryDialogController.show(); + }); + + 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. + */ + public void refresh() { + final long totalPlugins = pluginRepository.count(); + final long vst2Count = pluginRepository.countByFormat(PluginFormat.VST2); + final long vst3Count = pluginRepository.countByFormat(PluginFormat.VST3); + final long auCount = pluginRepository.countByFormat(PluginFormat.AU); + final long lv2Count = pluginRepository.countByFormat(PluginFormat.LV2); + final long projectCount = dawProjectRepository.count(); + final long unresolvedCount = pluginLookupRepository.countByResult(LookupResult.MISSING); + final long disabledCount = pluginRepository.countByDisabledTrue(); + final long pluginDirectoryCount = pluginService.getDirectoriesExplorationSet().size(); + final long projectDirectoryCount = projectService.getProjectDirectories().size(); + + + pluginCountLabel.setText(String.valueOf(totalPlugins)); + vst2CountLabel.setText(String.valueOf(vst2Count)); + vst3CountLabel.setText(String.valueOf(vst3Count)); + auCountLabel.setText(String.valueOf(auCount)); + lv2CountLabel.setText(String.valueOf(lv2Count)); + projectCountLabel.setText(String.valueOf(projectCount)); + unresolvedPluginCountLabel.setText(String.valueOf(unresolvedCount)); + disabledCountLabel.setText(String.valueOf(disabledCount)); + pluginDirectoryCountLabel.setText(String.valueOf(pluginDirectoryCount)); + projectDirectoryCountLabel.setText(String.valueOf(projectDirectoryCount)); + + final boolean hasPluginDirectories = !pluginService.getDirectoriesExplorationSet().isEmpty(); + final boolean hasProjectDirectories = !projectService.getProjectDirectories().isEmpty(); + final boolean hasPlugins = totalPlugins > 0; + + setNodeVisible(noPluginDirectorySuggestion, !hasPluginDirectories); + setNodeVisible(noPluginSuggestion, hasPluginDirectories && !hasPlugins); + setNodeVisible(noProjectSuggestion, !hasProjectDirectories); + setNodeVisible(setupPane, !hasPluginDirectories || !hasPlugins || !hasProjectDirectories); + + refreshFileSizeChart(); + } + + private void refreshFileSizeChart() { + final List topStats = fileStatRepository.findTop5ByParentIsNullOrderByLengthDesc(); + 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 void setNodeVisible(VBox node, boolean visible) { + node.setVisible(visible); + node.setManaged(visible); + } + + @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 9ad7c367..b7b90fcc 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 @@ -111,7 +111,11 @@ 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; + public static int OPTIONS_TAB_INDEX = 4; /** * FXML initialize method. 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 7a6e327d..e3db9001 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 @@ -224,6 +224,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); 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..247600af --- /dev/null +++ b/owlplug-client/src/main/resources/fxml/HomeView.fxml @@ -0,0 +1,276 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 0241347c..d50ab0d1 100644 --- a/owlplug-client/src/main/resources/fxml/MainView.fxml +++ b/owlplug-client/src/main/resources/fxml/MainView.fxml @@ -26,7 +26,7 @@ - + @@ -55,9 +55,8 @@ - - + + diff --git a/owlplug-client/src/main/resources/owlplug.css b/owlplug-client/src/main/resources/owlplug.css index 16a3cc34..424e3351 100644 --- a/owlplug-client/src/main/resources/owlplug.css +++ b/owlplug-client/src/main/resources/owlplug.css @@ -409,34 +409,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; @@ -472,6 +496,86 @@ ProgressIndicator { -fx-background-color: transparent; } +/**************************************************************** + * + * Home Dashboard + * + * **************************************************************/ + +.dashboard-tile { + -fx-background-color: card-pane-color; + -fx-background-radius: 6px; + -fx-border-radius: 6px; + -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.55), 8, 0, 0, 2); + -fx-spacing: 4; + -fx-min-width: 90px; +} + +.dashboard-tile-clickable { + -fx-cursor: hand; +} + +.dashboard-tile-clickable:hover { + -fx-background-color: rgb(60, 60, 60); +} + +.dashboard-tile-value { + -fx-font-size: 34px; + -fx-font-weight: bold; + -fx-text-fill: text-color; +} + +.dashboard-tile-value-small { + -fx-font-size: 24px; + -fx-font-weight: bold; + -fx-text-fill: text-color; +} + +.dashboard-tile-label { + -fx-font-size: 12px; + -fx-text-fill: text-emphase-color; +} + +.dashboard-section-title { + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-text-fill: text-emphase-color; +} + +.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: text-emphase-color; +} + +.dashboard-bar-chart .axis-label { + -fx-text-fill: text-emphase-color; +} + +.dashboard-bar-chart .axis-line { + visibility: hidden; +} + +.dashboard-bar-label { + -fx-font-size: 11px; + -fx-font-weight: bold; +} + .no-padding { -fx-padding: 0; } \ No newline at end of file