This commit is contained in:
Dmitry Isaenko 2019-02-18 03:06:49 +03:00
parent c4d0959cf3
commit f5a9ddf8df
13 changed files with 162 additions and 91 deletions

View file

@ -34,7 +34,7 @@ Install JRE/JDK 8 or higher (openJDK is good. Oracle's one is also good). JavaFX
See 'Linux' section.
Set 'Security & Privacy' if needed.
Set 'Security & Privacy' settings if needed.
### Windows:
@ -63,10 +63,18 @@ Table 'Status' = 'Uploaded' does not means that file installed. It means that it
Handling successful/failed installation is a purpose of the other side application (TinFoil/GoldLeaf). (And they don't provide any feedback interfaces so I can't detect success/failure.)
## TODO:
- [x] macOS QA by [Konstanin Kelemen](https://github.com/konstantin-kelemen). Appreciate assistance of [Vitaliy Natarov](https://github.com/SebastianUA).
- [x] macOS QA
-[x] v0.1
-[ ] v0.2 (partly)
- [x] Windows support
- [ ] code refactoring
- [ ] GoldLeaf support
- [ ] code refactoring (almost. todo: printLog() )
- [x] GoldLeaf support
- [ ] XCI support
- [ ] Settings
- [ ] File order sort (non-critical)
## 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)

View file

@ -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); }
}

View file

