Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,370 @@
/* OwlPlug
* Copyright (C) 2021 Arthur <dropsnorz@gmail.com>
*
* 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 <https://www.gnu.org/licenses/>.
*/

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<Number, String> 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<FileStat> 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<Number, String> 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 -> {
Comment thread
DropSnorz marked this conversation as resolved.
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<FileStat> 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();
});
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@EventListener
private void handle(PluginUpdateEvent event) {
FX.run(this::refresh);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Loading