Skip to content

Commit de3e900

Browse files
authored
Merge pull request #502 from DropSnorz/feat/project-filters
DAW Projects filters and backup file detection
2 parents 68b939b + a42d697 commit de3e900

21 files changed

Lines changed: 582 additions & 35 deletions

owlplug-client/src/main/java/com/owlplug/core/components/ApplicationDefaults.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ public static final class Auth {
120120

121121
public static final class Projects {
122122
public static final String DIRECTORY = "projects.directory";
123+
public static final String COLLECT_BACKUP_FILES = "projects.collect_backup_files";
123124
}
124125

125126
public static final class Telemetry {

owlplug-client/src/main/java/com/owlplug/core/controllers/settings/ProjectSettingsController.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import javafx.collections.ObservableList;
2828
import javafx.fxml.FXML;
2929
import javafx.scene.control.Button;
30+
import javafx.scene.control.CheckBox;
3031
import javafx.scene.control.Label;
3132
import javafx.scene.control.ListView;
3233
import javafx.stage.DirectoryChooser;
@@ -43,6 +44,8 @@ public class ProjectSettingsController extends BaseController {
4344
private Button addDirButton;
4445
@FXML
4546
private Button removeDirButton;
47+
@FXML
48+
private CheckBox collectBackupFilesCheckbox;
4649

4750
private ObservableList<String> projectDirectories;
4851

@@ -82,11 +85,18 @@ public void initialize() {
8285
String selected = projectListView.getSelectionModel().getSelectedItem();
8386
if (selected != null) projectDirectories.remove(selected);
8487
});
88+
89+
collectBackupFilesCheckbox.setSelected(
90+
getPreferences().getBoolean(Prefs.Projects.COLLECT_BACKUP_FILES, false));
91+
collectBackupFilesCheckbox.selectedProperty().addListener((obs, o, n) ->
92+
getPreferences().putBoolean(Prefs.Projects.COLLECT_BACKUP_FILES, n));
8593
}
8694

8795
public void refresh() {
8896
projectDirectories.setAll(
8997
getPreferences().getList(Prefs.Projects.DIRECTORY, new ArrayList<>()));
98+
collectBackupFilesCheckbox.setSelected(
99+
getPreferences().getBoolean(Prefs.Projects.COLLECT_BACKUP_FILES, false));
90100
}
91101

92102
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/* OwlPlug
2+
* Copyright (C) 2021 Arthur <dropsnorz@gmail.com>
3+
*
4+
* This file is part of OwlPlug.
5+
*
6+
* OwlPlug is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License version 3
8+
* as published by the Free Software Foundation.
9+
*
10+
* OwlPlug is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with OwlPlug. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
package com.owlplug.project.components;
20+
21+
import com.owlplug.project.model.DawApplication;
22+
import com.owlplug.project.model.DawProject;
23+
import java.util.function.Predicate;
24+
import javafx.beans.binding.Bindings;
25+
import javafx.beans.property.ObjectProperty;
26+
import javafx.beans.property.SimpleObjectProperty;
27+
import javafx.collections.FXCollections;
28+
import javafx.collections.ObservableSet;
29+
import org.springframework.stereotype.Component;
30+
31+
@Component
32+
public class ProjectFilterState {
33+
34+
private final ObservableSet<DawApplication> selectedApplications = FXCollections.observableSet();
35+
private final ObjectProperty<Predicate<DawProject>> predicate = new SimpleObjectProperty<>();
36+
37+
public ProjectFilterState() {
38+
predicate.bind(Bindings.createObjectBinding(this::buildPredicate, selectedApplications));
39+
}
40+
41+
private Predicate<DawProject> buildPredicate() {
42+
if (selectedApplications.isEmpty()) {
43+
return null;
44+
}
45+
return project -> selectedApplications.contains(project.getApplication());
46+
}
47+
48+
public ObservableSet<DawApplication> getSelectedApplications() {
49+
return selectedApplications;
50+
}
51+
52+
public ObjectProperty<Predicate<DawProject>> predicateProperty() {
53+
return predicate;
54+
}
55+
56+
public void clearAll() {
57+
selectedApplications.clear();
58+
}
59+
}

owlplug-client/src/main/java/com/owlplug/project/components/ProjectTaskFactory.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,9 @@ public class ProjectTaskFactory extends BaseTaskFactory {
5353
public TaskExecutionContext createSyncTask() {
5454

5555
List<String> directories = prefs.getList(Prefs.Projects.DIRECTORY);
56+
boolean collectBackupFiles = prefs.getBoolean(Prefs.Projects.COLLECT_BACKUP_FILES, false);
5657

57-
ProjectSyncTask task = new ProjectSyncTask(projectRepository, directories);
58+
ProjectSyncTask task = new ProjectSyncTask(projectRepository, directories, collectBackupFiles);
5859
task.setOnSucceeded(e -> {
5960
createLookupTask().scheduleNow();
6061
publisher.publishEvent(new ProjectSyncEvent());
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/* OwlPlug
2+
* Copyright (C) 2021 Arthur <dropsnorz@gmail.com>
3+
*
4+
* This file is part of OwlPlug.
5+
*
6+
* OwlPlug is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License version 3
8+
* as published by the Free Software Foundation.
9+
*
10+
* OwlPlug is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with OwlPlug. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
package com.owlplug.project.controllers;
20+
21+
import com.owlplug.core.ui.SideBar;
22+
import com.owlplug.project.components.ProjectFilterState;
23+
import com.owlplug.project.model.DawApplication;
24+
import com.owlplug.project.model.DawProject;
25+
import jakarta.annotation.PostConstruct;
26+
import java.util.EnumMap;
27+
import java.util.Map;
28+
import javafx.collections.SetChangeListener;
29+
import javafx.geometry.Insets;
30+
import javafx.geometry.Pos;
31+
import javafx.scene.control.CheckBox;
32+
import javafx.scene.control.Hyperlink;
33+
import javafx.scene.control.Label;
34+
import javafx.scene.control.ScrollPane;
35+
import javafx.scene.control.Separator;
36+
import javafx.scene.layout.HBox;
37+
import javafx.scene.layout.Priority;
38+
import javafx.scene.layout.Region;
39+
import javafx.scene.layout.VBox;
40+
import org.springframework.beans.factory.annotation.Autowired;
41+
import org.springframework.stereotype.Controller;
42+
43+
@Controller
44+
public class ProjectFilterController {
45+
46+
@Autowired
47+
private ProjectFilterState filterState;
48+
49+
private SideBar sideBar;
50+
private VBox typeSection;
51+
private final Map<DawApplication, CheckBox> typeCheckBoxes = new EnumMap<>(DawApplication.class);
52+
53+
@PostConstruct
54+
public void setup() {
55+
typeSection = new VBox(4);
56+
57+
for (DawApplication application : DawApplication.values()) {
58+
CheckBox cb = new CheckBox(application.getName());
59+
cb.selectedProperty().addListener((obs, old, selected) -> {
60+
if (selected != filterState.getSelectedApplications().contains(application)) {
61+
if (selected) {
62+
filterState.getSelectedApplications().add(application);
63+
} else {
64+
filterState.getSelectedApplications().remove(application);
65+
}
66+
}
67+
});
68+
filterState.getSelectedApplications().addListener((SetChangeListener<DawApplication>) change ->
69+
cb.setSelected(filterState.getSelectedApplications().contains(application)));
70+
typeCheckBoxes.put(application, cb);
71+
typeSection.getChildren().add(cb);
72+
}
73+
74+
ScrollPane scrollPane = new ScrollPane(buildContent());
75+
scrollPane.setFitToWidth(true);
76+
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
77+
scrollPane.getStyleClass().add("plugin-filter-scroll");
78+
79+
sideBar = new SideBar(360, scrollPane);
80+
sideBar.getStyleClass().add("plugin-filter-sidebar");
81+
sideBar.setVisible(false);
82+
sideBar.setPrefWidth(0);
83+
}
84+
85+
private VBox buildContent() {
86+
VBox content = new VBox(8);
87+
content.setPadding(new Insets(12));
88+
89+
HBox header = new HBox();
90+
header.setAlignment(Pos.CENTER_LEFT);
91+
Label titleLabel = new Label("Filters");
92+
titleLabel.getStyleClass().add("plugin-filter-title");
93+
Region spacer = new Region();
94+
HBox.setHgrow(spacer, Priority.ALWAYS);
95+
Hyperlink clearLink = new Hyperlink("Clear all");
96+
clearLink.getStyleClass().add("plugin-filter-clear");
97+
clearLink.setOnAction(e -> filterState.clearAll());
98+
header.getChildren().addAll(titleLabel, spacer, clearLink);
99+
100+
Label typeLabel = new Label("Type");
101+
typeLabel.getStyleClass().add("plugin-filter-section-title");
102+
103+
content.getChildren().addAll(header, new Separator(), typeLabel, typeSection);
104+
return content;
105+
}
106+
107+
/**
108+
* Refreshes filter section counts from the currently displayed projects.
109+
* Called after each project sync or data reload.
110+
*/
111+
public void refresh(Iterable<DawProject> projects) {
112+
Map<DawApplication, Long> counts = new EnumMap<>(DawApplication.class);
113+
for (DawProject project : projects) {
114+
counts.merge(project.getApplication(), 1L, Long::sum);
115+
}
116+
typeCheckBoxes.forEach((application, cb) ->
117+
cb.setText(application.getName() + " (" + counts.getOrDefault(application, 0L) + ")"));
118+
}
119+
120+
public SideBar getView() {
121+
return sideBar;
122+
}
123+
}

