diff --git a/README.md b/README.md index c08fa9e..4a28b4f 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Install JRE/JDK 8 or higher (openJDK is good. Oracle's one is also good). JavaFX See 'Linux' section. -Set 'Security & Privacy' if needed. +Set 'Security & Privacy' settings if needed. ### Windows: @@ -63,10 +63,18 @@ Table 'Status' = 'Uploaded' does not means that file installed. It means that it Handling successful/failed installation is a purpose of the other side application (TinFoil/GoldLeaf). (And they don't provide any feedback interfaces so I can't detect success/failure.) ## TODO: -- [x] macOS QA by [Konstanin Kelemen](https://github.com/konstantin-kelemen). Appreciate assistance of [Vitaliy Natarov](https://github.com/SebastianUA). +- [x] macOS QA + -[x] v0.1 + -[ ] v0.2 (partly) - [x] Windows support -- [ ] code refactoring -- [ ] GoldLeaf support +- [ ] code refactoring (almost. todo: printLog() ) +- [x] GoldLeaf support - [ ] XCI support -- [ ] Settings -- [ ] File order sort (non-critical) \ No newline at end of file +- [ ] File order sort (non-critical) + +## Thanks +Appreciate assistance and support of both Vitaliy and Konstantin. Without you all this magic would not have happened. + +[Konstanin Kelemen](https://github.com/konstantin-kelemen) + +[Vitaliy Natarov](https://github.com/SebastianUA) \ No newline at end of file diff --git a/src/main/java/nsusbloader/AppPreferences.java b/src/main/java/nsusbloader/AppPreferences.java new file mode 100644 index 0000000..66f24f2 --- /dev/null +++ b/src/main/java/nsusbloader/AppPreferences.java @@ -0,0 +1,23 @@ +package nsusbloader; + +import java.util.prefs.Preferences; + +public class AppPreferences { + private static final AppPreferences INSTANCE = new AppPreferences(); + public static AppPreferences getInstance() { return INSTANCE; } + + private Preferences preferences; + + private AppPreferences(){ preferences = Preferences.userRoot().node("NS-USBloader"); } + + public String getTheme(){ + String theme = preferences.get("THEME", "/res/app_dark.css"); // Don't let user to change settings manually + if (!theme.matches("(^/res/app_dark.css$)|(^/res/app_light.css$)")) + theme = "/res/app_dark.css"; + return theme; + } + public void setTheme(String theme){ preferences.put("THEME", theme); } + + public String getRecent(){ return preferences.get("RECENT", System.getProperty("user.home")); } + public void setRecent(String path){ preferences.put("RECENT", path); } +} diff --git a/src/main/java/nsusbloader/NSLMainController.java b/src/main/java/nsusbloader/Controllers/NSLMainController.java similarity index 88% rename from src/main/java/nsusbloader/NSLMainController.java rename to src/main/java/nsusbloader/Controllers/NSLMainController.java index 9b0ea22..1ae0ef0 100644 --- a/src/main/java/nsusbloader/NSLMainController.java +++ b/src/main/java/nsusbloader/Controllers/NSLMainController.java @@ -1,4 +1,4 @@ -package nsusbloader; +package nsusbloader.Controllers; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -8,7 +8,10 @@ import javafx.scene.control.*; import javafx.scene.layout.Pane; import javafx.scene.layout.Region; import javafx.stage.FileChooser; -import nsusbloader.Controllers.NSTableViewController; +import nsusbloader.AppPreferences; +import nsusbloader.MediatorControl; +import nsusbloader.NSLMain; +import nsusbloader.UsbCommunications; import java.io.File; import java.net.URL; @@ -46,7 +49,7 @@ public class NSLMainController implements Initializable { @Override public void initialize(URL url, ResourceBundle rb) { this.resourceBundle = rb; - logArea.setText(rb.getString("logsGreetingsMessage")+" "+NSLMain.appVersion+"!\n"); + logArea.setText(rb.getString("logsGreetingsMessage")+" "+ NSLMain.appVersion+"!\n"); if (System.getProperty("os.name").toLowerCase().startsWith("lin")) if (!System.getProperty("user.name").equals("root")) logArea.appendText(rb.getString("logsEnteredAsMsg1")+System.getProperty("user.name")+"\n"+rb.getString("logsEnteredAsMsg2") + "\n"); @@ -81,18 +84,20 @@ public class NSLMainController implements Initializable { btnSwitchImage.getStyleClass().add("regionLamp"); switchThemeBtn.setGraphic(btnSwitchImage); this.switchThemeBtn.setOnAction(e->switchTheme()); + + previouslyOpenedPath = AppPreferences.getInstance().getRecent(); } /** * Changes UI theme on the go * */ private void switchTheme(){ - if (switchThemeBtn.getScene().getStylesheets().get(0).equals("/res/app.css")) { - switchThemeBtn.getScene().getStylesheets().remove("/res/app.css"); + if (switchThemeBtn.getScene().getStylesheets().get(0).equals("/res/app_dark.css")) { + switchThemeBtn.getScene().getStylesheets().remove("/res/app_dark.css"); switchThemeBtn.getScene().getStylesheets().add("/res/app_light.css"); } else { - switchThemeBtn.getScene().getStylesheets().add("/res/app.css"); switchThemeBtn.getScene().getStylesheets().remove("/res/app_light.css"); + switchThemeBtn.getScene().getStylesheets().add("/res/app_dark.css"); } } /** @@ -103,16 +108,14 @@ public class NSLMainController implements Initializable { List filesList; FileChooser fileChooser = new FileChooser(); fileChooser.setTitle(resourceBundle.getString("btnFileOpen")); - if (previouslyOpenedPath == null) + + File validator = new File(previouslyOpenedPath); + if (validator.exists()) + fileChooser.setInitialDirectory(validator); // TODO: read from prefs + else fileChooser.setInitialDirectory(new File(System.getProperty("user.home"))); // TODO: read from prefs - else { - File validator = new File(previouslyOpenedPath); - if (validator.exists()) - fileChooser.setInitialDirectory(validator); // TODO: read from prefs - else - fileChooser.setInitialDirectory(new File(System.getProperty("user.home"))); // TODO: read from prefs - } - fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("NS ROM", "*.nsp")); + + fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("NSP ROM", "*.nsp")); filesList = fileChooser.showOpenMultipleDialog(logArea.getScene().getWindow()); if (filesList != null && !filesList.isEmpty()) { @@ -156,7 +159,7 @@ public class NSLMainController implements Initializable { * This thing modify UI for reusing 'Upload to NS' button and make functionality set for "Stop transmission" * Called from mediator * */ - void notifyTransmissionStarted(boolean isTransmissionStarted){ + public void notifyTransmissionStarted(boolean isTransmissionStarted){ if (isTransmissionStarted) { selectNspBtn.setDisable(true); uploadStopBtn.setOnAction(e->{ stopBtnAction(); }); @@ -182,4 +185,11 @@ public class NSLMainController implements Initializable { uploadStopBtn.getStyleClass().add("buttonUp"); } } + /** + * Save preferences before exit + * */ + public void exit(){ + AppPreferences.getInstance().setTheme(switchThemeBtn.getScene().getStylesheets().get(0)); + AppPreferences.getInstance().setRecent(previouslyOpenedPath); + } } diff --git a/src/main/java/nsusbloader/Controllers/NSLRowModel.java b/src/main/java/nsusbloader/Controllers/NSLRowModel.java index 61d0adc..1765eea 100644 --- a/src/main/java/nsusbloader/Controllers/NSLRowModel.java +++ b/src/main/java/nsusbloader/Controllers/NSLRowModel.java @@ -39,14 +39,14 @@ public class NSLRowModel { public void setStatus(EFileStatus status){ // TODO: Localization switch (status){ case UPLOADED: - this.status = "Uploaded"; + this.status = "Success"; markForUpload = false; break; case FAILED: - this.status = "Upload failed"; + this.status = "Failed"; break; case INCORRECT_FILE_FAILED: - this.status = "File incorrect"; + this.status = "Failed: Incorrect file"; markForUpload = false; break; } diff --git a/src/main/java/nsusbloader/Controllers/NSTableViewController.java b/src/main/java/nsusbloader/Controllers/NSTableViewController.java index a8606b6..0252da9 100644 --- a/src/main/java/nsusbloader/Controllers/NSTableViewController.java +++ b/src/main/java/nsusbloader/Controllers/NSTableViewController.java @@ -33,15 +33,29 @@ public class NSTableViewController implements Initializable { public void initialize(URL url, ResourceBundle resourceBundle) { rowsObsLst = FXCollections.observableArrayList(); table.setPlaceholder(new Label()); + table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); TableColumn statusColumn = new TableColumn<>(resourceBundle.getString("tableStatusLbl")); TableColumn fileNameColumn = new TableColumn<>(resourceBundle.getString("tableFileNameLbl")); TableColumn fileSizeColumn = new TableColumn<>(resourceBundle.getString("tableSizeLbl")); TableColumn uploadColumn = new TableColumn<>(resourceBundle.getString("tableUploadLbl")); - statusColumn.setMinWidth(70.0); - fileNameColumn.setMinWidth(250.0); - fileSizeColumn.setMinWidth(70.0); - uploadColumn.setMinWidth(70.0); + // See https://bugs.openjdk.java.net/browse/JDK-8157687 + statusColumn.setMinWidth(100.0); + statusColumn.setPrefWidth(100.0); + statusColumn.setMaxWidth(100.0); + statusColumn.setResizable(false); + + fileNameColumn.setMinWidth(25.0); + + fileSizeColumn.setMinWidth(120.0); + fileSizeColumn.setPrefWidth(120.0); + fileSizeColumn.setMaxWidth(120.0); + fileSizeColumn.setResizable(false); + + uploadColumn.setMinWidth(100.0); + uploadColumn.setPrefWidth(100.0); + uploadColumn.setMaxWidth(100.0); + uploadColumn.setResizable(false); statusColumn.setCellValueFactory(new PropertyValueFactory<>("status")); fileNameColumn.setCellValueFactory(new PropertyValueFactory<>("nspFileName")); @@ -76,12 +90,6 @@ public class NSTableViewController implements Initializable { table.setItems(rowsObsLst); table.getColumns().addAll(statusColumn, fileNameColumn, fileSizeColumn, uploadColumn); - /* debug content - rowsObsLst.add(new NSLRowModel(new File("/home/loper/стихи_2"), true)); - rowsObsLst.add(new NSLRowModel(new File("/home/loper/стихи_2"), false)); - rowsObsLst.add(new NSLRowModel(new File("/home/loper/стихи_2"), false)); - rowsObsLst.add(new NSLRowModel(new File("/home/loper/стихи_2"), true)); - */ } /** * See uploadColumn callback. In case of GoldLeaf we have to restrict selection @@ -145,6 +153,7 @@ public class NSTableViewController implements Initializable { model.setStatus(status); } } + table.refresh(); } /** * Called if selected different USB protocol diff --git a/src/main/java/nsusbloader/MediatorControl.java b/src/main/java/nsusbloader/MediatorControl.java index 5ba2aeb..5544864 100644 --- a/src/main/java/nsusbloader/MediatorControl.java +++ b/src/main/java/nsusbloader/MediatorControl.java @@ -1,26 +1,28 @@ package nsusbloader; +import nsusbloader.Controllers.NSLMainController; + import java.util.concurrent.atomic.AtomicBoolean; -class MediatorControl { +public class MediatorControl { private AtomicBoolean isTransferActive = new AtomicBoolean(false); // Overcoded just for sure private NSLMainController applicationController; - static MediatorControl getInstance(){ + public static MediatorControl getInstance(){ return MediatorControlHold.INSTANCE; } private static class MediatorControlHold { private static final MediatorControl INSTANCE = new MediatorControl(); } - void setController(NSLMainController controller){ + public void setController(NSLMainController controller){ this.applicationController = controller; } NSLMainController getContoller(){ return this.applicationController; } - synchronized void setTransferActive(boolean state) { + public synchronized void setTransferActive(boolean state) { isTransferActive.set(state); applicationController.notifyTransmissionStarted(state); } - synchronized boolean getTransferActive() { return this.isTransferActive.get(); } + public synchronized boolean getTransferActive() { return this.isTransferActive.get(); } } diff --git a/src/main/java/nsusbloader/MessagesConsumer.java b/src/main/java/nsusbloader/MessagesConsumer.java index 302ab80..66c6af9 100644 --- a/src/main/java/nsusbloader/MessagesConsumer.java +++ b/src/main/java/nsusbloader/MessagesConsumer.java @@ -49,19 +49,18 @@ public class MessagesConsumer extends AnimationTimer { if (progressRecieved > 0) progress.forEach(prg -> progressBar.setProgress(prg)); - if (isInterrupted) { + if (isInterrupted) { // It's safe 'cuz it's could't be interrupted while HashMap populating MediatorControl.getInstance().setTransferActive(false); progressBar.setProgress(0.0); - if (statusMap.size() > 0) // It's safe 'cuz it's could't be interrupted while HashMap populating + if (statusMap.size() > 0) for (String key : statusMap.keySet()) tableViewController.setFileStatus(key, statusMap.get(key)); this.stop(); } - //TODO } void interrupt(){ this.isInterrupted = true; } -} +} \ No newline at end of file diff --git a/src/main/java/nsusbloader/NSLMain.java b/src/main/java/nsusbloader/NSLMain.java index ba64a09..6e1d0d0 100644 --- a/src/main/java/nsusbloader/NSLMain.java +++ b/src/main/java/nsusbloader/NSLMain.java @@ -6,22 +6,24 @@ import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.image.Image; import javafx.stage.Stage; +import nsusbloader.Controllers.NSLMainController; import java.util.Locale; import java.util.ResourceBundle; public class NSLMain extends Application { - static final String appVersion = "v0.2-DEVELOPMENT"; + public static final String appVersion = "v0.2"; @Override public void start(Stage primaryStage) throws Exception{ + + FXMLLoader loader = new FXMLLoader(getClass().getResource("/NSLMain.fxml")); + ResourceBundle rb; if (Locale.getDefault().getISO3Language().equals("rus")) rb = ResourceBundle.getBundle("locale", new Locale("ru")); else rb = ResourceBundle.getBundle("locale", new Locale("en")); - FXMLLoader loader = new FXMLLoader(getClass().getResource("/NSLMain.fxml")); - loader.setResources(rb); Parent root = loader.load(); @@ -36,7 +38,9 @@ public class NSLMain extends Application { primaryStage.setMinWidth(600); primaryStage.setMinHeight(375); Scene mainScene = new Scene(root, 800, 400); - mainScene.getStylesheets().add("/res/app.css"); + + mainScene.getStylesheets().add(AppPreferences.getInstance().getTheme()); + primaryStage.setScene(mainScene); primaryStage.show(); @@ -45,6 +49,9 @@ public class NSLMain extends Application { if(! ServiceWindow.getConfirmationWindow(rb.getString("windowTitleConfirmExit"), rb.getString("windowBodyConfirmExit"))) e.consume(); }); + + NSLMainController controller = loader.getController(); + primaryStage.setOnHidden(e-> controller.exit()); } public static void main(String[] args) { diff --git a/src/main/java/nsusbloader/ServiceWindow.java b/src/main/java/nsusbloader/ServiceWindow.java index 6ae679f..2982201 100644 --- a/src/main/java/nsusbloader/ServiceWindow.java +++ b/src/main/java/nsusbloader/ServiceWindow.java @@ -35,12 +35,9 @@ public class ServiceWindow { alertBox.getDialogPane().setMinWidth(Region.USE_PREF_SIZE); alertBox.getDialogPane().setMinHeight(Region.USE_PREF_SIZE); alertBox.setResizable(true); // Java bug workaround for JDR11/OpenJFX. TODO: nothing. really. - alertBox.getDialogPane().getStylesheets().add("/res/app.css"); + alertBox.getDialogPane().getStylesheets().add(AppPreferences.getInstance().getTheme()); Optional result = alertBox.showAndWait(); - if (result.get() == ButtonType.OK) - return true; - else - return false; + return (result.isPresent() && result.get() == ButtonType.OK); } } diff --git a/src/main/java/nsusbloader/UsbCommunications.java b/src/main/java/nsusbloader/UsbCommunications.java index 0f3efb6..d0df921 100644 --- a/src/main/java/nsusbloader/UsbCommunications.java +++ b/src/main/java/nsusbloader/UsbCommunications.java @@ -18,12 +18,13 @@ import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; -class UsbCommunications extends Task { +public class UsbCommunications extends Task { private final int DEFAULT_INTERFACE = 0; private BlockingQueue msgQueue; private BlockingQueue progressQueue; private HashMap statusMap; // BlockingQueue for literally one object. TODO: read more books ; replace to hashMap + private EFileStatus status = EFileStatus.FAILED; private MessagesConsumer msgConsumer; @@ -44,7 +45,7 @@ class UsbCommunications extends Task { Since this application let user an ability (theoretically) to choose same files in different folders, the latest selected file will be added to the list and handled correctly. I have no idea why he/she will make a decision to do that. Just in case, we're good in this point. */ - UsbCommunications(List nspList, String protocol){ + public UsbCommunications(List nspList, String protocol){ this.protocol = protocol; this.nspMap = new HashMap<>(); for (File f: nspList) @@ -275,28 +276,17 @@ class UsbCommunications extends Task { printLog("\tEnd chain", EMsgType.INFO); return null; } - /** - * Report transfer status - * */ - private void reportTransferStatus(EFileStatus status){ - for (String fileName: nspMap.keySet()) - statusMap.put(fileName, status); - } /** * Tinfoil processing * */ private class TinFoil{ TinFoil(){ - if (!sendListOfNSP()) { - reportTransferStatus(EFileStatus.FAILED); + if (!sendListOfNSP()) return; - } if (proceedCommands()) // REPORT SUCCESS - reportTransferStatus(EFileStatus.UPLOADED); - else // REPORT FAILURE - reportTransferStatus(EFileStatus.FAILED); + status = EFileStatus.UPLOADED; // Don't change status that is already set to FAILED } /** * Send what NSP will be transferred @@ -547,15 +537,14 @@ class UsbCommunications extends Task { PFSProvider pfsElement = new PFSProvider(nspMap.get(nspMap.keySet().toArray()[0]), msgQueue); if (!pfsElement.init()) { printLog("GL File provided have incorrect structure and won't be uploaded", EMsgType.FAIL); - reportTransferStatus(EFileStatus.INCORRECT_FILE_FAILED); + status = EFileStatus.INCORRECT_FILE_FAILED; return; } printLog("GL File structure validated and it will be uploaded", EMsgType.PASS); if (initGoldLeafProtocol(pfsElement)) - reportTransferStatus(EFileStatus.UPLOADED); - else - reportTransferStatus(EFileStatus.FAILED); + status = EFileStatus.UPLOADED; + // else - no change status that is already set to FAILED } private boolean initGoldLeafProtocol(PFSProvider pfsElement){ // Go parse commands @@ -774,6 +763,11 @@ class UsbCommunications extends Task { LibUsb.exit(contextNS); printLog("Requested context close", EMsgType.INFO); } + + // Report status + for (String fileName: nspMap.keySet()) + statusMap.put(fileName, status); + msgConsumer.interrupt(); } /** diff --git a/src/main/resources/NSLMain.fxml b/src/main/resources/NSLMain.fxml index ac38942..5ef03a6 100644 --- a/src/main/resources/NSLMain.fxml +++ b/src/main/resources/NSLMain.fxml @@ -17,7 +17,7 @@ - + diff --git a/src/main/resources/res/app.css b/src/main/resources/res/app_dark.css similarity index 92% rename from src/main/resources/res/app.css rename to src/main/resources/res/app_dark.css index dfc093e..19443a6 100644 --- a/src/main/resources/res/app.css +++ b/src/main/resources/res/app_dark.css @@ -98,7 +98,7 @@ -fx-background-color: #f7fafa; -fx-min-height: 1; } - +// -======================== Choice box =========================- .choice-box { -fx-background-color: #4f4f4f; -fx-border-color: #4f4f4f; @@ -120,13 +120,19 @@ } // Background color of the whole context menu -.choice-box .context-menu { -fx-background-color: #2d2d2d; } +.choice-box .context-menu { + -fx-background-color: #2d2d2d; +} + // Focused item background color in the list -.choice-box .menu-item:focused { -fx-background-color: #eea11e; } -// Text color of non-focused items in the list -.choice-box .menu-item > .label { -fx-text-fill: #f7fafa; } -// Text color of focused item in the list -.choice-box .menu-item:focused > .label { -fx-text-fill: #2d2d2d; } +.choice-box .context-menu .menu-item:focused { + -fx-background-color: #eea11e; + +} +.choice-box .context-menu .menu-item:focused .label { + -fx-text-fill: #2c2c2c; +} + // -======================== TAB PANE =========================- .tab-pane .tab SVGPath{ @@ -176,12 +182,18 @@ -fx-border-radius: 3; -fx-border-width: 2; } +.table-view .arrow { + -fx-mark-color: #08f3ff ; +} .table-view .column-header { -fx-background-color: transparent; + -fx-border-width: 0 1 2 0; + -fx-border-color: #6d8484; } .table-view .column-header-background .label{ -fx-background-color: transparent; -fx-text-fill: #08f3ff; + } .table-view .column-header-background, .table-view .filler{ -fx-background-color: #4f4f4f; @@ -194,14 +206,14 @@ -fx-background-color: -fx-table-cell-border-color, #424242; -fx-background-insets: 0, 0 0 1 0; -fx-padding: 0.0em; /* 0 */ - -fx-table-cell-border-color: #f7fafa; + -fx-table-cell-border-color: #6d8484; } .table-row-cell:odd, .table-row-cell:odd:filled:selected, .table-row-cell:odd:selected{ -fx-background-color: -fx-table-cell-border-color, #4f4f4f; -fx-background-insets: 0, 0 0 1 0; -fx-padding: 0.0em; /* 0 */ - -fx-table-cell-border-color: #f7fafa; + -fx-table-cell-border-color: #6d8484; } // -========================== Context menu =====================- .context-menu { diff --git a/src/main/resources/res/app_light.css b/src/main/resources/res/app_light.css index fe3e337..e2857bf 100644 --- a/src/main/resources/res/app_light.css +++ b/src/main/resources/res/app_light.css @@ -99,7 +99,7 @@ -fx-background-color: #2c2c2c; -fx-min-height: 1; } - +// -======================== Choice box =========================- .choice-box { -fx-background-color: #fefefe; -fx-border-color: #fefefe; @@ -121,13 +121,18 @@ } // Background color of the whole context menu -.choice-box .context-menu { -fx-background-color: #fefefe; } +.choice-box .context-menu { + -fx-background-color: #fefefe; +} + // Focused item background color in the list -.choice-box .menu-item:focused { -fx-background-color: #eea11e; } -// Text color of non-focused items in the list -.choice-box .menu-item > .label { -fx-text-fill: #2c2c2c; } -// Text color of focused item in the list -.choice-box .menu-item:focused > .label { -fx-text-fill: #2d2d2d; } +.choice-box .context-menu .menu-item:focused { + -fx-background-color: #eea11e; + +} +.choice-box .context-menu .menu-item:focused .label { + -fx-text-fill: #2c2c2c; +} // -======================== TAB PANE =========================- .tab-pane .tab SVGPath{ @@ -177,8 +182,13 @@ -fx-border-radius: 3; -fx-border-width: 2; } +.table-view .arrow { + -fx-mark-color: #2c2c2c ; +} .table-view .column-header { -fx-background-color: transparent; + -fx-border-width: 0 1 2 0; + -fx-border-color: #b0b0b0; } .table-view .column-header-background .label{ -fx-background-color: transparent; @@ -195,14 +205,14 @@ -fx-background-color: -fx-table-cell-border-color, #d3fffd; -fx-background-insets: 0, 0 0 1 0; -fx-padding: 0.0em; /* 0 */ - -fx-table-cell-border-color: #2c2c2c; + -fx-table-cell-border-color: #b0b0b0; } .table-row-cell:odd, .table-row-cell:odd:filled:selected, .table-row-cell:odd:selected{ -fx-background-color: -fx-table-cell-border-color, #fefefe; -fx-background-insets: 0, 0 0 1 0; -fx-padding: 0.0em; /* 0 */ - -fx-table-cell-border-color: #2c2c2c; + -fx-table-cell-border-color: #b0b0b0; } // -========================== Context menu =====================- .context-menu {