@ -1,4 +1,4 @@
package nsusbloader;
package nsusbloader.Controllers;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
@ -8,7 +8,10 @@ import javafx.scene.control.*;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.stage.FileChooser;
import nsusbloader.Controllers.NSTableViewController;
import nsusbloader.AppPreferences;
import nsusbloader.MediatorControl;
import nsusbloader.NSLMain;
import nsusbloader.UsbCommunications;
import java.io.File;
import java.net.URL;
@ -46,7 +49,7 @@ public class NSLMainController implements Initializable {
@Override
public void initialize(URL url, ResourceBundle rb) {
this.resourceBundle = rb;
logArea.setText(rb.getString("logsGreetingsMessage")+" "+NSLMain.appVersion+"!\n");
logArea.setText(rb.getString("logsGreetingsMessage")+" "+ NSLMain.appVersion+"!\n");
if (System.getProperty("os.name").toLowerCase().startsWith("lin"))
if (!System.getProperty("user.name").equals("root"))
logArea.appendText(rb.getString("logsEnteredAsMsg1")+System.getProperty("user.name")+"\n"+rb.getString("logsEnteredAsMsg2") + "\n");
@ -81,18 +84,20 @@ public class NSLMainController implements Initializable {
btnSwitchImage.getStyleClass().add("regionLamp");
switchThemeBtn.setGraphic(btnSwitchImage);
this.switchThemeBtn.setOnAction(e->switchTheme());
previouslyOpenedPath = AppPreferences.getInstance().getRecent();
}
/**
* Changes UI theme on the go
* */
private void switchTheme(){
if (switchThemeBtn.getScene().getStylesheets().get(0).equals("/res/app.css")) {
switchThemeBtn.getScene().getStylesheets().remove("/res/app.css");
if (switchThemeBtn.getScene().getStylesheets().get(0).equals("/res/app_dark.css")) {
switchThemeBtn.getScene().getStylesheets().remove("/res/app_dark.css");
switchThemeBtn.getScene().getStylesheets().add("/res/app_light.css");
}
else {
switchThemeBtn.getScene().getStylesheets().add("/res/app.css");
switchThemeBtn.getScene().getStylesheets().remove("/res/app_light.css");
switchThemeBtn.getScene().getStylesheets().add("/res/app_dark.css");
}
}
/**
@ -103,16 +108,14 @@ public class NSLMainController implements Initializable {
List<File> filesList;
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(resourceBundle.getString("btnFileOpen"));
if (previouslyOpenedPath == null)
fileChooser.setInitialDirectory(new File(System.getProperty("user.home"))); // TODO: read from prefs
else {
File validator = new File(previouslyOpenedPath);
if (validator.exists())
fileChooser.setInitialDirectory(validator); // TODO: read from prefs
else
fileChooser.setInitialDirectory(new File(System.getProperty("user.home"))); // TODO: read from prefs
}
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("NS ROM", "*.nsp"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("NSP ROM", "*.nsp"));
filesList = fileChooser.showOpenMultipleDialog(logArea.getScene().getWindow());
if (filesList != null && !filesList.isEmpty()) {
@ -156,7 +159,7 @@ public class NSLMainController implements Initializable {
* This thing modify UI for reusing 'Upload to NS' button and make functionality set for "Stop transmission"
* Called from mediator
* */
void notifyTransmissionStarted(boolean isTransmissionStarted){
public void notifyTransmissionStarted(boolean isTransmissionStarted){
if (isTransmissionStarted) {
selectNspBtn.setDisable(true);
uploadStopBtn.setOnAction(e->{ stopBtnAction(); });
@ -182,4 +185,11 @@ public class NSLMainController implements Initializable {
uploadStopBtn.getStyleClass().add("buttonUp");
}
}
/**
* Save preferences before exit
* */
public void exit(){
AppPreferences.getInstance().setTheme(switchThemeBtn.getScene().getStylesheets().get(0));
AppPreferences.getInstance().setRecent(previouslyOpenedPath);
}
}

View file

@ -39,14 +39,14 @@ public class NSLRowModel {
public void setStatus(EFileStatus status){ // TODO: Localization
switch (status){
case UPLOADED:
this.status = "Uploaded";
this.status = "Success";
markForUpload = false;
break;
case FAILED:
this.status = "Upload failed";
this.status = "Failed";
break;
case INCORRECT_FILE_FAILED:
this.status = "File incorrect";
this.status = "Failed: Incorrect file";
markForUpload = false;
break;
}

View file

@ -33,15 +33,29 @@ public class NSTableViewController implements Initializable {
public void initialize(URL url, ResourceBundle resourceBundle) {
rowsObsLst = FXCollections.observableArrayList();
table.setPlaceholder(new Label());
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
TableColumn<NSLRowModel, String> statusColumn = new TableColumn<>(resourceBundle.getString("tableStatusLbl"));
TableColumn<NSLRowModel, String> fileNameColumn = new TableColumn<>(resourceBundle.getString("tableFileNameLbl"));
TableColumn<NSLRowModel, String> fileSizeColumn = new TableColumn<>(resourceBundle.getString("tableSizeLbl"));
TableColumn<NSLRowModel, Boolean> uploadColumn = new TableColumn<>(resourceBundle.getString("tableUploadLbl"));
statusColumn.setMinWidth(70.0);
fileNameColumn.setMinWidth(250.0);
fileSizeColumn.setMinWidth(70.0);
uploadColumn.setMinWidth(70.0);
// See https://bugs.openjdk.java.net/browse/JDK-8157687
statusColumn.setMinWidth(100.0);
statusColumn.setPrefWidth(100.0);
statusColumn.setMaxWidth(100.0);
statusColumn.setResizable(false);
fileNameColumn.setMinWidth(25.0);
fileSizeColumn.setMinWidth(120.0);
fileSizeColumn.setPrefWidth(120.0);
fileSizeColumn.setMaxWidth(120.0);
fileSizeColumn.setResizable(false);
uploadColumn.setMinWidth(100.0);
uploadColumn.setPrefWidth(100.0);
uploadColumn.setMaxWidth(100.0);
uploadColumn.setResizable(false);
statusColumn.setCellValueFactory(new PropertyValueFactory<>("status"));
fileNameColumn.setCellValueFactory(new PropertyValueFactory<>("nspFileName"));
@ -76,12 +90,6 @@ public class NSTableViewController implements Initializable {
table.setItems(rowsObsLst);
table.getColumns().addAll(statusColumn, fileNameColumn, fileSizeColumn, uploadColumn);
/* debug content
rowsObsLst.add(new NSLRowModel(new File("/home/loper/стихи_2"), true));
rowsObsLst.add(new NSLRowModel(new File("/home/loper/стихи_2"), false));
rowsObsLst.add(new NSLRowModel(new File("/home/loper/стихи_2"), false));
rowsObsLst.add(new NSLRowModel(new File("/home/loper/стихи_2"), true));
*/
}
/**
* See uploadColumn callback. In case of GoldLeaf we have to restrict selection
@ -145,6 +153,7 @@ public class NSTableViewController implements Initializable {
model.setStatus(status);
}
}
table.refresh();
}
/**
* Called if selected different USB protocol

View file

@ -1,26 +1,28 @@
package nsusbloader;
import nsusbloader.Controllers.NSLMainController;
import java.util.concurrent.atomic.AtomicBoolean;
class MediatorControl {
public class MediatorControl {
private AtomicBoolean isTransferActive = new AtomicBoolean(false); // Overcoded just for sure
private NSLMainController applicationController;
static MediatorControl getInstance(){
public static MediatorControl getInstance(){
return MediatorControlHold.INSTANCE;
}
private static class MediatorControlHold {
private static final MediatorControl INSTANCE = new MediatorControl();
}
void setController(NSLMainController controller){
public void setController(NSLMainController controller){
this.applicationController = controller;
}
NSLMainController getContoller(){ return this.applicationController; }
synchronized void setTransferActive(boolean state) {
public synchronized void setTransferActive(boolean state) {
isTransferActive.set(state);
applicationController.notifyTransmissionStarted(state);
}
synchronized boolean getTransferActive() { return this.isTransferActive.get(); }
public synchronized boolean getTransferActive() { return this.isTransferActive.get(); }
}

View file

@ -49,16 +49,15 @@ public class MessagesConsumer extends AnimationTimer {
if (progressRecieved > 0)
progress.forEach(prg -> progressBar.setProgress(prg));
if (isInterrupted) {
if (isInterrupted) { // It's safe 'cuz it's could't be interrupted while HashMap populating
MediatorControl.getInstance().setTransferActive(false);
progressBar.setProgress(0.0);
if (statusMap.size() > 0) // It's safe 'cuz it's could't be interrupted while HashMap populating
if (statusMap.size() > 0)
for (String key : statusMap.keySet())
tableViewController.setFileStatus(key, statusMap.get(key));
this.stop();
}
//TODO
}
void interrupt(){

View file

@ -6,22 +6,24 @@ import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.stage.Stage;
import nsusbloader.Controllers.NSLMainController;
import java.util.Locale;
import java.util.ResourceBundle;
public class NSLMain extends Application {
static final String appVersion = "v0.2-DEVELOPMENT";
public static final String appVersion = "v0.2";
@Override
public void start(Stage primaryStage) throws Exception{
FXMLLoader loader = new FXMLLoader(getClass().getResource("/NSLMain.fxml"));
ResourceBundle rb;
if (Locale.getDefault().getISO3Language().equals("rus"))
rb = ResourceBundle.getBundle("locale", new Locale("ru"));
else
rb = ResourceBundle.getBundle("locale", new Locale("en"));
FXMLLoader loader = new FXMLLoader(getClass().getResource("/NSLMain.fxml"));
loader.setResources(rb);
Parent root = loader.load();
@ -36,7 +38,9 @@ public class NSLMain extends Application {
primaryStage.setMinWidth(600);
primaryStage.setMinHeight(375);
Scene mainScene = new Scene(root, 800, 400);
mainScene.getStylesheets().add("/res/app.css");
mainScene.getStylesheets().add(AppPreferences.getInstance().getTheme());
primaryStage.setScene(mainScene);
primaryStage.show();
@ -45,6 +49,9 @@ public class NSLMain extends Application {
if(! ServiceWindow.getConfirmationWindow(rb.getString("windowTitleConfirmExit"), rb.getString("windowBodyConfirmExit")))
e.consume();
});
NSLMainController controller = loader.getController();
primaryStage.setOnHidden(e-> controller.exit());
}
public static void main(String[] args) {

View file

@ -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<ButtonType> result = alertBox.showAndWait();
if (result.get() == ButtonType.OK)
return true;
else
return false;
return (result.isPresent() && result.get() == ButtonType.OK);
}
}

View file

@ -18,12 +18,13 @@ import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
class UsbCommunications extends Task<Void> {
public class UsbCommunications extends Task<Void> {
private final int DEFAULT_INTERFACE = 0;
private BlockingQueue<String> msgQueue;
private BlockingQueue<Double> progressQueue;
private HashMap<String, EFileStatus> statusMap; // BlockingQueue for literally one object. TODO: read more books ; replace to hashMap
private EFileStatus status = EFileStatus.FAILED;
private MessagesConsumer msgConsumer;
@ -44,7 +45,7 @@ class UsbCommunications extends Task<Void> {
Since this application let user an ability (theoretically) to choose same files in different folders, the latest selected file will be added to the list and handled correctly.
I have no idea why he/she will make a decision to do that. Just in case, we're good in this point.
*/
UsbCommunications(List<File> nspList, String protocol){
public UsbCommunications(List<File> nspList, String protocol){
this.protocol = protocol;
this.nspMap = new HashMap<>();
for (File f: nspList)
@ -275,28 +276,17 @@ class UsbCommunications extends Task<Void> {
printLog("\tEnd chain", EMsgType.INFO);
return null;
}
/**
* Report transfer status
* */
private void reportTransferStatus(EFileStatus status){
for (String fileName: nspMap.keySet())
statusMap.put(fileName, status);
}
/**
* Tinfoil processing
* */
private class TinFoil{
TinFoil(){
if (!sendListOfNSP()) {
reportTransferStatus(EFileStatus.FAILED);
if (!sendListOfNSP())
return;
}
if (proceedCommands()) // REPORT SUCCESS
reportTransferStatus(EFileStatus.UPLOADED);
else // REPORT FAILURE
reportTransferStatus(EFileStatus.FAILED);
status = EFileStatus.UPLOADED; // Don't change status that is already set to FAILED
}
/**
* Send what NSP will be transferred
@ -547,15 +537,14 @@ class UsbCommunications extends Task<Void> {
PFSProvider pfsElement = new PFSProvider(nspMap.get(nspMap.keySet().toArray()[0]), msgQueue);
if (!pfsElement.init()) {
printLog("GL File provided have incorrect structure and won't be uploaded", EMsgType.FAIL);
reportTransferStatus(EFileStatus.INCORRECT_FILE_FAILED);
status = EFileStatus.INCORRECT_FILE_FAILED;
return;
}
printLog("GL File structure validated and it will be uploaded", EMsgType.PASS);
if (initGoldLeafProtocol(pfsElement))
reportTransferStatus(EFileStatus.UPLOADED);
else
reportTransferStatus(EFileStatus.FAILED);
status = EFileStatus.UPLOADED;
// else - no change status that is already set to FAILED
}
private boolean initGoldLeafProtocol(PFSProvider pfsElement){
// Go parse commands
@ -774,6 +763,11 @@ class UsbCommunications extends Task<Void> {
LibUsb.exit(contextNS);
printLog("Requested context close", EMsgType.INFO);
}
// Report status
for (String fileName: nspMap.keySet())
statusMap.put(fileName, status);
msgConsumer.interrupt();
}
/**

View file

@ -17,7 +17,7 @@
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.SVGPath?>
<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.141" xmlns:fx="http://javafx.com/fxml/1" fx:controller="nsusbloader.NSLMainController">
<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.141" xmlns:fx="http://javafx.com/fxml/1" fx:controller="nsusbloader.Controllers.NSLMainController">
<children>
<VBox AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<children>

View file

@ -98,7 +98,7 @@
-fx-background-color: #f7fafa;
-fx-min-height: 1;
}
// -======================== Choice box =========================-
.choice-box {
-fx-background-color: #4f4f4f;
-fx-border-color: #4f4f4f;
@ -120,13 +120,19 @@
}
// Background color of the whole context menu
.choice-box .context-menu { -fx-background-color: #2d2d2d; }
.choice-box .context-menu {
-fx-background-color: #2d2d2d;
}
// Focused item background color in the list
.choice-box .menu-item:focused { -fx-background-color: #eea11e; }
// Text color of non-focused items in the list
.choice-box .menu-item > .label { -fx-text-fill: #f7fafa; }
// Text color of focused item in the list
.choice-box .menu-item:focused > .label { -fx-text-fill: #2d2d2d; }
.choice-box .context-menu .menu-item:focused {
-fx-background-color: #eea11e;
}
.choice-box .context-menu .menu-item:focused .label {
-fx-text-fill: #2c2c2c;
}
// -======================== TAB PANE =========================-
.tab-pane .tab SVGPath{
@ -176,12 +182,18 @@
-fx-border-radius: 3;
-fx-border-width: 2;
}
.table-view .arrow {
-fx-mark-color: #08f3ff ;
}
.table-view .column-header {
-fx-background-color: transparent;
-fx-border-width: 0 1 2 0;
-fx-border-color: #6d8484;
}
.table-view .column-header-background .label{
-fx-background-color: transparent;
-fx-text-fill: #08f3ff;
}
.table-view .column-header-background, .table-view .filler{
-fx-background-color: #4f4f4f;
@ -194,14 +206,14 @@
-fx-background-color: -fx-table-cell-border-color, #424242;
-fx-background-insets: 0, 0 0 1 0;
-fx-padding: 0.0em; /* 0 */
-fx-table-cell-border-color: #f7fafa;
-fx-table-cell-border-color: #6d8484;
}
.table-row-cell:odd, .table-row-cell:odd:filled:selected, .table-row-cell:odd:selected{
-fx-background-color: -fx-table-cell-border-color, #4f4f4f;
-fx-background-insets: 0, 0 0 1 0;
-fx-padding: 0.0em; /* 0 */
-fx-table-cell-border-color: #f7fafa;
-fx-table-cell-border-color: #6d8484;
}
// -========================== Context menu =====================-
.context-menu {

View file

@ -99,7 +99,7 @@
-fx-background-color: #2c2c2c;
-fx-min-height: 1;
}
// -======================== Choice box =========================-
.choice-box {
-fx-background-color: #fefefe;
-fx-border-color: #fefefe;
@ -121,13 +121,18 @@
}
// Background color of the whole context menu
.choice-box .context-menu { -fx-background-color: #fefefe; }
.choice-box .context-menu {
-fx-background-color: #fefefe;
}
// Focused item background color in the list
.choice-box .menu-item:focused { -fx-background-color: #eea11e; }
// Text color of non-focused items in the list
.choice-box .menu-item > .label { -fx-text-fill: #2c2c2c; }
// Text color of focused item in the list
.choice-box .menu-item:focused > .label { -fx-text-fill: #2d2d2d; }
.choice-box .context-menu .menu-item:focused {
-fx-background-color: #eea11e;
}
.choice-box .context-menu .menu-item:focused .label {
-fx-text-fill: #2c2c2c;
}
// -======================== TAB PANE =========================-
.tab-pane .tab SVGPath{
@ -177,8 +182,13 @@
-fx-border-radius: 3;
-fx-border-width: 2;
}
.table-view .arrow {
-fx-mark-color: #2c2c2c ;
}
.table-view .column-header {
-fx-background-color: transparent;
-fx-border-width: 0 1 2 0;
-fx-border-color: #b0b0b0;
}
.table-view .column-header-background .label{
-fx-background-color: transparent;
@ -195,14 +205,14 @@
-fx-background-color: -fx-table-cell-border-color, #d3fffd;
-fx-background-insets: 0, 0 0 1 0;
-fx-padding: 0.0em; /* 0 */
-fx-table-cell-border-color: #2c2c2c;
-fx-table-cell-border-color: #b0b0b0;
}
.table-row-cell:odd, .table-row-cell:odd:filled:selected, .table-row-cell:odd:selected{
-fx-background-color: -fx-table-cell-border-color, #fefefe;
-fx-background-insets: 0, 0 0 1 0;
-fx-padding: 0.0em; /* 0 */
-fx-table-cell-border-color: #2c2c2c;
-fx-table-cell-border-color: #b0b0b0;
}
// -========================== Context menu =====================-
.context-menu {