owlplug-client/src/main/java/com/owlplug/project/controllers/ProjectsController.java

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@
2323
import com.owlplug.core.controllers.SettingsController;
2424
import com.owlplug.core.ui.FilterableTreeItem;
2525
import com.owlplug.core.utils.FX;
26+
import com.owlplug.project.components.ProjectFilterState;
2627
import com.owlplug.project.events.ProjectSyncEvent;
2728
import com.owlplug.project.model.DawProject;
2829
import com.owlplug.project.services.ProjectService;
2930
import com.owlplug.project.ui.ProjectTreeCell;
31+
import java.util.function.Predicate;
3032
import javafx.beans.binding.Bindings;
3133
import javafx.fxml.FXML;
3234
import javafx.scene.control.Button;
@@ -35,6 +37,7 @@
3537
import javafx.scene.control.TreeCell;
3638
import javafx.scene.control.TreeItem;
3739
import javafx.scene.control.TreeView;
40+
import javafx.scene.layout.HBox;
3841
import javafx.util.Callback;
3942
import org.springframework.beans.factory.annotation.Autowired;
4043
import org.springframework.context.event.EventListener;
@@ -51,17 +54,25 @@ public class ProjectsController extends BaseController {
5154
private SettingsController settingsController;
5255
@Autowired
5356
private ProjectInfoController projectInfoController;
57+
@Autowired
58+
private ProjectFilterController filterController;
59+
@Autowired
60+
private ProjectFilterState filterState;
5461

5562
@FXML
5663
private Button syncProjectButton;
5764
@FXML
5865
private Button projectDirectoryButton;
5966
@FXML
67+
private Button filterToggleButton;
68+
@FXML
6069
private TextField searchTextField;
6170
@FXML
6271
private TabPane projectTreeViewTabPane;
6372
@FXML
6473
private TreeView projectTreeView;
74+
@FXML
75+
private HBox filterContainer;
6576

6677
private FilterableTreeItem<Object> projectTreeNode;
6778

@@ -87,28 +98,37 @@ public void initialize() {
8798
mainController.navigateToMainTab(MainController.SETTINGS_TAB_INDEX);
8899
});
89100

