diff --git a/README.md b/README.md
index 13e2a20..d2b131d 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,14 @@
# NS-USBloader
-NS-USBloader is a PC-side tinfoil NSP USB uploader. Replacement for default usb_install_pc.py
+NS-USBloader is a PC-side TinFoil/GoldLeaf NSP USB uploader. Replacement for default usb_install_pc.py and GoldTree.
With GUI and cookies.
Read more: https://developersu.blogspot.com/2019/02/ns-usbloader-en.html
-Here is the version of 'not perfect byt anyway' [tinfoil I use](https://cloud.mail.ru/public/DwbX/H8d2p3aYR).
+Here is the version of 'not perfect but anyway' [tinfoil I use](https://cloud.mail.ru/public/DwbX/H8d2p3aYR).
+Ok, I'm almost sure that this version has bugs. I don't remember where I downloaded it. But it works for me somehow..
+Let's rephrase, if you have working version of TinFoil DO NOT use this one.
## License
@@ -32,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:
@@ -56,7 +58,22 @@ Set 'Security & Privacy' if needed.
## Known bugs
* Unable to interrupt transmission when libusb awaiting for read event (when user sent NSP list but didn't selected anything on NS).
+## NOTES
+Table 'Status' = 'Uploaded' does not means that file installed. It means that it has been sent to NS without any issues! That's what this app about.
+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 v0.1
+- [ ] macOS QA v0.2 (partly)
- [x] Windows support
-- [ ] code refactoring
\ No newline at end of file
+- [ ] code refactoring (almost. todo: printLog() )
+- [x] GoldLeaf support
+- [ ] XCI support
+- [ ] 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/pom.xml b/pom.xml
index 2232a90..a04cb06 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,12 +5,50 @@
4.0.0
loper
- NS-USBloader
- 0.1-SNAPSHOT
NS-USBloader
+
+ ns-usbloader
+ 0.2-SNAPSHOT
+
+ https://github.com/developersu/ns-usbloader/
+
+ NSP USB loader for TinFoil and GoldLeaf
+
+ 2019
+
+ Dmitry Isaenko
+ https://developersu.blogspot.com/
+
+
+
+
+ GPLv3
+ LICENSE
+ manual
+
+
+
+
+
+ developer.su
+ Dmitry Isaenko
+
+ Developer
+
+ +3
+ https://developersu.blogspot.com/
+
+
+
UTF-8
+
+
+ GitHub
+ https://github.com/developer_su/${project.artifactId}/issues
+
+
org.openjfx
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/Controllers/NSLMainController.java b/src/main/java/nsusbloader/Controllers/NSLMainController.java
new file mode 100644
index 0000000..1ae0ef0
--- /dev/null
+++ b/src/main/java/nsusbloader/Controllers/NSLMainController.java
@@ -0,0 +1,195 @@
+package nsusbloader.Controllers;
+
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.fxml.FXML;
+import javafx.fxml.Initializable;
+import javafx.scene.control.*;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.Region;
+import javafx.stage.FileChooser;
+import nsusbloader.AppPreferences;
+import nsusbloader.MediatorControl;
+import nsusbloader.NSLMain;
+import nsusbloader.UsbCommunications;
+
+import java.io.File;
+import java.net.URL;
+import java.util.List;
+import java.util.ResourceBundle;
+
+public class NSLMainController implements Initializable {
+
+ private ResourceBundle resourceBundle;
+
+ @FXML
+ public TextArea logArea; // Accessible from Mediator
+ @FXML
+ private Button selectNspBtn;
+ @FXML
+ private Button uploadStopBtn;
+ private Region btnUpStopImage;
+ @FXML
+ public ProgressBar progressBar; // Accessible from Mediator
+ @FXML
+ private ChoiceBox choiceProtocol;
+ @FXML
+ private Button switchThemeBtn;
+
+ @FXML
+ private Pane specialPane;
+
+ @FXML
+ public NSTableViewController tableFilesListController; // Accessible from Mediator
+
+ private Thread usbThread;
+
+ private String previouslyOpenedPath;
+
+ @Override
+ public void initialize(URL url, ResourceBundle rb) {
+ this.resourceBundle = rb;
+ 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");
+
+ logArea.appendText(rb.getString("logsGreetingsMessage2")+"\n");
+
+ MediatorControl.getInstance().setController(this);
+
+ specialPane.getStyleClass().add("special-pane-as-border"); // UI hacks
+
+ uploadStopBtn.setDisable(true);
+ selectNspBtn.setOnAction(e->{ selectFilesBtnAction(); });
+ uploadStopBtn.setOnAction(e->{ uploadBtnAction(); });
+
+ selectNspBtn.getStyleClass().add("buttonSelect");
+
+ this.btnUpStopImage = new Region();
+ btnUpStopImage.getStyleClass().add("regionUpload");
+ //uploadStopBtn.getStyleClass().remove("button");
+ uploadStopBtn.getStyleClass().add("buttonUp");
+ uploadStopBtn.setGraphic(btnUpStopImage);
+
+ ObservableList choiceProtocolList = FXCollections.observableArrayList("TinFoil", "GoldLeaf");
+ choiceProtocol.setItems(choiceProtocolList);
+ choiceProtocol.getSelectionModel().select(0); // TODO: shared settings
+ choiceProtocol.setOnAction(e->tableFilesListController.setNewProtocol(choiceProtocol.getSelectionModel().getSelectedItem())); // Add listener to notify tableView controller
+ tableFilesListController.setNewProtocol(choiceProtocol.getSelectionModel().getSelectedItem()); // Notify tableView controller
+
+ this.previouslyOpenedPath = null;
+
+ Region btnSwitchImage = new Region();
+ 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_dark.css")) {
+ switchThemeBtn.getScene().getStylesheets().remove("/res/app_dark.css");
+ switchThemeBtn.getScene().getStylesheets().add("/res/app_light.css");
+ }
+ else {
+ switchThemeBtn.getScene().getStylesheets().remove("/res/app_light.css");
+ switchThemeBtn.getScene().getStylesheets().add("/res/app_dark.css");
+ }
+ }
+ /**
+ * Functionality for selecting NSP button.
+ * Uses setReady and setNotReady to simplify code readability.
+ * */
+ private void selectFilesBtnAction(){
+ List filesList;
+ FileChooser fileChooser = new FileChooser();
+ fileChooser.setTitle(resourceBundle.getString("btnFileOpen"));
+
+ 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("NSP ROM", "*.nsp"));
+
+ filesList = fileChooser.showOpenMultipleDialog(logArea.getScene().getWindow());
+ if (filesList != null && !filesList.isEmpty()) {
+ tableFilesListController.setFiles(filesList);
+ uploadStopBtn.setDisable(false);
+ previouslyOpenedPath = filesList.get(0).getParent();
+ }
+ else{
+ tableFilesListController.setFiles(null);
+ uploadStopBtn.setDisable(true);
+ }
+ }
+ /**
+ * It's button listener when no transmission executes
+ * */
+ private void uploadBtnAction(){
+ if (usbThread == null || !usbThread.isAlive()){
+ List nspToUpload;
+ if ((nspToUpload = tableFilesListController.getFiles()) == null) {
+ resourceBundle.getString("logsNoFolderFileSelected");
+ return;
+ }else {
+ logArea.setText(resourceBundle.getString("logsFilesToUploadTitle")+"\n");
+ for (File item: nspToUpload)
+ logArea.appendText(" "+item.getAbsolutePath()+"\n");
+ }
+ UsbCommunications usbCommunications = new UsbCommunications(nspToUpload, choiceProtocol.getSelectionModel().getSelectedItem());
+ usbThread = new Thread(usbCommunications);
+ usbThread.start();
+ }
+ }
+ /**
+ * It's button listener when transmission in progress
+ * */
+ private void stopBtnAction(){
+ if (usbThread != null && usbThread.isAlive()){
+ usbThread.interrupt();
+ }
+ }
+ /**
+ * This thing modify UI for reusing 'Upload to NS' button and make functionality set for "Stop transmission"
+ * Called from mediator
+ * */
+ public void notifyTransmissionStarted(boolean isTransmissionStarted){
+ if (isTransmissionStarted) {
+ selectNspBtn.setDisable(true);
+ uploadStopBtn.setOnAction(e->{ stopBtnAction(); });
+
+ uploadStopBtn.setText(resourceBundle.getString("btnStop"));
+
+ btnUpStopImage.getStyleClass().remove("regionUpload");
+ btnUpStopImage.getStyleClass().add("regionStop");
+
+ uploadStopBtn.getStyleClass().remove("buttonUp");
+ uploadStopBtn.getStyleClass().add("buttonStop");
+ }
+ else {
+ selectNspBtn.setDisable(false);
+ uploadStopBtn.setOnAction(e->{ uploadBtnAction(); });
+
+ uploadStopBtn.setText(resourceBundle.getString("btnUpload"));
+
+ btnUpStopImage.getStyleClass().remove("regionStop");
+ btnUpStopImage.getStyleClass().add("regionUpload");
+
+ uploadStopBtn.getStyleClass().remove("buttonStop");
+ 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
new file mode 100644
index 0000000..1765eea
--- /dev/null
+++ b/src/main/java/nsusbloader/Controllers/NSLRowModel.java
@@ -0,0 +1,54 @@
+package nsusbloader.Controllers;
+
+import nsusbloader.NSLDataTypes.EFileStatus;
+
+import java.io.File;
+
+public class NSLRowModel {
+
+ private String status;
+ private File nspFile;
+ private String nspFileName;
+ private String nspFileSize;
+ private boolean markForUpload;
+
+ NSLRowModel(File nspFile, boolean checkBoxValue){
+ this.nspFile = nspFile;
+ this.markForUpload = checkBoxValue;
+ this.nspFileName = nspFile.getName();
+ this.nspFileSize = String.format("%.2f", nspFile.length()/1024.0/1024.0);
+ this.status = "";
+ }
+ // Model methods start
+ public String getStatus(){
+ return status;
+ }
+ public String getNspFileName(){
+ return nspFileName;
+ }
+ public String getNspFileSize() { return nspFileSize; }
+ public boolean isMarkForUpload() {
+ return markForUpload;
+ }
+ // Model methods end
+
+ public void setMarkForUpload(boolean value){
+ markForUpload = value;
+ }
+ public File getNspFile(){ return nspFile; }
+ public void setStatus(EFileStatus status){ // TODO: Localization
+ switch (status){
+ case UPLOADED:
+ this.status = "Success";
+ markForUpload = false;
+ break;
+ case FAILED:
+ this.status = "Failed";
+ break;
+ case INCORRECT_FILE_FAILED:
+ 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
new file mode 100644
index 0000000..0252da9
--- /dev/null
+++ b/src/main/java/nsusbloader/Controllers/NSTableViewController.java
@@ -0,0 +1,177 @@
+package nsusbloader.Controllers;
+
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.fxml.FXML;
+import javafx.fxml.Initializable;
+import javafx.scene.control.Label;
+import javafx.scene.control.TableCell;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableView;
+import javafx.scene.control.cell.CheckBoxTableCell;
+import javafx.scene.control.cell.PropertyValueFactory;
+import javafx.util.Callback;
+import nsusbloader.NSLDataTypes.EFileStatus;
+
+import java.io.File;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.ResourceBundle;
+
+public class NSTableViewController implements Initializable {
+ @FXML
+ private TableView table;
+ private ObservableList rowsObsLst;
+
+ private String protocol;
+
+ @Override
+ 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"));
+ // 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"));
+ fileSizeColumn.setCellValueFactory(new PropertyValueFactory<>("nspFileSize"));
+ // ><
+ uploadColumn.setCellValueFactory(new Callback, ObservableValue>() {
+ @Override
+ public ObservableValue call(TableColumn.CellDataFeatures paramFeatures) {
+ NSLRowModel model = paramFeatures.getValue();
+
+ SimpleBooleanProperty booleanProperty = new SimpleBooleanProperty(model.isMarkForUpload());
+
+ booleanProperty.addListener(new ChangeListener() {
+ @Override
+ public void changed(ObservableValue extends Boolean> observableValue, Boolean oldValue, Boolean newValue) {
+ model.setMarkForUpload(newValue);
+ restrictSelection(model);
+ }
+ });
+
+ return booleanProperty;
+ }
+ });
+
+ uploadColumn.setCellFactory(new Callback, TableCell>() {
+ @Override
+ public TableCell call(TableColumn paramFeatures) {
+ CheckBoxTableCell cell = new CheckBoxTableCell<>();
+ return cell;
+ }
+ });
+
+ table.setItems(rowsObsLst);
+ table.getColumns().addAll(statusColumn, fileNameColumn, fileSizeColumn, uploadColumn);
+ }
+ /**
+ * See uploadColumn callback. In case of GoldLeaf we have to restrict selection
+ * */
+ private void restrictSelection(NSLRowModel modelChecked){
+ if (!protocol.equals("TinFoil") && rowsObsLst.size() > 1) { // Tinfoil doesn't need any restrictions. If only one file in list, also useless
+ for (NSLRowModel model: rowsObsLst){
+ if (model != modelChecked)
+ model.setMarkForUpload(false);
+ }
+ table.refresh();
+ }
+ }
+ /**
+ * Add files when user selected them
+ * */
+ public void setFiles(List files){
+ rowsObsLst.clear(); // TODO: consider table refresh
+ if (files == null) {
+ return;
+ }
+ if (protocol.equals("TinFoil")){
+ for (File nspFile: files){
+ rowsObsLst.add(new NSLRowModel(nspFile, true));
+ }
+ }
+ else {
+ rowsObsLst.clear();
+ for (File nspFile: files){
+ rowsObsLst.add(new NSLRowModel(nspFile, false));
+ }
+ rowsObsLst.get(0).setMarkForUpload(true);
+ }
+ }
+ /**
+ * Return files ready for upload. Requested from NSLMainController only
+ * @return null if no files marked for upload
+ * List if there are files
+ * */
+ public List getFiles(){
+ List files = new ArrayList<>();
+ if (rowsObsLst.isEmpty())
+ return null;
+ else {
+ for (NSLRowModel model: rowsObsLst){
+ if (model.isMarkForUpload())
+ files.add(model.getNspFile());
+ }
+ if (!files.isEmpty())
+ return files;
+ else
+ return null;
+ }
+ }
+ /**
+ * Update files in case something is wrong. Requested from UsbCommunications
+ * */
+ public void setFileStatus(String fileName, EFileStatus status){
+ for (NSLRowModel model: rowsObsLst){
+ if (model.getNspFileName().equals(fileName)){
+ model.setStatus(status);
+ }
+ }
+ table.refresh();
+ }
+ /**
+ * Called if selected different USB protocol
+ * */
+ public void setNewProtocol(String newProtocol){
+ protocol = newProtocol;
+ if (rowsObsLst.isEmpty())
+ return;
+ if (newProtocol.equals("TinFoil")){
+ for (NSLRowModel model: rowsObsLst)
+ model.setMarkForUpload(true);
+ }
+ else {
+ for (NSLRowModel model: rowsObsLst)
+ model.setMarkForUpload(false);
+ rowsObsLst.get(0).setMarkForUpload(true);
+ }
+ table.refresh();
+ }
+
+}
diff --git a/src/main/java/nsusbloader/MediatorControl.java b/src/main/java/nsusbloader/MediatorControl.java
index 66be7e2..5544864 100644
--- a/src/main/java/nsusbloader/MediatorControl.java
+++ b/src/main/java/nsusbloader/MediatorControl.java
@@ -1,25 +1,28 @@
package nsusbloader;
-class MediatorControl {
- private boolean isTransferActive = false;
+import nsusbloader.Controllers.NSLMainController;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+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 registerController(NSLMainController controller){
+ public void setController(NSLMainController controller){
this.applicationController = controller;
}
+ NSLMainController getContoller(){ return this.applicationController; }
- synchronized void setTransferActive(boolean state) {
- isTransferActive = state;
+ public synchronized void setTransferActive(boolean state) {
+ isTransferActive.set(state);
applicationController.notifyTransmissionStarted(state);
}
- synchronized boolean getTransferActive() {
- return this.isTransferActive;
- }
+ 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 bedf460..66c6af9 100644
--- a/src/main/java/nsusbloader/MessagesConsumer.java
+++ b/src/main/java/nsusbloader/MessagesConsumer.java
@@ -3,8 +3,11 @@ package nsusbloader;
import javafx.animation.AnimationTimer;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.TextArea;
+import nsusbloader.Controllers.NSTableViewController;
+import nsusbloader.NSLDataTypes.EFileStatus;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.concurrent.BlockingQueue;
public class MessagesConsumer extends AnimationTimer {
@@ -13,18 +16,24 @@ public class MessagesConsumer extends AnimationTimer {
private final BlockingQueue progressQueue;
private final ProgressBar progressBar;
+ private final HashMap statusMap;
+ private final NSTableViewController tableViewController;
private boolean isInterrupted;
- MessagesConsumer(BlockingQueue msgQueue, TextArea logsArea, BlockingQueue progressQueue, ProgressBar progressBar){
- this.msgQueue = msgQueue;
- this.logsArea = logsArea;
+ MessagesConsumer(BlockingQueue msgQueue, BlockingQueue progressQueue, HashMap statusMap){
+ this.isInterrupted = false;
+
+ this.msgQueue = msgQueue;
+ this.logsArea = MediatorControl.getInstance().getContoller().logArea;
- this.progressBar = progressBar;
this.progressQueue = progressQueue;
+ this.progressBar = MediatorControl.getInstance().getContoller().progressBar;
+
+ this.statusMap = statusMap;
+ this.tableViewController = MediatorControl.getInstance().getContoller().tableFilesListController;
progressBar.setProgress(0.0);
- this.isInterrupted = false;
MediatorControl.getInstance().setTransferActive(true);
}
@@ -40,15 +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)
+ 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/NSLDataTypes/EFileStatus.java b/src/main/java/nsusbloader/NSLDataTypes/EFileStatus.java
new file mode 100644
index 0000000..674521a
--- /dev/null
+++ b/src/main/java/nsusbloader/NSLDataTypes/EFileStatus.java
@@ -0,0 +1,5 @@
+package nsusbloader.NSLDataTypes;
+
+public enum EFileStatus {
+ UPLOADED, INCORRECT_FILE_FAILED, FAILED
+}
diff --git a/src/main/java/nsusbloader/NSLDataTypes/EMsgType.java b/src/main/java/nsusbloader/NSLDataTypes/EMsgType.java
new file mode 100644
index 0000000..5d88ea5
--- /dev/null
+++ b/src/main/java/nsusbloader/NSLDataTypes/EMsgType.java
@@ -0,0 +1,5 @@
+package nsusbloader.NSLDataTypes;
+
+public enum EMsgType {
+ PASS, FAIL, INFO, WARNING
+}
diff --git a/src/main/java/nsusbloader/NSLMain.java b/src/main/java/nsusbloader/NSLMain.java
index d34fd56..6e1d0d0 100644
--- a/src/main/java/nsusbloader/NSLMain.java
+++ b/src/main/java/nsusbloader/NSLMain.java
@@ -1,11 +1,3 @@
-/**
- Name: NSL-USBFoil
- @author Dmitry Isaenko
- License: GNU GPL v.3
- @see https://github.com/developersu/
- @see https://developersu.blogspot.com/
- 2019, Russia
- */
package nsusbloader;
import javafx.application.Application;
@@ -14,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.1";
+ 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();
@@ -44,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();
@@ -53,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/NSLMainController.java b/src/main/java/nsusbloader/NSLMainController.java
deleted file mode 100644
index ae5febd..0000000
--- a/src/main/java/nsusbloader/NSLMainController.java
+++ /dev/null
@@ -1,133 +0,0 @@
-package nsusbloader;
-
-import javafx.fxml.FXML;
-import javafx.fxml.Initializable;
-import javafx.scene.control.Button;
-import javafx.scene.control.ProgressBar;
-import javafx.scene.control.TextArea;
-import javafx.scene.layout.Region;
-import javafx.stage.FileChooser;
-
-import java.io.File;
-import java.net.URL;
-import java.util.List;
-import java.util.ResourceBundle;
-
-public class NSLMainController implements Initializable {
-
- private ResourceBundle resourceBundle;
-
- private List nspToUpload;
-
- @FXML
- private TextArea logArea;
- @FXML
- private Button selectNspBtn;
- @FXML
- private Button uploadStopBtn;
- private Region btnUpStopImage;
- @FXML
- private ProgressBar progressBar;
-
- private Thread usbThread;
-
- @Override
- public void initialize(URL url, ResourceBundle rb) {
- this.resourceBundle = rb;
- 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");
-
- logArea.appendText(rb.getString("logsGreetingsMessage2")+"\n");
-
- MediatorControl.getInstance().registerController(this);
-
- uploadStopBtn.setDisable(true);
- selectNspBtn.setOnAction(e->{ selectFilesBtnAction(); });
- uploadStopBtn.setOnAction(e->{ uploadBtnAction(); });
-
- this.btnUpStopImage = new Region();
- btnUpStopImage.getStyleClass().add("regionUpload");
- //uploadStopBtn.getStyleClass().remove("button");
- uploadStopBtn.getStyleClass().add("buttonUp");
- uploadStopBtn.setGraphic(btnUpStopImage);
- }
- /**
- * Functionality for selecting NSP button.
- * Uses setReady and setNotReady to simplify code readability.
- * */
- private void selectFilesBtnAction(){
- List filesList;
- FileChooser fileChooser = new FileChooser();
- fileChooser.setTitle(resourceBundle.getString("btnFileOpen"));
- fileChooser.setInitialDirectory(new File(System.getProperty("user.home"))); // TODO: read from prefs
- fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("NS ROM", "*.nsp"));
-
- filesList = fileChooser.showOpenMultipleDialog(logArea.getScene().getWindow());
- if (filesList != null && !filesList.isEmpty())
- setReady(filesList);
- else
- setNotReady(resourceBundle.getString("logsNoFolderFileSelected"));
- }
- private void setReady(List filesList){
- logArea.setText(resourceBundle.getString("logsFilesToUploadTitle")+"\n");
- for (File item: filesList)
- logArea.appendText(" "+item.getAbsolutePath()+"\n");
- nspToUpload = filesList;
- uploadStopBtn.setDisable(false);
- }
- private void setNotReady(String whyNotReady){
- logArea.setText(whyNotReady);
- nspToUpload = null;
- uploadStopBtn.setDisable(true);
- }
- /**
- * It's button listener when no transmission executes
- * */
- private void uploadBtnAction(){
- if (usbThread == null || !usbThread.isAlive()){
- UsbCommunications usbCommunications = new UsbCommunications(logArea, progressBar, nspToUpload); //todo: progress bar
- usbThread = new Thread(usbCommunications);
- usbThread.start();
- }
- }
- /**
- * It's button listener when transmission in progress
- * */
- private void stopBtnAction(){
- if (usbThread != null && usbThread.isAlive()){
- usbThread.interrupt();
- }
- }
- /**
- * This thing modify UI for reusing 'Upload to NS' button and make functionality set for "Stop transmission"
- * Called from mediator
- * */
- void notifyTransmissionStarted(boolean isTransmissionStarted){
- if (isTransmissionStarted) {
- selectNspBtn.setDisable(true);
- uploadStopBtn.setOnAction(e->{ stopBtnAction(); });
-
- uploadStopBtn.setText(resourceBundle.getString("btnStop"));
-
- btnUpStopImage.getStyleClass().remove("regionUpload");
- btnUpStopImage.getStyleClass().add("regionStop");
-
- uploadStopBtn.getStyleClass().remove("buttonUp");
- uploadStopBtn.getStyleClass().add("buttonStop");
- }
- else {
- selectNspBtn.setDisable(false);
- uploadStopBtn.setOnAction(e->{ uploadBtnAction(); });
-
- uploadStopBtn.setText(resourceBundle.getString("btnUpload"));
-
- btnUpStopImage.getStyleClass().remove("regionStop");
- btnUpStopImage.getStyleClass().add("regionUpload");
-
- uploadStopBtn.getStyleClass().remove("buttonStop");
- uploadStopBtn.getStyleClass().add("buttonUp");
- }
- }
-}
diff --git a/src/main/java/nsusbloader/PFS/NCAFile.java b/src/main/java/nsusbloader/PFS/NCAFile.java
new file mode 100644
index 0000000..ea537ce
--- /dev/null
+++ b/src/main/java/nsusbloader/PFS/NCAFile.java
@@ -0,0 +1,25 @@
+package nsusbloader.PFS;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * Data class to hold NCA, tik, xml etc. meta-information
+ * */
+public class NCAFile {
+ //private int ncaNumber;
+ private byte[] ncaFileName;
+ private long ncaOffset;
+ private long ncaSize;
+
+ //public void setNcaNumber(int ncaNumber){ this.ncaNumber = ncaNumber; }
+ public void setNcaFileName(byte[] ncaFileName) { this.ncaFileName = ncaFileName; }
+ public void setNcaOffset(long ncaOffset) { this.ncaOffset = ncaOffset; }
+ public void setNcaSize(long ncaSize) { this.ncaSize = ncaSize; }
+
+ //public int getNcaNumber() {return this.ncaNumber; }
+ public byte[] getNcaFileName() { return ncaFileName; }
+ public byte[] getNcaFileNameLength() { return ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(ncaFileName.length).array(); }
+ public long getNcaOffset() { return ncaOffset; }
+ public long getNcaSize() { return ncaSize; }
+}
diff --git a/src/main/java/nsusbloader/PFS/PFSProvider.java b/src/main/java/nsusbloader/PFS/PFSProvider.java
new file mode 100644
index 0000000..2c22593
--- /dev/null
+++ b/src/main/java/nsusbloader/PFS/PFSProvider.java
@@ -0,0 +1,243 @@
+package nsusbloader.PFS;
+
+import nsusbloader.NSLDataTypes.EMsgType;
+import nsusbloader.ServiceWindow;
+
+import java.io.*;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.concurrent.BlockingQueue;
+
+/**
+ * Used in GoldLeaf USB protocol
+ * */
+public class PFSProvider {
+ private static final byte[] PFS0 = new byte[]{(byte)0x50, (byte)0x46, (byte)0x53, (byte)0x30}; // PFS0, and what did you think?
+
+ private BlockingQueue msgQueue;
+ private ResourceBundle rb;
+
+ private RandomAccessFile randAccessFile;
+ private String nspFileName;
+ private NCAFile[] ncaFiles;
+ private long bodySize;
+ private int ticketID = -1;
+
+ public PFSProvider(File nspFile, BlockingQueue msgQueue){
+ this.msgQueue = msgQueue;
+ try {
+ this.randAccessFile = new RandomAccessFile(nspFile, "r");
+ nspFileName = nspFile.getName();
+ }
+ catch (FileNotFoundException fnfe){
+ printLog("PFS File not founnd: \n "+fnfe.getMessage(), EMsgType.FAIL);
+ nspFileName = null;
+ }
+ if (Locale.getDefault().getISO3Language().equals("rus"))
+ rb = ResourceBundle.getBundle("locale", new Locale("ru"));
+ else
+ rb = ResourceBundle.getBundle("locale", new Locale("en"));
+ }
+
+ public boolean init() {
+ if (nspFileName == null)
+ return false;
+
+ int filesCount;
+ int header;
+
+ printLog("PFS Start NSP file analyze for ["+nspFileName+"]", EMsgType.INFO);
+ try {
+ byte[] fileStartingBytes = new byte[12];
+ // Read PFS0, files count, header, padding (4 zero bytes)
+ if (randAccessFile.read(fileStartingBytes) == 12)
+ printLog("PFS Read file starting bytes.", EMsgType.PASS);
+ else {
+ printLog("PFS Read file starting bytes.", EMsgType.FAIL);
+ randAccessFile.close();
+ return false;
+ }
+ // Check PFS0
+ if (Arrays.equals(PFS0, Arrays.copyOfRange(fileStartingBytes, 0, 4)))
+ printLog("PFS Read 'PFS0'.", EMsgType.PASS);
+ else {
+ printLog("PFS Read 'PFS0'.", EMsgType.WARNING);
+ if (!ServiceWindow.getConfirmationWindow(nspFileName+"\n"+rb.getString("windowTitleConfirmWrongPFS0"), rb.getString("windowBodyConfirmWrongPFS0"))) {
+ randAccessFile.close();
+ return false;
+ }
+ }
+ // Get files count
+ filesCount = ByteBuffer.wrap(Arrays.copyOfRange(fileStartingBytes, 4, 8)).order(ByteOrder.LITTLE_ENDIAN).getInt();
+ if (filesCount > 0 ) {
+ printLog("PFS Read files count [" + filesCount + "]", EMsgType.PASS);
+ }
+ else {
+ printLog("PFS Read files count", EMsgType.FAIL);
+ randAccessFile.close();
+ return false;
+ }
+ // Get header
+ header = ByteBuffer.wrap(Arrays.copyOfRange(fileStartingBytes, 8, 12)).order(ByteOrder.LITTLE_ENDIAN).getInt();
+ if (header > 0 )
+ printLog("PFS Read header ["+header+"]", EMsgType.PASS);
+ else {
+ printLog("PFS Read header ", EMsgType.FAIL);
+ randAccessFile.close();
+ return false;
+ }
+ //*********************************************************************************************
+ // Create NCA set
+ this.ncaFiles = new NCAFile[filesCount];
+ // Collect files from NSP
+ byte[] ncaInfoArr = new byte[24]; // should be unsigned long, but.. java.. u know my pain man
+
+ HashMap ncaNameOffsets = new LinkedHashMap<>();
+
+ int offset;
+ long nca_offset;
+ long nca_size;
+ long nca_name_offset;
+
+ for (int i=0; i= 0?EMsgType.PASS:EMsgType.WARNING);
+ printLog(" NCA size check: "+nca_size, nca_size >= 0?EMsgType.PASS: EMsgType.WARNING);
+ printLog(" NCA name offset check "+nca_name_offset, nca_name_offset >= 0?EMsgType.PASS:EMsgType.WARNING);
+
+ NCAFile ncaFile = new NCAFile();
+ ncaFile.setNcaOffset(nca_offset);
+ ncaFile.setNcaSize(nca_size);
+ this.ncaFiles[i] = ncaFile;
+
+ ncaNameOffsets.put(i, nca_name_offset);
+ }
+ // Final offset
+ byte[] bufForInt = new byte[4];
+ if ((randAccessFile.read(bufForInt) == 4) && (Arrays.equals(bufForInt, new byte[4])))
+ printLog("PFS Final padding check", EMsgType.PASS);
+ else
+ printLog("PFS Final padding check", EMsgType.WARNING);
+
+ // Calculate position including header for body size offset
+ bodySize = randAccessFile.getFilePointer()+header;
+ //*********************************************************************************************
+ // Collect file names from NCAs
+ printLog("PFS Collecting file names", EMsgType.INFO);
+ List ncaFN; // Temporary
+ byte[] b = new byte[1]; // Temporary
+ for (int i=0; i();
+ randAccessFile.seek(filesCount*24+16+ncaNameOffsets.get(i)); // Files cont * 24(bit for each meta-data) + 4 bytes goes after all of them + 12 bit what were in the beginning
+ while ((randAccessFile.read(b)) != -1){
+ if (b[0] == 0x00)
+ break;
+ else
+ ncaFN.add(b[0]);
+ }
+ byte[] exchangeTempArray = new byte[ncaFN.size()];
+ for (int j=0; j < ncaFN.size(); j++)
+ exchangeTempArray[j] = ncaFN.get(j);
+ // Find and store ticket (.tik)
+ if (new String(exchangeTempArray, StandardCharsets.UTF_8).toLowerCase().endsWith(".tik"))
+ this.ticketID = i;
+ this.ncaFiles[i].setNcaFileName(Arrays.copyOf(exchangeTempArray, exchangeTempArray.length));
+ }
+ randAccessFile.close();
+ }
+ catch (IOException ioe){
+ printLog("PFS Failed NSP file analyze for ["+nspFileName+"]\n "+ioe.getMessage(), EMsgType.FAIL);
+ ioe.printStackTrace();
+ }
+ printLog("PFS Finish NSP file analyze for ["+nspFileName+"]", EMsgType.PASS);
+
+ return true;
+ }
+ /**
+ * Return file name as byte array
+ * */
+ public byte[] getBytesNspFileName(){
+ return nspFileName.getBytes(StandardCharsets.UTF_8);
+ }
+ /**
+ * Return file name as String
+ * */
+ public String getStringNspFileName(){
+ return nspFileName;
+ }
+ /**
+ * Return file name length as byte array
+ * */
+ public byte[] getBytesNspFileNameLength(){
+ return ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(getBytesNspFileName().length).array();
+ }
+ /**
+ * Return NCA count inside of file as byte array
+ * */
+ public byte[] getBytesCountOfNca(){
+ return ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(ncaFiles.length).array();
+ }
+ /**
+ * Return NCA count inside of file as int
+ * */
+ public int getIntCountOfNca(){
+ return ncaFiles.length;
+ }
+ /**
+ * Return requested-by-number NCA file inside of file
+ * */
+ public NCAFile getNca(int ncaNumber){
+ return ncaFiles[ncaNumber];
+ }
+ /**
+ * Return bodySize
+ * */
+ public long getBodySize(){
+ return bodySize;
+ }
+ /**
+ * Return special NCA file: ticket
+ * (sugar)
+ * */
+ public int getNcaTicketID(){
+ return ticketID;
+ }
+ /**
+ * This is what will print to textArea of the application.
+ **/
+ private void printLog(String message, EMsgType type){
+ try {
+ switch (type){
+ case PASS:
+ msgQueue.put("[ PASS ] "+message+"\n");
+ break;
+ case FAIL:
+ msgQueue.put("[ FAIL ] "+message+"\n");
+ break;
+ case INFO:
+ msgQueue.put("[ INFO ] "+message+"\n");
+ break;
+ case WARNING:
+ msgQueue.put("[ WARN ] "+message+"\n");
+ break;
+ }
+ }catch (InterruptedException ie){
+ ie.printStackTrace(); //TODO: ???
+ }
+ }
+}
diff --git a/src/main/java/nsusbloader/RainbowHexDump.java b/src/main/java/nsusbloader/RainbowHexDump.java
new file mode 100644
index 0000000..ee428b2
--- /dev/null
+++ b/src/main/java/nsusbloader/RainbowHexDump.java
@@ -0,0 +1,31 @@
+package nsusbloader;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Debug tool like hexdump <3
+ */
+public class RainbowHexDump {
+ private static final String ANSI_RESET = "\u001B[0m";
+ private static final String ANSI_BLACK = "\u001B[30m";
+ private static final String ANSI_RED = "\u001B[31m";
+ private static final String ANSI_GREEN = "\u001B[32m";
+ private static final String ANSI_YELLOW = "\u001B[33m";
+ private static final String ANSI_BLUE = "\u001B[34m";
+ private static final String ANSI_PURPLE = "\u001B[35m";
+ private static final String ANSI_CYAN = "\u001B[36m";
+ private static final String ANSI_WHITE = "\u001B[37m";
+
+
+ public static void hexDumpUTF8(byte[] byteArray){
+ System.out.print(ANSI_BLUE);
+ for (int i=0; i < byteArray.length; i++)
+ System.out.print(String.format("%02d-", i%100));
+ System.out.println(">"+ANSI_RED+byteArray.length+ANSI_RESET);
+ for (byte b: byteArray)
+ System.out.print(String.format("%02x ", b));
+ System.out.print("\t\t\t"
+ + new String(byteArray, StandardCharsets.UTF_8)
+ + "\n");
+ }
+}
diff --git a/src/main/java/nsusbloader/ServiceWindow.java b/src/main/java/nsusbloader/ServiceWindow.java
index c362148..2982201 100644
--- a/src/main/java/nsusbloader/ServiceWindow.java
+++ b/src/main/java/nsusbloader/ServiceWindow.java
@@ -11,7 +11,7 @@ public class ServiceWindow {
* Create window with notification
* */
/* // not used
- static void getErrorNotification(String title, String body){
+ public static void getErrorNotification(String title, String body){
Alert alertBox = new Alert(Alert.AlertType.ERROR);
alertBox.setTitle(title);
alertBox.setHeaderText(null);
@@ -27,7 +27,7 @@ public class ServiceWindow {
/**
* Create notification window with confirm/deny
* */
- static boolean getConfirmationWindow(String title, String body){
+ public static boolean getConfirmationWindow(String title, String body){
Alert alertBox = new Alert(Alert.AlertType.CONFIRMATION);
alertBox.setTitle(title);
alertBox.setHeaderText(null);
@@ -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 1992287..d0df921 100644
--- a/src/main/java/nsusbloader/UsbCommunications.java
+++ b/src/main/java/nsusbloader/UsbCommunications.java
@@ -1,8 +1,9 @@
package nsusbloader;
import javafx.concurrent.Task;
-import javafx.scene.control.ProgressBar;
-import javafx.scene.control.TextArea;
+import nsusbloader.NSLDataTypes.EFileStatus;
+import nsusbloader.NSLDataTypes.EMsgType;
+import nsusbloader.PFS.PFSProvider;
import org.usb4java.*;
import java.io.*;
@@ -17,18 +18,22 @@ 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 enum MsgType {PASS, FAIL, INFO, WARNING}
+ 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;
private HashMap nspMap;
private Context contextNS;
private DeviceHandle handlerNS;
+
+ private String protocol;
/*
Ok, here is a story. We will pass to NS only file names, not full path. => see nspMap where 'key' is a file name.
File name itself should not be greater then 512 bytes, but in real world it's limited by OS to something like 256 bytes.
@@ -40,13 +45,15 @@ 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(TextArea logArea, ProgressBar progressBar, List nspList){
+ public UsbCommunications(List nspList, String protocol){
+ this.protocol = protocol;
this.nspMap = new HashMap<>();
for (File f: nspList)
nspMap.put(f.getName(), f);
this.msgQueue = new LinkedBlockingQueue<>();
this.progressQueue = new LinkedBlockingQueue<>();
- this.msgConsumer = new MessagesConsumer(this.msgQueue, logArea, this.progressQueue, progressBar);
+ this.statusMap = new HashMap<>();
+ this.msgConsumer = new MessagesConsumer(this.msgQueue, this.progressQueue, this.statusMap);
}
@Override
@@ -54,28 +61,28 @@ class UsbCommunications extends Task {
this.msgConsumer.start();
int result = -9999;
- printLog("\tStart chain", MsgType.INFO);
+ printLog("\tStart chain", EMsgType.INFO);
// Creating Context required by libusb. Optional. TODO: Consider removing.
contextNS = new Context();
result = LibUsb.init(contextNS);
if (result != LibUsb.SUCCESS) {
- printLog("libusb initialization\n Returned: "+result, MsgType.FAIL);
+ printLog("libusb initialization\n Returned: "+result, EMsgType.FAIL);
close();
return null;
}
else
- printLog("libusb initialization", MsgType.PASS);
+ printLog("libusb initialization", EMsgType.PASS);
// Searching for NS in devices: obtain list of all devices
DeviceList deviceList = new DeviceList();
result = LibUsb.getDeviceList(contextNS, deviceList);
if (result < 0) {
- printLog("Get device list\n Returned: "+result, MsgType.FAIL);
+ printLog("Get device list\n Returned: "+result, EMsgType.FAIL);
close();
return null;
}
else {
- printLog("Get device list", MsgType.PASS);
+ printLog("Get device list", EMsgType.PASS);
}
// Searching for NS in devices: looking for NS
DeviceDescriptor descriptor;
@@ -84,14 +91,14 @@ class UsbCommunications extends Task {
descriptor = new DeviceDescriptor(); // mmm.. leave it as is.
result = LibUsb.getDeviceDescriptor(device, descriptor);
if (result != LibUsb.SUCCESS){
- printLog("Read file descriptors for USB devices\n Returned: "+result, MsgType.FAIL);
+ printLog("Read file descriptors for USB devices\n Returned: "+result, EMsgType.FAIL);
LibUsb.freeDeviceList(deviceList, true);
close();
return null;
}
if ((descriptor.idVendor() == 0x057E) && descriptor.idProduct() == 0x3000){
deviceNS = device;
- printLog("Read file descriptors for USB devices", MsgType.PASS);
+ printLog("Read file descriptors for USB devices", EMsgType.PASS);
break;
}
}
@@ -134,10 +141,10 @@ class UsbCommunications extends Task {
////////////////////////////////////////// DEBUG INFORMATION END /////////////////////////////////////////////
if (deviceNS != null){
- printLog("NS in connected USB devices found", MsgType.PASS);
+ printLog("NS in connected USB devices found", EMsgType.PASS);
}
else {
- printLog("NS in connected USB devices not found\n Returned: "+result, MsgType.FAIL);
+ printLog("NS in connected USB devices not found", EMsgType.FAIL);
close();
return null;
}
@@ -147,25 +154,25 @@ class UsbCommunications extends Task {
if (result != LibUsb.SUCCESS) {
switch (result){
case LibUsb.ERROR_ACCESS:
- printLog("Open NS USB device\n Returned: ERROR_ACCESS", MsgType.FAIL);
- printLog("Double check that you have administrator privileges (you're 'root') or check 'udev' rules set for this user (linux only)!",MsgType.INFO);
+ printLog("Open NS USB device\n Returned: ERROR_ACCESS", EMsgType.FAIL);
+ printLog("Double check that you have administrator privileges (you're 'root') or check 'udev' rules set for this user (linux only)!", EMsgType.INFO);
break;
case LibUsb.ERROR_NO_MEM:
- printLog("Open NS USB device\n Returned: ERROR_NO_MEM", MsgType.FAIL);
+ printLog("Open NS USB device\n Returned: ERROR_NO_MEM", EMsgType.FAIL);
break;
case LibUsb.ERROR_NO_DEVICE:
- printLog("Open NS USB device\n Returned: ERROR_NO_DEVICE", MsgType.FAIL);
+ printLog("Open NS USB device\n Returned: ERROR_NO_DEVICE", EMsgType.FAIL);
break;
default:
- printLog("Open NS USB device\n Returned:" + result, MsgType.FAIL);
+ printLog("Open NS USB device\n Returned:" + result, EMsgType.FAIL);
}
close();
return null;
}
else
- printLog("Open NS USB device", MsgType.PASS);
+ printLog("Open NS USB device", EMsgType.PASS);
- printLog("Free device list", MsgType.INFO);
+ printLog("Free device list", EMsgType.INFO);
LibUsb.freeDeviceList(deviceList, true);
// DO some stuff to connected NS
@@ -174,140 +181,566 @@ class UsbCommunications extends Task {
if (canDetach){
int usedByKernel = LibUsb.kernelDriverActive(handlerNS, DEFAULT_INTERFACE);
if (usedByKernel == LibUsb.SUCCESS){
- printLog("Can proceed with libusb driver", MsgType.PASS); // we're good
+ printLog("Can proceed with libusb driver", EMsgType.PASS); // we're good
}
else {
switch (usedByKernel){
case 1: // used by kernel
result = LibUsb.detachKernelDriver(handlerNS, DEFAULT_INTERFACE);
- printLog("Detach kernel required", MsgType.INFO);
+ printLog("Detach kernel required", EMsgType.INFO);
if (result != 0) {
switch (result){
case LibUsb.ERROR_NOT_FOUND:
- printLog("Detach kernel\n Returned: ERROR_NOT_FOUND", MsgType.FAIL);
+ printLog("Detach kernel\n Returned: ERROR_NOT_FOUND", EMsgType.FAIL);
break;
case LibUsb.ERROR_INVALID_PARAM:
- printLog("Detach kernel\n Returned: ERROR_INVALID_PARAM", MsgType.FAIL);
+ printLog("Detach kernel\n Returned: ERROR_INVALID_PARAM", EMsgType.FAIL);
break;
case LibUsb.ERROR_NO_DEVICE:
- printLog("Detach kernel\n Returned: ERROR_NO_DEVICE", MsgType.FAIL);
+ printLog("Detach kernel\n Returned: ERROR_NO_DEVICE", EMsgType.FAIL);
break;
case LibUsb.ERROR_NOT_SUPPORTED: // Should never appear only if libusb buggy
- printLog("Detach kernel\n Returned: ERROR_NOT_SUPPORTED", MsgType.FAIL);
+ printLog("Detach kernel\n Returned: ERROR_NOT_SUPPORTED", EMsgType.FAIL);
break;
default:
- printLog("Detach kernel\n Returned: " + result, MsgType.FAIL);
+ printLog("Detach kernel\n Returned: " + result, EMsgType.FAIL);
break;
}
close();
return null;
}
else {
- printLog("Detach kernel", MsgType.PASS);
+ printLog("Detach kernel", EMsgType.PASS);
break;
}
case LibUsb.ERROR_NO_DEVICE:
- printLog("Can't proceed with libusb driver\n Returned: ERROR_NO_DEVICE", MsgType.FAIL);
+ printLog("Can't proceed with libusb driver\n Returned: ERROR_NO_DEVICE", EMsgType.FAIL);
break;
case LibUsb.ERROR_NOT_SUPPORTED:
- printLog("Can't proceed with libusb driver\n Returned: ERROR_NOT_SUPPORTED", MsgType.FAIL);
+ printLog("Can't proceed with libusb driver\n Returned: ERROR_NOT_SUPPORTED", EMsgType.FAIL);
break;
default:
- printLog("Can't proceed with libusb driver\n Returned: "+result, MsgType.FAIL);
+ printLog("Can't proceed with libusb driver\n Returned: "+result, EMsgType.FAIL);
}
}
}
else
- printLog("libusb doesn't supports function 'CAP_SUPPORTS_DETACH_KERNEL_DRIVER'. Proceeding.", MsgType.WARNING);
+ printLog("libusb doesn't supports function 'CAP_SUPPORTS_DETACH_KERNEL_DRIVER'. Proceeding.", EMsgType.WARNING);
// Set configuration (soft reset if needed)
result = LibUsb.setConfiguration(handlerNS, 1); // 1 - configuration all we need
if (result != LibUsb.SUCCESS){
switch (result){
case LibUsb.ERROR_NOT_FOUND:
- printLog("Set active configuration to device\n Returned: ERROR_NOT_FOUND", MsgType.FAIL);
+ printLog("Set active configuration to device\n Returned: ERROR_NOT_FOUND", EMsgType.FAIL);
break;
case LibUsb.ERROR_BUSY:
- printLog("Set active configuration to device\n Returned: ERROR_BUSY", MsgType.FAIL);
+ printLog("Set active configuration to device\n Returned: ERROR_BUSY", EMsgType.FAIL);
break;
case LibUsb.ERROR_NO_DEVICE:
- printLog("Set active configuration to device\n Returned: ERROR_NO_DEVICE", MsgType.FAIL);
+ printLog("Set active configuration to device\n Returned: ERROR_NO_DEVICE", EMsgType.FAIL);
break;
case LibUsb.ERROR_INVALID_PARAM:
- printLog("Set active configuration to device\n Returned: ERROR_INVALID_PARAM", MsgType.FAIL);
+ printLog("Set active configuration to device\n Returned: ERROR_INVALID_PARAM", EMsgType.FAIL);
break;
default:
- printLog("Set active configuration to device\n Returned: "+result, MsgType.FAIL);
+ printLog("Set active configuration to device\n Returned: "+result, EMsgType.FAIL);
break;
}
close();
return null;
}
else {
- printLog("Set active configuration to device.", MsgType.PASS);
+ printLog("Set active configuration to device.", EMsgType.PASS);
}
// Claim interface
result = LibUsb.claimInterface(handlerNS, DEFAULT_INTERFACE);
if (result != LibUsb.SUCCESS) {
- printLog("Claim interface\n Returned: "+result, MsgType.FAIL);
+ printLog("Claim interface\n Returned: "+result, EMsgType.FAIL);
close();
return null;
}
else
- printLog("Claim interface", MsgType.PASS);
+ printLog("Claim interface", EMsgType.PASS);
-
-
- // Send list of NSP files:
- // Proceed "TUL0"
- if (!writeToUsb("TUL0".getBytes(StandardCharsets.US_ASCII))) { // new byte[]{(byte) 0x54, (byte) 0x55, (byte) 0x76, (byte) 0x30}
- printLog("Send list of files: handshake", MsgType.FAIL);
- close();
- return null;
+ //--------------------------------------------------------------------------------------------------------------
+ if (protocol.equals("TinFoil")) {
+ new TinFoil();
+ } else {
+ new GoldLeaf();
}
- else
- printLog("Send list of files: handshake", MsgType.PASS);
- //Collect file names
- StringBuilder nspListNamesBuilder = new StringBuilder(); // Add every title to one stringBuilder
- for(String nspFileName: nspMap.keySet())
- nspListNamesBuilder.append(nspFileName+'\n'); // And here we come with java string default encoding (UTF-16)
-
- byte[] nspListNames = nspListNamesBuilder.toString().getBytes(StandardCharsets.UTF_8);
- ByteBuffer byteBuffer = ByteBuffer.allocate(Integer.BYTES).order(ByteOrder.LITTLE_ENDIAN); // integer = 4 bytes; BTW Java is stored in big-endian format
- byteBuffer.putInt(nspListNames.length); // This way we obtain length in int converted to byte array in correct Big-endian order. Trust me.
- byte[] nspListSize = byteBuffer.array(); // TODO: rewind? not sure..
- //byteBuffer.reset();
-
- // Sending NSP list
- if (!writeToUsb(nspListSize)) { // size of the list we're going to transfer goes...
- printLog("Send list of files: send length.", MsgType.FAIL);
- close();
- return null;
- } else
- printLog("Send list of files: send length.", MsgType.PASS);
- if (!writeToUsb(new byte[8])) { // 8 zero bytes goes...
- printLog("Send list of files: send padding.", MsgType.FAIL);
- close();
- return null;
- }
- else
- printLog("Send list of files: send padding.", MsgType.PASS);
- if (!writeToUsb(nspListNames)) { // list of the names goes...
- printLog("Send list of files: send list itself.", MsgType.FAIL);
- close();
- return null;
- }
- else
- printLog("Send list of files: send list itself.", MsgType.PASS);
-
- proceedCommands();
close();
- printLog("\tEnd chain", MsgType.INFO);
+ printLog("\tEnd chain", EMsgType.INFO);
return null;
}
+ /**
+ * Tinfoil processing
+ * */
+ private class TinFoil{
+ TinFoil(){
+
+ if (!sendListOfNSP())
+ return;
+
+ if (proceedCommands()) // REPORT SUCCESS
+ status = EFileStatus.UPLOADED; // Don't change status that is already set to FAILED
+ }
+ /**
+ * Send what NSP will be transferred
+ * */
+ private boolean sendListOfNSP(){
+ // Send list of NSP files:
+ // Proceed "TUL0"
+ if (!writeToUsb("TUL0".getBytes(StandardCharsets.US_ASCII))) { // new byte[]{(byte) 0x54, (byte) 0x55, (byte) 0x76, (byte) 0x30}
+ printLog("TF Send list of files: handshake", EMsgType.FAIL);
+ return false;
+ }
+ else
+ printLog("TF Send list of files: handshake", EMsgType.PASS);
+ //Collect file names
+ StringBuilder nspListNamesBuilder = new StringBuilder(); // Add every title to one stringBuilder
+ for(String nspFileName: nspMap.keySet()) {
+ nspListNamesBuilder.append(nspFileName); // And here we come with java string default encoding (UTF-16)
+ nspListNamesBuilder.append('\n');
+ }
+
+ byte[] nspListNames = nspListNamesBuilder.toString().getBytes(StandardCharsets.UTF_8);
+ ByteBuffer byteBuffer = ByteBuffer.allocate(Integer.BYTES).order(ByteOrder.LITTLE_ENDIAN); // integer = 4 bytes; BTW Java is stored in big-endian format
+ byteBuffer.putInt(nspListNames.length); // This way we obtain length in int converted to byte array in correct Big-endian order. Trust me.
+ byte[] nspListSize = byteBuffer.array(); // TODO: rewind? not sure..
+ //byteBuffer.reset();
+
+ // Sending NSP list
+ printLog("TF Send list of files", EMsgType.INFO);
+ if (!writeToUsb(nspListSize)) { // size of the list we're going to transfer goes...
+ printLog(" [send list length]", EMsgType.FAIL);
+ return false;
+ }
+ printLog(" [send list length]", EMsgType.PASS);
+
+ if (!writeToUsb(new byte[8])) { // 8 zero bytes goes...
+ printLog(" [send padding]", EMsgType.FAIL);
+ return false;
+ }
+ printLog(" [send padding]", EMsgType.PASS);
+
+ if (!writeToUsb(nspListNames)) { // list of the names goes...
+ printLog(" [send list itself]", EMsgType.FAIL);
+ return false;
+ }
+ printLog(" [send list itself]", EMsgType.PASS);
+
+ return true;
+ }
+ /**
+ * After we sent commands to NS, this chain starts
+ * */
+ private boolean proceedCommands(){
+ printLog("TF Awaiting for NS commands.", EMsgType.INFO);
+
+ /* byte[] magic = new byte[4];
+ ByteBuffer bb = StandardCharsets.UTF_8.encode("TUC0").rewind().get(magic);
+ // Let's rephrase this 'string' */
+ final byte[] magic = new byte[]{(byte) 0x54, (byte) 0x55, (byte) 0x43, (byte) 0x30}; // eq. 'TUC0' @ UTF-8 (actually ASCII lol, u know what I mean)
+
+ byte[] receivedArray;
+
+ while (true){
+ if (Thread.currentThread().isInterrupted()) // Check if user interrupted process.
+ return false;
+ receivedArray = readFromUsb();
+ if (receivedArray == null)
+ return false; // catches exception
+
+ if (!Arrays.equals(Arrays.copyOfRange(receivedArray, 0,4), magic)) // Bytes from 0 to 3 should contain 'magic' TUC0, so must be verified like this
+ continue;
+
+ // 8th to 12th(explicits) bytes in returned data stands for command ID as unsigned integer (Little-endian). Actually, we have to compare arrays here, but in real world it can't be greater then 0/1/2, thus:
+ // BTW also protocol specifies 4th byte to be 0x00 kinda indicating that that this command is valid. But, as you may see, never happens other situation when it's not = 0.
+ if (receivedArray[8] == 0x00){ //0x00 - exit
+ printLog("TF Received EXIT command. Terminating.", EMsgType.PASS);
+ return true; // All interaction with USB device should be ended (expected);
+ }
+ else if ((receivedArray[8] == 0x01) || (receivedArray[8] == 0x02)){ //0x01 - file range; 0x02 unknown bug on backend side (dirty hack).
+ printLog("TF Received FILE_RANGE command. Proceeding: [0x0"+receivedArray[8]+"]", EMsgType.PASS);
+ /*// We can get in this pocket a length of file name (+32). Why +32? I dunno man.. Do we need this? Definitely not. This app can live without it.
+ long receivedSize = ByteBuffer.wrap(Arrays.copyOfRange(receivedArray, 12,20)).order(ByteOrder.LITTLE_ENDIAN).getLong();
+ logsArea.appendText("[V] Received FILE_RANGE command. Size: "+Long.toUnsignedString(receivedSize)+"\n"); // this shit returns string that will be chosen next '+32'. And, BTW, can't be greater then 512
+ */
+ if (!fileRangeCmd()) {
+ return false; // catches exception
+ }
+ }
+ }
+ }
+ /**
+ * This is what returns requested file (files)
+ * Executes multiple times
+ * @return 'true' if everything is ok
+ * 'false' is error/exception occurs
+ * */
+ private boolean fileRangeCmd(){
+ boolean isProgessBarInitiated = false;
+
+ byte[] receivedArray;
+ // Here we take information of what other side wants
+ receivedArray = readFromUsb();
+ if (receivedArray == null)
+ return false;
+
+ // range_offset of the requested file. In the begining it will be 0x10.
+ long receivedRangeSize = ByteBuffer.wrap(Arrays.copyOfRange(receivedArray, 0,8)).order(ByteOrder.LITTLE_ENDIAN).getLong(); // Note - it could be unsigned long. Unfortunately, this app won't support files greater then 8796093022208 Gb
+ byte[] receivedRangeSizeRAW = Arrays.copyOfRange(receivedArray, 0,8); // used (only) when we use sendResponse(). It's just simply.
+ long receivedRangeOffset = ByteBuffer.wrap(Arrays.copyOfRange(receivedArray, 8,16)).order(ByteOrder.LITTLE_ENDIAN).getLong(); // Note - it could be unsigned long. Unfortunately, this app won't support files greater then 8796093022208 Gb
+ /* Below, it's REAL NSP file name length that we sent before among others (WITHOUT +32 byes). It can't be greater then... see what is written in the beginning of this code.
+ We don't need this since in next pocket we'll get name itself UTF-8 encoded. Could be used to double-checks or something like that.
+ long receivedNspNameLen = ByteBuffer.wrap(Arrays.copyOfRange(receivedArray, 16,24)).order(ByteOrder.LITTLE_ENDIAN).getLong(); */
+
+ // Requesting UTF-8 file name required:
+ receivedArray = readFromUsb();
+ if (receivedArray == null)
+ return false;
+
+ String receivedRequestedNSP = new String(receivedArray, StandardCharsets.UTF_8);
+ printLog("TF Reply to requested file: "+receivedRequestedNSP
+ +"\n Range Size: "+receivedRangeSize
+ +"\n Range Offset: "+receivedRangeOffset, EMsgType.INFO);
+
+ // Sending response header
+ if (!sendResponse(receivedRangeSizeRAW)) // Get receivedRangeSize in 'RAW' format exactly as it has been received. It's simply.
+ return false;
+
+ try {
+
+ BufferedInputStream bufferedInStream = new BufferedInputStream(new FileInputStream(nspMap.get(receivedRequestedNSP))); // TODO: refactor?
+ byte[] bufferCurrent ;//= new byte[1048576]; // eq. Allocate 1mb
+ int bufferLength;
+ if (bufferedInStream.skip(receivedRangeOffset) != receivedRangeOffset){
+ printLog("TF Requested skip is out of file size. Nothing to transmit.", EMsgType.FAIL);
+ return false;
+ }
+
+ long currentOffset = 0;
+ // 'End Offset' equal to receivedRangeSize.
+ int readPice = 8388608; // = 8Mb
+
+ while (currentOffset < receivedRangeSize){
+ if (Thread.currentThread().isInterrupted()) // Check if user interrupted process.
+ return true;
+ if ((currentOffset + readPice) >= receivedRangeSize )
+ readPice = Math.toIntExact(receivedRangeSize - currentOffset);
+ //System.out.println("CO: "+currentOffset+"\t\tEO: "+receivedRangeSize+"\t\tRP: "+readPice); // TODO: NOTE: DEBUG
+ // updating progress bar (if a lot of data requested) START BLOCK
+ if (isProgessBarInitiated){
+ try {
+ if (currentOffset+readPice == receivedRangeOffset){
+ progressQueue.put(1.0);
+ isProgessBarInitiated = false;
+ }
+ else
+ progressQueue.put((currentOffset+readPice)/(receivedRangeSize/100.0) / 100.0);
+ }catch (InterruptedException ie){
+ getException().printStackTrace(); // TODO: Do something with this
+ }
+ }
+ else {
+ if ((readPice == 8388608) && (currentOffset == 0))
+ isProgessBarInitiated = true;
+ }
+ // updating progress bar if needed END BLOCK
+
+ bufferCurrent = new byte[readPice]; // TODO: not perfect moment, consider refactoring.
+
+ bufferLength = bufferedInStream.read(bufferCurrent);
+
+ if (bufferLength != -1){
+ //write to USB
+ if (!writeToUsb(bufferCurrent)) {
+ printLog("TF Failure during NSP transmission.", EMsgType.FAIL);
+ return false;
+ }
+ currentOffset += readPice;
+ }
+ else {
+ printLog("TF Reading of stream suddenly ended.", EMsgType.WARNING);
+ return false;
+ }
+
+ }
+ bufferedInStream.close();
+ } catch (FileNotFoundException fnfe){
+ printLog("TF FileNotFoundException:\n "+fnfe.getMessage(), EMsgType.FAIL);
+ fnfe.printStackTrace();
+ return false;
+ } catch (IOException ioe){
+ printLog("TF IOException:\n "+ioe.getMessage(), EMsgType.FAIL);
+ ioe.printStackTrace();
+ return false;
+ } catch (ArithmeticException ae){
+ printLog("TF ArithmeticException (can't cast end offset minus current to 'integer'):\n "+ae.getMessage(), EMsgType.FAIL);
+ ae.printStackTrace();
+ return false;
+ }
+
+ return true;
+ }
+ /**
+ * Send response header.
+ * @return true if everything OK
+ * false if failed
+ * */
+ private boolean sendResponse(byte[] rangeSize){ // This method as separate function itself for application needed as a cookie in the middle of desert.
+ printLog("TF Sending response", EMsgType.INFO);
+ if (!writeToUsb(new byte[] { (byte) 0x54, (byte) 0x55, (byte) 0x43, (byte) 0x30, // 'TUC0'
+ (byte) 0x01, // CMD_TYPE_RESPONSE = 1
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, // kinda padding. Guys, didn't you want to use integer value for CMD semantic?
+ (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00} ) // Send integer value of '1' in Little-endian format.
+ ){
+ printLog(" [1/3]", EMsgType.FAIL);
+ return false;
+ }
+ printLog(" [1/3]", EMsgType.PASS);
+ if(!writeToUsb(rangeSize)) { // Send EXACTLY what has been received
+ printLog(" [2/3]", EMsgType.FAIL);
+ return false;
+ }
+ printLog(" [2/3]", EMsgType.PASS);
+ if(!writeToUsb(new byte[12])) { // kinda another one padding
+ printLog(" [3/3]", EMsgType.FAIL);
+ return false;
+ }
+ printLog(" [3/3]", EMsgType.PASS);
+ return true;
+ }
+
+ }
+ /**
+ * GoldLeaf processing
+ * */
+ private class GoldLeaf{
+ // CMD G L U C ID 0 0 0
+ private final byte[] CMD_ConnectionRequest = new byte[]{0x47, 0x4c, 0x55, 0x43, 0x00, 0x00, 0x00, 0x00}; // Write-only command
+ private final byte[] CMD_NSPName = new byte[]{0x47, 0x4c, 0x55, 0x43, 0x02, 0x00, 0x00, 0x00}; // Write-only command
+ private final byte[] CMD_NSPData = new byte[]{0x47, 0x4c, 0x55, 0x43, 0x04, 0x00, 0x00, 0x00}; // Write-only command
+
+ private final byte[] CMD_ConnectionResponse = new byte[]{0x47, 0x4c, 0x55, 0x43, 0x01, 0x00, 0x00, 0x00};
+ private final byte[] CMD_Start = new byte[]{0x47, 0x4c, 0x55, 0x43, 0x03, 0x00, 0x00, 0x00};
+ private final byte[] CMD_NSPContent = new byte[]{0x47, 0x4c, 0x55, 0x43, 0x05, 0x00, 0x00, 0x00};
+ private final byte[] CMD_NSPTicket = new byte[]{0x47, 0x4c, 0x55, 0x43, 0x06, 0x00, 0x00, 0x00};
+ private final byte[] CMD_Finish = new byte[]{0x47, 0x4c, 0x55, 0x43, 0x07, 0x00, 0x00, 0x00};
+
+ GoldLeaf(){
+ printLog("===========================================================================", EMsgType.INFO);
+ 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);
+ status = EFileStatus.INCORRECT_FILE_FAILED;
+ return;
+ }
+ printLog("GL File structure validated and it will be uploaded", EMsgType.PASS);
+
+ if (initGoldLeafProtocol(pfsElement))
+ status = EFileStatus.UPLOADED;
+ // else - no change status that is already set to FAILED
+ }
+ private boolean initGoldLeafProtocol(PFSProvider pfsElement){
+ // Go parse commands
+ byte[] readByte;
+
+ // Go connect to GoldLeaf
+ if (writeToUsb(CMD_ConnectionRequest))
+ printLog("GL Initiating GoldLeaf connection", EMsgType.PASS);
+ else {
+ printLog("GL Initiating GoldLeaf connection", EMsgType.FAIL);
+ return false;
+ }
+ while (true) {
+ readByte = readFromUsb();
+ if (readByte == null)
+ return false;
+ if (Arrays.equals(readByte, CMD_ConnectionResponse)) {
+ if (!handleConnectionResponse(pfsElement))
+ return false;
+ else
+ continue;
+ }
+ if (Arrays.equals(readByte, CMD_Start)) {
+ if (!handleStart(pfsElement))
+ return false;
+ else
+ continue;
+ }
+ if (Arrays.equals(readByte, CMD_NSPContent)) {
+ if (!handleNSPContent(pfsElement, true))
+ return false;
+ else
+ continue;
+ }
+ if (Arrays.equals(readByte, CMD_NSPTicket)) {
+ if (!handleNSPContent(pfsElement, false))
+ return false;
+ else
+ continue;
+ }
+ if (Arrays.equals(readByte, CMD_Finish)) {
+ printLog("GL Closing GoldLeaf connection: Transfer successful.", EMsgType.PASS);
+ break;
+ }
+ }
+ return true;
+ }
+ /**
+ * ConnectionResponse command handler
+ * */
+ private boolean handleConnectionResponse(PFSProvider pfsElement){
+ printLog("GL 'ConnectionResonse' command:", EMsgType.INFO);
+ if (!writeToUsb(CMD_NSPName)) {
+ printLog(" [1/3]", EMsgType.FAIL);
+ return false;
+ }
+ printLog(" [1/3]", EMsgType.PASS);
+
+ if (!writeToUsb(pfsElement.getBytesNspFileNameLength())) {
+ printLog(" [2/3]", EMsgType.FAIL);
+ return false;
+ }
+ printLog(" [2/3]", EMsgType.PASS);
+
+ if (!writeToUsb(pfsElement.getBytesNspFileName())) {
+ printLog(" [3/3]", EMsgType.FAIL);
+ return false;
+ }
+ printLog(" [3/3]", EMsgType.PASS);
+
+ return true;
+ }
+ /**
+ * Start command handler
+ * */
+ private boolean handleStart(PFSProvider pfsElement){
+ printLog("GL Handle 'Start' command:", EMsgType.INFO);
+ if (!writeToUsb(CMD_NSPData)) {
+ printLog(" [Send command]", EMsgType.FAIL);
+ return false;
+ }
+ printLog(" [Send command]", EMsgType.PASS);
+
+ if (!writeToUsb(pfsElement.getBytesCountOfNca())) {
+ printLog(" [Send length]", EMsgType.FAIL);
+ return false;
+ }
+ printLog(" [Send length]", EMsgType.PASS);
+
+ int ncaCount = pfsElement.getIntCountOfNca();
+ printLog(" [Send information for "+ncaCount+" files]", EMsgType.INFO);
+ for (int i = 0; i < ncaCount; i++){
+ if (!writeToUsb(pfsElement.getNca(i).getNcaFileNameLength())) {
+ printLog(" [1/4] File #"+i, EMsgType.FAIL);
+ return false;
+ }
+ printLog(" [1/4] File #"+i, EMsgType.PASS);
+
+ if (!writeToUsb(pfsElement.getNca(i).getNcaFileName())) {
+ printLog(" [2/4] File #"+i, EMsgType.FAIL);
+ return false;
+ }
+ printLog(" [2/4] File #"+i, EMsgType.PASS);
+ if (!writeToUsb(ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(pfsElement.getBodySize()+pfsElement.getNca(i).getNcaOffset()).array())) { // offset. real.
+ printLog(" [2/4] File #"+i, EMsgType.FAIL);
+ return false;
+ }
+ printLog(" [3/4] File #"+i, EMsgType.PASS);
+ if (!writeToUsb(ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(pfsElement.getNca(i).getNcaSize()).array())) { // size
+ printLog(" [4/4] File #"+i, EMsgType.FAIL);
+ return false;
+ }
+ printLog(" [4/4] File #"+i, EMsgType.PASS);
+ }
+ return true;
+ }
+ /**
+ * NSPContent command handler
+ * isItRawRequest - if True, just ask NS what's needed
+ * - if False, send ticket
+ * */
+ private boolean handleNSPContent(PFSProvider pfsElement, boolean isItRawRequest){
+ int requestedNcaID;
+ boolean isProgessBarInitiated = false;
+ if (isItRawRequest) {
+ printLog("GL Handle 'Content' command", EMsgType.INFO);
+ byte[] readByte = readFromUsb();
+ if (readByte == null || readByte.length != 4) {
+ printLog(" [Read requested ID]", EMsgType.FAIL);
+ return false;
+ }
+ requestedNcaID = ByteBuffer.wrap(readByte).order(ByteOrder.LITTLE_ENDIAN).getInt();
+ printLog(" [Read requested ID = "+requestedNcaID+" ]", EMsgType.PASS);
+ }
+ else {
+ requestedNcaID = pfsElement.getNcaTicketID();
+ printLog("GL Handle 'Ticket' command (ID = "+requestedNcaID+" )", EMsgType.INFO);
+ }
+
+ long realNcaOffset = pfsElement.getNca(requestedNcaID).getNcaOffset()+pfsElement.getBodySize();
+ long realNcaSize = pfsElement.getNca(requestedNcaID).getNcaSize();
+
+ long readFrom = 0;
+
+ int readPice = 8388608; // 8mb NOTE: consider switching to 1mb 1048576
+ byte[] readBuf;
+ File nspFile = nspMap.get(pfsElement.getStringNspFileName()); // wuuuut ( >< )
+ try{
+ BufferedInputStream bufferedInStream = new BufferedInputStream(new FileInputStream(nspFile)); // TODO: refactor?
+ if (bufferedInStream.skip(realNcaOffset) != realNcaOffset)
+ return false;
+
+ while (readFrom < realNcaSize){
+
+ if (Thread.currentThread().isInterrupted()) // Check if user interrupted process.
+ return false;
+
+ if (realNcaSize - readFrom < readPice)
+ readPice = Math.toIntExact(realNcaSize - readFrom); // it's safe, I guarantee
+ readBuf = new byte[readPice];
+ if (bufferedInStream.read(readBuf) != readPice)
+ return false;
+
+ if (!writeToUsb(readBuf))
+ return false;
+ //-----------------------------------------/
+ if (isProgessBarInitiated){
+ try {
+ if (readFrom+readPice == realNcaSize){
+ progressQueue.put(1.0);
+ isProgessBarInitiated = false;
+ }
+ else
+ progressQueue.put((readFrom+readPice)/(realNcaSize/100.0) / 100.0);
+ }catch (InterruptedException ie){
+ getException().printStackTrace(); // TODO: Do something with this
+ }
+ }
+ else {
+ if ((readPice == 8388608) && (readFrom == 0))
+ isProgessBarInitiated = true;
+ }
+ //-----------------------------------------/
+ readFrom += readPice;
+ }
+ bufferedInStream.close();
+ }
+ catch (IOException ioe){
+ printLog(" Failed to read NCA ID "+requestedNcaID+". IO Exception:\n "+ioe.getMessage(), EMsgType.FAIL);
+ ioe.printStackTrace();
+ return false;
+ }
+ return true;
+ }
+ }
+ //------------------------------------------------------------------------------------------------------------------
/**
* Correct exit
* */
@@ -318,201 +751,25 @@ class UsbCommunications extends Task {
int result = LibUsb.releaseInterface(handlerNS, DEFAULT_INTERFACE);
if (result != LibUsb.SUCCESS)
- printLog("Release interface\n Returned: "+result+" (sometimes it's not an issue)", MsgType.WARNING);
+ printLog("Release interface\n Returned: "+result+" (sometimes it's not an issue)", EMsgType.WARNING);
else
- printLog("Release interface", MsgType.PASS);
+ printLog("Release interface", EMsgType.PASS);
LibUsb.close(handlerNS);
- printLog("Requested handler close", MsgType.INFO);
+ printLog("Requested handler close", EMsgType.INFO);
}
// close context in the end
if (contextNS != null) {
LibUsb.exit(contextNS);
- printLog("Requested context close", MsgType.INFO);
+ printLog("Requested context close", EMsgType.INFO);
}
+
+ // Report status
+ for (String fileName: nspMap.keySet())
+ statusMap.put(fileName, status);
+
msgConsumer.interrupt();
}
- /**
- * After we sent commands to NS, this chain starts
- * */
- private void proceedCommands(){
- printLog("Awaiting for NS commands.", MsgType.INFO);
-
- /* byte[] magic = new byte[4];
- ByteBuffer bb = StandardCharsets.UTF_8.encode("TUC0").rewind().get(magic);
- // Let's rephrase this 'string' */
- final byte[] magic = new byte[]{(byte) 0x54, (byte) 0x55, (byte) 0x43, (byte) 0x30}; // eq. 'TUC0' @ UTF-8 (actually ASCII lol, u know what I mean)
-
- byte[] receivedArray;
-
- while (true){
- if (Thread.currentThread().isInterrupted()) // Check if user interrupted process.
- return;
- receivedArray = readFromUsb();
- if (receivedArray == null)
- return; // catches exception
-
- if (!Arrays.equals(Arrays.copyOfRange(receivedArray, 0,4), magic)) // Bytes from 0 to 3 should contain 'magic' TUC0, so must be verified like this
- continue;
-
- // 8th to 12th(explicits) bytes in returned data stands for command ID as unsigned integer (Little-endian). Actually, we have to compare arrays here, but in real world it can't be greater then 0/1/2, thus:
- // BTW also protocol specifies 4th byte to be 0x00 kinda indicating that that this command is valid. But, as you may see, never happens other situation when it's not = 0.
- if (receivedArray[8] == 0x00){ //0x00 - exit
- printLog("Received EXIT command. Terminating.", MsgType.PASS);
- return; // All interaction with USB device should be ended (expected);
- }
- else if ((receivedArray[8] == 0x01) || (receivedArray[8] == 0x02)){ //0x01 - file range; 0x02 unknown bug on backend side (dirty hack).
- printLog("Received FILE_RANGE command. Proceeding: [0x0"+receivedArray[8]+"]", MsgType.PASS);
- /*// We can get in this pocket a length of file name (+32). Why +32? I dunno man.. Do we need this? Definitely not. This app can live without it.
- long receivedSize = ByteBuffer.wrap(Arrays.copyOfRange(receivedArray, 12,20)).order(ByteOrder.LITTLE_ENDIAN).getLong();
- logsArea.appendText("[V] Received FILE_RANGE command. Size: "+Long.toUnsignedString(receivedSize)+"\n"); // this shit returns string that will be chosen next '+32'. And, BTW, can't be greater then 512
- */
- if (!fileRangeCmd()) {
- return; // catches exception
- }
- }
- }
- }
- /**
- * This is what returns requested file (files)
- * Executes multiple times
- * @return 'true' if everything is ok
- * 'false' is error/exception occurs
- * */
- private boolean fileRangeCmd(){
- boolean isProgessBarInitiated = false;
-
- byte[] receivedArray;
- // Here we take information of what other side wants
- receivedArray = readFromUsb();
- if (receivedArray == null)
- return false;
-
- // range_offset of the requested file. In the begining it will be 0x10.
- long receivedRangeSize = ByteBuffer.wrap(Arrays.copyOfRange(receivedArray, 0,8)).order(ByteOrder.LITTLE_ENDIAN).getLong(); // Note - it could be unsigned long. Unfortunately, this app won't support files greater then 8796093022208 Gb
- byte[] receivedRangeSizeRAW = Arrays.copyOfRange(receivedArray, 0,8); // used (only) when we use sendResponse(). It's just simply.
- long receivedRangeOffset = ByteBuffer.wrap(Arrays.copyOfRange(receivedArray, 8,16)).order(ByteOrder.LITTLE_ENDIAN).getLong(); // Note - it could be unsigned long. Unfortunately, this app won't support files greater then 8796093022208 Gb
- /* Below, it's REAL NSP file name length that we sent before among others (WITHOUT +32 byes). It can't be greater then... see what is written in the beginning of this code.
- We don't need this since in next pocket we'll get name itself UTF-8 encoded. Could be used to double-checks or something like that.
- long receivedNspNameLen = ByteBuffer.wrap(Arrays.copyOfRange(receivedArray, 16,24)).order(ByteOrder.LITTLE_ENDIAN).getLong(); */
-
- // Requesting UTF-8 file name required:
- receivedArray = readFromUsb();
- if (receivedArray == null)
- return false;
-
- String receivedRequestedNSP = new String(receivedArray, StandardCharsets.UTF_8);
- printLog("Reply to requested file: "+receivedRequestedNSP
- +"\n Range Size: "+receivedRangeSize
- +"\n Range Offset: "+receivedRangeOffset, MsgType.INFO);
-
- // Sending response header
- if (!sendResponse(receivedRangeSizeRAW)) // Get receivedRangeSize in 'RAW' format exactly as it has been received. It's simply.
- return false;
-
- // Read file starting:
- // from Range Offset (receivedRangeOffset)
- // to Range Size (receivedRangeSize) like end: receivedRangeOffset+receivedRangeSize
-
- try {
-
- BufferedInputStream bufferedInStream = new BufferedInputStream(new FileInputStream(nspMap.get(receivedRequestedNSP))); // TODO: refactor?
- byte[] bufferCurrent ;//= new byte[1048576]; // eq. Allocate 1mb
- int bufferLength;
- if (bufferedInStream.skip(receivedRangeOffset) != receivedRangeOffset){
- printLog("Requested skip is out of File size. Nothing to transmit.", MsgType.FAIL);
- return false;
- }
-
- long currentOffset = 0;
- // 'End Offset' equal to receivedRangeSize.
- int readPice = 8388608; // = 8Mb
-
- while (currentOffset < receivedRangeSize){
- if (Thread.currentThread().isInterrupted()) // Check if user interrupted process.
- return true;
- if ((currentOffset + readPice) >= receivedRangeSize )
- readPice = Math.toIntExact(receivedRangeSize - currentOffset);
- //System.out.println("CO: "+currentOffset+"\t\tEO: "+receivedRangeSize+"\t\tRP: "+readPice); // TODO: NOTE: -----------------------DEBUG-----------------
- // updating progress bar (if a lot of data requested) START BLOCK
- if (isProgessBarInitiated){
- try {
- if (currentOffset+readPice == receivedRangeOffset){
- progressQueue.put(1.0);
- isProgessBarInitiated = false;
- }
- else
- progressQueue.put((currentOffset+readPice)/(receivedRangeSize/100.0) / 100.0);
- }catch (InterruptedException ie){
- getException().printStackTrace();
- }
- }
- else {
- if ((readPice == 8388608) && (currentOffset == 0))
- isProgessBarInitiated = true;
- }
- // updating progress bar if needed END BLOCK
-
- bufferCurrent = new byte[readPice]; // TODO: not perfect moment, consider refactoring.
-
- bufferLength = bufferedInStream.read(bufferCurrent);
-
- if (bufferLength != -1){
- //write to USB
- if (!writeToUsb(bufferCurrent)) {
- printLog("Failure during NSP transmission.", MsgType.FAIL);
- return false;
- }
- currentOffset += readPice;
- }
- else {
- printLog("Unexpected reading of stream ended.", MsgType.WARNING);
- return false;
- }
-
- }
- } catch (FileNotFoundException fnfe){
- printLog("FileNotFoundException:\n"+fnfe.getMessage(), MsgType.FAIL);
- return false;
- } catch (IOException ioe){
- printLog("IOException:\n"+ioe.getMessage(), MsgType.FAIL);
- return false;
- } catch (ArithmeticException ae){
- printLog("ArithmeticException (can't cast end offset minus current to 'integer'):\n"+ae.getMessage(), MsgType.FAIL);
- return false;
- }
-
- return true;
- }
- /**
- * Send response header.
- * @return true if everything OK
- * false if failed
- * */
- private boolean sendResponse(byte[] rangeSize){ // This method as separate function itself for application needed as a cookie in the middle of desert.
- printLog("Sending response", MsgType.INFO);
- if (!writeToUsb(new byte[] { (byte) 0x54, (byte) 0x55, (byte) 0x43, (byte) 0x30, // 'TUC0'
- (byte) 0x01, // CMD_TYPE_RESPONSE = 1
- (byte) 0x00, (byte) 0x00, (byte) 0x00, // kinda padding. Guys, didn't you want to use integer value for CMD semantic?
- (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00} ) // Send integer value of '1' in Little-endian format.
- ){
- printLog("[1/3]", MsgType.FAIL);
- return false;
- }
- printLog("[1/3]", MsgType.PASS);
- if(!writeToUsb(rangeSize)) { // Send EXACTLY what has been received
- printLog("[2/3]", MsgType.FAIL);
- return false;
- }
- printLog("[2/3]", MsgType.PASS);
- if(!writeToUsb(new byte[12])) { // kinda another one padding
- printLog("[3/3]", MsgType.FAIL);
- return false;
- }
- printLog("[3/3]", MsgType.PASS);
- return true;
- }
-
/**
* Sending any byte array to USB device
* @return 'true' if no issues
@@ -528,25 +785,25 @@ class UsbCommunications extends Task {
if (result != LibUsb.SUCCESS){
switch (result){
case LibUsb.ERROR_TIMEOUT:
- printLog("Data transfer (write) issue\n Returned: ERROR_TIMEOUT", MsgType.FAIL);
+ printLog("Data transfer (write) issue\n Returned: ERROR_TIMEOUT", EMsgType.FAIL);
break;
case LibUsb.ERROR_PIPE: //WUT?? I dunno man looks overkill in here..
- printLog("Data transfer (write) issue\n Returned: ERROR_PIPE", MsgType.FAIL);
+ printLog("Data transfer (write) issue\n Returned: ERROR_PIPE", EMsgType.FAIL);
break;
case LibUsb.ERROR_OVERFLOW:
- printLog("Data transfer (write) issue\n Returned: ERROR_OVERFLOW", MsgType.FAIL);
+ printLog("Data transfer (write) issue\n Returned: ERROR_OVERFLOW", EMsgType.FAIL);
break;
case LibUsb.ERROR_NO_DEVICE:
- printLog("Data transfer (write) issue\n Returned: ERROR_NO_DEVICE", MsgType.FAIL);
+ printLog("Data transfer (write) issue\n Returned: ERROR_NO_DEVICE", EMsgType.FAIL);
break;
default:
- printLog("Data transfer (write) issue\n Returned: "+result, MsgType.FAIL);
+ printLog("Data transfer (write) issue\n Returned: "+result, EMsgType.FAIL);
}
- printLog("Execution stopped", MsgType.FAIL);
+ printLog("Execution stopped", EMsgType.FAIL);
return false;
}else {
if (writeBufTransferred.get() != message.length){
- printLog("Data transfer (write) issue\n Requested: "+message.length+"\n Transferred: "+writeBufTransferred.get(), MsgType.FAIL);
+ printLog("Data transfer (write) issue\n Requested: "+message.length+"\n Transferred: "+writeBufTransferred.get(), EMsgType.FAIL);
return false;
}
else {
@@ -557,7 +814,7 @@ class UsbCommunications extends Task {
/**
* Reading what USB device responded.
* @return byte array if data read successful
- * 'null' if read failed
+ * 'null' if read failed
* */
private byte[] readFromUsb(){
ByteBuffer readBuffer = ByteBuffer.allocateDirect(512);// //readBuffer.order() equals BIG_ENDIAN; DON'T TOUCH. And we will always allocate readBuffer for max-size endpoint supports (512 bytes)
@@ -570,21 +827,24 @@ class UsbCommunications extends Task {
if (result != LibUsb.SUCCESS){
switch (result){
case LibUsb.ERROR_TIMEOUT:
- printLog("Data transfer (read) issue\n Returned: ERROR_TIMEOUT", MsgType.FAIL);
+ printLog("Data transfer (read) issue\n Returned: ERROR_TIMEOUT", EMsgType.FAIL);
break;
case LibUsb.ERROR_PIPE: //WUT?? I dunno man looks overkill in here..
- printLog("Data transfer (read) issue\n Returned: ERROR_PIPE", MsgType.FAIL);
+ printLog("Data transfer (read) issue\n Returned: ERROR_PIPE", EMsgType.FAIL);
break;
case LibUsb.ERROR_OVERFLOW:
- printLog("Data transfer (read) issue\n Returned: ERROR_OVERFLOW", MsgType.FAIL);
+ printLog("Data transfer (read) issue\n Returned: ERROR_OVERFLOW", EMsgType.FAIL);
break;
case LibUsb.ERROR_NO_DEVICE:
- printLog("Data transfer (read) issue\n Returned: ERROR_NO_DEVICE", MsgType.FAIL);
+ printLog("Data transfer (read) issue\n Returned: ERROR_NO_DEVICE", EMsgType.FAIL);
+ break;
+ case LibUsb.ERROR_IO:
+ printLog("Data transfer (read) issue\n Returned: ERROR_IO", EMsgType.FAIL);
break;
default:
- printLog("Data transfer (read) issue\n Returned: "+result, MsgType.FAIL);
+ printLog("Data transfer (read) issue\n Returned: "+result, EMsgType.FAIL);
}
- printLog("Execution stopped", MsgType.FAIL);
+ printLog("Execution stopped", EMsgType.FAIL);
return null;
} else {
int trans = readBufTransferred.get();
@@ -596,10 +856,11 @@ class UsbCommunications extends Task {
return receivedBytes;
}
}
+
/**
* This is what will print to textArea of the application.
* */
- private void printLog(String message, MsgType type){
+ private void printLog(String message, EMsgType type){
try {
switch (type){
case PASS:
@@ -620,21 +881,5 @@ class UsbCommunications extends Task {
}catch (InterruptedException ie){
ie.printStackTrace();
}
-
}
- /**
- * Debug tool like hexdump <3
- */
- /*
- private void hexDumpUTF8(byte[] byteArray){
- for (int i=0; i < byteArray.length; i++)
- System.out.print(String.format("%02d-", i%10));
- System.out.println("\t[[COLUMNS LEN = "+byteArray.length+"]]");
- for (byte b: byteArray)
- System.out.print(String.format("%02x ", b));
- System.out.print("\t\t\t"
- + new String(byteArray, StandardCharsets.UTF_8)
- + "\n");
- }
- */
-}
+}
\ No newline at end of file
diff --git a/src/main/resources/NSLMain.fxml b/src/main/resources/NSLMain.fxml
index e3114b7..5ef03a6 100644
--- a/src/main/resources/NSLMain.fxml
+++ b/src/main/resources/NSLMain.fxml
@@ -2,23 +2,80 @@
+
+
+
+
+
+
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/TableView.fxml b/src/main/resources/TableView.fxml
new file mode 100644
index 0000000..198ed60
--- /dev/null
+++ b/src/main/resources/TableView.fxml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/locale_en.properties b/src/main/resources/locale_en.properties
index 5333998..17ee4b2 100644
--- a/src/main/resources/locale_en.properties
+++ b/src/main/resources/locale_en.properties
@@ -12,3 +12,10 @@ logsGreetingsMessage2=--\n\
Source: https://github.com/developersu/ns-usbloader/\n\
Site: https://developersu.blogspot.com/search/label/NS-USBloader\n\
Dmitry Isaenko [developer.su]
+windowTitleConfirmWrongPFS0=Incorrect file type
+windowBodyConfirmWrongPFS0=Selected NSP file has incrrect starting symbols. Most likely it's corrupted.\n\
+It's better to interrupt proccess now. Continue process anyway?
+tableStatusLbl=Status
+tableFileNameLbl=File name
+tableSizeLbl=Size (~Mb)
+tableUploadLbl=Upload?
diff --git a/src/main/resources/locale_ru.properties b/src/main/resources/locale_ru.properties
index 1f32036..1c4d268 100644
--- a/src/main/resources/locale_ru.properties
+++ b/src/main/resources/locale_ru.properties
@@ -12,4 +12,12 @@ logsGreetingsMessage2=--\n\
\u0418\u0441\u0445\u043E\u0434\u043D\u044B\u0439 \u043A\u043E\u0434: https://github.com/developersu/ns-usbloader/\n\
\u0421\u0430\u0439\u0442: https://developersu.blogspot.com/search/label/NS-USBloader\n\
\u0418\u0441\u0430\u0435\u043D\u043A\u043E \u0414\u043C\u0438\u0442\u0440\u0438\u0439 [developer.su]
+windowTitleConfirmWrongPFS0=\u041D\u0435\u043F\u0440\u0438\u0430\u0432\u0438\u043B\u044C\u043D\u044B\u0439 \u0442\u0438\u043F \u0444\u0430\u0439\u043B\u0430
+windowBodyConfirmWrongPFS0=\u0412\u044B\u0431\u0440\u0430\u043D\u043D\u044B\u0439 \u0444\u0430\u0439\u043B NSP \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u0442 \u043D\u0435\u0432\u0435\u0440\u043D\u044B\u0435 \u0441\u0438\u043C\u0432\u043E\u043B\u044B. \u041E\u043D \u043D\u0430\u0432\u0435\u0440\u043D\u044F\u043A\u0430 \u043F\u043E\u0432\u0440\u0435\u0436\u0434\u0451\u043D.\n\
+\u041B\u0443\u0447\u0448\u0435 \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C\u0441\u044F \u043F\u0440\u044F\u043C\u043E \u0441\u0435\u0439\u0447\u0430\u0441. \u0425\u043E\u0447\u0435\u0448\u044C \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0442\u044C \u043D\u0438 \u0441\u043C\u043E\u0442\u0440\u044F \u043D\u0438 \u043D\u0430 \u0447\u0442\u043E?\
+
+tableUploadLbl=\u0417\u0430\u0433\u0440\u0443\u0436\u0430\u0442\u044C?
+tableSizeLbl=\u0420\u0430\u0437\u043C\u0435\u0440 (~\u041C\u0431)
+tableFileNameLbl=\u0418\u043C\u044F \u0444\u0430\u0439\u043B\u0430
+tableStatusLbl=\u0421\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435
diff --git a/src/main/resources/res/app.css b/src/main/resources/res/app.css
deleted file mode 100644
index b68dee0..0000000
--- a/src/main/resources/res/app.css
+++ /dev/null
@@ -1,100 +0,0 @@
-@font-face {
- src: url("NotoMono-Regular.ttf");
-}
-.root{
- -fx-background: #2d2d2d;
-}
-
-.button, .buttonUp, .buttonStop{
- -fx-background-color: #4f4f4f;
- -fx-border-color: #4f4f4f;
- -fx-border-radius: 3;
- -fx-border-width: 2;
- -fx-text-fill: #f7fafa;
-
-}
-.button:hover, .buttonStop:hover, .buttonUp:hover{
- -fx-background-color: #4f4f4f;
- -fx-border-color: #a4ffff;
- -fx-border-radius: 3;
- -fx-border-width: 2;
- -fx-text-fill: #f7fafa;
-}
-.button:focused, .buttonStop:focused, .buttonUp:focused{
- -fx-background-color: #6a6a6a;
-}
-
-.button:pressed{
- -fx-background-color: #4f4f4f;
- -fx-border-color: #289de8;
- -fx-border-radius: 3;
- -fx-border-width: 2;
- -fx-text-fill: #f7fafa;
-}
-.buttonUp:pressed{
- -fx-background-color: #4f4f4f;
- -fx-border-color: #a2e019;
- -fx-border-radius: 3;
- -fx-border-width: 2;
- -fx-text-fill: #f7fafa;
-}
-.buttonStop:pressed{
- -fx-background-color: #4f4f4f;
- -fx-border-color: #fb582c;
- -fx-border-radius: 3;
- -fx-border-width: 2;
- -fx-text-fill: #f7fafa;
-}
-
-.text-area{
- -fx-background-color: transparent;
- -fx-control-inner-background: #4f4f4f;
- -fx-font-family: "Noto Mono";
- -fx-border-color: #00ffc9;
- -fx-border-radius: 3;
- -fx-border-width: 2;
- -fx-text-fill: #f7fafa;
-}
-
-.progress-bar {
- -fx-background-color: transparent;
- -fx-box-border: transparent;
-}
-.progress-bar > .track {
- -fx-background-color: transparent;
- -fx-box-border: transparent;
-}
-.progress-bar > .bar {
- -fx-background-color: linear-gradient(to right, #00bce4, #ff5f53);
- -fx-background-radius: 2;
- -fx-background-insets: 1 1 2 1;
- -fx-padding: 0.23em;
-}
-
-.dialog-pane {
- -fx-background-color: #4f4f4f;
-}
-.dialog-pane > .button-bar > .container{
- -fx-background-color: #2d2d2d;
-}
-
-.dialog-pane > .label{
- -fx-padding: 10 5 10 5;
-}
-
-
-
-.regionUpload{
- -fx-shape: "M8,21V19H16V21H8M8,17V15H16V17H8M8,13V11H16V13H8M19,9H5L12,2L19,9Z";
- -fx-background-color: #a2e019;
- -size: 24;
- -fx-min-height: -size;
- -fx-min-width: 20;
-}
-.regionStop{
- -fx-shape: "M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z";
- -fx-background-color: #fb582c;
- -size: 24;
- -fx-min-height: -size;
- -fx-min-width: -size;
-}
diff --git a/src/main/resources/res/app_dark.css b/src/main/resources/res/app_dark.css
new file mode 100644
index 0000000..19443a6
--- /dev/null
+++ b/src/main/resources/res/app_dark.css
@@ -0,0 +1,255 @@
+@font-face {
+ src: url("NotoMono-Regular.ttf");
+}
+.root{
+ -fx-background: #2d2d2d;
+}
+
+.button, .buttonUp, .buttonStop, .buttonSelect{
+ -fx-background-color: #4f4f4f;
+ -fx-border-color: #4f4f4f;
+ -fx-border-radius: 3;
+ -fx-border-width: 2;
+ -fx-text-fill: #f7fafa;
+ -fx-effect: none;
+}
+.button:hover, .buttonStop:hover, .buttonUp:hover, .choice-box:hover, .button:focused:hover, .buttonStop:focused:hover, .buttonUp:focused:hover, .buttonSelect:focused:hover .choice-box:focused:hover{
+ -fx-background-color: #4f4f4f;
+ -fx-border-color: #a4ffff;
+ -fx-border-radius: 3;
+ -fx-border-width: 2;
+ -fx-text-fill: #f7fafa;
+}
+.button:focused, .buttonStop:focused, .buttonUp:focused, .buttonSelect:focused, .choice-box:focused{
+ -fx-background-color: #6a6a6a;
+ -fx-border-color: #6a6a6a;
+}
+
+.button:pressed, .button:pressed:hover{
+ -fx-background-color: #4f4f4f;
+ -fx-border-color: #e82382;
+ -fx-border-radius: 3;
+ -fx-border-width: 2;
+ -fx-text-fill: #f7fafa;
+}
+.buttonSelect:pressed, .buttonSelect:pressed:hover{
+ -fx-background-color: #4f4f4f;
+ -fx-border-color: #289de8;
+ -fx-border-radius: 3;
+ -fx-border-width: 2;
+ -fx-text-fill: #f7fafa;
+}
+.buttonUp:pressed, .buttonUp:pressed:hover{
+ -fx-background-color: #4f4f4f;
+ -fx-border-color: #a2e019;
+ -fx-border-radius: 3;
+ -fx-border-width: 2;
+ -fx-text-fill: #f7fafa;
+}
+.buttonStop:pressed, .buttonStop:pressed:hover{
+ -fx-background-color: #4f4f4f;
+ -fx-border-color: #fb582c;
+ -fx-border-radius: 3;
+ -fx-border-width: 2;
+ -fx-text-fill: #f7fafa;
+}
+
+.text-area{
+ -fx-background-color: transparent;
+ -fx-control-inner-background: #4f4f4f;
+ -fx-font-family: "Noto Mono";
+ -fx-border-color: #00ffc9;
+ -fx-border-radius: 3;
+ -fx-border-width: 2;
+ -fx-text-fill: #f7fafa;
+}
+
+.progress-bar {
+ -fx-background-color: transparent;
+ -fx-box-border: transparent;
+}
+.progress-bar > .track {
+ -fx-background-color: transparent;
+ -fx-box-border: transparent;
+}
+.progress-bar > .bar {
+ -fx-background-color: linear-gradient(to right, #00bce4, #ff5f53);
+ -fx-background-radius: 2;
+ -fx-background-insets: 1 1 2 1;
+ -fx-padding: 0.23em;
+}
+
+.dialog-pane {
+ -fx-background-color: #4f4f4f;
+}
+.dialog-pane > .button-bar > .container{
+ -fx-background-color: #2d2d2d;
+}
+
+.dialog-pane > .label{
+ -fx-padding: 10 5 10 5;
+}
+
+.tool-bar{
+ -fx-background-color: transparent;
+}
+
+.special-pane-as-border{
+ -fx-background-color: #f7fafa;
+ -fx-min-height: 1;
+}
+// -======================== Choice box =========================-
+.choice-box {
+ -fx-background-color: #4f4f4f;
+ -fx-border-color: #4f4f4f;
+ -fx-border-radius: 3;
+ -fx-border-width: 2;
+ -fx-mark-color: #eea11e;
+ -fx-effect: none;
+}
+.choice-box > .label {
+ -fx-text-fill: #f7fafa;
+}
+
+.choice-box:pressed, .choice-box:pressed:hover{
+ -fx-background-color: #4f4f4f;
+ -fx-border-color: #eea11e;
+ -fx-border-radius: 3;
+ -fx-border-width: 2;
+ -fx-text-fill: #f7fafa;
+}
+
+// Background color of the whole context menu
+.choice-box .context-menu {
+ -fx-background-color: #2d2d2d;
+}
+
+// Focused item background color in the list
+.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{
+ -fx-fill: #f7fafa;
+}
+.tab-pane .tab:selected SVGPath{
+ -fx-fill: #08f3ff;
+}
+.tab-pane .tab{
+ -fx-background-color: #424242;
+ -fx-focus-color: transparent;
+ -fx-faint-focus-color: transparent;
+ -fx-border-radius: 0 0 0 0;
+ -fx-border-width: 3 0 0 0;
+ -fx-border-color: #424242;
+}
+
+.tab-pane .tab:selected{
+ -fx-background-color: #2d2d2d;
+ -fx-focus-color: transparent;
+ -fx-faint-focus-color: transparent;
+ -fx-border-radius: 0 0 0 0;
+ -fx-border-width: 3 0 0 0;
+ -fx-border-color: #08f3ff;
+}
+
+.tab-pane > .tab-header-area {
+ -fx-background-insets: 0.0;
+ -fx-padding: 5 5 5 5;
+}
+
+.tab-pane > .tab-header-area > .tab-header-background
+{
+ -fx-background-color: #424242;
+
+}
+.tab-pane > .tab-header-area > .headers-region > .tab {
+ -fx-padding: 10;
+}
+// -=========================== TABLE ======================-
+.table-view {
+ -fx-background-color: #4f4f4f;
+ -fx-background-image: url(app_logo.png);
+ -fx-background-position: center;
+ -fx-background-repeat: no-repeat;
+ -fx-border-color: #00ffc9;
+ -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;
+}
+
+.table-view .table-cell{
+ -fx-text-fill: #f7fafa;
+}
+.table-row-cell, .table-row-cell:filled:selected, .table-row-cell:selected{
+ -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: #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: #6d8484;
+}
+// -========================== Context menu =====================-
+.context-menu {
+ -fx-background-color: #2d2d2d;
+ -fx-text-fill: white;
+ -fx-cursor: hand;
+}
+.context-menu .menu-item .label {
+ -fx-text-fill: #f7fafa;
+}
+.context-menu .menu-item:focused .label {
+ -fx-text-fill: #f7fafa;
+}
+
+
+
+
+.regionUpload{
+ -fx-shape: "M8,21V19H16V21H8M8,17V15H16V17H8M8,13V11H16V13H8M19,9H5L12,2L19,9Z";
+ -fx-background-color: #a2e019;
+ -size: 24;
+ -fx-min-height: -size;
+ -fx-min-width: 20;
+}
+.regionStop{
+ -fx-shape: "M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z";
+ -fx-background-color: #fb582c;
+ -size: 24;
+ -fx-min-height: -size;
+ -fx-min-width: -size;
+}
+
+.regionLamp{
+ -fx-shape: "M12,2A7,7 0 0,1 19,9C19,11.38 17.81,13.47 16,14.74V17A1,1 0 0,1 15,18H9A1,1 0 0,1 8,17V14.74C6.19,13.47 5,11.38 5,9A7,7 0 0,1 12,2M9,21V20H15V21A1,1 0 0,1 14,22H10A1,1 0 0,1 9,21M12,4A5,5 0 0,0 7,9C7,11.05 8.23,12.81 10,13.58V16H14V13.58C15.77,12.81 17,11.05 17,9A5,5 0 0,0 12,4Z";
+ -fx-background-color: #f7fafa;
+ -size: 17.5;
+ -fx-min-height: -size;
+ -fx-min-width: 12.5;
+}
\ No newline at end of file
diff --git a/src/main/resources/res/app_light.css b/src/main/resources/res/app_light.css
new file mode 100644
index 0000000..e2857bf
--- /dev/null
+++ b/src/main/resources/res/app_light.css
@@ -0,0 +1,256 @@
+@font-face {
+ src: url("NotoMono-Regular.ttf");
+}
+.root{
+ -fx-background: #ebebeb;
+}
+
+.button, .buttonUp, .buttonStop, .buttonSelect{
+ -fx-background-color: #fefefe;
+ -fx-border-color: #fefefe;
+ -fx-border-radius: 3;
+ -fx-border-width: 2;
+ -fx-text-fill: #2c2c2c;
+ -fx-effect: dropshadow(three-pass-box, #b4b4b4, 2, 0, 0, 0);
+
+}
+.button:hover, .buttonStop:hover, .buttonUp:hover, .choice-box:hover, .button:focused:hover, .buttonStop:focused:hover, .buttonUp:focused:hover, .buttonSelect:focused:hover .choice-box:focused:hover{
+ -fx-background-color: #fefefe;
+ -fx-border-color: #00caca;
+ -fx-border-radius: 3;
+ -fx-border-width: 2;
+ -fx-text-fill: #2c2c2c;
+}
+.button:focused, .buttonStop:focused, .buttonUp:focused, .buttonSelect:focused, .choice-box:focused{
+ -fx-background-color: #cccccc;
+ -fx-border-color: #cccccc;
+}
+
+.button:pressed, .button:pressed:hover{
+ -fx-background-color: #fefefe;
+ -fx-border-color: #e82382;
+ -fx-border-radius: 3;
+ -fx-border-width: 2;
+ -fx-text-fill: #2c2c2c;
+}
+.buttonSelect:pressed, .buttonSelect:pressed:hover{
+ -fx-background-color: #fefefe;
+ -fx-border-color: #289de8;
+ -fx-border-radius: 3;
+ -fx-border-width: 2;
+ -fx-text-fill: #2c2c2c;
+}
+.buttonUp:pressed, .buttonUp:pressed:hover{
+ -fx-background-color: #fefefe;
+ -fx-border-color: #a2e019;
+ -fx-border-radius: 3;
+ -fx-border-width: 2;
+ -fx-text-fill: #2c2c2c;
+}
+.buttonStop:pressed, .buttonStop:pressed:hover{
+ -fx-background-color: #fefefe;
+ -fx-border-color: #fb582c;
+ -fx-border-radius: 3;
+ -fx-border-width: 2;
+ -fx-text-fill: #2c2c2c;
+}
+// -========================+ TextArea =====================-
+.text-area{
+ -fx-background-color: transparent;
+ -fx-control-inner-background: #fefefe;
+ -fx-font-family: "Noto Mono";
+ -fx-border-color: #06b9bb;
+ -fx-border-radius: 3;
+ -fx-border-width: 2;
+ -fx-text-fill: #2c2c2c;
+}
+
+.progress-bar {
+ -fx-background-color: transparent;
+ -fx-box-border: transparent;
+}
+.progress-bar > .track {
+ -fx-background-color: transparent;
+ -fx-box-border: transparent;
+}
+.progress-bar > .bar {
+ -fx-background-color: linear-gradient(to right, #00bce4, #ff5f53);
+ -fx-background-radius: 2;
+ -fx-background-insets: 1 1 2 1;
+ -fx-padding: 0.23em;
+}
+
+.dialog-pane {
+ -fx-background-color: #fefefe;
+}
+.dialog-pane > .button-bar > .container{
+ -fx-background-color: #2d2d2d;
+}
+
+.dialog-pane > .label{
+ -fx-padding: 10 5 10 5;
+}
+
+.tool-bar{
+ -fx-background-color: transparent;
+}
+
+.special-pane-as-border{
+ -fx-background-color: #2c2c2c;
+ -fx-min-height: 1;
+}
+// -======================== Choice box =========================-
+.choice-box {
+ -fx-background-color: #fefefe;
+ -fx-border-color: #fefefe;
+ -fx-border-radius: 3;
+ -fx-border-width: 2;
+ -fx-mark-color: #eea11e;
+ -fx-effect: dropshadow(three-pass-box, #b4b4b4, 2, 0, 0, 0);
+}
+.choice-box > .label {
+ -fx-text-fill: #2c2c2c;
+}
+
+.choice-box:pressed, .choice-box:pressed:hover{
+ -fx-background-color: #fefefe;
+ -fx-border-color: #eea11e;
+ -fx-border-radius: 3;
+ -fx-border-width: 2;
+ -fx-text-fill: #2c2c2c;
+}
+
+// Background color of the whole context menu
+.choice-box .context-menu {
+ -fx-background-color: #fefefe;
+}
+
+// Focused item background color in the list
+.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{
+ -fx-fill: #2c2c2c; // OK
+}
+.tab-pane .tab:selected SVGPath{
+ -fx-fill: #289de8; // OK
+}
+.tab-pane .tab{
+ -fx-background-color: #fefefe; //ok
+ -fx-focus-color: transparent;
+ -fx-faint-focus-color: transparent;
+ -fx-border-radius: 0 0 0 0;
+ -fx-border-width: 3 0 0 0;
+ -fx-border-color: #fefefe; //OK
+}
+
+.tab-pane .tab:selected{
+ -fx-background-color: #ebebeb; // OK
+ -fx-focus-color: transparent;
+ -fx-faint-focus-color: transparent;
+ -fx-border-radius: 0 0 0 0;
+ -fx-border-width: 3 0 0 0;
+ -fx-border-color: #289de8; // OK
+}
+
+.tab-pane > .tab-header-area {
+ -fx-background-insets: 0.0;
+ -fx-padding: 5 5 5 5;
+}
+
+.tab-pane > .tab-header-area > .tab-header-background
+{
+ -fx-background-color: #fefefe; // OK
+
+}
+.tab-pane > .tab-header-area > .headers-region > .tab {
+ -fx-padding: 10;
+}
+// -=========================== TABLE ======================-
+.table-view {
+ -fx-background-color: #fefefe;
+ -fx-background-image: url(app_logo.png);
+ -fx-background-position: center;
+ -fx-background-repeat: no-repeat;
+ -fx-border-color: #06b9bb;
+ -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;
+ -fx-text-fill: #2c2c2c;
+}
+.table-view .column-header-background, .table-view .filler{
+ -fx-background-color: #fefefe;
+}
+
+.table-view .table-cell{
+ -fx-text-fill: #2c2c2c;
+}
+.table-row-cell, .table-row-cell:filled:selected, .table-row-cell:selected{
+ -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: #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: #b0b0b0;
+}
+// -========================== Context menu =====================-
+.context-menu {
+ -fx-background-color: #fefefe;
+ -fx-text-fill: white;
+ -fx-cursor: hand;
+}
+.context-menu .menu-item .label {
+ -fx-text-fill: #2c2c2c;
+}
+.context-menu .menu-item:focused .label {
+ -fx-text-fill: #2c2c2c;
+}
+
+
+
+
+
+.regionUpload{
+ -fx-shape: "M8,21V19H16V21H8M8,17V15H16V17H8M8,13V11H16V13H8M19,9H5L12,2L19,9Z";
+ -fx-background-color: #a2e019;
+ -size: 24;
+ -fx-min-height: -size;
+ -fx-min-width: 20;
+}
+
+.regionStop{
+ -fx-shape: "M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z";
+ -fx-background-color: #fb582c;
+ -size: 24;
+ -fx-min-height: -size;
+ -fx-min-width: -size;
+}
+
+.regionLamp {
+ -fx-shape: "M12,2A7,7 0 0,0 5,9C5,11.38 6.19,13.47 8,14.74V17A1,1 0 0,0 9,18H15A1,1 0 0,0 16,17V14.74C17.81,13.47 19,11.38 19,9A7,7 0 0,0 12,2M9,21A1,1 0 0,0 10,22H14A1,1 0 0,0 15,21V20H9V21Z";
+ -fx-background-color: #2c2c2c;
+ -size: 17.5;
+ -fx-min-height: -size;
+ -fx-min-width: 12.5;
+}
\ No newline at end of file
diff --git a/src/main/resources/res/app_logo.png b/src/main/resources/res/app_logo.png
new file mode 100644
index 0000000..53c27bb
Binary files /dev/null and b/src/main/resources/res/app_logo.png differ