Skip to content

Commit b4b3075

Browse files
gdrososgliargovas
andauthored
Fix for issue #4652: Add Find Unlinked Files Filter based on Date (#7846)
Co-authored-by: George Liargkovas <[email protected]>
1 parent aa60dd6 commit b4b3075

File tree

11 files changed

+433
-13
lines changed

11 files changed

+433
-13
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ Note that this project **does not** adhere to [Semantic Versioning](http://semve
1414
- We added a progress counter to the title bar in Possible Duplicates dialog window. [#7366](https://github.com/JabRef/jabref/issues/7366)
1515
- We added new "Customization" tab to the preferences which includes option to choose a custom address for DOI access. [#7337](https://github.com/JabRef/jabref/issues/7337)
1616
- We added zbmath to the public databases from which the bibliographic information of an existing entry can be updated. [#7437](https://github.com/JabRef/jabref/issues/7437)
17+
- We showed to the find Unlinked Files Dialog the date of the files' most recent modification. [#4652](https://github.com/JabRef/jabref/issues/4652)
18+
- We added to the find Unlinked Files function a filter to show only files based on date of last modification (Last Year, Last Month, Last Week, Last Day). [#4652](https://github.com/JabRef/jabref/issues/4652)
19+
- We added to the find Unlinked Files function a filter that sorts the files based on the date of last modification(Sort by Newest, Sort by Oldest First). [#4652](https://github.com/JabRef/jabref/issues/4652)
1720
- We added the possibility to add a new entry via its zbMath ID (zbMATH can be chosen as ID type in the "Select entry type" window). [#7202](https://github.com/JabRef/jabref/issues/7202)
1821
- We added the extension support and the external application support (For Texshow, Texmaker and LyX) to the flatpak [#7248](https://github.com/JabRef/jabref/pull/7248)
1922
- We added some symbols and keybindings to the context menu in the entry editor. [#7268](https://github.com/JabRef/jabref/pull/7268)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package org.jabref.gui.externalfiles;
2+
3+
import org.jabref.logic.l10n.Localization;
4+
5+
public enum DateRange {
6+
ALL_TIME(Localization.lang("All time")),
7+
YEAR(Localization.lang("Last year")),
8+
MONTH(Localization.lang("Last month")),
9+
WEEK(Localization.lang("Last week")),
10+
DAY(Localization.lang("Last day"));
11+
12+
private final String dateRange;
13+
14+
DateRange(String dateRange) {
15+
this.dateRange = dateRange;
16+
}
17+
18+
public String getDateRange() {
19+
return dateRange;
20+
}
21+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package org.jabref.gui.externalfiles;
2+
3+
import org.jabref.logic.l10n.Localization;
4+
5+
public enum ExternalFileSorter {
6+
DEFAULT(Localization.lang("Default")),
7+
DATE_ASCENDING(Localization.lang("Newest first")),
8+
DATE_DESCENDING(Localization.lang("Oldest first"));
9+
10+
private final String sorter;
11+
12+
ExternalFileSorter(String sorter) {
13+
this.sorter = sorter;
14+
}
15+
16+
public String getSorter() {
17+
return sorter;
18+
}
19+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package org.jabref.gui.externalfiles;
2+
3+
import java.io.IOException;
4+
import java.nio.file.Files;
5+
import java.nio.file.Path;
6+
import java.nio.file.attribute.FileTime;
7+
import java.time.LocalDateTime;
8+
import java.time.ZoneId;
9+
import java.util.Comparator;
10+
import java.util.List;
11+
import java.util.stream.Collectors;
12+
13+
import org.slf4j.Logger;
14+
import org.slf4j.LoggerFactory;
15+
16+
public class FileFilterUtils {
17+
18+
private static final Logger LOGGER = LoggerFactory.getLogger(FileFilterUtils.class);
19+
20+
/* Returns the last edited time of a file as LocalDateTime. */
21+
public static LocalDateTime getFileTime(Path path) {
22+
FileTime lastEditedTime = null;
23+
try {
24+
lastEditedTime = Files.getLastModifiedTime(path);
25+
} catch (IOException e) {
26+
LOGGER.error("Could not retrieve file time", e);
27+
return LocalDateTime.now();
28+
}
29+
LocalDateTime localDateTime = lastEditedTime
30+
.toInstant()
31+
.atZone(ZoneId.systemDefault())
32+
.toLocalDateTime();
33+
return localDateTime;
34+
}
35+
36+
/* Returns true if a file with a specific path
37+
* was edited during the last 24 hours. */
38+
public boolean isDuringLastDay(LocalDateTime fileEditTime) {
39+
LocalDateTime NOW = LocalDateTime.now(ZoneId.systemDefault());
40+
return fileEditTime.isAfter(NOW.minusHours(24));
41+
}
42+
43+
/* Returns true if a file with a specific path
44+
* was edited during the last 7 days. */
45+
public boolean isDuringLastWeek(LocalDateTime fileEditTime) {
46+
LocalDateTime NOW = LocalDateTime.now(ZoneId.systemDefault());
47+
return fileEditTime.isAfter(NOW.minusDays(7));
48+
}
49+
50+
/* Returns true if a file with a specific path
51+
* was edited during the last 30 days. */
52+
public boolean isDuringLastMonth(LocalDateTime fileEditTime) {
53+
LocalDateTime NOW = LocalDateTime.now(ZoneId.systemDefault());
54+
return fileEditTime.isAfter(NOW.minusDays(30));
55+
}
56+
57+
/* Returns true if a file with a specific path
58+
* was edited during the last 365 days. */
59+
public boolean isDuringLastYear(LocalDateTime fileEditTime) {
60+
LocalDateTime NOW = LocalDateTime.now(ZoneId.systemDefault());
61+
return fileEditTime.isAfter(NOW.minusDays(365));
62+
}
63+
64+
/* Returns true if a file is edited in the time margin specified by the given filter. */
65+
public static boolean filterByDate(Path path, DateRange filter) {
66+
FileFilterUtils fileFilter = new FileFilterUtils();
67+
LocalDateTime fileTime = FileFilterUtils.getFileTime(path);
68+
boolean isInDateRange = switch (filter) {
69+
case DAY -> fileFilter.isDuringLastDay(fileTime);
70+
case WEEK -> fileFilter.isDuringLastWeek(fileTime);
71+
case MONTH -> fileFilter.isDuringLastMonth(fileTime);
72+
case YEAR -> fileFilter.isDuringLastYear(fileTime);
73+
case ALL_TIME -> true;
74+
};
75+
return isInDateRange;
76+
}
77+
78+
/* Sorts a list of Path objects according to the last edited date
79+
* of their corresponding files, from newest to oldest. */
80+
public List<Path> sortByDateAscending(List<Path> files) {
81+
return files.stream()
82+
.sorted(Comparator.comparingLong(file -> FileFilterUtils.getFileTime(file)
83+
.atZone(ZoneId.systemDefault())
84+
.toInstant()
85+
.toEpochMilli()))
86+
.collect(Collectors.toList());
87+
}
88+
89+
/* Sorts a list of Path objects according to the last edited date
90+
* of their corresponding files, from oldest to newest. */
91+
public List<Path> sortByDateDescending(List<Path> files) {
92+
return files.stream()
93+
.sorted(Comparator.comparingLong(file -> -FileFilterUtils.getFileTime(file)
94+
.atZone(ZoneId.systemDefault())
95+
.toInstant()
96+
.toEpochMilli()))
97+
.collect(Collectors.toList());
98+
}
99+
100+
/* Sorts a list of Path objects according to the last edited date
101+
* the order depends on the specified sorter type. */
102+
public static List<Path> sortByDate(List<Path> files, ExternalFileSorter sortType) {
103+
FileFilterUtils fileFilter = new FileFilterUtils();
104+
List<Path> sortedFiles = switch (sortType) {
105+
case DEFAULT -> files;
106+
case DATE_ASCENDING -> fileFilter.sortByDateDescending(files);
107+
case DATE_DESCENDING -> fileFilter.sortByDateAscending(files);
108+
};
109+
return sortedFiles;
110+
}
111+
}
112+

src/main/java/org/jabref/gui/externalfiles/UnlinkedFilesCrawler.java

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,16 @@ public class UnlinkedFilesCrawler extends BackgroundTask<FileNodeViewModel> {
3232

3333
private final Path directory;
3434
private final Filter<Path> fileFilter;
35+
private final DateRange dateFilter;
36+
private final ExternalFileSorter sorter;
3537
private final BibDatabaseContext databaseContext;
3638
private final FilePreferences filePreferences;
3739

38-
public UnlinkedFilesCrawler(Path directory, Filter<Path> fileFilter, BibDatabaseContext databaseContext, FilePreferences filePreferences) {
40+
public UnlinkedFilesCrawler(Path directory, Filter<Path> fileFilter, DateRange dateFilter, ExternalFileSorter sorter, BibDatabaseContext databaseContext, FilePreferences filePreferences) {
3941
this.directory = directory;
4042
this.fileFilter = fileFilter;
43+
this.dateFilter = dateFilter;
44+
this.sorter = sorter;
4145
this.databaseContext = databaseContext;
4246
this.filePreferences = filePreferences;
4347
}
@@ -61,6 +65,9 @@ protected FileNodeViewModel call() throws IOException {
6165
* For ensuring the capability to cancel the work of this recursive method, the first position in the integer array
6266
* 'state' must be set to 1, to keep the recursion running. When the states value changes, the method will resolve
6367
* its recursion and return what it has saved so far.
68+
* <br>
69+
* The files are filtered according to the {@link DateRange} filter value
70+
* and then sorted according to the {@link ExternalFileSorter} value.
6471
*
6572
* @throws IOException if directory is not a directory or empty
6673
*/
@@ -92,11 +99,19 @@ private FileNodeViewModel searchDirectory(Path directory, UnlinkedPDFFileFilter
9299
parent.getChildren().add(subRoot);
93100
}
94101
}
95-
96-
parent.setFileCount(files.size() + fileCount);
97-
parent.getChildren().addAll(files.stream()
98-
.map(FileNodeViewModel::new)
99-
.collect(Collectors.toList()));
102+
// filter files according to last edited date.
103+
List<Path> filteredFiles = new ArrayList<Path>();
104+
for (Path path : files) {
105+
if (FileFilterUtils.filterByDate(path, dateFilter)) {
106+
filteredFiles.add(path);
107+
}
108+
}
109+
// sort files according to last edited date.
110+
filteredFiles = FileFilterUtils.sortByDate(filteredFiles, sorter);
111+
parent.setFileCount(filteredFiles.size() + fileCount);
112+
parent.getChildren().addAll(filteredFiles.stream()
113+
.map(FileNodeViewModel::new)
114+
.collect(Collectors.toList()));
100115
return parent;
101116
}
102117
}

src/main/java/org/jabref/gui/externalfiles/UnlinkedFilesDialog.fxml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,12 @@
5252

5353
<Label text="%File type" GridPane.columnIndex="0" GridPane.rowIndex="1"/>
5454
<ComboBox fx:id="fileTypeCombo" maxWidth="Infinity" GridPane.columnIndex="1" GridPane.rowIndex="1"/>
55+
<Label text="%Last edited:" GridPane.columnIndex="0" GridPane.rowIndex="2"/>
56+
<ComboBox fx:id="fileDateCombo" maxWidth="Infinity" GridPane.columnIndex="1" GridPane.rowIndex="2"/>
57+
<Label text="%Sort by:" GridPane.columnIndex="0" GridPane.rowIndex="3"/>
58+
<ComboBox fx:id="fileSortCombo" maxWidth="Infinity" GridPane.columnIndex="1" GridPane.rowIndex="3"/>
5559
<Button fx:id="scanButton" onAction="#scanFiles" text="%Search"
56-
GridPane.columnIndex="2" GridPane.rowIndex="1">
60+
GridPane.columnIndex="2" GridPane.rowIndex="3">
5761
<tooltip>
5862
<Tooltip text="%Searches the selected directory for unlinked files."/>
5963
</tooltip>

src/main/java/org/jabref/gui/externalfiles/UnlinkedFilesDialogView.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ public class UnlinkedFilesDialogView extends BaseDialog<Void> {
5454

5555
@FXML private TextField directoryPathField;
5656
@FXML private ComboBox<FileExtensionViewModel> fileTypeCombo;
57+
@FXML private ComboBox<DateRange> fileDateCombo;
58+
@FXML private ComboBox<ExternalFileSorter> fileSortCombo;
5759
@FXML private CheckTreeView<FileNodeViewModel> unlinkedFilesList;
5860
@FXML private Button scanButton;
5961
@FXML private Button exportButton;
@@ -141,11 +143,23 @@ private void initDirectorySelection() {
141143
fileTypeCombo.setItems(viewModel.getFileFilters());
142144
fileTypeCombo.valueProperty().bindBidirectional(viewModel.selectedExtensionProperty());
143145
fileTypeCombo.getSelectionModel().selectFirst();
146+
new ViewModelListCellFactory<DateRange>()
147+
.withText(DateRange::getDateRange)
148+
.install(fileDateCombo);
149+
fileDateCombo.setItems(viewModel.getDateFilters());
150+
fileDateCombo.valueProperty().bindBidirectional(viewModel.selectedDateProperty());
151+
fileDateCombo.getSelectionModel().selectFirst();
152+
new ViewModelListCellFactory<ExternalFileSorter>()
153+
.withText(ExternalFileSorter::getSorter)
154+
.install(fileSortCombo);
155+
fileSortCombo.setItems(viewModel.getSorters());
156+
fileSortCombo.valueProperty().bindBidirectional(viewModel.selectedSortProperty());
157+
fileSortCombo.getSelectionModel().selectFirst();
144158
}
145159

146160
private void initUnlinkedFilesList() {
147161
new ViewModelTreeCellFactory<FileNodeViewModel>()
148-
.withText(FileNodeViewModel::getDisplayText)
162+
.withText(FileNodeViewModel::getDisplayTextWithEditDate)
149163
.install(unlinkedFilesList);
150164

151165
unlinkedFilesList.maxHeightProperty().bind(((Control) filePane.contentProperty().get()).heightProperty());

src/main/java/org/jabref/gui/externalfiles/UnlinkedFilesDialogViewModel.java

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ public class UnlinkedFilesDialogViewModel {
5555
private final ImportHandler importHandler;
5656
private final StringProperty directoryPath = new SimpleStringProperty("");
5757
private final ObjectProperty<FileExtensionViewModel> selectedExtension = new SimpleObjectProperty<>();
58+
private final ObjectProperty<DateRange> selectedDate = new SimpleObjectProperty<>();
59+
private final ObjectProperty<ExternalFileSorter> selectedSort = new SimpleObjectProperty<>();
5860

5961
private final ObjectProperty<Optional<FileNodeViewModel>> treeRootProperty = new SimpleObjectProperty<>();
6062
private final SimpleListProperty<TreeItem<FileNodeViewModel>> checkedFileListProperty = new SimpleListProperty<>(FXCollections.observableArrayList());
@@ -65,6 +67,9 @@ public class UnlinkedFilesDialogViewModel {
6567

6668
private final ObservableList<ImportFilesResultItemViewModel> resultList = FXCollections.observableArrayList();
6769
private final ObservableList<FileExtensionViewModel> fileFilterList;
70+
private final ObservableList<DateRange> dateFilterList;
71+
private final ObservableList<ExternalFileSorter> fileSortList;
72+
6873
private final DialogService dialogService;
6974
private final PreferencesService preferences;
7075
private BackgroundTask<FileNodeViewModel> findUnlinkedFilesTask;
@@ -90,9 +95,13 @@ public UnlinkedFilesDialogViewModel(DialogService dialogService, ExternalFileTyp
9095
stateManager);
9196

9297
this.fileFilterList = FXCollections.observableArrayList(
93-
new FileExtensionViewModel(StandardFileType.ANY_FILE, externalFileTypes),
94-
new FileExtensionViewModel(StandardFileType.BIBTEX_DB, externalFileTypes),
95-
new FileExtensionViewModel(StandardFileType.PDF, externalFileTypes));
98+
new FileExtensionViewModel(StandardFileType.ANY_FILE, externalFileTypes),
99+
new FileExtensionViewModel(StandardFileType.BIBTEX_DB, externalFileTypes),
100+
new FileExtensionViewModel(StandardFileType.PDF, externalFileTypes));
101+
102+
this.dateFilterList = FXCollections.observableArrayList(DateRange.values());
103+
104+
this.fileSortList = FXCollections.observableArrayList(ExternalFileSorter.values());
96105

97106
Predicate<String> isDirectory = path -> Files.isDirectory(Path.of(path));
98107
scanDirectoryValidator = new FunctionBasedValidator<>(directoryPath, isDirectory,
@@ -104,11 +113,12 @@ public UnlinkedFilesDialogViewModel(DialogService dialogService, ExternalFileTyp
104113
public void startSearch() {
105114
Path directory = this.getSearchDirectory();
106115
Filter<Path> selectedFileFilter = selectedExtension.getValue().dirFilter();
107-
116+
DateRange selectedDateFilter = selectedDate.getValue();
117+
ExternalFileSorter selectedSortFilter = selectedSort.getValue();
108118
progressValueProperty.unbind();
109119
progressTextProperty.unbind();
110120

111-
findUnlinkedFilesTask = new UnlinkedFilesCrawler(directory, selectedFileFilter, bibDatabase, preferences.getFilePreferences())
121+
findUnlinkedFilesTask = new UnlinkedFilesCrawler(directory, selectedFileFilter, selectedDateFilter, selectedSortFilter, bibDatabase, preferences.getFilePreferences())
112122
.onRunning(() -> {
113123
progressValueProperty.set(ProgressIndicator.INDETERMINATE_PROGRESS);
114124
progressTextProperty.setValue(Localization.lang("Searching file system..."));
@@ -189,6 +199,14 @@ public ObservableList<FileExtensionViewModel> getFileFilters() {
189199
return this.fileFilterList;
190200
}
191201

202+
public ObservableList<DateRange> getDateFilters() {
203+
return this.dateFilterList;
204+
}
205+
206+
public ObservableList<ExternalFileSorter> getSorters() {
207+
return this.fileSortList;
208+
}
209+
192210
public void cancelTasks() {
193211
if (findUnlinkedFilesTask != null) {
194212
findUnlinkedFilesTask.cancel();
@@ -234,6 +252,14 @@ public ObjectProperty<FileExtensionViewModel> selectedExtensionProperty() {
234252
return this.selectedExtension;
235253
}
236254

255+
public ObjectProperty<DateRange> selectedDateProperty() {
256+
return this.selectedDate;
257+
}
258+
259+
public ObjectProperty<ExternalFileSorter> selectedSortProperty() {
260+
return this.selectedSort;
261+
}
262+
237263
public StringProperty directoryPathProperty() {
238264
return this.directoryPath;
239265
}

0 commit comments

Comments
 (0)