101+
// Wire filter sidebar into the scene graph
102+
filterContainer.getChildren().add(0, filterController.getView());
103+
90104
projectTreeNode = new FilterableTreeItem<>("(all)");
91105
projectTreeView.setRoot(projectTreeNode);
92106

93-
// Binds search property to plugin tree filter
94-
projectTreeNode.predicateProperty().bind(Bindings.createObjectBinding(() -> {
95-
if (searchTextField.getText() == null || searchTextField.getText().isEmpty()) {
96-
return null;
97-
}
98-
return (item) -> {
99-
if (item instanceof DawProject project) {
100-
return project.getName().toLowerCase().contains(searchTextField.getText().toLowerCase());
101-
} else {
102-
return item.toString().toLowerCase().contains(searchTextField.getText().toLowerCase());
103-
}
104-
};
105-
}, searchTextField.textProperty()));
106-
107+
// Binds search text and type filter to the project tree predicate
108+
projectTreeNode.predicateProperty().bind(Bindings.createObjectBinding(
109+
() -> buildTreePredicate(searchTextField.getText(), filterState.predicateProperty().get()),
110+
searchTextField.textProperty(), filterState.predicateProperty()));
107111

108112
refresh();
109113

110114
}
111115

116+
private Predicate<Object> buildTreePredicate(String searchVal, Predicate<DawProject> filterPredicate) {
117+
boolean hasSearch = searchVal != null && !searchVal.isEmpty();
118+
if (!hasSearch && filterPredicate == null) {
119+
return null;
120+
}
121+
return (item) -> {
122+
if (item instanceof DawProject project) {
123+
boolean searchMatch = !hasSearch
124+
|| project.getName().toLowerCase().contains(searchVal.toLowerCase());
125+
return searchMatch && (filterPredicate == null || filterPredicate.test(project));
126+
} else {
127+
return !hasSearch || item.toString().toLowerCase().contains(searchVal.toLowerCase());
128+
}
129+
};
130+
}
131+
112132
public void refresh() {
113133
Iterable<DawProject> projects = projectService.getAllProjects();
114134
projectTreeNode.getInternalChildren().clear();
@@ -118,9 +138,15 @@ public void refresh() {
118138
}
119139

120140
projectTreeNode.setExpanded(true);
141+
filterController.refresh(projects);
121142

122143
}
123144

145+
@FXML
146+
public void toggleFilter() {
147+
filterController.getView().toggle();
148+
}
149+
124150
@EventListener
125151
private void handle(ProjectSyncEvent event) {
126152
FX.run(this::refresh);

owlplug-client/src/main/java/com/owlplug/project/model/DawProject.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
package com.owlplug.project.model;
2020

2121
import jakarta.persistence.CascadeType;
22+
import jakarta.persistence.Column;
2223
import jakarta.persistence.Entity;
2324
import jakarta.persistence.FetchType;
2425
import jakarta.persistence.GeneratedValue;
@@ -49,6 +50,13 @@ public class DawProject {
4950
private Date lastModifiedAt;
5051
private Date createdAt;
5152

53+
/* True if this project file was detected as a DAW-generated backup/autosave file
54+
(e.g. Ableton "Backup" folder, Reaper .rpp-bak / "Backups" folder, Studio One
55+
autosave / "History" folder). Only ever true when the "Collect backup files"
56+
setting is enabled - otherwise backup files are never persisted at all. */
57+
@Column(columnDefinition = "boolean default false")
58+
private boolean backup = false;
59+
5260
public Long getId() {
5361
return id;
5462
}
@@ -117,6 +125,14 @@ public void setCreatedAt(Date createdAt) {
117125
this.createdAt = createdAt;
118126
}
119127

128+
public boolean isBackup() {
129+
return backup;
130+
}
131+
132+
public void setBackup(boolean backup) {
133+
this.backup = backup;
134+
}
135+
120136
public List<DawPlugin> getPluginByLookupResult(LookupResult result) {
121137
return plugins.stream()
122138
.filter(p -> p.getLookup() != null && p.getLookup().getResult().equals(result))

0 commit comments

Comments
 (0)