From d70ad34df4853cc19e87f5cee39dd3e4befe7f87 Mon Sep 17 00:00:00 2001 From: Jannik Date: Fri, 27 Apr 2018 17:44:31 +0200 Subject: [PATCH 01/88] added travis --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..78fa8d1 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,3 @@ +language: java +jdk: + - oraclejdk9 From 46c1c43cab8e5c0299cd8ed216a3f638d1f3328a Mon Sep 17 00:00:00 2001 From: Jannik Date: Fri, 27 Apr 2018 17:47:01 +0200 Subject: [PATCH 02/88] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2491954..44f1590 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ ![Total Downloads](https://img.shields.io/github/downloads/Seil0/Project-HomeFlix/total.svg?style=flat-square) +[![](https://img.shields.io/travis/Seil0/Project-HomeFlix/dev.svg?style=flat-square)](https://travis-ci.org/Seil0/Project-HomeFlix) [![Latest](https://img.shields.io/github/release/Seil0/Project-HomeFlix/all.svg?style=flat-square)](https://github.com/Seil0/Project-HomeFlix/releases) [![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg?style=flat-square)](https://www.gnu.org/licenses/gpl-3.0) From 85a403a834a8dc57d4b64f328e7852c95bb324c2 Mon Sep 17 00:00:00 2001 From: Jannik Date: Sat, 28 Apr 2018 12:53:44 +0200 Subject: [PATCH 03/88] fixed Hamburger icon has wrong color after color change --- .../application/MainWindowController.java | 40 +++++++++---------- .../HomeFlix/controller/DBController.java | 28 +++++-------- .../HomeFlix/player/PlayerController.java | 2 +- 3 files changed, 30 insertions(+), 40 deletions(-) diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index 45cb8b0..fa8cba6 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -550,17 +550,6 @@ public class MainWindowController { } } - /** - * check if a film is supported by the HomeFlixPlayer or not - * this is the case if the mime type is mp4 - * @param entry the film you want to check - * @return true if so, false if not - */ - private boolean isSupportedFormat(FilmTabelDataType film) { - String mimeType = URLConnection.guessContentTypeFromName(film.getStreamUrl()); - return mimeType != null && (mimeType.contains("mp4") || mimeType.contains("vp6")); - } - @FXML private void openfolderbtnclicked() { String dest = new File(getCurrentStreamUrl()).getParentFile().getAbsolutePath(); @@ -629,13 +618,14 @@ public class MainWindowController { } @FXML - private void colorPickerAction(){ - editColor(colorPicker.getValue().toString()); + private void colorPickerAction() { + setColor(colorPicker.getValue().toString().substring(2, 10)); + saveSettings(); applyColor(); } @FXML - private void updateBtnAction(){ + private void updateBtnAction() { updateController = new UpdateController(this, buildNumber, useBeta); Thread updateThread = new Thread(updateController); updateThread.setName("Updater"); @@ -757,9 +747,12 @@ public class MainWindowController { openfolderbtn.setStyle(btnStyleWhite); returnBtn.setStyle(btnStyleWhite); forwardBtn.setStyle(btnStyleWhite); + playbtn.setGraphic(play_arrow_white); returnBtn.setGraphic(skip_previous_white); forwardBtn.setGraphic(skip_next_white); + + menuHam.getStyleClass().clear(); menuHam.getStyleClass().add("jfx-hamburgerW"); } else { dialogBtnStyle = btnStyleBlack; @@ -772,9 +765,12 @@ public class MainWindowController { openfolderbtn.setStyle(btnStyleBlack); returnBtn.setStyle(btnStyleBlack); forwardBtn.setStyle(btnStyleBlack); + playbtn.setGraphic(play_arrow_black); returnBtn.setGraphic(skip_previous_black); forwardBtn.setGraphic(skip_next_black); + + menuHam.getStyleClass().clear(); menuHam.getStyleClass().add("jfx-hamburgerB"); } } @@ -953,14 +949,18 @@ public class MainWindowController { } } - // cuts 0x of the Color-pickers return value - private void editColor(String input) { - StringBuilder sb = new StringBuilder(input); - sb.delete(0, 2); - this.color = sb.toString(); - saveSettings(); + /** + * check if a film is supported by the HomeFlixPlayer or not + * this is the case if the mime type is mp4 + * @param entry the film you want to check + * @return true if so, false if not + */ + private boolean isSupportedFormat(FilmTabelDataType film) { + String mimeType = URLConnection.guessContentTypeFromName(film.getStreamUrl()); + return mimeType != null && (mimeType.contains("mp4") || mimeType.contains("vp6")); } + // getter and setter public DBController getDbController() { return dbController; diff --git a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java index 8ef2aac..495eb8a 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java @@ -731,7 +731,7 @@ public class DBController { return nextFilm; } - /** TODO check if we relay need to separate between favorites and none favorites + /** * get the last watched episode * @param title the title of the series * @return the last watched episode as {@link FilmTabelDataType} object @@ -745,26 +745,16 @@ public class DBController { Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM films WHERE title = \"" + title + "\";"); while (rs.next()) { - if (rs.getBoolean("favorite") == true) { - nextFilm = new FilmTabelDataType(rs.getString("streamUrl"), - rs.getString("title"), rs.getString("season"), rs.getString("episode") ,rs.getBoolean("favorite"), - rs.getBoolean("cached"), new ImageView(favorite_black)); - } else { - nextFilm = new FilmTabelDataType(rs.getString("streamUrl"), - rs.getString("title"), rs.getString("season"), rs.getString("episode"), rs.getBoolean("favorite"), - rs.getBoolean("cached"), new ImageView(favorite_border_black)); - } + // favorite image is black + nextFilm = new FilmTabelDataType(rs.getString("streamUrl"), rs.getString("title"), + rs.getString("season"), rs.getString("episode"), rs.getBoolean("favorite"), + rs.getBoolean("cached"), new ImageView(favorite_black)); if (rs.getDouble("currentTime") > lastCurrentTime) { lastCurrentTime = rs.getDouble("currentTime"); - if (rs.getBoolean("favorite") == true) { - nextFilm = new FilmTabelDataType(rs.getString("streamUrl"), - rs.getString("title"), rs.getString("season"), rs.getString("episode") ,rs.getBoolean("favorite"), - rs.getBoolean("cached"), new ImageView(favorite_black)); - } else { - nextFilm = new FilmTabelDataType(rs.getString("streamUrl"), - rs.getString("title"), rs.getString("season"), rs.getString("episode"), rs.getBoolean("favorite"), - rs.getBoolean("cached"), new ImageView(favorite_border_black)); - } + // favorite image is black + nextFilm = new FilmTabelDataType(rs.getString("streamUrl"), rs.getString("title"), + rs.getString("season"), rs.getString("episode"), rs.getBoolean("favorite"), + rs.getBoolean("cached"), new ImageView(favorite_black)); break; } } diff --git a/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java b/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java index 40fcee6..5cb2e31 100644 --- a/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java +++ b/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java @@ -92,7 +92,7 @@ public class PlayerController { private ImageView fullscreen_black = new ImageView(new Image("icons/ic_fullscreen_black_24dp_1x.png")); private ImageView fullscreen_exit_black = new ImageView(new Image("icons/ic_fullscreen_exit_black_24dp_1x.png")); - /** FIXME double set currentTime( + /** FIXME double set currentTime() * initialize the new PlayerWindow * @param entry the film object * @param player the player object (needed for closing action) From 391ef59f70bfb5cb584e73aea300da082f276730 Mon Sep 17 00:00:00 2001 From: Jannik Date: Sat, 28 Apr 2018 18:02:02 +0200 Subject: [PATCH 04/88] code cleanup --- pom.xml | 1 + .../HomeFlix/application/Main.java | 49 ++--- .../application/MainWindowController.java | 184 ++++++++---------- .../locals/HomeFlix-Local_de_DE.properties | 7 - .../locals/HomeFlix-Local_en_US.properties | 7 - 5 files changed, 101 insertions(+), 147 deletions(-) diff --git a/pom.xml b/pom.xml index 95f22a2..2887ba2 100644 --- a/pom.xml +++ b/pom.xml @@ -80,6 +80,7 @@ maven-shade-plugin 3.1.1 + Project-HomeFlix true diff --git a/src/main/java/kellerkinder/HomeFlix/application/Main.java b/src/main/java/kellerkinder/HomeFlix/application/Main.java index d9012c4..588952e 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/Main.java +++ b/src/main/java/kellerkinder/HomeFlix/application/Main.java @@ -40,7 +40,6 @@ import javafx.scene.layout.AnchorPane; import javafx.stage.DirectoryChooser; import javafx.stage.FileChooser; import javafx.stage.Stage; -import javafx.stage.WindowEvent; public class Main extends Application { @@ -56,8 +55,7 @@ public class Main extends Application { private static String javaVers = System.getProperty("java.version"); private static String javaVend = System.getProperty("java.vendor"); private static String local = System.getProperty("user.language") + "_" + System.getProperty("user.country"); - private String dirWin = userHome + "/Documents/HomeFlix"; // Windows: C:/Users/"User"/Documents/HomeFlix - private String dirLinux = userHome + "/HomeFlix"; // Linux: /home/"User"/HomeFlix + private static String dirHomeFlix; private File directory; private File configFile; private File posterCache; @@ -88,26 +86,14 @@ public class Main extends Application { primaryStage.setResizable(false); primaryStage.setTitle("Project HomeFlix"); primaryStage.getIcons().add(new Image(Main.class.getResourceAsStream("/icons/Homeflix_Icon_64x64.png"))); //adds application icon - primaryStage.setOnCloseRequest(new EventHandler() { - public void handle(WindowEvent we) { - System.exit(1); - } - }); + primaryStage.setOnCloseRequest(event -> System.exit(1)); mainWindowController = loader.getController(); //Link of FXMLController and controller class mainWindowController.setMain(this); //call setMain - - // get OS and the specific paths - if (osName.contains("Windows")) { - directory = new File(dirWin); - configFile = new File(dirWin + "/config.xml"); - posterCache = new File(dirWin + "/posterCache"); - } else { - directory = new File(dirLinux); - configFile = new File(dirLinux + "/config.xml"); - posterCache = new File(dirLinux + "/posterCache"); - } + directory = new File(dirHomeFlix); + configFile = new File(dirHomeFlix + "/config.xml"); + posterCache = new File(dirHomeFlix + "/posterCache"); // generate window scene = new Scene(pane); // create new scene, append pane to scene @@ -117,7 +103,8 @@ public class Main extends Application { // startup checks if (!configFile.exists()) { - directory.mkdir(); + directory.mkdir(); + addFirstSource(); mainWindowController.setColor("ee3523"); mainWindowController.setFontSize(17.0); @@ -130,7 +117,7 @@ public class Main extends Application { posterCache.mkdir(); } - // init here as it loads the games to the mwc and the gui, therefore the window must exist + // initialize here as it loads the games to the mwc and the GUI, therefore the window must exist mainWindowController.init(); mainWindowController.getDbController().init(); } catch (IOException e) { @@ -148,7 +135,7 @@ public class Main extends Application { bundle = ResourceBundle.getBundle("locals.HomeFlix-Local", Locale.US); // us_english break; case "de_DE": - bundle = ResourceBundle.getBundle("locals.HomeFlix-Local", Locale.GERMAN); // German + bundle = ResourceBundle.getBundle("locals.HomeFlix-Local", Locale.GERMAN); // de_german break; default: bundle = ResourceBundle.getBundle("locals.HomeFlix-Local", Locale.US); // default local @@ -200,20 +187,20 @@ public class Main extends Application { } /** - * set the log file location and initialize the logger - * launch the GUI + * set the log file location and initialize the logger launch the GUI * @param args arguments given at the start */ public static void main(String[] args) { - if (System.getProperty("os.name").equals("Windows")) { - System.setProperty("logFilename", userHome + "/Documents/HomeFlix/app.log"); - File logFile = new File(userHome + "/Documents/HomeFlix/app.log"); - logFile.delete(); + + if (osName.contains("Windows")) { + dirHomeFlix = userHome + "/Documents/HomeFlix"; } else { - System.setProperty("logFilename", userHome + "/HomeFlix/app.log"); - File logFile = new File(userHome + "/HomeFlix/app.log"); - logFile.delete(); + dirHomeFlix = userHome + "/HomeFlix"; } + + System.setProperty("logFilename", dirHomeFlix + "/app.log"); + File logFile = new File(dirHomeFlix + "/app.log"); + logFile.delete(); LOGGER = LogManager.getLogger(Main.class.getName()); launch(args); } diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index fa8cba6..7945689 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -200,30 +200,30 @@ public class MainWindowController { private TableColumn sourceColumn; @FXML private TableColumn modeColumn; - + + private Main main; + private MainWindowController mainWindowController; + private UpdateController updateController; + private OMDbAPIController omdbAPIController; + private DBController dbController; + private static final Logger LOGGER = LogManager.getLogger(MainWindowController.class.getName()); + private boolean menuTrue = false; private boolean settingsTrue = false; private boolean autoUpdate = false; private boolean useBeta = false; private boolean autoplay = false; - private static final Logger LOGGER = LogManager.getLogger(MainWindowController.class.getName()); - private int hashA = -647380320; private final String version = "0.7.0"; private final String buildNumber = "151"; private final String versionName = "toothless dragon"; - private String dialogBtnStyle; + private String btnStyle; private String color; private String local; private String omdbAPIKey; - // text strings - private String errorLoad; - private String errorSave; - private String infoText; - private String vlcNotInstalled; - private double fontSize; + private final int hashA = -647380320; private int last; private int indexTable; private int indexList; @@ -247,12 +247,6 @@ public class MainWindowController { private ContextMenu menu = new ContextMenu(like, dislike); private Properties props = new Properties(); - private Main main; - private MainWindowController mainWindowController; - private UpdateController updateController; - private OMDbAPIController omdbAPIController; - private DBController dbController; - /** * "Main" Method called in Main.java main() when starting * Initialize other objects: Updater, dbController and ApiQuery @@ -274,6 +268,28 @@ public class MainWindowController { initActions(); } + // Initialize UI elements + private void initUI() { + versionLbl.setText("Version: " + version + " (Build: " + buildNumber + ")"); + fontsizeSlider.setValue(getFontSize()); + colorPicker.setValue(Color.valueOf(getColor())); + + updateBtn.setFont(Font.font("System", 12)); + autoUpdateToggleBtn.setSelected(isAutoUpdate()); + autoplayToggleBtn.setSelected(isAutoplay()); + languageChoisBox.setItems(languages); + branchChoisBox.setItems(branches); + + if (isUseBeta()) { + branchChoisBox.getSelectionModel().select(1); + } else { + branchChoisBox.getSelectionModel().select(0); + } + + setLocalUI(); + applyColor(); + } + // Initialize the tables (treeTableViewfilm and sourcesTable) private void initTabel() { @@ -402,7 +418,7 @@ public class MainWindowController { public void handle(ActionEvent event) { dbController.like(getCurrentStreamUrl()); dbController.refresh(getCurrentStreamUrl(), indexList); - refreshTable(); + refreshTableElement(); } }); @@ -411,7 +427,7 @@ public class MainWindowController { public void handle(ActionEvent event) { dbController.dislike(getCurrentStreamUrl()); dbController.refresh(getCurrentStreamUrl(), indexList); - refreshTable(); + refreshTableElement(); } }); @@ -479,28 +495,6 @@ public class MainWindowController { }); } - // initialize UI elements - private void initUI() { - versionLbl.setText("Version: " + version + " (Build: " + buildNumber + ")"); - fontsizeSlider.setValue(getFontSize()); - colorPicker.setValue(Color.valueOf(getColor())); - - updateBtn.setFont(Font.font("System", 12)); - autoUpdateToggleBtn.setSelected(isAutoUpdate()); - autoplayToggleBtn.setSelected(isAutoplay()); - languageChoisBox.setItems(languages); - branchChoisBox.setItems(branches); - - if (isUseBeta()) { - branchChoisBox.getSelectionModel().select(1); - } else { - branchChoisBox.getSelectionModel().select(0); - } - - setLocalUI(); - applyColor(); - } - @FXML private void playbtnclicked() { if (currentTableFilm.getStreamUrl().contains("_rootNode")) { @@ -528,7 +522,7 @@ public class MainWindowController { e1.printStackTrace(); } if (output.contains("which: no vlc") || output == "") { - JFXInfoAlert vlcInfoAlert = new JFXInfoAlert("Info", vlcNotInstalled, dialogBtnStyle, main.getPrimaryStage()); + JFXInfoAlert vlcInfoAlert = new JFXInfoAlert("Info", getBundle().getString("vlcNotInstalled"), btnStyle, main.getPrimaryStage()); vlcInfoAlert.showAndWait(); } else { try { @@ -575,8 +569,8 @@ public class MainWindowController { @FXML private void aboutBtnAction() { String bodyText = "cemu_UI by @Seil0 \nVersion: " + version + " (Build: " + buildNumber + ") \"" - + versionName + "\" \n" + infoText; - JFXInfoAlert infoAlert = new JFXInfoAlert("Project HomeFlix", bodyText, dialogBtnStyle, main.getPrimaryStage()); + + versionName + "\" \n" + getBundle().getString("infoText"); + JFXInfoAlert infoAlert = new JFXInfoAlert("Project HomeFlix", bodyText, btnStyle, main.getPrimaryStage()); infoAlert.showAndWait(); } @@ -634,26 +628,18 @@ public class MainWindowController { @FXML private void autoUpdateToggleBtnAction(){ - if (isAutoUpdate()) { - setAutoUpdate(false); - } else { - setAutoUpdate(true); - } + autoUpdate = isAutoUpdate() ? false : true; saveSettings(); } @FXML private void autoplayToggleBtnAction(){ - if (isAutoplay()) { - setAutoplay(false); - } else { - setAutoplay(true); - } + autoplay = isAutoplay() ? false : true; saveSettings(); } // refresh the selected child of the root node - private void refreshTable() { + private void refreshTableElement() { filmRoot.getChildren().get(indexTable).setValue(filmsList.get(indexList)); } @@ -726,27 +712,15 @@ public class MainWindowController { * if usedColor is less than checkColor set text fill white, else black */ private void applyColor() { - String style = "-fx-background-color: #" + getColor() + ";"; + String menuBtnStyle; String btnStyleBlack = "-fx-button-type: RAISED; -fx-background-color: #" + getColor() + "; -fx-text-fill: BLACK;"; String btnStyleWhite = "-fx-button-type: RAISED; -fx-background-color: #" + getColor() + "; -fx-text-fill: WHITE;"; BigInteger usedColor = new BigInteger(getColor(), 16); BigInteger checkColor = new BigInteger("78909cff", 16); - sideMenuVBox.setStyle(style); - topHBox.setStyle(style); - searchTextField.setFocusColor(Color.valueOf(getColor())); - if (usedColor.compareTo(checkColor) == -1) { - dialogBtnStyle = btnStyleWhite; - settingsBtn.setStyle("-fx-text-fill: WHITE;"); - aboutBtn.setStyle("-fx-text-fill: WHITE;"); - addDirectoryBtn.setStyle(btnStyleWhite); - addStreamSourceBtn.setStyle(btnStyleWhite); - updateBtn.setStyle(btnStyleWhite); - playbtn.setStyle(btnStyleWhite); - openfolderbtn.setStyle(btnStyleWhite); - returnBtn.setStyle(btnStyleWhite); - forwardBtn.setStyle(btnStyleWhite); + btnStyle = btnStyleWhite; + menuBtnStyle = "-fx-text-fill: WHITE;"; playbtn.setGraphic(play_arrow_white); returnBtn.setGraphic(skip_previous_white); @@ -755,16 +729,8 @@ public class MainWindowController { menuHam.getStyleClass().clear(); menuHam.getStyleClass().add("jfx-hamburgerW"); } else { - dialogBtnStyle = btnStyleBlack; - settingsBtn.setStyle("-fx-text-fill: BLACK;"); - aboutBtn.setStyle("-fx-text-fill: BLACK;"); - addDirectoryBtn.setStyle(btnStyleBlack); - addStreamSourceBtn.setStyle(btnStyleBlack); - updateBtn.setStyle(btnStyleBlack); - playbtn.setStyle(btnStyleBlack); - openfolderbtn.setStyle(btnStyleBlack); - returnBtn.setStyle(btnStyleBlack); - forwardBtn.setStyle(btnStyleBlack); + btnStyle = btnStyleBlack; + menuBtnStyle = "-fx-text-fill: BLACK;"; playbtn.setGraphic(play_arrow_black); returnBtn.setGraphic(skip_previous_black); @@ -773,6 +739,24 @@ public class MainWindowController { menuHam.getStyleClass().clear(); menuHam.getStyleClass().add("jfx-hamburgerB"); } + + // boxes and TextFields + sideMenuVBox.setStyle("-fx-background-color: #" + getColor() + ";"); + topHBox.setStyle("-fx-background-color: #" + getColor() + ";"); + searchTextField.setFocusColor(Color.valueOf(getColor())); + + // normal buttons + addDirectoryBtn.setStyle(btnStyle); + addStreamSourceBtn.setStyle(btnStyle); + updateBtn.setStyle(btnStyle); + playbtn.setStyle(btnStyle); + openfolderbtn.setStyle(btnStyle); + returnBtn.setStyle(btnStyle); + forwardBtn.setStyle(btnStyle); + + // menu buttons + settingsBtn.setStyle(menuBtnStyle); + aboutBtn.setStyle(menuBtnStyle); } // slide in in 400ms @@ -829,10 +813,6 @@ public class MainWindowController { columnSeason.setText(getBundle().getString("columnSeason")); columnEpisode.setText(getBundle().getString("columnEpisode")); columnFavorite.setText(getBundle().getString("columnFavorite")); - errorLoad = getBundle().getString("errorLoad"); - errorSave = getBundle().getString("errorSave"); - infoText = getBundle().getString("infoText"); - vlcNotInstalled = getBundle().getString("vlcNotInstalled"); } /** @@ -853,7 +833,7 @@ public class MainWindowController { props.storeToXML(outputStream, "Project HomeFlix settings"); // write new .xml outputStream.close(); } catch (IOException e) { - LOGGER.error(errorLoad, e); + LOGGER.error("An error occurred while saving the settings!", e); } } @@ -872,7 +852,7 @@ public class MainWindowController { setColor(props.getProperty("color")); } catch (Exception e) { LOGGER.error("cloud not load color", e); - setColor(""); + setColor("00a8cc"); } try { @@ -912,7 +892,7 @@ public class MainWindowController { inputStream.close(); } catch (IOException e) { - LOGGER.error(errorSave, e); + LOGGER.error("An error occurred while loading the settings!", e); } // try loading the omdbAPI key @@ -966,14 +946,14 @@ public class MainWindowController { return dbController; } - public void setColor(String input) { - this.color = input; - } - public String getColor() { return color; } + public void setColor(String input) { + this.color = input; + } + public FilmTabelDataType getCurrentTableFilm() { return currentTableFilm; } @@ -986,13 +966,13 @@ public class MainWindowController { return currentTableFilm.getStreamUrl(); } - public void setFontSize(Double input) { - this.fontSize = input; - } - public Double getFontSize() { return fontSize; } + + public void setFontSize(Double input) { + this.fontSize = input; + } public int getIndexTable() { return indexTable; @@ -1002,13 +982,13 @@ public class MainWindowController { return indexList; } - public void setAutoUpdate(boolean input) { - this.autoUpdate = input; - } - public boolean isAutoUpdate() { return autoUpdate; } + + public void setAutoUpdate(boolean input) { + this.autoUpdate = input; + } public boolean isUseBeta() { return useBeta; @@ -1026,13 +1006,13 @@ public class MainWindowController { this.autoplay = autoplay; } - public void setLocal(String input) { - this.local = input; - } - public String getLocal() { return local; } + + public void setLocal(String input) { + this.local = input; + } public String getOmdbAPIKey() { return omdbAPIKey; diff --git a/src/main/resources/locals/HomeFlix-Local_de_DE.properties b/src/main/resources/locals/HomeFlix-Local_de_DE.properties index 166e686..93e9015 100644 --- a/src/main/resources/locals/HomeFlix-Local_de_DE.properties +++ b/src/main/resources/locals/HomeFlix-Local_de_DE.properties @@ -31,13 +31,6 @@ columnEpisode = Episode columnFavorite = Favorit #error translations -errorUpdateV = Beim ausf\u00FChren des Updates ist ein Fehler aufgetreten! \nError: could not check update version (nvc)\nWeitere Hilfe erhalten sie unter www.kellerkinder.xyz \noder wenden sie sich an support@kellerkinder.xyz -errorUpdateD = Beim ausf\u00FChren des Updates ist ein Fehler aufgetreten! \nError: could not download update files (ndf)\nWeitere Hilfe erhalten sie unter www.kellerkinder.xyz \noder wenden sie sich an support@kellerkinder.xyz -errorMode = Oh, da lief etwas falsch! Da hat jemand einen falschen Modus verwendet. \nError: mode unknow (muk)\nWeitere Hilfe erhalten sie unter www.kellerkinder.xyz \noder wenden sie sich an support@kellerkinder.xyz -errorOpenStream = Beim \u00F6ffnen des Streams ist ein Fehler aufgetreten! -errorLoad = Beim laden der Einstellungen ist ein Fehler aufgetreten! -errorSave = Beim speichern der Einstellungen ist ein Fehler aufgetreten! -noFilmFound = Kein Film mit diesem Titel gefunden! vlcNotInstalled = Um einen Film abspielen wird der VLC Media Player ben\u00F6tigt! infoText = \nAutoren: \n \u2022 seil0@kellerkinder.xyz \n \u2022 hendrik.schutter@coptersicht.de \n(c) 2016-2018 Kellerkinder www.kellerkinder.xyz diff --git a/src/main/resources/locals/HomeFlix-Local_en_US.properties b/src/main/resources/locals/HomeFlix-Local_en_US.properties index 8b16294..4080802 100644 --- a/src/main/resources/locals/HomeFlix-Local_en_US.properties +++ b/src/main/resources/locals/HomeFlix-Local_en_US.properties @@ -31,13 +31,6 @@ columnEpisode = Episode columnFavorite = Favorite #error translations -errorUpdateV = An error has occurred during update! \nError: could not check update version (nvc) \nTo get help, visit www.kellerkinder.xyz \nor contcat support@kellerkinder.xyz -errorUpdateD = An error has occurred during update! \nError: could not download update files (ndf) \nTo get help, visit www.kellerkinder.xyz \nor contcat support@kellerkinder.xyz -errorMode = Oh, something went wrong! It seems someone has used a wrong mode. \nError: mode unknow (muk) \nTo get help, visit www.kellerkinder.xyz \nor contcat support@kellerkinder.xyz -errorOpenStream = An error has occurred during opening the stream! -errorLoad = An error occurred while loading the settings! -errorSave = An error occurred while saving the settings! -noFilmFound = No film with this title found! vlcNotInstalled = VLC Media Player is required to play a movie! infoText = \nMaintainers: \n \u2022 seil0@kellerkinder.xyz \n \u2022 hendrik.schutter@coptersicht.de \n(c) 2016-2018 Kellerkinder www.kellerkinder.xyz From 74e87830825f15be666fd740d5393df0141a8d90 Mon Sep 17 00:00:00 2001 From: Jannik Date: Sun, 29 Apr 2018 14:21:19 +0200 Subject: [PATCH 05/88] HomeFLix now shows the plot for each episode of a series * added episode plot support * fixed omdb api search not working correctly --- .../application/MainWindowController.java | 4 +- .../HomeFlix/controller/DBController.java | 193 +++++++------ .../controller/OMDbAPIController.java | 193 +++++++------ .../datatypes/OMDbAPIResponseDataType.java | 267 ++++++++++++++++++ .../locals/HomeFlix-Local_de_DE.properties | 10 +- .../locals/HomeFlix-Local_en_US.properties | 10 +- 6 files changed, 484 insertions(+), 193 deletions(-) create mode 100644 src/main/java/kellerkinder/HomeFlix/datatypes/OMDbAPIResponseDataType.java diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index 7945689..5e6b035 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -215,7 +215,7 @@ public class MainWindowController { private boolean autoplay = false; private final String version = "0.7.0"; - private final String buildNumber = "151"; + private final String buildNumber = "155"; private final String versionName = "toothless dragon"; private String btnStyle; private String color; @@ -482,7 +482,7 @@ public class MainWindowController { last = indexTable - 1; next = indexTable + 1; - if (currentTableFilm.getCached() || dbController.searchCache(getCurrentStreamUrl())) { + if (currentTableFilm.getCached() || dbController.searchCacheByURL(getCurrentStreamUrl())) { LOGGER.info("loading from cache: " + getCurrentTitle()); dbController.readCache(getCurrentStreamUrl()); } else { diff --git a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java index 495eb8a..4e04eed 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java @@ -42,6 +42,8 @@ import com.eclipsesource.json.JsonArray; import com.eclipsesource.json.JsonObject; import com.eclipsesource.json.JsonValue; +import javafx.collections.ObservableList; +import javafx.scene.Node; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.text.Font; @@ -51,6 +53,7 @@ import kellerkinder.HomeFlix.application.Main; import kellerkinder.HomeFlix.application.MainWindowController; import kellerkinder.HomeFlix.datatypes.SourceDataType; import kellerkinder.HomeFlix.datatypes.FilmTabelDataType; +import kellerkinder.HomeFlix.datatypes.OMDbAPIResponseDataType; public class DBController { @@ -112,14 +115,14 @@ public class DBController { * films table: streamUrl is primary key * cache table: streamUrl is primary key */ - private void createDatabase() { + private void createDatabase() { try { Statement stmt = connection.createStatement(); stmt.executeUpdate("create table if not exists films (streamUrl, title, season, episode, favorite, cached, currentTime)"); stmt.executeUpdate("create table if not exists cache (" - + "streamUrl, Title, Year, Rated, Released, Runtime, Genre, Director, Writer," - + " Actors, Plot, Language, Country, Awards, Metascore, imdbRating, imdbVotes," - + " imdbID, Type, Poster, Response)"); + + "streamUrl, Title, Year, Rated, Released, Season, Episode ,Runtime, Genre, Director, Writer," + + " Actors, Plot, Language, Country, Awards, Poster, Metascore, imdbRating, imdbVotes," + + " imdbID, Type, dvd, BoxOffice, Website, Response)"); stmt.close(); } catch (SQLException e) { LOGGER.error(e); @@ -481,62 +484,45 @@ public class DBController { /** * add the received data to the cache table - * @param streamUrl URL of the film - * @param Title - * @param Year - * @param Rated - * @param Released - * @param Runtime - * @param Genre - * @param Director - * @param Writer - * @param Actors - * @param Plot - * @param Language - * @param Country - * @param Awards - * @param Metascore - * @param imdbRating - * @param Type - * @param imdbVotes - * @param imdbID - * @param Poster - * @param Response - * @throws SQLException + * @param streamUrl URL of the film + * @param omdbResponse the response data from omdbAPI */ - void addCache( String streamUrl, String Title, String Year, String Rated, String Released, String Runtime, String Genre, String Director, - String Writer, String Actors, String Plot, String Language, String Country, String Awards, String Metascore, String imdbRating, - String Type, String imdbVotes, String imdbID, String Poster, String Response) { + void addCache(String streamUrl, OMDbAPIResponseDataType omdbResponse) { try { - PreparedStatement ps = connection.prepareStatement("insert into cache values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"); + PreparedStatement ps = connection.prepareStatement("insert into cache values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"); - LOGGER.info("adding to cache: " + Title); + LOGGER.info("adding to cache: " + omdbResponse.getTitle()); ps.setString(1,streamUrl); - ps.setString(2,Title); - ps.setString(3,Year); - ps.setString(4,Rated); - ps.setString(5,Released); - ps.setString(6,Runtime); - ps.setString(7,Genre); - ps.setString(8,Director); - ps.setString(9,Writer); - ps.setString(10,Actors); - ps.setString(11,Plot); - ps.setString(12,Language); - ps.setString(13,Country); - ps.setString(14,Awards); - ps.setString(15,Metascore); - ps.setString(16,imdbRating); - ps.setString(17,imdbVotes); - ps.setString(18,imdbID); - ps.setString(19,Type); - ps.setString(20,Poster); - ps.setString(21,Response); + ps.setString(2,omdbResponse.getTitle()); + ps.setString(3,omdbResponse.getYear()); + ps.setString(4,omdbResponse.getRated()); + ps.setString(5,omdbResponse.getReleased()); + ps.setString(6,omdbResponse.getSeason()); + ps.setString(7,omdbResponse.getEpisode()); + ps.setString(8,omdbResponse.getRuntime()); + ps.setString(9,omdbResponse.getGenre()); + ps.setString(10,omdbResponse.getDirector()); + ps.setString(11,omdbResponse.getWriter()); + ps.setString(12,omdbResponse.getActors()); + ps.setString(13,omdbResponse.getPlot()); + ps.setString(14,omdbResponse.getLanguage()); + ps.setString(15,omdbResponse.getCountry()); + ps.setString(16,omdbResponse.getAwards()); + ps.setString(17,omdbResponse.getPoster()); + ps.setString(18,omdbResponse.getMetascore()); + ps.setString(19,omdbResponse.getImdbRating()); + ps.setString(20,omdbResponse.getImdbVotes()); + ps.setString(21,omdbResponse.getImdbID()); + ps.setString(22,omdbResponse.getType()); + ps.setString(23,omdbResponse.getDvd()); + ps.setString(24,omdbResponse.getBoxOffice()); + ps.setString(25,omdbResponse.getWebsite()); + ps.setString(26,omdbResponse.getResponse()); + ps.addBatch(); ps.executeBatch(); connection.commit(); ps.close(); - LOGGER.info("done!"); } catch (Exception e) { LOGGER.error(e); } @@ -547,7 +533,7 @@ public class DBController { * @param streamUrl URL of the element * @return true if the element is already cached, else false */ - public boolean searchCache(String streamUrl) { + public boolean searchCacheByURL(String streamUrl) { boolean retValue = false; try { Statement stmt = connection.createStatement(); @@ -568,62 +554,73 @@ public class DBController { */ public void readCache(String streamUrl) { try { - Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM cache WHERE streamUrl=\"" + streamUrl + "\";"); + Font font = Font.font("System", FontWeight.BOLD, (int) Math.round(mainWindowController.getFontSize())); + ObservableList textFlow = mainWindowController.getTextFlow().getChildren(); ArrayList nameText = new ArrayList(); - ArrayList responseText = new ArrayList(); - Image im; - int fontSize = (int) Math.round(mainWindowController.getFontSize()); - int j = 2; - - nameText.add(0, new Text(mainWindowController.getBundle().getString("title") + ": ")); - nameText.add(1, new Text(mainWindowController.getBundle().getString("year") + ": ")); - nameText.add(2, new Text(mainWindowController.getBundle().getString("rating") + ": ")); - nameText.add(3, new Text(mainWindowController.getBundle().getString("publishedOn") + ": ")); - nameText.add(4, new Text(mainWindowController.getBundle().getString("duration") + ": ")); - nameText.add(5, new Text(mainWindowController.getBundle().getString("genre") + ": ")); - nameText.add(6, new Text(mainWindowController.getBundle().getString("director") + ": ")); - nameText.add(7, new Text(mainWindowController.getBundle().getString("writer") + ": ")); - nameText.add(8, new Text(mainWindowController.getBundle().getString("actors") + ": ")); - nameText.add(9, new Text(mainWindowController.getBundle().getString("plot") + ": ")); - nameText.add(10, new Text(mainWindowController.getBundle().getString("language") + ": ")); - nameText.add(11, new Text(mainWindowController.getBundle().getString("country") + ": ")); - nameText.add(12, new Text(mainWindowController.getBundle().getString("awards") + ": ")); - nameText.add(13, new Text(mainWindowController.getBundle().getString("metascore") + ": ")); - nameText.add(14, new Text(mainWindowController.getBundle().getString("imdbRating") + ": ")); - nameText.add(15, new Text(mainWindowController.getBundle().getString("type") + ": ")); - for (int i = 0; i < 15; i++) { - responseText.add(new Text(rs.getString(j) + "\n")); - j++; + nameText.add(new Text(mainWindowController.getBundle().getString("title") + ": ")); + nameText.add(new Text(mainWindowController.getBundle().getString("year") + ": ")); + nameText.add(new Text(mainWindowController.getBundle().getString("rated") + ": ")); + nameText.add(new Text(mainWindowController.getBundle().getString("released") + ": ")); + nameText.add(new Text(mainWindowController.getBundle().getString("season") + ": ")); + nameText.add(new Text(mainWindowController.getBundle().getString("episode") + ": ")); + nameText.add(new Text(mainWindowController.getBundle().getString("runtime") + ": ")); + nameText.add(new Text(mainWindowController.getBundle().getString("genre") + ": ")); + nameText.add(new Text(mainWindowController.getBundle().getString("director") + ": ")); + nameText.add(new Text(mainWindowController.getBundle().getString("writer") + ": ")); + nameText.add(new Text(mainWindowController.getBundle().getString("actors") + ": ")); + nameText.add(new Text(mainWindowController.getBundle().getString("plot") + ": ")); + nameText.add(new Text(mainWindowController.getBundle().getString("language") + ": ")); + nameText.add(new Text(mainWindowController.getBundle().getString("country") + ": ")); + nameText.add(new Text(mainWindowController.getBundle().getString("awards") + ": ")); + nameText.add(new Text(mainWindowController.getBundle().getString("metascore") + ": ")); + nameText.add(new Text(mainWindowController.getBundle().getString("imdbRating") + ": ")); + nameText.add(new Text(mainWindowController.getBundle().getString("type") + ": ")); + nameText.add(new Text(mainWindowController.getBundle().getString("boxOffice") + ": ")); + nameText.add(new Text(mainWindowController.getBundle().getString("website") + ": ")); + + // set the correct font for the nameText + for (Text text : nameText) { + text.setFont(font); } - responseText.add(new Text(rs.getString(19) + "\n")); - im = new Image(new File(rs.getString(20)).toURI().toString()); - - stmt.close(); - rs.close(); - - for (int i = 0; i < nameText.size(); i++) { - nameText.get(i).setFont(Font.font("System", FontWeight.BOLD, fontSize)); - responseText.get(i).setFont(Font.font("System", fontSize)); - } - - mainWindowController.getTextFlow().getChildren().remove(0, - mainWindowController.getTextFlow().getChildren().size()); - - for (int i = 0; i < nameText.size(); i++) { - mainWindowController.getTextFlow().getChildren().addAll(nameText.get(i), responseText.get(i)); - } - + // clear the textFlow and all the new text + textFlow.clear(); + textFlow.addAll(nameText.get(0), new Text(rs.getString("Title") + "\n")); + textFlow.addAll(nameText.get(1), new Text(rs.getString("Year") + "\n")); + textFlow.addAll(nameText.get(2), new Text(rs.getString("Rated") + "\n")); + textFlow.addAll(nameText.get(3), new Text(rs.getString("Released") + "\n")); + textFlow.addAll(nameText.get(4), new Text(rs.getString("Season") + "\n")); + textFlow.addAll(nameText.get(5), new Text(rs.getString("Episode") + "\n")); + textFlow.addAll(nameText.get(6), new Text(rs.getString("Runtime") + "\n")); + textFlow.addAll(nameText.get(7), new Text(rs.getString("Genre") + "\n")); + textFlow.addAll(nameText.get(8), new Text(rs.getString("Director") + "\n")); + textFlow.addAll(nameText.get(9), new Text(rs.getString("Writer") + "\n")); + textFlow.addAll(nameText.get(10), new Text(rs.getString("Actors") + "\n")); + textFlow.addAll(nameText.get(11), new Text(rs.getString("Plot") + "\n")); + textFlow.addAll(nameText.get(12), new Text(rs.getString("Language") + "\n")); + textFlow.addAll(nameText.get(13), new Text(rs.getString("Country") + "\n")); + textFlow.addAll(nameText.get(14), new Text(rs.getString("Awards") + "\n")); + textFlow.addAll(nameText.get(15), new Text(rs.getString("metascore") + "\n")); + textFlow.addAll(nameText.get(16), new Text(rs.getString("imdbRating") + "\n")); + textFlow.addAll(nameText.get(17), new Text(rs.getString("Type") + "\n")); + textFlow.addAll(nameText.get(18), new Text(rs.getString("BoxOffice") + "\n")); + textFlow.addAll(nameText.get(19), new Text(rs.getString("Website") + "\n")); + + mainWindowController.getTextFlow().setStyle("-fx-font-size : " + ((int) Math.round(mainWindowController.getFontSize()) + 1) + "px;"); + + // add the image try { - mainWindowController.getPosterImageView().setImage(im); + mainWindowController.getPosterImageView().setImage(new Image(new File(rs.getString("Poster")).toURI().toString())); } catch (Exception e) { mainWindowController.getPosterImageView().setImage(new Image("resources/icons/close_black_2048x2048.png")); LOGGER.error(e); } - + + stmt.close(); + rs.close(); } catch (SQLException e) { LOGGER.error("Ups! an error occured!", e); } diff --git a/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java b/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java index bc4db09..17760bf 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java @@ -39,13 +39,13 @@ import com.eclipsesource.json.JsonValue; import javafx.application.Platform; import kellerkinder.HomeFlix.application.Main; import kellerkinder.HomeFlix.application.MainWindowController; +import kellerkinder.HomeFlix.datatypes.OMDbAPIResponseDataType; public class OMDbAPIController implements Runnable { private MainWindowController mainWindowController; private DBController dbController; private Main main; - private String[] responseString = new String[20]; private String URL = "https://www.omdbapi.com/?apikey="; private static final Logger LOGGER = LogManager.getLogger(MainWindowController.class.getName()); @@ -65,106 +65,125 @@ public class OMDbAPIController implements Runnable { @Override public void run() { - String output = null; - String posterPath = null; + JsonObject object; + object = getByTitle(mainWindowController.getCurrentTitle()); + if (object == null) return; - // get information by title - try { - URL apiUrl = new URL(URL + mainWindowController.getOmdbAPIKey() + "&t=" - + mainWindowController.getCurrentTitle().replace(" ", "%20")); - BufferedReader ina = new BufferedReader(new InputStreamReader(apiUrl.openStream())); - output = ina.readLine(); - ina.close(); - System.out.println(apiUrl); - LOGGER.info("response from '" + URL + "&t=" + mainWindowController.getCurrentTitle() + "' was:" + output); - } catch (IOException e) { - LOGGER.error("error while making api request or reading response"); - LOGGER.error("response from '" + URL + "&t=" + mainWindowController.getCurrentTitle() + "' was:" + output, e); - return; - } - - JsonObject object = Json.parse(output).asObject(); - - if (object.getString("Error", "").equals("Movie not found!")) { - // if the movie was not found try to search it - LOGGER.warn("Movie was not found at first try, searching again!"); - /** TODO - * split the name intelligent as it may contain the film title - * search for English name - * use tmdb - */ - try { - URL apiUrl = new URL(URL + mainWindowController.getOmdbAPIKey() + "&s=" - + mainWindowController.getCurrentTitle().replace(" ", "%20")); - BufferedReader ina = new BufferedReader(new InputStreamReader(apiUrl.openStream())); - output = ina.readLine(); - ina.close(); - LOGGER.info("response from '" + URL + "&s=" + mainWindowController.getCurrentTitle() + "' was:" + output); - } catch (Exception e) { - LOGGER.error("error while making api request or reading response"); - LOGGER.error("response from '" + URL + "&s=" + mainWindowController.getCurrentTitle() + "' was:" + output, e); - return; - } - - JsonObject searchObject = Json.parse(output).asObject(); - if (searchObject.getString("Response", "").equals("True")) { - for (JsonValue movie : searchObject.get("Search").asArray()) { - // get first entry from the array and set object = movie - object = (JsonObject) movie; - System.out.println(movie.toString()); - break; - - } - System.out.println(object.getString("Title", "")); + if (object.getString("Error", "").contains("not found!")) { + String title = searchByTitle(mainWindowController.getCurrentTitle()); + if (title.length() > 0) { + object = getByTitle(title); } else { - LOGGER.warn("Movie not found! Not adding cache!"); return; } } - // add the response to the responseString[] - responseString[0] = object.getString("Title", ""); - responseString[1] = object.getString("Year", ""); - responseString[2] = object.getString("Rated", ""); - responseString[3] = object.getString("Released", ""); - responseString[4] = object.getString("Runtime", ""); - responseString[5] = object.getString("Genre", ""); - responseString[6] = object.getString("Director", ""); - responseString[7] = object.getString("Writer", ""); - responseString[8] = object.getString("Actors", ""); - responseString[9] = object.getString("Plot", ""); - responseString[10] = object.getString("Language", ""); - responseString[11] = object.getString("Country", ""); - responseString[12] = object.getString("Awards", ""); - responseString[13] = object.getString("Metascore", ""); - responseString[14] = object.getString("imdbRating", ""); - responseString[15] = object.getString("Type", ""); - responseString[16] = object.getString("imdbVotes", ""); - responseString[17] = object.getString("imdbID", ""); - responseString[18] = object.getString("Poster", ""); - responseString[19] = object.getString("Response", ""); - - //resize the image to fit in the posterImageView and add it to the cache - try { - BufferedImage originalImage = ImageIO.read(new URL(responseString[18])); //change path to where file is located - posterPath = main.getPosterCache() + "/" + mainWindowController.getCurrentTitle() + ".png"; - ImageIO.write(originalImage, "png", new File(posterPath)); - LOGGER.info("adding poster to cache: "+posterPath); + OMDbAPIResponseDataType omdbResponse = new OMDbAPIResponseDataType(); + omdbResponse.setTitle(object.getString("Title", "")); + omdbResponse.setYear(object.getString("Year", "")); + omdbResponse.setRated(object.getString("Rated", "")); + omdbResponse.setReleased(object.getString("Release", "")); + omdbResponse.setSeason(object.getString("Season", "")); + omdbResponse.setEpisode(object.getString("Episode", "")); + omdbResponse.setRuntime(object.getString("Runtime", "")); + omdbResponse.setGenre(object.getString("Genre", "")); + omdbResponse.setDirector(object.getString("Director", "")); + omdbResponse.setWriter(object.getString("Writer", "")); + omdbResponse.setActors(object.getString("Actors", "")); + omdbResponse.setPlot(object.getString("Plot", "")); + omdbResponse.setLanguage(object.getString("Language", "")); + omdbResponse.setCountry(object.getString("Country", "")); + omdbResponse.setAwards(object.getString("Awards", "")); + omdbResponse.setMetascore(object.getString("Metascore", "")); + omdbResponse.setImdbRating(object.getString("imdbRating", "")); + omdbResponse.setImdbVotes(object.getString("imdbVotes", "")); + omdbResponse.setImdbID(object.getString("imdbID", "")); + omdbResponse.setType(object.getString("Type", "")); + omdbResponse.setDvd(object.getString("DVD", "")); + omdbResponse.setBoxOffice(object.getString("BoxOffice", "")); + omdbResponse.setProduction(object.getString("Production", "")); + omdbResponse.setWebsite(object.getString("Website", "")); + omdbResponse.setResponse(object.getString("Response", "")); + + // resize the image to fit in the posterImageView and add it to the cache + try { + BufferedImage originalImage = ImageIO.read(new URL(object.getString("Poster", ""))); + // change path to where file is located + omdbResponse.setPoster(main.getPosterCache() + "/" + mainWindowController.getCurrentTitle() + ".png"); + ImageIO.write(originalImage, "png", new File(omdbResponse.getPoster())); + LOGGER.info("adding poster to cache: " + omdbResponse.getPoster()); } catch (Exception e) { LOGGER.error(e); } - - // adding strings to the cache - dbController.addCache(mainWindowController.getCurrentStreamUrl(), responseString[0], responseString[1], - responseString[2], responseString[3], responseString[4], responseString[5], responseString[6], - responseString[7], responseString[8], responseString[9], responseString[10], responseString[11], - responseString[12], responseString[13], responseString[14], responseString[15], responseString[16], - responseString[17], posterPath, responseString[19]); + + // adding to cache + dbController.addCache(mainWindowController.getCurrentStreamUrl(), omdbResponse); dbController.setCached(mainWindowController.getCurrentStreamUrl()); // load data to the MainWindowController Platform.runLater(() -> { dbController.readCache(mainWindowController.getCurrentStreamUrl()); }); + + return; + } + + private JsonObject getByTitle(String title) { + String output = null; + URL apiUrl; + try { + if (mainWindowController.getCurrentTableFilm().getSeason().length() > 0) { + apiUrl = new URL(URL + mainWindowController.getOmdbAPIKey() + "&t=" + + title.replace(" ", "%20") + + "&Season=" + mainWindowController.getCurrentTableFilm().getSeason() + + "&Episode=" + mainWindowController.getCurrentTableFilm().getEpisode()); + } else { + apiUrl = new URL(URL + mainWindowController.getOmdbAPIKey() + "&t=" + + title.replace(" ", "%20")); + } + + BufferedReader ina = new BufferedReader(new InputStreamReader(apiUrl.openStream())); + output = ina.readLine(); + ina.close(); + LOGGER.info("response from '" + URL + "&t=" + title + "' was:" + output); + } catch (IOException e) { + LOGGER.error("error while making api request or reading response"); + LOGGER.error("response from '" + URL + "&t=" + title + "' was:" + output, e); + return null; + } + + return Json.parse(output).asObject(); + } + + private String searchByTitle(String title) { + String output = null; + // if the movie was not found try to search it + LOGGER.warn("Movie was not found at first try, searching again!"); + /** + * TODO split the name intelligent as it may contain the film title search for + * English name use tmdb + */ + try { + URL apiUrl = new URL(URL + mainWindowController.getOmdbAPIKey() + "&s=" + title.replace(" ", "%20")); + BufferedReader ina = new BufferedReader(new InputStreamReader(apiUrl.openStream())); + output = ina.readLine(); + ina.close(); + LOGGER.info("response from '" + URL + "&s=" + title + "' was:" + output); + } catch (Exception e) { + LOGGER.error("error while making api request or reading response"); + LOGGER.error("response from '" + URL + "&s=" + title + "' was:" + output, e); + return ""; + } + + JsonObject searchObject = Json.parse(output).asObject(); + if (searchObject.getString("Response", "").equals("True")) { + for (JsonValue movie : searchObject.get("Search").asArray()) { + // get first entry from the array and set object = movie + return movie.asObject().getString("Title", ""); + } + } else { + LOGGER.warn("Movie not found! Not adding cache!"); + } + return ""; } } diff --git a/src/main/java/kellerkinder/HomeFlix/datatypes/OMDbAPIResponseDataType.java b/src/main/java/kellerkinder/HomeFlix/datatypes/OMDbAPIResponseDataType.java new file mode 100644 index 0000000..7a2efcb --- /dev/null +++ b/src/main/java/kellerkinder/HomeFlix/datatypes/OMDbAPIResponseDataType.java @@ -0,0 +1,267 @@ +/** + * Project-HomeFlix + * + * Copyright 2018 <@Seil0> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + */ +package kellerkinder.HomeFlix.datatypes; + +public class OMDbAPIResponseDataType { + + private String title; + private String year; + private String rated; + private String released; + private String season; + private String episode; + private String runtime; + private String genre; + private String director; + private String writer; + private String actors; + private String plot; + private String language; + private String country; + private String awards; + private String poster; + private String metascore; + private String imdbRating; + private String imdbVotes; + private String imdbID; + private String type; + private String dvd; + private String boxOffice; + private String production; + private String website; + private String response; + + /** + * OMDbAPIResponseDataType is the data-type for the omdbAPI response + */ + public OMDbAPIResponseDataType() { + // Auto-generated constructor stub + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getYear() { + return year; + } + + public void setYear(String year) { + this.year = year; + } + + public String getRated() { + return rated; + } + + public void setRated(String rated) { + this.rated = rated; + } + + public String getReleased() { + return released; + } + + public void setReleased(String released) { + this.released = released; + } + + public String getSeason() { + return season; + } + + public void setSeason(String season) { + this.season = season; + } + + public String getEpisode() { + return episode; + } + + public void setEpisode(String episode) { + this.episode = episode; + } + + public String getRuntime() { + return runtime; + } + + public void setRuntime(String runtime) { + this.runtime = runtime; + } + + public String getGenre() { + return genre; + } + + public void setGenre(String genre) { + this.genre = genre; + } + + public String getDirector() { + return director; + } + + public void setDirector(String director) { + this.director = director; + } + + public String getWriter() { + return writer; + } + + public void setWriter(String writer) { + this.writer = writer; + } + + public String getActors() { + return actors; + } + + public void setActors(String actors) { + this.actors = actors; + } + + public String getPlot() { + return plot; + } + + public void setPlot(String plot) { + this.plot = plot; + } + + public String getLanguage() { + return language; + } + + public void setLanguage(String language) { + this.language = language; + } + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + public String getAwards() { + return awards; + } + + public void setAwards(String awards) { + this.awards = awards; + } + + public String getPoster() { + return poster; + } + + public void setPoster(String poster) { + this.poster = poster; + } + + public String getMetascore() { + return metascore; + } + + public void setMetascore(String metascore) { + this.metascore = metascore; + } + + public String getImdbRating() { + return imdbRating; + } + + public void setImdbRating(String imdbRating) { + this.imdbRating = imdbRating; + } + + public String getImdbVotes() { + return imdbVotes; + } + + public void setImdbVotes(String imdbVotes) { + this.imdbVotes = imdbVotes; + } + + public String getImdbID() { + return imdbID; + } + + public void setImdbID(String imdbID) { + this.imdbID = imdbID; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getDvd() { + return dvd; + } + + public void setDvd(String dvd) { + this.dvd = dvd; + } + + public String getBoxOffice() { + return boxOffice; + } + + public void setBoxOffice(String boxOffice) { + this.boxOffice = boxOffice; + } + + public String getProduction() { + return production; + } + + public void setProduction(String production) { + this.production = production; + } + + public String getWebsite() { + return website; + } + + public void setWebsite(String website) { + this.website = website; + } + + public String getResponse() { + return response; + } + + public void setResponse(String response) { + this.response = response; + } +} diff --git a/src/main/resources/locals/HomeFlix-Local_de_DE.properties b/src/main/resources/locals/HomeFlix-Local_de_DE.properties index 93e9015..a991b31 100644 --- a/src/main/resources/locals/HomeFlix-Local_de_DE.properties +++ b/src/main/resources/locals/HomeFlix-Local_de_DE.properties @@ -37,9 +37,11 @@ infoText = \nAutoren: \n \u2022 seil0@kellerkinder.xyz \n \u2022 hendrik.schutte #textFlow translations title = Titel year = Jahr -rating = Einstufung -publishedOn = Ver\u00F6ffentlicht am -duration = Laufzeit +rated = Einstufung +released = Ver\u00F6ffentlicht am +season = Staffel +episode = Episode +runtime = Laufzeit genre = Gener director = Regisseur writer = Autor @@ -51,6 +53,8 @@ awards = Auszeichnungen metascore = Metascore imdbRating = IMDB-Bewertung type = Type +boxOffice = BoxOffice +website = Webseite #first start addSourceHeader = Neue Quelle hinzuf\u00FCgen diff --git a/src/main/resources/locals/HomeFlix-Local_en_US.properties b/src/main/resources/locals/HomeFlix-Local_en_US.properties index 4080802..807a661 100644 --- a/src/main/resources/locals/HomeFlix-Local_en_US.properties +++ b/src/main/resources/locals/HomeFlix-Local_en_US.properties @@ -37,9 +37,11 @@ infoText = \nMaintainers: \n \u2022 seil0@kellerkinder.xyz \n \u2022 hendrik.sch #textFlow translations title = Title year = Year -rating = Rating -publishedOn = published on -duration = Duration +rated = Rating +released = published on +season = Season +episode = Episode +runtime = Duration genre = Gener director = Director writer = Writer @@ -51,6 +53,8 @@ awards = Awards metascore = Metascore imdbRating = IMDB-Rating type = Type +boxOffice = BoxOffice +website = Website #first start addSourceHeader = add a new source From 48b9c99baadb687b1ae993f705e45f6bc65d9253 Mon Sep 17 00:00:00 2001 From: Jannik Date: Sun, 29 Apr 2018 14:28:12 +0200 Subject: [PATCH 06/88] more TODO --- .../kellerkinder/HomeFlix/controller/OMDbAPIController.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java b/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java index 17760bf..82b7a46 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java @@ -65,6 +65,7 @@ public class OMDbAPIController implements Runnable { @Override public void run() { + LOGGER.info("Querying omdbAPI ..."); JsonObject object; object = getByTitle(mainWindowController.getCurrentTitle()); if (object == null) return; @@ -179,6 +180,7 @@ public class OMDbAPIController implements Runnable { if (searchObject.getString("Response", "").equals("True")) { for (JsonValue movie : searchObject.get("Search").asArray()) { // get first entry from the array and set object = movie + // TODO if the search was successful, we should add the wrong and correct title to a list, for later speedup return movie.asObject().getString("Title", ""); } } else { From 402a004ef61f23ec3733119ad034550fedcf513e Mon Sep 17 00:00:00 2001 From: Jannik Date: Fri, 4 May 2018 18:53:51 +0200 Subject: [PATCH 07/88] jfoenix 9.0.3 -> 9.0.4 --- pom.xml | 2 +- .../kellerkinder/HomeFlix/controller/DBController.java | 4 ++-- .../HomeFlix/controller/OMDbAPIController.java | 8 +++++++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 2887ba2..dd5cf57 100644 --- a/pom.xml +++ b/pom.xml @@ -32,7 +32,7 @@ com.jfoenix jfoenix - 9.0.3 + 9.0.4 diff --git a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java index 4e04eed..cc8e0de 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java @@ -615,8 +615,8 @@ public class DBController { try { mainWindowController.getPosterImageView().setImage(new Image(new File(rs.getString("Poster")).toURI().toString())); } catch (Exception e) { - mainWindowController.getPosterImageView().setImage(new Image("resources/icons/close_black_2048x2048.png")); - LOGGER.error(e); + mainWindowController.getPosterImageView().setImage(new Image("icons/close_black_2048x2048.png")); + LOGGER.error("No Poster found, useing default."); } stmt.close(); diff --git a/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java b/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java index 82b7a46..b662ed2 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java @@ -110,7 +110,7 @@ public class OMDbAPIController implements Runnable { try { BufferedImage originalImage = ImageIO.read(new URL(object.getString("Poster", ""))); // change path to where file is located - omdbResponse.setPoster(main.getPosterCache() + "/" + mainWindowController.getCurrentTitle() + ".png"); + omdbResponse.setPoster(main.getPosterCache() + "/" + omdbResponse.getTitle() + ".png"); ImageIO.write(originalImage, "png", new File(omdbResponse.getPoster())); LOGGER.info("adding poster to cache: " + omdbResponse.getPoster()); } catch (Exception e) { @@ -146,6 +146,7 @@ public class OMDbAPIController implements Runnable { BufferedReader ina = new BufferedReader(new InputStreamReader(apiUrl.openStream())); output = ina.readLine(); ina.close(); + System.out.println(apiUrl.toString()); LOGGER.info("response from '" + URL + "&t=" + title + "' was:" + output); } catch (IOException e) { LOGGER.error("error while making api request or reading response"); @@ -156,6 +157,11 @@ public class OMDbAPIController implements Runnable { return Json.parse(output).asObject(); } + /** TODO if responser == false & isSereis, query without series + * search for a movie/series title + * @param title the movie/series title + * @return the correct title if found + */ private String searchByTitle(String title) { String output = null; // if the movie was not found try to search it From 46efa77fceacb8f4d4db3c0613309a35f5a40350 Mon Sep 17 00:00:00 2001 From: Jannik Date: Wed, 9 May 2018 22:40:14 +0200 Subject: [PATCH 08/88] omdb fixes * if there is no episode for a series in the omdb, use the regular series description * show episode and season only if needed --- .../kellerkinder/HomeFlix/controller/DBController.java | 8 ++++++-- .../HomeFlix/controller/OMDbAPIController.java | 10 ++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java index cc8e0de..7cc5df2 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java @@ -592,8 +592,12 @@ public class DBController { textFlow.addAll(nameText.get(1), new Text(rs.getString("Year") + "\n")); textFlow.addAll(nameText.get(2), new Text(rs.getString("Rated") + "\n")); textFlow.addAll(nameText.get(3), new Text(rs.getString("Released") + "\n")); - textFlow.addAll(nameText.get(4), new Text(rs.getString("Season") + "\n")); - textFlow.addAll(nameText.get(5), new Text(rs.getString("Episode") + "\n")); + + if (rs.getString("Episode").length() > 0) { + textFlow.addAll(nameText.get(4), new Text(rs.getString("Season") + "\n")); + textFlow.addAll(nameText.get(5), new Text(rs.getString("Episode") + "\n")); + } + textFlow.addAll(nameText.get(6), new Text(rs.getString("Runtime") + "\n")); textFlow.addAll(nameText.get(7), new Text(rs.getString("Genre") + "\n")); textFlow.addAll(nameText.get(8), new Text(rs.getString("Director") + "\n")); diff --git a/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java b/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java index b662ed2..0fc48a9 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java @@ -47,6 +47,7 @@ public class OMDbAPIController implements Runnable { private DBController dbController; private Main main; private String URL = "https://www.omdbapi.com/?apikey="; + private boolean useEpisode = true; private static final Logger LOGGER = LogManager.getLogger(MainWindowController.class.getName()); /** @@ -74,6 +75,11 @@ public class OMDbAPIController implements Runnable { String title = searchByTitle(mainWindowController.getCurrentTitle()); if (title.length() > 0) { object = getByTitle(title); + + if(object.getString("Error", "").contains("Series or episode not found!")) { + useEpisode = false; + object = getByTitle(title); + } } else { return; } @@ -133,7 +139,7 @@ public class OMDbAPIController implements Runnable { String output = null; URL apiUrl; try { - if (mainWindowController.getCurrentTableFilm().getSeason().length() > 0) { + if (mainWindowController.getCurrentTableFilm().getSeason().length() > 0 && useEpisode) { apiUrl = new URL(URL + mainWindowController.getOmdbAPIKey() + "&t=" + title.replace(" ", "%20") + "&Season=" + mainWindowController.getCurrentTableFilm().getSeason() @@ -157,7 +163,7 @@ public class OMDbAPIController implements Runnable { return Json.parse(output).asObject(); } - /** TODO if responser == false & isSereis, query without series + /** * search for a movie/series title * @param title the movie/series title * @return the correct title if found From 1e0ab2b94391cea3005748c7e3c473a11e2297c9 Mon Sep 17 00:00:00 2001 From: Jannik Date: Wed, 9 May 2018 22:55:32 +0200 Subject: [PATCH 09/88] code cleanup --- .../application/MainWindowController.java | 31 +++++++------------ .../HomeFlix/controller/DBController.java | 2 +- .../controller/OMDbAPIController.java | 1 - 3 files changed, 12 insertions(+), 22 deletions(-) diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index 5e6b035..583aabe 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -215,7 +215,7 @@ public class MainWindowController { private boolean autoplay = false; private final String version = "0.7.0"; - private final String buildNumber = "155"; + private final String buildNumber = "157"; private final String versionName = "toothless dragon"; private String btnStyle; private String color; @@ -236,12 +236,6 @@ public class MainWindowController { private ObservableList filterData = FXCollections.observableArrayList(); private ObservableList filmsList = FXCollections.observableArrayList(); private ObservableList sourcesList = FXCollections.observableArrayList(); - private ImageView skip_previous_white = new ImageView(new Image("icons/ic_skip_previous_white_18dp_1x.png")); - private ImageView skip_previous_black = new ImageView(new Image("icons/ic_skip_previous_black_18dp_1x.png")); - private ImageView skip_next_white = new ImageView(new Image("icons/ic_skip_next_white_18dp_1x.png")); - private ImageView skip_next_black = new ImageView(new Image("icons/ic_skip_next_black_18dp_1x.png")); - private ImageView play_arrow_white = new ImageView(new Image("icons/ic_play_arrow_white_18dp_1x.png")); - private ImageView play_arrow_black = new ImageView(new Image("icons/ic_play_arrow_black_18dp_1x.png")); private MenuItem like = new MenuItem("like"); private MenuItem dislike = new MenuItem("dislike"); // TODO one option (like or dislike) private ContextMenu menu = new ContextMenu(like, dislike); @@ -408,7 +402,6 @@ public class MainWindowController { if (!getCurrentTitle().isEmpty()) { dbController.readCache(getCurrentStreamUrl()); } - // ta1.setFont(Font.font("System", size)); saveSettings(); } }); @@ -459,7 +452,7 @@ public class MainWindowController { } } - addDataUI(filterData); + addFilmsToTable(filterData); } }); @@ -646,7 +639,7 @@ public class MainWindowController { /** * add data from films-list to films-table */ - public void addDataUI(ObservableList elementsList) { + public void addFilmsToTable(ObservableList elementsList) { for (FilmTabelDataType element : elementsList) { @@ -713,28 +706,26 @@ public class MainWindowController { */ private void applyColor() { String menuBtnStyle; - String btnStyleBlack = "-fx-button-type: RAISED; -fx-background-color: #" + getColor() + "; -fx-text-fill: BLACK;"; - String btnStyleWhite = "-fx-button-type: RAISED; -fx-background-color: #" + getColor() + "; -fx-text-fill: WHITE;"; BigInteger usedColor = new BigInteger(getColor(), 16); BigInteger checkColor = new BigInteger("78909cff", 16); if (usedColor.compareTo(checkColor) == -1) { - btnStyle = btnStyleWhite; + btnStyle = "-fx-button-type: RAISED; -fx-background-color: #" + getColor() + "; -fx-text-fill: WHITE;"; menuBtnStyle = "-fx-text-fill: WHITE;"; - playbtn.setGraphic(play_arrow_white); - returnBtn.setGraphic(skip_previous_white); - forwardBtn.setGraphic(skip_next_white); + playbtn.setGraphic(new ImageView(new Image("icons/ic_play_arrow_white_18dp_1x.png"))); + returnBtn.setGraphic(new ImageView(new Image("icons/ic_skip_previous_white_18dp_1x.png"))); + forwardBtn.setGraphic(new ImageView(new Image("icons/ic_skip_next_white_18dp_1x.png"))); menuHam.getStyleClass().clear(); menuHam.getStyleClass().add("jfx-hamburgerW"); } else { - btnStyle = btnStyleBlack; + btnStyle = "-fx-button-type: RAISED; -fx-background-color: #" + getColor() + "; -fx-text-fill: BLACK;"; menuBtnStyle = "-fx-text-fill: BLACK;"; - playbtn.setGraphic(play_arrow_black); - returnBtn.setGraphic(skip_previous_black); - forwardBtn.setGraphic(skip_next_black); + playbtn.setGraphic(new ImageView(new Image("icons/ic_play_arrow_black_18dp_1x.png"))); + returnBtn.setGraphic(new ImageView(new Image("icons/ic_skip_previous_black_18dp_1x.png"))); + forwardBtn.setGraphic(new ImageView(new Image("icons/ic_skip_next_black_18dp_1x.png"))); menuHam.getStyleClass().clear(); menuHam.getStyleClass().add("jfx-hamburgerB"); diff --git a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java index 7cc5df2..be39a93 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java @@ -235,7 +235,7 @@ public class DBController { } LOGGER.info("loading data to the GUI ..."); - mainWindowController.addDataUI(mainWindowController.getFilmsList()); + mainWindowController.addFilmsToTable(mainWindowController.getFilmsList()); } /** diff --git a/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java b/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java index 0fc48a9..de203c7 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java @@ -152,7 +152,6 @@ public class OMDbAPIController implements Runnable { BufferedReader ina = new BufferedReader(new InputStreamReader(apiUrl.openStream())); output = ina.readLine(); ina.close(); - System.out.println(apiUrl.toString()); LOGGER.info("response from '" + URL + "&t=" + title + "' was:" + output); } catch (IOException e) { LOGGER.error("error while making api request or reading response"); From d7ed7c7b21fd7c312d7b62634a8faa49d54d7dbb Mon Sep 17 00:00:00 2001 From: Jannik Date: Wed, 16 May 2018 14:57:55 +0200 Subject: [PATCH 10/88] target java 10 --- .classpath | 3 +-- .settings/org.eclipse.jdt.core.prefs | 6 +++--- .travis.yml | 2 +- pom.xml | 4 ++-- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.classpath b/.classpath index d9ff1e3..56515cc 100644 --- a/.classpath +++ b/.classpath @@ -17,9 +17,8 @@ - + - diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs index 6f1d295..bbe505d 100644 --- a/.settings/org.eclipse.jdt.core.prefs +++ b/.settings/org.eclipse.jdt.core.prefs @@ -1,13 +1,13 @@ eclipse.preferences.version=1 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate -org.eclipse.jdt.core.compiler.codegen.targetPlatform=9 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=10 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.compliance=9 +org.eclipse.jdt.core.compiler.compliance=10 org.eclipse.jdt.core.compiler.debug.lineNumber=generate org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.enumIdentifier=error org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning -org.eclipse.jdt.core.compiler.source=9 +org.eclipse.jdt.core.compiler.source=10 diff --git a/.travis.yml b/.travis.yml index 78fa8d1..937c21f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,3 @@ language: java jdk: - - oraclejdk9 + - oraclejdk10 diff --git a/pom.xml b/pom.xml index dd5cf57..00391f9 100644 --- a/pom.xml +++ b/pom.xml @@ -68,8 +68,8 @@ maven-compiler-plugin 3.7.0 - 9 - 9 + 10 + 10 true true From 95775083119b95785fbe3fef9024d24902014748 Mon Sep 17 00:00:00 2001 From: Jannik Date: Wed, 16 May 2018 15:02:33 +0200 Subject: [PATCH 11/88] openjdk10 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 937c21f..6fb36db 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,3 @@ language: java jdk: - - oraclejdk10 + - openjdk10 From 311577fe7c5ecc24337967a4d824500e00cb506a Mon Sep 17 00:00:00 2001 From: Jannik Date: Wed, 16 May 2018 15:04:10 +0200 Subject: [PATCH 12/88] openjdk9 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 6fb36db..87ad79c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,3 @@ language: java jdk: - - openjdk10 + - openjdk9 From e1ec3cae7e9b9c889d7ed701c0384607bdcc4743 Mon Sep 17 00:00:00 2001 From: Jannik Date: Wed, 16 May 2018 15:08:46 +0200 Subject: [PATCH 13/88] travis oraclejdk9 & openjdk8 --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 87ad79c..0d18c3c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ language: java jdk: - - openjdk9 + - oraclejdk9 + - openjdk8 From 26f26ec9f804c95a1c6798bed93cc17473e25a62 Mon Sep 17 00:00:00 2001 From: Jannik Date: Wed, 16 May 2018 15:12:18 +0200 Subject: [PATCH 14/88] target java 9 until travis offers jdk10 --- .travis.yml | 1 - pom.xml | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0d18c3c..78fa8d1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,3 @@ language: java jdk: - oraclejdk9 - - openjdk8 diff --git a/pom.xml b/pom.xml index 00391f9..dd5cf57 100644 --- a/pom.xml +++ b/pom.xml @@ -68,8 +68,8 @@ maven-compiler-plugin 3.7.0 - 10 - 10 + 9 + 9 true true From 036ed8ad1198d903b208ad17ae25bf8d546c577e Mon Sep 17 00:00:00 2001 From: Jannik Date: Wed, 16 May 2018 23:02:07 +0200 Subject: [PATCH 15/88] DBController cleanup --- .classpath | 2 +- .settings/org.eclipse.jdt.core.prefs | 6 +-- .../HomeFlix/controller/DBController.java | 53 +++++++------------ 3 files changed, 23 insertions(+), 38 deletions(-) diff --git a/.classpath b/.classpath index 56515cc..430e97d 100644 --- a/.classpath +++ b/.classpath @@ -17,7 +17,7 @@ - + diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs index bbe505d..6f1d295 100644 --- a/.settings/org.eclipse.jdt.core.prefs +++ b/.settings/org.eclipse.jdt.core.prefs @@ -1,13 +1,13 @@ eclipse.preferences.version=1 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate -org.eclipse.jdt.core.compiler.codegen.targetPlatform=10 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=9 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.compliance=10 +org.eclipse.jdt.core.compiler.compliance=9 org.eclipse.jdt.core.compiler.debug.lineNumber=generate org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.enumIdentifier=error org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning -org.eclipse.jdt.core.compiler.source=10 +org.eclipse.jdt.core.compiler.source=9 diff --git a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java index be39a93..02edc09 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java @@ -51,21 +51,19 @@ import javafx.scene.text.FontWeight; import javafx.scene.text.Text; import kellerkinder.HomeFlix.application.Main; import kellerkinder.HomeFlix.application.MainWindowController; -import kellerkinder.HomeFlix.datatypes.SourceDataType; import kellerkinder.HomeFlix.datatypes.FilmTabelDataType; import kellerkinder.HomeFlix.datatypes.OMDbAPIResponseDataType; +import kellerkinder.HomeFlix.datatypes.SourceDataType; public class DBController { private MainWindowController mainWindowController; private Main main; - private String DB_PATH = System.getProperty("user.home") + "\\Documents\\HomeFlix" + "\\" + "Homeflix.db"; //path to database file + private String DB_PATH; private Image favorite_black = new Image("icons/ic_favorite_black_18dp_1x.png"); private Image favorite_border_black = new Image("icons/ic_favorite_border_black_18dp_1x.png"); - private List filmsdbAll = new ArrayList(); - private List filmsdbDir = new ArrayList(); - private List filmsdbStreamURL = new ArrayList(); // needed - private List filmsStreamURL = new ArrayList(); // needed + private List filmsdbStreamURL = new ArrayList(); // contains all films stored in the database + private List filmsStreamURL = new ArrayList(); // contains all films from the sources private Connection connection = null; private static final Logger LOGGER = LogManager.getLogger(DBController.class.getName()); @@ -74,7 +72,7 @@ public class DBController { * @param main the Main object * @param mainWindowController the MainWindowController object */ - public DBController(Main main, MainWindowController mainWindowController) { + public DBController(Main main, MainWindowController mainWindowController) { this.main = main; this.mainWindowController = mainWindowController; } @@ -138,7 +136,6 @@ public class DBController { Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM films"); while (rs.next()) { - filmsdbDir.add(rs.getString("title")); filmsdbStreamURL.add(rs.getString("streamUrl")); } stmt.close(); @@ -146,11 +143,6 @@ public class DBController { } catch (SQLException e) { LOGGER.error("Ups! an error occured!", e); } - - // add all entries to filmsAll and filmsdbAl, for later comparing - filmsdbAll.addAll(filmsdbDir); - LOGGER.info("films in directory: " + filmsStreamURL.size()); - LOGGER.info("filme in db: " + filmsdbStreamURL.size()); } /** @@ -210,7 +202,7 @@ public class DBController { * load the data to the mainWindowController * order entries by title */ - private void loadDataToMWC() { + private void loadDataToFilmsList() { LOGGER.info("loading data to mwc ..."); try { //load local Data @@ -276,14 +268,15 @@ public class DBController { LOGGER.info("refreshing the Database ..."); // clean all ArraLists - filmsdbAll.clear(); - filmsdbDir.clear(); filmsdbStreamURL.clear(); filmsStreamURL.clear(); loadSources(); // reload all sources loadDatabase(); // reload all films saved in the DB + LOGGER.info("films in directory: " + filmsStreamURL.size()); + LOGGER.info("filme in db: " + filmsdbStreamURL.size()); + try { checkAddEntry(); checkRemoveEntry(); @@ -291,11 +284,11 @@ public class DBController { LOGGER.error("Error while refreshing the database", e); } - // remove all films from the mwc lists - mainWindowController.getFilmsList().removeAll(mainWindowController.getFilmsList()); - mainWindowController.getFilmRoot().getChildren().removeAll(mainWindowController.getFilmRoot().getChildren()); + // clear the FilmsList and FilmRoot chlidren + mainWindowController.getFilmsList().clear(); + mainWindowController.getFilmRoot().getChildren().clear(); - loadDataToMWC(); // load the new data to the mwc + loadDataToFilmsList(); // load the new data to the FilmsList } /** @@ -469,7 +462,7 @@ public class DBController { * update the database entry for the given film, cached = 1 * @param streamUrl URL of the film */ - void setCached(String streamUrl) { + public void setCached(String streamUrl) { try { Statement stmt = connection.createStatement(); stmt.executeUpdate("UPDATE films SET cached=1 WHERE streamUrl=\"" + streamUrl + "\";"); @@ -487,7 +480,7 @@ public class DBController { * @param streamUrl URL of the film * @param omdbResponse the response data from omdbAPI */ - void addCache(String streamUrl, OMDbAPIResponseDataType omdbResponse) { + public void addCache(String streamUrl, OMDbAPIResponseDataType omdbResponse) { try { PreparedStatement ps = connection.prepareStatement("insert into cache values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"); @@ -688,17 +681,14 @@ public class DBController { if (rsEpisode > episode && rsEpisode < nextEpisode) { // fitting episode found in current season, if rsEpisode < nextEpisode -> nextEpisode = rsEpisode nextEpisode = rsEpisode; - System.out.println("next episode is: " + nextEpisode); - // favorite image is black nextFilm = new FilmTabelDataType(rs.getString("streamUrl"), rs.getString("title"), rs.getString("season"), rs.getString("episode"), rs.getBoolean("favorite"), - rs.getBoolean("cached"), new ImageView(favorite_black)); + rs.getBoolean("cached"), new ImageView()); } } if (nextFilm == null) { int nextSeason = 3000; - System.out.println("searching next season"); rs = stmt.executeQuery("SELECT * FROM films WHERE title = \"" + title + "\";"); while(rs.next()) { int rsSeason = Integer.parseInt(rs.getString("season")); @@ -708,18 +698,15 @@ public class DBController { } if (nextSeason != 3000) { - System.out.println("next season is: " + nextSeason); rs = stmt.executeQuery("SELECT * FROM films WHERE title = \"" + title + "\" AND season = \"" + season + "\";"); while(rs.next()) { int rsEpisode = Integer.parseInt(rs.getString("episode")); if (rsEpisode > episode && rsEpisode < nextEpisode) { // fitting episode found in current season, if rsEpisode < nextEpisode -> nextEpisode = rsEpisode nextEpisode = rsEpisode; - System.out.println("next episode is: " + nextEpisode); - // favorite image is black nextFilm = new FilmTabelDataType(rs.getString("streamUrl"), rs.getString("title"), rs.getString("season"), rs.getString("episode"), rs.getBoolean("favorite"), - rs.getBoolean("cached"), new ImageView(favorite_black)); + rs.getBoolean("cached"), new ImageView()); } } } @@ -746,16 +733,14 @@ public class DBController { Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM films WHERE title = \"" + title + "\";"); while (rs.next()) { - // favorite image is black nextFilm = new FilmTabelDataType(rs.getString("streamUrl"), rs.getString("title"), rs.getString("season"), rs.getString("episode"), rs.getBoolean("favorite"), - rs.getBoolean("cached"), new ImageView(favorite_black)); + rs.getBoolean("cached"), new ImageView()); if (rs.getDouble("currentTime") > lastCurrentTime) { lastCurrentTime = rs.getDouble("currentTime"); - // favorite image is black nextFilm = new FilmTabelDataType(rs.getString("streamUrl"), rs.getString("title"), rs.getString("season"), rs.getString("episode"), rs.getBoolean("favorite"), - rs.getBoolean("cached"), new ImageView(favorite_black)); + rs.getBoolean("cached"), new ImageView()); break; } } From 1fca1c551d6ec8168a977d4d3b25b546f0e0b3c1 Mon Sep 17 00:00:00 2001 From: Jannik Date: Wed, 16 May 2018 23:05:59 +0200 Subject: [PATCH 16/88] more code cleanup --- .../HomeFlix/controller/UpdateController.java | 14 ++++++++------ .../HomeFlix/player/PlayerController.java | 2 -- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/kellerkinder/HomeFlix/controller/UpdateController.java b/src/main/java/kellerkinder/HomeFlix/controller/UpdateController.java index efa3ffe..f25981a 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/UpdateController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/UpdateController.java @@ -48,8 +48,10 @@ public class UpdateController implements Runnable { private String buildNumber; private String apiOutput; private String updateBuildNumber; // tag_name from Github -// private String updateName; -// private String updateChanges; + @SuppressWarnings("unused") + private String updateName; + @SuppressWarnings("unused") + private String updateChanges; private String browserDownloadUrl; // update download link private String githubApiRelease = "https://api.github.com/repos/Seil0/Project-HomeFlix/releases/latest"; private String githubApiBeta = "https://api.github.com/repos/Seil0/Project-HomeFlix/releases"; @@ -101,8 +103,8 @@ public class UpdateController implements Runnable { JsonArray objectAssets = object.asObject().get("assets").asArray(); updateBuildNumber = object.asObject().getString("tag_name", ""); -// updateName = object.asObject().getString("name", ""); -// updateChanges = object.asObject().getString("body", ""); + updateName = object.asObject().getString("name", ""); + updateChanges = object.asObject().getString("body", ""); for (JsonValue asset : objectAssets) { browserDownloadUrl = asset.asObject().getString("browser_download_url", ""); @@ -113,8 +115,8 @@ public class UpdateController implements Runnable { JsonArray objectAssets = Json.parse(apiOutput).asObject().get("assets").asArray(); updateBuildNumber = object.getString("tag_name", ""); -// updateName = object.getString("name", ""); -// updateChanges = object.getString("body", ""); + updateName = object.getString("name", ""); + updateChanges = object.getString("body", ""); for (JsonValue asset : objectAssets) { browserDownloadUrl = asset.asObject().getString("browser_download_url", ""); diff --git a/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java b/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java index 5cb2e31..52f2741 100644 --- a/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java +++ b/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java @@ -65,10 +65,8 @@ public class PlayerController { @FXML private JFXButton stopBtn; - @FXML private JFXButton playBtn; - @FXML private JFXButton fullscreenBtn; From 031046643c0397e79e47cdb8f783a43b9adf3dad Mon Sep 17 00:00:00 2001 From: Jannik Date: Thu, 17 May 2018 18:58:54 +0200 Subject: [PATCH 17/88] save&load are now in the main class --- .../HomeFlix/application/Main.java | 163 ++++++++++++++--- .../application/MainWindowController.java | 166 ++++-------------- src/main/resources/fxml/MainWindow.fxml | 2 +- 3 files changed, 170 insertions(+), 161 deletions(-) diff --git a/src/main/java/kellerkinder/HomeFlix/application/Main.java b/src/main/java/kellerkinder/HomeFlix/application/Main.java index 588952e..fbf0b2b 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/Main.java +++ b/src/main/java/kellerkinder/HomeFlix/application/Main.java @@ -21,15 +21,25 @@ */ package kellerkinder.HomeFlix.application; +import java.io.BufferedReader; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; import java.util.Locale; +import java.util.Properties; import java.util.ResourceBundle; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.kellerkinder.Alerts.JFX2BtnCancelAlert; +import com.eclipsesource.json.Json; +import com.eclipsesource.json.JsonObject; + import javafx.application.Application; import javafx.event.ActionEvent; import javafx.event.EventHandler; @@ -56,11 +66,12 @@ public class Main extends Application { private static String javaVend = System.getProperty("java.vendor"); private static String local = System.getProperty("user.language") + "_" + System.getProperty("user.country"); private static String dirHomeFlix; - private File directory; - private File configFile; - private File posterCache; + private static File directory; + private static File configFile; + private static File posterCache; private ResourceBundle bundle; private static Logger LOGGER; + private Properties props = new Properties(); @Override public void start(Stage primaryStage) throws IOException { @@ -69,6 +80,8 @@ public class Main extends Application { LOGGER.info("User: " + userName + " " + userHome); this.primaryStage = primaryStage; + mainWindowController = new MainWindowController(this); + mainWindow(); } @@ -80,6 +93,7 @@ public class Main extends Application { try { FXMLLoader loader = new FXMLLoader(); loader.setLocation(ClassLoader.getSystemResource("fxml/MainWindow.fxml")); + loader.setController(mainWindowController); pane = (AnchorPane) loader.load(); primaryStage.setMinHeight(600.00); primaryStage.setMinWidth(1000.00); @@ -87,21 +101,14 @@ public class Main extends Application { primaryStage.setTitle("Project HomeFlix"); primaryStage.getIcons().add(new Image(Main.class.getResourceAsStream("/icons/Homeflix_Icon_64x64.png"))); //adds application icon primaryStage.setOnCloseRequest(event -> System.exit(1)); - - mainWindowController = loader.getController(); //Link of FXMLController and controller class - mainWindowController.setMain(this); //call setMain - directory = new File(dirHomeFlix); - configFile = new File(dirHomeFlix + "/config.xml"); - posterCache = new File(dirHomeFlix + "/posterCache"); - // generate window scene = new Scene(pane); // create new scene, append pane to scene scene.getStylesheets().add(getClass().getResource("/css/MainWindow.css").toExternalForm()); primaryStage.setScene(scene); // append scene to stage primaryStage.show(); // show stage - // startup checks + // startup checks TODO move to mwc if (!configFile.exists()) { directory.mkdir(); @@ -110,21 +117,41 @@ public class Main extends Application { mainWindowController.setFontSize(17.0); mainWindowController.setAutoUpdate(false); mainWindowController.setLocal(local); - mainWindowController.saveSettings(); + saveSettings(); } if (!posterCache.exists()) { posterCache.mkdir(); } - - // initialize here as it loads the games to the mwc and the GUI, therefore the window must exist - mainWindowController.init(); - mainWindowController.getDbController().init(); } catch (IOException e) { LOGGER.error(e); } } + /** + * set the log file location and initialize the logger launch the GUI + * @param args arguments given at the start + */ + public static void main(String[] args) { + + if (osName.contains("Windows")) { + dirHomeFlix = userHome + "/Documents/HomeFlix"; + } else { + dirHomeFlix = userHome + "/HomeFlix"; + } + + // set the concrete files + directory = new File(dirHomeFlix); + configFile = new File(dirHomeFlix + "/config.xml"); + posterCache = new File(dirHomeFlix + "/posterCache"); + + System.setProperty("logFilename", dirHomeFlix + "/app.log"); + File logFile = new File(dirHomeFlix + "/app.log"); + logFile.delete(); + LOGGER = LogManager.getLogger(Main.class.getName()); + launch(args); + } + /** * we need to get the path for the first source from the user and add it to * sources.json, if the user ends the file-/directory-chooser the program will exit @@ -187,23 +214,101 @@ public class Main extends Application { } /** - * set the log file location and initialize the logger launch the GUI - * @param args arguments given at the start + * save the configuration to the config.xml file */ - public static void main(String[] args) { + public void saveSettings() { + LOGGER.info("saving settings ..."); + try { + props.setProperty("color", mainWindowController.getColor()); + props.setProperty("autoUpdate", String.valueOf(mainWindowController.isAutoUpdate())); + props.setProperty("useBeta", String.valueOf(mainWindowController.isUseBeta())); + props.setProperty("autoplay", String.valueOf(mainWindowController.isAutoplay())); + props.setProperty("size", mainWindowController.getFontSize().toString()); + props.setProperty("local", mainWindowController.getLocal()); - if (osName.contains("Windows")) { - dirHomeFlix = userHome + "/Documents/HomeFlix"; - } else { - dirHomeFlix = userHome + "/HomeFlix"; + OutputStream outputStream = new FileOutputStream(getConfigFile()); // new output-stream + props.storeToXML(outputStream, "Project HomeFlix settings"); // write new .xml + outputStream.close(); + } catch (IOException e) { + LOGGER.error("An error occurred while saving the settings!", e); } - - System.setProperty("logFilename", dirHomeFlix + "/app.log"); - File logFile = new File(dirHomeFlix + "/app.log"); - logFile.delete(); - LOGGER = LogManager.getLogger(Main.class.getName()); - launch(args); } + + /** + * load the configuration from the config.xml file + * and try to load the API keys from apiKeys.json + */ + public void loadSettings() { + LOGGER.info("loading settings ..."); + + try { + InputStream inputStream = new FileInputStream(getConfigFile()); + props.loadFromXML(inputStream); // new input-stream from .xml + + try { + mainWindowController.setColor(props.getProperty("color")); + } catch (Exception e) { + LOGGER.error("cloud not load color", e); + mainWindowController.setColor("00a8cc"); + } + + try { + mainWindowController.setFontSize(Double.parseDouble(props.getProperty("size"))); + } catch (Exception e) { + LOGGER.error("cloud not load fontsize", e); + mainWindowController.setFontSize(17.0); + } + + try { + mainWindowController.setAutoUpdate(Boolean.parseBoolean(props.getProperty("autoUpdate"))); + } catch (Exception e) { + LOGGER.error("cloud not load autoUpdate", e); + mainWindowController.setAutoUpdate(false); + } + + try { + mainWindowController.setUseBeta(Boolean.parseBoolean(props.getProperty("useBeta"))); + } catch (Exception e) { + LOGGER.error("cloud not load autoUpdate", e); + mainWindowController.setUseBeta(false); + } + + try { + mainWindowController.setAutoplay(Boolean.parseBoolean(props.getProperty("autoplay"))); + } catch (Exception e) { + LOGGER.error("cloud not load autoplay", e); + mainWindowController.setAutoplay(false); + } + + try { + mainWindowController.setLocal(props.getProperty("local")); + } catch (Exception e) { + LOGGER.error("cloud not load local", e); + mainWindowController.setLocal(System.getProperty("user.language") + "_" + System.getProperty("user.country")); + } + + inputStream.close(); + } catch (IOException e) { + LOGGER.error("An error occurred while loading the settings!", e); + } + + // try loading the omdbAPI key + try { + InputStream in = getClass().getClassLoader().getResourceAsStream("apiKeys.json"); + if (in != null) { + BufferedReader reader = new BufferedReader(new InputStreamReader(in)); + JsonObject apiKeys = Json.parse(reader).asObject(); + mainWindowController.setOmdbAPIKey(apiKeys.getString("omdbAPIKey", "")); + reader.close(); + in.close(); + } else { + LOGGER.warn("Cloud not load apiKeys.json. No such file"); + } + } catch (Exception e) { + LOGGER.error("Cloud not load the omdbAPI key. Please contact the developer!", e); + } + } + public Stage getPrimaryStage() { return primaryStage; diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index 583aabe..aa1d5cb 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -24,19 +24,14 @@ package kellerkinder.HomeFlix.application; import java.awt.Desktop; import java.io.BufferedReader; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; -import java.io.InputStream; import java.io.InputStreamReader; -import java.io.OutputStream; import java.io.Writer; import java.math.BigInteger; import java.net.URLConnection; import java.util.Locale; -import java.util.Properties; import java.util.ResourceBundle; import org.apache.logging.log4j.LogManager; @@ -239,27 +234,29 @@ public class MainWindowController { private MenuItem like = new MenuItem("like"); private MenuItem dislike = new MenuItem("dislike"); // TODO one option (like or dislike) private ContextMenu menu = new ContextMenu(like, dislike); - private Properties props = new Properties(); - + /** - * "Main" Method called in Main.java main() when starting * Initialize other objects: Updater, dbController and ApiQuery + * @param main the main object */ - void setMain(Main main) { + public MainWindowController(Main main) { this.main = main; mainWindowController = this; dbController = new DBController(this.main, this); omdbAPIController = new OMDbAPIController(this, dbController, this.main); } - // call all initialize methods - void init() { + @FXML + public void initialize() { LOGGER.info("Initializing Project-HomeFlix build " + buildNumber); - loadSettings(); + main.loadSettings(); // load settings checkAutoUpdate(); + + // initialize the GUI and the DBController initTabel(); initUI(); initActions(); + dbController.init(); } // Initialize UI elements @@ -328,20 +325,21 @@ public class MainWindowController { HamburgerBackArrowBasicTransition burgerTask = new HamburgerBackArrowBasicTransition(menuHam); menuHam.addEventHandler(MouseEvent.MOUSE_PRESSED, (e) -> { - if (menuTrue == false) { - sideMenuSlideIn(); - burgerTask.setRate(1.0); - burgerTask.play(); - menuTrue = true; - } else { + if (menuTrue) { sideMenuSlideOut(); burgerTask.setRate(-1.0); burgerTask.play(); menuTrue = false; + + } else { + sideMenuSlideIn(); + burgerTask.setRate(1.0); + burgerTask.play(); + menuTrue = true; } - if (settingsTrue == true) { + if (settingsTrue) { settingsScrollPane.setVisible(false); - saveSettings(); + main.saveSettings(); settingsTrue = false; } }); @@ -351,11 +349,10 @@ public class MainWindowController { public void changed(ObservableValue observable, String oldValue, String newValue) { ObservableList helpData; filterData.clear(); - filmRoot.getChildren().removeAll(filmRoot.getChildren()); + filmRoot.getChildren().clear(); helpData = filmsList; - for (int i = 0; i < helpData.size(); i++) { if (helpData.get(i).getTitle().toLowerCase().contains(searchTextField.getText().toLowerCase())) { filterData.add(helpData.get(i)); // add data from newDaten to filteredData where title contains search input @@ -379,7 +376,7 @@ public class MainWindowController { local = local.substring(local.length() - 6, local.length() - 1); // reading only en_US from English (en_US) setLocal(local); setLocalUI(); - saveSettings(); + main.saveSettings(); } }); @@ -391,7 +388,7 @@ public class MainWindowController { } else { setUseBeta(false); } - saveSettings(); + main.saveSettings(); } }); @@ -402,7 +399,7 @@ public class MainWindowController { if (!getCurrentTitle().isEmpty()) { dbController.readCache(getCurrentStreamUrl()); } - saveSettings(); + main.saveSettings(); } }); @@ -568,14 +565,14 @@ public class MainWindowController { } @FXML - private void settingsBtnclicked(){ - if(settingsTrue == false){ - settingsScrollPane.setVisible(true); - settingsTrue = true; - }else{ + private void settingsBtnclicked() { + if (settingsTrue) { settingsScrollPane.setVisible(false); - saveSettings(); + main.saveSettings(); settingsTrue = false; + } else { + settingsScrollPane.setVisible(true); + settingsTrue = true; } } @@ -607,7 +604,7 @@ public class MainWindowController { @FXML private void colorPickerAction() { setColor(colorPicker.getValue().toString().substring(2, 10)); - saveSettings(); + main.saveSettings(); applyColor(); } @@ -622,13 +619,13 @@ public class MainWindowController { @FXML private void autoUpdateToggleBtnAction(){ autoUpdate = isAutoUpdate() ? false : true; - saveSettings(); + main.saveSettings(); } @FXML private void autoplayToggleBtnAction(){ autoplay = isAutoplay() ? false : true; - saveSettings(); + main.saveSettings(); } // refresh the selected child of the root node @@ -806,103 +803,6 @@ public class MainWindowController { columnFavorite.setText(getBundle().getString("columnFavorite")); } - /** - * save the configuration to the config.xml file - */ - public void saveSettings() { - LOGGER.info("saving settings ..."); - try { - props.setProperty("color", getColor()); - props.setProperty("autoUpdate", String.valueOf(isAutoUpdate())); - props.setProperty("useBeta", String.valueOf(isUseBeta())); - props.setProperty("autoplay", String.valueOf(isAutoplay())); - props.setProperty("size", getFontSize().toString()); - props.setProperty("local", getLocal()); - props.setProperty("ratingSortType", columnFavorite.getSortType().toString()); - - OutputStream outputStream = new FileOutputStream(main.getConfigFile()); // new output-stream - props.storeToXML(outputStream, "Project HomeFlix settings"); // write new .xml - outputStream.close(); - } catch (IOException e) { - LOGGER.error("An error occurred while saving the settings!", e); - } - } - - /** - * load the configuration from the config.xml file - * and try to load the API keys from apiKeys.json - */ - public void loadSettings() { - LOGGER.info("loading settings ..."); - - try { - InputStream inputStream = new FileInputStream(main.getConfigFile()); - props.loadFromXML(inputStream); // new input-stream from .xml - - try { - setColor(props.getProperty("color")); - } catch (Exception e) { - LOGGER.error("cloud not load color", e); - setColor("00a8cc"); - } - - try { - setFontSize(Double.parseDouble(props.getProperty("size"))); - } catch (Exception e) { - LOGGER.error("cloud not load fontsize", e); - setFontSize(17.0); - } - - try { - setAutoUpdate(Boolean.parseBoolean(props.getProperty("autoUpdate"))); - } catch (Exception e) { - LOGGER.error("cloud not load autoUpdate", e); - setAutoUpdate(false); - } - - try { - setUseBeta(Boolean.parseBoolean(props.getProperty("useBeta"))); - } catch (Exception e) { - LOGGER.error("cloud not load autoUpdate", e); - setUseBeta(false); - } - - try { - setAutoplay(Boolean.parseBoolean(props.getProperty("autoplay"))); - } catch (Exception e) { - LOGGER.error("cloud not load autoplay", e); - setAutoplay(false); - } - - try { - setLocal(props.getProperty("local")); - } catch (Exception e) { - LOGGER.error("cloud not load local", e); - setLocal(System.getProperty("user.language") + "_" + System.getProperty("user.country")); - } - - inputStream.close(); - } catch (IOException e) { - LOGGER.error("An error occurred while loading the settings!", e); - } - - // try loading the omdbAPI key - try { - InputStream in = getClass().getClassLoader().getResourceAsStream("apiKeys.json"); - if (in != null) { - BufferedReader reader = new BufferedReader(new InputStreamReader(in)); - JsonObject apiKeys = Json.parse(reader).asObject(); - omdbAPIKey = apiKeys.getString("omdbAPIKey", ""); - reader.close(); - in.close(); - } else { - LOGGER.warn("Cloud not load apiKeys.json. No such file"); - } - } catch (Exception e) { - LOGGER.error("Cloud not load the omdbAPI key. Please contact the developer!", e); - } - } - // if AutoUpdate, then check for updates private void checkAutoUpdate() { @@ -1008,6 +908,10 @@ public class MainWindowController { public String getOmdbAPIKey() { return omdbAPIKey; } + + public void setOmdbAPIKey(String omdbAPIKey) { + this.omdbAPIKey = omdbAPIKey; + } public ObservableList getFilmsList() { return filmsList; diff --git a/src/main/resources/fxml/MainWindow.fxml b/src/main/resources/fxml/MainWindow.fxml index 45b120a..aecbae1 100644 --- a/src/main/resources/fxml/MainWindow.fxml +++ b/src/main/resources/fxml/MainWindow.fxml @@ -22,7 +22,7 @@ - + From 3840e318499c3f35c14eb5076817868042b44f54 Mon Sep 17 00:00:00 2001 From: Jannik Date: Wed, 30 May 2018 19:28:03 +0200 Subject: [PATCH 18/88] try to build with java 10 again --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 78fa8d1..2b01596 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ language: java jdk: - oraclejdk9 + - oraclejdk10 From 95d29d772c1732be2ffeba5249c96f5dff5997b1 Mon Sep 17 00:00:00 2001 From: Jannik Date: Wed, 30 May 2018 19:32:24 +0200 Subject: [PATCH 19/88] try openjdk build too --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 2b01596..d1533e2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,3 +2,5 @@ language: java jdk: - oraclejdk9 - oraclejdk10 + - openjdk9 + - openjdk10 From 4b1b20f81472653e1a6dd2c328fd1f2d25a1f81d Mon Sep 17 00:00:00 2001 From: Jannik Date: Wed, 30 May 2018 19:37:39 +0200 Subject: [PATCH 20/88] build against java 9/10/11 --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index d1533e2..e5af81d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,5 +2,4 @@ language: java jdk: - oraclejdk9 - oraclejdk10 - - openjdk9 - - openjdk10 + - oraclejdk11 From b1f9a6c5059d8a939529ad4ef2a173aaaa05141f Mon Sep 17 00:00:00 2001 From: Jannik Date: Wed, 30 May 2018 19:40:04 +0200 Subject: [PATCH 21/88] java 9/10 --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e5af81d..2b01596 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,4 +2,3 @@ language: java jdk: - oraclejdk9 - oraclejdk10 - - oraclejdk11 From 50ae1747e6440bf9556bbfb115bd9c0990813ff4 Mon Sep 17 00:00:00 2001 From: Jannik Date: Wed, 6 Jun 2018 17:54:52 +0200 Subject: [PATCH 22/88] update sqlite-jdbc 3.21.0.1 -> 3.23.1 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index dd5cf57..7980eb8 100644 --- a/pom.xml +++ b/pom.xml @@ -44,7 +44,7 @@ org.xerial sqlite-jdbc - 3.21.0.1 + 3.23.1 From 3b188904f6f072c6213bf943ba4caf6b84c6f3d6 Mon Sep 17 00:00:00 2001 From: Jannik Date: Tue, 12 Jun 2018 15:47:22 +0200 Subject: [PATCH 23/88] nothing to see here --- .../kellerkinder/HomeFlix/application/MainWindowController.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index aa1d5cb..56973ff 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -364,6 +364,7 @@ public class MainWindowController { } if (searchTextField.getText().hashCode() == hashA) { setColor("000000"); + colorPicker.setValue(new Color(0, 0, 0, 1)); applyColor(); } } From b603051c592565585cdb00fd9bd5340adf5e1b9e Mon Sep 17 00:00:00 2001 From: Jannik Date: Wed, 13 Jun 2018 13:50:21 +0200 Subject: [PATCH 24/88] typo --- .../HomeFlix/application/MainWindowController.java | 3 ++- src/main/resources/locals/HomeFlix-Local_de_DE.properties | 2 +- src/main/resources/locals/HomeFlix-Local_en_US.properties | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index 56973ff..2ddfcdb 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -432,6 +432,7 @@ public class MainWindowController { filterData.clear(); if (paramT2.equals(SortType.DESCENDING)) { + // add favorites at the top for (FilmTabelDataType film : filmsList) { if (film.getFavorite()) { filterData.add(0, film); @@ -440,7 +441,7 @@ public class MainWindowController { } } } else { -// System.out.println("ascending"); + // add favorites at the bottom for (FilmTabelDataType film : filmsList) { if (!film.getFavorite()) { filterData.add(0, film); diff --git a/src/main/resources/locals/HomeFlix-Local_de_DE.properties b/src/main/resources/locals/HomeFlix-Local_de_DE.properties index a991b31..736e40d 100644 --- a/src/main/resources/locals/HomeFlix-Local_de_DE.properties +++ b/src/main/resources/locals/HomeFlix-Local_de_DE.properties @@ -32,7 +32,7 @@ columnFavorite = Favorit #error translations vlcNotInstalled = Um einen Film abspielen wird der VLC Media Player ben\u00F6tigt! -infoText = \nAutoren: \n \u2022 seil0@kellerkinder.xyz \n \u2022 hendrik.schutter@coptersicht.de \n(c) 2016-2018 Kellerkinder www.kellerkinder.xyz +infoText = \nAutoren: \n \u2022 seil0@mosad.xyz \n \u2022 hendrik.schutter@coptersicht.de \n(c) 2016-2018 mosad www.mosad.xyz #textFlow translations title = Titel diff --git a/src/main/resources/locals/HomeFlix-Local_en_US.properties b/src/main/resources/locals/HomeFlix-Local_en_US.properties index 807a661..5fe7c1e 100644 --- a/src/main/resources/locals/HomeFlix-Local_en_US.properties +++ b/src/main/resources/locals/HomeFlix-Local_en_US.properties @@ -32,7 +32,7 @@ columnFavorite = Favorite #error translations vlcNotInstalled = VLC Media Player is required to play a movie! -infoText = \nMaintainers: \n \u2022 seil0@kellerkinder.xyz \n \u2022 hendrik.schutter@coptersicht.de \n(c) 2016-2018 Kellerkinder www.kellerkinder.xyz +infoText = \nMaintainers: \n \u2022 seil0@mosad.xyz \n \u2022 hendrik.schutter@coptersicht.de \n(c) 2016-2018 mosad www.mosad.xyz #textFlow translations title = Title From 67142662d8b27e3848f80e40cfc375556fa127de Mon Sep 17 00:00:00 2001 From: Seil0 Date: Wed, 13 Jun 2018 18:38:58 +0200 Subject: [PATCH 25/88] =?UTF-8?q?'README.md'=20=C3=A4ndern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 44f1590..5afa974 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,10 @@ Project-HomeFlix is a Kellerkinder Project, that alowes you to sort all your local saved movies in clean UI. Project-HomeFlix is free and open-source software and uses other open-source projects to provied a nice user experience. ## Installation -Simply download the Project-HomeFlix.jar from [releases](https://github.com/Seil0/Project-HomeFlix/releases), make sure you have the latest version of java 8 oracle jre/jdk installed, open the .jar file. If you need additional information pleas visit our [wiki](https://github.com/Seil0/Project-HomeFlix/wiki). +Simply download the Project-HomeFlix.jar from [releases](https://git.mosad.xyz/Seil0/Project-HomeFlix/releases), make sure you have the latest version of java 8 oracle jre/jdk installed, open the .jar file. If you need additional information pleas visit our [wiki](https://git.mosad.xyz/Seil0/Project-HomeFlix/wiki). ## Development information -The dev branch is **only merged** into master when a **new release** is released, so **master contains the latest released version**. Please commit all changes to [dev](https://github.com/Seil0/Project-HomeFlix/tree/dev). +The dev branch is **only merged** into master when a **new release** is released, so **master contains the latest released version**. Please commit all changes to [dev](https://git.mosad.xyz/Seil0/Project-HomeFlix/src/branch/dev). Librarys used in this Project: JFoenix: https://github.com/jfoenixadmin/JFoenix @@ -21,5 +21,4 @@ apache commons io : https://commons.apache.org/proper/commons-io/ ## screenshots ![Screenshot](https://github.com/Seil0/Seil0.github.io/blob/master/images/Project-HomeFlix_MainWindow.png) -Project-HomeFlix © 2016-2018 Kellerkinder ([Seil0](https://github.com/Seil0), [Windoofs](https://github.com/Windoofs)) -www.kellerkinder.xyz +Project-HomeFlix © 2016-2018 mosad www.mosad.xyz, Project by [@Seil0](https://git.mosad.xyz/Seil0) and [@localhorst](https://git.mosad.xyz/localhorst) \ No newline at end of file From 23ac33ca9b90ddd3c6565aae3770d13c5b32f8fa Mon Sep 17 00:00:00 2001 From: Seil0 Date: Mon, 18 Jun 2018 22:56:40 +0200 Subject: [PATCH 26/88] =?UTF-8?q?'README.md'=20=C3=A4ndern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 5afa974..f723ea0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,4 @@ -![Total Downloads](https://img.shields.io/github/downloads/Seil0/Project-HomeFlix/total.svg?style=flat-square) -[![](https://img.shields.io/travis/Seil0/Project-HomeFlix/dev.svg?style=flat-square)](https://travis-ci.org/Seil0/Project-HomeFlix) -[![Latest](https://img.shields.io/github/release/Seil0/Project-HomeFlix/all.svg?style=flat-square)](https://github.com/Seil0/Project-HomeFlix/releases) +[![Latest](https://img.shields.io/badge/Download-latest-brightgreen.svg?style=flat-square)](https://git.mosad.xyz/Seil0/Project-HomeFlix/releases) [![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg?style=flat-square)](https://www.gnu.org/licenses/gpl-3.0) # Project-HomeFlix From d16e724cce6663abf56b3d9d8a4453011a77a1d5 Mon Sep 17 00:00:00 2001 From: Jannik Date: Sun, 22 Jul 2018 23:30:52 +0200 Subject: [PATCH 27/88] documentation improvements --- .classpath | 1 + .settings/org.eclipse.jdt.core.prefs | 1 + pom.xml | 4 +- .../application/MainWindowController.java | 71 +++++++++++-------- .../controller/OMDbAPIController.java | 7 +- 5 files changed, 51 insertions(+), 33 deletions(-) diff --git a/.classpath b/.classpath index 430e97d..7e55b77 100644 --- a/.classpath +++ b/.classpath @@ -10,6 +10,7 @@ + diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs index 6f1d295..2978067 100644 --- a/.settings/org.eclipse.jdt.core.prefs +++ b/.settings/org.eclipse.jdt.core.prefs @@ -10,4 +10,5 @@ org.eclipse.jdt.core.compiler.debug.sourceFile=generate org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.enumIdentifier=error org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.release=disabled org.eclipse.jdt.core.compiler.source=9 diff --git a/pom.xml b/pom.xml index 7980eb8..cda8a69 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ jar Project-HomeFlix - http://www.kellerkinder.xyz + http://www.mosad.xyz UTF-8 @@ -32,7 +32,7 @@ com.jfoenix jfoenix - 9.0.4 + 9.0.5 diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index 2ddfcdb..ce82cf4 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -259,7 +259,7 @@ public class MainWindowController { dbController.init(); } - // Initialize UI elements + // Initialize general UI elements private void initUI() { versionLbl.setText("Version: " + version + " (Build: " + buildNumber + ")"); fontsizeSlider.setValue(getFontSize()); @@ -281,7 +281,10 @@ public class MainWindowController { applyColor(); } - // Initialize the tables (treeTableViewfilm and sourcesTable) + /** + * Initialize the tables (treeTableViewfilm and sourcesTable) + * only needed for Tabel-Mode + */ private void initTabel() { // film Table @@ -323,6 +326,7 @@ public class MainWindowController { // Initializing the actions private void initActions() { + // general actions HamburgerBackArrowBasicTransition burgerTask = new HamburgerBackArrowBasicTransition(menuHam); menuHam.addEventHandler(MouseEvent.MOUSE_PRESSED, (e) -> { if (menuTrue) { @@ -343,32 +347,6 @@ public class MainWindowController { settingsTrue = false; } }); - - searchTextField.textProperty().addListener(new ChangeListener() { - @Override - public void changed(ObservableValue observable, String oldValue, String newValue) { - ObservableList helpData; - filterData.clear(); - filmRoot.getChildren().clear(); - - helpData = filmsList; - - for (int i = 0; i < helpData.size(); i++) { - if (helpData.get(i).getTitle().toLowerCase().contains(searchTextField.getText().toLowerCase())) { - filterData.add(helpData.get(i)); // add data from newDaten to filteredData where title contains search input - } - } - - for (int i = 0; i < filterData.size(); i++) { - filmRoot.getChildren().add(new TreeItem(filterData.get(i))); // add filtered data to root node after search - } - if (searchTextField.getText().hashCode() == hashA) { - setColor("000000"); - colorPicker.setValue(new Color(0, 0, 0, 1)); - applyColor(); - } - } - }); languageChoisBox.getSelectionModel().selectedIndexProperty().addListener(new ChangeListener() { @Override @@ -404,6 +382,33 @@ public class MainWindowController { } }); + // Table-Mode actions + searchTextField.textProperty().addListener(new ChangeListener() { + @Override + public void changed(ObservableValue observable, String oldValue, String newValue) { + ObservableList helpData; + filterData.clear(); + filmRoot.getChildren().clear(); + + helpData = filmsList; + + for (int i = 0; i < helpData.size(); i++) { + if (helpData.get(i).getTitle().toLowerCase().contains(searchTextField.getText().toLowerCase())) { + filterData.add(helpData.get(i)); // add data from newDaten to filteredData where title contains search input + } + } + + for (int i = 0; i < filterData.size(); i++) { + filmRoot.getChildren().add(new TreeItem(filterData.get(i))); // add filtered data to root node after search + } + if (searchTextField.getText().hashCode() == hashA) { + setColor("000000"); + colorPicker.setValue(new Color(0, 0, 0, 1)); + applyColor(); + } + } + }); + like.setOnAction(new EventHandler() { @Override public void handle(ActionEvent event) { @@ -485,8 +490,11 @@ public class MainWindowController { } } }); + + // Poster-Mode actions } + // Table-Mode fxml actions @FXML private void playbtnclicked() { if (currentTableFilm.getStreamUrl().contains("_rootNode")) { @@ -558,6 +566,7 @@ public class MainWindowController { filmsTreeTable.getSelectionModel().select(next); } + // general fxml actions @FXML private void aboutBtnAction() { String bodyText = "cemu_UI by @Seil0 \nVersion: " + version + " (Build: " + buildNumber + ") \"" @@ -674,7 +683,11 @@ public class MainWindowController { sourceRoot.getChildren().add(new TreeItem(sourcesList.get(sourcesList.size() - 1))); // adds data to root-node } - // add a source to the newsources list + /** + * add a source to the newsources list + * @param path to the source + * @param mode of the source (local or streaming) + */ public void addSource(String path, String mode) { JsonObject source = null; JsonArray newsources = null; diff --git a/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java b/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java index de203c7..8e0f419 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java @@ -60,8 +60,6 @@ public class OMDbAPIController implements Runnable { this.mainWindowController = mainWindowController; this.dbController = dbController; this.main = main; - - } @Override @@ -135,6 +133,11 @@ public class OMDbAPIController implements Runnable { return; } + /** + * get a movie/series by its title + * @param title of the movie/series + * @return a jsonObject of the API answer + */ private JsonObject getByTitle(String title) { String output = null; URL apiUrl; From f68b0c0feb95cf8cb9c39586ae8d74c3e3015b3a Mon Sep 17 00:00:00 2001 From: Jannik Date: Tue, 24 Jul 2018 14:14:34 +0200 Subject: [PATCH 28/88] code clean up --- .../application/MainWindowController.java | 148 ++++++------------ 1 file changed, 52 insertions(+), 96 deletions(-) diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index ce82cf4..efc61c7 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -89,112 +89,68 @@ import kellerkinder.HomeFlix.datatypes.FilmTabelDataType; public class MainWindowController { - @FXML - private AnchorPane mainAnchorPane; - @FXML - private AnchorPane tableModeAnchorPane; + // general/settings fxml elements + @FXML private AnchorPane mainAnchorPane; + @FXML private ScrollPane settingsScrollPane; - @FXML - private ScrollPane settingsScrollPane; - @FXML - private ScrollPane textScrollPane; + @FXML private JFXHamburger menuHam; + @FXML private HBox topHBox; + @FXML private VBox sideMenuVBox; + @FXML private TableView sourcesTable; - @FXML - private HBox topHBox; + @FXML private JFXButton aboutBtn; + @FXML private JFXButton settingsBtn; + @FXML private JFXButton updateBtn; + @FXML private JFXButton addDirectoryBtn; + @FXML private JFXButton addStreamSourceBtn; - @FXML - private VBox sideMenuVBox; + @FXML private JFXToggleButton autoUpdateToggleBtn; + @FXML private JFXToggleButton autoplayToggleBtn; - @FXML - private TreeTableView filmsTreeTable; + @FXML private JFXColorPicker colorPicker; - @FXML - private TableView sourcesTable; + @FXML private ChoiceBox languageChoisBox = new ChoiceBox<>(); + @FXML private ChoiceBox branchChoisBox = new ChoiceBox<>(); - @FXML - private TextFlow textFlow; + @FXML private JFXSlider fontsizeSlider; - @FXML - private JFXButton playbtn; - @FXML - private JFXButton openfolderbtn; - @FXML - private JFXButton returnBtn; - @FXML - private JFXButton forwardBtn; - @FXML - private JFXButton aboutBtn; - @FXML - private JFXButton settingsBtn; - @FXML - private JFXButton updateBtn; - @FXML - private JFXButton addDirectoryBtn; - @FXML - private JFXButton addStreamSourceBtn; + @FXML private Label homeflixSettingsLbl; + @FXML private Label mainColorLbl; + @FXML private Label fontsizeLbl; + @FXML private Label languageLbl; + @FXML private Label updateLbl; + @FXML private Label branchLbl; + @FXML private Label sourcesLbl; + @FXML private Label versionLbl; - @FXML - private JFXHamburger menuHam; + @FXML private TreeItem sourceRoot = new TreeItem<>(new SourceDataType("", "")); + @FXML private TableColumn sourceColumn; + @FXML private TableColumn modeColumn; + + // table-mode + @FXML private AnchorPane tableModeAnchorPane; + @FXML private JFXTextField searchTextField; + + @FXML private TreeTableView filmsTreeTable; + @FXML private TreeItem filmRoot = new TreeItem<>(new FilmTabelDataType("", "", "", "", false, false, null)); + @FXML private TreeTableColumn columnStreamUrl = new TreeTableColumn<>("File Name"); + @FXML private TreeTableColumn columnTitle = new TreeTableColumn<>("Title"); + @FXML private TreeTableColumn columnSeason = new TreeTableColumn<>("Season"); + @FXML private TreeTableColumn columnEpisode = new TreeTableColumn<>("Episode"); + @FXML private TreeTableColumn columnFavorite = new TreeTableColumn<>("Favorite"); + + @FXML private ScrollPane textScrollPane; + @FXML private TextFlow textFlow; + @FXML private ImageView posterImageView; + + @FXML private JFXButton playbtn; + @FXML private JFXButton openfolderbtn; + @FXML private JFXButton returnBtn; + @FXML private JFXButton forwardBtn; - @FXML - private JFXToggleButton autoUpdateToggleBtn; - @FXML - private JFXToggleButton autoplayToggleBtn; + // poster-mode +// @FXML private AnchorPane posterModeAnchorPane; - @FXML - private JFXTextField searchTextField; - - @FXML - private JFXColorPicker colorPicker; - - @FXML - private ChoiceBox languageChoisBox = new ChoiceBox<>(); - @FXML - private ChoiceBox branchChoisBox = new ChoiceBox<>(); - - @FXML - private JFXSlider fontsizeSlider; - - @FXML - private Label homeflixSettingsLbl; - @FXML - private Label mainColorLbl; - @FXML - private Label fontsizeLbl; - @FXML - private Label languageLbl; - @FXML - private Label updateLbl; - @FXML - private Label branchLbl; - @FXML - private Label sourcesLbl; - @FXML - private Label versionLbl; - - @FXML - private ImageView posterImageView; - private ImageView imv1; - - @FXML - private TreeItem filmRoot = new TreeItem<>(new FilmTabelDataType("", "", "", "", false, false, imv1)); - @FXML - private TreeTableColumn columnStreamUrl = new TreeTableColumn<>("File Name"); - @FXML - private TreeTableColumn columnTitle = new TreeTableColumn<>("Title"); - @FXML - private TreeTableColumn columnSeason = new TreeTableColumn<>("Season"); - @FXML - private TreeTableColumn columnEpisode = new TreeTableColumn<>("Episode"); - @FXML - private TreeTableColumn columnFavorite = new TreeTableColumn<>("Favorite"); - - @FXML - private TreeItem sourceRoot = new TreeItem<>(new SourceDataType("", "")); - @FXML - private TableColumn sourceColumn; - @FXML - private TableColumn modeColumn; private Main main; private MainWindowController mainWindowController; From 814bb001584520967442ed5b21e198db799544e9 Mon Sep 17 00:00:00 2001 From: Jannik Date: Mon, 13 Aug 2018 23:56:16 +0200 Subject: [PATCH 29/88] added getAllNotCachedEntries() * added a method to get all not cached entries from the films db --- .../application/MainWindowController.java | 18 +++++++++++++ .../HomeFlix/controller/DBController.java | 25 ++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index efc61c7..7bacad9 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -213,6 +213,8 @@ public class MainWindowController { initUI(); initActions(); dbController.init(); + + checkAllPosters(); // TODO testing } // Initialize general UI elements @@ -801,6 +803,22 @@ public class MainWindowController { String mimeType = URLConnection.guessContentTypeFromName(film.getStreamUrl()); return mimeType != null && (mimeType.contains("mp4") || mimeType.contains("vp6")); } + + private void posterModeStartup() { + checkAllPosters(); + } + + /** + * check if all posters are cached, if not cache the missing ones + */ + private void checkAllPosters() { + // get all not cached entries, none of them should have a cached poster + for (FilmTabelDataType entry : dbController.getAllNotCachedEntries()) { + System.out.println(entry.getStreamUrl() + " is NOT cached!"); + // TODO get all needed posters eg cache all not cached entries + // TODO for entries not available show homeflix logo + } + } // getter and setter diff --git a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java index 02edc09..74e0d41 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java @@ -465,7 +465,7 @@ public class DBController { public void setCached(String streamUrl) { try { Statement stmt = connection.createStatement(); - stmt.executeUpdate("UPDATE films SET cached=1 WHERE streamUrl=\"" + streamUrl + "\";"); + stmt.executeUpdate("UPDATE films SET cached = 1 WHERE streamUrl = \"" + streamUrl + "\";"); connection.commit(); stmt.close(); } catch (SQLException e) { @@ -623,6 +623,29 @@ public class DBController { } } + /** + * get all NOT cached entries + * @return a {@link ArrayList} of all NOT cached entries + */ + public ArrayList getAllNotCachedEntries() { + ArrayList notCachedEntries = new ArrayList<>(); + + try { + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT * FROM films WHERE cached = 0"); + while (rs.next()) { + notCachedEntries.add(new FilmTabelDataType(rs.getString("streamUrl"), + rs.getString("title"), rs.getString("season"), rs.getString("episode"), rs.getBoolean("favorite"), + rs.getBoolean("cached"), new ImageView(favorite_border_black))); + } + stmt.close(); + rs.close(); + } catch (SQLException e) { + LOGGER.error("An error occured, while getting all NOT cached entries", e); + } + return notCachedEntries; + } + /** * return the currentTime in ms saved in the database * @param streamUrl URL of the film From fe1dc01605320b3874038bc22865fba28d361044 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Mon, 13 Aug 2018 23:59:02 +0200 Subject: [PATCH 30/88] code cleanup --- .../java/kellerkinder/HomeFlix/controller/DBController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java index 74e0d41..f17baa5 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java @@ -450,7 +450,7 @@ public class DBController { LOGGER.info("like " + streamUrl); try { Statement stmt = connection.createStatement(); - stmt.executeUpdate("UPDATE films SET favorite=1 WHERE streamUrl=\"" + streamUrl + "\";"); + stmt.executeUpdate("UPDATE films SET favorite = 1 WHERE streamUrl = \"" + streamUrl + "\";"); connection.commit(); stmt.close(); } catch (SQLException e) { From 8f6832c1472598eb6ab92503231a02ed1ef9b6c1 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Sat, 8 Sep 2018 13:16:43 +0200 Subject: [PATCH 31/88] updated some libraries * jfoenix 9.0.5 -> 9.0.6 * log4j 2.11.0 -> 2.11.1 * maven-compiler-plugin 3.7.0 -> 3.8.0 --- pom.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index cda8a69..72319b0 100644 --- a/pom.xml +++ b/pom.xml @@ -32,7 +32,7 @@ com.jfoenix jfoenix - 9.0.5 + 9.0.6 @@ -50,13 +50,13 @@ org.apache.logging.log4j log4j-api - 2.11.0 + 2.11.1 org.apache.logging.log4j log4j-core - 2.11.0 + 2.11.1 @@ -66,7 +66,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.7.0 + 3.8.0 9 9 From 76bb0fa8b1ea89481ab8579d0abbf603325ece57 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Fri, 21 Sep 2018 18:43:29 +0200 Subject: [PATCH 32/88] prepare java11 * this is not working with java11 yet --- .classpath | 5 +++-- .settings/org.eclipse.jdt.core.prefs | 8 ++++---- pom.xml | 24 +++++++++++++++++------- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/.classpath b/.classpath index 7e55b77..7f5ea8f 100644 --- a/.classpath +++ b/.classpath @@ -8,9 +8,9 @@ + - @@ -18,7 +18,7 @@ - + @@ -28,5 +28,6 @@ + diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs index 2978067..f472f69 100644 --- a/.settings/org.eclipse.jdt.core.prefs +++ b/.settings/org.eclipse.jdt.core.prefs @@ -1,14 +1,14 @@ eclipse.preferences.version=1 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate -org.eclipse.jdt.core.compiler.codegen.targetPlatform=9 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.compliance=9 +org.eclipse.jdt.core.compiler.compliance=11 org.eclipse.jdt.core.compiler.debug.lineNumber=generate org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.enumIdentifier=error org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning -org.eclipse.jdt.core.compiler.release=disabled -org.eclipse.jdt.core.compiler.source=9 +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=11 diff --git a/pom.xml b/pom.xml index 72319b0..5dc1686 100644 --- a/pom.xml +++ b/pom.xml @@ -15,14 +15,25 @@ - + - junit - junit - 4.12 - test + org.openjfx + javafx-controls + 11 + + org.openjfx + javafx-fxml + 11 + + + + org.openjfx + javafx-media + 11 + + commons-io commons-io @@ -68,8 +79,7 @@ maven-compiler-plugin 3.8.0 - 9 - 9 + 11 true true From 32fc533c00df09e129f3536960a6adf8e19d9d7d Mon Sep 17 00:00:00 2001 From: Seil0 Date: Fri, 21 Sep 2018 18:57:39 +0200 Subject: [PATCH 33/88] fixed junit --- .classpath | 2 +- .settings/org.eclipse.jdt.core.prefs | 6 +++--- pom.xml | 9 ++++++++- .../java/kellerkinder/HomeFlix/application/Main.java | 2 +- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.classpath b/.classpath index 7f5ea8f..51caf01 100644 --- a/.classpath +++ b/.classpath @@ -18,7 +18,7 @@ - + diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs index f472f69..54a32cd 100644 --- a/.settings/org.eclipse.jdt.core.prefs +++ b/.settings/org.eclipse.jdt.core.prefs @@ -1,9 +1,9 @@ eclipse.preferences.version=1 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate -org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=10 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.compliance=11 +org.eclipse.jdt.core.compiler.compliance=10 org.eclipse.jdt.core.compiler.debug.lineNumber=generate org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate @@ -11,4 +11,4 @@ org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.enumIdentifier=error org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning org.eclipse.jdt.core.compiler.release=enabled -org.eclipse.jdt.core.compiler.source=11 +org.eclipse.jdt.core.compiler.source=10 diff --git a/pom.xml b/pom.xml index 5dc1686..ca997ce 100644 --- a/pom.xml +++ b/pom.xml @@ -15,6 +15,13 @@ + + + junit + junit + 4.12 + test + org.openjfx @@ -79,7 +86,7 @@ maven-compiler-plugin 3.8.0 - 11 + 10 true true diff --git a/src/main/java/kellerkinder/HomeFlix/application/Main.java b/src/main/java/kellerkinder/HomeFlix/application/Main.java index fbf0b2b..81581f1 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/Main.java +++ b/src/main/java/kellerkinder/HomeFlix/application/Main.java @@ -329,4 +329,4 @@ public class Main extends Application { public File getPosterCache() { return posterCache; } -} \ No newline at end of file +} From 96bfc461cb53990eb30136cc74128f982ae71f27 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Fri, 21 Sep 2018 19:50:43 +0200 Subject: [PATCH 34/88] formation fixes --- pom.xml | 42 +++++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/pom.xml b/pom.xml index ca997ce..9ae5fb7 100644 --- a/pom.xml +++ b/pom.xml @@ -24,22 +24,22 @@ - org.openjfx - javafx-controls - 11 + org.openjfx + javafx-controls + 11 - org.openjfx - javafx-fxml - 11 - - - - org.openjfx - javafx-media - 11 - + org.openjfx + javafx-fxml + 11 + + + + org.openjfx + javafx-media + 11 + commons-io @@ -91,6 +91,22 @@ true + + + org.codehaus.mojo + exec-maven-plugin + 1.6.0 + + + + java + + + + + kellerkinder.HomeFlix.application.Main + + org.apache.maven.plugins From abbb272bff2742dc1b9ccff80005f903b4541173 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Sun, 23 Sep 2018 14:33:16 +0200 Subject: [PATCH 35/88] fixed java11 execution * use mvn compile exec:java for now to run HomeFlix * compilation is not possible yet * see #4 --- .classpath | 2 +- .settings/org.eclipse.jdt.core.prefs | 8 +- pom.xml | 258 +++++++++--------- .../HomeFlix/application/Main.java | 2 +- 4 files changed, 137 insertions(+), 133 deletions(-) diff --git a/.classpath b/.classpath index 51caf01..7f5ea8f 100644 --- a/.classpath +++ b/.classpath @@ -18,7 +18,7 @@ - + diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs index 54a32cd..b807efd 100644 --- a/.settings/org.eclipse.jdt.core.prefs +++ b/.settings/org.eclipse.jdt.core.prefs @@ -1,14 +1,14 @@ eclipse.preferences.version=1 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate -org.eclipse.jdt.core.compiler.codegen.targetPlatform=10 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.compliance=10 +org.eclipse.jdt.core.compiler.compliance=11 org.eclipse.jdt.core.compiler.debug.lineNumber=generate org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.enumIdentifier=error org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning -org.eclipse.jdt.core.compiler.release=enabled -org.eclipse.jdt.core.compiler.source=10 +org.eclipse.jdt.core.compiler.release=disabled +org.eclipse.jdt.core.compiler.source=11 diff --git a/pom.xml b/pom.xml index 9ae5fb7..071aa6a 100644 --- a/pom.xml +++ b/pom.xml @@ -1,137 +1,141 @@ - - 4.0.0 + + 4.0.0 - org.kellerkinder - Project-HomeFlix - 0.7.0 - jar + org.kellerkinder + Project-HomeFlix + 0.7.0 + jar - Project-HomeFlix - http://www.mosad.xyz + Project-HomeFlix + http://www.mosad.xyz - - UTF-8 - + + UTF-8 + - - - - junit - junit - 4.12 - test - - - - org.openjfx - javafx-controls - 11 - - - - org.openjfx - javafx-fxml - 11 - + - - org.openjfx - javafx-media - 11 - + + junit + junit + 4.12 + test + - - commons-io - commons-io - 2.6 - + + org.openjfx + javafx-controls + 11 + - - com.jfoenix - jfoenix - 9.0.6 - - - - com.eclipsesource.minimal-json - minimal-json - 0.9.5 - - - - org.xerial - sqlite-jdbc - 3.23.1 - - - - org.apache.logging.log4j - log4j-api - 2.11.1 - + + org.openjfx + javafx-fxml + 11 + - - org.apache.logging.log4j - log4j-core - 2.11.1 - - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.8.0 - - 10 - true - true - - - - - org.codehaus.mojo - exec-maven-plugin - 1.6.0 - - - - java - - - - - kellerkinder.HomeFlix.application.Main - - + + org.openjfx + javafx-media + 11 + + + + commons-io + commons-io + 2.6 + + + + com.jfoenix + jfoenix + 9.0.6 + + + + com.eclipsesource.minimal-json + minimal-json + 0.9.5 + + + + org.xerial + sqlite-jdbc + 3.23.1 + + + + org.apache.logging.log4j + log4j-api + 2.11.1 + + + + org.apache.logging.log4j + log4j-core + 2.11.1 + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + 11 + 11 + + true + true + + + + + org.codehaus.mojo + exec-maven-plugin + 1.6.0 + + + + java + + + + + kellerkinder.HomeFlix.application.Main + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.0 + + Project-HomeFlix + true + + + kellerkinder.HomeFlix.application.Main + + + + + + package + + shade + + + + + + + - - org.apache.maven.plugins - maven-shade-plugin - 3.1.1 - - Project-HomeFlix - true - - - kellerkinder.HomeFlix.application.Main - - - - - - package - - shade - - - - - - - - diff --git a/src/main/java/kellerkinder/HomeFlix/application/Main.java b/src/main/java/kellerkinder/HomeFlix/application/Main.java index 81581f1..1263763 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/Main.java +++ b/src/main/java/kellerkinder/HomeFlix/application/Main.java @@ -92,7 +92,7 @@ public class Main extends Application { private void mainWindow(){ try { FXMLLoader loader = new FXMLLoader(); - loader.setLocation(ClassLoader.getSystemResource("fxml/MainWindow.fxml")); + loader.setLocation(getClass().getResource("/fxml/MainWindow.fxml")); loader.setController(mainWindowController); pane = (AnchorPane) loader.load(); primaryStage.setMinHeight(600.00); From e6ce1dc464e42aee441c1612dae4bdfdd9ba81e4 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Mon, 1 Oct 2018 18:17:24 +0200 Subject: [PATCH 36/88] fixed javafx11 executable jar * added a workaround for the javafx 11 executable jar, this should be removed as soon as the new packager is available --- pom.xml | 2 +- .../kellerkinder/HomeFlix/application/JavaFX11Main.java | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 src/main/java/kellerkinder/HomeFlix/application/JavaFX11Main.java diff --git a/pom.xml b/pom.xml index 071aa6a..57509a4 100644 --- a/pom.xml +++ b/pom.xml @@ -121,7 +121,7 @@ - kellerkinder.HomeFlix.application.Main + kellerkinder.HomeFlix.application.JavaFX11Main diff --git a/src/main/java/kellerkinder/HomeFlix/application/JavaFX11Main.java b/src/main/java/kellerkinder/HomeFlix/application/JavaFX11Main.java new file mode 100644 index 0000000..1b2bc01 --- /dev/null +++ b/src/main/java/kellerkinder/HomeFlix/application/JavaFX11Main.java @@ -0,0 +1,8 @@ +package kellerkinder.HomeFlix.application; + +public class JavaFX11Main { + + public static void main(String[] args) { + Main.main(args); + } +} From a1319382ff4395661a6c8bded14191b23a3d15e3 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Mon, 1 Oct 2018 21:12:53 +0200 Subject: [PATCH 37/88] removed the usage of mwc in omdbAPIQuery --- .../application/MainWindowController.java | 17 ++++---- .../controller/OMDbAPIController.java | 40 ++++++++++--------- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index 7bacad9..bc73bdf 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -155,7 +155,6 @@ public class MainWindowController { private Main main; private MainWindowController mainWindowController; private UpdateController updateController; - private OMDbAPIController omdbAPIController; private DBController dbController; private static final Logger LOGGER = LogManager.getLogger(MainWindowController.class.getName()); @@ -166,7 +165,7 @@ public class MainWindowController { private boolean autoplay = false; private final String version = "0.7.0"; - private final String buildNumber = "157"; + private final String buildNumber = "159"; private final String versionName = "toothless dragon"; private String btnStyle; private String color; @@ -198,8 +197,7 @@ public class MainWindowController { public MainWindowController(Main main) { this.main = main; mainWindowController = this; - dbController = new DBController(this.main, this); - omdbAPIController = new OMDbAPIController(this, dbController, this.main); + dbController = new DBController(this.main, this); } @FXML @@ -440,11 +438,10 @@ public class MainWindowController { if (currentTableFilm.getCached() || dbController.searchCacheByURL(getCurrentStreamUrl())) { LOGGER.info("loading from cache: " + getCurrentTitle()); dbController.readCache(getCurrentStreamUrl()); - } else { - omdbAPIController = new OMDbAPIController(mainWindowController, dbController, main); - Thread omdbAPIThread = new Thread(omdbAPIController); + } else { + Thread omdbAPIThread = new Thread(new OMDbAPIController(main, dbController, currentTableFilm, omdbAPIKey)); omdbAPIThread.setName("OMDbAPI"); - omdbAPIThread.start(); + omdbAPIThread.start(); } } }); @@ -818,6 +815,10 @@ public class MainWindowController { // TODO get all needed posters eg cache all not cached entries // TODO for entries not available show homeflix logo } + +// Thread omdbAPIThread = new Thread(new OMDbAPIController(main, dbController, currentTableFilm, omdbAPIKey)); +// omdbAPIThread.setName("OMDbAPI"); +// omdbAPIThread.start(); } diff --git a/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java b/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java index 8e0f419..73e4299 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java @@ -39,38 +39,42 @@ import com.eclipsesource.json.JsonValue; import javafx.application.Platform; import kellerkinder.HomeFlix.application.Main; import kellerkinder.HomeFlix.application.MainWindowController; +import kellerkinder.HomeFlix.datatypes.FilmTabelDataType; import kellerkinder.HomeFlix.datatypes.OMDbAPIResponseDataType; public class OMDbAPIController implements Runnable { - private MainWindowController mainWindowController; - private DBController dbController; private Main main; + private DBController dbController; + private FilmTabelDataType currentTableFilm; + private String omdbAPIKey; private String URL = "https://www.omdbapi.com/?apikey="; private boolean useEpisode = true; private static final Logger LOGGER = LogManager.getLogger(MainWindowController.class.getName()); /** * constructor for the OMDbAPIController - * @param mainWindowController the MainWindowController object - * @param dbController the DBController object * @param main the Main object + * @param dbController the DBController object + * @param currentTableFilm the current film object + * @param omdbAPIKey the omdbAPI key */ - public OMDbAPIController(MainWindowController mainWindowController, DBController dbController, Main main){ - this.mainWindowController = mainWindowController; - this.dbController = dbController; + public OMDbAPIController(Main main, DBController dbController, FilmTabelDataType currentTableFilm, String omdbAPIKey) { this.main = main; + this.dbController = dbController; + this.currentTableFilm = currentTableFilm; + this.omdbAPIKey = omdbAPIKey; } @Override public void run() { LOGGER.info("Querying omdbAPI ..."); JsonObject object; - object = getByTitle(mainWindowController.getCurrentTitle()); + object = getByTitle(currentTableFilm.getTitle()); if (object == null) return; if (object.getString("Error", "").contains("not found!")) { - String title = searchByTitle(mainWindowController.getCurrentTitle()); + String title = searchByTitle(currentTableFilm.getTitle()); if (title.length() > 0) { object = getByTitle(title); @@ -122,12 +126,12 @@ public class OMDbAPIController implements Runnable { } // adding to cache - dbController.addCache(mainWindowController.getCurrentStreamUrl(), omdbResponse); - dbController.setCached(mainWindowController.getCurrentStreamUrl()); + dbController.addCache(currentTableFilm.getStreamUrl(), omdbResponse); + dbController.setCached(currentTableFilm.getStreamUrl()); // load data to the MainWindowController Platform.runLater(() -> { - dbController.readCache(mainWindowController.getCurrentStreamUrl()); + dbController.readCache(currentTableFilm.getStreamUrl()); }); return; @@ -142,13 +146,13 @@ public class OMDbAPIController implements Runnable { String output = null; URL apiUrl; try { - if (mainWindowController.getCurrentTableFilm().getSeason().length() > 0 && useEpisode) { - apiUrl = new URL(URL + mainWindowController.getOmdbAPIKey() + "&t=" + if (currentTableFilm.getSeason().length() > 0 && useEpisode) { + apiUrl = new URL(URL + omdbAPIKey + "&t=" + title.replace(" ", "%20") - + "&Season=" + mainWindowController.getCurrentTableFilm().getSeason() - + "&Episode=" + mainWindowController.getCurrentTableFilm().getEpisode()); + + "&Season=" + currentTableFilm.getSeason() + + "&Episode=" + currentTableFilm.getEpisode()); } else { - apiUrl = new URL(URL + mainWindowController.getOmdbAPIKey() + "&t=" + apiUrl = new URL(URL + omdbAPIKey + "&t=" + title.replace(" ", "%20")); } @@ -179,7 +183,7 @@ public class OMDbAPIController implements Runnable { * English name use tmdb */ try { - URL apiUrl = new URL(URL + mainWindowController.getOmdbAPIKey() + "&s=" + title.replace(" ", "%20")); + URL apiUrl = new URL(URL + omdbAPIKey + "&s=" + title.replace(" ", "%20")); BufferedReader ina = new BufferedReader(new InputStreamReader(apiUrl.openStream())); output = ina.readLine(); ina.close(); From a23b87fcb8271c4507e107384e98b4a68f44223c Mon Sep 17 00:00:00 2001 From: Jannik Date: Thu, 11 Oct 2018 12:29:26 +0200 Subject: [PATCH 38/88] fixed first startup issue --- .../HomeFlix/application/Main.java | 8 +++++- .../application/MainWindowController.java | 26 +++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/main/java/kellerkinder/HomeFlix/application/Main.java b/src/main/java/kellerkinder/HomeFlix/application/Main.java index 1263763..7bc8c76 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/Main.java +++ b/src/main/java/kellerkinder/HomeFlix/application/Main.java @@ -108,9 +108,12 @@ public class Main extends Application { primaryStage.setScene(scene); // append scene to stage primaryStage.show(); // show stage + System.out.println("test"); + // startup checks TODO move to mwc if (!configFile.exists()) { directory.mkdir(); + System.out.println("config not found"); addFirstSource(); mainWindowController.setColor("ee3523"); @@ -123,6 +126,8 @@ public class Main extends Application { if (!posterCache.exists()) { posterCache.mkdir(); } + + mainWindowController.init(); } catch (IOException e) { LOGGER.error(e); } @@ -156,7 +161,7 @@ public class Main extends Application { * we need to get the path for the first source from the user and add it to * sources.json, if the user ends the file-/directory-chooser the program will exit */ - private void addFirstSource() { + void addFirstSource() { switch (local) { case "en_US": bundle = ResourceBundle.getBundle("locals.HomeFlix-Local", Locale.US); // us_english @@ -208,6 +213,7 @@ public class Main extends Application { } } }; + selectFirstSource.setBtn1Action(btn1Action); selectFirstSource.setBtn2Action(btn2Action); selectFirstSource.showAndWait(); diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index bc73bdf..bdf6886 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -200,9 +200,11 @@ public class MainWindowController { dbController = new DBController(this.main, this); } - @FXML - public void initialize() { + + public void init() { LOGGER.info("Initializing Project-HomeFlix build " + buildNumber); + +// startupCheck(); main.loadSettings(); // load settings checkAutoUpdate(); @@ -215,6 +217,26 @@ public class MainWindowController { checkAllPosters(); // TODO testing } + + private void startupCheck() { + if (!main.getConfigFile().exists()) { + main.getDirectory().mkdir(); + System.out.println("config not found"); + + main.addFirstSource(); // need to get local + System.out.println("source finished"); + mainWindowController.setColor("ee3523"); + mainWindowController.setFontSize(17.0); + mainWindowController.setAutoUpdate(false); + mainWindowController.setLocal(local); // local dosen't exist here + main.saveSettings(); + } + + if (!main.getPosterCache().exists()) { + main.getPosterCache().mkdir(); + } + } + // Initialize general UI elements private void initUI() { versionLbl.setText("Version: " + version + " (Build: " + buildNumber + ")"); From 2e8683604155d52ad643fed63c51767722501f9a Mon Sep 17 00:00:00 2001 From: Seil0 Date: Sat, 13 Oct 2018 00:32:29 +0200 Subject: [PATCH 39/88] small code cleanup --- .../HomeFlix/application/Main.java | 4 +-- .../application/MainWindowController.java | 25 ++----------------- .../locals/HomeFlix-Local_de_DE.properties | 2 +- .../locals/HomeFlix-Local_en_US.properties | 2 +- 4 files changed, 5 insertions(+), 28 deletions(-) diff --git a/src/main/java/kellerkinder/HomeFlix/application/Main.java b/src/main/java/kellerkinder/HomeFlix/application/Main.java index 7bc8c76..393c152 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/Main.java +++ b/src/main/java/kellerkinder/HomeFlix/application/Main.java @@ -108,9 +108,7 @@ public class Main extends Application { primaryStage.setScene(scene); // append scene to stage primaryStage.show(); // show stage - System.out.println("test"); - - // startup checks TODO move to mwc + // startup checks if (!configFile.exists()) { directory.mkdir(); System.out.println("config not found"); diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index bdf6886..f11438b 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -204,7 +204,6 @@ public class MainWindowController { public void init() { LOGGER.info("Initializing Project-HomeFlix build " + buildNumber); -// startupCheck(); main.loadSettings(); // load settings checkAutoUpdate(); @@ -214,27 +213,7 @@ public class MainWindowController { initActions(); dbController.init(); - checkAllPosters(); // TODO testing - } - - - private void startupCheck() { - if (!main.getConfigFile().exists()) { - main.getDirectory().mkdir(); - System.out.println("config not found"); - - main.addFirstSource(); // need to get local - System.out.println("source finished"); - mainWindowController.setColor("ee3523"); - mainWindowController.setFontSize(17.0); - mainWindowController.setAutoUpdate(false); - mainWindowController.setLocal(local); // local dosen't exist here - main.saveSettings(); - } - - if (!main.getPosterCache().exists()) { - main.getPosterCache().mkdir(); - } + posterModeStartup(); // TODO testing } // Initialize general UI elements @@ -546,7 +525,7 @@ public class MainWindowController { // general fxml actions @FXML private void aboutBtnAction() { - String bodyText = "cemu_UI by @Seil0 \nVersion: " + version + " (Build: " + buildNumber + ") \"" + String bodyText = "Project HomeFlix \nVersion: " + version + " (Build: " + buildNumber + ") \"" + versionName + "\" \n" + getBundle().getString("infoText"); JFXInfoAlert infoAlert = new JFXInfoAlert("Project HomeFlix", bodyText, btnStyle, main.getPrimaryStage()); infoAlert.showAndWait(); diff --git a/src/main/resources/locals/HomeFlix-Local_de_DE.properties b/src/main/resources/locals/HomeFlix-Local_de_DE.properties index 736e40d..4506fe1 100644 --- a/src/main/resources/locals/HomeFlix-Local_de_DE.properties +++ b/src/main/resources/locals/HomeFlix-Local_de_DE.properties @@ -32,7 +32,7 @@ columnFavorite = Favorit #error translations vlcNotInstalled = Um einen Film abspielen wird der VLC Media Player ben\u00F6tigt! -infoText = \nAutoren: \n \u2022 seil0@mosad.xyz \n \u2022 hendrik.schutter@coptersicht.de \n(c) 2016-2018 mosad www.mosad.xyz +infoText = \nAutoren: \n \u2022 seil0@mosad.xyz \n \u2022 localhorst@mosad.xyz \n(c) 2016-2018 mosad www.mosad.xyz #textFlow translations title = Titel diff --git a/src/main/resources/locals/HomeFlix-Local_en_US.properties b/src/main/resources/locals/HomeFlix-Local_en_US.properties index 5fe7c1e..f0d0a38 100644 --- a/src/main/resources/locals/HomeFlix-Local_en_US.properties +++ b/src/main/resources/locals/HomeFlix-Local_en_US.properties @@ -32,7 +32,7 @@ columnFavorite = Favorite #error translations vlcNotInstalled = VLC Media Player is required to play a movie! -infoText = \nMaintainers: \n \u2022 seil0@mosad.xyz \n \u2022 hendrik.schutter@coptersicht.de \n(c) 2016-2018 mosad www.mosad.xyz +infoText = \nMaintainers: \n \u2022 seil0@mosad.xyz \n \u2022 localhorst@mosad.xyz \n(c) 2016-2018 mosad www.mosad.xyz #textFlow translations title = Title From 4d31e53a7ce186ad9e22d6b0b0efa8fefd011ebd Mon Sep 17 00:00:00 2001 From: Seil0 Date: Thu, 25 Oct 2018 10:07:57 +0200 Subject: [PATCH 40/88] updated gitignore to ignore eclipse files --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f8db64b..8b5a8ae 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,6 @@ config.xml .directory target/ apiKeys.json - +.classpath +.project +./settings/* From ffe068e1b01497b844d2b496cacc8351c08c9e08 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Thu, 25 Oct 2018 10:08:14 +0200 Subject: [PATCH 41/88] =?UTF-8?q?=E2=80=9E.classpath=E2=80=9C=20l=C3=B6sch?= =?UTF-8?q?en?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .classpath | 33 --------------------------------- 1 file changed, 33 deletions(-) delete mode 100644 .classpath diff --git a/.classpath b/.classpath deleted file mode 100644 index 7f5ea8f..0000000 --- a/.classpath +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 5a3b6470608e371533751a251f4b4be1affcf39e Mon Sep 17 00:00:00 2001 From: Seil0 Date: Thu, 25 Oct 2018 10:08:23 +0200 Subject: [PATCH 42/88] =?UTF-8?q?=E2=80=9E.project=E2=80=9C=20l=C3=B6schen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .project | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 .project diff --git a/.project b/.project deleted file mode 100644 index 01f0bec..0000000 --- a/.project +++ /dev/null @@ -1,23 +0,0 @@ - - - Project-HomeFlix - - - - - - org.eclipse.jdt.core.javabuilder - - - - - org.eclipse.m2e.core.maven2Builder - - - - - - org.eclipse.jdt.core.javanature - org.eclipse.m2e.core.maven2Nature - - From b8a67fbc27e14338c761c1d56e35341d5e68744e Mon Sep 17 00:00:00 2001 From: Seil0 Date: Thu, 25 Oct 2018 16:32:33 +0200 Subject: [PATCH 43/88] removed eclipse specific files --- .settings/org.eclipse.core.resources.prefs | 5 ----- .settings/org.eclipse.jdt.core.prefs | 14 -------------- .settings/org.eclipse.m2e.core.prefs | 4 ---- 3 files changed, 23 deletions(-) delete mode 100644 .settings/org.eclipse.core.resources.prefs delete mode 100644 .settings/org.eclipse.jdt.core.prefs delete mode 100644 .settings/org.eclipse.m2e.core.prefs diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs deleted file mode 100644 index 839d647..0000000 --- a/.settings/org.eclipse.core.resources.prefs +++ /dev/null @@ -1,5 +0,0 @@ -eclipse.preferences.version=1 -encoding//src/main/java=UTF-8 -encoding//src/main/resources=UTF-8 -encoding//src/test/java=UTF-8 -encoding/=UTF-8 diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs deleted file mode 100644 index b807efd..0000000 --- a/.settings/org.eclipse.jdt.core.prefs +++ /dev/null @@ -1,14 +0,0 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate -org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 -org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.compliance=11 -org.eclipse.jdt.core.compiler.debug.lineNumber=generate -org.eclipse.jdt.core.compiler.debug.localVariable=generate -org.eclipse.jdt.core.compiler.debug.sourceFile=generate -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning -org.eclipse.jdt.core.compiler.release=disabled -org.eclipse.jdt.core.compiler.source=11 diff --git a/.settings/org.eclipse.m2e.core.prefs b/.settings/org.eclipse.m2e.core.prefs deleted file mode 100644 index f897a7f..0000000 --- a/.settings/org.eclipse.m2e.core.prefs +++ /dev/null @@ -1,4 +0,0 @@ -activeProfiles= -eclipse.preferences.version=1 -resolveWorkspaceProjects=true -version=1 From 0cf02c52a69f8b96beb10bf27073243e24de2aeb Mon Sep 17 00:00:00 2001 From: Seil0 Date: Thu, 25 Oct 2018 16:33:35 +0200 Subject: [PATCH 44/88] updated gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 8b5a8ae..43baad2 100644 --- a/.gitignore +++ b/.gitignore @@ -51,4 +51,4 @@ target/ apiKeys.json .classpath .project -./settings/* +.settings/* From fed25f2fcec70f470b25a242e5c829d0747ffc28 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Wed, 7 Nov 2018 16:35:27 +0100 Subject: [PATCH 45/88] =?UTF-8?q?=E2=80=9EREADME.md=E2=80=9C=20=C3=A4ndern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f723ea0..a6e8e4e 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ minimal-json: https://github.com/ralfstx/minimal-json sqlite-jdbc: https://github.com/xerial/sqlite-jdbc apache commons io : https://commons.apache.org/proper/commons-io/ -## screenshots -![Screenshot](https://github.com/Seil0/Seil0.github.io/blob/master/images/Project-HomeFlix_MainWindow.png) +## Screenshots +![Screenshot](https://raw.githubusercontent.com/Seil0/Seil0.github.io/master/images/Project-HomeFlix_MainWindow.png) Project-HomeFlix © 2016-2018 mosad www.mosad.xyz, Project by [@Seil0](https://git.mosad.xyz/Seil0) and [@localhorst](https://git.mosad.xyz/localhorst) \ No newline at end of file From 5e89faff494677313d47f6b5d0f19b75bede3de6 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Fri, 16 Nov 2018 17:39:38 +0100 Subject: [PATCH 46/88] use gitea instead of github * use gitea instead of github * use prepareStatement instead of statement --- .../application/MainWindowController.java | 2 +- .../HomeFlix/controller/DBController.java | 31 ++++--- .../HomeFlix/controller/UpdateController.java | 83 ++++++++----------- 3 files changed, 53 insertions(+), 63 deletions(-) diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index bdf6886..14568f2 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -165,7 +165,7 @@ public class MainWindowController { private boolean autoplay = false; private final String version = "0.7.0"; - private final String buildNumber = "159"; + private final String buildNumber = "161"; private final String versionName = "toothless dragon"; private String btnStyle; private String color; diff --git a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java index f17baa5..c209fdb 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java @@ -321,7 +321,6 @@ public class DBController { * @throws IOException */ private void checkAddEntry() throws SQLException, FileNotFoundException, IOException { - Statement stmt = connection.createStatement(); PreparedStatement ps = connection.prepareStatement("insert into films values (?, ?, ?, ?, ?, ?, ?)"); LOGGER.info("checking for entrys to add to DB ..."); @@ -334,12 +333,15 @@ public class DBController { // if file is file and has mime type "video" if (file.isFile() && mimeType != null && mimeType.contains("video")) { // get all files (films) - if (!filmsdbStreamURL.contains(file.getPath())) { - stmt.executeUpdate("insert into films values (" - + "'" + file.getPath() + "'," - + "'" + cutOffEnd(file.getName()) + "', '', '', 0, 0, 0.0)"); - connection.commit(); - stmt.close(); + if (!filmsdbStreamURL.contains(file.getPath())) { + ps.setString(1, file.getPath()); + ps.setString(2, cutOffEnd(file.getName())); + ps.setString(3, ""); + ps.setString(4, ""); + ps.setInt(5, 0); + ps.setBoolean(6, false); + ps.setDouble(7, 0); + ps.addBatch(); // adds the entry LOGGER.info("Added \"" + file.getName() + "\" to database"); filmsdbStreamURL.add(file.getPath()); } @@ -350,13 +352,16 @@ public class DBController { if (season.isDirectory()) { int ep = 1; for (File episode : season.listFiles()) { - if (!filmsdbStreamURL.contains(episode.getPath())) { + if (!filmsdbStreamURL.contains(episode.getPath())) { + ps.setString(1, episode.getPath().replace("'", "''")); + ps.setString(2, cutOffEnd(file.getName())); + ps.setString(3, Integer.toString(sn)); + ps.setString(4, Integer.toString(ep)); + ps.setInt(5, 0); + ps.setBoolean(6, false); + ps.setDouble(7, 0); + ps.addBatch(); // adds the entry LOGGER.info("Added \"" + file.getName() + "\", Episode: " + episode.getName() + " to database"); - stmt.executeUpdate("insert into films values (" - + "'" + episode.getPath().replace("'", "''") + "'," - + "'" + cutOffEnd(file.getName()) + "','" + sn + "','" + ep + "', 0, 0, 0.0)"); - connection.commit(); - stmt.close(); filmsStreamURL.add(episode.getPath()); filmsdbStreamURL.add(episode.getPath()); ep++; diff --git a/src/main/java/kellerkinder/HomeFlix/controller/UpdateController.java b/src/main/java/kellerkinder/HomeFlix/controller/UpdateController.java index f25981a..50a664a 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/UpdateController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/UpdateController.java @@ -45,18 +45,17 @@ import kellerkinder.HomeFlix.application.MainWindowController; public class UpdateController implements Runnable { private MainWindowController mainWindowController; - private String buildNumber; + private int buildNumber; + private int updateBuildNumber; // tag_name from gitea private String apiOutput; - private String updateBuildNumber; // tag_name from Github @SuppressWarnings("unused") private String updateName; @SuppressWarnings("unused") private String updateChanges; - private String browserDownloadUrl; // update download link - private String githubApiRelease = "https://api.github.com/repos/Seil0/Project-HomeFlix/releases/latest"; - private String githubApiBeta = "https://api.github.com/repos/Seil0/Project-HomeFlix/releases"; + private String browserDownloadUrl; // update download link + private String giteaApiRelease = "https://git.mosad.xyz/api/v1/repos/Seil0/Project-HomeFlix/releases"; + private URL giteaApiUrl; - private URL githubApiUrl; private boolean useBeta; private static final Logger LOGGER = LogManager.getLogger(UpdateController.class.getName()); @@ -68,7 +67,7 @@ public class UpdateController implements Runnable { */ public UpdateController(MainWindowController mwc, String buildNumber, boolean useBeta) { mainWindowController = mwc; - this.buildNumber = buildNumber; + this.buildNumber = Integer.parseInt(buildNumber); this.useBeta = useBeta; } @@ -80,57 +79,41 @@ public class UpdateController implements Runnable { }); try { - - if (useBeta) { - githubApiUrl = new URL(githubApiBeta); - } else { - githubApiUrl = new URL(githubApiRelease); - } - - // URL githubApiUrl = new URL(githubApiRelease); - BufferedReader ina = new BufferedReader(new InputStreamReader(githubApiUrl.openStream())); + giteaApiUrl = new URL(giteaApiRelease); + + BufferedReader ina = new BufferedReader(new InputStreamReader(giteaApiUrl.openStream())); apiOutput = ina.readLine(); ina.close(); - } catch (IOException e) { + } catch (Exception e) { Platform.runLater(() -> { LOGGER.error("could not check update version", e); }); } - if (useBeta) { - JsonArray objectArray = Json.parse("{\"items\": " + apiOutput + "}").asObject().get("items").asArray(); - JsonValue object = objectArray.get(0); - JsonArray objectAssets = object.asObject().get("assets").asArray(); - - updateBuildNumber = object.asObject().getString("tag_name", ""); - updateName = object.asObject().getString("name", ""); - updateChanges = object.asObject().getString("body", ""); - - for (JsonValue asset : objectAssets) { - browserDownloadUrl = asset.asObject().getString("browser_download_url", ""); - } - - } else { - JsonObject object = Json.parse(apiOutput).asObject(); - JsonArray objectAssets = Json.parse(apiOutput).asObject().get("assets").asArray(); - - updateBuildNumber = object.getString("tag_name", ""); - updateName = object.getString("name", ""); - updateChanges = object.getString("body", ""); - for (JsonValue asset : objectAssets) { - browserDownloadUrl = asset.asObject().getString("browser_download_url", ""); - + JsonArray objectArray = Json.parse("{\"items\": " + apiOutput + "}").asObject().get("items").asArray(); + JsonValue object = objectArray.get(0).asObject(); // set to the latest release as default + JsonObject objectAsset = object.asObject().get("assets").asArray().get(0).asObject(); + + for(JsonValue objectIt : objectArray) { + if(objectIt.asObject().getBoolean("prerelease", false) == useBeta) { + // we found the needed release either beta or not + object = objectIt; + objectAsset = objectIt.asObject().get("assets").asArray().get(0).asObject(); + break; } } + updateBuildNumber = Integer.parseInt(object.asObject().getString("tag_name", "")); + updateName = object.asObject().getString("name", ""); + updateChanges = object.asObject().getString("body", ""); + LOGGER.info("Build: " + buildNumber + ", Update: " + updateBuildNumber); - - // Compares the program BuildNumber with the current BuildNumber if program - // BuildNumber < current BuildNumber then perform a update - int iversion = Integer.parseInt(buildNumber); - int iaktVersion = Integer.parseInt(updateBuildNumber.replace(".", "")); - - if (iversion >= iaktVersion) { + + /** + * Compare the program BuildNumber with the current BuildNumber + * if buildNumber < updateBuildNumber then perform a update + */ + if (buildNumber >= updateBuildNumber) { Platform.runLater(() -> { mainWindowController.getUpdateBtn().setText(mainWindowController.getBundle().getString("updateBtnNoUpdateAvailable")); }); @@ -140,9 +123,11 @@ public class UpdateController implements Runnable { mainWindowController.getUpdateBtn().setText(mainWindowController.getBundle().getString("updateBtnUpdateAvailable")); }); LOGGER.info("update available"); + browserDownloadUrl = objectAsset.getString("browser_download_url", ""); LOGGER.info("download link: " + browserDownloadUrl); try { - // open new Http connection, ProgressMonitorInputStream for downloading the data + // open new HTTP connection, ProgressMonitorInputStream for downloading the data + // FIXME the download progress dialog is not showing! HttpURLConnection connection = (HttpURLConnection) new URL(browserDownloadUrl).openConnection(); ProgressMonitorInputStream pmis = new ProgressMonitorInputStream(null, "Downloading...", connection.getInputStream()); ProgressMonitor pm = pmis.getProgressMonitor(); @@ -151,6 +136,7 @@ public class UpdateController implements Runnable { pm.setMinimum(0);// set beginning of the progress bar to 0 pm.setMaximum(connection.getContentLength());// set the end to the file length FileUtils.copyInputStreamToFile(pmis, new File("ProjectHomeFlix_update.jar")); // download update + LOGGER.info("update download successful, restarting ..."); org.apache.commons.io.FileUtils.copyFile(new File("ProjectHomeFlix_update.jar"), new File("ProjectHomeFlix.jar")); org.apache.commons.io.FileUtils.deleteQuietly(new File("ProjectHomeFlix_update.jar")); // delete update new ProcessBuilder("java", "-jar", "ProjectHomeFlix.jar").start(); // start the new application @@ -161,7 +147,6 @@ public class UpdateController implements Runnable { }); } } - } } From 079f15cd59f9dc385596fe729d6f2822cdff2579 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Sat, 17 Nov 2018 13:02:41 +0100 Subject: [PATCH 47/88] player fixes & lib updates * openjfx 11 -> 11.0.1 * sqlite-jdbc 3.23.1 -> 3.25.2 * fixed build in player not working --- pom.xml | 10 +++++----- .../HomeFlix/application/MainWindowController.java | 3 ++- src/main/java/kellerkinder/HomeFlix/player/Player.java | 3 ++- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/pom.xml b/pom.xml index 57509a4..34e6dd2 100644 --- a/pom.xml +++ b/pom.xml @@ -27,19 +27,19 @@ org.openjfx javafx-controls - 11 + 11.0.1 org.openjfx javafx-fxml - 11 + 11.0.1 org.openjfx javafx-media - 11 + 11.0.1 @@ -63,7 +63,7 @@ org.xerial sqlite-jdbc - 3.23.1 + 3.25.2 @@ -114,7 +114,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.2.0 + 3.2.1 Project-HomeFlix true diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index e74a696..95aea3d 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -19,6 +19,7 @@ * MA 02110-1301, USA. * */ + package kellerkinder.HomeFlix.application; import java.awt.Desktop; @@ -165,7 +166,7 @@ public class MainWindowController { private boolean autoplay = false; private final String version = "0.7.0"; - private final String buildNumber = "161"; + private final String buildNumber = "163"; private final String versionName = "toothless dragon"; private String btnStyle; private String color; diff --git a/src/main/java/kellerkinder/HomeFlix/player/Player.java b/src/main/java/kellerkinder/HomeFlix/player/Player.java index 1c8d946..98da798 100644 --- a/src/main/java/kellerkinder/HomeFlix/player/Player.java +++ b/src/main/java/kellerkinder/HomeFlix/player/Player.java @@ -45,7 +45,8 @@ public class Player { */ public Player(MainWindowController mainWindowController) { try { - FXMLLoader fxmlLoader = new FXMLLoader(ClassLoader.getSystemResource("fxml/PlayerWindow.fxml")); + FXMLLoader fxmlLoader = new FXMLLoader(); + fxmlLoader.setLocation(getClass().getResource("/fxml/PlayerWindow.fxml")); pane = (AnchorPane) fxmlLoader.load(); stage = new Stage(); scene = new Scene(pane); From 9f8c19ab8e9ab64f179236e4d49499bf8e1d3099 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Mon, 19 Nov 2018 22:49:09 +0100 Subject: [PATCH 48/88] fixed issue #6 --- .../HomeFlix/controller/DBController.java | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java index c209fdb..87ea8af 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java @@ -161,6 +161,7 @@ public class DBController { String path = source.asObject().getString("path", ""); String mode = source.asObject().getString("mode", ""); mainWindowController.addSourceToTable(path, mode); // add source to source-table + if (mode.equals("local")) { for (File file : new File(path).listFiles()) { if (file.isFile() && isVideoFile(file.getPath())) { @@ -350,11 +351,11 @@ public class DBController { int sn = 1; for (File season : file.listFiles()) { if (season.isDirectory()) { - int ep = 1; + int ep = getLastEpisode(cutOffEnd(file.getName()), Integer.toString(sn)) + 1; for (File episode : season.listFiles()) { if (!filmsdbStreamURL.contains(episode.getPath())) { ps.setString(1, episode.getPath().replace("'", "''")); - ps.setString(2, cutOffEnd(file.getName())); + ps.setString(2, cutOffEnd(file.getName())); // the title is the series root folder's name ps.setString(3, Integer.toString(sn)); ps.setString(4, Integer.toString(ep)); ps.setInt(5, 0); @@ -406,6 +407,34 @@ public class DBController { } } + /** + * gets the last episode of a season of a given series + * @param seriesTitle the actual series + * @param season the actual season + * @return the last episode number + */ + private int getLastEpisode(String seriesTitle, String season) { + int lastEpisode = 0; + try { + Statement stmt = connection.createStatement(); + PreparedStatement ps = connection.prepareStatement("SELECT * FROM films WHERE title = ? AND season = ?"); + ps.setString(1, seriesTitle); + ps.setString(2, season); + ResultSet rs = ps.executeQuery(); + + while (rs.next()) { + if (Integer.parseInt(rs.getString("episode")) > lastEpisode) + lastEpisode = Integer.parseInt(rs.getString("episode")); + } + stmt.close(); + rs.close(); + } catch (SQLException e) { + LOGGER.error("An error occured, while printing all entries", e); + } + + return lastEpisode; + } + /** * DEBUG * prints all entries from the database to the console From f1ccb04f9ef2df4aebd99c51f23ecf55191293bb Mon Sep 17 00:00:00 2001 From: Seil0 Date: Mon, 19 Nov 2018 22:57:42 +0100 Subject: [PATCH 49/88] =?UTF-8?q?=E2=80=9EREADME.md=E2=80=9C=20=C3=A4ndern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index a6e8e4e..e9d7e1b 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,7 @@ Simply download the Project-HomeFlix.jar from [releases](https://git.mosad.xyz/S ## Development information The dev branch is **only merged** into master when a **new release** is released, so **master contains the latest released version**. Please commit all changes to [dev](https://git.mosad.xyz/Seil0/Project-HomeFlix/src/branch/dev). -Librarys used in this Project: -JFoenix: https://github.com/jfoenixadmin/JFoenix -minimal-json: https://github.com/ralfstx/minimal-json -sqlite-jdbc: https://github.com/xerial/sqlite-jdbc -apache commons io : https://commons.apache.org/proper/commons-io/ +[Libraries used in this Project](https://git.mosad.xyz/Seil0/Project-HomeFlix/wiki/Documentation#used-libraries-and-apis) ## Screenshots ![Screenshot](https://raw.githubusercontent.com/Seil0/Seil0.github.io/master/images/Project-HomeFlix_MainWindow.png) From 201109e79d15b47f018b6e8810d901b5b989d4e7 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Fri, 30 Nov 2018 23:23:46 +0100 Subject: [PATCH 50/88] updated jfoenix 9.0.6 -> 9.0.8 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 34e6dd2..153bd94 100644 --- a/pom.xml +++ b/pom.xml @@ -51,7 +51,7 @@ com.jfoenix jfoenix - 9.0.6 + 9.0.8 From 65859087178da0a20ce357b3d52c0826703c7b0d Mon Sep 17 00:00:00 2001 From: Seil0 Date: Mon, 3 Dec 2018 00:16:20 +0100 Subject: [PATCH 51/88] minor gui fixes --- .../kellerkinder/HomeFlix/controller/UpdateController.java | 1 - src/main/resources/fxml/MainWindow.fxml | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/kellerkinder/HomeFlix/controller/UpdateController.java b/src/main/java/kellerkinder/HomeFlix/controller/UpdateController.java index 50a664a..7ca27e3 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/UpdateController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/UpdateController.java @@ -148,5 +148,4 @@ public class UpdateController implements Runnable { } } } - } diff --git a/src/main/resources/fxml/MainWindow.fxml b/src/main/resources/fxml/MainWindow.fxml index aecbae1..7ef5c57 100644 --- a/src/main/resources/fxml/MainWindow.fxml +++ b/src/main/resources/fxml/MainWindow.fxml @@ -22,7 +22,7 @@ - + @@ -58,7 +58,7 @@ - + From c3a148b2679915bf16b1caacec45a49273ae8ae3 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Tue, 4 Dec 2018 22:31:11 +0100 Subject: [PATCH 52/88] reworked the DBController * some tasks are now done at the SourcesController * use PrepareStatement * generall clean up --- .../application/MainWindowController.java | 15 +- .../HomeFlix/controller/DBController.java | 416 ++++++------------ .../controller/SourcesController.java | 165 +++++++ .../HomeFlix/datatypes/DatabaseDataType.java | 101 +++++ .../HomeFlix/player/PlayerController.java | 1 + 5 files changed, 412 insertions(+), 286 deletions(-) create mode 100644 src/main/java/kellerkinder/HomeFlix/controller/SourcesController.java create mode 100644 src/main/java/kellerkinder/HomeFlix/datatypes/DatabaseDataType.java diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index 95aea3d..8370c8d 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -166,7 +166,7 @@ public class MainWindowController { private boolean autoplay = false; private final String version = "0.7.0"; - private final String buildNumber = "163"; + private final String buildNumber = "165"; private final String versionName = "toothless dragon"; private String btnStyle; private String color; @@ -214,7 +214,7 @@ public class MainWindowController { initActions(); dbController.init(); - posterModeStartup(); // TODO testing +// posterModeStartup(); // TODO testing } // Initialize general UI elements @@ -550,7 +550,7 @@ public class MainWindowController { directoryChooser.setTitle(bundle.getString("addDirectory")); File selectedFolder = directoryChooser.showDialog(main.getPrimaryStage()); if (selectedFolder != null && selectedFolder.exists()) { - mainWindowController.addSource(selectedFolder.getPath(), "local"); + addSource(selectedFolder.getPath(), "local"); } else { LOGGER.error("The selected folder dosen't exist!"); } @@ -563,7 +563,6 @@ public class MainWindowController { File selectedFile = fileChooser.showOpenDialog(main.getPrimaryStage()); if (selectedFile != null && selectedFile.exists()) { addSource(selectedFile.getPath(), "stream"); - dbController.refreshDataBase(); } else { LOGGER.error("The selected file dosen't exist!"); } @@ -605,6 +604,8 @@ public class MainWindowController { * add data from films-list to films-table */ public void addFilmsToTable(ObservableList elementsList) { + + System.out.println(elementsList.size()); for (FilmTabelDataType element : elementsList) { @@ -667,6 +668,12 @@ public class MainWindowController { } catch (IOException e) { LOGGER.error(e); } + + // clear old sources list/table + getSourcesList().clear(); + getSourceRoot().getChildren().clear(); + // update the database and all films from the database + dbController.refreshDataBase(); } /** diff --git a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java index 87ea8af..fcd332c 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java @@ -18,11 +18,11 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. */ + package kellerkinder.HomeFlix.controller; import java.io.File; import java.io.FileNotFoundException; -import java.io.FileReader; import java.io.IOException; import java.net.URLConnection; import java.sql.Connection; @@ -37,11 +37,6 @@ import java.util.List; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import com.eclipsesource.json.Json; -import com.eclipsesource.json.JsonArray; -import com.eclipsesource.json.JsonObject; -import com.eclipsesource.json.JsonValue; - import javafx.collections.ObservableList; import javafx.scene.Node; import javafx.scene.image.Image; @@ -51,9 +46,9 @@ import javafx.scene.text.FontWeight; import javafx.scene.text.Text; import kellerkinder.HomeFlix.application.Main; import kellerkinder.HomeFlix.application.MainWindowController; +import kellerkinder.HomeFlix.datatypes.DatabaseDataType; import kellerkinder.HomeFlix.datatypes.FilmTabelDataType; import kellerkinder.HomeFlix.datatypes.OMDbAPIResponseDataType; -import kellerkinder.HomeFlix.datatypes.SourceDataType; public class DBController { @@ -62,8 +57,8 @@ public class DBController { private String DB_PATH; private Image favorite_black = new Image("icons/ic_favorite_black_18dp_1x.png"); private Image favorite_border_black = new Image("icons/ic_favorite_border_black_18dp_1x.png"); - private List filmsdbStreamURL = new ArrayList(); // contains all films stored in the database - private List filmsStreamURL = new ArrayList(); // contains all films from the sources + private List databaseStream = new ArrayList(); // contains all films stored in the database + private List sourceStreams = new ArrayList(); // contains all films from the sources private Connection connection = null; private static final Logger LOGGER = LogManager.getLogger(DBController.class.getName()); @@ -136,7 +131,9 @@ public class DBController { Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM films"); while (rs.next()) { - filmsdbStreamURL.add(rs.getString("streamUrl")); + databaseStream.add(new DatabaseDataType(rs.getString("streamUrl"), + rs.getString("title"), rs.getString("season"), rs.getString("episode"), + rs.getInt("favorite"), rs.getBoolean("cached"), rs.getDouble("currentTime"))); } stmt.close(); rs.close(); @@ -146,57 +143,11 @@ public class DBController { } /** - * load sources from sources.json - * if mode == local, get all files and series-folder from the directory - * else mode must be streaming, read all entries from the streaming file + * load all sources */ private void loadSources() { - // remove sources from table - mainWindowController.getSourcesList().removeAll(mainWindowController.getSourcesList()); - mainWindowController.getSourceRoot().getChildren().removeAll(mainWindowController.getSourceRoot().getChildren()); - - try { - JsonArray sources = Json.parse(new FileReader(main.getDirectory() + "/sources.json")).asArray(); - for (JsonValue source : sources) { - String path = source.asObject().getString("path", ""); - String mode = source.asObject().getString("mode", ""); - mainWindowController.addSourceToTable(path, mode); // add source to source-table - - if (mode.equals("local")) { - for (File file : new File(path).listFiles()) { - if (file.isFile() && isVideoFile(file.getPath())) { - filmsStreamURL.add(file.getPath()); - } else if(file.isDirectory()) { - // get all folders (series) - for (File season : file.listFiles()) { - if (season.isDirectory()) { - for (File episode : season.listFiles()) { - if (!filmsdbStreamURL.contains(episode.getPath())) { - filmsStreamURL.add(episode.getPath()); - } - } - } - } - } - } - LOGGER.info("added files from: " + path); - } else { - // getting all entries from the streaming lists - try { - JsonObject object = Json.parse(new FileReader(path)).asObject(); - JsonArray items = object.get("entries").asArray(); - for (JsonValue item : items) { - filmsStreamURL.add(item.asObject().getString("streamUrl", "")); - } - LOGGER.info("added films from: " + path); - } catch (IOException e) { - LOGGER.error(e); - } - } - } - } catch (Exception e) { - e.printStackTrace(); - } + SourcesController sourcesController = new SourcesController(main, mainWindowController); + sourceStreams = sourcesController.loadSources(); } /** @@ -204,22 +155,18 @@ public class DBController { * order entries by title */ private void loadDataToFilmsList() { + ImageView imageView; LOGGER.info("loading data to mwc ..."); try { //load local Data Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT * FROM films ORDER BY title"); + ResultSet rs = stmt.executeQuery("SELECT * FROM films ORDER BY title"); + while (rs.next()) { -// System.out.println(rs.getString("title") + "Season:" + rs.getString("season") + ":"); - if (rs.getBoolean("favorite") == true) { - mainWindowController.getFilmsList().add(new FilmTabelDataType(rs.getString("streamUrl"), - rs.getString("title"), rs.getString("season"), rs.getString("episode") ,rs.getBoolean("favorite"), - rs.getBoolean("cached"), new ImageView(favorite_black))); - } else { - mainWindowController.getFilmsList().add(new FilmTabelDataType(rs.getString("streamUrl"), - rs.getString("title"), rs.getString("season"), rs.getString("episode"), rs.getBoolean("favorite"), - rs.getBoolean("cached"), new ImageView(favorite_border_black))); - } + imageView = rs.getBoolean("favorite") ? new ImageView(favorite_black) : new ImageView(favorite_border_black); + mainWindowController.getFilmsList().add(new FilmTabelDataType(rs.getString("streamUrl"), + rs.getString("title"), rs.getString("season"), rs.getString("episode") ,rs.getBoolean("favorite"), + rs.getBoolean("cached"), imageView)); } stmt.close(); rs.close(); @@ -237,24 +184,20 @@ public class DBController { * @param index of the film in LocalFilms list */ public void refresh(String streamUrl, int indexList) { - LOGGER.info("refresh ..."); + LOGGER.info("refresh data for " + streamUrl); try { - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT * FROM films WHERE streamUrl = \"" + streamUrl + "\";"); + PreparedStatement ps = connection.prepareStatement("SELECT * FROM films WHERE streamUrl = ?"); + ps.setString(1, streamUrl); + ResultSet rs = ps.executeQuery(); while (rs.next()) { - if (rs.getBoolean("favorite") == true) { - mainWindowController.getFilmsList().set(indexList, new FilmTabelDataType(rs.getString("streamUrl"), - rs.getString("title"), rs.getString("season"), rs.getString("episode"), rs.getBoolean("favorite"), - rs.getBoolean("cached"), new ImageView(favorite_black))); - } else { - mainWindowController.getFilmsList().set(indexList, new FilmTabelDataType(rs.getString("streamUrl"), - rs.getString("title"), rs.getString("season"), rs.getString("episode"), rs.getBoolean("favorite"), - rs.getBoolean("cached"), new ImageView(favorite_border_black))); - } + ImageView imageView = rs.getBoolean("favorite") ? new ImageView(favorite_black) : new ImageView(favorite_border_black); + mainWindowController.getFilmsList().set(indexList, new FilmTabelDataType(rs.getString("streamUrl"), + rs.getString("title"), rs.getString("season"), rs.getString("episode"), rs.getBoolean("favorite"), + rs.getBoolean("cached"), imageView)); } rs.close(); - stmt.close(); + ps.close(); } catch (Exception e) { LOGGER.error("Ups! error while refreshing mwc!", e); } @@ -269,14 +212,13 @@ public class DBController { LOGGER.info("refreshing the Database ..."); // clean all ArraLists - filmsdbStreamURL.clear(); - filmsStreamURL.clear(); + databaseStream.clear(); + sourceStreams.clear(); loadSources(); // reload all sources loadDatabase(); // reload all films saved in the DB - LOGGER.info("films in directory: " + filmsStreamURL.size()); - LOGGER.info("filme in db: " + filmsdbStreamURL.size()); + LOGGER.info("filme in db: " + databaseStream.size()); try { checkAddEntry(); @@ -285,7 +227,7 @@ public class DBController { LOGGER.error("Error while refreshing the database", e); } - // clear the FilmsList and FilmRoot chlidren + // clear the FilmsList and FilmRoot children mainWindowController.getFilmsList().clear(); mainWindowController.getFilmRoot().getChildren().clear(); @@ -294,25 +236,32 @@ public class DBController { /** * check if there are any entries that have been removed from the film-directory + * @throws SQLException */ - private void checkRemoveEntry() { + private void checkRemoveEntry() throws SQLException { + PreparedStatement ps = connection.prepareStatement("DELETE FROM films WHERE streamUrl = ?"); LOGGER.info("checking for entrys to remove to DB ..."); - try { - Statement stmt = connection.createStatement(); + + for (DatabaseDataType dbStreamEntry : databaseStream) { + // if the directory doen't contain the entry form the database, remove it - for (String entry : filmsdbStreamURL) { - // if the directory doen't contain the entry form the db, remove it - if (!filmsStreamURL.contains(entry)) { - stmt.executeUpdate("delete from films where streamUrl = \"" + entry + "\""); - connection.commit(); - LOGGER.info("removed \"" + entry + "\" from database"); - } + // if sourceStreams has a item where StreamUrl equals dbStreamEntry.getStreamUrl() return it, else null + DatabaseDataType result = sourceStreams.stream() + .filter(x -> dbStreamEntry.getStreamUrl().equals(x.getStreamUrl())) + .findAny() + .orElse(null); + + // if the result is null, the file is missing, remove it from the database + if (result == null) { + ps.setString(1, dbStreamEntry.getStreamUrl()); + ps.addBatch(); + LOGGER.info("removed \"" + dbStreamEntry.getTitle() + "\" from database"); } - - stmt.close(); - } catch (Exception e) { - LOGGER.error(e); } + + ps.executeBatch(); + connection.commit(); + ps.close(); } /** @@ -325,114 +274,33 @@ public class DBController { PreparedStatement ps = connection.prepareStatement("insert into films values (?, ?, ?, ?, ?, ?, ?)"); LOGGER.info("checking for entrys to add to DB ..."); - // source is a single source of the sources list - for (SourceDataType source : mainWindowController.getSourcesList()) { - // if it's a local source check the folder for new film - if (source.getMode().equals("local")) { - for (File file : new File(source.getPath()).listFiles()) { - String mimeType = URLConnection.guessContentTypeFromName(file.getPath()); - // if file is file and has mime type "video" - if (file.isFile() && mimeType != null && mimeType.contains("video")) { - // get all files (films) - if (!filmsdbStreamURL.contains(file.getPath())) { - ps.setString(1, file.getPath()); - ps.setString(2, cutOffEnd(file.getName())); - ps.setString(3, ""); - ps.setString(4, ""); - ps.setInt(5, 0); - ps.setBoolean(6, false); - ps.setDouble(7, 0); - ps.addBatch(); // adds the entry - LOGGER.info("Added \"" + file.getName() + "\" to database"); - filmsdbStreamURL.add(file.getPath()); - } - } else if (file.isDirectory()) { - // get all folders (series) - int sn = 1; - for (File season : file.listFiles()) { - if (season.isDirectory()) { - int ep = getLastEpisode(cutOffEnd(file.getName()), Integer.toString(sn)) + 1; - for (File episode : season.listFiles()) { - if (!filmsdbStreamURL.contains(episode.getPath())) { - ps.setString(1, episode.getPath().replace("'", "''")); - ps.setString(2, cutOffEnd(file.getName())); // the title is the series root folder's name - ps.setString(3, Integer.toString(sn)); - ps.setString(4, Integer.toString(ep)); - ps.setInt(5, 0); - ps.setBoolean(6, false); - ps.setDouble(7, 0); - ps.addBatch(); // adds the entry - LOGGER.info("Added \"" + file.getName() + "\", Episode: " + episode.getName() + " to database"); - filmsStreamURL.add(episode.getPath()); - filmsdbStreamURL.add(episode.getPath()); - ep++; - } - } - sn++; - } - } - } - - } - } else { - // if it's a streaming source check the file for new films - for (String entry : filmsStreamURL) { - if (!filmsdbStreamURL.contains(entry)) { - JsonArray items = Json.parse(new FileReader(source.getPath())).asObject().get("entries").asArray(); - // for each item, check if it's the needed - for (JsonValue item : items) { - String streamUrl = item.asObject().getString("streamUrl", ""); - String title = item.asObject().getString("title", ""); - - // if it's the needed add it to the database - if (streamUrl.equals(entry)) { - ps.setString(1, streamUrl); - ps.setString(2, title); - ps.setString(3, item.asObject().getString("season", "")); - ps.setString(4, item.asObject().getString("episode", "")); - ps.setInt(5, 0); - ps.setBoolean(6, false); - ps.setDouble(7, 0); - ps.addBatch(); // adds the entry - LOGGER.info("Added \"" + title + "\" to database"); - filmsdbStreamURL.add(streamUrl); - } - } - } - } - ps.executeBatch(); - connection.commit(); - ps.close(); + // new + for (DatabaseDataType sourceStreamEntry : sourceStreams) { + + // if databaseStream has a item where StreamUrl equals sourceStreamEntry.getStreamUrl() return it, else null + DatabaseDataType result = databaseStream.stream() + .filter(x -> sourceStreamEntry.getStreamUrl().equals(x.getStreamUrl())) + .findAny() + .orElse(null); + + // if the result is null, the entry is missing, add it to the database + if (result == null) { + ps.setString(1, sourceStreamEntry.getStreamUrl()); + ps.setString(2, sourceStreamEntry.getTitle()); + ps.setString(3, sourceStreamEntry.getSeason()); + ps.setString(4, sourceStreamEntry.getEpisode()); + ps.setInt(5, sourceStreamEntry.getFavorite()); + ps.setBoolean(6, sourceStreamEntry.getCached()); + ps.setDouble(7, sourceStreamEntry.getCurrentTime()); + ps.addBatch(); // adds the entry + LOGGER.info("Added \"" + sourceStreamEntry.getTitle() + "\" to database"); + databaseStream.add(sourceStreamEntry); } } - } - - /** - * gets the last episode of a season of a given series - * @param seriesTitle the actual series - * @param season the actual season - * @return the last episode number - */ - private int getLastEpisode(String seriesTitle, String season) { - int lastEpisode = 0; - try { - Statement stmt = connection.createStatement(); - PreparedStatement ps = connection.prepareStatement("SELECT * FROM films WHERE title = ? AND season = ?"); - ps.setString(1, seriesTitle); - ps.setString(2, season); - ResultSet rs = ps.executeQuery(); - - while (rs.next()) { - if (Integer.parseInt(rs.getString("episode")) > lastEpisode) - lastEpisode = Integer.parseInt(rs.getString("episode")); - } - stmt.close(); - rs.close(); - } catch (SQLException e) { - LOGGER.error("An error occured, while printing all entries", e); - } - - return lastEpisode; + + ps.executeBatch(); + connection.commit(); + ps.close(); } /** @@ -453,8 +321,8 @@ public class DBController { System.out.println(rs.getString("cached")); System.out.println(rs.getString("currentTime") + "\n"); } - stmt.close(); rs.close(); + stmt.close(); } catch (SQLException e) { LOGGER.error("An error occured, while printing all entries", e); } @@ -467,10 +335,11 @@ public class DBController { public void dislike(String streamUrl) { LOGGER.info("dislike " + streamUrl); try { - Statement stmt = connection.createStatement(); - stmt.executeUpdate("UPDATE films SET favorite=0 WHERE streamUrl=\"" + streamUrl + "\";"); + PreparedStatement ps = connection.prepareStatement("UPDATE films SET favorite = 0 WHERE streamUrl = ?"); + ps.setString(1, streamUrl); + ps.executeUpdate(); connection.commit(); - stmt.close(); + ps.close(); } catch (SQLException e) { LOGGER.error("Ups! an error occured!", e); } @@ -483,10 +352,11 @@ public class DBController { public void like(String streamUrl) { LOGGER.info("like " + streamUrl); try { - Statement stmt = connection.createStatement(); - stmt.executeUpdate("UPDATE films SET favorite = 1 WHERE streamUrl = \"" + streamUrl + "\";"); + PreparedStatement ps = connection.prepareStatement("UPDATE films SET favorite = 1 WHERE streamUrl = ?"); + ps.setString(1, streamUrl); + ps.executeUpdate(); connection.commit(); - stmt.close(); + ps.close(); } catch (SQLException e) { LOGGER.error("Ups! an error occured!", e); } @@ -498,10 +368,11 @@ public class DBController { */ public void setCached(String streamUrl) { try { - Statement stmt = connection.createStatement(); - stmt.executeUpdate("UPDATE films SET cached = 1 WHERE streamUrl = \"" + streamUrl + "\";"); + PreparedStatement ps = connection.prepareStatement("UPDATE films SET cached = 1 WHERE streamUrl = ?"); + ps.setString(1, streamUrl); + ps.executeUpdate(); connection.commit(); - stmt.close(); + ps.close(); } catch (SQLException e) { LOGGER.error("Ups! an error occured!", e); } @@ -563,11 +434,12 @@ public class DBController { public boolean searchCacheByURL(String streamUrl) { boolean retValue = false; try { - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT * FROM cache WHERE streamUrl = \"" + streamUrl + "\";"); + PreparedStatement ps = connection.prepareStatement("SELECT * FROM cache WHERE streamUrl = ?"); + ps.setString(1, streamUrl); + ResultSet rs = ps.executeQuery(); retValue = rs.next(); rs.close(); - stmt.close(); + ps.close(); } catch (Exception e) { LOGGER.error("Ups! error while getting the current time!", e); } @@ -581,8 +453,9 @@ public class DBController { */ public void readCache(String streamUrl) { try { - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT * FROM cache WHERE streamUrl=\"" + streamUrl + "\";"); + PreparedStatement ps = connection.prepareStatement("SELECT * FROM cache WHERE streamUrl = ?"); + ps.setString(1, streamUrl); + ResultSet rs = ps.executeQuery(); Font font = Font.font("System", FontWeight.BOLD, (int) Math.round(mainWindowController.getFontSize())); ObservableList textFlow = mainWindowController.getTextFlow().getChildren(); ArrayList nameText = new ArrayList(); @@ -650,8 +523,8 @@ public class DBController { LOGGER.error("No Poster found, useing default."); } - stmt.close(); rs.close(); + ps.close(); } catch (SQLException e) { LOGGER.error("Ups! an error occured!", e); } @@ -686,14 +559,15 @@ public class DBController { * @return {@link Double} currentTime in ms */ public double getCurrentTime(String streamUrl) { - LOGGER.info("currentTime: " + streamUrl); + LOGGER.info("get currentTime: " + streamUrl); double currentTime = 0; try { - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT * FROM films WHERE streamUrl = \"" + streamUrl + "\";"); + PreparedStatement ps = connection.prepareStatement("SELECT * FROM films WHERE streamUrl = ?"); + ps.setString(1, streamUrl); + ResultSet rs = ps.executeQuery(); currentTime = rs.getDouble("currentTime"); rs.close(); - stmt.close(); + ps.close(); } catch (Exception e) { LOGGER.error("Ups! error while getting the current time!", e); } @@ -707,69 +581,55 @@ public class DBController { * @param currentTime currentTime in ms of the film */ public void setCurrentTime(String streamUrl, double currentTime) { - LOGGER.info("currentTime: " + streamUrl); + LOGGER.info("set currentTime: " + streamUrl); try { - Statement stmt = connection.createStatement(); - stmt.executeUpdate("UPDATE films SET currentTime=" + currentTime + " WHERE streamUrl=\"" + streamUrl + "\";"); + PreparedStatement ps = connection.prepareStatement("UPDATE films SET currentTime = ? WHERE streamUrl = ?"); + ps.setDouble(1, currentTime); + ps.setString(2, streamUrl); + ps.executeUpdate(); connection.commit(); - stmt.close(); + ps.close(); } catch (SQLException e) { - LOGGER.error("Ups! an error occured!", e); + LOGGER.error("Ups! error while updateing the current time!", e); } } /** - * get the next episode of a - * @param title URL of the film - * @param nextEp number of the next episode + * get the next episode of a series + * @param title title of the film + * @param episode episode currently played * @return {@link FilmTabelDataType} the next episode as object */ public FilmTabelDataType getNextEpisode(String title, int episode, int season) { FilmTabelDataType nextFilm = null; - ResultSet rs; - int nextEpisode = 3000; try { - Statement stmt = connection.createStatement(); + // try to get a new episode of the current season + PreparedStatement ps = connection.prepareStatement("SELECT * FROM films WHERE title = ? AND season = ? AND episode = ?"); + ps.setString(1, title); + ps.setString(2, Integer.toString(season)); + ps.setString(3, Integer.toString(episode + 1)); + ResultSet rs = ps.executeQuery(); - rs = stmt.executeQuery("SELECT * FROM films WHERE title = \"" + title + "\" AND season = \"" + season + "\";"); - while(rs.next()) { - int rsEpisode = Integer.parseInt(rs.getString("episode")); - if (rsEpisode > episode && rsEpisode < nextEpisode) { - // fitting episode found in current season, if rsEpisode < nextEpisode -> nextEpisode = rsEpisode - nextEpisode = rsEpisode; - nextFilm = new FilmTabelDataType(rs.getString("streamUrl"), rs.getString("title"), - rs.getString("season"), rs.getString("episode"), rs.getBoolean("favorite"), - rs.getBoolean("cached"), new ImageView()); - } + /* if that fails get the next season and try to get a episode there, + * we need to test only for the next season, first episode, + * any other entry would not exist in the database + */ + if(!rs.next()) { + ps.setString(1, title); + ps.setString(2, Integer.toString(season + 1)); + ps.setString(3, Integer.toString(1)); + rs = ps.executeQuery(); + if(!rs.next()) return nextFilm; // if we haven't found anything return an empty object } - if (nextFilm == null) { - int nextSeason = 3000; - rs = stmt.executeQuery("SELECT * FROM films WHERE title = \"" + title + "\";"); - while(rs.next()) { - int rsSeason = Integer.parseInt(rs.getString("season")); - if (rsSeason > season && rsSeason < nextSeason) { - nextSeason = rsSeason; - } - } - - if (nextSeason != 3000) { - rs = stmt.executeQuery("SELECT * FROM films WHERE title = \"" + title + "\" AND season = \"" + season + "\";"); - while(rs.next()) { - int rsEpisode = Integer.parseInt(rs.getString("episode")); - if (rsEpisode > episode && rsEpisode < nextEpisode) { - // fitting episode found in current season, if rsEpisode < nextEpisode -> nextEpisode = rsEpisode - nextEpisode = rsEpisode; - nextFilm = new FilmTabelDataType(rs.getString("streamUrl"), rs.getString("title"), - rs.getString("season"), rs.getString("episode"), rs.getBoolean("favorite"), - rs.getBoolean("cached"), new ImageView()); - } - } - } - } + // at this point we have found the correct episode + nextFilm = new FilmTabelDataType(rs.getString("streamUrl"), rs.getString("title"), + rs.getString("season"), rs.getString("episode"), rs.getBoolean("favorite"), + rs.getBoolean("cached"), new ImageView()); + rs.close(); - stmt.close(); + ps.close(); } catch (Exception e) { LOGGER.error("Ups! error while getting next episode!", e); } @@ -810,14 +670,6 @@ public class DBController { return nextFilm; } - // removes the ending - private String cutOffEnd(String str) { - if (str == null) return null; - int pos = str.lastIndexOf("."); - if (pos == -1) return str; - return str.substring(0, pos); - } - /** * check if a file is a video * @param path the path to the file diff --git a/src/main/java/kellerkinder/HomeFlix/controller/SourcesController.java b/src/main/java/kellerkinder/HomeFlix/controller/SourcesController.java new file mode 100644 index 0000000..a875e67 --- /dev/null +++ b/src/main/java/kellerkinder/HomeFlix/controller/SourcesController.java @@ -0,0 +1,165 @@ +/** + * Project-HomeFlix + * + * Copyright 2016-2018 <@Seil0> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + */ + +package kellerkinder.HomeFlix.controller; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.eclipsesource.json.Json; +import com.eclipsesource.json.JsonArray; +import com.eclipsesource.json.JsonObject; +import com.eclipsesource.json.JsonValue; + +import kellerkinder.HomeFlix.application.Main; +import kellerkinder.HomeFlix.application.MainWindowController; +import kellerkinder.HomeFlix.datatypes.DatabaseDataType; + +public class SourcesController { + + private MainWindowController mainWindowController; + private Main main; + private List sourceStreams = new ArrayList(); + private static final Logger LOGGER = LogManager.getLogger(SourcesController.class.getName()); + + public SourcesController(Main main, MainWindowController mainWindowController) { + this.main = main; + this.mainWindowController = mainWindowController; + } + + /** + * load all local and streaming sources and add them to a list + */ + public List loadSources() { + + try { + // create a JsonArray, containing all sources, add each source to the mwc, get all films from it + JsonArray sources = Json.parse(new FileReader(main.getDirectory() + "/sources.json")).asArray(); + for (JsonValue source : sources) { + String path = source.asObject().getString("path", ""); + String mode = source.asObject().getString("mode", ""); + mainWindowController.addSourceToTable(path, mode); // add loaded source to source-table TODO this should be done in mwc + + if (mode.equals("local")) + addLocalSource(path); + if (mode.equals("stream")) + addStreamSource(path); + } + } catch (Exception e) { + e.printStackTrace(); + } + + LOGGER.info("films in directory: " + sourceStreams.size()); + + return sourceStreams; + } + /** + * for each file in source path + * check whether it's valid file or a directory (series) + * if it's valid or a series add it to the sourceStreams + * @param a local source path (a directory) + */ + private void addLocalSource(String path) { + + int oldSize = sourceStreams.size(); + for (File file : new File(path).listFiles()) { + // if it's valid file add it to the sourceStreams + if (isValidFile(file)) { + sourceStreams.add(new DatabaseDataType(file.getPath(), cutOffEnd(file.getName()), "", "", 0, false, 0.0)); + } else if(file.isDirectory()) { + // get all directories (series), root and season must be directories + int sn = 1; + for (File season : file.listFiles()) { + if (season.isDirectory()) { + int ep = 1; + for (File episode : season.listFiles()) { + // check whether the episode is a valid file + if (isValidFile(episode)) { + sourceStreams.add(new DatabaseDataType(episode.getPath(), cutOffEnd(file.getName()), + Integer.toString(sn), Integer.toString(ep), 0, false, 0.0)); + ep++; + } + } + sn++; + } + } + } + } + + LOGGER.info("added " + (sourceStreams.size() - oldSize) + " local files from: " + path); + } + + private void addStreamSource(String path) { + // getting all entries from the streaming lists + try { + JsonObject object = Json.parse(new FileReader(path)).asObject(); + JsonArray items = object.get("entries").asArray(); + for (JsonValue item : items) { + sourceStreams.add(new DatabaseDataType(item.asObject().getString("streamUrl", ""), + item.asObject().getString("title", ""), + item.asObject().getString("season", ""), + item.asObject().getString("episode", ""), 0, false, 0.0)); + } + LOGGER.info("added " + items.size() +" stream entries from: " + path); + } catch (IOException e) { + LOGGER.error(e); + } + + + } + + /** + * check if a file is a valid file (is video and file) + * @param file the actual file + * @return true if it's valid, else false + */ + private boolean isValidFile(File file) { + try { + String mimeType = Files.probeContentType(file.toPath()); + if (file.isFile() && mimeType != null && (mimeType.startsWith("video") || mimeType.contains("matroska"))) { + return true; + } else { +// System.out.println(file.getPath() + " mime type: " + mimeType); + } + } catch (IOException e) { + LOGGER.error(e); + } + + return false; + } + + // removes the ending + private String cutOffEnd(String str) { + if (str == null) return null; + int pos = str.lastIndexOf("."); + if (pos == -1) return str; + return str.substring(0, pos); + } + +} diff --git a/src/main/java/kellerkinder/HomeFlix/datatypes/DatabaseDataType.java b/src/main/java/kellerkinder/HomeFlix/datatypes/DatabaseDataType.java new file mode 100644 index 0000000..053f80b --- /dev/null +++ b/src/main/java/kellerkinder/HomeFlix/datatypes/DatabaseDataType.java @@ -0,0 +1,101 @@ +/** + * Project-HomeFlix + * + * Copyright 2016-2018 <@Seil0> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + */ + +package kellerkinder.HomeFlix.datatypes; + +public class DatabaseDataType { + + private String streamUrl; + private String title; + private String season; + private String episode; + private int favorite; + private Boolean cached; + private double currentTime; + + public DatabaseDataType(String streamUrl, String title, String season, String episode, int favorite, Boolean cached, double currentTime) { + this.streamUrl = streamUrl; + this.title = title; + this.season = season; + this.episode = episode; + this.favorite = favorite; + this.cached = cached; + this.currentTime = currentTime; + } + + public String getStreamUrl() { + return streamUrl; + } + + public void setStreamUrl(String streamUrl) { + this.streamUrl = streamUrl; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getSeason() { + return season; + } + + public void setSeason(String season) { + this.season = season; + } + + public String getEpisode() { + return episode; + } + + public void setEpisode(String episode) { + this.episode = episode; + } + + public int getFavorite() { + return favorite; + } + + public void setFavorite(int favorite) { + this.favorite = favorite; + } + + public Boolean getCached() { + return cached; + } + + public void setCached(Boolean cached) { + this.cached = cached; + } + + public double getCurrentTime() { + return currentTime; + } + + public void setCurrentTime(double currentTime) { + this.currentTime = currentTime; + } + +} diff --git a/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java b/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java index 52f2741..4b244bf 100644 --- a/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java +++ b/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java @@ -19,6 +19,7 @@ * MA 02110-1301, USA. * */ + package kellerkinder.HomeFlix.player; import java.io.File; From a677abc15c3898a1a6691a79c55f5c218f4a83ec Mon Sep 17 00:00:00 2001 From: Seil0 Date: Sat, 8 Dec 2018 22:48:13 +0100 Subject: [PATCH 53/88] added a "next episode" button to the player --- .../application/MainWindowController.java | 4 +- .../HomeFlix/player/PlayerController.java | 89 ++++++++++++------- src/main/resources/fxml/PlayerWindow.fxml | 13 ++- 3 files changed, 72 insertions(+), 34 deletions(-) diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index 8370c8d..f3b4f24 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -473,7 +473,7 @@ public class MainWindowController { while ((line = input.readLine()) != null) { output = line; } - LOGGER.info(output); + LOGGER.info("which vlc: " + output); input.close(); } catch (IOException e1) { e1.printStackTrace(); @@ -604,8 +604,6 @@ public class MainWindowController { * add data from films-list to films-table */ public void addFilmsToTable(ObservableList elementsList) { - - System.out.println(elementsList.size()); for (FilmTabelDataType element : elementsList) { diff --git a/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java b/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java index 4b244bf..26f73cf 100644 --- a/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java +++ b/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java @@ -52,24 +52,18 @@ import kellerkinder.HomeFlix.datatypes.FilmTabelDataType; public class PlayerController { - @FXML - private MediaView mediaView; + @FXML private MediaView mediaView; - @FXML - private VBox bottomVBox; + @FXML private VBox bottomVBox; - @FXML - private HBox controlsHBox; + @FXML private HBox controlsHBox; - @FXML - private JFXSlider timeSlider; + @FXML private JFXSlider timeSlider; - @FXML - private JFXButton stopBtn; - @FXML - private JFXButton playBtn; - @FXML - private JFXButton fullscreenBtn; + @FXML private JFXButton stopBtn; + @FXML private JFXButton playBtn; + @FXML private JFXButton fullscreenBtn; + @FXML private JFXButton nextEpBtn; private Player player; private MainWindowController mainWCon; @@ -81,6 +75,9 @@ public class PlayerController { private double seekTime = 0; private double startTime = 0; private double duration = 0; + private int season = 0; + private int episode = 0; + private int countdown = 0; private boolean mousePressed = false; private boolean showControls = true; private boolean autoplay; @@ -123,6 +120,10 @@ public class PlayerController { width.bind(Bindings.selectDouble(mediaView.sceneProperty(), "width")); height.bind(Bindings.selectDouble(mediaView.sceneProperty(), "height")); + season = !film.getSeason().isEmpty() ? Integer.parseInt(film.getSeason()) : 0; + episode = !film.getEpisode().isEmpty() ? Integer.parseInt(film.getEpisode()) : 0; +// nextEpBtn.setStyle("-fx-button-type: RAISED; -fx-background-color: #" + mainWCon.getColor() + "; -fx-text-fill: WHITE;"); + // start the media if the player is ready mediaPlayer.setOnReady(new Runnable() { @Override @@ -141,27 +142,40 @@ public class PlayerController { @Override public void changed(ObservableValue observable, Duration oldValue, Duration newValue) { currentTime = newValue.toMillis(); // set the current time - int episode = !film.getEpisode().isEmpty() ? Integer.parseInt(film.getEpisode()) : 0; - int season = !film.getSeason().isEmpty() ? Integer.parseInt(film.getSeason()) : 0; - - // if we are end time -10 seconds, do autoplay, if activated - if ((duration - currentTime) < 10000 && episode != 0 && autoplay) { - autoplay = false; - mainWCon.getDbController().setCurrentTime(film.getStreamUrl(), 0); // reset old video start time - FilmTabelDataType nextFilm = mainWCon.getDbController().getNextEpisode(film.getTitle(), episode, season); - if (nextFilm != null) { - mediaPlayer.stop(); - init(mainWCon, player, nextFilm); - autoplay = true; + double timeToEnd = (duration - currentTime); + + if (timeToEnd < 20000 && episode != 0 && autoplay) { + // show 20 seconds before the end a button (next episode in 10 seconds) + if(!nextEpBtn.isVisible()) + nextEpBtn.setVisible(true); + + if(countdown != (int)((timeToEnd -10000) / 1000)) { + countdown = (int)((timeToEnd -10000) / 1000); + nextEpBtn.setText("next episode in " + countdown + " seconds"); // TODO translate + bottomVBox.setVisible(true); + } + + // if we are end time -10 seconds, do autoplay, if activated + if(timeToEnd < 10000){ + nextEpBtn.setVisible(false); + autoPlayNewFilm(); } - } else if ((duration - currentTime) < 120) { - // if we are -20ms stop the media + } else if (timeToEnd < 120) { + // if we are 120ms to the end stop the media mediaPlayer.stop(); mainWCon.getDbController().setCurrentTime(film.getStreamUrl(), 0); // reset old video start time + playBtn.setGraphic(play_arrow_black); + } else { + if(nextEpBtn.isVisible()) + nextEpBtn.setVisible(false); } if (!mousePressed) { timeSlider.setValue((currentTime / 1000) / 60); +// int sec = (int)(currentTime / 1000); +// int min = (int) TimeUnit.SECONDS.toMinutes(sec); +// int remSec = sec - (int)TimeUnit.MINUTES.toSeconds(min); +// System.out.println("\nTime: " + min + ":" + remSec); } } }); @@ -182,7 +196,7 @@ public class PlayerController { // hide controls timer initialization final Timer timer = new Timer(); TimerTask controlAnimationTask = null; // task to execute save operation - final long delayTime = 2000; // hide the controls after 2 seconds + final long delayTime = 3000; // hide the controls after 2 seconds @Override public void handle(MouseEvent mouseEvent) { @@ -254,7 +268,6 @@ public class PlayerController { @FXML void playBtnAction(ActionEvent event) { - if (mediaPlayer.getStatus().equals(Status.PLAYING)) { mediaPlayer.pause(); playBtn.setGraphic(play_arrow_black); @@ -263,6 +276,22 @@ public class PlayerController { playBtn.setGraphic(pause_black); } } + + @FXML + void nextEpBtnAction(ActionEvent event) { + autoPlayNewFilm(); + } + + private void autoPlayNewFilm() { + autoplay = false; + mainWCon.getDbController().setCurrentTime(film.getStreamUrl(), 0); // reset old video start time + FilmTabelDataType nextFilm = mainWCon.getDbController().getNextEpisode(film.getTitle(), episode, season); + if (nextFilm != null) { + mediaPlayer.stop(); + init(mainWCon, player, nextFilm); + autoplay = true; + } + } public MediaPlayer getMediaPlayer() { return mediaPlayer; diff --git a/src/main/resources/fxml/PlayerWindow.fxml b/src/main/resources/fxml/PlayerWindow.fxml index 9008334..8d7aa5d 100644 --- a/src/main/resources/fxml/PlayerWindow.fxml +++ b/src/main/resources/fxml/PlayerWindow.fxml @@ -7,8 +7,9 @@ + - + @@ -34,5 +35,15 @@ + + + + + + + + + + From 1d06aaeaa2a4cdad075d5276d81615e9c872efce Mon Sep 17 00:00:00 2001 From: Seil0 Date: Sat, 8 Dec 2018 23:44:17 +0100 Subject: [PATCH 54/88] clean up --- .../application/MainWindowController.java | 21 +++--- .../controller/OMDbAPIController.java | 14 ++-- .../kellerkinder/HomeFlix/player/Player.java | 6 +- .../HomeFlix/player/PlayerController.java | 67 ++++++++++--------- src/main/resources/fxml/PlayerWindow.fxml | 2 +- 5 files changed, 63 insertions(+), 47 deletions(-) diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index f3b4f24..8984d59 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -34,6 +34,8 @@ import java.math.BigInteger; import java.net.URLConnection; import java.util.Locale; import java.util.ResourceBundle; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -214,7 +216,7 @@ public class MainWindowController { initActions(); dbController.init(); -// posterModeStartup(); // TODO testing +// posterModeStartup(); // TODO testing DO NOT USE THIS!! } // Initialize general UI elements @@ -441,7 +443,7 @@ public class MainWindowController { LOGGER.info("loading from cache: " + getCurrentTitle()); dbController.readCache(getCurrentStreamUrl()); } else { - Thread omdbAPIThread = new Thread(new OMDbAPIController(main, dbController, currentTableFilm, omdbAPIKey)); + Thread omdbAPIThread = new Thread(new OMDbAPIController(main, dbController, currentTableFilm, omdbAPIKey, true)); omdbAPIThread.setName("OMDbAPI"); omdbAPIThread.start(); } @@ -817,18 +819,21 @@ public class MainWindowController { */ private void checkAllPosters() { // get all not cached entries, none of them should have a cached poster + ExecutorService executor = Executors.newFixedThreadPool(5); + for (FilmTabelDataType entry : dbController.getAllNotCachedEntries()) { System.out.println(entry.getStreamUrl() + " is NOT cached!"); - // TODO get all needed posters eg cache all not cached entries - // TODO for entries not available show homeflix logo + + Runnable OMDbAPIWorker = new OMDbAPIController(main, dbController, entry, omdbAPIKey, false); + executor.execute(OMDbAPIWorker); + + // TODO for entries not available show homeflix logo and set cached } + executor.shutdown(); -// Thread omdbAPIThread = new Thread(new OMDbAPIController(main, dbController, currentTableFilm, omdbAPIKey)); -// omdbAPIThread.setName("OMDbAPI"); -// omdbAPIThread.start(); + // TODO show loading screen } - // getter and setter public DBController getDbController() { return dbController; diff --git a/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java b/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java index 73e4299..500ef31 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java @@ -50,6 +50,7 @@ public class OMDbAPIController implements Runnable { private String omdbAPIKey; private String URL = "https://www.omdbapi.com/?apikey="; private boolean useEpisode = true; + private boolean refresh; private static final Logger LOGGER = LogManager.getLogger(MainWindowController.class.getName()); /** @@ -59,11 +60,12 @@ public class OMDbAPIController implements Runnable { * @param currentTableFilm the current film object * @param omdbAPIKey the omdbAPI key */ - public OMDbAPIController(Main main, DBController dbController, FilmTabelDataType currentTableFilm, String omdbAPIKey) { + public OMDbAPIController(Main main, DBController dbController, FilmTabelDataType currentTableFilm, String omdbAPIKey, boolean refresh) { this.main = main; this.dbController = dbController; this.currentTableFilm = currentTableFilm; this.omdbAPIKey = omdbAPIKey; + this.refresh = refresh; } @Override @@ -130,10 +132,12 @@ public class OMDbAPIController implements Runnable { dbController.setCached(currentTableFilm.getStreamUrl()); // load data to the MainWindowController - Platform.runLater(() -> { - dbController.readCache(currentTableFilm.getStreamUrl()); - }); - + if (refresh) { + Platform.runLater(() -> { + dbController.readCache(currentTableFilm.getStreamUrl()); + }); + } + return; } diff --git a/src/main/java/kellerkinder/HomeFlix/player/Player.java b/src/main/java/kellerkinder/HomeFlix/player/Player.java index 98da798..71397f2 100644 --- a/src/main/java/kellerkinder/HomeFlix/player/Player.java +++ b/src/main/java/kellerkinder/HomeFlix/player/Player.java @@ -44,9 +44,12 @@ public class Player { * @param mainWindowController the MainWindowController */ public Player(MainWindowController mainWindowController) { + playerController = new PlayerController(mainWindowController, this, mainWindowController.getCurrentTableFilm()); + try { FXMLLoader fxmlLoader = new FXMLLoader(); fxmlLoader.setLocation(getClass().getResource("/fxml/PlayerWindow.fxml")); + fxmlLoader.setController(playerController); pane = (AnchorPane) fxmlLoader.load(); stage = new Stage(); scene = new Scene(pane); @@ -62,8 +65,7 @@ public class Player { } }); - playerController = fxmlLoader.getController(); - playerController.init(mainWindowController, this, mainWindowController.getCurrentTableFilm()); + playerController.init(); stage.setFullScreen(true); stage.show(); diff --git a/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java b/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java index 26f73cf..44f76ba 100644 --- a/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java +++ b/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java @@ -87,19 +87,23 @@ public class PlayerController { private ImageView pause_black = new ImageView(new Image("icons/ic_pause_black_24dp_1x.png")); private ImageView fullscreen_black = new ImageView(new Image("icons/ic_fullscreen_black_24dp_1x.png")); private ImageView fullscreen_exit_black = new ImageView(new Image("icons/ic_fullscreen_exit_black_24dp_1x.png")); - - /** FIXME double set currentTime() - * initialize the new PlayerWindow - * @param entry the film object - * @param player the player object (needed for closing action) - * @param dbController the dbController object + + /** + * create a new PlayerWindow object + * @param mainWCon the MainWindowController TODO do we need this? + * @param player the player object (needed for closing action) + * @param film the film object */ - public void init(MainWindowController mainWCon, Player player, FilmTabelDataType film) { + public PlayerController(MainWindowController mainWCon, Player player, FilmTabelDataType film) { this.mainWCon = mainWCon; this.player = player; this.film = film; - startTime = mainWCon.getDbController().getCurrentTime(film.getStreamUrl()); - autoplay = mainWCon.isAutoplay(); + } + + /** + * initialize the PlayerWindow + */ + public void init() { initActions(); if (film.getStreamUrl().startsWith("http")) { @@ -107,9 +111,8 @@ public class PlayerController { } else { media = new Media(new File(film.getStreamUrl()).toURI().toString()); } - startTime = mainWCon.getDbController().getCurrentTime(film.getStreamUrl()); - autoplay = mainWCon.isAutoplay(); + // create the MediaPlayer object mediaPlayer = new MediaPlayer(media); mediaView.setPreserveRatio(true); mediaView.setMediaPlayer(mediaPlayer); @@ -120,16 +123,26 @@ public class PlayerController { width.bind(Bindings.selectDouble(mediaView.sceneProperty(), "width")); height.bind(Bindings.selectDouble(mediaView.sceneProperty(), "height")); + startTime = mainWCon.getDbController().getCurrentTime(film.getStreamUrl()); + autoplay = mainWCon.isAutoplay(); season = !film.getSeason().isEmpty() ? Integer.parseInt(film.getSeason()) : 0; episode = !film.getEpisode().isEmpty() ? Integer.parseInt(film.getEpisode()) : 0; -// nextEpBtn.setStyle("-fx-button-type: RAISED; -fx-background-color: #" + mainWCon.getColor() + "; -fx-text-fill: WHITE;"); + initMediaPlayer(); + + // set the control elements to the correct value + stopBtn.setGraphic(stop_black); + playBtn.setGraphic(pause_black); + fullscreenBtn.setGraphic(fullscreen_exit_black); + timeSlider.setValue(0); + } + + private void initMediaPlayer() { // start the media if the player is ready mediaPlayer.setOnReady(new Runnable() { @Override public void run() { duration = media.getDuration().toMillis(); - timeSlider.setMax((duration / 1000) / 60); mediaPlayer.play(); @@ -146,17 +159,17 @@ public class PlayerController { if (timeToEnd < 20000 && episode != 0 && autoplay) { // show 20 seconds before the end a button (next episode in 10 seconds) - if(!nextEpBtn.isVisible()) + if (!nextEpBtn.isVisible()) nextEpBtn.setVisible(true); - - if(countdown != (int)((timeToEnd -10000) / 1000)) { - countdown = (int)((timeToEnd -10000) / 1000); + + if (countdown != (int) ((timeToEnd - 10000) / 1000)) { + countdown = (int) ((timeToEnd - 10000) / 1000); nextEpBtn.setText("next episode in " + countdown + " seconds"); // TODO translate bottomVBox.setVisible(true); - } - + } + // if we are end time -10 seconds, do autoplay, if activated - if(timeToEnd < 10000){ + if (timeToEnd < 10000) { nextEpBtn.setVisible(false); autoPlayNewFilm(); } @@ -166,25 +179,16 @@ public class PlayerController { mainWCon.getDbController().setCurrentTime(film.getStreamUrl(), 0); // reset old video start time playBtn.setGraphic(play_arrow_black); } else { - if(nextEpBtn.isVisible()) + if (nextEpBtn.isVisible()) nextEpBtn.setVisible(false); } if (!mousePressed) { timeSlider.setValue((currentTime / 1000) / 60); -// int sec = (int)(currentTime / 1000); -// int min = (int) TimeUnit.SECONDS.toMinutes(sec); -// int remSec = sec - (int)TimeUnit.MINUTES.toSeconds(min); -// System.out.println("\nTime: " + min + ":" + remSec); } } }); - // set the control elements to the correct value - stopBtn.setGraphic(stop_black); - playBtn.setGraphic(pause_black); - fullscreenBtn.setGraphic(fullscreen_exit_black); - timeSlider.setValue(0); } /** @@ -288,7 +292,8 @@ public class PlayerController { FilmTabelDataType nextFilm = mainWCon.getDbController().getNextEpisode(film.getTitle(), episode, season); if (nextFilm != null) { mediaPlayer.stop(); - init(mainWCon, player, nextFilm); + film = nextFilm; + init(); autoplay = true; } } diff --git a/src/main/resources/fxml/PlayerWindow.fxml b/src/main/resources/fxml/PlayerWindow.fxml index 8d7aa5d..cfbdd27 100644 --- a/src/main/resources/fxml/PlayerWindow.fxml +++ b/src/main/resources/fxml/PlayerWindow.fxml @@ -9,7 +9,7 @@ - + From 29c6b30168d150013952bfe57d61b65faecd3f8d Mon Sep 17 00:00:00 2001 From: Seil0 Date: Thu, 20 Dec 2018 13:47:32 +0100 Subject: [PATCH 55/88] small omdbAPI query fixes --- .../application/MainWindowController.java | 2 -- .../HomeFlix/controller/DBController.java | 4 ++- .../controller/OMDbAPIController.java | 31 +++++++++++++------ 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index 8984d59..0b31bd4 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -826,8 +826,6 @@ public class MainWindowController { Runnable OMDbAPIWorker = new OMDbAPIController(main, dbController, entry, omdbAPIKey, false); executor.execute(OMDbAPIWorker); - - // TODO for entries not available show homeflix logo and set cached } executor.shutdown(); diff --git a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java index fcd332c..3c7cdaa 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java @@ -389,7 +389,7 @@ public class DBController { try { PreparedStatement ps = connection.prepareStatement("insert into cache values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"); - LOGGER.info("adding to cache: " + omdbResponse.getTitle()); + LOGGER.info("adding cache for: " + streamUrl); ps.setString(1,streamUrl); ps.setString(2,omdbResponse.getTitle()); ps.setString(3,omdbResponse.getYear()); @@ -550,6 +550,8 @@ public class DBController { } catch (SQLException e) { LOGGER.error("An error occured, while getting all NOT cached entries", e); } + + LOGGER.info("There are {} entries not Chached!", notCachedEntries.size()); return notCachedEntries; } diff --git a/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java b/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java index 500ef31..9f96abc 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java @@ -73,13 +73,19 @@ public class OMDbAPIController implements Runnable { LOGGER.info("Querying omdbAPI ..."); JsonObject object; object = getByTitle(currentTableFilm.getTitle()); - if (object == null) return; + if (object == null) { + LOGGER.error("Fatal error while querying omdbAPI!"); + return; + } + // if the answer contains "not found!" try to search by title if (object.getString("Error", "").contains("not found!")) { String title = searchByTitle(currentTableFilm.getTitle()); if (title.length() > 0) { + // we have at least on answer, get info by title now object = getByTitle(title); + // if we still have nothing found, get info by title without episode if(object.getString("Error", "").contains("Series or episode not found!")) { useEpisode = false; object = getByTitle(title); @@ -127,15 +133,18 @@ public class OMDbAPIController implements Runnable { LOGGER.error(e); } - // adding to cache - dbController.addCache(currentTableFilm.getStreamUrl(), omdbResponse); - dbController.setCached(currentTableFilm.getStreamUrl()); - // load data to the MainWindowController - if (refresh) { - Platform.runLater(() -> { - dbController.readCache(currentTableFilm.getStreamUrl()); - }); + synchronized (this) { + // adding to cache + dbController.addCache(currentTableFilm.getStreamUrl(), omdbResponse); + dbController.setCached(currentTableFilm.getStreamUrl()); + + // load data to the MainWindowController + if (refresh) { + Platform.runLater(() -> { + dbController.readCache(currentTableFilm.getStreamUrl()); + }); + } } return; @@ -206,7 +215,9 @@ public class OMDbAPIController implements Runnable { return movie.asObject().getString("Title", ""); } } else { - LOGGER.warn("Movie not found! Not adding cache!"); + // TODO set cached 1 and set the HomeFlix logo as picture +// System.out.println("Object is: " + searchObject); + LOGGER.warn("Movie \"{}\" not found! Not adding cache!", title); } return ""; } From 2daeb86d672310fb38d3a812cad9c73eb49d9c12 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Thu, 3 Jan 2019 01:54:52 +0100 Subject: [PATCH 56/88] updated readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e9d7e1b..fce24c2 100644 --- a/README.md +++ b/README.md @@ -15,4 +15,4 @@ The dev branch is **only merged** into master when a **new release** is released ## Screenshots ![Screenshot](https://raw.githubusercontent.com/Seil0/Seil0.github.io/master/images/Project-HomeFlix_MainWindow.png) -Project-HomeFlix © 2016-2018 mosad www.mosad.xyz, Project by [@Seil0](https://git.mosad.xyz/Seil0) and [@localhorst](https://git.mosad.xyz/localhorst) \ No newline at end of file +Project-HomeFlix © 2016-2019 mosad www.mosad.xyz, Project by [@Seil0](https://git.mosad.xyz/Seil0) and [@localhorst](https://git.mosad.xyz/localhorst) From 060527ae03c9eec2056fa237b8ac1e9aed4996e1 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Fri, 4 Jan 2019 23:35:42 +0100 Subject: [PATCH 57/88] removed travis.yml --- .travis.yml | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 2b01596..0000000 --- a/.travis.yml +++ /dev/null @@ -1,4 +0,0 @@ -language: java -jdk: - - oraclejdk9 - - oraclejdk10 From d3d22db7a8c564361338d66dd066d54218f23252 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Tue, 8 Jan 2019 17:10:33 +0100 Subject: [PATCH 58/88] moved static vars and save & load code to XMLController * moved static vars and save & load code to XMLController * code clean up * happy new year --- .../HomeFlix/application/Main.java | 181 ++--------- .../application/MainWindowController.java | 149 +++------ .../HomeFlix/controller/DBController.java | 15 +- .../controller/OMDbAPIController.java | 10 +- .../controller/SourcesController.java | 9 +- .../HomeFlix/controller/UpdateController.java | 3 +- .../HomeFlix/controller/XMLController.java | 294 ++++++++++++++++++ .../HomeFlix/datatypes/DatabaseDataType.java | 2 +- .../HomeFlix/datatypes/FilmTabelDataType.java | 2 +- .../datatypes/OMDbAPIResponseDataType.java | 3 +- .../HomeFlix/datatypes/SourceDataType.java | 2 +- .../kellerkinder/HomeFlix/player/Player.java | 3 +- .../HomeFlix/player/PlayerController.java | 5 +- .../Alerts/JFX2BtnCancelAlert.java | 3 +- .../org/kellerkinder/Alerts/JFXInfoAlert.java | 3 +- .../locals/HomeFlix-Local_de_DE.properties | 2 +- .../locals/HomeFlix-Local_en_US.properties | 2 +- 17 files changed, 392 insertions(+), 296 deletions(-) create mode 100644 src/main/java/kellerkinder/HomeFlix/controller/XMLController.java diff --git a/src/main/java/kellerkinder/HomeFlix/application/Main.java b/src/main/java/kellerkinder/HomeFlix/application/Main.java index 393c152..f8a71f0 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/Main.java +++ b/src/main/java/kellerkinder/HomeFlix/application/Main.java @@ -1,7 +1,7 @@ /** * Project-HomeFlix * - * Copyright 2016-2018 <@Seil0> + * Copyright 2016-2019 <@Seil0> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,25 +21,15 @@ */ package kellerkinder.HomeFlix.application; -import java.io.BufferedReader; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; import java.util.Locale; -import java.util.Properties; import java.util.ResourceBundle; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.kellerkinder.Alerts.JFX2BtnCancelAlert; -import com.eclipsesource.json.Json; -import com.eclipsesource.json.JsonObject; - import javafx.application.Application; import javafx.event.ActionEvent; import javafx.event.EventHandler; @@ -50,6 +40,7 @@ import javafx.scene.layout.AnchorPane; import javafx.stage.DirectoryChooser; import javafx.stage.FileChooser; import javafx.stage.Stage; +import kellerkinder.HomeFlix.controller.XMLController; public class Main extends Application { @@ -57,27 +48,15 @@ public class Main extends Application { private Scene scene; private AnchorPane pane; private MainWindowController mainWindowController; - private static String userHome = System.getProperty("user.home"); - private static String userName = System.getProperty("user.name"); - private static String osName = System.getProperty("os.name"); - private static String osArch = System.getProperty("os.arch"); - private static String osVers = System.getProperty("os.version"); - private static String javaVers = System.getProperty("java.version"); - private static String javaVend = System.getProperty("java.vendor"); - private static String local = System.getProperty("user.language") + "_" + System.getProperty("user.country"); - private static String dirHomeFlix; - private static File directory; - private static File configFile; - private static File posterCache; + private static XMLController xmlController; private ResourceBundle bundle; private static Logger LOGGER; - private Properties props = new Properties(); @Override public void start(Stage primaryStage) throws IOException { - LOGGER.info("OS: " + osName + " " + osVers + " " + osArch); - LOGGER.info("Java: " + javaVend + " " + javaVers); - LOGGER.info("User: " + userName + " " + userHome); + LOGGER.info("OS: " + XMLController.getOsName() + " " + XMLController.getOsVers() + " " + XMLController.getOsVers()); + LOGGER.info("Java: " + XMLController.getJavaVend() + " " + XMLController.getJavaVers()); + LOGGER.info("User: " + XMLController.getUserName() + " " + XMLController.getUserHome()); this.primaryStage = primaryStage; mainWindowController = new MainWindowController(this); @@ -109,20 +88,16 @@ public class Main extends Application { primaryStage.show(); // show stage // startup checks - if (!configFile.exists()) { - directory.mkdir(); + if (!XMLController.getConfigFile().exists()) { + XMLController.getDirHomeFlix().mkdir(); System.out.println("config not found"); addFirstSource(); - mainWindowController.setColor("ee3523"); - mainWindowController.setFontSize(17.0); - mainWindowController.setAutoUpdate(false); - mainWindowController.setLocal(local); - saveSettings(); + xmlController.saveSettings(); } - if (!posterCache.exists()) { - posterCache.mkdir(); + if (!XMLController.getPosterCache().exists()) { + XMLController.getPosterCache().mkdir(); } mainWindowController.init(); @@ -136,31 +111,30 @@ public class Main extends Application { * @param args arguments given at the start */ public static void main(String[] args) { - - if (osName.contains("Windows")) { - dirHomeFlix = userHome + "/Documents/HomeFlix"; + // Logger initialization + String logPath = ""; + + if (System.getProperty("os.name").contains("Windows")) { + logPath = System.getProperty("user.home") + "/Documents/HomeFlix/app.log"; } else { - dirHomeFlix = userHome + "/HomeFlix"; + logPath = System.getProperty("user.home") + "/HomeFlix/app.log"; } - // set the concrete files - directory = new File(dirHomeFlix); - configFile = new File(dirHomeFlix + "/config.xml"); - posterCache = new File(dirHomeFlix + "/posterCache"); - - System.setProperty("logFilename", dirHomeFlix + "/app.log"); - File logFile = new File(dirHomeFlix + "/app.log"); + System.setProperty("logFilename", logPath); + File logFile = new File(logPath); logFile.delete(); LOGGER = LogManager.getLogger(Main.class.getName()); + + xmlController = new XMLController(); launch(args); } - /** + /** TODO this should move * we need to get the path for the first source from the user and add it to * sources.json, if the user ends the file-/directory-chooser the program will exit */ void addFirstSource() { - switch (local) { + switch (XMLController.getSysLocal()) { case "en_US": bundle = ResourceBundle.getBundle("locals.HomeFlix-Local", Locale.US); // us_english break; @@ -217,103 +191,6 @@ public class Main extends Application { selectFirstSource.showAndWait(); } - /** - * save the configuration to the config.xml file - */ - public void saveSettings() { - LOGGER.info("saving settings ..."); - try { - props.setProperty("color", mainWindowController.getColor()); - props.setProperty("autoUpdate", String.valueOf(mainWindowController.isAutoUpdate())); - props.setProperty("useBeta", String.valueOf(mainWindowController.isUseBeta())); - props.setProperty("autoplay", String.valueOf(mainWindowController.isAutoplay())); - props.setProperty("size", mainWindowController.getFontSize().toString()); - props.setProperty("local", mainWindowController.getLocal()); - - OutputStream outputStream = new FileOutputStream(getConfigFile()); // new output-stream - props.storeToXML(outputStream, "Project HomeFlix settings"); // write new .xml - outputStream.close(); - } catch (IOException e) { - LOGGER.error("An error occurred while saving the settings!", e); - } - } - - /** - * load the configuration from the config.xml file - * and try to load the API keys from apiKeys.json - */ - public void loadSettings() { - LOGGER.info("loading settings ..."); - - try { - InputStream inputStream = new FileInputStream(getConfigFile()); - props.loadFromXML(inputStream); // new input-stream from .xml - - try { - mainWindowController.setColor(props.getProperty("color")); - } catch (Exception e) { - LOGGER.error("cloud not load color", e); - mainWindowController.setColor("00a8cc"); - } - - try { - mainWindowController.setFontSize(Double.parseDouble(props.getProperty("size"))); - } catch (Exception e) { - LOGGER.error("cloud not load fontsize", e); - mainWindowController.setFontSize(17.0); - } - - try { - mainWindowController.setAutoUpdate(Boolean.parseBoolean(props.getProperty("autoUpdate"))); - } catch (Exception e) { - LOGGER.error("cloud not load autoUpdate", e); - mainWindowController.setAutoUpdate(false); - } - - try { - mainWindowController.setUseBeta(Boolean.parseBoolean(props.getProperty("useBeta"))); - } catch (Exception e) { - LOGGER.error("cloud not load autoUpdate", e); - mainWindowController.setUseBeta(false); - } - - try { - mainWindowController.setAutoplay(Boolean.parseBoolean(props.getProperty("autoplay"))); - } catch (Exception e) { - LOGGER.error("cloud not load autoplay", e); - mainWindowController.setAutoplay(false); - } - - try { - mainWindowController.setLocal(props.getProperty("local")); - } catch (Exception e) { - LOGGER.error("cloud not load local", e); - mainWindowController.setLocal(System.getProperty("user.language") + "_" + System.getProperty("user.country")); - } - - inputStream.close(); - } catch (IOException e) { - LOGGER.error("An error occurred while loading the settings!", e); - } - - // try loading the omdbAPI key - try { - InputStream in = getClass().getClassLoader().getResourceAsStream("apiKeys.json"); - if (in != null) { - BufferedReader reader = new BufferedReader(new InputStreamReader(in)); - JsonObject apiKeys = Json.parse(reader).asObject(); - mainWindowController.setOmdbAPIKey(apiKeys.getString("omdbAPIKey", "")); - reader.close(); - in.close(); - } else { - LOGGER.warn("Cloud not load apiKeys.json. No such file"); - } - } catch (Exception e) { - LOGGER.error("Cloud not load the omdbAPI key. Please contact the developer!", e); - } - } - - public Stage getPrimaryStage() { return primaryStage; } @@ -321,16 +198,4 @@ public class Main extends Application { public AnchorPane getPane() { return pane; } - - public File getDirectory() { - return directory; - } - - public File getConfigFile() { - return configFile; - } - - public File getPosterCache() { - return posterCache; - } } diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index 0b31bd4..8433d61 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -1,7 +1,7 @@ /** * Project-HomeFlix * - * Copyright 2016-2018 <@Seil0> + * Copyright 2016-2019 <@Seil0> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -86,6 +86,7 @@ import javafx.util.Duration; import kellerkinder.HomeFlix.controller.DBController; import kellerkinder.HomeFlix.controller.OMDbAPIController; import kellerkinder.HomeFlix.controller.UpdateController; +import kellerkinder.HomeFlix.controller.XMLController; import kellerkinder.HomeFlix.datatypes.SourceDataType; import kellerkinder.HomeFlix.player.Player; import kellerkinder.HomeFlix.datatypes.FilmTabelDataType; @@ -153,29 +154,22 @@ public class MainWindowController { // poster-mode // @FXML private AnchorPane posterModeAnchorPane; - private Main main; private MainWindowController mainWindowController; private UpdateController updateController; private DBController dbController; + private XMLController xmlController; private static final Logger LOGGER = LogManager.getLogger(MainWindowController.class.getName()); private boolean menuTrue = false; private boolean settingsTrue = false; - private boolean autoUpdate = false; - private boolean useBeta = false; - private boolean autoplay = false; private final String version = "0.7.0"; - private final String buildNumber = "165"; + private final String buildNumber = "167"; private final String versionName = "toothless dragon"; private String btnStyle; - private String color; - private String local; - private String omdbAPIKey; - private double fontSize; private final int hashA = -647380320; private int last; private int indexTable; @@ -200,14 +194,15 @@ public class MainWindowController { public MainWindowController(Main main) { this.main = main; mainWindowController = this; - dbController = new DBController(this.main, this); + xmlController = new XMLController(); // TODO pass it over from main? + dbController = new DBController(this); } public void init() { LOGGER.info("Initializing Project-HomeFlix build " + buildNumber); - main.loadSettings(); // load settings + xmlController.loadSettings(); // load settings checkAutoUpdate(); // initialize the GUI and the DBController @@ -222,16 +217,16 @@ public class MainWindowController { // Initialize general UI elements private void initUI() { versionLbl.setText("Version: " + version + " (Build: " + buildNumber + ")"); - fontsizeSlider.setValue(getFontSize()); - colorPicker.setValue(Color.valueOf(getColor())); + fontsizeSlider.setValue(XMLController.getFontSize()); + colorPicker.setValue(Color.valueOf(XMLController.getColor())); updateBtn.setFont(Font.font("System", 12)); - autoUpdateToggleBtn.setSelected(isAutoUpdate()); - autoplayToggleBtn.setSelected(isAutoplay()); + autoUpdateToggleBtn.setSelected(XMLController.isAutoUpdate()); + autoplayToggleBtn.setSelected(XMLController.isAutoplay()); languageChoisBox.setItems(languages); branchChoisBox.setItems(branches); - if (isUseBeta()) { + if (XMLController.isUseBeta()) { branchChoisBox.getSelectionModel().select(1); } else { branchChoisBox.getSelectionModel().select(0); @@ -303,7 +298,7 @@ public class MainWindowController { } if (settingsTrue) { settingsScrollPane.setVisible(false); - main.saveSettings(); + xmlController.saveSettings(); settingsTrue = false; } }); @@ -313,9 +308,9 @@ public class MainWindowController { public void changed(ObservableValue ov, Number value, Number new_value) { String local = languageChoisBox.getItems().get((int) new_value).toString(); local = local.substring(local.length() - 6, local.length() - 1); // reading only en_US from English (en_US) - setLocal(local); + XMLController.setUsrLocal(local); setLocalUI(); - main.saveSettings(); + xmlController.saveSettings(); } }); @@ -323,22 +318,22 @@ public class MainWindowController { @Override public void changed(ObservableValue ov, Number value, Number new_value) { if (branchChoisBox.getItems().get((int) new_value).toString() == "beta") { - setUseBeta(true); + XMLController.setUseBeta(true); } else { - setUseBeta(false); + XMLController.setUseBeta(false); } - main.saveSettings(); + xmlController.saveSettings(); } }); fontsizeSlider.valueProperty().addListener(new ChangeListener() { @Override public void changed(ObservableValue ov, Number old_val, Number new_val) { - setFontSize(fontsizeSlider.getValue()); + XMLController.setFontSize(fontsizeSlider.getValue()); if (!getCurrentTitle().isEmpty()) { dbController.readCache(getCurrentStreamUrl()); } - main.saveSettings(); + xmlController.saveSettings(); } }); @@ -362,7 +357,7 @@ public class MainWindowController { filmRoot.getChildren().add(new TreeItem(filterData.get(i))); // add filtered data to root node after search } if (searchTextField.getText().hashCode() == hashA) { - setColor("000000"); + XMLController.setColor("000000"); colorPicker.setValue(new Color(0, 0, 0, 1)); applyColor(); } @@ -443,7 +438,7 @@ public class MainWindowController { LOGGER.info("loading from cache: " + getCurrentTitle()); dbController.readCache(getCurrentStreamUrl()); } else { - Thread omdbAPIThread = new Thread(new OMDbAPIController(main, dbController, currentTableFilm, omdbAPIKey, true)); + Thread omdbAPIThread = new Thread(new OMDbAPIController(dbController, currentTableFilm, XMLController.getOmdbAPIKey(), true)); omdbAPIThread.setName("OMDbAPI"); omdbAPIThread.start(); } @@ -538,7 +533,7 @@ public class MainWindowController { private void settingsBtnclicked() { if (settingsTrue) { settingsScrollPane.setVisible(false); - main.saveSettings(); + xmlController.saveSettings(); settingsTrue = false; } else { settingsScrollPane.setVisible(true); @@ -572,29 +567,29 @@ public class MainWindowController { @FXML private void colorPickerAction() { - setColor(colorPicker.getValue().toString().substring(2, 10)); - main.saveSettings(); + XMLController.setColor(colorPicker.getValue().toString().substring(2, 10)); + xmlController.saveSettings(); applyColor(); } @FXML private void updateBtnAction() { - updateController = new UpdateController(this, buildNumber, useBeta); + updateController = new UpdateController(this, buildNumber, XMLController.isUseBeta()); Thread updateThread = new Thread(updateController); updateThread.setName("Updater"); updateThread.start(); } @FXML - private void autoUpdateToggleBtnAction(){ - autoUpdate = isAutoUpdate() ? false : true; - main.saveSettings(); + private void autoUpdateToggleBtnAction() { + XMLController.setAutoUpdate(!XMLController.isAutoUpdate()); + xmlController.saveSettings(); } @FXML private void autoplayToggleBtnAction(){ - autoplay = isAutoplay() ? false : true; - main.saveSettings(); + XMLController.setAutoplay(!XMLController.isAutoplay()); + xmlController.saveSettings(); } // refresh the selected child of the root node @@ -652,9 +647,9 @@ public class MainWindowController { try { // read old array - File oldSources = new File(main.getDirectory() + "/sources.json"); + File oldSources = new File(XMLController.getDirHomeFlix() + "/sources.json"); if (oldSources.exists()) { - newsources = Json.parse(new FileReader(main.getDirectory() + "/sources.json")).asArray(); + newsources = Json.parse(new FileReader(XMLController.getDirHomeFlix() + "/sources.json")).asArray(); } else { newsources = Json.array(); } @@ -662,7 +657,7 @@ public class MainWindowController { // add new source source = Json.object().add("path", path).add("mode", mode); newsources.add(source); - Writer writer = new FileWriter(main.getDirectory() + "/sources.json"); + Writer writer = new FileWriter(XMLController.getDirHomeFlix() + "/sources.json"); newsources.writeTo(writer); writer.close(); } catch (IOException e) { @@ -682,11 +677,11 @@ public class MainWindowController { */ private void applyColor() { String menuBtnStyle; - BigInteger usedColor = new BigInteger(getColor(), 16); + BigInteger usedColor = new BigInteger(XMLController.getColor(), 16); BigInteger checkColor = new BigInteger("78909cff", 16); if (usedColor.compareTo(checkColor) == -1) { - btnStyle = "-fx-button-type: RAISED; -fx-background-color: #" + getColor() + "; -fx-text-fill: WHITE;"; + btnStyle = "-fx-button-type: RAISED; -fx-background-color: #" + XMLController.getColor() + "; -fx-text-fill: WHITE;"; menuBtnStyle = "-fx-text-fill: WHITE;"; playbtn.setGraphic(new ImageView(new Image("icons/ic_play_arrow_white_18dp_1x.png"))); @@ -696,7 +691,7 @@ public class MainWindowController { menuHam.getStyleClass().clear(); menuHam.getStyleClass().add("jfx-hamburgerW"); } else { - btnStyle = "-fx-button-type: RAISED; -fx-background-color: #" + getColor() + "; -fx-text-fill: BLACK;"; + btnStyle = "-fx-button-type: RAISED; -fx-background-color: #" + XMLController.getColor() + "; -fx-text-fill: BLACK;"; menuBtnStyle = "-fx-text-fill: BLACK;"; playbtn.setGraphic(new ImageView(new Image("icons/ic_play_arrow_black_18dp_1x.png"))); @@ -708,9 +703,9 @@ public class MainWindowController { } // boxes and TextFields - sideMenuVBox.setStyle("-fx-background-color: #" + getColor() + ";"); - topHBox.setStyle("-fx-background-color: #" + getColor() + ";"); - searchTextField.setFocusColor(Color.valueOf(getColor())); + sideMenuVBox.setStyle("-fx-background-color: #" + XMLController.getColor() + ";"); + topHBox.setStyle("-fx-background-color: #" + XMLController.getColor() + ";"); + searchTextField.setFocusColor(Color.valueOf(XMLController.getColor())); // normal buttons addDirectoryBtn.setStyle(btnStyle); @@ -747,7 +742,7 @@ public class MainWindowController { * set the local based on the languageChoisBox selection */ void setLocalUI() { - switch (getLocal()) { + switch (XMLController.getUsrLocal()) { case "en_US": setBundle(ResourceBundle.getBundle("locals.HomeFlix-Local", Locale.US)); // us_English languageChoisBox.getSelectionModel().select(0); @@ -785,10 +780,10 @@ public class MainWindowController { // if AutoUpdate, then check for updates private void checkAutoUpdate() { - if (isAutoUpdate()) { + if (XMLController.isAutoUpdate()) { try { LOGGER.info("AutoUpdate: looking for updates on startup ..."); - updateController = new UpdateController(this, buildNumber, useBeta); + updateController = new UpdateController(this, buildNumber, XMLController.isUseBeta()); Thread updateThread = new Thread(updateController); updateThread.setName("Updater"); updateThread.start(); @@ -824,7 +819,7 @@ public class MainWindowController { for (FilmTabelDataType entry : dbController.getAllNotCachedEntries()) { System.out.println(entry.getStreamUrl() + " is NOT cached!"); - Runnable OMDbAPIWorker = new OMDbAPIController(main, dbController, entry, omdbAPIKey, false); + Runnable OMDbAPIWorker = new OMDbAPIController(dbController, entry, XMLController.getOmdbAPIKey(), false); executor.execute(OMDbAPIWorker); } executor.shutdown(); @@ -836,14 +831,6 @@ public class MainWindowController { public DBController getDbController() { return dbController; } - - public String getColor() { - return color; - } - - public void setColor(String input) { - this.color = input; - } public FilmTabelDataType getCurrentTableFilm() { return currentTableFilm; @@ -857,14 +844,6 @@ public class MainWindowController { return currentTableFilm.getStreamUrl(); } - public Double getFontSize() { - return fontSize; - } - - public void setFontSize(Double input) { - this.fontSize = input; - } - public int getIndexTable() { return indexTable; } @@ -873,46 +852,6 @@ public class MainWindowController { return indexList; } - public boolean isAutoUpdate() { - return autoUpdate; - } - - public void setAutoUpdate(boolean input) { - this.autoUpdate = input; - } - - public boolean isUseBeta() { - return useBeta; - } - - public void setUseBeta(boolean useBeta) { - this.useBeta = useBeta; - } - - public boolean isAutoplay() { - return autoplay; - } - - public void setAutoplay(boolean autoplay) { - this.autoplay = autoplay; - } - - public String getLocal() { - return local; - } - - public void setLocal(String input) { - this.local = input; - } - - public String getOmdbAPIKey() { - return omdbAPIKey; - } - - public void setOmdbAPIKey(String omdbAPIKey) { - this.omdbAPIKey = omdbAPIKey; - } - public ObservableList getFilmsList() { return filmsList; } diff --git a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java index 3c7cdaa..8ed23b9 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java @@ -1,7 +1,7 @@ /** * Project-HomeFlix * - * Copyright 2016-2018 <@Seil0> + * Copyright 2016-2019 <@Seil0> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -44,7 +44,6 @@ import javafx.scene.image.ImageView; import javafx.scene.text.Font; import javafx.scene.text.FontWeight; import javafx.scene.text.Text; -import kellerkinder.HomeFlix.application.Main; import kellerkinder.HomeFlix.application.MainWindowController; import kellerkinder.HomeFlix.datatypes.DatabaseDataType; import kellerkinder.HomeFlix.datatypes.FilmTabelDataType; @@ -53,7 +52,6 @@ import kellerkinder.HomeFlix.datatypes.OMDbAPIResponseDataType; public class DBController { private MainWindowController mainWindowController; - private Main main; private String DB_PATH; private Image favorite_black = new Image("icons/ic_favorite_black_18dp_1x.png"); private Image favorite_border_black = new Image("icons/ic_favorite_border_black_18dp_1x.png"); @@ -67,8 +65,7 @@ public class DBController { * @param main the Main object * @param mainWindowController the MainWindowController object */ - public DBController(Main main, MainWindowController mainWindowController) { - this.main = main; + public DBController(MainWindowController mainWindowController) { this.mainWindowController = mainWindowController; } @@ -91,7 +88,7 @@ public class DBController { * AutoCommit is set to false to prevent some issues, so manual commit is active! */ private void initDatabaseConnection() { - DB_PATH = main.getDirectory() + "/Homeflix.db"; + DB_PATH = XMLController.getDirHomeFlix() + "/Homeflix.db"; try { // create a database connection connection = DriverManager.getConnection("jdbc:sqlite:" + DB_PATH); @@ -146,7 +143,7 @@ public class DBController { * load all sources */ private void loadSources() { - SourcesController sourcesController = new SourcesController(main, mainWindowController); + SourcesController sourcesController = new SourcesController(mainWindowController); sourceStreams = sourcesController.loadSources(); } @@ -456,7 +453,7 @@ public class DBController { PreparedStatement ps = connection.prepareStatement("SELECT * FROM cache WHERE streamUrl = ?"); ps.setString(1, streamUrl); ResultSet rs = ps.executeQuery(); - Font font = Font.font("System", FontWeight.BOLD, (int) Math.round(mainWindowController.getFontSize())); + Font font = Font.font("System", FontWeight.BOLD, (int) Math.round(XMLController.getFontSize())); ObservableList textFlow = mainWindowController.getTextFlow().getChildren(); ArrayList nameText = new ArrayList(); @@ -513,7 +510,7 @@ public class DBController { textFlow.addAll(nameText.get(18), new Text(rs.getString("BoxOffice") + "\n")); textFlow.addAll(nameText.get(19), new Text(rs.getString("Website") + "\n")); - mainWindowController.getTextFlow().setStyle("-fx-font-size : " + ((int) Math.round(mainWindowController.getFontSize()) + 1) + "px;"); + mainWindowController.getTextFlow().setStyle("-fx-font-size : " + ((int) Math.round(XMLController.getFontSize()) + 1) + "px;"); // add the image try { diff --git a/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java b/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java index 9f96abc..008d949 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java @@ -1,7 +1,7 @@ /** * Project-HomeFlix * - * Copyright 2018 <@Seil0> + * Copyright 2018-2019 <@Seil0> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,6 +19,7 @@ * MA 02110-1301, USA. * */ + package kellerkinder.HomeFlix.controller; import java.awt.image.BufferedImage; @@ -37,14 +38,12 @@ import com.eclipsesource.json.JsonObject; import com.eclipsesource.json.JsonValue; import javafx.application.Platform; -import kellerkinder.HomeFlix.application.Main; import kellerkinder.HomeFlix.application.MainWindowController; import kellerkinder.HomeFlix.datatypes.FilmTabelDataType; import kellerkinder.HomeFlix.datatypes.OMDbAPIResponseDataType; public class OMDbAPIController implements Runnable { - private Main main; private DBController dbController; private FilmTabelDataType currentTableFilm; private String omdbAPIKey; @@ -60,8 +59,7 @@ public class OMDbAPIController implements Runnable { * @param currentTableFilm the current film object * @param omdbAPIKey the omdbAPI key */ - public OMDbAPIController(Main main, DBController dbController, FilmTabelDataType currentTableFilm, String omdbAPIKey, boolean refresh) { - this.main = main; + public OMDbAPIController(DBController dbController, FilmTabelDataType currentTableFilm, String omdbAPIKey, boolean refresh) { this.dbController = dbController; this.currentTableFilm = currentTableFilm; this.omdbAPIKey = omdbAPIKey; @@ -126,7 +124,7 @@ public class OMDbAPIController implements Runnable { try { BufferedImage originalImage = ImageIO.read(new URL(object.getString("Poster", ""))); // change path to where file is located - omdbResponse.setPoster(main.getPosterCache() + "/" + omdbResponse.getTitle() + ".png"); + omdbResponse.setPoster(XMLController.getPosterCache() + "/" + omdbResponse.getTitle() + ".png"); ImageIO.write(originalImage, "png", new File(omdbResponse.getPoster())); LOGGER.info("adding poster to cache: " + omdbResponse.getPoster()); } catch (Exception e) { diff --git a/src/main/java/kellerkinder/HomeFlix/controller/SourcesController.java b/src/main/java/kellerkinder/HomeFlix/controller/SourcesController.java index a875e67..942f171 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/SourcesController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/SourcesController.java @@ -1,7 +1,7 @@ /** * Project-HomeFlix * - * Copyright 2016-2018 <@Seil0> + * Copyright 2016-2019 <@Seil0> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -37,19 +37,16 @@ import com.eclipsesource.json.JsonArray; import com.eclipsesource.json.JsonObject; import com.eclipsesource.json.JsonValue; -import kellerkinder.HomeFlix.application.Main; import kellerkinder.HomeFlix.application.MainWindowController; import kellerkinder.HomeFlix.datatypes.DatabaseDataType; public class SourcesController { private MainWindowController mainWindowController; - private Main main; private List sourceStreams = new ArrayList(); private static final Logger LOGGER = LogManager.getLogger(SourcesController.class.getName()); - public SourcesController(Main main, MainWindowController mainWindowController) { - this.main = main; + public SourcesController(MainWindowController mainWindowController) { this.mainWindowController = mainWindowController; } @@ -60,7 +57,7 @@ public class SourcesController { try { // create a JsonArray, containing all sources, add each source to the mwc, get all films from it - JsonArray sources = Json.parse(new FileReader(main.getDirectory() + "/sources.json")).asArray(); + JsonArray sources = Json.parse(new FileReader(XMLController.getDirHomeFlix() + "/sources.json")).asArray(); for (JsonValue source : sources) { String path = source.asObject().getString("path", ""); String mode = source.asObject().getString("mode", ""); diff --git a/src/main/java/kellerkinder/HomeFlix/controller/UpdateController.java b/src/main/java/kellerkinder/HomeFlix/controller/UpdateController.java index 7ca27e3..9b71c0a 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/UpdateController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/UpdateController.java @@ -1,7 +1,7 @@ /** * Project-HomeFlix * - * Copyright 2018 <@Seil0> + * Copyright 2018-2019 <@Seil0> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,6 +18,7 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. */ + package kellerkinder.HomeFlix.controller; import java.io.BufferedReader; diff --git a/src/main/java/kellerkinder/HomeFlix/controller/XMLController.java b/src/main/java/kellerkinder/HomeFlix/controller/XMLController.java new file mode 100644 index 0000000..2fb9a45 --- /dev/null +++ b/src/main/java/kellerkinder/HomeFlix/controller/XMLController.java @@ -0,0 +1,294 @@ +/** + * Project-HomeFlix + * + * Copyright 2016-2019 <@Seil0> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + */ + +package kellerkinder.HomeFlix.controller; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.util.Properties; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.eclipsesource.json.Json; +import com.eclipsesource.json.JsonObject; + +public class XMLController { + + private static String userHome = System.getProperty("user.home"); + private static String userName = System.getProperty("user.name"); + private static String osName = System.getProperty("os.name"); + private static String osArch = System.getProperty("os.arch"); + private static String osVers = System.getProperty("os.version"); + private static String javaVers = System.getProperty("java.version"); + private static String javaVend = System.getProperty("java.vendor"); + private static String sysLocal = System.getProperty("user.language") + "_" + System.getProperty("user.country"); + private static String dirHomeFlixPath; + private static File dirHomeFlix; + private static File configFile; + private static File posterCache; + + // user settings + private static String color = "ee3523"; + private static String usrLocal = sysLocal; + private static boolean autoUpdate = false; + private static boolean useBeta = false; + private static boolean autoplay = false; + private static double fontSize = 17; + + // user settings + private static String omdbAPIKey; + + private static final Logger LOGGER = LogManager.getLogger(XMLController.class.getName()); + private Properties props = new Properties(); + + /** + * prepare the start up, this is the first thing call by main + */ + public XMLController() { + + if (osName.contains("Windows")) { + dirHomeFlixPath = userHome + "/Documents/HomeFlix"; + } else { + dirHomeFlixPath = userHome + "/HomeFlix"; + } + + // set the concrete files + dirHomeFlix = new File(dirHomeFlixPath); + configFile = new File(dirHomeFlix + "/config.xml"); + posterCache = new File(dirHomeFlix + "/posterCache"); + + } + + /** + * save the configuration to the config.xml file + */ + public void saveSettings() { + LOGGER.info("saving settings ..."); + try { + props.setProperty("color", color); + props.setProperty("autoUpdate", String.valueOf(autoUpdate)); + props.setProperty("useBeta", String.valueOf(useBeta)); + props.setProperty("autoplay", String.valueOf(autoplay)); + props.setProperty("size", Double.toString(fontSize)); + props.setProperty("local", usrLocal); + + OutputStream outputStream = new FileOutputStream(getConfigFile()); // new output-stream + props.storeToXML(outputStream, "Project HomeFlix settings"); // write new .xml + outputStream.close(); + } catch (IOException e) { + LOGGER.error("An error occurred while saving the settings!", e); + } + } + + /** + * load the configuration from the config.xml file + * and try to load the API keys from apiKeys.json + */ + public void loadSettings() { + LOGGER.info("loading settings ..."); + + try { + InputStream inputStream = new FileInputStream(getConfigFile()); + props.loadFromXML(inputStream); // new input-stream from .xml + + try { + setColor(props.getProperty("color")); + } catch (Exception e) { + LOGGER.error("cloud not load color", e); + setColor("00a8cc"); + } + + try { + setFontSize(Double.parseDouble(props.getProperty("size"))); + } catch (Exception e) { + LOGGER.error("cloud not load fontsize", e); + setFontSize(17.0); + } + + try { + setAutoUpdate(Boolean.parseBoolean(props.getProperty("autoUpdate"))); + } catch (Exception e) { + LOGGER.error("cloud not load autoUpdate", e); + setAutoUpdate(false); + } + + try { + setUseBeta(Boolean.parseBoolean(props.getProperty("useBeta"))); + } catch (Exception e) { + LOGGER.error("cloud not load autoUpdate", e); + setUseBeta(false); + } + + try { + setAutoplay(Boolean.parseBoolean(props.getProperty("autoplay"))); + } catch (Exception e) { + LOGGER.error("cloud not load autoplay", e); + setAutoplay(false); + } + + try { + setUsrLocal(props.getProperty("local")); + } catch (Exception e) { + LOGGER.error("cloud not load local", e); + setUsrLocal(System.getProperty("user.language") + "_" + System.getProperty("user.country")); + } + + inputStream.close(); + } catch (IOException e) { + LOGGER.error("An error occurred while loading the settings!", e); + } + + // try loading the omdbAPI key + try { + InputStream in = getClass().getClassLoader().getResourceAsStream("apiKeys.json"); + if (in != null) { + BufferedReader reader = new BufferedReader(new InputStreamReader(in)); + JsonObject apiKeys = Json.parse(reader).asObject(); + setOmdbAPIKey(apiKeys.getString("omdbAPIKey", "")); + reader.close(); + in.close(); + } else { + LOGGER.warn("Cloud not load apiKeys.json. No such file"); + } + } catch (Exception e) { + LOGGER.error("Cloud not load the omdbAPI key. Please contact the developer!", e); + } + } + + // getters for application variables + + public static String getUserHome() { + return userHome; + } + + public static String getUserName() { + return userName; + } + + public static String getOsName() { + return osName; + } + + public static String getOsArch() { + return osArch; + } + + public static String getOsVers() { + return osVers; + } + + public static String getJavaVers() { + return javaVers; + } + + public static String getJavaVend() { + return javaVend; + } + + public static String getSysLocal() { + return sysLocal; + } + + public static String getDirHomeFlixPath() { + return dirHomeFlixPath; + } + + + public static File getDirHomeFlix() { + return dirHomeFlix; + } + + public static File getConfigFile() { + return configFile; + } + + public static File getPosterCache() { + return posterCache; + } + + // getters for user settings + + public static String getColor() { + return color; + } + + public static void setColor(String color) { + XMLController.color = color; + } + + public static String getUsrLocal() { + return usrLocal; + } + + public static void setUsrLocal(String usrLocal) { + XMLController.usrLocal = usrLocal; + } + + public static boolean isAutoUpdate() { + return autoUpdate; + } + + public static void setAutoUpdate(boolean autoUpdate) { + XMLController.autoUpdate = autoUpdate; + } + + public static boolean isUseBeta() { + return useBeta; + } + + public static void setUseBeta(boolean useBeta) { + XMLController.useBeta = useBeta; + } + + public static boolean isAutoplay() { + return autoplay; + } + + public static void setAutoplay(boolean autoplay) { + XMLController.autoplay = autoplay; + } + + public static double getFontSize() { + return fontSize; + } + + public static void setFontSize(double fontSize) { + XMLController.fontSize = fontSize; + } + + // getters for APIs + + public static String getOmdbAPIKey() { + return omdbAPIKey; + } + + private static void setOmdbAPIKey(String omdbAPIKey) { + XMLController.omdbAPIKey = omdbAPIKey; + } +} diff --git a/src/main/java/kellerkinder/HomeFlix/datatypes/DatabaseDataType.java b/src/main/java/kellerkinder/HomeFlix/datatypes/DatabaseDataType.java index 053f80b..31c089f 100644 --- a/src/main/java/kellerkinder/HomeFlix/datatypes/DatabaseDataType.java +++ b/src/main/java/kellerkinder/HomeFlix/datatypes/DatabaseDataType.java @@ -1,7 +1,7 @@ /** * Project-HomeFlix * - * Copyright 2016-2018 <@Seil0> + * Copyright 2016-2019 <@Seil0> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/kellerkinder/HomeFlix/datatypes/FilmTabelDataType.java b/src/main/java/kellerkinder/HomeFlix/datatypes/FilmTabelDataType.java index c2b86da..b019752 100644 --- a/src/main/java/kellerkinder/HomeFlix/datatypes/FilmTabelDataType.java +++ b/src/main/java/kellerkinder/HomeFlix/datatypes/FilmTabelDataType.java @@ -1,7 +1,7 @@ /** * Project-HomeFlix * - * Copyright 2016-2018 <@Seil0> + * Copyright 2016-2019 <@Seil0> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/kellerkinder/HomeFlix/datatypes/OMDbAPIResponseDataType.java b/src/main/java/kellerkinder/HomeFlix/datatypes/OMDbAPIResponseDataType.java index 7a2efcb..40d78b0 100644 --- a/src/main/java/kellerkinder/HomeFlix/datatypes/OMDbAPIResponseDataType.java +++ b/src/main/java/kellerkinder/HomeFlix/datatypes/OMDbAPIResponseDataType.java @@ -1,7 +1,7 @@ /** * Project-HomeFlix * - * Copyright 2018 <@Seil0> + * Copyright 2018-2019 <@Seil0> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,6 +19,7 @@ * MA 02110-1301, USA. * */ + package kellerkinder.HomeFlix.datatypes; public class OMDbAPIResponseDataType { diff --git a/src/main/java/kellerkinder/HomeFlix/datatypes/SourceDataType.java b/src/main/java/kellerkinder/HomeFlix/datatypes/SourceDataType.java index fec991a..7f16265 100644 --- a/src/main/java/kellerkinder/HomeFlix/datatypes/SourceDataType.java +++ b/src/main/java/kellerkinder/HomeFlix/datatypes/SourceDataType.java @@ -1,7 +1,7 @@ /** * Project-HomeFlix * - * Copyright 2016-2018 <@Seil0> + * Copyright 2016-2019 <@Seil0> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/kellerkinder/HomeFlix/player/Player.java b/src/main/java/kellerkinder/HomeFlix/player/Player.java index 71397f2..3481813 100644 --- a/src/main/java/kellerkinder/HomeFlix/player/Player.java +++ b/src/main/java/kellerkinder/HomeFlix/player/Player.java @@ -1,7 +1,7 @@ /** * Project-HomeFlix * - * Copyright 2016-2018 <@Seil0> + * Copyright 2016-2019 <@Seil0> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,6 +19,7 @@ * MA 02110-1301, USA. * */ + package kellerkinder.HomeFlix.player; import javafx.event.EventHandler; diff --git a/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java b/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java index 44f76ba..f94e7e9 100644 --- a/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java +++ b/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java @@ -1,7 +1,7 @@ /** * Project-HomeFlix * - * Copyright 2016-2018 <@Seil0> + * Copyright 2016-2019 <@Seil0> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -48,6 +48,7 @@ import javafx.scene.media.MediaPlayer.Status; import javafx.scene.media.MediaView; import javafx.util.Duration; import kellerkinder.HomeFlix.application.MainWindowController; +import kellerkinder.HomeFlix.controller.XMLController; import kellerkinder.HomeFlix.datatypes.FilmTabelDataType; public class PlayerController { @@ -124,7 +125,7 @@ public class PlayerController { height.bind(Bindings.selectDouble(mediaView.sceneProperty(), "height")); startTime = mainWCon.getDbController().getCurrentTime(film.getStreamUrl()); - autoplay = mainWCon.isAutoplay(); + autoplay = XMLController.isAutoplay(); season = !film.getSeason().isEmpty() ? Integer.parseInt(film.getSeason()) : 0; episode = !film.getEpisode().isEmpty() ? Integer.parseInt(film.getEpisode()) : 0; diff --git a/src/main/java/org/kellerkinder/Alerts/JFX2BtnCancelAlert.java b/src/main/java/org/kellerkinder/Alerts/JFX2BtnCancelAlert.java index 63b8ff2..5f471a4 100644 --- a/src/main/java/org/kellerkinder/Alerts/JFX2BtnCancelAlert.java +++ b/src/main/java/org/kellerkinder/Alerts/JFX2BtnCancelAlert.java @@ -1,7 +1,7 @@ /** * Kellerkinder Framework Alerts * - * Copyright 2018 <@Seil0> + * Copyright 2018-2019 <@Seil0> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,6 +18,7 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. */ + package org.kellerkinder.Alerts; import com.jfoenix.controls.JFXAlert; diff --git a/src/main/java/org/kellerkinder/Alerts/JFXInfoAlert.java b/src/main/java/org/kellerkinder/Alerts/JFXInfoAlert.java index ab31c4d..5d09315 100644 --- a/src/main/java/org/kellerkinder/Alerts/JFXInfoAlert.java +++ b/src/main/java/org/kellerkinder/Alerts/JFXInfoAlert.java @@ -1,7 +1,7 @@ /** * Kellerkinder Framework Alerts * - * Copyright 2018 <@Seil0> + * Copyright 2018-2019 <@Seil0> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,6 +18,7 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. */ + package org.kellerkinder.Alerts; import com.jfoenix.controls.JFXAlert; diff --git a/src/main/resources/locals/HomeFlix-Local_de_DE.properties b/src/main/resources/locals/HomeFlix-Local_de_DE.properties index 4506fe1..f93973c 100644 --- a/src/main/resources/locals/HomeFlix-Local_de_DE.properties +++ b/src/main/resources/locals/HomeFlix-Local_de_DE.properties @@ -32,7 +32,7 @@ columnFavorite = Favorit #error translations vlcNotInstalled = Um einen Film abspielen wird der VLC Media Player ben\u00F6tigt! -infoText = \nAutoren: \n \u2022 seil0@mosad.xyz \n \u2022 localhorst@mosad.xyz \n(c) 2016-2018 mosad www.mosad.xyz +infoText = \nAutoren: \n \u2022 seil0@mosad.xyz \n \u2022 localhorst@mosad.xyz \n(c) 2016-2019 mosad www.mosad.xyz #textFlow translations title = Titel diff --git a/src/main/resources/locals/HomeFlix-Local_en_US.properties b/src/main/resources/locals/HomeFlix-Local_en_US.properties index f0d0a38..a3db43f 100644 --- a/src/main/resources/locals/HomeFlix-Local_en_US.properties +++ b/src/main/resources/locals/HomeFlix-Local_en_US.properties @@ -32,7 +32,7 @@ columnFavorite = Favorite #error translations vlcNotInstalled = VLC Media Player is required to play a movie! -infoText = \nMaintainers: \n \u2022 seil0@mosad.xyz \n \u2022 localhorst@mosad.xyz \n(c) 2016-2018 mosad www.mosad.xyz +infoText = \nMaintainers: \n \u2022 seil0@mosad.xyz \n \u2022 localhorst@mosad.xyz \n(c) 2016-2019 mosad www.mosad.xyz #textFlow translations title = Title From 3e80354d883a0749fc42596cbc85a68d3af6ed51 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Tue, 8 Jan 2019 19:02:00 +0100 Subject: [PATCH 59/88] don't use mains primarystage, disabled column resizeing --- .../HomeFlix/application/Main.java | 3 +- .../application/MainWindowController.java | 50 +++++++++++-------- src/main/resources/fxml/MainWindow.fxml | 2 +- 3 files changed, 32 insertions(+), 23 deletions(-) diff --git a/src/main/java/kellerkinder/HomeFlix/application/Main.java b/src/main/java/kellerkinder/HomeFlix/application/Main.java index f8a71f0..04ed160 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/Main.java +++ b/src/main/java/kellerkinder/HomeFlix/application/Main.java @@ -59,7 +59,6 @@ public class Main extends Application { LOGGER.info("User: " + XMLController.getUserName() + " " + XMLController.getUserHome()); this.primaryStage = primaryStage; - mainWindowController = new MainWindowController(this); mainWindow(); } @@ -72,7 +71,6 @@ public class Main extends Application { try { FXMLLoader loader = new FXMLLoader(); loader.setLocation(getClass().getResource("/fxml/MainWindow.fxml")); - loader.setController(mainWindowController); pane = (AnchorPane) loader.load(); primaryStage.setMinHeight(600.00); primaryStage.setMinWidth(1000.00); @@ -100,6 +98,7 @@ public class Main extends Application { XMLController.getPosterCache().mkdir(); } + mainWindowController = loader.getController(); //Link of FXMLController and controller class mainWindowController.init(); } catch (IOException e) { LOGGER.error(e); diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index 8433d61..73d6b5f 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -82,6 +82,7 @@ import javafx.scene.text.Font; import javafx.scene.text.TextFlow; import javafx.stage.DirectoryChooser; import javafx.stage.FileChooser; +import javafx.stage.Stage; import javafx.util.Duration; import kellerkinder.HomeFlix.controller.DBController; import kellerkinder.HomeFlix.controller.OMDbAPIController; @@ -155,11 +156,11 @@ public class MainWindowController { // poster-mode // @FXML private AnchorPane posterModeAnchorPane; - private Main main; private MainWindowController mainWindowController; private UpdateController updateController; private DBController dbController; private XMLController xmlController; + private Stage primaryStage; private static final Logger LOGGER = LogManager.getLogger(MainWindowController.class.getName()); private boolean menuTrue = false; @@ -187,17 +188,16 @@ public class MainWindowController { private MenuItem dislike = new MenuItem("dislike"); // TODO one option (like or dislike) private ContextMenu menu = new ContextMenu(like, dislike); - /** - * Initialize other objects: Updater, dbController and ApiQuery - * @param main the main object - */ - public MainWindowController(Main main) { - this.main = main; - mainWindowController = this; - xmlController = new XMLController(); // TODO pass it over from main? - dbController = new DBController(this); + public MainWindowController() { + // the constructor } + @FXML + public void initialize() { + mainWindowController = this; + xmlController = new XMLController(); + dbController = new DBController(this); + } public void init() { LOGGER.info("Initializing Project-HomeFlix build " + buildNumber); @@ -206,6 +206,7 @@ public class MainWindowController { checkAutoUpdate(); // initialize the GUI and the DBController + primaryStage = (Stage) mainAnchorPane.getScene().getWindow(); // set primary stage for dialogs initTabel(); initUI(); initActions(); @@ -244,10 +245,21 @@ public class MainWindowController { // film Table columnStreamUrl.setMaxWidth(0); - columnTitle.setMaxWidth(190); + columnTitle.setMaxWidth(182); columnFavorite.setMaxWidth(80); - columnSeason.setMaxWidth(73); - columnEpisode.setMaxWidth(77); + columnSeason.setMaxWidth(70); + columnEpisode.setMaxWidth(70); + + columnTitle.setMinWidth(182); + columnFavorite.setMinWidth(80); + columnSeason.setMinWidth(70); + columnEpisode.setMinWidth(70); + + columnTitle.setResizable(false); + columnFavorite.setResizable(false); + columnSeason.setResizable(false); + columnEpisode.setResizable(false); + columnFavorite.setStyle("-fx-alignment: CENTER;"); filmsTreeTable.setRoot(filmRoot); @@ -298,7 +310,6 @@ public class MainWindowController { } if (settingsTrue) { settingsScrollPane.setVisible(false); - xmlController.saveSettings(); settingsTrue = false; } }); @@ -476,7 +487,7 @@ public class MainWindowController { e1.printStackTrace(); } if (output.contains("which: no vlc") || output == "") { - JFXInfoAlert vlcInfoAlert = new JFXInfoAlert("Info", getBundle().getString("vlcNotInstalled"), btnStyle, main.getPrimaryStage()); + JFXInfoAlert vlcInfoAlert = new JFXInfoAlert("Info", getBundle().getString("vlcNotInstalled"), btnStyle, primaryStage); vlcInfoAlert.showAndWait(); } else { try { @@ -525,7 +536,7 @@ public class MainWindowController { private void aboutBtnAction() { String bodyText = "Project HomeFlix \nVersion: " + version + " (Build: " + buildNumber + ") \"" + versionName + "\" \n" + getBundle().getString("infoText"); - JFXInfoAlert infoAlert = new JFXInfoAlert("Project HomeFlix", bodyText, btnStyle, main.getPrimaryStage()); + JFXInfoAlert infoAlert = new JFXInfoAlert("Project HomeFlix", bodyText, btnStyle, primaryStage); infoAlert.showAndWait(); } @@ -533,7 +544,6 @@ public class MainWindowController { private void settingsBtnclicked() { if (settingsTrue) { settingsScrollPane.setVisible(false); - xmlController.saveSettings(); settingsTrue = false; } else { settingsScrollPane.setVisible(true); @@ -545,7 +555,7 @@ public class MainWindowController { private void addDirectoryBtnAction(){ DirectoryChooser directoryChooser = new DirectoryChooser(); directoryChooser.setTitle(bundle.getString("addDirectory")); - File selectedFolder = directoryChooser.showDialog(main.getPrimaryStage()); + File selectedFolder = directoryChooser.showDialog(primaryStage); if (selectedFolder != null && selectedFolder.exists()) { addSource(selectedFolder.getPath(), "local"); } else { @@ -556,8 +566,8 @@ public class MainWindowController { @FXML private void addStreamSourceBtnAction(){ FileChooser fileChooser = new FileChooser(); - fileChooser.setTitle("addStreamSource"); - File selectedFile = fileChooser.showOpenDialog(main.getPrimaryStage()); + fileChooser.setTitle(bundle.getString("addStreamSource")); + File selectedFile = fileChooser.showOpenDialog(primaryStage); if (selectedFile != null && selectedFile.exists()) { addSource(selectedFile.getPath(), "stream"); } else { diff --git a/src/main/resources/fxml/MainWindow.fxml b/src/main/resources/fxml/MainWindow.fxml index 7ef5c57..e0aa81f 100644 --- a/src/main/resources/fxml/MainWindow.fxml +++ b/src/main/resources/fxml/MainWindow.fxml @@ -22,7 +22,7 @@ - + From a80e077d473e9e30a230cf5c9a1aa116d0215fb0 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Wed, 9 Jan 2019 22:36:50 +0100 Subject: [PATCH 60/88] code clean up and removed some unneeded mwc uses in DBController --- .../HomeFlix/application/Main.java | 21 +++--- .../application/MainWindowController.java | 67 +++++++++---------- .../HomeFlix/controller/DBController.java | 40 +++++------ .../HomeFlix/controller/UpdateController.java | 6 +- .../HomeFlix/controller/XMLController.java | 12 ++++ .../kellerkinder/HomeFlix/player/Player.java | 5 -- 6 files changed, 75 insertions(+), 76 deletions(-) diff --git a/src/main/java/kellerkinder/HomeFlix/application/Main.java b/src/main/java/kellerkinder/HomeFlix/application/Main.java index 04ed160..6e519d8 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/Main.java +++ b/src/main/java/kellerkinder/HomeFlix/application/Main.java @@ -85,6 +85,11 @@ public class Main extends Application { primaryStage.setScene(scene); // append scene to stage primaryStage.show(); // show stage + + + mainWindowController = loader.getController(); //Link of FXMLController and controller class + mainWindowController.init(); + // startup checks if (!XMLController.getConfigFile().exists()) { XMLController.getDirHomeFlix().mkdir(); @@ -98,8 +103,7 @@ public class Main extends Application { XMLController.getPosterCache().mkdir(); } - mainWindowController = loader.getController(); //Link of FXMLController and controller class - mainWindowController.init(); + } catch (IOException e) { LOGGER.error(e); } @@ -132,7 +136,7 @@ public class Main extends Application { * we need to get the path for the first source from the user and add it to * sources.json, if the user ends the file-/directory-chooser the program will exit */ - void addFirstSource() { + private void addFirstSource() { switch (XMLController.getSysLocal()) { case "en_US": bundle = ResourceBundle.getBundle("locals.HomeFlix-Local", Locale.US); // us_english @@ -174,7 +178,7 @@ public class Main extends Application { public void handle(ActionEvent event) { FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("addStreamSource"); - File selectedFile = fileChooser.showOpenDialog(getPrimaryStage()); + File selectedFile = fileChooser.showOpenDialog(primaryStage); if (selectedFile != null && selectedFile.exists()) { mainWindowController.addSource(selectedFile.getPath(), "stream"); selectFirstSource.getAlert().close(); @@ -189,12 +193,5 @@ public class Main extends Application { selectFirstSource.setBtn2Action(btn2Action); selectFirstSource.showAndWait(); } - - public Stage getPrimaryStage() { - return primaryStage; - } - - public AnchorPane getPane() { - return pane; - } + } diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index 73d6b5f..9e291bc 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -167,7 +167,7 @@ public class MainWindowController { private boolean settingsTrue = false; private final String version = "0.7.0"; - private final String buildNumber = "167"; + private final String buildNumber = "169"; private final String versionName = "toothless dragon"; private String btnStyle; @@ -176,7 +176,6 @@ public class MainWindowController { private int indexTable; private int indexList; private int next; - private ResourceBundle bundle; private FilmTabelDataType currentTableFilm = new FilmTabelDataType("", "", "", "", false, false, null); private ObservableList languages = FXCollections.observableArrayList("English (en_US)", "Deutsch (de_DE)"); @@ -487,7 +486,9 @@ public class MainWindowController { e1.printStackTrace(); } if (output.contains("which: no vlc") || output == "") { - JFXInfoAlert vlcInfoAlert = new JFXInfoAlert("Info", getBundle().getString("vlcNotInstalled"), btnStyle, primaryStage); + JFXInfoAlert vlcInfoAlert = new JFXInfoAlert("Info", + XMLController.getLocalBundle().getString("vlcNotInstalled"), + btnStyle, primaryStage); vlcInfoAlert.showAndWait(); } else { try { @@ -535,7 +536,7 @@ public class MainWindowController { @FXML private void aboutBtnAction() { String bodyText = "Project HomeFlix \nVersion: " + version + " (Build: " + buildNumber + ") \"" - + versionName + "\" \n" + getBundle().getString("infoText"); + + versionName + "\" \n" + XMLController.getLocalBundle().getString("infoText"); JFXInfoAlert infoAlert = new JFXInfoAlert("Project HomeFlix", bodyText, btnStyle, primaryStage); infoAlert.showAndWait(); } @@ -554,7 +555,7 @@ public class MainWindowController { @FXML private void addDirectoryBtnAction(){ DirectoryChooser directoryChooser = new DirectoryChooser(); - directoryChooser.setTitle(bundle.getString("addDirectory")); + directoryChooser.setTitle(XMLController.getLocalBundle().getString("addDirectory")); File selectedFolder = directoryChooser.showDialog(primaryStage); if (selectedFolder != null && selectedFolder.exists()) { addSource(selectedFolder.getPath(), "local"); @@ -566,7 +567,7 @@ public class MainWindowController { @FXML private void addStreamSourceBtnAction(){ FileChooser fileChooser = new FileChooser(); - fileChooser.setTitle(bundle.getString("addStreamSource")); + fileChooser.setTitle(XMLController.getLocalBundle().getString("addStreamSource")); File selectedFile = fileChooser.showOpenDialog(primaryStage); if (selectedFile != null && selectedFile.exists()) { addSource(selectedFile.getPath(), "stream"); @@ -677,8 +678,10 @@ public class MainWindowController { // clear old sources list/table getSourcesList().clear(); getSourceRoot().getChildren().clear(); + // update the database and all films from the database dbController.refreshDataBase(); + checkAllPosters(); // check if there is anything to cache } /** @@ -754,37 +757,37 @@ public class MainWindowController { void setLocalUI() { switch (XMLController.getUsrLocal()) { case "en_US": - setBundle(ResourceBundle.getBundle("locals.HomeFlix-Local", Locale.US)); // us_English + XMLController.setLocalBundle(ResourceBundle.getBundle("locals.HomeFlix-Local", Locale.US)); // us_English languageChoisBox.getSelectionModel().select(0); break; case "de_DE": - setBundle(ResourceBundle.getBundle("locals.HomeFlix-Local", Locale.GERMAN)); // German + XMLController.setLocalBundle(ResourceBundle.getBundle("locals.HomeFlix-Local", Locale.GERMAN)); // German languageChoisBox.getSelectionModel().select(1); break; default: - setBundle(ResourceBundle.getBundle("locals.HomeFlix-Local", Locale.US)); // default local + XMLController.setLocalBundle(ResourceBundle.getBundle("locals.HomeFlix-Local", Locale.US)); // default local languageChoisBox.getSelectionModel().select(0); break; } - aboutBtn.setText(getBundle().getString("info")); - settingsBtn.setText(getBundle().getString("settings")); - searchTextField.setPromptText(getBundle().getString("tfSearch")); - openfolderbtn.setText(getBundle().getString("openFolder")); - updateBtn.setText(getBundle().getString("checkUpdates")); - addDirectoryBtn.setText(getBundle().getString("addDirectory")); - addStreamSourceBtn.setText(getBundle().getString("addStreamSource")); - homeflixSettingsLbl.setText(getBundle().getString("homeflixSettingsLbl")); - mainColorLbl.setText(getBundle().getString("mainColorLbl")); - fontsizeLbl.setText(getBundle().getString("fontsizeLbl")); - languageLbl.setText(getBundle().getString("languageLbl")); - autoUpdateToggleBtn.setText(getBundle().getString("autoUpdate")); - autoplayToggleBtn.setText(getBundle().getString("autoplay")); - branchLbl.setText(getBundle().getString("branchLbl")); - columnStreamUrl.setText(getBundle().getString("columnStreamUrl")); - columnTitle.setText(getBundle().getString("columnName")); - columnSeason.setText(getBundle().getString("columnSeason")); - columnEpisode.setText(getBundle().getString("columnEpisode")); - columnFavorite.setText(getBundle().getString("columnFavorite")); + aboutBtn.setText(XMLController.getLocalBundle().getString("info")); + settingsBtn.setText(XMLController.getLocalBundle().getString("settings")); + searchTextField.setPromptText(XMLController.getLocalBundle().getString("tfSearch")); + openfolderbtn.setText(XMLController.getLocalBundle().getString("openFolder")); + updateBtn.setText(XMLController.getLocalBundle().getString("checkUpdates")); + addDirectoryBtn.setText(XMLController.getLocalBundle().getString("addDirectory")); + addStreamSourceBtn.setText(XMLController.getLocalBundle().getString("addStreamSource")); + homeflixSettingsLbl.setText(XMLController.getLocalBundle().getString("homeflixSettingsLbl")); + mainColorLbl.setText(XMLController.getLocalBundle().getString("mainColorLbl")); + fontsizeLbl.setText(XMLController.getLocalBundle().getString("fontsizeLbl")); + languageLbl.setText(XMLController.getLocalBundle().getString("languageLbl")); + autoUpdateToggleBtn.setText(XMLController.getLocalBundle().getString("autoUpdate")); + autoplayToggleBtn.setText(XMLController.getLocalBundle().getString("autoplay")); + branchLbl.setText(XMLController.getLocalBundle().getString("branchLbl")); + columnStreamUrl.setText(XMLController.getLocalBundle().getString("columnStreamUrl")); + columnTitle.setText(XMLController.getLocalBundle().getString("columnName")); + columnSeason.setText(XMLController.getLocalBundle().getString("columnSeason")); + columnEpisode.setText(XMLController.getLocalBundle().getString("columnEpisode")); + columnFavorite.setText(XMLController.getLocalBundle().getString("columnFavorite")); } // if AutoUpdate, then check for updates @@ -870,14 +873,6 @@ public class MainWindowController { return sourcesList; } - public ResourceBundle getBundle() { - return bundle; - } - - public void setBundle(ResourceBundle bundle) { - this.bundle = bundle; - } - public TreeTableView getFilmsTreeTable() { return filmsTreeTable; } diff --git a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java index 8ed23b9..b07d195 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java @@ -457,26 +457,26 @@ public class DBController { ObservableList textFlow = mainWindowController.getTextFlow().getChildren(); ArrayList nameText = new ArrayList(); - nameText.add(new Text(mainWindowController.getBundle().getString("title") + ": ")); - nameText.add(new Text(mainWindowController.getBundle().getString("year") + ": ")); - nameText.add(new Text(mainWindowController.getBundle().getString("rated") + ": ")); - nameText.add(new Text(mainWindowController.getBundle().getString("released") + ": ")); - nameText.add(new Text(mainWindowController.getBundle().getString("season") + ": ")); - nameText.add(new Text(mainWindowController.getBundle().getString("episode") + ": ")); - nameText.add(new Text(mainWindowController.getBundle().getString("runtime") + ": ")); - nameText.add(new Text(mainWindowController.getBundle().getString("genre") + ": ")); - nameText.add(new Text(mainWindowController.getBundle().getString("director") + ": ")); - nameText.add(new Text(mainWindowController.getBundle().getString("writer") + ": ")); - nameText.add(new Text(mainWindowController.getBundle().getString("actors") + ": ")); - nameText.add(new Text(mainWindowController.getBundle().getString("plot") + ": ")); - nameText.add(new Text(mainWindowController.getBundle().getString("language") + ": ")); - nameText.add(new Text(mainWindowController.getBundle().getString("country") + ": ")); - nameText.add(new Text(mainWindowController.getBundle().getString("awards") + ": ")); - nameText.add(new Text(mainWindowController.getBundle().getString("metascore") + ": ")); - nameText.add(new Text(mainWindowController.getBundle().getString("imdbRating") + ": ")); - nameText.add(new Text(mainWindowController.getBundle().getString("type") + ": ")); - nameText.add(new Text(mainWindowController.getBundle().getString("boxOffice") + ": ")); - nameText.add(new Text(mainWindowController.getBundle().getString("website") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("title") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("year") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("rated") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("released") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("season") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("episode") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("runtime") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("genre") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("director") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("writer") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("actors") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("plot") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("language") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("country") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("awards") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("metascore") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("imdbRating") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("type") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("boxOffice") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("website") + ": ")); // set the correct font for the nameText for (Text text : nameText) { diff --git a/src/main/java/kellerkinder/HomeFlix/controller/UpdateController.java b/src/main/java/kellerkinder/HomeFlix/controller/UpdateController.java index 9b71c0a..3c7caf1 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/UpdateController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/UpdateController.java @@ -76,7 +76,7 @@ public class UpdateController implements Runnable { public void run() { LOGGER.info("beta:" + useBeta + "; checking for updates ..."); Platform.runLater(() -> { - mainWindowController.getUpdateBtn().setText(mainWindowController.getBundle().getString("updateBtnChecking")); + mainWindowController.getUpdateBtn().setText(XMLController.getLocalBundle().getString("updateBtnChecking")); }); try { @@ -116,12 +116,12 @@ public class UpdateController implements Runnable { */ if (buildNumber >= updateBuildNumber) { Platform.runLater(() -> { - mainWindowController.getUpdateBtn().setText(mainWindowController.getBundle().getString("updateBtnNoUpdateAvailable")); + mainWindowController.getUpdateBtn().setText(XMLController.getLocalBundle().getString("updateBtnNoUpdateAvailable")); }); LOGGER.info("no update available"); } else { Platform.runLater(() -> { - mainWindowController.getUpdateBtn().setText(mainWindowController.getBundle().getString("updateBtnUpdateAvailable")); + mainWindowController.getUpdateBtn().setText(XMLController.getLocalBundle().getString("updateBtnUpdateAvailable")); }); LOGGER.info("update available"); browserDownloadUrl = objectAsset.getString("browser_download_url", ""); diff --git a/src/main/java/kellerkinder/HomeFlix/controller/XMLController.java b/src/main/java/kellerkinder/HomeFlix/controller/XMLController.java index 2fb9a45..8d36204 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/XMLController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/XMLController.java @@ -30,7 +30,9 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; +import java.util.Locale; import java.util.Properties; +import java.util.ResourceBundle; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -60,6 +62,7 @@ public class XMLController { private static boolean useBeta = false; private static boolean autoplay = false; private static double fontSize = 17; + private static ResourceBundle localBundle = ResourceBundle.getBundle("locals.HomeFlix-Local", Locale.US); // user settings private static String omdbAPIKey; @@ -281,6 +284,15 @@ public class XMLController { public static void setFontSize(double fontSize) { XMLController.fontSize = fontSize; } + + public static ResourceBundle getLocalBundle() { + return localBundle; + } + + public static void setLocalBundle(ResourceBundle localBundle) { + XMLController.localBundle = localBundle; + } + // getters for APIs diff --git a/src/main/java/kellerkinder/HomeFlix/player/Player.java b/src/main/java/kellerkinder/HomeFlix/player/Player.java index 3481813..f6d5cc8 100644 --- a/src/main/java/kellerkinder/HomeFlix/player/Player.java +++ b/src/main/java/kellerkinder/HomeFlix/player/Player.java @@ -24,7 +24,6 @@ package kellerkinder.HomeFlix.player; import javafx.event.EventHandler; import javafx.fxml.FXMLLoader; -import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.image.Image; import javafx.scene.layout.AnchorPane; @@ -78,10 +77,6 @@ public class Player { public Stage getStage() { return stage; } - - public Parent getPane() { - return pane; - } public Scene getScene() { return scene; From 291f183f5ea24321a5ecdc52b19351f89a1a03ce Mon Sep 17 00:00:00 2001 From: Seil0 Date: Sat, 12 Jan 2019 23:07:25 +0100 Subject: [PATCH 61/88] removed more mwc dependencies & code clean up --- .../application/MainWindowController.java | 64 ++++++------------- .../HomeFlix/controller/DBController.java | 2 +- .../controller/SourcesController.java | 8 +-- .../HomeFlix/controller/XMLController.java | 16 ++--- .../kellerkinder/HomeFlix/player/Player.java | 9 +-- .../HomeFlix/player/PlayerController.java | 14 ++-- src/main/resources/fxml/MainWindow.fxml | 10 ++- 7 files changed, 54 insertions(+), 69 deletions(-) diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index 9e291bc..4965855 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -132,17 +132,19 @@ public class MainWindowController { @FXML private TableColumn sourceColumn; @FXML private TableColumn modeColumn; + @FXML private TreeTableColumn columnStreamUrl; + @FXML private TreeTableColumn columnTitle; + @FXML private TreeTableColumn columnFavorite; + @FXML private TreeTableColumn columnSeason; + @FXML private TreeTableColumn columnEpisode; + // table-mode @FXML private AnchorPane tableModeAnchorPane; @FXML private JFXTextField searchTextField; @FXML private TreeTableView filmsTreeTable; @FXML private TreeItem filmRoot = new TreeItem<>(new FilmTabelDataType("", "", "", "", false, false, null)); - @FXML private TreeTableColumn columnStreamUrl = new TreeTableColumn<>("File Name"); - @FXML private TreeTableColumn columnTitle = new TreeTableColumn<>("Title"); - @FXML private TreeTableColumn columnSeason = new TreeTableColumn<>("Season"); - @FXML private TreeTableColumn columnEpisode = new TreeTableColumn<>("Episode"); - @FXML private TreeTableColumn columnFavorite = new TreeTableColumn<>("Favorite"); + @FXML private ScrollPane textScrollPane; @FXML private TextFlow textFlow; @@ -156,9 +158,8 @@ public class MainWindowController { // poster-mode // @FXML private AnchorPane posterModeAnchorPane; - private MainWindowController mainWindowController; + private static DBController dbController; // the player needs the initialized dbController private UpdateController updateController; - private DBController dbController; private XMLController xmlController; private Stage primaryStage; private static final Logger LOGGER = LogManager.getLogger(MainWindowController.class.getName()); @@ -182,7 +183,7 @@ public class MainWindowController { private ObservableList branches = FXCollections.observableArrayList("stable", "beta"); private ObservableList filterData = FXCollections.observableArrayList(); private ObservableList filmsList = FXCollections.observableArrayList(); - private ObservableList sourcesList = FXCollections.observableArrayList(); + private static ObservableList sourcesList = FXCollections.observableArrayList(); private MenuItem like = new MenuItem("like"); private MenuItem dislike = new MenuItem("dislike"); // TODO one option (like or dislike) private ContextMenu menu = new ContextMenu(like, dislike); @@ -193,7 +194,6 @@ public class MainWindowController { @FXML public void initialize() { - mainWindowController = this; xmlController = new XMLController(); dbController = new DBController(this); } @@ -211,6 +211,9 @@ public class MainWindowController { initActions(); dbController.init(); + // load sources list in gui + addSourceToTable(); + // posterModeStartup(); // TODO testing DO NOT USE THIS!! } @@ -243,24 +246,6 @@ public class MainWindowController { private void initTabel() { // film Table - columnStreamUrl.setMaxWidth(0); - columnTitle.setMaxWidth(182); - columnFavorite.setMaxWidth(80); - columnSeason.setMaxWidth(70); - columnEpisode.setMaxWidth(70); - - columnTitle.setMinWidth(182); - columnFavorite.setMinWidth(80); - columnSeason.setMinWidth(70); - columnEpisode.setMinWidth(70); - - columnTitle.setResizable(false); - columnFavorite.setResizable(false); - columnSeason.setResizable(false); - columnEpisode.setResizable(false); - - columnFavorite.setStyle("-fx-alignment: CENTER;"); - filmsTreeTable.setRoot(filmRoot); filmsTreeTable.setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY); filmsTreeTable.setShowRoot(false); @@ -272,14 +257,6 @@ public class MainWindowController { columnEpisode.setCellValueFactory(cellData -> cellData.getValue().getValue().episodeProperty()); columnFavorite.setCellValueFactory(cellData -> cellData.getValue().getValue().imageProperty()); - // add columns to treeTableViewfilm - filmsTreeTable.getColumns().add(columnStreamUrl); - filmsTreeTable.getColumns().add(columnTitle); - filmsTreeTable.getColumns().add(columnFavorite); - filmsTreeTable.getColumns().add(columnSeason); - filmsTreeTable.getColumns().add(columnEpisode); - filmsTreeTable.getColumns().get(0).setVisible(false); // hide columnStreamUrl (important) - // context menu for treeTableViewfilm filmsTreeTable.setContextMenu(menu); @@ -467,7 +444,7 @@ public class MainWindowController { } if (isSupportedFormat(currentTableFilm)) { - new Player(mainWindowController); + new Player(getCurrentTableFilm()); } else { LOGGER.error("using fallback player!"); if (System.getProperty("os.name").contains("Linux")) { @@ -641,10 +618,11 @@ public class MainWindowController { } } - // add a source to the sources table on the settings pane - public void addSourceToTable(String path, String mode) { - sourcesList.add(new SourceDataType(path, mode)); - sourceRoot.getChildren().add(new TreeItem(sourcesList.get(sourcesList.size() - 1))); // adds data to root-node + // add a all elements of sourcesList to the sources table on the settings pane + public void addSourceToTable() { + for (SourceDataType source: sourcesList) { + sourceRoot.getChildren().add(new TreeItem(source)); // add data to root-node + } } /** @@ -785,9 +763,9 @@ public class MainWindowController { branchLbl.setText(XMLController.getLocalBundle().getString("branchLbl")); columnStreamUrl.setText(XMLController.getLocalBundle().getString("columnStreamUrl")); columnTitle.setText(XMLController.getLocalBundle().getString("columnName")); + columnFavorite.setText(XMLController.getLocalBundle().getString("columnFavorite")); columnSeason.setText(XMLController.getLocalBundle().getString("columnSeason")); columnEpisode.setText(XMLController.getLocalBundle().getString("columnEpisode")); - columnFavorite.setText(XMLController.getLocalBundle().getString("columnFavorite")); } // if AutoUpdate, then check for updates @@ -841,7 +819,7 @@ public class MainWindowController { } // getter and setter - public DBController getDbController() { + public static DBController getDbController() { return dbController; } @@ -869,7 +847,7 @@ public class MainWindowController { return filmsList; } - public ObservableList getSourcesList() { + public static ObservableList getSourcesList() { return sourcesList; } diff --git a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java index b07d195..78a347c 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java @@ -143,7 +143,7 @@ public class DBController { * load all sources */ private void loadSources() { - SourcesController sourcesController = new SourcesController(mainWindowController); + SourcesController sourcesController = new SourcesController(); sourceStreams = sourcesController.loadSources(); } diff --git a/src/main/java/kellerkinder/HomeFlix/controller/SourcesController.java b/src/main/java/kellerkinder/HomeFlix/controller/SourcesController.java index 942f171..a55d6f8 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/SourcesController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/SourcesController.java @@ -39,15 +39,15 @@ import com.eclipsesource.json.JsonValue; import kellerkinder.HomeFlix.application.MainWindowController; import kellerkinder.HomeFlix.datatypes.DatabaseDataType; +import kellerkinder.HomeFlix.datatypes.SourceDataType; public class SourcesController { - private MainWindowController mainWindowController; private List sourceStreams = new ArrayList(); private static final Logger LOGGER = LogManager.getLogger(SourcesController.class.getName()); - public SourcesController(MainWindowController mainWindowController) { - this.mainWindowController = mainWindowController; + public SourcesController() { + // Auto-generated constructor stub } /** @@ -61,7 +61,7 @@ public class SourcesController { for (JsonValue source : sources) { String path = source.asObject().getString("path", ""); String mode = source.asObject().getString("mode", ""); - mainWindowController.addSourceToTable(path, mode); // add loaded source to source-table TODO this should be done in mwc + MainWindowController.getSourcesList().add(new SourceDataType(path, mode)); if (mode.equals("local")) addLocalSource(path); diff --git a/src/main/java/kellerkinder/HomeFlix/controller/XMLController.java b/src/main/java/kellerkinder/HomeFlix/controller/XMLController.java index 8d36204..02670e6 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/XMLController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/XMLController.java @@ -42,14 +42,14 @@ import com.eclipsesource.json.JsonObject; public class XMLController { - private static String userHome = System.getProperty("user.home"); - private static String userName = System.getProperty("user.name"); - private static String osName = System.getProperty("os.name"); - private static String osArch = System.getProperty("os.arch"); - private static String osVers = System.getProperty("os.version"); - private static String javaVers = System.getProperty("java.version"); - private static String javaVend = System.getProperty("java.vendor"); - private static String sysLocal = System.getProperty("user.language") + "_" + System.getProperty("user.country"); + private static final String userHome = System.getProperty("user.home"); + private static final String userName = System.getProperty("user.name"); + private static final String osName = System.getProperty("os.name"); + private static final String osArch = System.getProperty("os.arch"); + private static final String osVers = System.getProperty("os.version"); + private static final String javaVers = System.getProperty("java.version"); + private static final String javaVend = System.getProperty("java.vendor"); + private static final String sysLocal = System.getProperty("user.language") + "_" + System.getProperty("user.country"); private static String dirHomeFlixPath; private static File dirHomeFlix; private static File configFile; diff --git a/src/main/java/kellerkinder/HomeFlix/player/Player.java b/src/main/java/kellerkinder/HomeFlix/player/Player.java index f6d5cc8..209185b 100644 --- a/src/main/java/kellerkinder/HomeFlix/player/Player.java +++ b/src/main/java/kellerkinder/HomeFlix/player/Player.java @@ -31,6 +31,7 @@ import javafx.stage.Stage; import javafx.stage.WindowEvent; import kellerkinder.HomeFlix.application.Main; import kellerkinder.HomeFlix.application.MainWindowController; +import kellerkinder.HomeFlix.datatypes.FilmTabelDataType; public class Player { @@ -41,10 +42,10 @@ public class Player { /** * generate a new PlayerWindow - * @param mainWindowController the MainWindowController + * @param currentTableFilm the currently selected film */ - public Player(MainWindowController mainWindowController) { - playerController = new PlayerController(mainWindowController, this, mainWindowController.getCurrentTableFilm()); + public Player(FilmTabelDataType currentTableFilm) { + playerController = new PlayerController(this, currentTableFilm); try { FXMLLoader fxmlLoader = new FXMLLoader(); @@ -58,7 +59,7 @@ public class Player { stage.getIcons().add(new Image(Main.class.getResourceAsStream("/icons/Homeflix_Icon_64x64.png"))); stage.setOnCloseRequest(new EventHandler() { public void handle(WindowEvent we) { - mainWindowController.getDbController().setCurrentTime(mainWindowController.getCurrentStreamUrl(), + MainWindowController.getDbController().setCurrentTime(currentTableFilm.getStreamUrl(), playerController.getCurrentTime()); playerController.getMediaPlayer().stop(); stage.close(); diff --git a/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java b/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java index f94e7e9..9750932 100644 --- a/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java +++ b/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java @@ -67,7 +67,6 @@ public class PlayerController { @FXML private JFXButton nextEpBtn; private Player player; - private MainWindowController mainWCon; private Media media; private MediaPlayer mediaPlayer; @@ -95,8 +94,7 @@ public class PlayerController { * @param player the player object (needed for closing action) * @param film the film object */ - public PlayerController(MainWindowController mainWCon, Player player, FilmTabelDataType film) { - this.mainWCon = mainWCon; + public PlayerController(Player player, FilmTabelDataType film) { this.player = player; this.film = film; } @@ -124,7 +122,7 @@ public class PlayerController { width.bind(Bindings.selectDouble(mediaView.sceneProperty(), "width")); height.bind(Bindings.selectDouble(mediaView.sceneProperty(), "height")); - startTime = mainWCon.getDbController().getCurrentTime(film.getStreamUrl()); + startTime = MainWindowController.getDbController().getCurrentTime(film.getStreamUrl()); autoplay = XMLController.isAutoplay(); season = !film.getSeason().isEmpty() ? Integer.parseInt(film.getSeason()) : 0; episode = !film.getEpisode().isEmpty() ? Integer.parseInt(film.getEpisode()) : 0; @@ -177,7 +175,7 @@ public class PlayerController { } else if (timeToEnd < 120) { // if we are 120ms to the end stop the media mediaPlayer.stop(); - mainWCon.getDbController().setCurrentTime(film.getStreamUrl(), 0); // reset old video start time + MainWindowController.getDbController().setCurrentTime(film.getStreamUrl(), 0); // reset old video start time playBtn.setGraphic(play_arrow_black); } else { if (nextEpBtn.isVisible()) @@ -255,7 +253,7 @@ public class PlayerController { @FXML void stopBtnAction(ActionEvent event) { - mainWCon.getDbController().setCurrentTime(film.getStreamUrl(), currentTime); + MainWindowController.getDbController().setCurrentTime(film.getStreamUrl(), currentTime); mediaPlayer.stop(); player.getStage().close(); } @@ -289,8 +287,8 @@ public class PlayerController { private void autoPlayNewFilm() { autoplay = false; - mainWCon.getDbController().setCurrentTime(film.getStreamUrl(), 0); // reset old video start time - FilmTabelDataType nextFilm = mainWCon.getDbController().getNextEpisode(film.getTitle(), episode, season); + MainWindowController.getDbController().setCurrentTime(film.getStreamUrl(), 0); // reset old video start time + FilmTabelDataType nextFilm = MainWindowController.getDbController().getNextEpisode(film.getTitle(), episode, season); if (nextFilm != null) { mediaPlayer.stop(); film = nextFilm; diff --git a/src/main/resources/fxml/MainWindow.fxml b/src/main/resources/fxml/MainWindow.fxml index e0aa81f..b850b67 100644 --- a/src/main/resources/fxml/MainWindow.fxml +++ b/src/main/resources/fxml/MainWindow.fxml @@ -13,6 +13,7 @@ + @@ -31,7 +32,14 @@ - + + + + + + + + From cc05600e0a554fc2825e5bd79ae8ddadf554178e Mon Sep 17 00:00:00 2001 From: Seil0 Date: Mon, 14 Jan 2019 18:44:36 +0100 Subject: [PATCH 62/88] removed all mwc usage in DBController --- .../HomeFlix/application/Main.java | 2 - .../application/MainWindowController.java | 119 ++++++++++++-- .../HomeFlix/controller/DBController.java | 155 +++++++----------- .../controller/OMDbAPIController.java | 15 +- 4 files changed, 167 insertions(+), 124 deletions(-) diff --git a/src/main/java/kellerkinder/HomeFlix/application/Main.java b/src/main/java/kellerkinder/HomeFlix/application/Main.java index 6e519d8..33b7938 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/Main.java +++ b/src/main/java/kellerkinder/HomeFlix/application/Main.java @@ -85,8 +85,6 @@ public class Main extends Application { primaryStage.setScene(scene); // append scene to stage primaryStage.show(); // show stage - - mainWindowController = loader.getController(); //Link of FXMLController and controller class mainWindowController.init(); diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index 4965855..57bfac4 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -32,6 +32,7 @@ import java.io.InputStreamReader; import java.io.Writer; import java.math.BigInteger; import java.net.URLConnection; +import java.util.ArrayList; import java.util.Locale; import java.util.ResourceBundle; import java.util.concurrent.ExecutorService; @@ -53,6 +54,7 @@ import com.jfoenix.controls.JFXToggleButton; import com.jfoenix.transitions.hamburger.HamburgerBackArrowBasicTransition; import javafx.animation.TranslateTransition; +import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; @@ -60,6 +62,7 @@ import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.fxml.FXML; +import javafx.scene.Node; import javafx.scene.control.ChoiceBox; import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; @@ -79,6 +82,8 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; +import javafx.scene.text.Text; import javafx.scene.text.TextFlow; import javafx.stage.DirectoryChooser; import javafx.stage.FileChooser; @@ -195,7 +200,7 @@ public class MainWindowController { @FXML public void initialize() { xmlController = new XMLController(); - dbController = new DBController(this); + dbController = DBController.getInstance(); } public void init() { @@ -210,6 +215,7 @@ public class MainWindowController { initUI(); initActions(); dbController.init(); + refreshAllFilms(); // load sources list in gui addSourceToTable(); @@ -318,7 +324,7 @@ public class MainWindowController { public void changed(ObservableValue ov, Number old_val, Number new_val) { XMLController.setFontSize(fontsizeSlider.getValue()); if (!getCurrentTitle().isEmpty()) { - dbController.readCache(getCurrentStreamUrl()); + setSelectedFilmInfo(dbController.readCache(getCurrentStreamUrl())); } xmlController.saveSettings(); } @@ -355,7 +361,7 @@ public class MainWindowController { @Override public void handle(ActionEvent event) { dbController.like(getCurrentStreamUrl()); - dbController.refresh(getCurrentStreamUrl(), indexList); + filmsList.set(indexList, dbController.getDatabaseFilm(getCurrentStreamUrl())); refreshTableElement(); } }); @@ -364,7 +370,7 @@ public class MainWindowController { @Override public void handle(ActionEvent event) { dbController.dislike(getCurrentStreamUrl()); - dbController.refresh(getCurrentStreamUrl(), indexList); + filmsList.set(indexList, dbController.getDatabaseFilm(getCurrentStreamUrl())); refreshTableElement(); } }); @@ -423,11 +429,28 @@ public class MainWindowController { if (currentTableFilm.getCached() || dbController.searchCacheByURL(getCurrentStreamUrl())) { LOGGER.info("loading from cache: " + getCurrentTitle()); - dbController.readCache(getCurrentStreamUrl()); + setSelectedFilmInfo(dbController.readCache(getCurrentStreamUrl())); } else { - Thread omdbAPIThread = new Thread(new OMDbAPIController(dbController, currentTableFilm, XMLController.getOmdbAPIKey(), true)); - omdbAPIThread.setName("OMDbAPI"); - omdbAPIThread.start(); + // this is not perfect! + new Thread(new Runnable() { + public void run() { + Thread omdbAPIThread = new Thread(new OMDbAPIController(dbController, currentTableFilm, XMLController.getOmdbAPIKey())); + omdbAPIThread.setName("OMDbAPI"); + omdbAPIThread.start(); + + synchronized (omdbAPIThread) { + try { + omdbAPIThread.wait(); + } catch (InterruptedException e) { + LOGGER.error(e); + } + // update the GUI for the selected film + Platform.runLater(() -> { + setSelectedFilmInfo(dbController.readCache(getCurrentStreamUrl())); + }); + } + } + }).start(); } } }); @@ -585,6 +608,18 @@ public class MainWindowController { filmRoot.getChildren().get(indexTable).setValue(filmsList.get(indexList)); } + /** + * refresh all films in filmsList and in filmsTable + * clear the FilmsList and FilmRoot children, then update the database + */ + private void refreshAllFilms() { + getFilmsList().clear(); + getFilmRoot().getChildren().clear(); + dbController.refreshDataBase(); // refreshes the database after a source path was added + filmsList = dbController.getDatabaseFilmsList(); // returns a list of all films stored in the database + addFilmsToTable(filmsList); + } + /** * add data from films-list to films-table */ @@ -657,8 +692,9 @@ public class MainWindowController { getSourcesList().clear(); getSourceRoot().getChildren().clear(); - // update the database and all films from the database - dbController.refreshDataBase(); + // clear the FilmsList and FilmRoot children, then update the database + refreshAllFilms(); + checkAllPosters(); // check if there is anything to cache } @@ -732,7 +768,7 @@ public class MainWindowController { /** * set the local based on the languageChoisBox selection */ - void setLocalUI() { + private void setLocalUI() { switch (XMLController.getUsrLocal()) { case "en_US": XMLController.setLocalBundle(ResourceBundle.getBundle("locals.HomeFlix-Local", Locale.US)); // us_English @@ -768,6 +804,62 @@ public class MainWindowController { columnEpisode.setText(XMLController.getLocalBundle().getString("columnEpisode")); } + private void setSelectedFilmInfo(String[] cacheData) { + Font font = Font.font("System", FontWeight.BOLD, (int) Math.round(XMLController.getFontSize())); + ObservableList textFlow = getTextFlow().getChildren(); + + // TODO this should move! *** + ArrayList nameText = new ArrayList(); + nameText.add(new Text(XMLController.getLocalBundle().getString("title") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("year") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("rated") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("released") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("season") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("episode") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("runtime") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("genre") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("director") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("writer") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("actors") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("plot") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("language") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("country") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("awards") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("metascore") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("imdbRating") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("type") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("boxOffice") + ": ")); + nameText.add(new Text(XMLController.getLocalBundle().getString("website") + ": ")); + // *** + + // set the correct font for the nameText + for (Text text : nameText) { + text.setFont(font); + } + + // clear the textFlow and add the new text + textFlow.clear(); + + // TODO rework + for(int i = 0; i < 20; i++) { + if (cacheData[i] != null && (i == 5 || i == 6) && cacheData[5].length() == 0) { + // do nothing + } else if(cacheData[i] != null){ + textFlow.addAll(nameText.get(i), new Text(cacheData[i] + "\n")); + } + } + + getTextFlow().setStyle("-fx-font-size : " + ((int) Math.round(XMLController.getFontSize()) + 1) + "px;"); + + // add the image + try { + getPosterImageView().setImage(new Image(new File(cacheData[20]).toURI().toString())); + } catch (Exception e) { + getPosterImageView().setImage(new Image("icons/close_black_2048x2048.png")); + LOGGER.error("No Poster found, useing default."); + } + } + // if AutoUpdate, then check for updates private void checkAutoUpdate() { @@ -810,12 +902,15 @@ public class MainWindowController { for (FilmTabelDataType entry : dbController.getAllNotCachedEntries()) { System.out.println(entry.getStreamUrl() + " is NOT cached!"); - Runnable OMDbAPIWorker = new OMDbAPIController(dbController, entry, XMLController.getOmdbAPIKey(), false); + Runnable OMDbAPIWorker = new OMDbAPIController(dbController, entry, XMLController.getOmdbAPIKey()); executor.execute(OMDbAPIWorker); } executor.shutdown(); // TODO show loading screen + + // update all elements from the database + refreshAllFilms(); } // getter and setter diff --git a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java index 78a347c..6e87e16 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java @@ -21,7 +21,6 @@ package kellerkinder.HomeFlix.controller; -import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.net.URLConnection; @@ -37,21 +36,17 @@ import java.util.List; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import javafx.collections.FXCollections; import javafx.collections.ObservableList; -import javafx.scene.Node; import javafx.scene.image.Image; import javafx.scene.image.ImageView; -import javafx.scene.text.Font; -import javafx.scene.text.FontWeight; -import javafx.scene.text.Text; -import kellerkinder.HomeFlix.application.MainWindowController; import kellerkinder.HomeFlix.datatypes.DatabaseDataType; import kellerkinder.HomeFlix.datatypes.FilmTabelDataType; import kellerkinder.HomeFlix.datatypes.OMDbAPIResponseDataType; public class DBController { - private MainWindowController mainWindowController; + private static DBController instance = null; private String DB_PATH; private Image favorite_black = new Image("icons/ic_favorite_black_18dp_1x.png"); private Image favorite_border_black = new Image("icons/ic_favorite_border_black_18dp_1x.png"); @@ -65,8 +60,16 @@ public class DBController { * @param main the Main object * @param mainWindowController the MainWindowController object */ - public DBController(MainWindowController mainWindowController) { - this.mainWindowController = mainWindowController; + public DBController() { + // Auto-generated constructor stub + } + + public static DBController getInstance() { + if (instance == null) { + instance = new DBController(); + } + + return instance; } /** @@ -148,11 +151,12 @@ public class DBController { } /** - * load the data to the mainWindowController + * load the data from the database to a ObservableList * order entries by title + * @return a ObservableList that contains all films from the database */ - private void loadDataToFilmsList() { - ImageView imageView; + public ObservableList getDatabaseFilmsList() { + ObservableList filmsList = FXCollections.observableArrayList(); LOGGER.info("loading data to mwc ..."); try { //load local Data @@ -160,8 +164,8 @@ public class DBController { ResultSet rs = stmt.executeQuery("SELECT * FROM films ORDER BY title"); while (rs.next()) { - imageView = rs.getBoolean("favorite") ? new ImageView(favorite_black) : new ImageView(favorite_border_black); - mainWindowController.getFilmsList().add(new FilmTabelDataType(rs.getString("streamUrl"), + ImageView imageView = rs.getBoolean("favorite") ? new ImageView(favorite_black) : new ImageView(favorite_border_black); + filmsList.add(new FilmTabelDataType(rs.getString("streamUrl"), rs.getString("title"), rs.getString("season"), rs.getString("episode") ,rs.getBoolean("favorite"), rs.getBoolean("cached"), imageView)); } @@ -172,15 +176,17 @@ public class DBController { } LOGGER.info("loading data to the GUI ..."); - mainWindowController.addFilmsToTable(mainWindowController.getFilmsList()); + + return filmsList; } /** - * refresh data in mainWindowController for one element + * get one film from the database with streamUrl = ? * @param streamUrl of the film - * @param index of the film in LocalFilms list + * @return a FilmTabelDataType Object of the film with the given streamUrl */ - public void refresh(String streamUrl, int indexList) { + public FilmTabelDataType getDatabaseFilm(String streamUrl) { + FilmTabelDataType film = null; LOGGER.info("refresh data for " + streamUrl); try { PreparedStatement ps = connection.prepareStatement("SELECT * FROM films WHERE streamUrl = ?"); @@ -189,21 +195,21 @@ public class DBController { while (rs.next()) { ImageView imageView = rs.getBoolean("favorite") ? new ImageView(favorite_black) : new ImageView(favorite_border_black); - mainWindowController.getFilmsList().set(indexList, new FilmTabelDataType(rs.getString("streamUrl"), + film = new FilmTabelDataType(rs.getString("streamUrl"), rs.getString("title"), rs.getString("season"), rs.getString("episode"), rs.getBoolean("favorite"), - rs.getBoolean("cached"), imageView)); + rs.getBoolean("cached"), imageView); } rs.close(); ps.close(); } catch (Exception e) { LOGGER.error("Ups! error while refreshing mwc!", e); - } + } + + return film; } /** * refresh database to contain all (new added) entries - * refresh the MainWindowController content, - * to contain all (new added) entries from the database */ public void refreshDataBase() { LOGGER.info("refreshing the Database ..."); @@ -223,12 +229,6 @@ public class DBController { } catch (Exception e) { LOGGER.error("Error while refreshing the database", e); } - - // clear the FilmsList and FilmRoot children - mainWindowController.getFilmsList().clear(); - mainWindowController.getFilmRoot().getChildren().clear(); - - loadDataToFilmsList(); // load the new data to the FilmsList } /** @@ -373,8 +373,6 @@ public class DBController { } catch (SQLException e) { LOGGER.error("Ups! an error occured!", e); } - - refresh(streamUrl, mainWindowController.getIndexList()); } /** @@ -445,79 +443,40 @@ public class DBController { } /** - * sets the cached data to mwc's TextFlow + * read the cached data from the Database * @param streamUrl URL of the film + * @return a String array (length = 21) with all cached data */ - public void readCache(String streamUrl) { + public String[] readCache(String streamUrl) { + String[] cacheData = new String[21]; + try { PreparedStatement ps = connection.prepareStatement("SELECT * FROM cache WHERE streamUrl = ?"); ps.setString(1, streamUrl); ResultSet rs = ps.executeQuery(); - Font font = Font.font("System", FontWeight.BOLD, (int) Math.round(XMLController.getFontSize())); - ObservableList textFlow = mainWindowController.getTextFlow().getChildren(); - ArrayList nameText = new ArrayList(); - nameText.add(new Text(XMLController.getLocalBundle().getString("title") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("year") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("rated") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("released") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("season") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("episode") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("runtime") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("genre") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("director") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("writer") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("actors") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("plot") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("language") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("country") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("awards") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("metascore") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("imdbRating") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("type") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("boxOffice") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("website") + ": ")); - - // set the correct font for the nameText - for (Text text : nameText) { - text.setFont(font); - } - - // clear the textFlow and all the new text - textFlow.clear(); - textFlow.addAll(nameText.get(0), new Text(rs.getString("Title") + "\n")); - textFlow.addAll(nameText.get(1), new Text(rs.getString("Year") + "\n")); - textFlow.addAll(nameText.get(2), new Text(rs.getString("Rated") + "\n")); - textFlow.addAll(nameText.get(3), new Text(rs.getString("Released") + "\n")); - - if (rs.getString("Episode").length() > 0) { - textFlow.addAll(nameText.get(4), new Text(rs.getString("Season") + "\n")); - textFlow.addAll(nameText.get(5), new Text(rs.getString("Episode") + "\n")); - } - - textFlow.addAll(nameText.get(6), new Text(rs.getString("Runtime") + "\n")); - textFlow.addAll(nameText.get(7), new Text(rs.getString("Genre") + "\n")); - textFlow.addAll(nameText.get(8), new Text(rs.getString("Director") + "\n")); - textFlow.addAll(nameText.get(9), new Text(rs.getString("Writer") + "\n")); - textFlow.addAll(nameText.get(10), new Text(rs.getString("Actors") + "\n")); - textFlow.addAll(nameText.get(11), new Text(rs.getString("Plot") + "\n")); - textFlow.addAll(nameText.get(12), new Text(rs.getString("Language") + "\n")); - textFlow.addAll(nameText.get(13), new Text(rs.getString("Country") + "\n")); - textFlow.addAll(nameText.get(14), new Text(rs.getString("Awards") + "\n")); - textFlow.addAll(nameText.get(15), new Text(rs.getString("metascore") + "\n")); - textFlow.addAll(nameText.get(16), new Text(rs.getString("imdbRating") + "\n")); - textFlow.addAll(nameText.get(17), new Text(rs.getString("Type") + "\n")); - textFlow.addAll(nameText.get(18), new Text(rs.getString("BoxOffice") + "\n")); - textFlow.addAll(nameText.get(19), new Text(rs.getString("Website") + "\n")); - - mainWindowController.getTextFlow().setStyle("-fx-font-size : " + ((int) Math.round(XMLController.getFontSize()) + 1) + "px;"); - - // add the image - try { - mainWindowController.getPosterImageView().setImage(new Image(new File(rs.getString("Poster")).toURI().toString())); - } catch (Exception e) { - mainWindowController.getPosterImageView().setImage(new Image("icons/close_black_2048x2048.png")); - LOGGER.error("No Poster found, useing default."); + while (rs.next()) { + cacheData[0] = rs.getString("Title"); + cacheData[1] = rs.getString("Year"); + cacheData[2] = rs.getString("Rated"); + cacheData[3] = rs.getString("Released"); + cacheData[4] = rs.getString("Season"); + cacheData[5] = rs.getString("Episode"); + cacheData[6] = rs.getString("Runtime"); + cacheData[7] = rs.getString("Genre"); + cacheData[8] = rs.getString("Director"); + cacheData[9] = rs.getString("Writer"); + cacheData[10] = rs.getString("Actors"); + cacheData[11] = rs.getString("Plot"); + cacheData[12] = rs.getString("Language"); + cacheData[13] = rs.getString("Country"); + cacheData[14] = rs.getString("Awards"); + cacheData[15] = rs.getString("metascore"); + cacheData[16] = rs.getString("imdbRating"); + cacheData[17] = rs.getString("Type"); + cacheData[18] = rs.getString("BoxOffice"); + cacheData[19] = rs.getString("Website"); + cacheData[20] = rs.getString("Poster"); } rs.close(); @@ -525,6 +484,8 @@ public class DBController { } catch (SQLException e) { LOGGER.error("Ups! an error occured!", e); } + + return cacheData; } /** diff --git a/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java b/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java index 008d949..3ab3219 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java @@ -37,8 +37,6 @@ import com.eclipsesource.json.Json; import com.eclipsesource.json.JsonObject; import com.eclipsesource.json.JsonValue; -import javafx.application.Platform; -import kellerkinder.HomeFlix.application.MainWindowController; import kellerkinder.HomeFlix.datatypes.FilmTabelDataType; import kellerkinder.HomeFlix.datatypes.OMDbAPIResponseDataType; @@ -49,8 +47,7 @@ public class OMDbAPIController implements Runnable { private String omdbAPIKey; private String URL = "https://www.omdbapi.com/?apikey="; private boolean useEpisode = true; - private boolean refresh; - private static final Logger LOGGER = LogManager.getLogger(MainWindowController.class.getName()); + private static final Logger LOGGER = LogManager.getLogger(OMDbAPIController.class.getName()); /** * constructor for the OMDbAPIController @@ -59,11 +56,10 @@ public class OMDbAPIController implements Runnable { * @param currentTableFilm the current film object * @param omdbAPIKey the omdbAPI key */ - public OMDbAPIController(DBController dbController, FilmTabelDataType currentTableFilm, String omdbAPIKey, boolean refresh) { + public OMDbAPIController(DBController dbController, FilmTabelDataType currentTableFilm, String omdbAPIKey) { this.dbController = dbController; this.currentTableFilm = currentTableFilm; this.omdbAPIKey = omdbAPIKey; - this.refresh = refresh; } @Override @@ -136,13 +132,6 @@ public class OMDbAPIController implements Runnable { // adding to cache dbController.addCache(currentTableFilm.getStreamUrl(), omdbResponse); dbController.setCached(currentTableFilm.getStreamUrl()); - - // load data to the MainWindowController - if (refresh) { - Platform.runLater(() -> { - dbController.readCache(currentTableFilm.getStreamUrl()); - }); - } } return; From 5600456556f2fd43f563530865d0e35442a5e901 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Mon, 14 Jan 2019 20:45:49 +0100 Subject: [PATCH 63/88] fixed open folder btn when using Linux, more clean up --- .../HomeFlix/application/Main.java | 1 - .../application/MainWindowController.java | 97 +++++++------------ .../controller/SourcesController.java | 2 +- .../kellerkinder/HomeFlix/player/Player.java | 4 +- .../HomeFlix/player/PlayerController.java | 12 +-- 5 files changed, 45 insertions(+), 71 deletions(-) diff --git a/src/main/java/kellerkinder/HomeFlix/application/Main.java b/src/main/java/kellerkinder/HomeFlix/application/Main.java index 33b7938..cd3d0ed 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/Main.java +++ b/src/main/java/kellerkinder/HomeFlix/application/Main.java @@ -101,7 +101,6 @@ public class Main extends Application { XMLController.getPosterCache().mkdir(); } - } catch (IOException e) { LOGGER.error(e); } diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index 57bfac4..df5ed4a 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -32,12 +32,13 @@ import java.io.InputStreamReader; import java.io.Writer; import java.math.BigInteger; import java.net.URLConnection; -import java.util.ArrayList; import java.util.Locale; import java.util.ResourceBundle; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import javax.swing.SwingUtilities; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.kellerkinder.Alerts.JFXInfoAlert; @@ -93,9 +94,9 @@ import kellerkinder.HomeFlix.controller.DBController; import kellerkinder.HomeFlix.controller.OMDbAPIController; import kellerkinder.HomeFlix.controller.UpdateController; import kellerkinder.HomeFlix.controller.XMLController; +import kellerkinder.HomeFlix.datatypes.FilmTabelDataType; import kellerkinder.HomeFlix.datatypes.SourceDataType; import kellerkinder.HomeFlix.player.Player; -import kellerkinder.HomeFlix.datatypes.FilmTabelDataType; public class MainWindowController { @@ -163,14 +164,13 @@ public class MainWindowController { // poster-mode // @FXML private AnchorPane posterModeAnchorPane; - private static DBController dbController; // the player needs the initialized dbController + private DBController dbController; private UpdateController updateController; private XMLController xmlController; private Stage primaryStage; private static final Logger LOGGER = LogManager.getLogger(MainWindowController.class.getName()); private boolean menuTrue = false; - private boolean settingsTrue = false; private final String version = "0.7.0"; private final String buildNumber = "169"; @@ -290,9 +290,8 @@ public class MainWindowController { burgerTask.play(); menuTrue = true; } - if (settingsTrue) { + if (settingsScrollPane.isVisible()) { settingsScrollPane.setVisible(false); - settingsTrue = false; } }); @@ -512,10 +511,11 @@ public class MainWindowController { @FXML private void openfolderbtnclicked() { - String dest = new File(getCurrentStreamUrl()).getParentFile().getAbsolutePath(); + File dest = new File(getCurrentStreamUrl()).getParentFile(); + if (!System.getProperty("os.name").contains("Linux")) { try { - Desktop.getDesktop().open(new File(dest)); + Desktop.getDesktop().open(dest); } catch (IOException e) { e.printStackTrace(); } @@ -543,12 +543,10 @@ public class MainWindowController { @FXML private void settingsBtnclicked() { - if (settingsTrue) { + if (settingsScrollPane.isVisible()) { settingsScrollPane.setVisible(false); - settingsTrue = false; } else { settingsScrollPane.setVisible(true); - settingsTrue = true; } } @@ -613,8 +611,8 @@ public class MainWindowController { * clear the FilmsList and FilmRoot children, then update the database */ private void refreshAllFilms() { - getFilmsList().clear(); - getFilmRoot().getChildren().clear(); + filmsList.clear(); + filmRoot.getChildren().clear(); dbController.refreshDataBase(); // refreshes the database after a source path was added filmsList = dbController.getDatabaseFilmsList(); // returns a list of all films stored in the database addFilmsToTable(filmsList); @@ -690,7 +688,7 @@ public class MainWindowController { // clear old sources list/table getSourcesList().clear(); - getSourceRoot().getChildren().clear(); + sourceRoot.getChildren().clear(); // clear the FilmsList and FilmRoot children, then update the database refreshAllFilms(); @@ -809,27 +807,27 @@ public class MainWindowController { ObservableList textFlow = getTextFlow().getChildren(); // TODO this should move! *** - ArrayList nameText = new ArrayList(); - nameText.add(new Text(XMLController.getLocalBundle().getString("title") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("year") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("rated") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("released") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("season") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("episode") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("runtime") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("genre") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("director") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("writer") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("actors") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("plot") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("language") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("country") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("awards") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("metascore") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("imdbRating") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("type") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("boxOffice") + ": ")); - nameText.add(new Text(XMLController.getLocalBundle().getString("website") + ": ")); + Text[] nameText = new Text[20]; + nameText[0] = new Text(XMLController.getLocalBundle().getString("title") + ": "); + nameText[1] = new Text(XMLController.getLocalBundle().getString("year") + ": "); + nameText[2] = new Text(XMLController.getLocalBundle().getString("rated") + ": "); + nameText[3] = new Text(XMLController.getLocalBundle().getString("released") + ": "); + nameText[4] = new Text(XMLController.getLocalBundle().getString("season") + ": "); + nameText[5] = new Text(XMLController.getLocalBundle().getString("episode") + ": "); + nameText[6] = new Text(XMLController.getLocalBundle().getString("runtime") + ": "); + nameText[7] = new Text(XMLController.getLocalBundle().getString("genre") + ": "); + nameText[8] = new Text(XMLController.getLocalBundle().getString("director") + ": "); + nameText[9] = new Text(XMLController.getLocalBundle().getString("writer") + ": "); + nameText[10] = new Text(XMLController.getLocalBundle().getString("actors") + ": "); + nameText[11] = new Text(XMLController.getLocalBundle().getString("plot") + ": "); + nameText[12] = new Text(XMLController.getLocalBundle().getString("language") + ": "); + nameText[13] = new Text(XMLController.getLocalBundle().getString("country") + ": "); + nameText[14] = new Text(XMLController.getLocalBundle().getString("awards") + ": "); + nameText[15] = new Text(XMLController.getLocalBundle().getString("metascore") + ": "); + nameText[16] = new Text(XMLController.getLocalBundle().getString("imdbRating") + ": "); + nameText[17] = new Text(XMLController.getLocalBundle().getString("type") + ": "); + nameText[18] = new Text(XMLController.getLocalBundle().getString("boxOffice") + ": "); + nameText[19] = new Text(XMLController.getLocalBundle().getString("website") + ": "); // *** // set the correct font for the nameText @@ -845,7 +843,7 @@ public class MainWindowController { if (cacheData[i] != null && (i == 5 || i == 6) && cacheData[5].length() == 0) { // do nothing } else if(cacheData[i] != null){ - textFlow.addAll(nameText.get(i), new Text(cacheData[i] + "\n")); + textFlow.addAll(nameText[i], new Text(cacheData[i] + "\n")); } } @@ -853,9 +851,9 @@ public class MainWindowController { // add the image try { - getPosterImageView().setImage(new Image(new File(cacheData[20]).toURI().toString())); + posterImageView.setImage(new Image(new File(cacheData[20]).toURI().toString())); } catch (Exception e) { - getPosterImageView().setImage(new Image("icons/close_black_2048x2048.png")); + posterImageView.setImage(new Image("icons/close_black_2048x2048.png")); LOGGER.error("No Poster found, useing default."); } } @@ -914,9 +912,6 @@ public class MainWindowController { } // getter and setter - public static DBController getDbController() { - return dbController; - } public FilmTabelDataType getCurrentTableFilm() { return currentTableFilm; @@ -938,35 +933,15 @@ public class MainWindowController { return indexList; } - public ObservableList getFilmsList() { - return filmsList; - } - public static ObservableList getSourcesList() { return sourcesList; } - public TreeTableView getFilmsTreeTable() { - return filmsTreeTable; - } - public TextFlow getTextFlow() { return textFlow; } - public ImageView getPosterImageView() { - return posterImageView; - } - public JFXButton getUpdateBtn() { return updateBtn; } - - public TreeItem getFilmRoot() { - return filmRoot; - } - - public TreeItem getSourceRoot() { - return sourceRoot; - } } diff --git a/src/main/java/kellerkinder/HomeFlix/controller/SourcesController.java b/src/main/java/kellerkinder/HomeFlix/controller/SourcesController.java index a55d6f8..dc2f6ff 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/SourcesController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/SourcesController.java @@ -61,7 +61,7 @@ public class SourcesController { for (JsonValue source : sources) { String path = source.asObject().getString("path", ""); String mode = source.asObject().getString("mode", ""); - MainWindowController.getSourcesList().add(new SourceDataType(path, mode)); + MainWindowController.getSourcesList().add(new SourceDataType(path, mode)); // TODO if (mode.equals("local")) addLocalSource(path); diff --git a/src/main/java/kellerkinder/HomeFlix/player/Player.java b/src/main/java/kellerkinder/HomeFlix/player/Player.java index 209185b..b585d36 100644 --- a/src/main/java/kellerkinder/HomeFlix/player/Player.java +++ b/src/main/java/kellerkinder/HomeFlix/player/Player.java @@ -30,7 +30,7 @@ import javafx.scene.layout.AnchorPane; import javafx.stage.Stage; import javafx.stage.WindowEvent; import kellerkinder.HomeFlix.application.Main; -import kellerkinder.HomeFlix.application.MainWindowController; +import kellerkinder.HomeFlix.controller.DBController; import kellerkinder.HomeFlix.datatypes.FilmTabelDataType; public class Player { @@ -59,7 +59,7 @@ public class Player { stage.getIcons().add(new Image(Main.class.getResourceAsStream("/icons/Homeflix_Icon_64x64.png"))); stage.setOnCloseRequest(new EventHandler() { public void handle(WindowEvent we) { - MainWindowController.getDbController().setCurrentTime(currentTableFilm.getStreamUrl(), + DBController.getInstance().setCurrentTime(currentTableFilm.getStreamUrl(), playerController.getCurrentTime()); playerController.getMediaPlayer().stop(); stage.close(); diff --git a/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java b/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java index 9750932..cb63d4a 100644 --- a/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java +++ b/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java @@ -47,7 +47,7 @@ import javafx.scene.media.MediaPlayer; import javafx.scene.media.MediaPlayer.Status; import javafx.scene.media.MediaView; import javafx.util.Duration; -import kellerkinder.HomeFlix.application.MainWindowController; +import kellerkinder.HomeFlix.controller.DBController; import kellerkinder.HomeFlix.controller.XMLController; import kellerkinder.HomeFlix.datatypes.FilmTabelDataType; @@ -122,7 +122,7 @@ public class PlayerController { width.bind(Bindings.selectDouble(mediaView.sceneProperty(), "width")); height.bind(Bindings.selectDouble(mediaView.sceneProperty(), "height")); - startTime = MainWindowController.getDbController().getCurrentTime(film.getStreamUrl()); + startTime = DBController.getInstance().getCurrentTime(film.getStreamUrl()); autoplay = XMLController.isAutoplay(); season = !film.getSeason().isEmpty() ? Integer.parseInt(film.getSeason()) : 0; episode = !film.getEpisode().isEmpty() ? Integer.parseInt(film.getEpisode()) : 0; @@ -175,7 +175,7 @@ public class PlayerController { } else if (timeToEnd < 120) { // if we are 120ms to the end stop the media mediaPlayer.stop(); - MainWindowController.getDbController().setCurrentTime(film.getStreamUrl(), 0); // reset old video start time + DBController.getInstance().setCurrentTime(film.getStreamUrl(), 0); // reset old video start time playBtn.setGraphic(play_arrow_black); } else { if (nextEpBtn.isVisible()) @@ -253,7 +253,7 @@ public class PlayerController { @FXML void stopBtnAction(ActionEvent event) { - MainWindowController.getDbController().setCurrentTime(film.getStreamUrl(), currentTime); + DBController.getInstance().setCurrentTime(film.getStreamUrl(), currentTime); mediaPlayer.stop(); player.getStage().close(); } @@ -287,8 +287,8 @@ public class PlayerController { private void autoPlayNewFilm() { autoplay = false; - MainWindowController.getDbController().setCurrentTime(film.getStreamUrl(), 0); // reset old video start time - FilmTabelDataType nextFilm = MainWindowController.getDbController().getNextEpisode(film.getTitle(), episode, season); + DBController.getInstance().setCurrentTime(film.getStreamUrl(), 0); // reset old video start time + FilmTabelDataType nextFilm = DBController.getInstance().getNextEpisode(film.getTitle(), episode, season); if (nextFilm != null) { mediaPlayer.stop(); film = nextFilm; From c735583a040a3b938e9e0ac613b63332b32bb946 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Tue, 22 Jan 2019 17:29:56 +0100 Subject: [PATCH 64/88] cache is now date based, minor clean up --- .../application/MainWindowController.java | 32 ++++++---- .../HomeFlix/controller/DBController.java | 64 +++++++++---------- .../controller/OMDbAPIController.java | 19 ++++-- .../controller/SourcesController.java | 7 +- .../HomeFlix/datatypes/DatabaseDataType.java | 10 +-- .../HomeFlix/datatypes/FilmTabelDataType.java | 12 ++-- 6 files changed, 82 insertions(+), 62 deletions(-) diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index df5ed4a..efa48af 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -32,13 +32,12 @@ import java.io.InputStreamReader; import java.io.Writer; import java.math.BigInteger; import java.net.URLConnection; +import java.time.LocalDate; import java.util.Locale; import java.util.ResourceBundle; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import javax.swing.SwingUtilities; - import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.kellerkinder.Alerts.JFXInfoAlert; @@ -149,7 +148,7 @@ public class MainWindowController { @FXML private JFXTextField searchTextField; @FXML private TreeTableView filmsTreeTable; - @FXML private TreeItem filmRoot = new TreeItem<>(new FilmTabelDataType("", "", "", "", false, false, null)); + @FXML private TreeItem filmRoot = new TreeItem<>(new FilmTabelDataType("", "", "", "", false, null, null)); @FXML private ScrollPane textScrollPane; @@ -182,7 +181,7 @@ public class MainWindowController { private int indexTable; private int indexList; private int next; - private FilmTabelDataType currentTableFilm = new FilmTabelDataType("", "", "", "", false, false, null); + private FilmTabelDataType currentTableFilm = new FilmTabelDataType("", "", "", "", false, null, null); private ObservableList languages = FXCollections.observableArrayList("English (en_US)", "Deutsch (de_DE)"); private ObservableList branches = FXCollections.observableArrayList("stable", "beta"); @@ -192,6 +191,7 @@ public class MainWindowController { private MenuItem like = new MenuItem("like"); private MenuItem dislike = new MenuItem("dislike"); // TODO one option (like or dislike) private ContextMenu menu = new ContextMenu(like, dislike); + private LocalDate lastValidCache = LocalDate.now().minusDays(30); // current date - 30 days is the last valid cache date public MainWindowController() { // the constructor @@ -425,11 +425,15 @@ public class MainWindowController { last = indexTable - 1; next = indexTable + 1; + - if (currentTableFilm.getCached() || dbController.searchCacheByURL(getCurrentStreamUrl())) { + if ((currentTableFilm.getCached().isAfter(lastValidCache) )|| dbController.searchCacheByURL(getCurrentStreamUrl())) { LOGGER.info("loading from cache: " + getCurrentTitle()); setSelectedFilmInfo(dbController.readCache(getCurrentStreamUrl())); + System.out.println("cache date is: " + currentTableFilm.getCached().toString()); } else { + System.out.println("either not cached or to old!"); + System.out.println("cache date is: " + currentTableFilm.getCached().toString()); // this is not perfect! new Thread(new Runnable() { public void run() { @@ -640,7 +644,7 @@ public class MainWindowController { // if no root node exists, create one and add element as child TreeItem seriesRootNode = new TreeItem<>(new FilmTabelDataType( element.getTitle() + "_rootNode", element.getTitle(), "", "", element.getFavorite(), - false, element.getImage())); + element.getCached() , element.getImage())); filmRoot.getChildren().add(seriesRootNode); } } @@ -829,7 +833,7 @@ public class MainWindowController { nameText[18] = new Text(XMLController.getLocalBundle().getString("boxOffice") + ": "); nameText[19] = new Text(XMLController.getLocalBundle().getString("website") + ": "); // *** - + // set the correct font for the nameText for (Text text : nameText) { text.setFont(font); @@ -850,11 +854,15 @@ public class MainWindowController { getTextFlow().setStyle("-fx-font-size : " + ((int) Math.round(XMLController.getFontSize()) + 1) + "px;"); // add the image - try { - posterImageView.setImage(new Image(new File(cacheData[20]).toURI().toString())); - } catch (Exception e) { - posterImageView.setImage(new Image("icons/close_black_2048x2048.png")); - LOGGER.error("No Poster found, useing default."); + if (new File(cacheData[20]).isFile()) { + try { + posterImageView.setImage(new Image(new File(cacheData[20]).toURI().toString())); + } catch (Exception e) { + posterImageView.setImage(new Image(cacheData[20])); + LOGGER.error("No Poster found, useing default."); + } + } else { + posterImageView.setImage(new Image(cacheData[20])); } } diff --git a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java index 6e87e16..bba1c71 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java @@ -25,6 +25,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.net.URLConnection; import java.sql.Connection; +import java.sql.Date; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -50,7 +51,7 @@ public class DBController { private String DB_PATH; private Image favorite_black = new Image("icons/ic_favorite_black_18dp_1x.png"); private Image favorite_border_black = new Image("icons/ic_favorite_border_black_18dp_1x.png"); - private List databaseStream = new ArrayList(); // contains all films stored in the database + private List databaseStreams = new ArrayList(); // contains all films stored in the database private List sourceStreams = new ArrayList(); // contains all films from the sources private Connection connection = null; private static final Logger LOGGER = LogManager.getLogger(DBController.class.getName()); @@ -79,11 +80,8 @@ public class DBController { * refresh the database */ public void init() { - LOGGER.info("<========== starting loading sql ==========>"); initDatabaseConnection(); createDatabase(); - refreshDataBase(); - LOGGER.info("<========== finished loading sql ==========>"); } /** @@ -98,9 +96,9 @@ public class DBController { connection.setAutoCommit(false); } catch (SQLException e) { // if the error message is "out of memory", it probably means no database file is found - LOGGER.error("error while loading the ROM database", e); + LOGGER.error("error while loading the HomeFlix database", e); } - LOGGER.info("ROM database loaded successfull"); + LOGGER.info("HomeFlix database loaded successfull"); } /** @@ -131,9 +129,9 @@ public class DBController { Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM films"); while (rs.next()) { - databaseStream.add(new DatabaseDataType(rs.getString("streamUrl"), + databaseStreams.add(new DatabaseDataType(rs.getString("streamUrl"), rs.getString("title"), rs.getString("season"), rs.getString("episode"), - rs.getInt("favorite"), rs.getBoolean("cached"), rs.getDouble("currentTime"))); + rs.getInt("favorite"), rs.getDate("cached"), rs.getDouble("currentTime"))); } stmt.close(); rs.close(); @@ -157,17 +155,17 @@ public class DBController { */ public ObservableList getDatabaseFilmsList() { ObservableList filmsList = FXCollections.observableArrayList(); - LOGGER.info("loading data to mwc ..."); + LOGGER.info("loading data from database ..."); try { //load local Data Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM films ORDER BY title"); - while (rs.next()) { + while (rs.next()) { ImageView imageView = rs.getBoolean("favorite") ? new ImageView(favorite_black) : new ImageView(favorite_border_black); filmsList.add(new FilmTabelDataType(rs.getString("streamUrl"), rs.getString("title"), rs.getString("season"), rs.getString("episode") ,rs.getBoolean("favorite"), - rs.getBoolean("cached"), imageView)); + rs.getDate("cached").toLocalDate(), imageView)); } stmt.close(); rs.close(); @@ -175,8 +173,6 @@ public class DBController { LOGGER.error("Ups! an error occured!", e); } - LOGGER.info("loading data to the GUI ..."); - return filmsList; } @@ -197,7 +193,7 @@ public class DBController { ImageView imageView = rs.getBoolean("favorite") ? new ImageView(favorite_black) : new ImageView(favorite_border_black); film = new FilmTabelDataType(rs.getString("streamUrl"), rs.getString("title"), rs.getString("season"), rs.getString("episode"), rs.getBoolean("favorite"), - rs.getBoolean("cached"), imageView); + rs.getDate("cached").toLocalDate(), imageView); } rs.close(); ps.close(); @@ -212,16 +208,16 @@ public class DBController { * refresh database to contain all (new added) entries */ public void refreshDataBase() { - LOGGER.info("refreshing the Database ..."); + LOGGER.info("<========== starting refreshing database ==========>"); // clean all ArraLists - databaseStream.clear(); + databaseStreams.clear(); sourceStreams.clear(); loadSources(); // reload all sources loadDatabase(); // reload all films saved in the DB - LOGGER.info("filme in db: " + databaseStream.size()); + LOGGER.info("filme in db: " + databaseStreams.size()); try { checkAddEntry(); @@ -229,6 +225,8 @@ public class DBController { } catch (Exception e) { LOGGER.error("Error while refreshing the database", e); } + + LOGGER.info("<========== finished refreshing database ==========>"); } /** @@ -239,7 +237,7 @@ public class DBController { PreparedStatement ps = connection.prepareStatement("DELETE FROM films WHERE streamUrl = ?"); LOGGER.info("checking for entrys to remove to DB ..."); - for (DatabaseDataType dbStreamEntry : databaseStream) { + for (DatabaseDataType dbStreamEntry : databaseStreams) { // if the directory doen't contain the entry form the database, remove it // if sourceStreams has a item where StreamUrl equals dbStreamEntry.getStreamUrl() return it, else null @@ -275,7 +273,7 @@ public class DBController { for (DatabaseDataType sourceStreamEntry : sourceStreams) { // if databaseStream has a item where StreamUrl equals sourceStreamEntry.getStreamUrl() return it, else null - DatabaseDataType result = databaseStream.stream() + DatabaseDataType result = databaseStreams.stream() .filter(x -> sourceStreamEntry.getStreamUrl().equals(x.getStreamUrl())) .findAny() .orElse(null); @@ -287,11 +285,11 @@ public class DBController { ps.setString(3, sourceStreamEntry.getSeason()); ps.setString(4, sourceStreamEntry.getEpisode()); ps.setInt(5, sourceStreamEntry.getFavorite()); - ps.setBoolean(6, sourceStreamEntry.getCached()); + ps.setDate(6, sourceStreamEntry.getCached()); ps.setDouble(7, sourceStreamEntry.getCurrentTime()); ps.addBatch(); // adds the entry LOGGER.info("Added \"" + sourceStreamEntry.getTitle() + "\" to database"); - databaseStream.add(sourceStreamEntry); + databaseStreams.add(sourceStreamEntry); } } @@ -365,8 +363,9 @@ public class DBController { */ public void setCached(String streamUrl) { try { - PreparedStatement ps = connection.prepareStatement("UPDATE films SET cached = 1 WHERE streamUrl = ?"); - ps.setString(1, streamUrl); + PreparedStatement ps = connection.prepareStatement("UPDATE films SET cached = ? WHERE streamUrl = ?"); + ps.setDate(1, new Date(System.currentTimeMillis())); + ps.setString(2, streamUrl); ps.executeUpdate(); connection.commit(); ps.close(); @@ -501,7 +500,7 @@ public class DBController { while (rs.next()) { notCachedEntries.add(new FilmTabelDataType(rs.getString("streamUrl"), rs.getString("title"), rs.getString("season"), rs.getString("episode"), rs.getBoolean("favorite"), - rs.getBoolean("cached"), new ImageView(favorite_border_black))); + rs.getDate("cached").toLocalDate(), new ImageView(favorite_border_black))); } stmt.close(); rs.close(); @@ -519,7 +518,7 @@ public class DBController { * @return {@link Double} currentTime in ms */ public double getCurrentTime(String streamUrl) { - LOGGER.info("get currentTime: " + streamUrl); + LOGGER.info("get currentTime for " + streamUrl); double currentTime = 0; try { PreparedStatement ps = connection.prepareStatement("SELECT * FROM films WHERE streamUrl = ?"); @@ -541,7 +540,7 @@ public class DBController { * @param currentTime currentTime in ms of the film */ public void setCurrentTime(String streamUrl, double currentTime) { - LOGGER.info("set currentTime: " + streamUrl); + LOGGER.info("set currentTime = " + currentTime + " for " + streamUrl); try { PreparedStatement ps = connection.prepareStatement("UPDATE films SET currentTime = ? WHERE streamUrl = ?"); ps.setDouble(1, currentTime); @@ -586,7 +585,7 @@ public class DBController { // at this point we have found the correct episode nextFilm = new FilmTabelDataType(rs.getString("streamUrl"), rs.getString("title"), rs.getString("season"), rs.getString("episode"), rs.getBoolean("favorite"), - rs.getBoolean("cached"), new ImageView()); + rs.getDate("cached").toLocalDate(), new ImageView()); rs.close(); ps.close(); @@ -596,7 +595,7 @@ public class DBController { return nextFilm; } - /** + /** TODO rework, we should save the next episode in the root entry * get the last watched episode * @param title the title of the series * @return the last watched episode as {@link FilmTabelDataType} object @@ -612,12 +611,9 @@ public class DBController { while (rs.next()) { nextFilm = new FilmTabelDataType(rs.getString("streamUrl"), rs.getString("title"), rs.getString("season"), rs.getString("episode"), rs.getBoolean("favorite"), - rs.getBoolean("cached"), new ImageView()); + rs.getDate("cached").toLocalDate(), new ImageView()); + // get the first episode where currentTime != 0 if (rs.getDouble("currentTime") > lastCurrentTime) { - lastCurrentTime = rs.getDouble("currentTime"); - nextFilm = new FilmTabelDataType(rs.getString("streamUrl"), rs.getString("title"), - rs.getString("season"), rs.getString("episode"), rs.getBoolean("favorite"), - rs.getBoolean("cached"), new ImageView()); break; } } @@ -626,7 +622,7 @@ public class DBController { } catch (Exception e) { LOGGER.error("Ups! error while getting the last watched episode!", e); } - + return nextFilm; } diff --git a/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java b/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java index 3ab3219..f598446 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java @@ -85,6 +85,20 @@ public class OMDbAPIController implements Runnable { object = getByTitle(title); } } else { + // add default poster and cache + LOGGER.warn("Adding default poster and cache entries for \"{}\"!", currentTableFilm.getTitle()); + OMDbAPIResponseDataType omdbResponse = new OMDbAPIResponseDataType(); + omdbResponse.setTitle(currentTableFilm.getTitle()); + omdbResponse.setSeason(currentTableFilm.getSeason()); + omdbResponse.setEpisode(currentTableFilm.getEpisode()); + omdbResponse.setPoster("icons/Homeflix_Poster.png"); + + synchronized (this) { + // adding to cache + dbController.addCache(currentTableFilm.getStreamUrl(), omdbResponse); + dbController.setCached(currentTableFilm.getStreamUrl()); + } + return; } } @@ -198,13 +212,10 @@ public class OMDbAPIController implements Runnable { if (searchObject.getString("Response", "").equals("True")) { for (JsonValue movie : searchObject.get("Search").asArray()) { // get first entry from the array and set object = movie - // TODO if the search was successful, we should add the wrong and correct title to a list, for later speedup return movie.asObject().getString("Title", ""); } } else { - // TODO set cached 1 and set the HomeFlix logo as picture -// System.out.println("Object is: " + searchObject); - LOGGER.warn("Movie \"{}\" not found! Not adding cache!", title); + LOGGER.warn("Movie \"{}\" not found!", title); } return ""; } diff --git a/src/main/java/kellerkinder/HomeFlix/controller/SourcesController.java b/src/main/java/kellerkinder/HomeFlix/controller/SourcesController.java index dc2f6ff..ef07925 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/SourcesController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/SourcesController.java @@ -26,6 +26,7 @@ import java.io.File; import java.io.FileReader; import java.io.IOException; import java.nio.file.Files; +import java.sql.Date; import java.util.ArrayList; import java.util.List; @@ -88,7 +89,7 @@ public class SourcesController { for (File file : new File(path).listFiles()) { // if it's valid file add it to the sourceStreams if (isValidFile(file)) { - sourceStreams.add(new DatabaseDataType(file.getPath(), cutOffEnd(file.getName()), "", "", 0, false, 0.0)); + sourceStreams.add(new DatabaseDataType(file.getPath(), cutOffEnd(file.getName()), "", "", 0, new Date(0), 0.0)); } else if(file.isDirectory()) { // get all directories (series), root and season must be directories int sn = 1; @@ -99,7 +100,7 @@ public class SourcesController { // check whether the episode is a valid file if (isValidFile(episode)) { sourceStreams.add(new DatabaseDataType(episode.getPath(), cutOffEnd(file.getName()), - Integer.toString(sn), Integer.toString(ep), 0, false, 0.0)); + Integer.toString(sn), Integer.toString(ep), 0, new Date(0), 0.0)); ep++; } } @@ -121,7 +122,7 @@ public class SourcesController { sourceStreams.add(new DatabaseDataType(item.asObject().getString("streamUrl", ""), item.asObject().getString("title", ""), item.asObject().getString("season", ""), - item.asObject().getString("episode", ""), 0, false, 0.0)); + item.asObject().getString("episode", ""), 0, new Date(0), 0.0)); } LOGGER.info("added " + items.size() +" stream entries from: " + path); } catch (IOException e) { diff --git a/src/main/java/kellerkinder/HomeFlix/datatypes/DatabaseDataType.java b/src/main/java/kellerkinder/HomeFlix/datatypes/DatabaseDataType.java index 31c089f..e22548c 100644 --- a/src/main/java/kellerkinder/HomeFlix/datatypes/DatabaseDataType.java +++ b/src/main/java/kellerkinder/HomeFlix/datatypes/DatabaseDataType.java @@ -22,6 +22,8 @@ package kellerkinder.HomeFlix.datatypes; +import java.sql.Date; + public class DatabaseDataType { private String streamUrl; @@ -29,10 +31,10 @@ public class DatabaseDataType { private String season; private String episode; private int favorite; - private Boolean cached; + private Date cached; private double currentTime; - public DatabaseDataType(String streamUrl, String title, String season, String episode, int favorite, Boolean cached, double currentTime) { + public DatabaseDataType(String streamUrl, String title, String season, String episode, int favorite, Date cached, double currentTime) { this.streamUrl = streamUrl; this.title = title; this.season = season; @@ -82,11 +84,11 @@ public class DatabaseDataType { this.favorite = favorite; } - public Boolean getCached() { + public Date getCached() { return cached; } - public void setCached(Boolean cached) { + public void setCached(Date cached) { this.cached = cached; } diff --git a/src/main/java/kellerkinder/HomeFlix/datatypes/FilmTabelDataType.java b/src/main/java/kellerkinder/HomeFlix/datatypes/FilmTabelDataType.java index b019752..0d7301c 100644 --- a/src/main/java/kellerkinder/HomeFlix/datatypes/FilmTabelDataType.java +++ b/src/main/java/kellerkinder/HomeFlix/datatypes/FilmTabelDataType.java @@ -20,6 +20,8 @@ */ package kellerkinder.HomeFlix.datatypes; +import java.time.LocalDate; + import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; @@ -33,7 +35,7 @@ public class FilmTabelDataType { private final StringProperty season = new SimpleStringProperty(); private final StringProperty episode = new SimpleStringProperty(); private final BooleanProperty favorite = new SimpleBooleanProperty(); - private final BooleanProperty cached = new SimpleBooleanProperty(); + private final SimpleObjectProperty cached = new SimpleObjectProperty<>(); private final SimpleObjectProperty image = new SimpleObjectProperty<>(); /** @@ -47,7 +49,7 @@ public class FilmTabelDataType { * @param image favorite icon */ public FilmTabelDataType(final String streamUrl, final String title, final String season, final String episode, - final boolean favorite, final boolean cached, final ImageView image) { + final boolean favorite, final LocalDate cached, final ImageView image) { this.streamUrl.set(streamUrl); this.title.set(title); this.season.set(season); @@ -77,7 +79,7 @@ public class FilmTabelDataType { return favorite; } - public BooleanProperty cachedProperty(){ + public SimpleObjectProperty cachedProperty(){ return cached; } @@ -106,7 +108,7 @@ public class FilmTabelDataType { return favoriteProperty().get(); } - public final boolean getCached(){ + public final LocalDate getCached(){ return cachedProperty().get(); } @@ -135,7 +137,7 @@ public class FilmTabelDataType { favoriteProperty().set(favorite); } - public final void setCached(boolean cached){ + public final void setCached(LocalDate cached){ cachedProperty().set(cached); } From e47f3ea2f72d3c18c46f1aedc2f65417d72a2624 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Tue, 22 Jan 2019 18:12:30 +0100 Subject: [PATCH 65/88] minor bug fixes --- .../application/MainWindowController.java | 102 +++++++++--------- src/main/resources/fxml/MainWindow.fxml | 9 ++ 2 files changed, 63 insertions(+), 48 deletions(-) diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index efa48af..3c87741 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -137,17 +137,16 @@ public class MainWindowController { @FXML private TableColumn sourceColumn; @FXML private TableColumn modeColumn; - @FXML private TreeTableColumn columnStreamUrl; - @FXML private TreeTableColumn columnTitle; - @FXML private TreeTableColumn columnFavorite; - @FXML private TreeTableColumn columnSeason; - @FXML private TreeTableColumn columnEpisode; - // table-mode @FXML private AnchorPane tableModeAnchorPane; @FXML private JFXTextField searchTextField; @FXML private TreeTableView filmsTreeTable; + @FXML private TreeTableColumn columnStreamUrl; + @FXML private TreeTableColumn columnTitle; + @FXML private TreeTableColumn columnFavorite; + @FXML private TreeTableColumn columnSeason; + @FXML private TreeTableColumn columnEpisode; @FXML private TreeItem filmRoot = new TreeItem<>(new FilmTabelDataType("", "", "", "", false, null, null)); @@ -161,7 +160,8 @@ public class MainWindowController { @FXML private JFXButton forwardBtn; // poster-mode -// @FXML private AnchorPane posterModeAnchorPane; + @FXML private AnchorPane posterModeAnchorPane; + @FXML private ScrollPane posterModeScrollPane; private DBController dbController; private UpdateController updateController; @@ -220,7 +220,7 @@ public class MainWindowController { // load sources list in gui addSourceToTable(); -// posterModeStartup(); // TODO testing DO NOT USE THIS!! +// posterModeStartup(); // TODO testing DO NOT USE THIS!! } // Initialize general UI elements @@ -464,51 +464,53 @@ public class MainWindowController { // Table-Mode fxml actions @FXML private void playbtnclicked() { - if (currentTableFilm.getStreamUrl().contains("_rootNode")) { - LOGGER.info("rootNode found, getting last watched episode"); - currentTableFilm = dbController.getLastWatchedEpisode(currentTableFilm.getTitle()); - } - - if (isSupportedFormat(currentTableFilm)) { - new Player(getCurrentTableFilm()); - } else { - LOGGER.error("using fallback player!"); - if (System.getProperty("os.name").contains("Linux")) { - String line; - String output = ""; - Process p; - try { - p = Runtime.getRuntime().exec("which vlc"); - BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream())); - while ((line = input.readLine()) != null) { - output = line; - } - LOGGER.info("which vlc: " + output); - input.close(); - } catch (IOException e1) { - e1.printStackTrace(); - } - if (output.contains("which: no vlc") || output == "") { - JFXInfoAlert vlcInfoAlert = new JFXInfoAlert("Info", - XMLController.getLocalBundle().getString("vlcNotInstalled"), - btnStyle, primaryStage); - vlcInfoAlert.showAndWait(); - } else { + if (currentTableFilm.getStreamUrl().length() > 0) { + if (currentTableFilm.getStreamUrl().contains("_rootNode")) { + LOGGER.info("rootNode found, getting last watched episode"); + currentTableFilm = dbController.getLastWatchedEpisode(currentTableFilm.getTitle()); + } + + if (isSupportedFormat(currentTableFilm)) { + new Player(getCurrentTableFilm()); + } else { + LOGGER.error("using fallback player!"); + if (System.getProperty("os.name").contains("Linux")) { + String line; + String output = ""; + Process p; try { - new ProcessBuilder("vlc", getCurrentStreamUrl()).start(); + p = Runtime.getRuntime().exec("which vlc"); + BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream())); + while ((line = input.readLine()) != null) { + output = line; + } + LOGGER.info("which vlc: " + output); + input.close(); + } catch (IOException e1) { + e1.printStackTrace(); + } + if (output.contains("which: no vlc") || output == "") { + JFXInfoAlert vlcInfoAlert = new JFXInfoAlert("Info", + XMLController.getLocalBundle().getString("vlcNotInstalled"), + btnStyle, primaryStage); + vlcInfoAlert.showAndWait(); + } else { + try { + new ProcessBuilder("vlc", getCurrentStreamUrl()).start(); + } catch (IOException e) { + LOGGER.warn("An error has occurred while opening the file!", e); + } + } + + } else if (System.getProperty("os.name").contains("Windows") || System.getProperty("os.name").contains("Mac OS X")) { + try { + Desktop.getDesktop().open(new File(getCurrentStreamUrl())); } catch (IOException e) { LOGGER.warn("An error has occurred while opening the file!", e); } + } else { + LOGGER.error(System.getProperty("os.name") + ", OS is not supported, please contact a developer! "); } - - } else if (System.getProperty("os.name").contains("Windows") || System.getProperty("os.name").contains("Mac OS X")) { - try { - Desktop.getDesktop().open(new File(getCurrentStreamUrl())); - } catch (IOException e) { - LOGGER.warn("An error has occurred while opening the file!", e); - } - } else { - LOGGER.error(System.getProperty("os.name") + ", OS is not supported, please contact a developer! "); } } } @@ -894,6 +896,10 @@ public class MainWindowController { return mimeType != null && (mimeType.contains("mp4") || mimeType.contains("vp6")); } + /** + * Poser Mode WIP + */ + private void posterModeStartup() { checkAllPosters(); } diff --git a/src/main/resources/fxml/MainWindow.fxml b/src/main/resources/fxml/MainWindow.fxml index b850b67..9bd0731 100644 --- a/src/main/resources/fxml/MainWindow.fxml +++ b/src/main/resources/fxml/MainWindow.fxml @@ -64,6 +64,15 @@ + + + + + + + + + From 0786770e97b0a7ff9f85e97cf3d013c7a37d296d Mon Sep 17 00:00:00 2001 From: Seil0 Date: Fri, 1 Mar 2019 19:00:04 +0100 Subject: [PATCH 66/88] added a comment --- .../java/kellerkinder/HomeFlix/controller/XMLController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/kellerkinder/HomeFlix/controller/XMLController.java b/src/main/java/kellerkinder/HomeFlix/controller/XMLController.java index 02670e6..098f6db 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/XMLController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/XMLController.java @@ -64,7 +64,7 @@ public class XMLController { private static double fontSize = 17; private static ResourceBundle localBundle = ResourceBundle.getBundle("locals.HomeFlix-Local", Locale.US); - // user settings + // api settings private static String omdbAPIKey; private static final Logger LOGGER = LogManager.getLogger(XMLController.class.getName()); From 3381047e76fed041289b956dd6548511fc698105 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Mon, 11 Mar 2019 18:38:56 +0100 Subject: [PATCH 67/88] updated javafx to 11.0.2 --- pom.xml | 6 +++--- .../HomeFlix/application/MainWindowController.java | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 153bd94..85239ae 100644 --- a/pom.xml +++ b/pom.xml @@ -27,19 +27,19 @@ org.openjfx javafx-controls - 11.0.1 + 11.0.2 org.openjfx javafx-fxml - 11.0.1 + 11.0.2 org.openjfx javafx-media - 11.0.1 + 11.0.2 diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index 3c87741..b93b516 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -430,10 +430,10 @@ public class MainWindowController { if ((currentTableFilm.getCached().isAfter(lastValidCache) )|| dbController.searchCacheByURL(getCurrentStreamUrl())) { LOGGER.info("loading from cache: " + getCurrentTitle()); setSelectedFilmInfo(dbController.readCache(getCurrentStreamUrl())); - System.out.println("cache date is: " + currentTableFilm.getCached().toString()); +// System.out.println("cache date is: " + currentTableFilm.getCached().toString()); } else { - System.out.println("either not cached or to old!"); - System.out.println("cache date is: " + currentTableFilm.getCached().toString()); +// System.out.println("either not cached or to old!"); +// System.out.println("cache date is: " + currentTableFilm.getCached().toString()); // this is not perfect! new Thread(new Runnable() { public void run() { From 46cd28a642f96865810bfa6a18f0eeb5710f6d4a Mon Sep 17 00:00:00 2001 From: Seil0 Date: Mon, 1 Apr 2019 11:16:58 +0200 Subject: [PATCH 68/88] updated to javafx 12 --- pom.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 85239ae..fe41879 100644 --- a/pom.xml +++ b/pom.xml @@ -27,19 +27,19 @@ org.openjfx javafx-controls - 11.0.2 + 12 org.openjfx javafx-fxml - 11.0.2 + 12 org.openjfx javafx-media - 11.0.2 + 12 @@ -63,19 +63,19 @@ org.xerial sqlite-jdbc - 3.25.2 + 3.27.2.1 org.apache.logging.log4j log4j-api - 2.11.1 + 2.11.2 org.apache.logging.log4j log4j-core - 2.11.1 + 2.11.2 From fdb39ea2f529f4a104b94bdf591745d0f2d600a1 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Mon, 6 May 2019 00:50:27 +0200 Subject: [PATCH 69/88] fixed missing elements when searching * minor clean up --- .../application/MainWindowController.java | 499 +++++++++--------- 1 file changed, 238 insertions(+), 261 deletions(-) diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index b93b516..0883075 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -37,6 +37,7 @@ import java.util.Locale; import java.util.ResourceBundle; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -55,12 +56,8 @@ import com.jfoenix.transitions.hamburger.HamburgerBackArrowBasicTransition; import javafx.animation.TranslateTransition; import javafx.application.Platform; -import javafx.beans.value.ChangeListener; -import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ObservableList; -import javafx.event.ActionEvent; -import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.scene.Node; import javafx.scene.control.ChoiceBox; @@ -168,21 +165,21 @@ public class MainWindowController { private XMLController xmlController; private Stage primaryStage; private static final Logger LOGGER = LogManager.getLogger(MainWindowController.class.getName()); - + private boolean menuTrue = false; private final String version = "0.7.0"; private final String buildNumber = "169"; private final String versionName = "toothless dragon"; private String btnStyle; - + private final int hashA = -647380320; private int last; private int indexTable; private int indexList; private int next; private FilmTabelDataType currentTableFilm = new FilmTabelDataType("", "", "", "", false, null, null); - + private ObservableList languages = FXCollections.observableArrayList("English (en_US)", "Deutsch (de_DE)"); private ObservableList branches = FXCollections.observableArrayList("stable", "beta"); private ObservableList filterData = FXCollections.observableArrayList(); @@ -196,19 +193,19 @@ public class MainWindowController { public MainWindowController() { // the constructor } - + @FXML public void initialize() { xmlController = new XMLController(); dbController = DBController.getInstance(); } - - public void init() { + + public void init() { LOGGER.info("Initializing Project-HomeFlix build " + buildNumber); - + xmlController.loadSettings(); // load settings checkAutoUpdate(); - + // initialize the GUI and the DBController primaryStage = (Stage) mainAnchorPane.getScene().getWindow(); // set primary stage for dialogs initTabel(); @@ -216,13 +213,13 @@ public class MainWindowController { initActions(); dbController.init(); refreshAllFilms(); - + // load sources list in gui addSourceToTable(); - + // posterModeStartup(); // TODO testing DO NOT USE THIS!! } - + // Initialize general UI elements private void initUI() { versionLbl.setText("Version: " + version + " (Build: " + buildNumber + ")"); @@ -234,20 +231,20 @@ public class MainWindowController { autoplayToggleBtn.setSelected(XMLController.isAutoplay()); languageChoisBox.setItems(languages); branchChoisBox.setItems(branches); - + if (XMLController.isUseBeta()) { branchChoisBox.getSelectionModel().select(1); } else { branchChoisBox.getSelectionModel().select(0); } - + setLocalUI(); applyColor(); } - + /** - * Initialize the tables (treeTableViewfilm and sourcesTable) - * only needed for Tabel-Mode + * Initialize the tables (treeTableViewfilm and sourcesTable) only needed for + * Tabel-Mode */ private void initTabel() { @@ -283,7 +280,6 @@ public class MainWindowController { burgerTask.setRate(-1.0); burgerTask.play(); menuTrue = false; - } else { sideMenuSlideIn(); burgerTask.setRate(1.0); @@ -294,173 +290,134 @@ public class MainWindowController { settingsScrollPane.setVisible(false); } }); - - languageChoisBox.getSelectionModel().selectedIndexProperty().addListener(new ChangeListener() { - @Override - public void changed(ObservableValue ov, Number value, Number new_value) { - String local = languageChoisBox.getItems().get((int) new_value).toString(); - local = local.substring(local.length() - 6, local.length() - 1); // reading only en_US from English (en_US) - XMLController.setUsrLocal(local); - setLocalUI(); - xmlController.saveSettings(); - } + + languageChoisBox.getSelectionModel().selectedIndexProperty().addListener((e, oldValue, newValue) -> { + String local = languageChoisBox.getItems().get((int) newValue).toString(); + local = local.substring(local.length() - 6, local.length() - 1); // reading only en_US from English (en_US) + XMLController.setUsrLocal(local); + setLocalUI(); + xmlController.saveSettings(); }); - branchChoisBox.getSelectionModel().selectedIndexProperty().addListener(new ChangeListener() { - @Override - public void changed(ObservableValue ov, Number value, Number new_value) { - if (branchChoisBox.getItems().get((int) new_value).toString() == "beta") { - XMLController.setUseBeta(true); - } else { - XMLController.setUseBeta(false); - } - xmlController.saveSettings(); + branchChoisBox.getSelectionModel().selectedIndexProperty().addListener((e, oldValue, newValue) -> { + if (branchChoisBox.getItems().get((int) newValue).toString() == "beta") { + XMLController.setUseBeta(true); + } else { + XMLController.setUseBeta(false); } + xmlController.saveSettings(); }); - - fontsizeSlider.valueProperty().addListener(new ChangeListener() { - @Override - public void changed(ObservableValue ov, Number old_val, Number new_val) { - XMLController.setFontSize(fontsizeSlider.getValue()); - if (!getCurrentTitle().isEmpty()) { - setSelectedFilmInfo(dbController.readCache(getCurrentStreamUrl())); - } - xmlController.saveSettings(); + + fontsizeSlider.valueProperty().addListener(e -> { + XMLController.setFontSize(fontsizeSlider.getValue()); + if (!getCurrentTitle().isEmpty()) { + setSelectedFilmInfo(dbController.readCache(getCurrentStreamUrl())); } + xmlController.saveSettings(); }); - + // Table-Mode actions - searchTextField.textProperty().addListener(new ChangeListener() { - @Override - public void changed(ObservableValue observable, String oldValue, String newValue) { - ObservableList helpData; - filterData.clear(); - filmRoot.getChildren().clear(); + searchTextField.textProperty().addListener((e, oldValue, newValue) -> { + filmRoot.getChildren().clear(); + filterData.clear(); + filterData = filmsList.stream() + .filter(x -> x.getTitle().toLowerCase().contains(searchTextField.getText().toLowerCase())) + .collect(Collectors.toCollection(FXCollections::observableArrayList)); - helpData = filmsList; + addFilmsToTable(filterData); - for (int i = 0; i < helpData.size(); i++) { - if (helpData.get(i).getTitle().toLowerCase().contains(searchTextField.getText().toLowerCase())) { - filterData.add(helpData.get(i)); // add data from newDaten to filteredData where title contains search input - } - } + if (searchTextField.getText().hashCode() == hashA) { + XMLController.setColor("000000"); + colorPicker.setValue(new Color(0, 0, 0, 1)); + applyColor(); + } + }); - for (int i = 0; i < filterData.size(); i++) { - filmRoot.getChildren().add(new TreeItem(filterData.get(i))); // add filtered data to root node after search - } - if (searchTextField.getText().hashCode() == hashA) { - XMLController.setColor("000000"); - colorPicker.setValue(new Color(0, 0, 0, 1)); - applyColor(); - } - } + like.setOnAction(e -> { + dbController.like(getCurrentStreamUrl()); + filmsList.set(indexList, dbController.getDatabaseFilm(getCurrentStreamUrl())); + refreshTableElement(); }); - - like.setOnAction(new EventHandler() { - @Override - public void handle(ActionEvent event) { - dbController.like(getCurrentStreamUrl()); - filmsList.set(indexList, dbController.getDatabaseFilm(getCurrentStreamUrl())); - refreshTableElement(); - } + + dislike.setOnAction(e -> { + dbController.dislike(getCurrentStreamUrl()); + filmsList.set(indexList, dbController.getDatabaseFilm(getCurrentStreamUrl())); + refreshTableElement(); }); - - dislike.setOnAction(new EventHandler() { - @Override - public void handle(ActionEvent event) { - dbController.dislike(getCurrentStreamUrl()); - filmsList.set(indexList, dbController.getDatabaseFilm(getCurrentStreamUrl())); - refreshTableElement(); - } - }); - - /** - * FIXME fix bug when sort by ASCENDING, wrong order - */ - columnFavorite.sortTypeProperty().addListener(new ChangeListener() { - @Override - public void changed(ObservableValue paramObservableValue, SortType paramT1, SortType paramT2) { - filmRoot.getChildren().clear(); - filterData.clear(); - - if (paramT2.equals(SortType.DESCENDING)) { - // add favorites at the top - for (FilmTabelDataType film : filmsList) { - if (film.getFavorite()) { - filterData.add(0, film); - } else { - filterData.add(film); - } - } - } else { - // add favorites at the bottom - for (FilmTabelDataType film : filmsList) { - if (!film.getFavorite()) { - filterData.add(0, film); - } else { - filterData.add(film); - } - } - } - - addFilmsToTable(filterData); - } - }); - - // Change-listener for treeTableViewfilm - filmsTreeTable.getSelectionModel().selectedItemProperty().addListener(new ChangeListener() { - @Override - public void changed(ObservableValue observable, Object oldVal, Object newVal) { - if (filmsTreeTable.getSelectionModel().getSelectedItem() == null) { - return; - } - - currentTableFilm = filmsTreeTable.getSelectionModel().getSelectedItem().getValue(); // set the current film object - indexTable = filmsTreeTable.getSelectionModel().getSelectedIndex(); // get selected items table index + + // FIXME fix bug when sort by ASCENDING, wrong order + columnFavorite.sortTypeProperty().addListener((e, paramT1, paramT2) -> { + filmRoot.getChildren().clear(); + filterData.clear(); + + if (paramT2.equals(SortType.DESCENDING)) { + // add favorites at the top for (FilmTabelDataType film : filmsList) { - if (film.equals(currentTableFilm)) { - indexList = filmsList.indexOf(film); // get selected items list index + if (film.getFavorite()) { + filterData.add(0, film); + } else { + filterData.add(film); } } - - last = indexTable - 1; - next = indexTable + 1; - - - if ((currentTableFilm.getCached().isAfter(lastValidCache) )|| dbController.searchCacheByURL(getCurrentStreamUrl())) { - LOGGER.info("loading from cache: " + getCurrentTitle()); - setSelectedFilmInfo(dbController.readCache(getCurrentStreamUrl())); -// System.out.println("cache date is: " + currentTableFilm.getCached().toString()); - } else { -// System.out.println("either not cached or to old!"); -// System.out.println("cache date is: " + currentTableFilm.getCached().toString()); - // this is not perfect! - new Thread(new Runnable() { - public void run() { - Thread omdbAPIThread = new Thread(new OMDbAPIController(dbController, currentTableFilm, XMLController.getOmdbAPIKey())); - omdbAPIThread.setName("OMDbAPI"); - omdbAPIThread.start(); - - synchronized (omdbAPIThread) { - try { - omdbAPIThread.wait(); - } catch (InterruptedException e) { - LOGGER.error(e); - } - // update the GUI for the selected film - Platform.runLater(() -> { - setSelectedFilmInfo(dbController.readCache(getCurrentStreamUrl())); - }); - } - } - }).start(); + } else { + // add favorites at the bottom + for (FilmTabelDataType film : filmsList) { + if (!film.getFavorite()) { + filterData.add(0, film); + } else { + filterData.add(film); + } } } + + addFilmsToTable(filterData); }); - + + // Change-listener for treeTableViewfilm + filmsTreeTable.getSelectionModel().selectedItemProperty().addListener((e, paramT1, paramT2) -> { + if (filmsTreeTable.getSelectionModel().getSelectedItem() == null) { + return; + } + + currentTableFilm = filmsTreeTable.getSelectionModel().getSelectedItem().getValue(); // set the current film object + indexTable = filmsTreeTable.getSelectionModel().getSelectedIndex(); // get selected items table index + for (FilmTabelDataType film : filmsList) { + if (film.equals(currentTableFilm)) { + indexList = filmsList.indexOf(film); // get selected items list index + } + } + + last = indexTable - 1; + next = indexTable + 1; + + if ((currentTableFilm.getCached().isAfter(lastValidCache)) && dbController.searchCacheByURL(getCurrentStreamUrl())) { + LOGGER.info("loading from cache: " + getCurrentTitle()); + setSelectedFilmInfo(dbController.readCache(getCurrentStreamUrl())); + } else { + // this is not perfect! + new Thread(() -> { + Thread omdbAPIThread = new Thread(new OMDbAPIController(dbController, currentTableFilm, XMLController.getOmdbAPIKey())); + omdbAPIThread.setName("OMDbAPI"); + omdbAPIThread.start(); + + synchronized (omdbAPIThread) { + try { + omdbAPIThread.wait(); + } catch (InterruptedException e1) { + LOGGER.error(e1); + } + // update the GUI for the selected film + Platform.runLater(() -> { + setSelectedFilmInfo(dbController.readCache(getCurrentStreamUrl())); + }); + } + }).start(); + } + }); + // Poster-Mode actions } - + // Table-Mode fxml actions @FXML private void playbtnclicked() { @@ -469,7 +426,7 @@ public class MainWindowController { LOGGER.info("rootNode found, getting last watched episode"); currentTableFilm = dbController.getLastWatchedEpisode(currentTableFilm.getTitle()); } - + if (isSupportedFormat(currentTableFilm)) { new Player(getCurrentTableFilm()); } else { @@ -491,8 +448,7 @@ public class MainWindowController { } if (output.contains("which: no vlc") || output == "") { JFXInfoAlert vlcInfoAlert = new JFXInfoAlert("Info", - XMLController.getLocalBundle().getString("vlcNotInstalled"), - btnStyle, primaryStage); + XMLController.getLocalBundle().getString("vlcNotInstalled"), btnStyle, primaryStage); vlcInfoAlert.showAndWait(); } else { try { @@ -514,11 +470,11 @@ public class MainWindowController { } } } - + @FXML private void openfolderbtnclicked() { File dest = new File(getCurrentStreamUrl()).getParentFile(); - + if (!System.getProperty("os.name").contains("Linux")) { try { Desktop.getDesktop().open(dest); @@ -527,26 +483,26 @@ public class MainWindowController { } } } - + @FXML - private void returnBtnclicked(){ + private void returnBtnclicked() { filmsTreeTable.getSelectionModel().select(last); } - + @FXML - private void forwardBtnclicked(){ + private void forwardBtnclicked() { filmsTreeTable.getSelectionModel().select(next); } - + // general fxml actions @FXML private void aboutBtnAction() { - String bodyText = "Project HomeFlix \nVersion: " + version + " (Build: " + buildNumber + ") \"" - + versionName + "\" \n" + XMLController.getLocalBundle().getString("infoText"); + String bodyText = "Project HomeFlix \nVersion: " + version + " (Build: " + buildNumber + ") \"" + versionName + + "\" \n" + XMLController.getLocalBundle().getString("infoText"); JFXInfoAlert infoAlert = new JFXInfoAlert("Project HomeFlix", bodyText, btnStyle, primaryStage); infoAlert.showAndWait(); } - + @FXML private void settingsBtnclicked() { if (settingsScrollPane.isVisible()) { @@ -555,9 +511,9 @@ public class MainWindowController { settingsScrollPane.setVisible(true); } } - + @FXML - private void addDirectoryBtnAction(){ + private void addDirectoryBtnAction() { DirectoryChooser directoryChooser = new DirectoryChooser(); directoryChooser.setTitle(XMLController.getLocalBundle().getString("addDirectory")); File selectedFolder = directoryChooser.showDialog(primaryStage); @@ -567,9 +523,9 @@ public class MainWindowController { LOGGER.error("The selected folder dosen't exist!"); } } - + @FXML - private void addStreamSourceBtnAction(){ + private void addStreamSourceBtnAction() { FileChooser fileChooser = new FileChooser(); fileChooser.setTitle(XMLController.getLocalBundle().getString("addStreamSource")); File selectedFile = fileChooser.showOpenDialog(primaryStage); @@ -579,42 +535,42 @@ public class MainWindowController { LOGGER.error("The selected file dosen't exist!"); } } - + @FXML private void colorPickerAction() { XMLController.setColor(colorPicker.getValue().toString().substring(2, 10)); xmlController.saveSettings(); applyColor(); } - + @FXML private void updateBtnAction() { updateController = new UpdateController(this, buildNumber, XMLController.isUseBeta()); Thread updateThread = new Thread(updateController); updateThread.setName("Updater"); - updateThread.start(); + updateThread.start(); } - + @FXML private void autoUpdateToggleBtnAction() { XMLController.setAutoUpdate(!XMLController.isAutoUpdate()); xmlController.saveSettings(); } - + @FXML - private void autoplayToggleBtnAction(){ + private void autoplayToggleBtnAction() { XMLController.setAutoplay(!XMLController.isAutoplay()); xmlController.saveSettings(); } - + // refresh the selected child of the root node private void refreshTableElement() { filmRoot.getChildren().get(indexTable).setValue(filmsList.get(indexList)); } - + /** - * refresh all films in filmsList and in filmsTable - * clear the FilmsList and FilmRoot children, then update the database + * refresh all films in filmsList and in filmsTable clear the FilmsList and + * FilmRoot children, then update the database */ private void refreshAllFilms() { filmsList.clear(); @@ -623,54 +579,74 @@ public class MainWindowController { filmsList = dbController.getDatabaseFilmsList(); // returns a list of all films stored in the database addFilmsToTable(filmsList); } - + /** - * add data from films-list to films-table + * TODO rework! add data from a ObservableList to the films-table + * + * @param elementsList a list of elements you want to add */ public void addFilmsToTable(ObservableList elementsList) { for (FilmTabelDataType element : elementsList) { - + // only if the entry contains a season and a episode it's a valid series if (!element.getSeason().isEmpty() && !element.getEpisode().isEmpty()) { - // check if there is a series node to add the item - for (int i = 0; i < filmRoot.getChildren().size(); i++) { - if (filmRoot.getChildren().get(i).getValue().getTitle().equals(element.getTitle())) { - // if a root node exists, add element as child - TreeItem episodeNode = new TreeItem<>(new FilmTabelDataType( - element.getStreamUrl(), element.getTitle(), element.getSeason(), element.getEpisode(), - element.getFavorite(), element.getCached(), element.getImage())); - filmRoot.getChildren().get(i).getChildren().add(episodeNode); - } else if (filmRoot.getChildren().get(i).nextSibling() == null) { - // if no root node exists, create one and add element as child - TreeItem seriesRootNode = new TreeItem<>(new FilmTabelDataType( - element.getTitle() + "_rootNode", element.getTitle(), "", "", element.getFavorite(), - element.getCached() , element.getImage())); - filmRoot.getChildren().add(seriesRootNode); + // if there is any node check if it's the root node to the episode + // else there is no node at all so we must create a new + if (filmRoot.getChildren().size() > 0) { + for (int i = 0; i < filmRoot.getChildren().size(); i++) { + // if a root node exists, add element + // else create a new root node and add the element (if rootNode is the last + // node) (works since we edit the element!) + if (filmRoot.getChildren().get(i).getValue().getTitle().equals(element.getTitle())) { + TreeItem episodeNode = new TreeItem<>( + new FilmTabelDataType(element.getStreamUrl(), element.getTitle(), + element.getSeason(), element.getEpisode(), element.getFavorite(), + element.getCached(), element.getImage())); + filmRoot.getChildren().get(i).getChildren().add(episodeNode); + } else if (filmRoot.getChildren().get(i).nextSibling() == null) { + TreeItem seriesRootNode = new TreeItem<>( + new FilmTabelDataType(element.getTitle() + "_rootNode", element.getTitle(), "", "", + element.getFavorite(), element.getCached(), element.getImage())); + filmRoot.getChildren().add(seriesRootNode); + } } + } else { + // add new root node + TreeItem seriesRootNode = new TreeItem<>( + new FilmTabelDataType(element.getTitle() + "_rootNode", element.getTitle(), "", "", + element.getFavorite(), element.getCached(), element.getImage())); + filmRoot.getChildren().add(seriesRootNode); + + // add new element + TreeItem episodeNode = new TreeItem<>(new FilmTabelDataType( + element.getStreamUrl(), element.getTitle(), element.getSeason(), element.getEpisode(), + element.getFavorite(), element.getCached(), element.getImage())); + filmRoot.getChildren().get(0).getChildren().add(episodeNode); } + } else { // if season and episode are empty, we can assume the object is a film filmRoot.getChildren().add(new TreeItem(element)); } } } - + // add a all elements of sourcesList to the sources table on the settings pane public void addSourceToTable() { - for (SourceDataType source: sourcesList) { - sourceRoot.getChildren().add(new TreeItem(source)); // add data to root-node + for (SourceDataType source : sourcesList) { + sourceRoot.getChildren().add(new TreeItem(source)); // add data to root-node } } - + /** * add a source to the newsources list + * * @param path to the source * @param mode of the source (local or streaming) */ public void addSource(String path, String mode) { - JsonObject source = null; JsonArray newsources = null; try { @@ -683,7 +659,7 @@ public class MainWindowController { } // add new source - source = Json.object().add("path", path).add("mode", mode); + JsonObject source = Json.object().add("path", path).add("mode", mode); newsources.add(source); Writer writer = new FileWriter(XMLController.getDirHomeFlix() + "/sources.json"); newsources.writeTo(writer); @@ -691,20 +667,18 @@ public class MainWindowController { } catch (IOException e) { LOGGER.error(e); } - + // clear old sources list/table getSourcesList().clear(); sourceRoot.getChildren().clear(); - - // clear the FilmsList and FilmRoot children, then update the database - refreshAllFilms(); - + + refreshAllFilms(); // refresh the FilmsList checkAllPosters(); // check if there is anything to cache } - + /** - * set the color of the GUI-Elements - * if usedColor is less than checkColor set text fill white, else black + * set the color of the GUI-Elements if usedColor is less than checkColor set + * text fill white, else black */ private void applyColor() { String menuBtnStyle; @@ -712,32 +686,34 @@ public class MainWindowController { BigInteger checkColor = new BigInteger("78909cff", 16); if (usedColor.compareTo(checkColor) == -1) { - btnStyle = "-fx-button-type: RAISED; -fx-background-color: #" + XMLController.getColor() + "; -fx-text-fill: WHITE;"; + btnStyle = "-fx-button-type: RAISED; -fx-background-color: #" + XMLController.getColor() + + "; -fx-text-fill: WHITE;"; menuBtnStyle = "-fx-text-fill: WHITE;"; - + playbtn.setGraphic(new ImageView(new Image("icons/ic_play_arrow_white_18dp_1x.png"))); returnBtn.setGraphic(new ImageView(new Image("icons/ic_skip_previous_white_18dp_1x.png"))); forwardBtn.setGraphic(new ImageView(new Image("icons/ic_skip_next_white_18dp_1x.png"))); - + menuHam.getStyleClass().clear(); menuHam.getStyleClass().add("jfx-hamburgerW"); } else { - btnStyle = "-fx-button-type: RAISED; -fx-background-color: #" + XMLController.getColor() + "; -fx-text-fill: BLACK;"; + btnStyle = "-fx-button-type: RAISED; -fx-background-color: #" + XMLController.getColor() + + "; -fx-text-fill: BLACK;"; menuBtnStyle = "-fx-text-fill: BLACK;"; - + playbtn.setGraphic(new ImageView(new Image("icons/ic_play_arrow_black_18dp_1x.png"))); returnBtn.setGraphic(new ImageView(new Image("icons/ic_skip_previous_black_18dp_1x.png"))); forwardBtn.setGraphic(new ImageView(new Image("icons/ic_skip_next_black_18dp_1x.png"))); - + menuHam.getStyleClass().clear(); menuHam.getStyleClass().add("jfx-hamburgerB"); } - + // boxes and TextFields sideMenuVBox.setStyle("-fx-background-color: #" + XMLController.getColor() + ";"); topHBox.setStyle("-fx-background-color: #" + XMLController.getColor() + ";"); searchTextField.setFocusColor(Color.valueOf(XMLController.getColor())); - + // normal buttons addDirectoryBtn.setStyle(btnStyle); addStreamSourceBtn.setStyle(btnStyle); @@ -746,12 +722,12 @@ public class MainWindowController { openfolderbtn.setStyle(btnStyle); returnBtn.setStyle(btnStyle); forwardBtn.setStyle(btnStyle); - + // menu buttons settingsBtn.setStyle(menuBtnStyle); aboutBtn.setStyle(menuBtnStyle); } - + // slide in in 400ms private void sideMenuSlideIn() { sideMenuVBox.setVisible(true); @@ -760,7 +736,7 @@ public class MainWindowController { translateTransition.setToX(0); translateTransition.play(); } - + // slide out in 400ms private void sideMenuSlideOut() { TranslateTransition translateTransition = new TranslateTransition(Duration.millis(400), sideMenuVBox); @@ -768,7 +744,7 @@ public class MainWindowController { translateTransition.setToX(-150); translateTransition.play(); } - + /** * set the local based on the languageChoisBox selection */ @@ -807,11 +783,11 @@ public class MainWindowController { columnSeason.setText(XMLController.getLocalBundle().getString("columnSeason")); columnEpisode.setText(XMLController.getLocalBundle().getString("columnEpisode")); } - + private void setSelectedFilmInfo(String[] cacheData) { Font font = Font.font("System", FontWeight.BOLD, (int) Math.round(XMLController.getFontSize())); ObservableList textFlow = getTextFlow().getChildren(); - + // TODO this should move! *** Text[] nameText = new Text[20]; nameText[0] = new Text(XMLController.getLocalBundle().getString("title") + ": "); @@ -836,23 +812,23 @@ public class MainWindowController { nameText[19] = new Text(XMLController.getLocalBundle().getString("website") + ": "); // *** - // set the correct font for the nameText + // set the correct font for the nameText for (Text text : nameText) { text.setFont(font); } - + // clear the textFlow and add the new text textFlow.clear(); - + // TODO rework - for(int i = 0; i < 20; i++) { + for (int i = 0; i < 20; i++) { if (cacheData[i] != null && (i == 5 || i == 6) && cacheData[5].length() == 0) { // do nothing - } else if(cacheData[i] != null){ + } else if (cacheData[i] != null) { textFlow.addAll(nameText[i], new Text(cacheData[i] + "\n")); } } - + getTextFlow().setStyle("-fx-font-size : " + ((int) Math.round(XMLController.getFontSize()) + 1) + "px;"); // add the image @@ -867,7 +843,7 @@ public class MainWindowController { posterImageView.setImage(new Image(cacheData[20])); } } - + // if AutoUpdate, then check for updates private void checkAutoUpdate() { @@ -884,49 +860,50 @@ public class MainWindowController { } } } - + /** - * check if a film is supported by the HomeFlixPlayer or not - * this is the case if the mime type is mp4 + * check if a film is supported by the HomeFlixPlayer or not this is the case if + * the mime type is mp4 + * * @param entry the film you want to check * @return true if so, false if not */ private boolean isSupportedFormat(FilmTabelDataType film) { - String mimeType = URLConnection.guessContentTypeFromName(film.getStreamUrl()); - return mimeType != null && (mimeType.contains("mp4") || mimeType.contains("vp6")); + String mimeType = URLConnection.guessContentTypeFromName(film.getStreamUrl()); + return mimeType != null && (mimeType.contains("mp4") || mimeType.contains("vp6")); } - + /** * Poser Mode WIP */ - + private void posterModeStartup() { checkAllPosters(); } - + /** * check if all posters are cached, if not cache the missing ones */ private void checkAllPosters() { // get all not cached entries, none of them should have a cached poster ExecutorService executor = Executors.newFixedThreadPool(5); - + for (FilmTabelDataType entry : dbController.getAllNotCachedEntries()) { System.out.println(entry.getStreamUrl() + " is NOT cached!"); - + Runnable OMDbAPIWorker = new OMDbAPIController(dbController, entry, XMLController.getOmdbAPIKey()); executor.execute(OMDbAPIWorker); } executor.shutdown(); - + // TODO show loading screen - + // update all elements from the database refreshAllFilms(); } // getter and setter - + public FilmTabelDataType getCurrentTableFilm() { return currentTableFilm; } @@ -942,7 +919,7 @@ public class MainWindowController { public int getIndexTable() { return indexTable; } - + public int getIndexList() { return indexList; } From ea9e7405528a531f12b5c3304730717445f53b9b Mon Sep 17 00:00:00 2001 From: Seil0 Date: Mon, 6 May 2019 17:26:19 +0200 Subject: [PATCH 70/88] minor clean up --- .../application/MainWindowController.java | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index 0883075..c065d84 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -374,6 +374,7 @@ public class MainWindowController { }); // Change-listener for treeTableViewfilm + filmsTreeTable.getSelectionModel().selectedItemProperty().addListener((e, paramT1, paramT2) -> { if (filmsTreeTable.getSelectionModel().getSelectedItem() == null) { return; @@ -381,15 +382,15 @@ public class MainWindowController { currentTableFilm = filmsTreeTable.getSelectionModel().getSelectedItem().getValue(); // set the current film object indexTable = filmsTreeTable.getSelectionModel().getSelectedIndex(); // get selected items table index + last = indexTable - 1; + next = indexTable + 1; + for (FilmTabelDataType film : filmsList) { if (film.equals(currentTableFilm)) { indexList = filmsList.indexOf(film); // get selected items list index } } - last = indexTable - 1; - next = indexTable + 1; - if ((currentTableFilm.getCached().isAfter(lastValidCache)) && dbController.searchCacheByURL(getCurrentStreamUrl())) { LOGGER.info("loading from cache: " + getCurrentTitle()); setSelectedFilmInfo(dbController.readCache(getCurrentStreamUrl())); @@ -505,11 +506,7 @@ public class MainWindowController { @FXML private void settingsBtnclicked() { - if (settingsScrollPane.isVisible()) { - settingsScrollPane.setVisible(false); - } else { - settingsScrollPane.setVisible(true); - } + settingsScrollPane.setVisible(!settingsScrollPane.isVisible()); } @FXML @@ -686,8 +683,7 @@ public class MainWindowController { BigInteger checkColor = new BigInteger("78909cff", 16); if (usedColor.compareTo(checkColor) == -1) { - btnStyle = "-fx-button-type: RAISED; -fx-background-color: #" + XMLController.getColor() - + "; -fx-text-fill: WHITE;"; + btnStyle = "-fx-button-type: RAISED; -fx-background-color: #" + XMLController.getColor() + "; -fx-text-fill: WHITE;"; menuBtnStyle = "-fx-text-fill: WHITE;"; playbtn.setGraphic(new ImageView(new Image("icons/ic_play_arrow_white_18dp_1x.png"))); @@ -697,8 +693,7 @@ public class MainWindowController { menuHam.getStyleClass().clear(); menuHam.getStyleClass().add("jfx-hamburgerW"); } else { - btnStyle = "-fx-button-type: RAISED; -fx-background-color: #" + XMLController.getColor() - + "; -fx-text-fill: BLACK;"; + btnStyle = "-fx-button-type: RAISED; -fx-background-color: #" + XMLController.getColor() + "; -fx-text-fill: BLACK;"; menuBtnStyle = "-fx-text-fill: BLACK;"; playbtn.setGraphic(new ImageView(new Image("icons/ic_play_arrow_black_18dp_1x.png"))); @@ -819,12 +814,10 @@ public class MainWindowController { // clear the textFlow and add the new text textFlow.clear(); - - // TODO rework + for (int i = 0; i < 20; i++) { - if (cacheData[i] != null && (i == 5 || i == 6) && cacheData[5].length() == 0) { - // do nothing - } else if (cacheData[i] != null) { + // if the cacheData exists and they are not empty add the text + if(cacheData[i] != null && cacheData[i].length() > 0) { textFlow.addAll(nameText[i], new Text(cacheData[i] + "\n")); } } From 7dbe0d46d8d4e46b501da6afdcf1cffb75c2ad38 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Wed, 15 May 2019 17:14:15 +0200 Subject: [PATCH 71/88] some clean up and first bits for poster mode GUI logic --- pom.xml | 6 +- .../application/MainWindowController.java | 66 +++++++++++++-- .../HomeFlix/controller/DBController.java | 57 +++++++++++-- .../HomeFlix/datatypes/DatabaseDataType.java | 4 + .../HomeFlix/datatypes/PosterModeElement.java | 81 +++++++++++++++++++ 5 files changed, 198 insertions(+), 16 deletions(-) create mode 100644 src/main/java/kellerkinder/HomeFlix/datatypes/PosterModeElement.java diff --git a/pom.xml b/pom.xml index fe41879..d9f7a89 100644 --- a/pom.xml +++ b/pom.xml @@ -27,19 +27,19 @@ org.openjfx javafx-controls - 12 + 12.0.1 org.openjfx javafx-fxml - 12 + 12.0.1 org.openjfx javafx-media - 12 + 12.0.1 diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index c065d84..c527baa 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -91,6 +91,7 @@ import kellerkinder.HomeFlix.controller.OMDbAPIController; import kellerkinder.HomeFlix.controller.UpdateController; import kellerkinder.HomeFlix.controller.XMLController; import kellerkinder.HomeFlix.datatypes.FilmTabelDataType; +import kellerkinder.HomeFlix.datatypes.PosterModeElement; import kellerkinder.HomeFlix.datatypes.SourceDataType; import kellerkinder.HomeFlix.player.Player; @@ -157,8 +158,12 @@ public class MainWindowController { @FXML private JFXButton forwardBtn; // poster-mode - @FXML private AnchorPane posterModeAnchorPane; @FXML private ScrollPane posterModeScrollPane; + @FXML private AnchorPane posterModeAnchorPane; + + private int xPos = -200; + private int yPos = 17; + private int xNextElement = 0; private DBController dbController; private UpdateController updateController; @@ -184,6 +189,7 @@ public class MainWindowController { private ObservableList branches = FXCollections.observableArrayList("stable", "beta"); private ObservableList filterData = FXCollections.observableArrayList(); private ObservableList filmsList = FXCollections.observableArrayList(); + private ObservableList posterEmenents = FXCollections.observableArrayList(); private static ObservableList sourcesList = FXCollections.observableArrayList(); private MenuItem like = new MenuItem("like"); private MenuItem dislike = new MenuItem("dislike"); // TODO one option (like or dislike) @@ -335,13 +341,13 @@ public class MainWindowController { like.setOnAction(e -> { dbController.like(getCurrentStreamUrl()); - filmsList.set(indexList, dbController.getDatabaseFilm(getCurrentStreamUrl())); + filmsList.set(indexList, dbController.getStream(getCurrentStreamUrl())); refreshTableElement(); }); dislike.setOnAction(e -> { dbController.dislike(getCurrentStreamUrl()); - filmsList.set(indexList, dbController.getDatabaseFilm(getCurrentStreamUrl())); + filmsList.set(indexList, dbController.getStream(getCurrentStreamUrl())); refreshTableElement(); }); @@ -571,10 +577,14 @@ public class MainWindowController { */ private void refreshAllFilms() { filmsList.clear(); - filmRoot.getChildren().clear(); dbController.refreshDataBase(); // refreshes the database after a source path was added - filmsList = dbController.getDatabaseFilmsList(); // returns a list of all films stored in the database + filmsList = dbController.getStreamsList(); // returns a list of all films stored in the database + + // refresh filmRoot in filmsTreeTable + filmRoot.getChildren().clear(); addFilmsToTable(filmsList); + + // } /** @@ -872,6 +882,7 @@ public class MainWindowController { private void posterModeStartup() { checkAllPosters(); + addGUIElements(); } /** @@ -890,10 +901,55 @@ public class MainWindowController { executor.shutdown(); // TODO show loading screen +// executor.awaitTermination(1, TimeUnit.MINUTES); // we might need this as otherwise it would load before all tasks are finished // update all elements from the database refreshAllFilms(); + System.out.println("finished refresh"); } + + /** + * add all cached films/series to the PosterMode GUI + */ + private void addGUIElements() { + // refresh the posterModeElements list + posterEmenents.clear(); + posterEmenents = dbController.getPosterElementsList(); // returns a list of all PosterElements stored in the database + + posterModeAnchorPane.getChildren().clear(); // remove all GUIElements from the posterModeAnchorPane + + // reset the position + xPos = -200; + yPos = 17; + xNextElement = 0; + + // add all films/series as new GUIElements to the posterModeAnchorPane + for(PosterModeElement element : posterEmenents) { + generatePosition(); + element.setLayoutX(xPos); + element.setLayoutY(yPos); + posterModeAnchorPane.getChildren().add(element); + } + + } + + /** + * xMaxElements based on window width -36 + * calculates how many games can be displayed in one row + */ + private void generatePosition() { + int xMaxElements = (int) Math.floor((mainAnchorPane.getWidth() - 36) / 217); + + if(xNextElement >= xMaxElements){ +// oldXPosHelper = xNextElement; // only needed if window resizing is allowed + xPos = 17; + yPos = yPos + 345; + xNextElement = 1; + }else{ + xPos = xPos + 217; + xNextElement++; + } + } // getter and setter diff --git a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java index bba1c71..c5f8043 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java @@ -21,6 +21,7 @@ package kellerkinder.HomeFlix.controller; +import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.net.URLConnection; @@ -44,6 +45,7 @@ import javafx.scene.image.ImageView; import kellerkinder.HomeFlix.datatypes.DatabaseDataType; import kellerkinder.HomeFlix.datatypes.FilmTabelDataType; import kellerkinder.HomeFlix.datatypes.OMDbAPIResponseDataType; +import kellerkinder.HomeFlix.datatypes.PosterModeElement; public class DBController { @@ -149,11 +151,11 @@ public class DBController { } /** - * load the data from the database to a ObservableList + * load all streams from the database to a ObservableList, * order entries by title - * @return a ObservableList that contains all films from the database + * @return a ObservableList that contains all streams from the database */ - public ObservableList getDatabaseFilmsList() { + public ObservableList getStreamsList() { ObservableList filmsList = FXCollections.observableArrayList(); LOGGER.info("loading data from database ..."); try { @@ -177,11 +179,11 @@ public class DBController { } /** - * get one film from the database with streamUrl = ? - * @param streamUrl of the film - * @return a FilmTabelDataType Object of the film with the given streamUrl + * get one stream from the database with streamUrl = ? + * @param streamUrl of the stream + * @return a FilmTabelDataType Object of the stream with the given streamUrl */ - public FilmTabelDataType getDatabaseFilm(String streamUrl) { + public FilmTabelDataType getStream(String streamUrl) { FilmTabelDataType film = null; LOGGER.info("refresh data for " + streamUrl); try { @@ -204,6 +206,45 @@ public class DBController { return film; } + /** + * get all entries of the database as PosterModeElement + * @return a ObservableList of PosterModeElements + */ + public ObservableList getPosterElementsList() { + + ObservableList posterElementsList = FXCollections.observableArrayList(); + LOGGER.info("loading data from database ..."); + try { + //load local Data + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT * FROM films ORDER BY title"); + + while (rs.next()) { + // create the stream object + PosterModeElement posterElement = new PosterModeElement(); + posterElement.setStreamURL(rs.getString("streamUrl")); + + // get from the cache table + String[] cacheData = readCache(rs.getString("streamUrl")); + posterElement.getLabel().setText(cacheData[0]); + + if(cacheData[20] != null) { + posterElement.getImageView().setImage(new Image(new File(cacheData[20]).toURI().toString())); + } else { + posterElement.getImageView().setImage(new Image("/icons/Homeflix_Poster.png")); + } + + posterElementsList.add(posterElement); + } + stmt.close(); + rs.close(); + } catch (SQLException e) { + LOGGER.error("An error occured while getting all PosterElements!", e); + } + + return posterElementsList; + } + /** * refresh database to contain all (new added) entries */ @@ -217,7 +258,7 @@ public class DBController { loadSources(); // reload all sources loadDatabase(); // reload all films saved in the DB - LOGGER.info("filme in db: " + databaseStreams.size()); + LOGGER.info("There are {} entries in the Database", databaseStreams.size()); try { checkAddEntry(); diff --git a/src/main/java/kellerkinder/HomeFlix/datatypes/DatabaseDataType.java b/src/main/java/kellerkinder/HomeFlix/datatypes/DatabaseDataType.java index e22548c..f3fcd2f 100644 --- a/src/main/java/kellerkinder/HomeFlix/datatypes/DatabaseDataType.java +++ b/src/main/java/kellerkinder/HomeFlix/datatypes/DatabaseDataType.java @@ -34,6 +34,10 @@ public class DatabaseDataType { private Date cached; private double currentTime; + public DatabaseDataType() { + // constructor stub + } + public DatabaseDataType(String streamUrl, String title, String season, String episode, int favorite, Date cached, double currentTime) { this.streamUrl = streamUrl; this.title = title; diff --git a/src/main/java/kellerkinder/HomeFlix/datatypes/PosterModeElement.java b/src/main/java/kellerkinder/HomeFlix/datatypes/PosterModeElement.java new file mode 100644 index 0000000..101641b --- /dev/null +++ b/src/main/java/kellerkinder/HomeFlix/datatypes/PosterModeElement.java @@ -0,0 +1,81 @@ +/** + * Project-HomeFlix + * + * Copyright 2016-2019 <@Seil0> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + */ + +package kellerkinder.HomeFlix.datatypes; + +import com.jfoenix.controls.JFXButton; + +import javafx.scene.control.Label; +import javafx.scene.image.ImageView; +import javafx.scene.layout.VBox; + +public class PosterModeElement extends VBox{ + + private String streamURL; + private Label label = new Label(); + private JFXButton button = new JFXButton(); + private ImageView imageView = new ImageView(); + + public PosterModeElement() { + // constructor stub + } + + public PosterModeElement(String streamURL, Label label, JFXButton button, ImageView imageView) { + this.streamURL = streamURL; + this.label = label; + this.button = button; + this.imageView = imageView; + } + + public String getStreamURL() { + return streamURL; + } + + public Label getLabel() { + return label; + } + + public JFXButton getButton() { + return button; + } + + public ImageView getImageView() { + return imageView; + } + + public void setStreamURL(String streamURL) { + this.streamURL = streamURL; + } + + public void setLabel(Label label) { + this.label = label; + } + + public void setButton(JFXButton button) { + this.button = button; + } + + public void setImageView(ImageView imageView) { + this.imageView = imageView; + } + +} From 4ff8b7819fa49777e85b683d2a0984fb281b2678 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Sat, 15 Jun 2019 11:09:59 +0200 Subject: [PATCH 72/88] Use a FlowPane fot the PosterMode --- .../HomeFlix/application/Main.java | 2 +- .../application/MainWindowController.java | 42 ++----------------- .../HomeFlix/controller/DBController.java | 12 ++---- .../Alerts/JFX2BtnCancelAlert.java | 9 +--- .../org/kellerkinder/Alerts/JFXInfoAlert.java | 8 +--- src/main/resources/fxml/MainWindow.fxml | 11 ++--- 6 files changed, 18 insertions(+), 66 deletions(-) diff --git a/src/main/java/kellerkinder/HomeFlix/application/Main.java b/src/main/java/kellerkinder/HomeFlix/application/Main.java index cd3d0ed..881144b 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/Main.java +++ b/src/main/java/kellerkinder/HomeFlix/application/Main.java @@ -77,7 +77,7 @@ public class Main extends Application { primaryStage.setResizable(false); primaryStage.setTitle("Project HomeFlix"); primaryStage.getIcons().add(new Image(Main.class.getResourceAsStream("/icons/Homeflix_Icon_64x64.png"))); //adds application icon - primaryStage.setOnCloseRequest(event -> System.exit(1)); + primaryStage.setOnCloseRequest(event -> System.exit(0)); // generate window scene = new Scene(pane); // create new scene, append pane to scene diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index c527baa..fc0b6bd 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -75,6 +75,7 @@ import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.MouseEvent; import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; @@ -159,11 +160,7 @@ public class MainWindowController { // poster-mode @FXML private ScrollPane posterModeScrollPane; - @FXML private AnchorPane posterModeAnchorPane; - - private int xPos = -200; - private int yPos = 17; - private int xNextElement = 0; + @FXML private FlowPane posterModeFlowPane; private DBController dbController; private UpdateController updateController; @@ -916,40 +913,9 @@ public class MainWindowController { posterEmenents.clear(); posterEmenents = dbController.getPosterElementsList(); // returns a list of all PosterElements stored in the database - posterModeAnchorPane.getChildren().clear(); // remove all GUIElements from the posterModeAnchorPane - - // reset the position - xPos = -200; - yPos = 17; - xNextElement = 0; - - // add all films/series as new GUIElements to the posterModeAnchorPane - for(PosterModeElement element : posterEmenents) { - generatePosition(); - element.setLayoutX(xPos); - element.setLayoutY(yPos); - posterModeAnchorPane.getChildren().add(element); - } - + posterModeFlowPane.getChildren().clear(); // remove all GUIElements from the posterModeFlowPane + posterModeFlowPane.getChildren().addAll(posterEmenents); // add all films/series as new GUIElements to the posterModeFlowPane } - - /** - * xMaxElements based on window width -36 - * calculates how many games can be displayed in one row - */ - private void generatePosition() { - int xMaxElements = (int) Math.floor((mainAnchorPane.getWidth() - 36) / 217); - - if(xNextElement >= xMaxElements){ -// oldXPosHelper = xNextElement; // only needed if window resizing is allowed - xPos = 17; - yPos = yPos + 345; - xNextElement = 1; - }else{ - xPos = xPos + 217; - xNextElement++; - } - } // getter and setter diff --git a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java index c5f8043..737e6e8 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java @@ -60,8 +60,6 @@ public class DBController { /** * constructor for DBController - * @param main the Main object - * @param mainWindowController the MainWindowController object */ public DBController() { // Auto-generated constructor stub @@ -76,10 +74,8 @@ public class DBController { } /** - * initialize the {@link DBController} - * initialize the database connection - * check if there is a need to create a new database - * refresh the database + * initialize the {@link DBController} with the database connection check if + * there is a need to create a new database refresh the database */ public void init() { initDatabaseConnection(); @@ -116,6 +112,7 @@ public class DBController { + "streamUrl, Title, Year, Rated, Released, Season, Episode ,Runtime, Genre, Director, Writer," + " Actors, Plot, Language, Country, Awards, Poster, Metascore, imdbRating, imdbVotes," + " imdbID, Type, dvd, BoxOffice, Website, Response)"); + connection.commit(); stmt.close(); } catch (SQLException e) { LOGGER.error(e); @@ -151,8 +148,7 @@ public class DBController { } /** - * load all streams from the database to a ObservableList, - * order entries by title + * load all streams from the database to a ObservableList, order entries by title * @return a ObservableList that contains all streams from the database */ public ObservableList getStreamsList() { diff --git a/src/main/java/org/kellerkinder/Alerts/JFX2BtnCancelAlert.java b/src/main/java/org/kellerkinder/Alerts/JFX2BtnCancelAlert.java index 5f471a4..9fc68d6 100644 --- a/src/main/java/org/kellerkinder/Alerts/JFX2BtnCancelAlert.java +++ b/src/main/java/org/kellerkinder/Alerts/JFX2BtnCancelAlert.java @@ -89,13 +89,8 @@ public class JFX2BtnCancelAlert { JFXButton cancelBtn = new JFXButton(); cancelBtn.setText(cancelText); - cancelBtn.setOnAction(new EventHandler() { - @Override - public void handle(ActionEvent event) { - alert.close(); - System.exit(1); - } - }); + cancelBtn.addEventHandler(ActionEvent.ACTION, (e)-> alert.close()); + cancelBtn.addEventHandler(ActionEvent.ACTION, (e)-> System.exit(0)); cancelBtn.setButtonType(com.jfoenix.controls.JFXButton.ButtonType.RAISED); cancelBtn.setPrefHeight(32); cancelBtn.setStyle(btnStyle); diff --git a/src/main/java/org/kellerkinder/Alerts/JFXInfoAlert.java b/src/main/java/org/kellerkinder/Alerts/JFXInfoAlert.java index 5d09315..79d045b 100644 --- a/src/main/java/org/kellerkinder/Alerts/JFXInfoAlert.java +++ b/src/main/java/org/kellerkinder/Alerts/JFXInfoAlert.java @@ -26,7 +26,6 @@ import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXDialogLayout; import javafx.event.ActionEvent; -import javafx.event.EventHandler; import javafx.scene.text.Text; import javafx.stage.Stage; @@ -59,12 +58,7 @@ public class JFXInfoAlert { JFXAlert alert = new JFXAlert<>(stage); JFXButton button = new JFXButton("Okay"); - button.setOnAction(new EventHandler() { - @Override - public void handle(ActionEvent event) { - alert.close(); - } - }); + button.addEventHandler(ActionEvent.ACTION, (e)-> alert.close()); button.setButtonType(com.jfoenix.controls.JFXButton.ButtonType.RAISED); button.setPrefHeight(32); button.setStyle(btnStyle); diff --git a/src/main/resources/fxml/MainWindow.fxml b/src/main/resources/fxml/MainWindow.fxml index 9bd0731..c0fb05b 100644 --- a/src/main/resources/fxml/MainWindow.fxml +++ b/src/main/resources/fxml/MainWindow.fxml @@ -18,12 +18,13 @@ + - + @@ -65,13 +66,13 @@ - - + + - - + + From 4d503546ffbc7faf1b29b375abc549388c6a11cc Mon Sep 17 00:00:00 2001 From: Seil0 Date: Sat, 15 Jun 2019 12:09:31 +0200 Subject: [PATCH 73/88] bug fixes * fixed loading cache only after restart * fixed loading cache for rootNodes * fixed default image if api doesn't have a poster * updated JFoenix 9.0.8 -> 9.0.9 --- pom.xml | 2 +- .../application/MainWindowController.java | 17 ++++---- .../HomeFlix/controller/DBController.java | 41 +++++++++++++++---- .../controller/OMDbAPIController.java | 23 +++++++---- .../controller/SourcesController.java | 7 ++-- .../HomeFlix/datatypes/DatabaseDataType.java | 14 +------ .../HomeFlix/datatypes/FilmTabelDataType.java | 18 +------- 7 files changed, 62 insertions(+), 60 deletions(-) diff --git a/pom.xml b/pom.xml index d9f7a89..f69889e 100644 --- a/pom.xml +++ b/pom.xml @@ -51,7 +51,7 @@ com.jfoenix jfoenix - 9.0.8 + 9.0.9 diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index fc0b6bd..8943e12 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -146,7 +146,7 @@ public class MainWindowController { @FXML private TreeTableColumn columnFavorite; @FXML private TreeTableColumn columnSeason; @FXML private TreeTableColumn columnEpisode; - @FXML private TreeItem filmRoot = new TreeItem<>(new FilmTabelDataType("", "", "", "", false, null, null)); + @FXML private TreeItem filmRoot = new TreeItem<>(new FilmTabelDataType("", "", "", "", false, null)); @FXML private ScrollPane textScrollPane; @@ -180,7 +180,7 @@ public class MainWindowController { private int indexTable; private int indexList; private int next; - private FilmTabelDataType currentTableFilm = new FilmTabelDataType("", "", "", "", false, null, null); + private FilmTabelDataType currentTableFilm = new FilmTabelDataType("", "", "", "", false, null); private ObservableList languages = FXCollections.observableArrayList("English (en_US)", "Deutsch (de_DE)"); private ObservableList branches = FXCollections.observableArrayList("stable", "beta"); @@ -394,7 +394,8 @@ public class MainWindowController { } } - if ((currentTableFilm.getCached().isAfter(lastValidCache)) && dbController.searchCacheByURL(getCurrentStreamUrl())) { + if ((dbController.getCached(getCurrentStreamUrl()).isAfter(lastValidCache) || getCurrentStreamUrl().contains("_rootNode")) + && dbController.searchCacheByURL(getCurrentStreamUrl())) { LOGGER.info("loading from cache: " + getCurrentTitle()); setSelectedFilmInfo(dbController.readCache(getCurrentStreamUrl())); } else { @@ -607,12 +608,12 @@ public class MainWindowController { TreeItem episodeNode = new TreeItem<>( new FilmTabelDataType(element.getStreamUrl(), element.getTitle(), element.getSeason(), element.getEpisode(), element.getFavorite(), - element.getCached(), element.getImage())); + element.getImage())); filmRoot.getChildren().get(i).getChildren().add(episodeNode); } else if (filmRoot.getChildren().get(i).nextSibling() == null) { TreeItem seriesRootNode = new TreeItem<>( new FilmTabelDataType(element.getTitle() + "_rootNode", element.getTitle(), "", "", - element.getFavorite(), element.getCached(), element.getImage())); + element.getFavorite(), element.getImage())); filmRoot.getChildren().add(seriesRootNode); } } @@ -620,13 +621,13 @@ public class MainWindowController { // add new root node TreeItem seriesRootNode = new TreeItem<>( new FilmTabelDataType(element.getTitle() + "_rootNode", element.getTitle(), "", "", - element.getFavorite(), element.getCached(), element.getImage())); + element.getFavorite(), element.getImage())); filmRoot.getChildren().add(seriesRootNode); // add new element TreeItem episodeNode = new TreeItem<>(new FilmTabelDataType( element.getStreamUrl(), element.getTitle(), element.getSeason(), element.getEpisode(), - element.getFavorite(), element.getCached(), element.getImage())); + element.getFavorite(), element.getImage())); filmRoot.getChildren().get(0).getChildren().add(episodeNode); } @@ -836,7 +837,7 @@ public class MainWindowController { try { posterImageView.setImage(new Image(new File(cacheData[20]).toURI().toString())); } catch (Exception e) { - posterImageView.setImage(new Image(cacheData[20])); + posterImageView.setImage(new Image("icons/Homeflix_Poster.png")); LOGGER.error("No Poster found, useing default."); } } else { diff --git a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java index 737e6e8..4986208 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java @@ -32,6 +32,7 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; +import java.time.LocalDate; import java.util.ArrayList; import java.util.List; @@ -130,7 +131,7 @@ public class DBController { while (rs.next()) { databaseStreams.add(new DatabaseDataType(rs.getString("streamUrl"), rs.getString("title"), rs.getString("season"), rs.getString("episode"), - rs.getInt("favorite"), rs.getDate("cached"), rs.getDouble("currentTime"))); + rs.getInt("favorite"), rs.getDouble("currentTime"))); } stmt.close(); rs.close(); @@ -140,7 +141,7 @@ public class DBController { } /** - * load all sources + * load all source streams */ private void loadSources() { SourcesController sourcesController = new SourcesController(); @@ -163,7 +164,7 @@ public class DBController { ImageView imageView = rs.getBoolean("favorite") ? new ImageView(favorite_black) : new ImageView(favorite_border_black); filmsList.add(new FilmTabelDataType(rs.getString("streamUrl"), rs.getString("title"), rs.getString("season"), rs.getString("episode") ,rs.getBoolean("favorite"), - rs.getDate("cached").toLocalDate(), imageView)); + imageView)); } stmt.close(); rs.close(); @@ -191,7 +192,7 @@ public class DBController { ImageView imageView = rs.getBoolean("favorite") ? new ImageView(favorite_black) : new ImageView(favorite_border_black); film = new FilmTabelDataType(rs.getString("streamUrl"), rs.getString("title"), rs.getString("season"), rs.getString("episode"), rs.getBoolean("favorite"), - rs.getDate("cached").toLocalDate(), imageView); + imageView); } rs.close(); ps.close(); @@ -322,7 +323,7 @@ public class DBController { ps.setString(3, sourceStreamEntry.getSeason()); ps.setString(4, sourceStreamEntry.getEpisode()); ps.setInt(5, sourceStreamEntry.getFavorite()); - ps.setDate(6, sourceStreamEntry.getCached()); + ps.setDate(6, new Date(0)); ps.setDouble(7, sourceStreamEntry.getCurrentTime()); ps.addBatch(); // adds the entry LOGGER.info("Added \"" + sourceStreamEntry.getTitle() + "\" to database"); @@ -411,6 +412,30 @@ public class DBController { } } + /** + * get the cached date for the film + * @param streamUrl URL of the film + */ + public LocalDate getCached(String streamUrl) { + LocalDate cacheDate = LocalDate.now().minusDays(31); + try { + PreparedStatement ps = connection.prepareStatement("SELECT cached FROM films WHERE streamUrl = ?"); + ps.setString(1, streamUrl); + ResultSet rs = ps.executeQuery(); + + while (rs.next()) { + cacheDate = rs.getDate("cached").toLocalDate(); + } + + rs.close(); + ps.close(); + } catch (SQLException e) { + LOGGER.error("Ups! an error occured!", e); + } + + return cacheDate; + } + /** * add the received data to the cache table * @param streamUrl URL of the film @@ -537,7 +562,7 @@ public class DBController { while (rs.next()) { notCachedEntries.add(new FilmTabelDataType(rs.getString("streamUrl"), rs.getString("title"), rs.getString("season"), rs.getString("episode"), rs.getBoolean("favorite"), - rs.getDate("cached").toLocalDate(), new ImageView(favorite_border_black))); + new ImageView(favorite_border_black))); } stmt.close(); rs.close(); @@ -622,7 +647,7 @@ public class DBController { // at this point we have found the correct episode nextFilm = new FilmTabelDataType(rs.getString("streamUrl"), rs.getString("title"), rs.getString("season"), rs.getString("episode"), rs.getBoolean("favorite"), - rs.getDate("cached").toLocalDate(), new ImageView()); + new ImageView()); rs.close(); ps.close(); @@ -648,7 +673,7 @@ public class DBController { while (rs.next()) { nextFilm = new FilmTabelDataType(rs.getString("streamUrl"), rs.getString("title"), rs.getString("season"), rs.getString("episode"), rs.getBoolean("favorite"), - rs.getDate("cached").toLocalDate(), new ImageView()); + new ImageView()); // get the first episode where currentTime != 0 if (rs.getDouble("currentTime") > lastCurrentTime) { break; diff --git a/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java b/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java index f598446..ab7c63a 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java @@ -130,18 +130,23 @@ public class OMDbAPIController implements Runnable { omdbResponse.setWebsite(object.getString("Website", "")); omdbResponse.setResponse(object.getString("Response", "")); - // resize the image to fit in the posterImageView and add it to the cache - try { - BufferedImage originalImage = ImageIO.read(new URL(object.getString("Poster", ""))); - // change path to where file is located - omdbResponse.setPoster(XMLController.getPosterCache() + "/" + omdbResponse.getTitle() + ".png"); - ImageIO.write(originalImage, "png", new File(omdbResponse.getPoster())); - LOGGER.info("adding poster to cache: " + omdbResponse.getPoster()); - } catch (Exception e) { - LOGGER.error(e); + // if a poster exist try resizing it to fit in the posterImageView and add it to the cache, else use the default + if (omdbResponse.getPoster() != null) { + try { + BufferedImage originalImage = ImageIO.read(new URL(object.getString("Poster", ""))); + // change path to where file is located + omdbResponse.setPoster(XMLController.getPosterCache() + "/" + omdbResponse.getTitle() + ".png"); + ImageIO.write(originalImage, "png", new File(omdbResponse.getPoster())); + LOGGER.info("adding poster to cache: " + omdbResponse.getPoster()); + } catch (Exception e) { + LOGGER.error(e); + } + } else { + omdbResponse.setPoster("icons/Homeflix_Poster.png"); } + synchronized (this) { // adding to cache dbController.addCache(currentTableFilm.getStreamUrl(), omdbResponse); diff --git a/src/main/java/kellerkinder/HomeFlix/controller/SourcesController.java b/src/main/java/kellerkinder/HomeFlix/controller/SourcesController.java index ef07925..f4eade9 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/SourcesController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/SourcesController.java @@ -26,7 +26,6 @@ import java.io.File; import java.io.FileReader; import java.io.IOException; import java.nio.file.Files; -import java.sql.Date; import java.util.ArrayList; import java.util.List; @@ -89,7 +88,7 @@ public class SourcesController { for (File file : new File(path).listFiles()) { // if it's valid file add it to the sourceStreams if (isValidFile(file)) { - sourceStreams.add(new DatabaseDataType(file.getPath(), cutOffEnd(file.getName()), "", "", 0, new Date(0), 0.0)); + sourceStreams.add(new DatabaseDataType(file.getPath(), cutOffEnd(file.getName()), "", "", 0, 0.0)); } else if(file.isDirectory()) { // get all directories (series), root and season must be directories int sn = 1; @@ -100,7 +99,7 @@ public class SourcesController { // check whether the episode is a valid file if (isValidFile(episode)) { sourceStreams.add(new DatabaseDataType(episode.getPath(), cutOffEnd(file.getName()), - Integer.toString(sn), Integer.toString(ep), 0, new Date(0), 0.0)); + Integer.toString(sn), Integer.toString(ep), 0, 0.0)); ep++; } } @@ -122,7 +121,7 @@ public class SourcesController { sourceStreams.add(new DatabaseDataType(item.asObject().getString("streamUrl", ""), item.asObject().getString("title", ""), item.asObject().getString("season", ""), - item.asObject().getString("episode", ""), 0, new Date(0), 0.0)); + item.asObject().getString("episode", ""), 0, 0.0)); } LOGGER.info("added " + items.size() +" stream entries from: " + path); } catch (IOException e) { diff --git a/src/main/java/kellerkinder/HomeFlix/datatypes/DatabaseDataType.java b/src/main/java/kellerkinder/HomeFlix/datatypes/DatabaseDataType.java index f3fcd2f..cabd80e 100644 --- a/src/main/java/kellerkinder/HomeFlix/datatypes/DatabaseDataType.java +++ b/src/main/java/kellerkinder/HomeFlix/datatypes/DatabaseDataType.java @@ -22,8 +22,6 @@ package kellerkinder.HomeFlix.datatypes; -import java.sql.Date; - public class DatabaseDataType { private String streamUrl; @@ -31,20 +29,18 @@ public class DatabaseDataType { private String season; private String episode; private int favorite; - private Date cached; private double currentTime; public DatabaseDataType() { // constructor stub } - public DatabaseDataType(String streamUrl, String title, String season, String episode, int favorite, Date cached, double currentTime) { + public DatabaseDataType(String streamUrl, String title, String season, String episode, int favorite, double currentTime) { this.streamUrl = streamUrl; this.title = title; this.season = season; this.episode = episode; this.favorite = favorite; - this.cached = cached; this.currentTime = currentTime; } @@ -88,14 +84,6 @@ public class DatabaseDataType { this.favorite = favorite; } - public Date getCached() { - return cached; - } - - public void setCached(Date cached) { - this.cached = cached; - } - public double getCurrentTime() { return currentTime; } diff --git a/src/main/java/kellerkinder/HomeFlix/datatypes/FilmTabelDataType.java b/src/main/java/kellerkinder/HomeFlix/datatypes/FilmTabelDataType.java index 0d7301c..d774532 100644 --- a/src/main/java/kellerkinder/HomeFlix/datatypes/FilmTabelDataType.java +++ b/src/main/java/kellerkinder/HomeFlix/datatypes/FilmTabelDataType.java @@ -20,8 +20,6 @@ */ package kellerkinder.HomeFlix.datatypes; -import java.time.LocalDate; - import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; @@ -35,7 +33,6 @@ public class FilmTabelDataType { private final StringProperty season = new SimpleStringProperty(); private final StringProperty episode = new SimpleStringProperty(); private final BooleanProperty favorite = new SimpleBooleanProperty(); - private final SimpleObjectProperty cached = new SimpleObjectProperty<>(); private final SimpleObjectProperty image = new SimpleObjectProperty<>(); /** @@ -49,13 +46,12 @@ public class FilmTabelDataType { * @param image favorite icon */ public FilmTabelDataType(final String streamUrl, final String title, final String season, final String episode, - final boolean favorite, final LocalDate cached, final ImageView image) { + final boolean favorite, final ImageView image) { this.streamUrl.set(streamUrl); this.title.set(title); this.season.set(season); this.episode.set(episode); this.favorite.set(favorite); - this.cached.set(cached); this.image.set(image); } @@ -79,10 +75,6 @@ public class FilmTabelDataType { return favorite; } - public SimpleObjectProperty cachedProperty(){ - return cached; - } - public SimpleObjectProperty imageProperty(){ return image; } @@ -108,10 +100,6 @@ public class FilmTabelDataType { return favoriteProperty().get(); } - public final LocalDate getCached(){ - return cachedProperty().get(); - } - public final ImageView getImage() { return imageProperty().get(); } @@ -137,10 +125,6 @@ public class FilmTabelDataType { favoriteProperty().set(favorite); } - public final void setCached(LocalDate cached){ - cachedProperty().set(cached); - } - public final void setImage(ImageView image) { imageProperty().set(image); } From bb8bcd460afb2d75042acf76ee2eb0e51d7237d2 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Sat, 15 Jun 2019 18:44:35 +0200 Subject: [PATCH 74/88] PosterMode now has posters, UI optimizations * the settings page is now re-sizable * the postermode now shows actual posters * the series root- folder is now treated as root node --- .../HomeFlix/application/Main.java | 4 +- .../application/MainWindowController.java | 15 ++- .../HomeFlix/controller/DBController.java | 22 ++--- .../controller/OMDbAPIController.java | 50 +++++----- .../controller/SourcesController.java | 22 ++++- .../HomeFlix/datatypes/PosterModeElement.java | 36 ++++++- src/main/resources/css/MainWindow.css | 99 +++++++++++++++++-- src/main/resources/fxml/MainWindow.fxml | 20 ++-- 8 files changed, 200 insertions(+), 68 deletions(-) diff --git a/src/main/java/kellerkinder/HomeFlix/application/Main.java b/src/main/java/kellerkinder/HomeFlix/application/Main.java index 881144b..2d580c6 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/Main.java +++ b/src/main/java/kellerkinder/HomeFlix/application/Main.java @@ -73,8 +73,8 @@ public class Main extends Application { loader.setLocation(getClass().getResource("/fxml/MainWindow.fxml")); pane = (AnchorPane) loader.load(); primaryStage.setMinHeight(600.00); - primaryStage.setMinWidth(1000.00); - primaryStage.setResizable(false); + primaryStage.setMinWidth(1130.00); + //primaryStage.setResizable(false); primaryStage.setTitle("Project HomeFlix"); primaryStage.getIcons().add(new Image(Main.class.getResourceAsStream("/icons/Homeflix_Icon_64x64.png"))); //adds application icon primaryStage.setOnCloseRequest(event -> System.exit(0)); diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index 8943e12..a615e15 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -37,6 +37,7 @@ import java.util.Locale; import java.util.ResourceBundle; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; @@ -71,6 +72,7 @@ import javafx.scene.control.TreeItem; import javafx.scene.control.TreeTableColumn; import javafx.scene.control.TreeTableColumn.SortType; import javafx.scene.control.TreeTableView; +import javafx.scene.control.ScrollPane.ScrollBarPolicy; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.MouseEvent; @@ -225,6 +227,10 @@ public class MainWindowController { // Initialize general UI elements private void initUI() { + //JFXScrollPane.smoothScrolling(posterModeScrollPane); + posterModeScrollPane.setVbarPolicy(ScrollBarPolicy.ALWAYS); + settingsScrollPane.setVbarPolicy(ScrollBarPolicy.ALWAYS); + versionLbl.setText("Version: " + version + " (Build: " + buildNumber + ")"); fontsizeSlider.setValue(XMLController.getFontSize()); colorPicker.setValue(Color.valueOf(XMLController.getColor())); @@ -899,7 +905,12 @@ public class MainWindowController { executor.shutdown(); // TODO show loading screen -// executor.awaitTermination(1, TimeUnit.MINUTES); // we might need this as otherwise it would load before all tasks are finished + // we might need this as otherwise it would load before all tasks are finished + try { + executor.awaitTermination(1, TimeUnit.MINUTES); + } catch (InterruptedException e) { + LOGGER.error(e); + } // update all elements from the database refreshAllFilms(); @@ -916,6 +927,8 @@ public class MainWindowController { posterModeFlowPane.getChildren().clear(); // remove all GUIElements from the posterModeFlowPane posterModeFlowPane.getChildren().addAll(posterEmenents); // add all films/series as new GUIElements to the posterModeFlowPane + + System.out.println("added gui elements"); } // getter and setter diff --git a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java index 4986208..e00d869 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java @@ -204,7 +204,7 @@ public class DBController { } /** - * get all entries of the database as PosterModeElement + * get entries which have no season and episode eg. root or movie of the database as PosterModeElement * @return a ObservableList of PosterModeElements */ public ObservableList getPosterElementsList() { @@ -214,24 +214,20 @@ public class DBController { try { //load local Data Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT * FROM films ORDER BY title"); + ResultSet rs = stmt.executeQuery("SELECT * FROM films WHERE season = '' OR season = '0' ORDER BY title"); while (rs.next()) { - // create the stream object - PosterModeElement posterElement = new PosterModeElement(); - posterElement.setStreamURL(rs.getString("streamUrl")); + String[] cacheData = readCache(rs.getString("streamUrl")); // get from the cache table - // get from the cache table - String[] cacheData = readCache(rs.getString("streamUrl")); - posterElement.getLabel().setText(cacheData[0]); + System.out.println(rs.getString("streamUrl")); + System.out.println(":" + cacheData[20] + ":"); - if(cacheData[20] != null) { - posterElement.getImageView().setImage(new Image(new File(cacheData[20]).toURI().toString())); + if(cacheData[20] != null && cacheData[20].length() > 0) { + posterElementsList.add(new PosterModeElement(rs.getString("streamUrl"), cacheData[0], new Image(new File(cacheData[20]).toURI().toString()))); } else { - posterElement.getImageView().setImage(new Image("/icons/Homeflix_Poster.png")); + System.out.println("adding default"); + posterElementsList.add(new PosterModeElement(rs.getString("streamUrl"), cacheData[0], new Image("icons/Homeflix_Poster.png"))); } - - posterElementsList.add(posterElement); } stmt.close(); rs.close(); diff --git a/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java b/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java index ab7c63a..a746558 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java @@ -46,7 +46,6 @@ public class OMDbAPIController implements Runnable { private FilmTabelDataType currentTableFilm; private String omdbAPIKey; private String URL = "https://www.omdbapi.com/?apikey="; - private boolean useEpisode = true; private static final Logger LOGGER = LogManager.getLogger(OMDbAPIController.class.getName()); /** @@ -66,7 +65,13 @@ public class OMDbAPIController implements Runnable { public void run() { LOGGER.info("Querying omdbAPI ..."); JsonObject object; - object = getByTitle(currentTableFilm.getTitle()); + + if (currentTableFilm.getSeason() != null && Integer.parseInt(currentTableFilm.getSeason() + 0) > 0) { + object = getByTitle(currentTableFilm.getTitle(), true); + } else { + object = getByTitle(currentTableFilm.getTitle(), false); + } + if (object == null) { LOGGER.error("Fatal error while querying omdbAPI!"); return; @@ -76,13 +81,11 @@ public class OMDbAPIController implements Runnable { if (object.getString("Error", "").contains("not found!")) { String title = searchByTitle(currentTableFilm.getTitle()); if (title.length() > 0) { - // we have at least on answer, get info by title now - object = getByTitle(title); - - // if we still have nothing found, get info by title without episode - if(object.getString("Error", "").contains("Series or episode not found!")) { - useEpisode = false; - object = getByTitle(title); + // we have at least one answer, get info by search title now + if (currentTableFilm.getSeason() != null && Integer.parseInt(currentTableFilm.getSeason() + 0) > 0) { + object = getByTitle(title, true); + } else { + object = getByTitle(title, false); } } else { // add default poster and cache @@ -91,7 +94,6 @@ public class OMDbAPIController implements Runnable { omdbResponse.setTitle(currentTableFilm.getTitle()); omdbResponse.setSeason(currentTableFilm.getSeason()); omdbResponse.setEpisode(currentTableFilm.getEpisode()); - omdbResponse.setPoster("icons/Homeflix_Poster.png"); synchronized (this) { // adding to cache @@ -131,22 +133,16 @@ public class OMDbAPIController implements Runnable { omdbResponse.setResponse(object.getString("Response", "")); // if a poster exist try resizing it to fit in the posterImageView and add it to the cache, else use the default - if (omdbResponse.getPoster() != null) { - try { - BufferedImage originalImage = ImageIO.read(new URL(object.getString("Poster", ""))); - // change path to where file is located - omdbResponse.setPoster(XMLController.getPosterCache() + "/" + omdbResponse.getTitle() + ".png"); - ImageIO.write(originalImage, "png", new File(omdbResponse.getPoster())); - LOGGER.info("adding poster to cache: " + omdbResponse.getPoster()); - } catch (Exception e) { - LOGGER.error(e); - } - } else { - omdbResponse.setPoster("icons/Homeflix_Poster.png"); + try { + BufferedImage originalImage = ImageIO.read(new URL(object.getString("Poster", ""))); + // change path to where file is located + omdbResponse.setPoster(XMLController.getPosterCache() + "/" + omdbResponse.getTitle() + ".png"); + ImageIO.write(originalImage, "png", new File(omdbResponse.getPoster())); + LOGGER.info("adding poster to cache: " + omdbResponse.getPoster()); + } catch (Exception e) { + LOGGER.warn("could not load poster, seting null -> using default"); } - - synchronized (this) { // adding to cache dbController.addCache(currentTableFilm.getStreamUrl(), omdbResponse); @@ -161,11 +157,11 @@ public class OMDbAPIController implements Runnable { * @param title of the movie/series * @return a jsonObject of the API answer */ - private JsonObject getByTitle(String title) { + private JsonObject getByTitle(String title, boolean useEpisode) { String output = null; URL apiUrl; - try { - if (currentTableFilm.getSeason().length() > 0 && useEpisode) { + try { + if (useEpisode) { apiUrl = new URL(URL + omdbAPIKey + "&t=" + title.replace(" ", "%20") + "&Season=" + currentTableFilm.getSeason() diff --git a/src/main/java/kellerkinder/HomeFlix/controller/SourcesController.java b/src/main/java/kellerkinder/HomeFlix/controller/SourcesController.java index f4eade9..7e5c213 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/SourcesController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/SourcesController.java @@ -89,8 +89,9 @@ public class SourcesController { // if it's valid file add it to the sourceStreams if (isValidFile(file)) { sourceStreams.add(new DatabaseDataType(file.getPath(), cutOffEnd(file.getName()), "", "", 0, 0.0)); - } else if(file.isDirectory()) { + } else if(isValidSeriesRoot(file)) { // get all directories (series), root and season must be directories + sourceStreams.add(new DatabaseDataType(file.getPath(), file.getName(), "0", "0", 0, 0.0)); // add the series root node int sn = 1; for (File season : file.listFiles()) { if (season.isDirectory()) { @@ -150,6 +151,25 @@ public class SourcesController { return false; } + + /** + * + * @param file the root file you want to check + * @return true if it's a valid series root, else false + */ + private boolean isValidSeriesRoot(File file) { + if(file.isDirectory()) { + for (File season : file.listFiles()) { + if (season.isDirectory()) { + return true; + } else { + return false; // the root directory not only folders + } + } + } + + return false; + } // removes the ending private String cutOffEnd(String str) { diff --git a/src/main/java/kellerkinder/HomeFlix/datatypes/PosterModeElement.java b/src/main/java/kellerkinder/HomeFlix/datatypes/PosterModeElement.java index 101641b..621d350 100644 --- a/src/main/java/kellerkinder/HomeFlix/datatypes/PosterModeElement.java +++ b/src/main/java/kellerkinder/HomeFlix/datatypes/PosterModeElement.java @@ -24,31 +24,53 @@ package kellerkinder.HomeFlix.datatypes; import com.jfoenix.controls.JFXButton; +import javafx.geometry.Insets; import javafx.scene.control.Label; +import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.VBox; +import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; public class PosterModeElement extends VBox{ private String streamURL; + private String title; private Label label = new Label(); private JFXButton button = new JFXButton(); private ImageView imageView = new ImageView(); public PosterModeElement() { - // constructor stub + super.getChildren().addAll(label, button); + + label.setMaxWidth(200); + label.setPadding(new Insets(0,0,0,8)); + label.setFont(Font.font("System", FontWeight.BOLD, 14)); + + imageView.setFitHeight(300); + imageView.setFitWidth(200); + + button.setStyle("-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.8), 10, 0, 0, 3); "); + button.setGraphic(imageView); } - public PosterModeElement(String streamURL, Label label, JFXButton button, ImageView imageView) { + public PosterModeElement(String streamURL, String title, Image poster) { + this(); + this.streamURL = streamURL; - this.label = label; - this.button = button; - this.imageView = imageView; + this.title = title; + + label.setText(title); + imageView.setImage(poster); } public String getStreamURL() { return streamURL; } + + public String getTitle() { + return title; + } public Label getLabel() { return label; @@ -65,6 +87,10 @@ public class PosterModeElement extends VBox{ public void setStreamURL(String streamURL) { this.streamURL = streamURL; } + + public void setTitle(String title) { + this.title = title; + } public void setLabel(Label label) { this.label = label; diff --git a/src/main/resources/css/MainWindow.css b/src/main/resources/css/MainWindow.css index 92eccee..26b7275 100644 --- a/src/main/resources/css/MainWindow.css +++ b/src/main/resources/css/MainWindow.css @@ -1,6 +1,8 @@ -/* - * HAMBURGER CSS - */ +/******************************************************************************* + * * + * Hamburger Menu * + * * + ******************************************************************************/ .jfx-hamburgerW StackPane { -fx-background-color: white; @@ -12,9 +14,12 @@ -fx-background-radius: 5px; } -/* - * TREE TABLE CSS - */ + +/******************************************************************************* + * * + * TreeTable * + * * + ******************************************************************************/ .tree-table-view { -fx-tree-table-color: rgba(0, 168, 204, 0.2); @@ -84,9 +89,11 @@ -fx-padding: 1; /* 0.083333em; */ } -/* - * ChoiceBox - */ +/******************************************************************************* + * * + * ChoiceBox * + * * + ******************************************************************************/ .choice-box { -fx-background-color: transparent; @@ -106,4 +113,78 @@ .menu-item:focused { -fx-background-color: #EE3523; +} + +/******************************************************************************* + * * + * ScrollBar * + * * + ******************************************************************************/ + +.scroll-bar:vertical > .track-background, .scroll-bar:horizontal > .track-background { + -fx-background-color: #F1F1F1; + -fx-background-insets: 0.0; +} + +.scroll-bar:vertical > .thumb, .scroll-bar:horizontal > .thumb { + -fx-background-color: #BCBCBC; + -fx-background-insets: 0.0; + -fx-background-radius: 1.0; +} + +/* Up- and Down-Button Padding */ +.scroll-bar:vertical > .increment-button, .scroll-bar:vertical > .decrement-button { + -fx-padding: 5 2 5 2; +} + +/* Left- and Right-Button Padding */ +.scroll-bar:horizontal > .increment-button, .scroll-bar:horizontal > .decrement-button { + -fx-padding: 2 5 2 5; +} + +.scroll-bar > .increment-button, .scroll-bar > .decrement-button, .scroll-bar:hover > .increment-button, .scroll-bar:hover > .decrement-button { + -fx-background-color: transparent; +} + +.scroll-bar > .increment-button > .increment-arrow, .scroll-bar > .decrement-button > .decrement-arrow { + -fx-background-color: rgb(150.0, 150.0, 150.0); +} + +/* Up Arrow */ +.scroll-bar:vertical > .increment-button > .increment-arrow { + -fx-shape: "M298 426h428l-214 214z"; +} + +/* Down Arrow */ +.scroll-bar:vertical > .decrement-button > .decrement-arrow { + -fx-shape: "M298 598l214-214 214 214h-428z"; +} + +/* Right Arrow */ +.scroll-bar:horizontal > .increment-button > .increment-arrow { + -fx-shape: "M0 428l0 -428l214 214l-214 214z"; +} + +/* Left Arrow */ +.scroll-bar:horizontal > .decrement-button > .decrement-arrow { + -fx-shape: "M214 0l0 428l-214 -214l214 -214z"; +} + +/******************************************************************************* + * * + * ScrollPane * + * * + ******************************************************************************/ + +.scroll-pane { + -fx-background-insets: 0; + -fx-padding: 0; +} + +.scroll-pane:focused { + -fx-background-insets: 0; +} + +.scroll-pane .corner { + -fx-background-insets: 0; } \ No newline at end of file diff --git a/src/main/resources/fxml/MainWindow.fxml b/src/main/resources/fxml/MainWindow.fxml index c0fb05b..d8283f1 100644 --- a/src/main/resources/fxml/MainWindow.fxml +++ b/src/main/resources/fxml/MainWindow.fxml @@ -24,7 +24,7 @@ - + @@ -36,10 +36,10 @@ - - - - + + + + @@ -65,18 +65,18 @@ - + - + - + - + @@ -166,7 +166,7 @@ - + From f23e6c77ea89fd226bf6988d4d5a8ae45a46b496 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Sun, 16 Jun 2019 13:00:29 +0200 Subject: [PATCH 75/88] added a sample nested controller --- .../HomeFlix/application/FilmDetailView.java | 18 ++++++++++++++++++ .../HomeFlix/application/Main.java | 1 + .../application/MainWindowController.java | 4 ++++ src/main/resources/fxml/FilmDetailView.fxml | 10 ++++++++++ src/main/resources/fxml/MainWindow.fxml | 3 +++ 5 files changed, 36 insertions(+) create mode 100644 src/main/java/kellerkinder/HomeFlix/application/FilmDetailView.java create mode 100644 src/main/resources/fxml/FilmDetailView.fxml diff --git a/src/main/java/kellerkinder/HomeFlix/application/FilmDetailView.java b/src/main/java/kellerkinder/HomeFlix/application/FilmDetailView.java new file mode 100644 index 0000000..c5da73e --- /dev/null +++ b/src/main/java/kellerkinder/HomeFlix/application/FilmDetailView.java @@ -0,0 +1,18 @@ +package kellerkinder.HomeFlix.application; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; + +public class FilmDetailView { + + @FXML private Label lblFilm; + + public void initialize() { + System.out.println("init nested controller"); + } + + public void foo() { + System.out.println("test"); + } + +} diff --git a/src/main/java/kellerkinder/HomeFlix/application/Main.java b/src/main/java/kellerkinder/HomeFlix/application/Main.java index 2d580c6..26e78b5 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/Main.java +++ b/src/main/java/kellerkinder/HomeFlix/application/Main.java @@ -34,6 +34,7 @@ import javafx.application.Application; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.image.Image; import javafx.scene.layout.AnchorPane; diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index a615e15..9481d9c 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -163,6 +163,9 @@ public class MainWindowController { // poster-mode @FXML private ScrollPane posterModeScrollPane; @FXML private FlowPane posterModeFlowPane; + + // FilmDetailView + @FXML private FilmDetailView filmDetailViewController; private DBController dbController; private UpdateController updateController; @@ -498,6 +501,7 @@ public class MainWindowController { @FXML private void returnBtnclicked() { filmsTreeTable.getSelectionModel().select(last); + filmDetailViewController.foo(); } @FXML diff --git a/src/main/resources/fxml/FilmDetailView.fxml b/src/main/resources/fxml/FilmDetailView.fxml new file mode 100644 index 0000000..94bc190 --- /dev/null +++ b/src/main/resources/fxml/FilmDetailView.fxml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/src/main/resources/fxml/MainWindow.fxml b/src/main/resources/fxml/MainWindow.fxml index d8283f1..865bb27 100644 --- a/src/main/resources/fxml/MainWindow.fxml +++ b/src/main/resources/fxml/MainWindow.fxml @@ -74,6 +74,9 @@ + + + From 2bbbfff5329927366c2330b6c210e83007a24f51 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Sun, 16 Jun 2019 15:23:17 +0200 Subject: [PATCH 76/88] more FilmDeatilView work --- .../HomeFlix/application/FilmDetailView.java | 28 ++++++++- .../HomeFlix/application/Main.java | 1 - .../application/MainWindowController.java | 25 +++++--- .../HomeFlix/controller/DBController.java | 4 -- src/main/resources/fxml/FilmDetailView.fxml | 55 +++++++++++++++++- src/main/resources/fxml/MainWindow.fxml | 4 +- src/main/resources/icons/spider-man.jpg | Bin 0 -> 242839 bytes 7 files changed, 97 insertions(+), 20 deletions(-) create mode 100644 src/main/resources/icons/spider-man.jpg diff --git a/src/main/java/kellerkinder/HomeFlix/application/FilmDetailView.java b/src/main/java/kellerkinder/HomeFlix/application/FilmDetailView.java index c5da73e..9db14e6 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/FilmDetailView.java +++ b/src/main/java/kellerkinder/HomeFlix/application/FilmDetailView.java @@ -1,18 +1,44 @@ package kellerkinder.HomeFlix.application; +import com.jfoenix.controls.JFXButton; + import javafx.fxml.FXML; import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.AnchorPane; +import javafx.scene.text.Text; public class FilmDetailView { - @FXML private Label lblFilm; + @FXML private AnchorPane filmDVPane; + @FXML private Label lblTitle; + @FXML private Label lblYear; + @FXML private Label lblScore; + + @FXML private JFXButton btnWhishlist; + @FXML private JFXButton btnFavourite; + + @FXML private Text textPlot; public void initialize() { System.out.println("init nested controller"); + filmDVPane.setStyle("-fx-background-color: rgba(89,89,89,0.9);"); + btnWhishlist.setGraphic(new ImageView(new Image("icons/ic_play_arrow_black_18dp_1x.png"))); } public void foo() { System.out.println("test"); } + + @FXML + private void btnWhishlistAction() { + + } + + @FXML + private void btnFavouriteAction() { + + } } diff --git a/src/main/java/kellerkinder/HomeFlix/application/Main.java b/src/main/java/kellerkinder/HomeFlix/application/Main.java index 26e78b5..2d580c6 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/Main.java +++ b/src/main/java/kellerkinder/HomeFlix/application/Main.java @@ -34,7 +34,6 @@ import javafx.application.Application; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.fxml.FXMLLoader; -import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.image.Image; import javafx.scene.layout.AnchorPane; diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index 9481d9c..e8febb7 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -72,6 +72,7 @@ import javafx.scene.control.TreeItem; import javafx.scene.control.TreeTableColumn; import javafx.scene.control.TreeTableColumn.SortType; import javafx.scene.control.TreeTableView; +import javafx.scene.effect.BoxBlur; import javafx.scene.control.ScrollPane.ScrollBarPolicy; import javafx.scene.image.Image; import javafx.scene.image.ImageView; @@ -225,7 +226,7 @@ public class MainWindowController { // load sources list in gui addSourceToTable(); -// posterModeStartup(); // TODO testing DO NOT USE THIS!! + posterModeStartup(); // TODO testing DO NOT USE THIS!! } // Initialize general UI elements @@ -252,6 +253,12 @@ public class MainWindowController { setLocalUI(); applyColor(); + + BoxBlur boxBlur = new BoxBlur(); + boxBlur.setWidth(9); + boxBlur.setHeight(7); + boxBlur.setIterations(3); + posterModeFlowPane.setEffect(boxBlur); } /** @@ -501,7 +508,7 @@ public class MainWindowController { @FXML private void returnBtnclicked() { filmsTreeTable.getSelectionModel().select(last); - filmDetailViewController.foo(); + filmDetailViewController.foo(); // TODO } @FXML @@ -843,15 +850,15 @@ public class MainWindowController { getTextFlow().setStyle("-fx-font-size : " + ((int) Math.round(XMLController.getFontSize()) + 1) + "px;"); // add the image - if (new File(cacheData[20]).isFile()) { - try { + try { + if (new File(cacheData[20]).isFile()) { posterImageView.setImage(new Image(new File(cacheData[20]).toURI().toString())); - } catch (Exception e) { - posterImageView.setImage(new Image("icons/Homeflix_Poster.png")); - LOGGER.error("No Poster found, useing default."); + } else { + posterImageView.setImage(new Image(cacheData[20])); } - } else { - posterImageView.setImage(new Image(cacheData[20])); + } catch (Exception e) { + posterImageView.setImage(new Image("icons/Homeflix_Poster.png")); + LOGGER.error("No Poster found, useing default."); } } diff --git a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java index e00d869..3ecd905 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java @@ -219,13 +219,9 @@ public class DBController { while (rs.next()) { String[] cacheData = readCache(rs.getString("streamUrl")); // get from the cache table - System.out.println(rs.getString("streamUrl")); - System.out.println(":" + cacheData[20] + ":"); - if(cacheData[20] != null && cacheData[20].length() > 0) { posterElementsList.add(new PosterModeElement(rs.getString("streamUrl"), cacheData[0], new Image(new File(cacheData[20]).toURI().toString()))); } else { - System.out.println("adding default"); posterElementsList.add(new PosterModeElement(rs.getString("streamUrl"), cacheData[0], new Image("icons/Homeflix_Poster.png"))); } } diff --git a/src/main/resources/fxml/FilmDetailView.fxml b/src/main/resources/fxml/FilmDetailView.fxml index 94bc190..3faf40e 100644 --- a/src/main/resources/fxml/FilmDetailView.fxml +++ b/src/main/resources/fxml/FilmDetailView.fxml @@ -1,10 +1,61 @@ + + + + + + + + - + - diff --git a/src/main/resources/fxml/MainWindow.fxml b/src/main/resources/fxml/MainWindow.fxml index 865bb27..308ca49 100644 --- a/src/main/resources/fxml/MainWindow.fxml +++ b/src/main/resources/fxml/MainWindow.fxml @@ -74,9 +74,7 @@ - - - + diff --git a/src/main/resources/icons/spider-man.jpg b/src/main/resources/icons/spider-man.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8f11ab621c809134da40cdd22e40e9e5b20bc152 GIT binary patch literal 242839 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

    6X7skHgw$W%GL{U2 zyk?MbXm32s;ivM>+@CvJmfWKlx_7rs_BPTi!PS0d`!Igoz|l;u#zqXOs9jc>i1;ZeDna(UC7p_+CpUD$EEP|%L#!r4Tb?=WUNa;fk{)l}Mj zR^S*q?q35j2pFGDq?(&4;rSt%>~in+U#Abn6pUR7<}T%#7jyHnwckx>Rc$j_jRS{ZsZV6dN0}>FrNHVweo3=dHt~_O04&>Q@F; zrn1tzby_x|L|RF>g~?RtwN2bhMp`Gb%zC=k7Ct6U*ty&xi}Medp6Dsj+fLipgb#8) znw>rAx9Vz|o+gz56^q*C-B7W@u9G9ybRE4885<5JB8{pCJjU{2$4-9{t8_#0mPKO^k(@4LUyNuJqJtIBk@_C9%`nqT?3RYVYGTboDKP$j%@69#g5k z(@~aYeF#&%jQ~qB;Iz@)w`r|r)K#;mI$4^;>)=c17UCw8{{UbB7!XH9uwkswy7#Y7r?YS`$(#we=a<2Q@wu1& z8zSMW@1z)Kn(r0Q+G76zksSI?EIC#XT(4P6c!vt<18p4dZ>ZsVYiG|RIDh16Tj8xU zcp5U3spwUN_mp1LS{iiew9|TZX$bgtkfY%85e(th=BufSM@bCVCfiq)nre)* zM(g0KFObk!C3X-)EqqSJZ(HiB)e(&aZ(j9nU-@w|u2yP4DVh%!%Z@I5JL9z6aTDLo zqWhtGs<3QbW?K%rgzDPF6`qOcnDH_8<}>G(4=;m1V~GCNo&?jg7ip@mb{ceA6vU~I zttRd2`QP$yW3tI^kE{b$K~sY`QR-Ez(?jMSS`Tx|f9;L_I?keyqG|@qrJX&sve9+5 zpGK7FY{&=DKsuVY?Tj#agEZG{u^u?BJu_L->MqEuoI-Vq+ZnQHn&J9&ncPJ60zmF!>)ZgB^)yZ6RZ9EUkclcR`c!$u3ma21sb}I4=V6KN zmGt(mbyX{#-PFNrMYd?~vb#phXj=zXjAkcgcpLQzh7sw5%pH`(>N|`9P074dv#8kS z^rfESao#<+MRIkccT6|9{f^n~Fk%g`pa-@&Ti{LI+($h?))A62YWSL#!mcgq>}cIh z#wz;3lx_=&XYQT4R!V(~9QrDkY1aE37p3X2sj%i}w=E)VrUEByPOXP-Z#kv8aD-b1 zsZEvlXg4fW3}u65TZ!+@4B^DTQi;SjbyIZHDje?BboPy%=hK9)$gJQ18@qb78g0qF zV&~ylk2GrROknKP0&74(uom}QH)0d%ihV*F_X0}xU}s-PDY+|5AFE0AX+EtXzOjwk zfn(M-nr<;`u^2boj``+&iwT6rIS~2Gx|G`HSyN*vMT2O|k!!A8dSOQ052bS`T+$`o zndP5}$k_h?1Xw#Bo%k;uL#t-xD%`6=>=a`F2r2$-Pfi8@0EreFgQg48)xH~qxJ^RK z!3^%Sqd8vnr*g?btDE%)Y(YEOa&R z+aE^Tiie`rmFgWBN_AA;yKTOgP0t*T^QfB`N_H0{3$G2B8%wIEx>2Qd+`mbupGl{m zNvDmc(Ksj;Wnf1*Dm%$3^qd)Db?F-n9~)8`j~bX){r`sz5(xW_-r>3Gya;PnLtY zGS6d6e=AKfUCoU`tz|ISK9g-bPo}H`GD4;j&C^+8zMSp-Hq6weOZ59vz5Q8YGFa+# zkGD{hRr#Bm=s)*LDo<^m#)>^6dIff8O;28?X?=TcxJ9uP5TKBWHC1oPA7YtJtd6d> zqK6EcZM`5)uT-UWAOxJ8`4Y!1ksr*f+GU|}G$BAZ)?@0kDr2744_vKKrGf9Kp^7hT32Oq3N(a{MfAd+lOFp~QmX9?EIXB$ zXBu@j5E)7>gRBui>e6jo=s}nWKKKHd0)jwTRyaZ=0&=_Fc@tL!h|D2W6sU zY+1wGaqU;0*f4I>Y-|M6 zTDYFS-;9aUNVYPU0c-)8rGlb+Z2mW5upsd(3eKRNCXrOfS_6=~GwdMjQ9I^j&ii8i z`L(K60ho(DRJP^@YY>pFGVr_P&f6SH>8X@t==4KMuP&;v096TtmIHcYMeTX2{LeIp zZlD>f{Kt`u%Atw7UH;fvR(EO3MPATX>3lmom5WIAWC^AK6f>+5t(VyLwn!h%Uy^Gy zHCbI7T^SFmX)fD+Z7QtWwqOK|dl^!(Qe{hv>*Sb)zBIqKTkM~< zQa?f~Y+%#+l6ITHS2FN0kf`cB9_X2DEV*&EX_qC8u#oK!iP5gwu+68nag8F8S9r(N zWk7&)y;t1pRk+F1QG-9H#nOtzSJRSmMx3Dt&ig9~)+G9x$_T^?r@*G@15JiwatKGT z&zRr36WOK1HCQD!?5ov5^)$EIC7U{J7xFB*2Ucl`={&Rit7Y%F)*BT8h%va$Givs1LadSJ{Z#Ue6>s%gdvnu|qYV_fBN1+Bb7 za>UIs8${|WKYd#4v1eAZ7^!9|8@+Mt&8}vOi(FXCtNf>)lTUN4q+MIrMQYaX2swNh z8wDX_m%`8oL5=l*@D4@sD?g|uUyiv`x>Goo{-SQ0fBN}r{{Y0c2JFGE+w>bo-J(-0 zt8&hbV%?+qT>h5ZNE-bW)&8l{zR_unGh@&G;M8id+Pkg&1(px@DU2DWAF6WNTpZQ( zEi$9D+^{m{RjKM*cOwF>_3C{do^dTb2*A`=xtSs>SiRRIl1*sVI8{wERIavs&Kv$& zjJPd|zkFgs*EFA!*?8+Z0N+u8m1~Pkx2|dYM9)>Rsv~|4On?lheBnU)lv!eJx#b_= zwA6k^=Bt@Ux^y@sn3{}mF2`UMjq0i4F7jPp9sYOZzTL}#{wg!F{{VYj z&xjA|8JVB5y2`tAeM8ZE75<~Bmui~j+WjQWj$WYN#IDS)S4^*`w9Xe=4ztU3A5o(g zPOWo9;V)*LTB6#Uy92o>R0!( zyRS1#Z7WrkS*Z87G1@*@#nO^fFyu1(6lz!nH;q0A3 z>M?I_z+JgQ%8qW+=4AEEVg%|hERNr8?iRNZ9~%T_Vmnv8($O6XD>Uh|Y!GQ4x^K-F zTTW44%bt>ltY;4GZs=`)jn{7O>FBAahfsAZwedC_;#e)N0%o5o;Km(6k*nOP6`gz= zt?fG2<&0fT>OmTIa_lCfBF6;I@oH|_Ri$yh%PM`syqS|KqxGxJia*>>tEg9C2dLHV zH5vsTrYc#mLBvkRb!ywAm0(NGZnW|sm8uoW=6H)nYHK4{D`Zh)Z+yf!Mwm=hK1i7F z7iXEM80gIq^*p!oG_khgVghGf1*p*&FwCv@TGGJ8#8%wAi|*E_`*jJqWo_#OH`at# zmFh2BgA@A3)np64sQ1#yL?HDL#{}(~!Hdk$+$wF*>y(o|u}twm1r^=F8o)CsQP)aXG(W{IPa5!pj)P$PP&38Aq9xJZvkEOJWPM*d+ zqN#5Swwe!8>o?h92vm1AUFksp5eg?%o&JCzVtif0HffU8UKY%t&1FWL6%4FrWNy{W zb~&ZC>2nt>S2t&xSC`7f?i8AlggbB&7c4J(U#~YXDw45)V*A9Wdy!n#1y!nOz}RJH1&mKxKI>|)rbttT zX{9dgs6^$0K@S!r6>(_+|xqYR1CmaZ6yfi|q_;+8So z`Y;b=&%~+2)wp#309&avpYjhlh|Z45k4S*6hfIe?#hzWAtBZ}@8#05`M6m#|rkB`e zvnN$}kK+(0spYgX8d%j<`m-O)%`fJzY8MY+0Q#l|U>SiT`-IUZr0HwhsZBtl&DJ+k z&dFF<&K6Sp?Gbiwvvuw3a0$dy?3H)Xpo?OSJz{ZSg8KGKfas~09M_icC4IEaBe42h zL77_aGU~9fc#9|@H#yb<4PdQ|nr13kl1bBh*iMUMeB0Q~68^9dx}}9@QFrEbQZ4D& zL#ycqAvlXOs6SIvqp8!{_U2G)Ms_`Fr&_6g)@+C+)i+6=1HPnjO?_7@F4Dh2%+Y^} zu(GZ)owan%Bypp%R!2w)Rx865%}fgYRgZwqL9SU&)ekSh_C^w^X0D#*~Z0qh|`ZS9hn+ugv} zA$q_74;|GBqV}5ciz|#zVG&Cl4I1~~3MoyX(d$JWGL|aOr8Lr=T574?vx?=EP>clB zF3m%zHfo==#;#MtuQc@zlSNR}W$9=BKR9>oH!1=bc`aVsdXB?z?Kby^+9G$^Eb+tTCAwm9rn9eJ{nC;8 zYGuuYB33=|f5LfK3wrL;)*XgkrBpbxrL(s$S5C>Pv#Dn2-x9Ve*_t|UG>x4za+Q`P zi)s_;C#YV=bC%^>w|E?h)@Zg87{+o55u0!v)ymGPhU+sr*3PZ4bpdIl-LW9G7j{<9 znP#Rb01sgN^YZL#jF8X2IeTCiV=zU#*%aGG_DerEG9-$MDrW-4J@EQ}X0rJ|-h z-ap4BsBO&C`(uy~eyKJ%C+trg>-NV!qci=TAL0`K0Lc^Zh&#&r*EJQjhOnbeRsd~v zLc4ax8+PPu&tQqzM(t6;4WS_$32^&*%2*njZmnJL)+bmH?lElBmDoh~u~I)Q{{XN` zyju=>VlPdeu&Tf9?J~;B-=v1T>o&cE^)3;aoG6N$Sz%(^HscJ@k7Ebzl~j#g1lBaW z?dsi*qv8SSs$3Pw@;+LX{ollM)#n(Ef8%`Cff6fW|OEvKmg^g zzN=XkZC!?R`%96lu!x>gGWV(#_laoBuryb^EMMP&`Ijy2vQ&+~U8Mir= zvv(6vFy=4+0M7vZ3uTqJ)M&)9x_Yu>9Fk{L;0SC!B3M(p$WWMWUa^o7wUBbQuSq3^Pq}x zEIY|DZ^upx6Qi;l(XOT9joRN;$@;cV8Kp~Z&6l2yrJ8ww^n&rn!jyt#)=v<^T53s_I7vvU~k#7daw5k2~epa9ylVok@i z#wwNf-<4W6h~lqCD6Kc&H9wZwr&{V1?bSB@M5%MK4*TMK!R}e98PjP*9>kn){G^I9 zWgf+Z_MAKMJpTZ)A8DT3>;ja{Vq9`8S!>@{rD?MFY#zR*b@ek$NHT;8UeMi?n6m7X zUm@Z*Bjoe=`?M+uvo?S(7>(^|rvTkpI+RgYX?OW_^BzREBevo&Ay=msi$2V#L`x_Gj94?Kip0n2UN^Y|HJ@A5C8!J0|W*L z0|f^J0s;d7009635g{=H5J6EAAR;nSVK72*fsvuH!2}c0GeA>fk|RTMg0kW8P@=-| z6jWn_U~}UC+5iXv0RRa<0_ij82l_*=-cB$KG|{x;-zcEzL0b5QXw%>KVu2}*f}vH>muXw} zdoK1j&8W*XVQqoq{`!UpN1gLzQ9%tsaZp9N*(@&a2u3vpZaY4zn6nVLz{mm=EgIva z5;li=!s;70`Cn0M6f(H0LOTj}kmjWEf?y!4A2nv=9Gqv#KgmDIKgmDIS|bsA0nl_D zn%@NFyecj-`FcI|yzCOHHitMZ*GR1I@Lm}s3x28N<=Tbf@QH$x;DQ&8Lfv}d^uC2r zV(YPYqH!NB;vLHpZu~cSxL?F|up%oKpQ?r4sMDJMA=uIE4*DG_mJz! zh*Ef{U1&SOP|RPvD7PQ9)x6!8YDl36DO(CVi;?Dn34+jnn5#_}a6U<}zC~9x{rh_n z5xD9(x~I7rTqbMvyETA|d+!ujnE2n-Ya8!>7rpM71D-0)f#-4U8Z+m8Y~KUv?|TcO zlCiY#O(GjVtNY%<;mI(z?Z-%>yC3YgnIqYGfkGCxXUMEJP5Z%pt(4a5T*tKC6_#3Lz74ZI{S-4M$R>}TG z;Y2hJ(|JE;*~7G_ zN0D1jydkTjsLeL@n~!MbpYs{!J3&SP^S_8JbY0^wZBEroR_HllcJ4^To4%8 zx*Wy-0BF%ZVQ&qW_TTX0*G{f^lTTvNYx=JO1F)fU3G`pIEgeUHwaJ_A_ zbOpMx4#lEg2!}(Bpr%=VfUQh25*}9#7B+pbSd(@Phm4n1hUS5H1^gq?a=VFjUh(IC zVSn;B^qAjj#%Nbdn)t7WKmCd9ByBfEz|nPwo~%AtvW1w)s8x;s%V!5nmB!Y05A@YRHb|*Ld=p%neQp@Mrv2 zE3hikwvKS@H|7^p;{9cR?^s*jntT>V(SGgPP3*pgml-QUBUXgxl-a`)sl3IxXe0fu z!^|j~D1Oe6FI= z{Vqa@pbc#P<&tDs=&c^!NBf}BqrxFoi7ve1ert#RE0yH<-$n?Y<=1=4#(p<-OjVcr zlM38^!@Jf7?bye&n(jH>Ot0T$$I)Xpe->T$_OrJn-8&OjWz2I<)3S|m9E#kuuOhdH z*`N5J7~J2ot<9mz%zKsa%nh#A(sP~pTTcLhV|?xibZ71W6xenw1TV>aJHxYUzN_Mk zKaIadZDncL@2bE#3Hw|HM zL!kqvbO>4{Srj^jT5K7BKr7*_9sIELUj+~0f7KgicL#bejsE~)OeOySi2nd!qX@<3 z;T@pw5&e@&2q38alCb@&E(`5!zueSzIwOMJf)v<)a z_-ht-X&*Ig;5kdtRo;O~(VsA=3Sce*x8$bkSKo; z_g2mUTiPGyC=!GTpN-Ga(=>vMr<$^8?!Sm#nY%4Up$pETMH`%r`d@koA#*{RvTCyl zo@gHxrQ)Nai;;JVjA5PDO-vpdnXJ}V@oHt#3xck$mZ0bgI3kKDqKY?KyaotJ_otT@ zoO#`9@*8MmP72VCCt=v0bhImqwsgNTwRC=5T6D5cE|4=;uZEd~)1Y2O6Nx0^tsLEq z3N!g$$?kN8O0;nev@(i$5pbe$F3@!GR<1h|nzV6KRJMKeYXU1|8Pvq@ZE-r*1g3Sf?oj8KM^7ZcSV>bM-T3;0$8)p7eP5PQ>M z{{U*F%e7I)U8=%SkTM}M14<=+8&!buScRS0bb}6nbSGCkg3V`mO-;B6-7Cr~(GrP6 z$s585A#*|qK{pi8P(e4KJqBF>>8l%!`5-V^^f`UQJF>rvqB?@?t_UHh-?<%5C7RCc z@6YZT-JeJh)I9;czi=4bE*ECG>STr4ES6n1cV%{Ki&HH^iai81o6u#@RyQ2hT8CGi z*({b?p)~{$K?D$4g~h4FF3jDQpy@^J`?=HzEU(K~JMt@qaIA)=3oH1wGU+*7b()Rm z?q=huk|OMs0L?fa^>1bqi@TFNLbC8bRQSp(CBm}S%KT4GZaU*sqjA-HbdC|7z`>$2 zQI-c;sB2^pC_}Yvz7iOBrzexD;t(bsV68!wG1toc9nhYH>WHRvvbS&hzq^>izO1ETtQ=$;QgB54%|-+=7z^D#^JiJ2WHMh5c+*G^605^9+rqT!MVM&vIw;o$u{`YROYQDwZg7MXf_R~Va8DX3?eR_;GlHTYa2sxULi2MWX^72t-M;;i$YP8F5NM#(##dm| zc-cF``YAI3nrH(d_ZvMo9nfes2DWH@aW9!PmC(H97&0XyT}scAtPYcxkaVuajw zx-PIc(ZUL#z}z-lGky&#K7$2{ejS9;8Mo3Jw8;-OT5JS}k1QSnkW4`b=Ho6)bBLPryK!;{1 zR?BfFs+Sq!GgeNIXo5eqzzQ?_^dfGxK38RSCsCl@ZW^1*WzoikN55N@)mk@*>NVaM zo{Me%L0Xt)heJrqlCBDhnvYw~=y;rOG)EM5g~w3rktH)YcA(()w;HQLoiy^Cjyi&m zh4|_^dNAo8Tcj$bpmvI*1P^L*YHqy38``uT7YD8Myxi-B*deMSG~vxcp?Z}M5{H_H zi9|!iJ1j57`d5^1heH%fB{~R*nheUBS>J}UQQq<@qf@`4{5I>9R1<$wGGjV7s9&W#5Oh8 z8djNQ+MN^k3~r!71XS*z=ro_EcCk0)v%4jcyNPDA)HikDJXUF@YzdA)!7!j_ak`1X znCpn)Q>=ez{okss%qS)u9MifSufmAl5W5xMsax|3$Ozw-w-U_^YM^^Ha~?|)p&^=w ziie7aiie7-C+-GK*&WJ5GO3wN2em;w1!JOf&%31WM`dCz6}pOood@DIVSZZNOTzZW zPnF|kyWEuag1}voR&H5cUYIKA%e4soQ#6l~2rt$2Q#~Fc>=2PtZV0N2q}8(Auftb~ z1(m@BH7E>jFas5DW`CGoD|R5P9WKbCC&>}uiYSi-o%Kbg_;>0zQj1wOJ0EKN{Y7vNa3antT z3q2F+w5o|(QBm<)N)Q>KFuMfUUyh-_G&>hp`j62tRR)2Y8x0U>_xp!_y-vP2QqrBP zhV1SLAhWydSbtHx{>sR0CXbh+qYg@TIz^A<=E^9<5gV|7vcdOryFYt|SDkliFzB?7 z@VBb(;&tC-%6(Oqi+?ncv;9cf{-tig;(82%-A|~}`G)#l5pRlrf;VXGb#Yd1ZoNT> zM|rVD6j4QUy9P_WTCuGnLMfUL!P(u$2f8q8MAKkx3+*A=w;j94-HA|8P*6}=^x2`b z!w^>_YMBZ4td?sv&!EU$@7a?QIy!u|!@)L6#vI!uT?3w=frZ^e5!hB}AtVsLuI7(* zP-4W`k50t>BM8~MHjMd%+b`M>bewrYirm{mjiVO<^iBxvw`BcRK}9>w86$OXFjl%i z^1aa1ERc=vgw>+toQYg06w;d(mq3L^yY7~R@XDN{b_=R?H|)#-?7D|!_uqxs-=DJ5 zhlhgrs~rQ^(b`15(c8~;P5cEr$#tzglvb3%yU|C%6RKdxyGqR9IbH#lqbebOYbDD3 zt|-vOb-+5=^tCF;S`9GlrV~pa2*9IMXQ8Q%&{Q{9t=n-`SSKgYD#7T_nu%^G--OR{ zkT|9|w|T^2q@Eudj6!N^YHDh%Z*R=A8?MB5tbfTP^O*@bwQnS1-%iwOA*mb2DS>i8 zELI^D5baoO9)`Ms9Zw6)92AJ&Zc7!N_!X3u6(OXZ6zL-*)l&N{CPokC98?QVBJl^}%nxF5Yl4Ja+1&-26GDsT6dj~?o$M10*e2<- z{P0FUmm7|zTA>t0!ZMQXIghxFM&Se8o4(&W>f;4~0T?*82u~B8*s3fOr+~z-hyMU_ zupi7~txJD~O4-MeRztO^z=RiFq$fi$f$c-(KR&8(b50`aT8%FJQ8CgXN69%f7L3)K zZnM&P5>L~%bEnGYgg`hI-Vq>059c1~@o8)UzcmyxK%js?CWO#{P*SuWN9cqc-k9r7 z8$ma8#1tgyi@%rac1R%v5K}yVMPq1AWF{RhYEeKHWMP>yPj7V4J?8osX%FU|)`h-` z7IwewD>pS%IyVYN;9+$YO|f@&pqN{x?bA?k3+&t?-=)VCTa* zhN~lJ!3ahX8LI*Mim-2qR!6zZ<~P15=`iEa;pm+xWUA9HVFDY9pt~e)Dj}|{F;;DD z(+Ft9&24s~rqd!3i%I>Jwqu$x7j>Ht4NF4iG{i>*M@b(veAZ{7-8C1@xVPLR7CMLP z&>cf|&7j@1`li78_G%b8sttIkPY1BvRN8ALHl5SZ!(-5+;OHY3%NX6saIDdZMGo~c zZsA$#2(bBc2*%6zcD$D9n(A5)JzNU2Y{D2PICdwW6>6w1Z0d+M-4|OBAlFxqf!3=^`B+O9~tWKH`AcNERdsRb`AAC7HjuRi0xlv zYJW+}?+nVDjDPK9m@0&!1XLrBK5A2<-j9-uQ<0ll=2d4N`@#yHUdzOVN)hIrcT}ns zFapBtk%AjgO`JLqtJX#i1|X`w%xR3AC8<|94;A~UGr%B%8rd$hU=w#%SI}$bj2mj$ zN17l*Xh$(~zD1rKMG`{>gijVI0~9K|Zpupr`mU&gX)zE&igZ*G@RqMqN3f>FJGdbN z?@d~=T8Cyg;?Z*m28tmB)Y5^EVzdn9in9J^eG{T}x0()*@nE8K`Qn=Y02D1eLem}L za07oAvKTHE-mqC-YtXp56pCh+gTaEcb2GGmSF@Vkzbm+wD~PLY6T)%0!YJU1D5on% zxMdKa(a9N9to3ONnwjRA=9}i81=%Z83o$8PZr?R>F+C0iP~Or&V4`EZF2&_SiPVRh zz)f`qlB_P?JqGKk-m|}Sd!U&Zb5=$n3%5*FFg%d!6u=dro)=`Af(lz9)}8bUOZ6*2 z9Ol8g*tFahG@6__rvCuACi$RlY;?(wE9B8qsh>%_&r971@&yg~9#et?Lr4yDOrxNg zRw&0rBT%iJGn%x1(LH5pY1Zo_(6V&W7;{#E>==zl03@*$zLN@kCndWL7ja!xRafd-wb1M_`UZj7ORNYc?}yF3l}?<|nNjJL&RbwontyUb*=SzKEd&%d z9aWOeV{|DbFT47Qyw@X=sa4HyLU+QN){SRv zd=;LTH+>bMhEy8O4>0ac1F&4~Qw@FCp-~0daEcf# zmRjK!=;qy}w6q!qW((r1yDF{8G0W<VkeMPlt*SI@wFKp+equTsc7$b#Q4ny}l@Q zAP+KxA$fr6j7olka%_<$QZ-qjwPZtqT(yMC?tY51iUo(o1-?M0OnPr5Dx4r&BM6@nE{zo zd?w}1a5Sfgt3VEF2bvW^5K%SsRG*^B8HGoUFRrgGQA9Su8aQ^W2WJ^A2-0p}+UQN%Gwg~OW`QB4G=m)^>!}vS3Buuu%T$jVD@VY)43N?s=Dr=Oz-eHN zNkS>!3H~XtzX7XVF>`RW``C1xw1=7JI}qO@x*i$I!@w6@ykd0S1# zu~5uvaINmDbsyxL)e}c*F$?V)C-qJ)C4i>+>$ZO`HN{!k2#Sp)@MRR;Y&V&UexXJm z`3U+6xwKQ5;XW+SZKO4 z+$4o$JGd2Q-~uZ#=mGgBN5oGep{g+;qIFQ0JvJASX}_KKuo{+ zJybHer43hya|pXb6xatafG%=!HR&@KnE?gZo8*E5YlW&Yn#dpny-uFW>~vr{F) zR}o)^sNoc9HoH-gt8gixT4Z|=fr_jTuC4}Z5@Rs@$U<2{?8ln?w-|OJ9V#IU_N?;D zu}y>t{ZIsxcP&h=t_ylNnyXS=+#;f)qO_1h$rnL_LK=*WRhvR!^i=fKozKy zL)8hxn&J1qJGhUc06HKCcE+m(gS?h!QwEc8KW1y$c%@O%Ijb>fFgW)WXy$ToihEtz zt2hSPpXGEyY$cp-&A#jM`YcV>WcDV)T9AXJL7`i?w-I(QLhR7M-7^Jd(*Ce<(N;}t zGIVvVzF0UxKTP7Y*P*{PkFpdJM6M9Hf+iou{gi2+G&@xa#y4?$FT`)DTyP!h16@pS zz(HYf{oF@#2t*?UA*H*jci`dL)Zk^5E_5+XfS0<#R5~7N5=SM4@<(=x0@EzefdnoS za4XRr-b*#`jZL2<(wb9BIwvBUbV*HeW2Mk@)h3DNKtx3zs*qO1({*5U>;Ml@M0US;01M-YrB z39SlXj5`yPZZI<13T$&s9T?o|tQwmr#t@RThq7do6B83;6RotkjLLb?{HG+@1(!jY z)Xl3ax~bYmGg*w2$M{jl-DexQAS9Y1Uerk1h0Tu1R)!a~Ni@Sj(@rVJl6N?)y-Mnc zp*?-(Ma=BPTz|-+0VR@+PAiB>azGRFg?kFolO39LAp~#-%{W1rW}4W3RASTqO&ymb zugjEH>1w_vIHoWn z3J{XWDy}dS8k*6!g3jz^;~? z0(l+j?jtn+02v`ToAV2VdZu^Xz_3nd_#m9$@S~ClMg6LTk3<_Axi7?Trm2vssdOf> zWK?R2rfVn#h1nE(!U!yuNNBmjz0pGK3hIDY<@8yqE+KJRJ}7}SK@bXoV89pUP~E=* zjKR|G!r{#l*EV8F;xLMfvV!=6)55J?ObWCnQ4zrqoQ)`skmi_%r6OmN6HFc|lUPH| zWUAJUF2%sOf(?+&-=DIy1=%hr!?|dvZmO@uZ=%3^>KgAZCHP@xw2xs%fe6I|St$1` z3KwLBEBaH$mVoO~TIry_%C26EHN>zg%&NvorhyKzs^L$BV6h(S2roN_VaW)^3HuAd zOnLqk!wj656|a(XVxI+S3ymaVoOW7!(?Rm|O?ysY#aoZ&h(bMvDqXTgzo;VGoI=3B z-I~S=CDvue8$nu=E+v@tSeIRy^jI|dBGr<{ONqXVFMUe7kw$uSP6gTmSD7a5iTf@% zt`)_8ON1*Y8E~eNvxtS=Vgc-0UMor?n*2m=X>w_h9!nXnA9Uu3X1G>D_Ny@Hweavl zP`ataLQ!899V}Xn=4&)@nOiM5{{YgraqcTS+~ek$(byq(6K)Y3O3pzA!|shju9X#> z6f2XKYa!^Y$L8-)EjDLInrayY)4UKn!8TNkbsN0LL%9i#rVGKjeqQqO{~w)|Tax5QYhYS*C`$w1NXkHXpQtc=23LO#c81Z0NXNd{+Zvy4WhjDDNNY zTuUWXAbwwp;#B_t$zuNi*>9;s_o~gu_L@Q*rK~8@FTuAOOpaFoPm+O>q;BJX^smVe z6-^@xzsajY0(o#K?8Y!wa*-uTq7qz_MaLVs6-*XTRCQC;c9-GZ!|$kTt5U3*{!m{6 z2XR(#kU+ykDQOTB#?k^LTcq!^gwAbM(kS^fR~Ch(3fgF`%n=sP=)r5lieBqR7TS* zvF5nlbu?hS7HY0A0X89qV={(~B&dU_&1P0eMSdT;%TkJs5R#94mPiy0ZliUH?q3k- zI-5mWIOmL&37={*Yk}}juOuY`xPr(b8T%$VFskT#ssN!8zlf z;R2rG4XY}#EV#lgD~Rd{^F}VwxusA*9?+y5X?87KNCPP31)6`F#|zSQ4L9{V6tTGgh&X?nXOCYlS>+wO}B*U79fhf{BgL5r_kl8wSkas$mPN zNKuSada8PQQTA3wv5E<`K~)%OD@dZM<(8p_!sCL75rxBz4rm_?(R8^RqQA)|5v2%s z1xUBmJ@w5w+rlyArpreFj6`m%P5uZ#ER(bvX^c~>^mJweE9Q)gqE#*%!9z{2za{t{ z%GS$pbq-yqw4axNhBtEeQPVJ%Pzd!}zsai@h>50#h&D;WF@)48F3Ce`5$;fo)@vn# z0_hOa0^V6%76>!EEguBN=O)!`yze&Sa6uELW-u@)^GiRwZ(R_Q^gjqy|^ zf8l#8caPYx{iZR56{&v4F4D&1Js~7^iS-w8D?_EFxrR4!!^sgtlvv)Na0J6QUcO~$xY)wdCErte)Kox0UC?_W)crb-lP&|{sfQooK3;dcjFbgLv zV6#4ofP0X-%^rxtV98q(NogS`L&auRY0uj;@IbaEk;o!^Q=%}ML(fO*cnjrvAksj`B14ZbUuB28cB=s5Hp}8(Aj2j>4>8;GOcs zUM3ZdL3UiA}PvBp^l$&j7K#YHNI;H1;jlTQ%+gkjKD|tlun<^K8n|^xtg~JxNM^- ztdzFqvnrS5RikBD{zdT&V32`^1}fX;LAAQK1z7v2tilLL>qb8=K5HO~aV^qR*WwF4 zi<-zT6hgrX#QGtnRh7{)_FoUyIF2B!^kWZFv20M{iUtDCCK#@yA!_ir7X*znL7(hZ&Pj`x(@l@zI^@3>eKzN`$Q+(55 zTBiyu6I&Gqh)>4(!yAqn@``j|YMxA6;8b@8Jk)yFsO+%8MkUMf*gaR`A61&Pa8{0V z2ovXX)F}!sT$kX#(I4HiSh=Z1>$!WZhxDvn z?4wsJ2Ykt8w5$sv(I^QkH)-e*6iTx$hDyfM!B{`RSU40m|9)|ai2wqiSN{Sj(|QWgLNCLo~HrU z&2Xzsa#n&%ud{73*PB_FkimZo3uEAVW8 zt#EtYm|vICXLd`5&-zoHjw=WID|f&Et=_HPt=^3yJC!1`yAFf!^@b~h-t3lWV|NL^ zH0V`3HOXXzX8!={2o2&qFX1WPh{(OZMGRSR>XMnePX+ix*6;ts04Wdw00II50|WvC z0RaI40000101+WEK~Z6G5P^}QvG5?l(c$qhK>ykR2mt{A0Y4$AY>muH9ZB4onVIw~ zj+ewag!;W3K*nF=EptKxaZuWc-au^hhnYT0oamV|H~I8^Hh7*+AWVNVJ^ehVlorEB zq5UTh6GAgx{T=$pQ^hUKN|AiNpIi8ZsF1mL9`0f3NGSgR3zz&hg}xYH+AYh0;^u60 z^fW1#pG}9%&(RU@s9A@Zd&&@Q(rxqT>(gh6+(q70f0w_eXOtzj&oKPc{l{cO_ZY78 zv)*m%9WwgToE04Tl+Aj5Z{u>~00qYudGviB;C~&x1b=Cy;Eb&%)2A^`gd;?AZ!-&b zn2L+FT>4(|^og5?EV+^5RCtyD0Lgxvg<$O~E41k|&v@XWPLb5{P4z21!XBSn z{{V$ACM7Leh8KKXxl+5t{uL;9D}ASzphZ-DLLb^1mLTQyEe78{mZ^E4Su+=(OmF5X z-b~J8kI3}d=m4)CKk}CJ%XPm`t^WYUM)3@4 zxT$je7yK?-%inq7>C@hQLLb@=ClIBvW-*KR^gR))(_h{fT&i``*qIR60f z>FPf;3xys@Ivu7M?#&-YBhT?p-IC$bE7R+L@Mrl5`f=)iE6sPGNQS;)sm42I-i2lJ z=xXyRUK3Q=4gAcB*oSXv#SNaCK0O(k!}&gymZfE_LUZ#5y^`=dP98lClj1S;PMKIw zStpKPHs7b#{{ZsbrPTZxRJ+?PDiw5?o+nAqzoGp>!-UoF&HbPiM{R%duIY(cSVbT(?*_AviNnO#-5vZ&Js;s3 z7Lb&t+>(g*zxd||uVQv5Vs5Gfr$*;lVrZXJaj%9t`GtIC zozb_x{huqjOYIVhC9w?aVxwhKQPjjt=uMpSA923FLK>@inW@(h)KH)j(>Gi*wA95N!JBi$S0u4aw zm4E3T?7*;E_=uXZi~Y=?Z|+}zxTC)O%DMz>i>bSZRCR#&P}?%QmC-BAcU(qU#2${J z2I7yQEmPKHLl(L+m+cE7TD>@1^3-u$e9N zWjlOI_V}I;iB8`W!SN2-tEMch^w)wkt4MUz_C^j-4@NgKW`mePVi+LO`O2lR3MINY zr=a1U#O-)Gu9E$_;cZ*Z4`@{i=`~X6(mzT%HOyeGT_@y-2e5DAU}mT$IL_;o>okVm z(;#&jeODTQy2Q%rV65&RP9Uai`Scvhe=2=TW@piwL$7vOPX3?AGb{$rcm7Mo*L0&Z zHmiEZf0${&_C4RLjRKPJ#+x4e!7KK!8jI-_YRhLYx)i>#vnVQNf=iM2E`9L9=(Tu8USJ@UO z=?Xlfh<@TwoOHKxtQ1ypIm2@OBxhWSHo0Gx9Al>ua?HRHUKv&Xrpw4Dd`YhHF#GWo zojdL;^E5xmYvOLDpJ0mb=n=_QZcw!I);scn#5u-ka}VGzZ&DPnOU+O5AM-5%dz?YI zeTY`kQE<3bebe`;-TbHPQiK!8kVsq!nQRu~%vmlmWWA~45ZPkfQK_+q>>iD^53?X* zx>EIuAcCzv5SKsk2{D2VRHu4GWoZSm&EZUG?z)0J8NLrk$Ie z%O8G+ur$%)4;$_rTVr0U5S9rJ9b6=R;(k?Z1 z$pf$|ZlBV}wGpjASC%-`1j9>*+9`dY_H@Q6_Tnb-*NJ>5*Jz&lINYR4tzY|z)m2%% zjtq3@F12%F&Cb)98gP;D&gm}!4pC4sO2e6hPWsM+nB>kH#p+E`yqgnA;#-JqG^Y;F zrh&4?<&@v;a(&Hw=a@Ksq7*eL>pQ_&+pM8a?rg5FS=@i}tC#z8kB()XSM<-mPky}0 z?KO}d#vvcMcW5>-v>lCZIgT7J63Zoz7x|a{#fp0Z>gKn5{{YBlg=;v|=@C}qk4INx zT{i{`B9=|d>evrR0W30_U$#<*qwer${he{v71%A~oJ@%sI_{l!`r4k~Q`_oj9j*<+ z&!y$-^DTZan@_YN+4$bX(+!S}!1hJjr@p5(QBb$~fz54Te&`Cj@VG7%fcD%!$e-^~ z7>iUNY0u0PQ@ml+K#op z0pGEY{yKE~!oL$BRoByl7Wx>WN_YPNXZ`CAMEt}k?YU1dNyfY#$6kzhbTOOj0tcV= zCogxEJ24mHcL}M(Lka1p>v}`3CQHMSrLy zj3K5OnRa8Fg-~n$$?lIn^Y@rc>#Q@Fnq6PKxc)`vG!x7vKrZ?XX@~y+60!DStxB!M z6Ew_zd6&O_V`lsFDDC_0IITEcOfR@^hkv6F1WaaJXtNplh~FIl0OHgotN2siE}eO- z9tdAIADF4fSQNV>vC>d6%*`XUVtITf$Sh{25gd~K>CTU>_Z7j~{{Uc$(3l+z9_Jb^ zae&3j^EDVH@C^q-0gScd%08mK^OOX9wHc2=-{8apWCDs zT|cq(Z{1C9kC2C72k=CyhHU5SPpl+Zv0FOrvj!kd5OL86NWBsFtFp0Qj12 z5pxXJpYtvjV2U3l!STIi`tQ~$3Ks>=?{~yRHt2p`CwoEW7M*+N5}Xcs%<6v75aurF zr`zf}W1f=Y>G|yXK9_IzalvokC4umNQ~pOvaWXfn{6EX-{{UiG47x+3h#Tr3+;?rN z^Wo_LZaeVbqtCqS@5}>XeRzTjD$Yjp-p$4i43d*fQJoJN_y{%n6zMKz(^kVP* zlqUY+Ev;jN)BQ(BTE-vOS%ieoAN2~FydQjnzxx7vaGBp}l`vI&j~6f4RejB)%(|YZ zrKj1KA#|U-LZQ{a{d*_(&ObS2iEw=4zqxqIr$@{>70`u>aROBj+Fey_aRV+o$~r-r zDe1`@ub9|2^Dr=cMO(2^ap>Md<}@!x~2 z!M{0hm$*_WcmDw5mmlQ?v&>vAZ-FoOG5dSNXV9Og_{)q{0<0ybn!KMgp(8{e>KZ*@ zdV2g3{${LpS@#D!>n`>^xLFEec~YR87|YA<*?8aFSbh0{UJP|Jo_N#TyHMs#$8IWl{o}eM8ei?TLtn$=xmjjvVGGd;wsX% zo4lHYD%a+!8;dq>=BqhN^Jocu<^5A}(f$$q6Mg>W@zK^b10}^pADQLTm_LL|DSYgY zG%**NCjL{)uQQs*yi@uK>YV_uxp-iUd%TBZ$1>npEV={E%!P5hkdzkzwtZ5 zxF2$!jo3*(?=!t%=tKr*MNlf(3l}572vo%qjayR%Q3Ha-ZY;{MGWl|8yxc&PG|jh| zGE=<7g_Il_o%)X?!!*ZPik}~)T_4Q2%+tbDDKhykZtPv6`DJU3oX=y8npX&tUCEK} zLUa3~pLUm-gLHTCNluID0W1Qq>6mP1`{sYHeHH+|CuQ&Hd1c~YQRj2Xe}(#-z31;z z{{ZGC=A3X#H#xnzC;n8^@6lBYM*O!H>%H=B9}hCOZ#)C32FhTI;KCb|qcDJ7iX0h9 zNx^_PHbbELgNkZ48hxi+l>UZup@FgJAo}77v^z1a*I}ic31myE-?mUEbTXM%>B}Ee zqU{RwY>Z7=~MRzZckS`uZS(SdrH)WqvAQE zZ--~Z&qv}Sth#CiTR?se8a0&7EOB;d$R$P<^v2z!U{umOwboe`)>8xF({n zHIP0*`IC)$L$Bci(ykXQI0>)$JHe1E;G9OS07aqbjX!}$2qZOa4s&rE_J+OiW88-> z&=>D2U!_D-E~5RSE9O+P-#FN4i~j(WE~e=L<*8g*hzem1O74go$_}$kNa+@%lQH^3 zFw%Y}-1K$!k3M}$xIn;TwZp0V-e(T@U&TcK04oK+@eFM2f(5@IUaKjMLNL-Q(`0(L znrBwtU;2jA$^KKV=0kk1$MC}jygJiDxhvi|^PBf#JrX`I*D^Vt-gxDK9W=ekSr z4zXUyBfL=j!$Sb4mn_PRlROU-k4hbQpm|8G`VrJ_$k+IR5Tt6!ddu_T6v{NXC!PqH z&ocu)V?$^_jn(rz$$^tOkzXm8wKXV(#RI7=o%2$_xDN;-hg?GE(0_SQrn54$!~<>S zU#AZr)VW2=VWtKcgR=cC>*&ElJ9r{cnI6|2LTu!?kMfuQ0NQvy=>GuMPVA4R6KM7RmmZ?d^YhGie)ewg;zDy%s2)=?}Z&*1cSUL9eWW(_VhZ955u;L<&>Rcf> z+$Ed#h$?r8NKC*UM5P&`qK|9(l%bGCV3nUFK?c!`g`7)w5LLAt!OHBl8&y;oKyvEF z(Wgz3;@NJud7Ay%zJ@GL`h3*WIpgN(6!!uOs;!^3gY!3r^LzYx1DZXwCL^lsJ zVl-7C>ZN-Xl~_g`nN7G~FlDDAu=OJ-n#O4*_D)i;niQNy!A_zVy5MT z(PO|47|N1sE=q$7td!?W^BZEyLpK0fl26Ek7ip(Kg>H)%unqN z3xii&J5S!3f2io0Kkxa8$l3O3F)p*kI?J#Ke1Z z?oM-a-Y#`ncswsI_L7Y^3#heo(orNF+kR$3d4RS|%6c4~$<{&zhW$066r0@gm4sDU zG=(uxn)rGZ-ONJs!~&l4TI0SYv!N9Q!FtZ*%^AbrarR#d4kDF;W61|x0(<&dh!26v zC_P{13Il1jwH1?5DY)t}Q{tvIet*6rJF0wb4G@-6uY-3VWo*A1*I18mQQaPwuRgzz z`#)>n(AZZZy|PTatgnAZg#hO(CFOn9UFb|3hkk&vBWkoec!3|YubyRfVArC*xpTAq z(;@Xv{6Vj*umzl{&(xPG!abuMDo-<9n7jp7uQ6yRdIPA`jlbBbld?S@>K?B zQ5Hq#}T{% z4WQ)n(i#dnM0@sFy!2y}j_1@>0`+y)bH}tX1zsjg%zx!i{As+YAE9Sf&!@VRTnVJD6gM)X8+Run@&!%Z4cuSc{C5~zE@%NCNGzs>@)p&rLb;AUF6 zn+s=E2;144d`^+ABYtGMXyQ}C8PG)bY7{WnSi-W*!JHBQ02?T^6_lnN%6G0)a+gq? zvWp&L0e>+IWBECsYtWQiL?RkTs~0pVI=?IgVgA3Kvq<nh39t5E= zti9%s5vX0}8p?Vj%5Uz9x8W$l@#?(ID|mw>E`$R7p~+AQ=_{rT>lJc=!RY9~Q^i0! zb=D%IYZDLJSqus;7O?Sh#Zl9KaG5#jI3hDWA~1l}b!PJUfWjke#5$@9CIp>IanJI0kO8CfFrhOSqaXo=8u8 z&ps#DP3p}|Eb9(+DF~{mSEa=N0Pboi8G;N=bZ%B2wG9`{JscS$q8dN>zo`TZgk89S zs%~^MHdTw9on0n(5P*K7;}su2>J9Q@HeiYZqcQoOpPT6?Pu}O#e+f}u#%_B*al`4r zxb>O!t4%%9)%S*;pt((mf-Xq!q*G~(K)o)ehn0%ec#6aOsLhi#9+3pMq`uPAulttw zBjy2C%+rLWXEA}sNPZKHe9_SjAHu(p5913L2KkuX(b2Q^SsHNg=fwmyi2j;ffq5==NW`T>%Y>N#UbAB=_x^KN^|~89PGhm*h|M?o;b^x3HVaL zZmBXgJ=Y&G3gb!fG=f_kTR21U`fgKlI36WBm*X0`~M{1fwR7kti2>Z$2gZT)A^C z9Yjy}4XLj1@;>@bk%G-XFg+ibij=~Hg5C<;UWj~MJf$I{j)%1S?|o`iR^g;!)exg5 zJIwKBtzQNhroy6h&Zl9VL`D=|B7ki(=*yRE#12}1=g?yS>{yjip5LM(?h_Wg-=Tgg zp@F%cv&uYM4>Fs1k2!@wth$~_ohHy%%nbSmW~G6xOssr?&9D9m1QOuM%VcXwGkrfI`x}6(8&C{Ca}GR z=4_?Atb4-lCb$QL1^x4^tbUvW-x-R3B4R6;+9wdV$&YBp(d-}cXz}R!Z~}A*^yTgx z!IK$iy6n^j-PQS*8jqlAYP{D;K=t(PO@?P!?1=T0F-W7V!861%HyI;c=elkDI7s$B zVDO5Vjnm?GIfeB~1wtUo%V}p>&!8~9q1))YROB3|CwL^n43L+=4=7xuIMbWYQeQF9 zXna9vS!QS6W$o6o7)z6F_o)2~`e%cf`v(!BHN7%-<$;%QwlN*Erm>XUPTR77acZ7r zc&ICO4+dkM*!k93V@0{;{yY$;nrAn_%rw0xw8`}lQyWTf!)H#qLy6yrcj!X+2nP{~ zb|aMP$FIn<5$IfYou>&}`%kCnh=AbsiM}}0FfY>9@*Q9WGq0vBgO<;rjQ};6L%k!P^UBWxreHb zsonX9FEAqv!K-7u8xDx}K*z)I>kaeSvGft25vB;hyNo7fQkKO308+BOtS_{c_p>wF zT5%iuls9PL>pe&_r~4BuR&>mN@U9~9#~$&5$GI|d@tCujLGy^F0b;;$;2=WbD}F!R z#OcPO@H$3xwa{I7LxSiVhe>=vmbUAs+~#9RXF**_^?e_NnjqGh!SbD4?JI1zP@%6$ zUMWb0jw39t$czhF`ozz9I^uru0Bm>H5e&B5G0Eut=_0kD%rts6P;{F4dTj9>3%T^Y zGdn61hFA*Vj5@|7yt)^m?*nNpyiByqls7n?9mQ}m-Pv0~V-HDt#RRS#p2+J#le?*#*=rmQ8!)SUXi>?W790g}U!9M#$%*%KUf{{S(q zWJ7e}Hcgs>*fkz>a6mq=R2P^Q{@u#BmpOg9uW0;5>_vFs_<-e{)rsD`J>atHpxM{O zCbQl0cxA704zKAM)Bb%s@dk>xL5Z7rl)l(5yzobj1d88|ka^cuGZtC>U{Uk}^u(u8 zGK&tReJF^ykC7R19wmsBfIhOGamgsGQJ?rF-w{CW6$g!FUD4$82M=3UR`l)Y-c{+| zrx&TDp;_-3kOZ5E%$&*w0BBGB$yoZY%zxu0!-U|Ss-+7;y)IeT{{WV+qMnhFK*B&i zS#{c`PS71i`3J~Gggl0i`#E-A2O=`Od6sJhpCjIP^PSNWX)lldyt2-q^O%|@JtE$P z`&^Mmkm@6BzPc;Vyj1vpy(v6*2J6IOKhgPu(da0OGw6#6_%kXws3Q{POb9uJiAvzy z$apc%;g)&y8C?VN#PQ>Y#eVFv>;C{{OkBrt@=zW+!qtX#RXWng&Hn&%=t@fG&?|!d zKA!g#A6kebXm9(bvy^a!+nHV8FwIQU%sNTOqU)@&T=SX8OKXM1mL5p#+IL~2(7sey zyQ%g)K7_6mquPBBlf(p2XSn;H(0-Z=1Uj!gFYZ*&`~A7Lo)F!2I=e}6!yO8#tV`OhI1)T z$5`5WaKupNmK&%0BBRD0-}WU7eKJG34@U9dF%!MiYa0dcDRBm>mg8}3pRpphhDUVn zev>R+(TX=5aaxrvr>4DYqxfhKL=vD{KHvZzism24D^(Dd%=MY;DIJg8CEf{I!HWn& z^x1x4tAUbZOnblLD6(96)9`hMFNH)>s1Lc%y#A$2H$`E~Cr78T_XS1WLkPp22n0z( z<_O*|FFzA+`rqnME)D#~?oEP0p&0n13ddPJ+M9G|l$e&pAwsrlW?{PA!t~kiKub#R z02e{)(srM7_K{x?x%#elUvlQ%D`OvUY!3T>_;qJVeWmt~Se06-*qxYZ$wBS8erh5! z+AmpxWTR%Y~cS0!Y4-X*fq_1}wSseH-H4=w)D7t;a?s^u$~S z3(N`!4bfYgoM7Y!;fk->s&@3%Tfc6;gVC^jVt4RihIQ!(fV%2gbX{WoPK9qZDcx7D z5YDTHOZ36Czu{M@)?n)NXh3cKu3M})W8h0Tscc@PJ(9gI9+SPhw^UZnx79fJg3`W* zVAXUUsH|z2D&E_e?c5$aKN5{-pJHAjn`5Zv8#l}|T134%sf1GS1yRCGaPc48txv55 zt-$z`>-uDb*4^(K*!YhPzC2Ln`Rx+UA+`|LIEP@m`Xp6S0pi{?P3u{gSe=;%s}b z=ZRBYa;O1MGMi&^on}=zs7pU+VtPx<5dNsrh>MnH+{wkF;nE@D^sTf4+#)_M)6a=` z)+(%N|j;mU6Y$pxteQ zRVz}fk4|E^d_JSLbiCBm@`q>U0~hluS2%IZR7C?`($q$oh6<{DfAqJ|e?7{{Y8+C3XBt80~`|2yHJ%h(qyTF+*H&oU;gCn2Ks0R|iTqIje<&0C<(?0fX6BCRzEA^jwm>Kj6cbHL7$vgxwxG_6;_?FXjong{`VIMl{E9UAvemcjM zqIG2(Q@^7!cbIjQnmq`XE`5(hmxvNoO}7`t>Gq~gMPca@;y4@oJE7|z;P7!W<_u*` zOR3)@#V(8ll^5)#>*KUkXM?bY6Qna%0A7WVILR9V)1`dQR(-uNNLjs1{yun6gNT||lpZ8^P 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

    nU$tQH@DWE##0AcWGp1Ip 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 YlHWf6tfgy7^z)_miHJ0D`t_H80n4rv`v3p{ literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/baseline_keyboard_arrow_down_white_48dp_48x16.png b/src/main/resources/icons/baseline_keyboard_arrow_down_white_48dp_48x16.png new file mode 100644 index 0000000000000000000000000000000000000000..e4c34b37dc15541fb48f4d291e6e6d9f20461ca5 GIT binary patch literal 290 zcmeAS@N?(olHy`uVBq!ia0vp^20$#p!3HD)*8SNCq&N#aB8wRq_>O=u<5X=vX`tX0 zPZ!4!kIt+8)?CdFBCc<@2sjGp#I((w>RrbEEi*?a$LWL=%k9a}Y>gHe|2R3}W0=zC zced(Xerzimzr5LdaWd1_tMe8w<~3mESJ-`FvgdIgll>_rz1t2PGH0GJ|Ha9tVVfD& zvOl?6aNO##AZMezd6m`tH&$}YdtDdZD2$JIP_6JyPeH(}^Q5Zchs*H~c_pU1?EJWo zhwWa&CQqBjQx)MS_ZK|iwU~V*`hfONKK{7J6DnBw*B5%U@UOqAD5$sRn4+NFo;2r2 mb|0jiAJu)3QeYICFD>7$Q2(@NvOYV|e+-_kelF{r5}E-0k8u6~ literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/spider-man.jpg b/src/main/resources/icons/spider-man.jpg deleted file mode 100644 index 8f11ab621c809134da40cdd22e40e9e5b20bc152..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 242839 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

      6X7skHgw$W%GL{U2 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%0A7WVILR9V)1`dQR(-uNNLjs1{yun6gNT||lpZ8^P zXq@}Odb$(;05pqaVye4D74<}(GsjpQWVPHS$nu}NOdM2J;f^^%DnR>cW zqd1>e)9-pzuZclJaC(2lY^6=B&BC}%qrcN%Q~a6 zzm``NyJod|ohMaIYNfC$y>y>pKAJis&4Oqlt)Kvu+b|xHRCSg&KT(FFkV#xH&tW@I z?Mm5p;0%Zr!DeEduHKjb0K>8OG3l?Dq8MYw?~{Z6rdRik9`R}YufYm1e5qFkbVP@% zS8MD`zP~3BqfeL@Br(Yl4r)F+I)AZoGmWK>OzhJU6}fc5tf2GX+FihtFcz^rWu(Iz zk87^tI(|K6XvY&2^C(v&5YuGTOq-$`iPS$ND_9~fsgFhJ4})BNs6ZCt^efXy{m3=v zAMRMhWV$cP5MBn=qaX^I(r##IHZ^m~w9?F5b~3twcp=9L(zZlai{NT&%a^e}AYmj$*NW zSsKoWa}NTt8*YRN=P{e)TtP^H#bb!IIunXFD0Ma*_JXa@OeT70{@GkbQ z{{X1uh^(~`F~U2f{-hqk-e>*46DUiMUiAePT=iT?ZjULL7ttwP z4elB}98$VsY31p&#Aqzz6X<$h(fnScKczayEt+a_+oL(6kC!$!625RfMRknZ zd@u6?6?o<*qW!}Q@i40=hBz9J!o&x7>Y7?GX!84hf)n;WtA4Cs+%~RCi+D{ zlHohTsX)13b_eEIpxdw1w7=RvU11jans8rk6Z^Q(V(W`pE#~a2cODtzq__#QJ)nZ2 zqsnM=<){_T`T3N{fH@2z3N1kgGRW zNC0YIU1^rC{{X;XziO8eOHS@SgXxY2T&WmU`Y6WZh9wuQCM7if&Z736>%P?(nDp~R z#fe`2oa3VX`I>@_C9MNcYF4^nTd?o7PEtqvpW8P|!pryq4R1TVJ0sqed0kWrS9v@^ z{{W<@n*Cuo5L$&|RQMrM;+O*BchAnEt=V3<$GDp|P(qyi=X>yr^@ZYjCiZnR=7(4# zjd%3w+nL81;Yji%^7RSZ=h-!y_nWxo`f@Y8o-?&?f3 zjDKHOI>&{7@`ElZoqjr)G||8D6~5oM{Yvp`;JzYwgfR0f`ZFpqar#uEMH(rOtWi^> zOxowtq6$3s#C0~CYrD!!JM?c1yKP{fW7FtLTIElx=v3EN;s%~mcaOq;SU&Uh`@;q~ zf^sfu6vlkcjSu*SrYrar4#>o6GU7C2yg~j2yb$GdO7*bI1g~~f4HXacu}t`ZQ+rB| z+K%SB!LjO#4WB&2FfK8~u+gN~v{30_bUT+;+!AEWJ40d4lc6iL4tP50F+PFl-A!ZR zY|gq_eQzv!R6Asja~ z+9=ZE0UM;Iu@fB{baaF^)|c0->OrF=*Q=4t=&3JPLB!CD7ls*Y(JW4}D;N|VJr`MC zsEl%vex8v%Uun++!-)R2UzjGA`bE_eoXZ_V@R+}!=Oxzu0-MH0V1ZX96*)Yu`!YbS=Td|5t%uDC1ui|GKp@y9y3 zbNiL$R%A8#o4G)4fLIM`xUQ3XbRj2!{k>&G9GdbHjnUN&yKAf%UPr{n0@6~T!bXQ0 zxp>y@v6#6Lz}8)*!p-R1JamZN)wcNVd7oMn#{LYkCH%2t7R!6;4#3ai2ZlSqHCxd; z>E2Gn>7B)Ffr@C;2ZOtbaS0eAr9-URX#u!@MrH`Z_G3q-NL*3N@du$jkzBzSRB|o_kW2|0gG76 z7vghL8pt}eGhQyG{{SL1iaMigBq>5@GBgn<)_%K~D57!=hyMT@8$aPNlUtQjoiXY} zgF3A-7gealMJltoXggTlBzKuKH}w}8GH*ZnO;z$iE?@#%4DlGZM{(sWma5(l^zrHX zh6t8=AnEHBD?U@$2jXJitTtnbaWJMJ(Up~V6Gfy4LH7>b&L(b+{pX~86Wvu7KQwse0Ijis&xFvcH zud~cFM63@`C1aLuvlBq;s{|w#GfD5(4}_(2dwGDluw-77>iRtd%Qf0_Yqv>gwdwH^ z(|$8FqPV$l_)CfSCF_D&{)|qf2}O+DD#mF3*+howMgxSP9=@9M24li2)IB>1+d7{Z z98iETsBElY!(KUs^mBMCNfv#gg)4S+G8TZs41?-0qdWD- zFDEm#9R3^A6QceD1EpTt^s34fVd{*^NXMgPLChgFgS(h$^q_iTq1kmI10du4_Mf67 zC>8k1lSkGX4yG!yP0__I(a=?g?iQA5Ie4zkR!` zBr>zlq9mYObB@ix0r_cO$CzwTP;{I2!W#~Y^2^+rxouwNTBXFMfWDA$y&#gOrv6;S z#jC8chAi1+w!WT4b{HW~g+3zjYB-MMqumpN$ezIEs=7SO1@2Q@Yxn;E-{C?tIcZpW zM_G^lWl2&7wQs!&H2Q4+03Y-wolIvTSHr76V2#KtavL=edO$W{<@A39Kij-}eoX6wo(PCXc4wL! zu=N~GxVJvT@dA#na!r$z?ZO*hl3X|c0NLB|2y2n`M(lxU9inm9%r+?zc=kZLW;}VtCaUb81 zeygcFkYVG{L?;t5sC7Z4X@`&QADAyM!o%0?j70~FkEiw7FWPt~f_Noc59_)- z#P?#Cl+k#!f)DT(alrYAmGP=sO|30zwGa>%skz-w>(u->twvr_2h8wC{$ycbIh}Os zZcsiy_c_fT3@AMR0BL4u?KJ?JK5^cI^2K^rZ%Q$($Fr*aOzIZ|ao`bRaaD7OZ{Aa^ zR^3&_Z6!~TYl2j{gVFx^p*7aox|pE&zbs? zBh%!bFOqovNZQ4ke|nm3vDiNtOZ67aMrp&Je@;-Ab#HzqJ$@fcJwXeuaf|e`_2wjTN6;w66rVHQq8eUVL#elf^0n1?J2 zE~~_NSE?(Y2pn!^is*X5X!!uY@Xao2$gJ$gm~0^j@{tz zwuOE4VB~V2RBPI2?!?tA3kjyrc;*{HS7$P#W!^hPBE6E2fD28=GY`xe%*ya)u@R2E z1fnOdx|BP&3{J~t`bhSEf)jlmBoMgm@!SfOl>rLiXCexYNKa^G!#C|6IG?^E0Lz;U z03KBtwlb?!67H380Livh=Nb0&`_naWxIcs@voGFMuSSVk)C2v9tfo)ksTeK)0BWAu z?Vj0vFU($uB(epHrGFBgC8p)-tFaZBooI=pJWFTBv;P1WKN5H@1nYyuWAjnVG}9#Z)?V^a2 zB~DYtJO`11_E(d=yvlWNY0oFq))sMa`G@VKa3J-v*zExqQMmD=vBuQL1*=dw?-Ts$Ecv1g1-wr%)=Yvx~UA;d$dw#4k?akl)r zf#__8o8UaeFxOPZs=62<-eNP8eYup7-4)@5G3cKVn9<|+5~a76Z^<83hHp+zuw+rb zF>wwcE;%4Wjv%4#{{RJ8W*D7DFFg4?M_L#-Ov*tE@)!L68u?GUrC6{sb*t)Eu2-&C zu2IL-&twF+E@R>rhnnIloDaVdubR+!j@&2@dX|vi_LtEwqGDnVRL213&i%je03OI@d~e=O+bt`SHf%ED)Qq`FL_*? ziszMZ_TWh3G|fN;eq@8{I1B#(MX<*(x2aPXOfFUOuGojathwY* zgj+dW{SN-1fvwi&XkAnk(Zm%)EaEti9I;&SEr$~o1;lL%!UZ6*`3ZnGjC?%H3fc}g z&eyEAeON#pHT#)9EHv4+{=^rSOZ_>YzBWCv$LELhGB~A&P7d+=fq!0NCjI8& z1Sj(wZjY=AJokE%(7|om8GV9<2dZY0w#3FuvM+sB)+x^Bz(k{jf|8i;E7_Gq6AK<8 zsHa6}x{9!@IdOF_Q4X!KFR=##Ipc`3)M2?|3d|#~%(`|L+0jmBXSH&rbvTBHWv?vX z;F}&lqGgK0`CyOwsC{+sjNDT_jdfRoHO0^Y@J!)v;FWlOvauL!=271|)gLTsFe@4pS&@y@4cSYd zX^+sKiPd4fxbrfr1FzvLaesjpE|mL}*+WV|A3njs7q`H363gm`p^x%@wy!4O;@Fk7 zEv=+gpz?QYLEiiU-~RwRLf#!uzY`5ja#dV3M|@c?e=sr!AHrgAjpHv@7dZI-Ji z7leH{8FppppN{58;S>m(c&?FCKq#Aba}gT!>)FSWWCtC1n;Z9^LWp-{(G&t}t6>^)D^_Hb+h>JPiaUR0v~7zj(Yc z2tVsQ#y!W633BF;6s)8tZdOVP3OyFkUJZIaZ)Cg%-5nuseRl9nXRgeDth#ETdWNUCqmC=MVWuZqMHVX#KFJPu&6k20T_Ps zgY|RRQ_Ria`+Iqo)VoZh3aut+Q!^RkH|v#fSaxZ$&iIRP&%(@ypxhT;VF>RfZW`Rd zxDPISvZen3Cz#vT-H;j%kUd+K6C}y)xNNkmj`))yzTPEnLUoT%&|1S zWu^kX7*WX^n&sQ#8M|Epf(dd`-|`AJL|gQ@zxGW30JEw^?~52p{S_94@jv9#ss@)V zWh_V3Ervdte+~G+7r1|dS>2qN_$6Wxpb7$Bj74k_ptgVQCQAC*XKsaYSUkiquy2+0 zb%|08ZI@Dq3I5=Y1-Nxzz$Wq!;SzlDcgh-uwr8&BvIfe6j&eUUFsdtEn*75trVO6u zd~qMEt@o5}_;W4d`MMc_=RQ#-{{S-F73~E#9eo1k944X)k*+Elx4_j^9L&qwqqB;HG&sDcF}ih@1e=PSVcZxTwB;ES;MVn5R*@joR^KZB;EO zH3eiT`jraTB3@00ztxypu;IPRk5f<;pte(!(S8iewLcl~nEuiH1X^FEOsE@zzG2=E zO6v!q`vku5A=}Hhc|oK$BQEAIM^`f3&1VqZ)Gl-$c2{V?j;_G(>4{0T_Vy*8pP1L<1v70pxKphhZFpxME+?K{!{y9=*2n@9_^IDkzuKJf< z9MQbhTQs@jr(&dMTMp4s71kx@TDfMY*mY`F zsNH7KS=Oc5u~O|dw>)}WwH=dXdy|O5&oJ%3?-KAYWYX7vpFu*?K`KRj$6xaduU#x- zc+97k`;xQq3Jh{F7?(b^BA@hy-|O7O5~He@gq4FSIzUCITb=4!-ds3>%F`WtH0CO<3wr~k(|7_>c~f<}VQN<{ZX}W1XVI8L z)TL%Z_=eX#dPZc4liyeVeLN$)?1ySsjl6Tzfo|Ue27E)r^%~nC8v?f0 zvxX#7mwKTX;jcmISi3(yi*ACt`UWKy(aRe20r$il+Vu&Qu}k)q zciV@mB}Gc+azWI|4@-k7oadyaJ_V-Bc8ZdX5~TAAv;P1H&YN1f>)3{+x6jP)q`fs1 z(Y@jbXNvn{Ldzl3Ek$*!UCUkIX6cA?22ok@Qt=im2?ZhyCHULG(SjwMGYlR3#z$~- zQ=FF0AIJVna%Z#CXsm@p$d6C^o7s9F@>!#qa|K}tega&$`Z3G@00d55AswY!g503c zaR>)Uoe>jGy8KZK^g6rXHXSPUqy^oFE2~=kM@%>zJw4^`3*p`UO0P}AM)E2hd>A|I z=TV#bZ9ikY8+BgVUOl4HR$I;RAo+%6w;PuSApWI!TQTZTe8#I~9r%LinCOe!lXvj6 zL9#2}t!)T(Pz{01y}rF0n*{I2pwy?(HAU$X#C9h}CJU!uyg*#Sw>^=KrP>eI!iu{U z`6mnaWk#Gg7OFecwrT}=OXl|*=`KuE45+Ikh@3pG?#j|)t+Vazge?OF|gp8P&RV1)xgI8#78EOzcH`_54VTFHo<_o>P4D5ZS za@7v{lji&iGEA!XxTSG3J7MTUo|3e>_m?ZzkLEqCodD!fiFXcii?BSEEYi61z{bo|N5t%$ z$4b=3j-%}}>T0iWxbeZU^t=B6R{DacNX%dNm@WiY&-TheZswhpF6_biLN8j~3mrXB zhQ?j>>`{H;Q>>Kn4A8}hWV6upx*hO2no6a7uFLTnuNS&}`}!-(AKJgfGybetFq#0q ztU+L{9TK@_*;Drsxf>mjawiKZ?)i|iic!YSIvCwqy6Y`g`w^`=esH$KmAvSV4g#kq z&-D>b-4M3glmMLY>Uf6E1(Z{oy;N5eTH5Q{WA?$To&Nxw@BHF7s)GWNxjMBM#YpOGdG=-R7d4zZ}gI)A0?Rr!f~YPjpZNa-5kS=V-<8-*trehg)CW z9ejFV@eqbgN0Myw8ylg#FR}|qCO$|x4}S1GOzg}>%N-tjFrl9S>`Hd`V)-MAW4>N# ziC&64*IC^tI!ELgZf^S=yLxbuSLxm^!2bXjGV-~w%osVDArDXyNe zz3=`vsVxqt!6-d&-@GKlKP9iJY{Y$=uo+fqE&;?x|nT;I^ghvO*5abV;a*UTS zHV=j@tPY6WJ2>=qh~952_9L(yS^KGu7Vh&^68YC&(V`u3gS|vB+srt#Ihg=+Q=K?H zDi@u=T|1Pi#8Gzx95JrC74ruY%8vI}a?N7da*<}4meyLo=Xl`26REAfB~70Gxd>@e zo=<;>4$$kV@9B{MpE0>8WL%sQ2{CP=<6j zEftAIobxT8BSH4){$l#DjuUZGmD($$M+RE>6Nm|P@Rpz!anl&L0gwF2co{Z=e(67S zpSnP8A#`S1cOed(Kml4PvBcjIdj2AKpj5n1vOl;0=TS_vLTMNqaJs)T_Y&9^j|5S% zv96bhL`+e?IGP|h?cQ0;I9y!x9-R=abAOSST2TH11T|SpdxGgqpfan(JXin@rz8Vt zjhbCc3rZ~N`HFcseKzpG8K9yOUqmiRLK;K5wsAN~^`m0&@XNo9yI~i~uV7$u7FK6i zgx2#L1%a!BYmIEzqs-Tt9@6RJWWy|9O--K=QBsAKTqSvw?k4G1z97ky+}4xNY#J$z zx}0U`6(2ec%ClDvpQl=A>s-T9)-Kn?I5xAKb5kay?BC)$UQT~^3F`x9KPPE!C03!t z9TzabQHrii=ICx0smk1&1`A51`*97T*H)&d=vn#}eub6u*_LyH%m#rh*8bt0^=9>Y zjUc8|)7PI(z9mKdaUO(Jr&5Oz)8_}v3X!|RCp07b#4Ss}VUKdNKe+J#L}NT`t?lCBvQ3`@_pKS5ET#pwApf>AF44%F7Nc%&8o^U9pHc z3f0%mM=u?O!A9eiCF{a-Ey|Te=2cfugj#mC_l3KaG1H_aOm*))_tRdQeHYWN>-8kr z;$R@6y5UUTP+UP?BPYw-taR?(j{Lw-3eV*gfdtO7RgNLH!JfaPj)PL zVh)r%4`>+Y=C%c~@t7Lmmrn zyzPC@*uaM;tgYKZ7J;WZvp(}nxE$|1Mlfm6ZWx7)VL5n}3LCqVPh1fNca}wPYx}x_ zpKQ3{`6rkQ<*z~^T%$58+}`5)ZZLzQC9a6O8GBvX)G1M+&v?E_m3+WSR6gPYu)D)j zk}yi2igUL)w#afLf@%fBH|_Us!w zFd8(*z)ge)w*LS#@4k#Y5b{sGfQJ53)G&{JB}xmh&9GM^mhMow!o8<^v&`ZN*KTDt z;|rB8o19fTkS$)%`yPEU(x==0tjF1MHOAQLjL~nm{vm6td&?cBwXx!*S60CHn{Snb zKF6m-X!;y^66M%_5N5`NZ=A2JVofa_`^V4^-s4K(EI!eQvg@Nc2jqZm+srA~#K&MX zEp7(raK#PkE#_eOyTkrL44MUI42l&`XIKIqT00^bY&$CbH%bVMnD(2!rPEsM+_p8X z8Lv3HiKpFv(p#Aiw8kthG_lbKTC=t#3Xc(eGD4w)AH>{q(jsmu3jYA@DLJs#w{ZJ( zmk`~FSRj`!kUcawaUFNyP9j<^D5=?BWG|{# z;BetTI#kUfG}h=mLd!dkFj6oZHSx@@2~-QB)r|%}^E+g2qz2T>C%TS&x%m33p_37?oYR&V&W{=1^S14c&UY z$`KNdVeE7ltl;K(yvDsT(~Bo)!0_TI?{g|?a_Qm-mgRr+g_rGxcz?710Af%;#V1nW zrya8sOurvvf0)?NdW-m#T(5H-1hLXzv_16$mcLUKxTxz7dO~b)K490}PA((aLQ+|H zMl70h)$Idn=pSsXIm71aZwN5&u8eNmiqWm&S@X!Z;FP=}2eeXJvTUU1(iao|07+Y% zYsKH8NJPztsH_FH!AEXexlh~43#aKsc6YxqFsoQFUw5>0Lf!$ts3H#NJIz&5#@nb? z?zjH{A#{L42Bo;%RQ$M!U{!P)Ku!R+FA0e&DU5 zekYN=NV4|boUj;IE%V}SASpKYnrruS83Eexd-#am-e0i=jo}@7A zt>5RQJdmiqp+pNpSwilk^@mcn_p$kcE>PHfMF2`A^q6Wc@@V3rU2?Nae-M+#5Kyy; z-FD0-TcW=RO0?(u8ktWU{*rYQ)V~;|D5qEUl}rx?y36w!%qUdfb5r|4e)Plj<>9&C z$1MwFJT{Tjz6^KkE0&G-Rhg|<4DZ{}Po3V&8a-0}CtaLUVN0^S*_U@{tHx!OR+^Wl zu}8n~9JL=zo*6pZgVi^}H75hQd!|xoFYZ}uCLI*X!2#;zc9zZsfp%Q*xgF;fXj|tf5?$&8;fEa$;@Z7 zpdR0tKt#-c;tOt;+sQt<4T9Gq{{XnThqKDh-*tzfz8S(A(0Y&r3QT*S5k)fkYdkTe zI<-(_I~}!)gSeWDSL{Po8wOd{S=JR4QMvjjQSKhY?qmmb&Ge092J3A`DCIKd!eWif z6@}MNhT-Kymeb}VS6GAgIiK29wbndN?iyMhqNU=J#&;4}M=y-(G1bvsQKBJC=d2P| z1LW~9z_D=NSF9jB{V|xQrfsR8XnCj$qN-&y7qQYWT51EDmGJyqr$hjA@N^}UchFP5 ze}%l+wKHtWl_=zLaj2}yHUh9zTpnPv%T^7>Dev((q>05w4Nxz?pylxmiI)7awU8OUCsoP3ykQy4&@^DW|RVL%<=9FJdnLoQY9))khC>d(C-h4>a=dKZWl-j$w zZ0A8Pu)1!N_9t2Wr|5bx#f17ed%;HzpJ`d8WA3?N614j{oZ~)Na;Aj69DJ?`8?Asp zEGkZE3Ggr90YtVDK)joeap9cdRr!V|8ow~XwX?6S*i6WNL`#=9@vTK` z-35-q37AK$)Z8zPLLf(yV=b74R=Ar06pu?(;1!n{jLpEh?mBzTM0G*S+t7xFhlehH zgQ5-CLvrWKtO9|nvK?^-DFaRf5|Y&@=a9o?+ryF=Y+s}ZwKdzHSx;1vZkG~RL8MNz z@BaXJFZS^TXgVKIF;G($uWOV6fL!WhF_xclmaC34n1Gxgm?KivQxg*r7lfCIh#4Wl zLbHBnTkeZejbyv8Gmfb;!`XqUp`NVoFh@ZySo)W>BDoI4xuxS*cUZa^Y?d9b8-=~z zh^{O&cNwT10vY#53q%`q)Ns`1cqua>AW#7EMX?k{hgAoUh1oHFyun_ezi6X_Cw`L{ zuwHU6KXAA8aoTOBRAY?lUDAO;vLyV>o-!(=Z9dnny1r$S-fL~@HW!)mJh_-6_W=bv zedX5f#s)4k;CX_~IgCDQ1I*qVqyySrmjo|ye&N-B*$C~vt^H5@O0beevUN=!vcT5e{rVOiObRR&0dw2YBp`I}{A&S7UrS^4 zyCrS-pBNACnfl6#f4IyiyQ-E!oNRo-_E4nap>5U9pyd^z$?H00Mxe&v+i>(k)1zc6 zfO10q<{T^U;x$gSJj`M8<*+%bvteSy+Ykvo+j^AH&0E3_T>h~Cd}nL(S!o@s){B0cLKi|ILLmi=V~I+l-lq)zNw zM<)z*8AZL0tg^lto)`w4CBHK0ZnRA#=j||+G7oOEJOHX(39uNLWSpw^Kvu7KLR$yV zDd_|l%KA>nkM@e!p&S&pp?%CjreD~(goVp-iEBGv-OYPS5xOzhE>$#X#@s@*DTU5v zU%POu*N2wgGsL-TOU11EQAXP8$MKB8-!zkpsBUA@9Z-dJCzlYSc8;5uYEnOQI8+rH zxk7cr8mhn^Al1PdAg;JD1`k5Qj0|e}5K?LVOH;diR^Vy_SRu|&XvRxH198Ce_53Jf zeJ^!qYJ-#J~ryUo89H&g0;6j+l_K6i|q!z*jB<>fbSml1J^|>VM~s(pemttMKCOG z8(YU&!fNhYv{nIlGvJ_=btgK1xpA!j0Enbji}O;oLrXp(36L9}(659kKxG%KP<_Gr$e|OZ#mS1E@?%T9@ShX4#gL7P08oNHw zmlh_GN*Gl5in=a;5jjo}=$ba-txkEkRBL*~5nQ{yMM_`xFbH)x(%d-ketD?%lp+ye zgl{Z1J>rdB1%i~_%%8zj(W5g~-ZL`W>WAljBc_yN^C{tdbaKl$n-gh}O<_;BH$_T7DqPU03Xft?8^V9GKqaX0x+dO>qX| z-_^K76BLJ-7)LxcIAC-{WGa>UBKe)M4fUUjS;Y66m${c|KM38~Uzyb_9ZxsJZbtQ} z!fn?jfFGk+^D<0SgEi(@@xYvLOvWlB%Y=Ga+Wp6ntn_?D;zyf_gpUz{?lmhW`W8J+I^~7jyF~KXmbDZ!S&qx(@hshtf z$hxn&fk#j1#fC3bqsCSy*s@#Cd6z46_Y)efrqz;upP13urW*>gWJPMeto*aR)NN=P zfMBs*R`A1`fwk2k*kA+GxCpM!41x18lHnGv`{zk5d|^OjHAvz5*QQ0vwO&u|a*i%!6N@G?(kDL9q$d)&a~lL+frGKyQ+jrG z?1^skkK9Bs+}~&`xMoz%#+JC0ew4e}xkARO>QPH+els&Tmrk}u8IGB(++MX(_JK&| z7|U=}zPYxF)xq+?JGbT^`+w{R6}RqSJ&XXmoZ+xA4bb_=59Et?iKCL6P!1ScSbs!k zqE+FVX+#CiE%Pb()wcQfiuKDTzcZ@LbE?MO%KED$yUE@YRLWb>R@Ug7oWd~tLj|*udF7vJnxivf)CQ1vYT{cog&$^Ibkr3YPUG; zxWhf>5tKcz^9}ab>TrR+=02wT+l6S=?NC~tKIEcgF*SJjAPNot07Sw2z9D*Ubmt^| zP?L|$_R;31CjqusHK}~ErHM}RoMP3OclMjT1^Z{^esOh{gW3M#D267Vu$~a!N`4j} zATUKmOQ*cXVbvd!#+{0R4uNOIiE|6a%4wm7)#Dx>&a;T<^9_Ed%7SPKU}oszZ{a8n z{z9fd9K4);O9gZhz&w1pmt3R_^_O_4J&#lt*AZ+)tFJPaX)E4gHWQdzx*GUN-?Wf`(zWtYvz)0GL=x{{RV>RYTk;(SO$|*+=Dxe#=y+ zrY3LChmb#W8c^h8p8QKe-F^i#XC$cFAsAcBW#24nO%rHrk14&o!)Byj%cfxVz+k=5 zOP%AacXRh~x@FTU5M7seWt(e1>Y@6gcpfX(VS$dN>6_fRl)*`SR=4jL8XNm8^Faob zVORSQp`{3BAq(*w)2<~C8g(&WALz#Qew<`xSCDL-c=2$}OnQKB>mHr4G<(Nww%cSi z9KPZjmfy5lqFHspFufa(s`(=z8lJdk5z*!+6Hh3%+^7~`E)x=0{t;ybuG*G;2Ajds zBJq6}`~`jGk=!36pCW`U{{Zm)?(=`4L>g%2tChC)KG}Lb-UF&o+BSRcPz@PhUB?Y? zgMuZ10k@G{Yq|9mZ`y5Pw#|{C^gdw@+b<7uaJL5+ ziP@B_bc4R|80gC2H=f<*AEz<>uA~N99Lv7PMj?I{uk{(HB+(pt)bV6dnmO48%+vzO z?Z0@JS9WgCNK^p8BFL;SFZTfpV-A`EhdFLz7dW|H9ZC3~%m_M|R~g+a?LYOAQ=uiRt6?hAs{DTii}m>*i5AuVJiG5x{{R6$^0WT{J3DWLQC(Go zxNZs>G&;;fxp%j~4?pyv*t5=4nGmf3*dTIUuJU_^+AXZZRCNcj2Yvu=B z13XWeUxMk6_KP<#KW11cxXNb7E{gSx`#rw*Gw~q2&b3$g)v52yFANJ z;G-2?W$BrBzXbh%`4zcK={6&->>Z)zZ_w%)BVo1@!xD#c1k#R*u)c$fh~^=LsUym2 z;bG?E;$FxS@}HWIaswIs%vDh2GlbbTT@xvJ`y!57*;*{UJqPx9XXq9O z41M8LzGJ27D?~XtI?w(Z9FAofHs0rQrt(B9Z8?zBausV6W>zm7N$F=EBC6o@P;y=c z^D#9T9guUP&JA_^%!yrfndJ%YLQ_hThS>e(0*9pi*G3>tumA>w_>P_@-F_t)xC~lq z?Cy$=w)2%Vih*bO57FNV{!iLJ$@@q74;Fbn8YZZPL{wW7Ik8(VWmlg~Q*sX-;h^v9 z{ePe3R1OlcR$h<1hQ*9ssA(++JBRf(aMz+v7jdir6+^uvyW$o*OAzHaNZz@%gRC*lurpD_GJ@epwb ziH^96zu6ukQv&2-ZNo21e2>sPiGc?}{thP)&*cxgf7%b2)90pO@gLfH;Kt1LM$bj^ zKfqMx0D2CC{3qfMai1{HlN3QexCAg}e{e^DW0=R(Chj8uX2A&NVgbng1N?u>6F6z` zM@HfgJvg5c{im-Hr>Y){Tc+MAom!2&zX)){-Vrz>->yw zi2Qhe0)G)3+_MwnDCQq==sb_mKgNF`n9EFberoJZ%ll}_ZjmH`7`1tiT%%xpUlSoGS3D-v`ldr@+Nqn5P65(dJiM?5Apv1$S3lj z-2UT#aCw~c5&eg*(G2xNo{QvvjDL?1d(8QUeu3)% zGd#k0iQ;FIGsi;-<}-gxawg-5$2jOb37%$p50CL=^vujYAw0qEpT-ZFXUradL;Fu% z%Qq_wtUU*j{w#@+GA2aGnVA#JXGIw{eMkQQtM9c&C)70q%^KRT7z5mA%xrFl8=xW~ zoNkThd~N$RcQX|=@FZshuv>+fHv zQ0JVh`*FlU2<8q4O@u?7h;x|pX~bcSbWt{$wDcbxK0m~9IO%7^8P6R4h4DC#^%|2Z zwtYtXIF;(IQ9GJ#O6+U&&rg_-$TLr?SHfGgO{1x7c;06DxXs3HF`6*PO`O0pJu}lW zpBm=HL1P?}dN0$)(Z207)y$%XwDEeaT5YTS&BiO>#0O~cmkkD!p0}g~{Z4>4U&7{qzGmvJzSkxP! z#_njdxR8!HL{(fay!%H$2{;`qcHA)b2GcVDz<6C=p?$hb?6nT}*VkRFj5YdJ7>R8x z!)rIhA_94f)93toS(W`4+Lb6~x9U|NZ~Bed;=4tNtfcVMUtU#h9TLO4BAdmW9Zr~C z>KMVnn1RUhRaoU>v010Rs^uuPFI=O4?U{IivT-vr#N4nji2IBpZJg!~3?$5)&Df2L z6_D7CTe7sK)>Z^jYF}6(sKMi0-}w$YXh-g`hN|r0m|=+NTYvcGhv%k;ie|8|)Y}8J z%KWW`eXFayc++YU(p9gx(yGcnpx-}!G7K8}<$qAqu~nAqboDkH{Yyzd)U=cROGy{% zIztqr42(nKYV{UU0Ymi-MEl)IB55^(@q;x>+!iFso($3Idqm~A1`rq(E`=GC^jGnk zbveS>s?+zqLj+_Pd`}%gBavOCFlDDwF2XUS@FG@sW)vjmZS?J(lNPc#hzMv0nTUMF z#07brW^n;tX17haa#^jOgc@{Q*+y>|O>g_Fe@#xo zff}K|NX$8$&Bg~aSNoJDqj8(e-e&nl&she&HmNR5!dDW-SKg)TDD0+IO`W@b?T`a8O z`YY##t5V5NgAiwYAIfcna|8f$FlF}FUoEF8Vg?7|eL|p@WJM%q zU2feirXM{jy2{r9^-lG&1Ig*BUfzupu8;5}*MGXVr_=fFgJzPu2N<6-PIu`_byRlo z+i^6XxlNTZ7$-4mvAwL0ht)Lqn%p!Gks52N+@itJAH=U!-B7QuPh<71z9l#J>*bU> zvX@ef%2l?R+83$YkAkjNQ+cn&wI-MJj_Gp0XEC^dk(~V8*yVz?uRPhP~u+f?U#zBj?t`aBClL^ z(Nn(7=U#tldg(s}GhP1b-k(qWtp#L)My&$hVCnSZ`2^H1+w2`ao_}{8F*T{S;(BP@ z?jYMxwF=J=92uc>H4x1DQRlrzcFU?YTF81U!*_R1$lYnNfmR6W>KS$Q__P^Ivalle z7m!v0SD9Umr*b;itW2o{5(vti%4}sjlB*KM%EP0f+jK-wRbT@#ii|)+%E18j#W~Ex zBbYNV#1Sm5sXHbn+0Qc_G&CQ|+S8T;pc&#f)9o3@+G-_w0(r0nRSLTQ0Bv1#pMr_5 z{{VGwO$X-^zZ2?9i}Taz?Ttc-uUuWyrz*gKtiDyOZR@{XU+o^1G2AeAwMlAg02-Sy zG^^c9kmL!!nP~6ngZ_nYAfG&R97-LUienr16)W_?9Hjnfkm z4>*ALX+l^PBp(JYrqxiY?YQTrfezamYpdIKdT8$3U=vYKYSIM9=vuq9WJNnn(_4%o z*Hy~eF$bJYKAx69#@#J;D8nNw6>LSza0Z>PMK@PPb&M(uc{A1ULvIjg9N@v`W^tGR z-#@;DLFRGH#9_=hZ*ZoF5kw*$5tyHe@iZ&@gvIj*#N#3<;LUmc-k&B=KW>?={{VAu zO)16IMPfLgP;9bvP7KzJ)n{7hvdr!KJLZvW?b@%dOk2D31>A5neY-0mY(;Da6`=43 z^{A`6S5>WT2L(tZ8K7buiL9U_Z(KwpkZNIdZ&Q##ApFfem|#q@l(}}be^37a>`IXa z6%+d)3Ct|ni}NsY2TYhej+|~WK-~}IgmEh$w*(40e9ftzc8C{_m!YzoR6dtbIgSYP zq8^J{Qn?Kkd`xn%0fRN)?riD*0BKi>=Cl3Hy(-~>pLwRt)W31dbW!S-0K&!VqC0I9 z22=S2*H!y((0oFyF_}%yt{o^f>VaYQ$rhAf7B0&;)zjOzh>SttqG~G!Sz95)X`2=- zDy*ei+Ko<7n2d6yYIeTcfKuMt^mmbhiRb{ToWq&K;v7uC({?8R09IgrV?Hqp@tNH* z8*t=fGx>;EO8)>H!~>b>oMvr@;tq@DqgFxSqMtprQNpoG#2*sI+5=AU@ikoPE|{P9 ziR1~a{{VAr#2HhOy|~HAn3_c2eQ|sfE={*Mn3ZH62h{Zg&-$x;vn|M$M?#LuaWwla zPTN~kRNGopb1R2UHl;62iF)LTeO=m$_0!w6vR*y6BCWmKqhfGfl>ug02zngAyX^-bDZ(&$(+t-t8_mT zc0A`Xf0s;t;f-4H8n2Fqy~{fz%(q0Y?9k{f9vexh8={Q7))DtM=ZT4lh$HL3fG(-NXX3^G@JU>hrH+^b^}iYr7^e#by}4Fa^vYaX9$QV>ySJnSq=|`Ir-o zMFTKm3}zpE=MhU|5m@9v)0vzYADDh2gQg(-h98cL;B+1gdiP=)nr&%N{mE@fxaG8z z7<3f;AE%#>u)0&DTF&E)nP;@kYWgZ^xn|{=SV)>%g?6J&b$Lg`92ul-`;c2w+PE&P zj8(R`#ciopS46Y9v8JGB5Y#PFjG8?6{p>xiI`g;wc#FGS53?HJu zU1YcQGXRH(Pe)C)<}!LyJ63E-*s6WC5a~4W*V2>hwDavW@~SGXz{`C<$S4GJfdDZ^ z3QGoB1mw-9nebx{Oi}6(DfG|oH-SE8pW0*l3^8VTjpR&0j7H?~#3K*^%=GRtF_=i7 z8JmP$7%{pp&qIvCpNyQ$h)+BQIfVFx@gL$>;K48rrzJ-afE44W@#sXvxZ@Kz>a(;R zR(-m`LV^s``(l7pL0V4LbF`DRld`k6yKy@SI|(*mo}ILq9UI;w`HDxX76)`+F}>y~ z0jBkX$(H#u=4t(=KjS?F7|dWYAb{~W=4NLw7&h)O5Hk+R>BPcC$H2zvo=;COd70_* zW@q&X$e4&Rx&y=q_Y5TQ7@Qfn=48zJX5*7GX6bSE#8I|bqZ#M`#2ElgoK9t+dP@$3 z!QwHW5oURec<3UYV;D~Zqj?i(^9ug}NP=wy#R6>*<8xtw93Ppd_L*m}!w}Eq8_0qV z7JRXvGX^L;z>T=grvyV=6POh3#MseJIDq=6{DL0_C*y<67|EPZm>F{k@d5MEe6$dL zLl4An2658^265Agf0dpAhm}1Cj+|l)%f|@>_=n{GID3nMEQz+MrJY2AkHT-Ob69J zn1#M2e~x3BiJ9{-GY^qD=)PJA2ksa^gabA%VFM5-!JAhz69PK&rUB802@rUT?GtFg zFdR#PVyCC^gQE$%h_HL33H?Ql7tDA^A|G%w;F#VF?TkSe<`I*bj1nV^aSYRYOtaWy zMT36nU~G(l6Y|lhZty52_z{g?KW*Wb$UxNtiPn;%zyH;$(^H zF-N)s2787xV8Y&l(My~d&CIdCX{CG0tT~*_!_2`Dl6;3CM6rhvoq)*iG<%%7&g)L= zPU}wVPUFtwLwhV!l?S2o(;{SaB<>OUY)$RrcNoQOu##I460JLp#Lp%<+;bXf{ia#% zF{1pzvoPa4E;47tIp;a%7FmFE7j78AfjuyJ>64QNcSG*aKrzHzhx%9!ih(2Q+ z^FNfR92nn>#)|+|j6 z6FA}@0x4dC5zGU`1BiV>e%FW!jF`ILxT> z&^CG!Fe6=9@Yh;l+NB!dI_ zhvE$LIg8hFgN}@LsWQ7mmLI4(sOP+s#taf*( zGl|DTD*S5)L?j8^Gq`7P!e;6777TMfXBp&e=6_uWh&-ORq0Z^2usaIN(8U^Q+>5FQ zR$7`J=8osJ7`d=~cN!wI`D(W;WB~08UE{*uqsX;i#6BX}wblu+mWMBt1PI%L7%#0TRuGca-? z&J3$Pwdu?rKS3wsFEPvje9kjIA?KJ54q|Pvoz?j=zvVH>n3x0*ZOt!(r`Kl{*=N&k zEcXpXHe3-|biiN}r*2} zPr#fRv_|r0#MKNaFh*j8Fs0VS4K|2C#M66P3Xbf&q?F)C{NXeLr-xthS8%4X{31nk*6R?U{r-J7hN-fwA{n6?%1 zxIu2;1C`wm$3se7HnP_A+7>OgcD4Z(7FowdV5sx$nEp&M zk?4*N=IQVv_%SgsXR33#Wz{gXo)bH4p&O@dz5NYj0MT1(0|kVM*hmw$cP%b$B^$Qi z8!d`Td+({${X1-d7?2>+R89j{0U+(FAg^Hn)DMo#TK1gHSLP8tlb9?b1DH(t5&4f` z5mS3|Fg>Q%Oth$-q@92KQ&Hxz6n^5zwJQs|pn%MHnC1v)^&8sJcp_QsS*JL04y2KZ z-E+iBt8YEJL4a3-g$N8SW_S)pa=2rOXSl;C{BZf`$^9>b6F8WdpUGpkNz0YmVvuV< zeG_uy6DbQR7@QY9YE^pxB8uxEsq%VPNVhrEdzYg7ga|>&e|vP#9Za&-H)PRGt+>Nk z6BCg$7n3YM#4T9zzqg!j{0iDMviOVjXCs;=HT@qi!=FmO*x z^?G`ng1G>K~dk?1_UIaTCPC z?vJx6la0$Rpk%VOZWNN#fQVOP#-wb_?OPh0B9Vs%D{wQ@2mEFe!I@{k8hTcmf{ZS6 zI5T3I+gQ|-lK>+Tj}TxiGQ0si%tl?xR#phE>WmpSz`R0|+Ka9Q7K8Q1kDy3Cp zCMpFO25G$?O?Z45e-cd+XL*CBA?UosUB+PZ6w4daJ|bv)wiha^SEYBJjp8SX#^`qB zL+(iV-f;W2G#S92sgp4C4KUk`!RffKBTa3MW*GTB1%*6n01Pn~mI5V}J)t9vI}U*qJ2iL-lKRDv+AhD@zJGTo9#}O4&i%BQJ z98Dr^)1)IXz+sr+zzz#U^r#aalM3MRRV;F02YFGXIY$w_#wzEb`HAhH${tFX$1IY@ zv%bJS{iZ2=&S$T9guLGQiKw-F4MVcFWdk-0qKw1j^cw~@YQ4+RJ?jRJq;T@z%Ka8w z@%mR2iNy5B8Bzh~o}=_@Deq8L1HgddpwTd9IWx?&i)b@cPiG2>JOVlt7dq81ea_7g za}HdEm;-?unf)G)xm*pH06;!V`Is2vls=oai&cHY^zB}C54eGzhd&?QQ^dn=gf`_d zv$kih6H@aSe*|h;y0hkDbXaC@gHd zF4K?dnYhi(8zl}=X3+>D4*+FN(Gbc{l80vPW2=(|6awf4vz2PY6zNx&lngARA87W{ss`HXn!cClB5 zd+u|Hntf^HZP%xj*4{j(zO(N~t^K_d+$OTT-(#+w#(1+$U59k1OI1fBqX2RU(dqX~ z0_gNW$Fnn*np$i39*3coxlapWQ<2;|vlmcC?ETl7UEIq&m}MjO9=_O$+osTg-ymkPKK3xH@>pdss+3oSrqVin+sw_?P2bD7N3-YPAd?GC_<#cVJz>uDlb>dwS& z5qXyUNA`%o0XB%f(FtsT0dqfcC-zRzrU|L|m(U)N!m9<98K!(>Ow7hAtCMD^Rk@+v zb9yS$u=ni>IO?FW0BUR%)->rY98AiQ9k9nnRPL&)+jh$_(8|~aGPVHoTLAekL2_aE zjzPo&GX`xkvQ?*a;KZ?lfi8X6mA=C=4>O5oHzd{9hG&=gz_Or~D@+(EUdVa2Js>Ku zUg$Xl-c)~;Wxo>NVC!G7M+*Y1J+b6D0~3?xY6e2U97?oL+$|Q6IVTdw3a_^N828l< zj>l^(w@;%LG zpki}UhI}2Py|!ARUjpO`Ml{C`W}xPoOL`DSKlwsuD$`EbtW96=jZkzEM%4i1OFhXo zY!8lt0PlarffY?-ot00i0~Ayh`R{{SBC zrXT%*0O;r&5LSbuTA(o(*uJpWl;pKm_Z9#q?lCy%nu;B;xSL=c#`6-}KJ}4^mjEru zt%$)0+kjyz^p{9snLLOK4;eEpnUeu0&Yd%smyg+rUfe>t)R~McN>ldMYX1O)UucB$ zGb}k|m$7YD*=w_EgMnk@m~MEk*%P?ay4jR!92Hn?rmx<;6W+3| zMXjh+HjmtXf->%wgS23V$0u~pTugMxg~T>;1ZxJ+TFpUhgs5PWY#CEDjr)wSLJm04 zUu&BZ fdV~q};x{+*h^@#fIGRFF!{cHM-E$Xr!grfILNtB*tKa@wvxZ%kxC5Jm^m)O%$__`1GO%JuDKZz!<_(9-9oXnYS zcPthUx_2wJw#HKQ*0b$eZ2f?f?Z}LoMOoYD5eZNSLaxZ$bXD57dYjzcG^AywtK7W- z!3JsUdoNKg{IAh^T5zH}uuf>rg;#PbUp~!MTh(6LO&Wo6)0oOuIa<($o3b^y+qwNAV1x$H>ZO$O^7HZcF8dTenS{4d78|{5f_+1@!{_Rin=h`i(vnr`# zn!1g4FsE5nt+h3Jj@n^DGS<3f)Y6r*g`u*%`)qZB11oUo7()^Ep;=q(CL`=^)3`gE z33E6E;M9D?W;~x#G@czT}#K%Isxr=>YawLb1StmuDFJ zW*%CmToR{i10C7xKjjdW9(5m%D&`6$7(s*Fu4?|HMZ5h(A(tv;Z_MT_D){lGaTJ&%)L zrn#xdaT*9FowmP2jvxa(?HlnO25~K*ArRD?K?zdeG^Bb=#1mU-uE)TcfQU~`8}3b} z(dtR5Z`@%k@_?0A03OwFDhM`?V=Ax!_S1C780ZRv>Znm`Z3oHdy}##meX;<9k^~xj zoCXD0j)Gg_YAoO>uC0fq_bn@vcB0EY+rO;z&rV%p?5VNF8)GN2j_ zO?fF{e&OHID|-*7y2o@Y>brEN#M38}7|Ya}vlQ_bFOdPtt3qO#pH^WcnPr8@&UnN| z@FiZ}f@>`9Y8=4=e|$pA0|q=)=DPuxGI|&m$tECW+Viz$($l>L<5_v#6SZyuh-!8j zYEUUXE3dfQWja$!?uW_flr^~nSME-W?h#Oi%(wEtO@*bk?OdHY8!aqZS$Ks)h9_5f zvcePz#NsLzI0B3rwsy~niHJY;^kcvfMKGXcD>UTMsA`*7-}tYOr_h8mzN<^zwR?Z* zwuBD)I)zj$b|30OSgn4b;Ne#S0i3B$IExd^r1=n@Wef$QBOUz9aBMSEjms_|rEy5@ zAe9le0t`M6L>oq7JrAkC=y_bjY3%HRU4czP20ei}fa6Hr63nXP76cKzn)%GG!&`bG?w6@;i9Ra}x9dI$*bFt{838}j`V9*sQmRP*aR-vu1;WYLc_Lk4HwtbzP z%8?O3fJ_YX%f!eBF^ZY-F_jX^xy%}R`&l0`2*lNC7WLxx)gt!n(p^$g0UDYmwyl-y zh406vs%^JWS@5d3Q%f)eGCKK|j9N?(9z<3n#e)ud08A*u$wwY`J~K8xGXgo95&1fJ zF^z@?=LP}mV+=xZ3fZ(p86pwtTmsW|lXJ|x%R+s8mS6IP2Qbl7urYD6STo|SGA2Yp zgJ)0bUFGNU7{n8tL(}39XoN=G697f&c80rKbgNF34XX|_1bEPRgzz%d(ne;kl~qy% z&oCJIAxV`UaUACn&O*jz>>z;pSVU|Fnos8knPA1%2zlmU5dLU)qzs96^_YyU15>T(f z$wnR&W2lZRXFF;}7T8FwoMd*!D#rDedX>QJJbLp|sO=o$FQ%+?O4h@)(#PrA(i?DY zL;)C$`oD5a#XhBppj5G5UHM*SDIDc|8DHa%^AF5lHq3||GA4TDLf_WXe5!>IHzs$xu$u~njoj?AL+&s^G#*T|-ovMOza{Q1jGM_ov%Gd~ zi*Zmur&HU^O?F*G)(|yLuq&{`)=>ImSH|V(@M3J*`KR{O$`2TTiItpAClM)k^#el% z<=1ksiK)9qW*bqdr(0grMy;u0jaBvA?F4+=X|RG|I3UBVw8m}BPfe`e60^1^v18Vv zcI)OMu3S#tUpY{K24(%gH6RAaQ^bET_=aL&%IrQGFg`#bhd^co+cD8*VFb;M4)r01 zn^G-Uc@tKfFL*jshsF++7PS7!Gudv2eEAVPm-OXw{t3IRY)E1dc zFL9rZ$S(CAp$(o5E+*E8+K5&Z=PEL$1DT;Cw2L;3N2$RqQMOow@7-SD_g7f~wHmD+ z&97juf9;(7?qqLHpXxux)n#Fk`a>x4w$~FI7CoqASG?{WgFf8PwR!fhm~@1}l!g%q zn_&^t3L-Y;n2ZmQrz7-YV1A6lh^^YNI7SS)pHoglLC}6Slx{n5Vz4JFFvd7>jl@2{ zNEUX^62QpR`^*p(U2RoWhu0&H!AKl3558 zl7jeVS}V9jP$=rt8FZzraX z#72GEpSRSZ)4i12rn#uM%nfBs9d!DixJ~^7GM*OOxr)~TfbV=(^z*nybQ#IF*T;96 z-2TOhi0wi4%x)N!S5p!n?R4|)wD9Sv_!@f8iz{m2oK1;4X#xQ; zG0_gz4G~`~!|D_}Rhg#K*<%380)Phnw=+WdebU=Ewqri*N4wFqI~Ev~*6oE+rb>og zoVTS%)63l9lj+nCa?wZvYIMtL04!T6ek!_~czdebEgS z5N&P5N_7pn5)X;@6Ygi+Pq?3P0=_xHi?}c&TcgwJ%9}NROVsJ|!nX&Bn`p2yXmzye z6at8|V#h%S>`zovV9p13acFyt+~UDAW+rW!wr1G$Gi1X@{g^U(Sct4pf*r$Vcswqq z)Do?2Z^7LgoP&Z12ldwF8+5MZj_BCVK$g8=w$jef1K=)+Q=5&3y8i%cW3b&7c8o<6 z1E%S?P1AI~w6RQ5)meja4ur*k%Nsp(8l|sIQoXCW09LKDiAJlpQY8CYXWG*~)|vK{ z!KSNs9l<-(ykeY)tW?jdeIK|Er%6+XU8SpZJFC!rRQoUQ(+#yB1EPGzJehUXBYM8~ zOn8XO>y?;?J7R-@HW05go1Ui4lvn1n(1`pgjN58#tZ~Z5X+z{)k+jXMK0x|G#bmF=0^;soGFQV@}&Owc8^FLZ!)0qbR8^K`WF z2+b}fDYB$ke3m56v%9MA z#~-^tcY9%0@_7@Ux|%m`!5E(%X&JQyZ7!^_FlhC$G@7pB5-NB{5qEs_547ScWJ<=| z9o&tCn_M~Sb)tow%sd(H2(lnTazjEU0dIN zAzw^yCO3)n-9e*Nlet{(SAiDF@h!Twt)`8Ybm=<_i%W`GpJ*CtzHX${(c)>;Jt%A& zT#lY&juoICd4u!=owHYN)Y~;Q?AmA3leU}ArH69?nU9j}R4i6*7plI|KH^lxozGnK z#8%&Jw#TuX8S1AD45_nPOw7#AXFV}E?k0AR6K_lJRK{$yGh}a>J>Am3s4&25>upYg z^Z`gXgNdBRRWnTLuemE;Zi23m7r>V8%a(~g*k3g8p_+QSHAamU(Oyb`>P*y)=k(~Gbu0Qg&*(17>x(!?ipOnJ zCz<~MC8lO(W>H@D=Q*ELr?aoLOwK3%py61}DuT08xm?RUnZ``e)Ye)Iv*HJEIM)Wn z4=lk$H-sMDJH9@SDuGtOa^5Nkp@kPnn)3(dD|t@Q-4 zEtq~kK=VG6%`Z01vuxV45T1HeM(%7GMgdgqt|}3_YJWYGiNr)M*cvTvfwafBTce<4 z`mKe&qkPNi%W2ePyjq2xO*kY?WkmfuaNw+cxf4L8bfC*$ zxo!rX_A{A}aT+!SuPWqj7(RbTmBbz=23R)f%f#^$q4rOw*}9#-qq3de=R<-pbz@;v zl+@0xiWObmSM7~n>Rl3*+_%);H>7@jQS~Z3U9CPP*V}eCevyos^!shgA_J_OwUc(X zA@ef~&p3nLll$wa{O{HEs<+V9QK}W)eU^Xm>fC79olSzxUDi$BNXhv-mQ!-Kq~O+g zYn-gpSQf*Y)Y!onvH}HEuWn=ejIBdZRNtj-E#0$adA_w3s7lpEXEr-T18yU5uRHeE zYy(61P@dXH3bVG6!Y&|fI}AdVqVVjY4mGX&ov~QtOktvre$?g2c_sR<_cM z^{Z1!^s5n90$`ZBcF~O07!_n z;&&cUJsF#G%nQWCZX$PI*`?DpWV_$sNB0?}P3FNhDi*rZZTCzKz)URCW3w;ztgz1K z0TSENjDcmZVasAFK%zGg!}_$g8t+pTX9_bYLJ-V8L;?7id`mC=`JT7_O+@-i?x5-> zw?|I4lU2I^0H%GGvbv6!PgNgLI!#y5s37W#^!lEI?rPqk!^nj>g&79nrTUNYwt@{y zi0)IucpW~ar(WNDleXnD#$MS5ug0AO*Il(cVwKxPLU$HeLBpg;f>%tG>)UUO1bk>8!TR+Xp+)0H+;w6*i|{x<~?m6EI&;4{5`Q0fwtVhGlb zbbnN?%p?6`{j{Y@g92OcpG)1?q{h$NBw(ii)`^2YA?OYxw$x^nknLr2Q>SF918G{S zt03K}8*rLRdvz{)2N)(_X6EWoJqAd~Yb#$Zt3So8V>9|b)oVao9V)ykfLg?IfLg8Y zlvvxjP``1Nq<+Af)D5=L(PIuFpHFV)lA}9o-7gxjb^_QO%WUpBWBisUh&-9C(>9|; zszFkXv+3IHn5EC?3h(R|9pzeF;aEXq`3q(F^>6<5x<%C0h{5Y(tQ2RBf&}JUV_CB? z&gSDoQLjg(mAGnGqdq zXQ2Ml#ZBVsWvheGPl#vH$vRRYhs3x8Q-hq$sC38}>0hR-&!~Mk<*uiRq57ha+q_`l zgTzGKyUjX*;#u0TZFbP(@mjW__3g@tRAwqDU|`e~vDK~9>9qiNF8V0OX|}6reXdWW zm<~k8rX_o|0=3tZT2zTg`*Aj@JfH$`GY$+QYrh1+IUUIx6KR-=k(fXWHU^fZu9X7bTF99|Wz2XWT+?#x zFtPZ0n$Jkm>94F<89gd6&)a(KpHZMKgpP~iJo+mh-J)bo>6}eQrm&VMg0SZ?Jan^! zQPWrSx-}UiQS{wyQ%`=Xk7(Cxt|3!?+LE}fh_*$t3qB>93SZQgt-5oIXtmzT#J;sx zB-4$YZ~etsdsc=<)O{Tr)kU>ir+u~pC(!i0@2EY;>R<5977c3^0p=~0?q!DR{ULf0 zHLLqm>DFQ1vT39~=mPv{#_O}tuuw_VYKzj$ja#|Q8e{Tw9x|nhRjkuC zlU9-PY_e1Aji0)zQJwciCuohb+|>7d8AmkzJ55%bNT`~m4`l$>#WdzWZm>B%aQ*a_)kEYbX%VLIHs*oi#8mpgmtB!%F3T!7s+fz`3zI_5&5t)Wf|Urv^Imz$^BBi>SMCGQCYC9ET)I*e0MEs!gt9 z!5)1N^IbA#Oddooia05VkJ8mMe6EiRvkBE;fuDh!$ z-)!`1Fw)DCyzbNL1*F2t7cE%a=D~WaRhgx1S1lg8vc0iQdvXIt3T)-b?Y1hwoZvwl z#MV`9t$?~3>t+)}N_H&A+TAY#f;Y1@tP3JD94OGa9NR&yu`~Vj$5mK7ME4kZsNoq| z(8b6S;pwtzMtg#1nY+=L%or${Wy=oIcGhEQA_%PLa;>Rm7&@QasVs@-qferLxp?VX zn&8?s95)P1Yd73j=`ME&lB;*`g~SJa0LuAe3^KSd>Opq7H9fXB4D(qIGCAl861SDS zwxqhUtzddA32vu5_|cc=I>jor%^%?tQId%?Yx`i$PS8=!IQw;*SAJowd%(x63bF%E z`hYq+NA~>W#_JL^A4kqI^awQ6b_q*KucGIu0Q9sAwUZlu0)Smi)o7L1dU7%k#L_k`!LfF{ZxGL-etS%__(AzvZ6kEn zR95={1I$ZWv&BDBE)ZA!@{W~lh&ofKAYi^b}Qq#D5n1R@`EWq4E zH>hH!ol90@GT4EF1GG(F)2qMKC|0aBFW6^L1p{hgYX;*;H5IOqz&E4`hTQRiD~QXp z0Nc(8_0*oKuh$k`=TP90KIP}!ynm>bwQG%Qv^F^K*Rnt8u?yl?Rv;Z$`#F^34I9GB zkdv8L6^W^R)yng`gzoQL)JVaWb6oyO2Ga(2h~JH+v?ZnH(!Q3eqRf+8xU(?+G1KUa z`Pw@Xv!EPVKBS(ddBL5x<^os68oO%tC)N1%=>DOn)e~BggA7B=p!12UK$^E%qyuYW zU*(|urdB;d0WIDSr8!(S=SIXDyGAoz!GNDps<+Zw=jp#yr%_(pD>wpLH#KHs_LXKr z`OmH{Lvv>=lGUkdY_Yzl*w%*#x$2-RmjGvom1Wri=#Jg(`T*!m$(iX-lN~>77=Wt< zh&>Yj047v9GQFFYCfo?e(qYEo2f1fE*Ej`mcH!)fiuDYHV4`X)EIHlj$4{hHvrD>T zphf0=PO;#A%z)evm1`J~3y#MJKJ z!UhaCAyBzB>UUg%I95E21>uh(d4}OY+}~3A&wEPLP{`Gl$I||^psiY)h%ZSL`%*fV zFeV;4`WVHtYk(V-B6c-$>Ydvo?Vf$J&$f6fbM4c;AWjC*7TejN!^%)6$Wemp1PGJc5UvhGOapkwDr`v*xQ2v?o%pmVMV%^t+eXM z8CcCh#j$xHoaLsKGi_#Zzia~$pAZE*fVb&&I+ri>Ygd$gCZe&IY7~sFi|SYD7%OP6 zs?}Ypjgn!m&fsaN-jwq9Xr8qNH5b)pRO>ZDeeR}x?xub2rY5UbV+B-SsjCb(+rH`( zQ>022WnT4xSokW#{4}r`mS=j(e=Ttk8l|`s)ZwrZ@?xW4L7n1iJ^6`YL%?v>R#unZ zfW&4lz`Cha1WiJ#*M`&mDQ*PKHjbSz^i8}$9oNz$H>k&~U+7`T5Yqw3QyG5woH2(lmqU}EV*q!e$=BY(BHlVXMhO=ulI&>tN_Uk^~ zV^V+J`~pS+dbIne2jX9)tcB_L*idvcGytbJ|&}&eJJo zaM>Jk5%M{2h(IN>)s+)YrES5ed>YGdeB9ebF<3AJTsKx{)BgY)b!|ueTE}lI?h)Gz zVBXHSy57^Mi2IRDIX+XH3d`oMgY+za081~xB*kGdBxNTiFya67TMk{CApcRuwk#y;?{{Uw>jxmuiCOZEB zU!t!JBl}iVFr=D}X$ESJYMM{y2{?)ESaX%Y4cu&~A$de1@?w1FaLPig}c@*QVPmycHeBF<0Vj zimlvinv^Lp*qPzKB2IU-)h-B{L;GMt3Gt^Jw@Zc|usF7iKD$TSxS@4rY#*X)2d;+| zpvKpCdmMze`i|kJ*9%#}ZB-4Kn$NR1GraIoyO(N-MOj-~1{=Bk*b5fHq2-lWs9JZe zCMi2ce?@*we`3HzjIkeWL1v(S(VABPkub0-;6t6uEwRD$*Qm9w+EAUqk%)ZFZ~RiP z#lZf6?`doXH%KVs{_lf*W zM)XU#0yp4UO5=G080mP3G(nB0Fy^$Q;ZPQKpmR7&Gj6Gjd#wrDj~HftK{ z`;AJ znQ6m`>Blt#v_tGL@Mhq2@f&gof85Wf`yZ(>4n|^o+w|gboI?aY>HWi77I;?#O)}v+ zYFvvL3{5}gSi3`Q7I8dB&T%W;tl353g#Ca3CG`8$N*1H)T~EHLSK(O9GGQksHhXuH zr~d#A%)#amJ8EH2eBft4qRRs!_3SP5s6Xk{N_3Uf+m_pGQCf<%Bz{XTsH0=L%c!Ym z{{SPl!gRIG4uI%Bru0tR&0x-)b*y18aN|4R8QrrMdb41k{c`b!5nq{DI7l&w{&5K+ zp%mh=ELYyHzfbl*TtG1ecxF9*IzFeCQAdQ2n2nfx#++vPX6%er>|D#iYuE(Sq#AIn z2nmY|#;oxN%wy|W#^#Df4E|Y|7|haV7EMd(-ZdyKOyesp0f7_PNHlx!cNw*LowVW_YdjkQfF_Kd(-i&e ziTk|5!ZEP>QEUWmoJY6>VyegXAk)6!S$k^WD~BvFfQ9vcBMjL2zFEwqe%g+Y`C1DG z;2>Z~5nT6~XOwXiZCF6wuom5CObkt+t|pVkv+88*tW7D~R_KFqJp;bdu>t#6>0)f9 zWlV~^*=lTT0tm=d#*y3f(s`Pa2Hog1Ozx9Vntg`2$FiDt7Z*BBR@yW=+HbDN3XMC6 zpU!E{YZFv5xPdSrq?9icJ=}|}MXtp^*cQY8c3t z{XE-Cq#sRMbaz&lC7)?(hgUXMMxgg8UAIwV*o1whyUCSl?m*G9XxODT*hw^4d7ZY% zEP2K$+_yY>stm;F$C^fT?k1O()QPED zytNw?@GKd`?hV0V_|2SHB04hgwSMSmh(%RG4(6JKIH`h?Kr+u^nlXx;L**dy(%nzO z5=qIU^CZ7sl{V2`>29X7Yy5Tf-^1i{ILAggnZ~O!&cLz6`gucBZJLkBVr0Cl-52u0 z{{Y4!uug2<#tk%K|%eJjf z&D^QUHRtxB$p&wEr)wI(lk9;!-JsGdaz8DK62M7Vrw22=DQ(IROqqQ|wg=OY-9wvQ zJ$3~i(;YSw6z*M1qlf2E?!pBCiDh(L~+l15C zez+^Pp1P|WZwZ7Xy?yPIZ<(z0>7HrocU|MO`!PE&Ez(<-VPZFSFtiNEYXW9!X|A23 zX>|4)t7Yt6I~PqjWn46AzOvgUmZ7I!%=(79y%kF5PxXqd)TL6w4Y+|v#;4mf-XBkmX>v9?}4N@23quWVh)NH`l3cxkWq(HF>u0zkl<-_J^P z`LwN6u=UZ>U(`W2YWYi$-Hzr&~p9cB0f7ufJ~4O*)N3v+loFYBgXR zFpOrB=hRcY>bt*pTBXfs()SzcnzLS?>H3pSpxyP9x$JUWu%^TNn~JswZ9Wl+WFxU? zI&Lfx&KB8N+z5deCR<7ivIy}lJ_NVeX5d|lsPwEDGg_ziNdBX;jtz}Q7eB%S`({{` z(A1EB-iX8ZBNYDtQb8$d@vX;0z$AjWBpjGHpD_Hz^Qn#o<364jwA*Fp_gG4Ys7&|} zepqtadoiuOsid;$wYFWTtxlfpOKLX1L9=~1qQ2TUABmtJ)RRm9Ew4VlNQZr_bB3G3vu0iCAP)5q6W+>kS1W3M$0x(>pZAT3J*ZmaUA$ z;%)wgSd1{!{wrpP^Qz5@`&LBsf=KP|d1F?`mbcgj_7?Mjji!~`+{P4z!WnvlQJ>Uk z20)(^iFIU}L;(HCFwCntH#GAN%U( zEinYU-zy%4nEwC{tMQ25V*dabWxf_GVyDl3(YtL9UWX-4cVO?wC7aA1s}vUH%p8nF zCt;t%g~crQ%uC3!+Wj9%6X?B{^_6D_WJzr8v&dKaF}{?`fPiVlM4M z&3>BAMGabArTV9TS8}$GTiMZ=`fXv>ZcsgF`Hh|>Y<4XECNbt$XY7S5!nU z&5hmM1uV5%uzG1+1HhWQSu6C`35||S9w+iW7gDWZLAp}=!96ecv_56ThZqdqf@b$V zll5&!+IG^tSJWQ8MX5A4_dcb%T*a(!&~>7<8NSZtto<~V_UOz!oD5<-Sbg82z0dl! zUge+h7`pr_iTCQ>nEuA;{YrrB+cqBPXe$irA}XTbcWRyLqq;K&=!hN=&cVlGbx*sh*o%mWiZCuSqs>$AO-D~fb!(KO(@PD;+9^aN zeU6%aj+!_Hz?#3yn1e2&Kg1_%;6vslKinLF*ZF?0s51jzDJ0Ny7u>T_O3`5zJD0(g z{tG&v1}tDAe<+`oW=g9OStS;18?(@;+@^d9Xd76oL?G6Z??;#jqc|CMkYWePnk{w$uKy2DnAAsmcRHc^-Spu(LA17FE;3%BdA7w zV?t<634s8BKq*KmhW#!3@OrM(8cgz-9pObbc&7vZ0D|%21>|{rq8K?YnDn1_l952XR=dh#HU7p<=X16n$W}fhrEA?QP=GN zp7G|NKNZn%(KdCG*!!)UlV9k!fjJ4p8DTj36yxYqkD*VOh(=?SKq7K8MWe}W@uCuV zxcs53eeWK_ugK;WB1K$MK1T}r9SZ*d5PSHl5yVrsU!3@hl98(5u0<56nc6bsu7|ofz1B^lYAD^V+Zq1 zEj;7@0K{h9ANip#pApFlpoarp!hAWSczr@}{Yh%tdBvg4sZ8Amv}ZW!x;_BzKoP(3 z?;n+)v3gj$;UAkL(-j=$!P*1VVOXyN_J*GcvX|6#kNTUTMq5GpkwQdV zLi)Etzxf6P6q7;;urLpay7kWx{-4cgK4L*kPLRO55@@hlCOG@7{h@>Ku(fE0vmVP) z{h(xMzh+Y#DcluyA7cZrxK?mk2i6+5-7-in-p>9U7UArk-!5v-3gD1_VT}?D1;`z+Ya}|A>N;-1(uCJG42JQv@`ZWcTdba!DNmP z1@$*|-YVdc4XF*qHB;v}YGvrG@jpFbV@26DZ-)@+GQJ``liB>1w0Dd|&a=wsxLE#a zvUC9_mW#GE8jfka7vq`^;1B@#g|cU&5c)*dD19tG@WK%8kC^ud!q3_r_QSjPj)rlS zwLl>%?*;V<`S6KuiAxEf%PE7>1Ek82$~1PE@ItW1!W|bMLl6FQU8zX@int^gS<(*i z(8I{;6TFjA;WdGdILP9p&<;%@YT!LMbd)f=qv2=m4G2Do+UgLHHXMX9wU~~y^+F#( zpOi$PWbyw1FBD{crr{G0CE0aH{hF}mv<{C*2WV`Iv(4qX4jl(*l_DTSA^a7fI3Q?*fQKBV#WjzMhbQSM3kFq0r$8 z6xVt!ncp7cni4vNg=ECo_johFAUd3I1ki#U1FPr5Q(iljgcCwwTQIg^Vo8B)!e|9~ zz#+li;O>}ULp%;fa0qZU1Q1OKM|Z3XC`Ua>PAJ46geM89tiT~0EGC;Y#KrBO8U#Tal$|$0psyrG9A{W+_4-3agkhD4U_(3vK=B=TL`+W&P~pOrkWyg(tgQ98SXc`yD+{~9o&bVx z6yZJ`r|^_vlrzWK-IBQocYT+5m|I8YIOj!^rs{&-VS_i33B&M1s$fs~u z;Xis>Y1Cl|RbLz#SuoLP3dGlh2Mduw1Q3Qg!@%7ko(mfleI21r-C%!2-m7M5Uo_x3 z1OVn!r{Js=&#+WfDn&7Y0I-ilDh2gdis-8#v@r-nDC;y~pjrU4!I-vbv2a^f&22*3 zfMm9zZJM@?TUMupQ1?wJ6}*W>7zj2U?g?~GE)gMS{ji6rj0_Bh|ox_p<6^Z)TC1y zwoSVDTPt%-atF1lqA0HF&lQ)DN5wP`l=I0rl5||xb!BR0!+Pq3mX(en%s@~hA;~^| z%VzHht&(a2UI7gxva`exfrCP0Q$-URm=?{cfeBJJLMF=)%rgw|8<_;dyPi20pF#cM zQ|ylOy@WEbvJ;C~1k8lZYyewPwrKh+;pBeGj=_wROyc>GpkK`n_t|AJzKN1Nld0gZ z#oC&7l}^3$raE<3iG*|@)50<82FPOJnkB-z%GVVu(nVVuO%3TfX=ccNk5 zn3|zRu^nw*<5A5T>T*@!AZmf<4PkidB(B3hv4yu|+ML9$)r;;TQJjBq_8CxA`17CXQEa(<}rIPe+7k43CC>PJ=C z+I+(8w}f;%=j^NdH-}5X8foFA4Um!AbXYLmPn>Muo;n0cl5N7wbgO(3xPUKg#Q3kM^#du?*N&!^^*Ol_e38Mnp^Ii405|bpEYBmn07)FIWZFpWEc3GQBFc8 zXQ;XpnimPYMgv%sIzZMIC2Ys3Os9%qOcX;-2TysM*2>H@ScXDv1E#aqQddLH4m0GN zKMPx^w{`Yq}I07*=fA6C|Cg=xb^GNh|$Fe7B?^%sbPRS`zvYL2>aUsXf6 z3Z06a`~sh#Q@#S9piMJ6**7}kE$p*)EhcCiKx((W7N@9{PzW_PVKTXfv_hay$#&W< ztW7vCvfBZ>kVR0W%$td)c?G8qm3hg>lr)AD&zcf<9#$d{C=}NqP$5{$f^}LktF%o} zMDJk*dMkwC;ro~=u0hD~5hQv#BQpxPZi#e3lzYgf0y7)u1+bv=&u?tQCVPq^r3oDwumBeQv;EahBMaRW&E#drc_az$4Od)i@{5!7`lJMH(x*fqgKsfs=>7BAQx#Q1d90y+aObD*_o=5bmrM zh0S3EA*cTU5g?7Oy!;UWjHk5`Jg0<3B#1%)nkKjjEW-;B(Krok7I-p*LMW$U9aZF; z^@HRr5zo2>9pGhBEMu8Q&k}NMnyNBNE%ihM@_Z3+C|gu)D1_-a+*tjfCV}-p2_pln z4EQ^%1v;{_nDIwW57lOY6LVdiCXS2YPti}(0*Ry7s&^HmG&D|<*-M#>(B_X0p=hcs z0&JeUO>!~;JE9cVZPWpOi0ZuI7-(bWA})k83P~9ESDT{Z6*;2=+IxT6oxM#*IV%$k zf$Fx6R+`aR_09npJSQgw{_n~MD1AQpK=x!ja+(E4w-=JTT_yDu^-w2G&BExvRMFy% z9d&hA=uV5Lw0CLBrCohOzEQD>k~*S_aZaktZsyB~)xQwZNuor5?5q$zs2~*Nu&oKp zk`EqYB#!`$<|DKoi%8+$3iAlV$Ri<+A;*12Rgsg}Y38<|6)o*z(ojALpiF!r52WyT z9uXuES&gmf5P}jJS|dqKYK;z_YjRSW2sd;odM()yr@U~9)&^5fo{KOxs>Cva%u29N zHd{j=5i%S#FuZNTC|0&tq7%YzijWnblDVl`5RXI&6B`Fu7n->DoCGix z6TISN;bN?P)1-XX)O(?{Ga5Ag*1-P&sQyWW4Rk_60vgwdCZ?3rB-13)xkE(dLkz=I z!>*9gs%mO#YHJ;E(GJU&3TLuqFXpnau)Bd`H~jc93r~T>9Z@`HXbJF&*u>6=%^IWR ziscS+;WLBU4Rfj>sV2A4FnT79;F{C|mhAAk!Lt}!A>>S0Pn?#b8DF%SGSO&Y$*q>F znrQVvF7;6f!5{M00{v6>GPPQ)@OOS8m4&LgS6VJm?;(Ly99^H;q)xk(*)^*wanvqwI<(qwI<*$fGHXuPh$~Lmn#9RJvDAfmE5^&S9`H=w6qr)kk8+IF`kg9(Q@qv@pcX+AJWAG>bcY)t1VnJQDfAb91vF1`#N;PZh+w0s%j@5u`8(PcDxCf6V%y|Z^= z{{Wvg+Fd4vUYyopqLjI#nWr@rN+md_6iS?xzLKvBljtFSr`_))1}K*CNz4g(^cMG(ZP8u<#vY~-BqDXVFcKo z_@F^S-iuPMI+p?Fr#i}BFn0HZUoYhsOblt)Rhz&dfm7kS#}wuhMCS$>fSwb=+AXSi zGOPk@ThjjkGiEjBAXLZmExUa}+YeD3EhL-*+5|!3GwB)>?-n1j(@qbjsK~7vVZ(fl6wmqDav!N1&|8cF)uZU z_DPXWPEQHq#!1LX?-?&uGOTQ2nUrJwC#t)?6Q|GySXXk^=%YZ5ClH#2_>Bb>#|Bmh zQJw%y@S6VsMAv#Iy4X#3jFxX;vzDDa!ZL~gUewgIf|7j%65(}8KR3qvnK!H8iERZ0?-5wa@#H%=q8 z;*Vy93(;Asa*(>TxO}6T0mF6~a1NBCqmR0k)4?seAk8=nr-b3^oF(F`&qZHgjjTA{ z){|O^r|ggVN_NZrr5%y*X<3dD)n+-3U9)i}cI380Y5P{V5n)vBiFuI1Jr_KlGYTTI ztap`KYSYPmQdVe^7jZ^e=7{q}wL*1z38*Hb-vrQ`(k4U1uFlS$dhps%b$k>3ihr?B z_9=vn*$QYDR>V(eYgltZdMo`c$5o+P#+61G3U{QCI{Phbjm(jx;r<@XO!Jjirf{oH z)xus$=6%A-oQP9p|SaoSo4W0Z1!l zS+xgA@oEfPIbAkWUl5cuja+OIo8{@&UB3|)=B~?g5}GDpO+5BDllu-`;((!BApUY# zlu`CYzKDq#4^9q9e~a1vkxz6Cr!YmqTUoL4lgQ^UWwCD4d!i4)TQUH6-I&Df5&r<{ z!T#Zn0=-cjF0C5T=DWz$h0J;w+~2`pbbe^St8cU2cc2%aHAT`W?>pFV{X$6BgC%H* zBPPi2IQE^CCD%HHO4@|(mi(>3*&z$?P;IL7{EfO2Xc7TRa-{6G;msBe*TVsAXWeYA zNpOjqy@TX%nrRHj8^v;6z_mNg`@+Fp#=L^p;RMtW?6fSVlO$B+Q{>VoAcVLLXUCF+ z!)Vc=D4wKo{{T?(K=#{c;c8UnI?IDbZnwgHRGGEc4)Nx;PL@SSv~ClFUNfr941l>^ zn>WBhr}lqz92_dwCJBs$3%Lsgt7GlY;l0Ov`cXGes;KdbHmfmc_g&$XO!I`M^rCYH}Zj|kNnkP z*_x0Ep~ITmf2GwPsfE1mJyEz{m>q`;t*4W}G{Vw?DBWC;&^&02hUF84$N!C{JMsnALotwXw-ie^MzAfgvvp$Nm zt~FUGTolkmB@6 zv$poGhl+Sr+SaNr}rdsHaWMtn`5 zP$mi|_HHr;=7Jr^9F~4CnC18o{#*|n~RBg=D(c#DfyYKRU3h%^+)`{q?U+E z8m$b%u~l)ibtFloO;H9qy&%B&VV##T{{oPSK-6 ze@Lsiz{rH}AcThndW}I@hk0RV?y>&>s_%YFyyZtBT<3#I@OKdiizha-h?#b(eUOht zfCyGL;m8Fsy`nzo0a$W{AmjlDLY(GOSrR@ezqD-TOK<3m?!TED0rIlHpW#m0_@bUE zqICFJTVBacZvK-`nQOr$sV1hOYoWq8c`j=!3tgh|PWOh3yya+6gewa=p1d8!-dvY` zccvhJvUiw9VOo4Tk9FR6#BU?)7G3Qit*V-x3aew$+Gjp0j}xcli2qM zh;j>4C=uIS#00>)vVW4<`YDHjW5e}LOPJhhuA2RYpE7MveJk??XFz)oQO zk(o0->m>>hr-hQQ2ZpzrncosFgKfz@orA1G(sT| zhVaNwY`~>c*WJJXND-2`P?r5c6QPDBG`Yo%bDM`sQd9IX?g4r77ejK4G*Q(_VtRwzq zJrOj*FDMYQvQ?I>Mt&%pEJu__sw*fDMApa}s?woqsEw?TgnXlC2^HI6k^I*sh)FmT z<4VEO=s7xe_*p9@RzTTzif-_knz8J=>`{@OvX1GM(JoL^3tDjOqI&wtyc`D}0YreTG{nzS{X^}#!Q5+S5u(@$d z(E(Li6YPY9Jk}RE!mGf>$DscJu|l+1_(dL!sGDJxHO`5+hd3J*hbcH4r*mQCu7Nwk znNwjW>O0DJ(Gm}WX71}|Oz6KCwvql{?3_z-XpEN7`mL_~#K$D+Hq!`i;RcTyLP$MbYVuYbr)_*uliOC5-PaFgaAu@h=HHn>+@dCFT$CE-zoKh} zvmv0#baiy=UQ6(S?U3oO!wGNh$d7gSeb61cbeaV;rqJCD)5?k7s$5Q43tzIz?7hZb zE4;8#H^7hTsj`;2s?oCdjfdS}!<0p=u)E6v06EGeDN$pCU(Gg?!fc_>?6Us=`vrov z5`&ebh`C1#3gBUAW%plH{uHgJio&55Av1hv=BG@xwec_s!b@nu{Yz*kscn|fo=au! zhUft@Wa%)rBr&(Bl3?jPRGVm=;qUT`95E>^g*iy64-q9_3Z|d-rW+5X3K8KBO9f?I zZ9ls2A@r2N=&-bswAU|{sUF5Wqsk)JLXtqsdw|hlQe|UMLrErEnx63_t4z&Vd7y#q zGvyzt6|3a71>1yf%33Xx{g6RTZ3&Sl#&MZ`8fzu{e^7&`Iyk@}4~n3Ybehppx^0p2 zI3gX!#_V%Xs_&t64CGNZ`z%6I-Tf7t<{iSjhv8v=5AL~e_s3X2QbJQO{Y*zeZc7gM zfD_#*xL>NRgC{I6!?vPpW*Sx;u#ad_d#!g0ogRr=JMG1IlGzhcMf+v@X{Qi#m1C-X zN^x^uHBLK*W?pM(UJv27tt8UBt%>@WJqrFQMr12UuIqjwWofF{(jUWezwAoxCjrrs zT)0il_;G%4aRCZPy5E)j-wN^&N!bxNjn{ct2Tlf|Nq;KuhC|+e$fe!Zw1EnGAp3g@j4RrqS!pVlKMA=o#M9Ejxa$VAfuJ&5Em6DH= z!rE?cYAQn7J??R7-g&`Yt?;`db6E<&6heNpPL}TPn*2jSl%$@vl?ba8&}5k?HbH_! zuJDH?geTHiDoebMtSYr4>*oj$P_PfQKUGed0;>ggRJktGL05-#N7fll-Xi)Yzj}8r zS};dKtVaj&KIx9_9Z(=|YYZz_<#T}(k@!yDsj8ono#BNph%)m;%3&IRcs#dwka^TC ztH>mRA#LDFvhO0CRLs-1o<>ML&hcAA(LL)?9pKsrKrKXfWwax-CMKml_cx+9M@7U@ zeJdehW^-7ES9@pdSMe8gqU+&z8}ST%*YfAv!IP9!T5aa$l;TIyUu08hz0=LsiZ+fiM;mT-MC=nhx~& zCQEixtmnayuPzVOQ@*!r$vSn+;fQirU76Mnmqj?}gf*Ho>69x~ki2H9Q#&|7%Y0Ey zjTlAlCCg~^=QgWp(l=Xwh4_K&wqfoK?Iw8uf$|BY0ctO(r#@gDBH>vn@5*4U?e?KE z-Msf_$v1SnZ8<7UJLo9GA$Q&Iwc5e@9aShb14w9o>lOb1av;!rtV2n{bzii$S9Kji zsnub3$I53)U8((+&QiT^8!b`b{dAG~o&Nw-XVUP~gD|VGfODqV49(IjW&=w_WM?u7 zD`3{Qbe-Xl>3l=FS87H|`ka-`WGb0TnkhfwKezT)CS?f@BXDQrwH!0`MEatAQ9h}y zJ|bv0epi)ZXXJpWSsyD$HePgJ#hd}LQ|!0?s&W)Y&PH>W%Ed^p*rG(4xTMX|MkP>1>>JpNNuX?A>T`;cUaVx4fjczuHkhv<)*DsE2g$ zZp-$P_E|6jVXFLkY}~Lqti%`{8|yS=30L2N3v2X``lnCy7KTXuL0q~)UPxtQh#RS= zeQnp87bJQn*t(N7R3WYRh04;LCi}lCeV2Nx(jTfUt4t?KTV0p^f*V&qWMv_=e#}mF z@mp87_GYavai2Foe2FU8AQZpRyX&ST5t1Akdfu!GKfsT-Wg% z@`w44@}gj)olL4sH>mz!lw~2b3j#U&tpwD!iD47uX1^BS*v--exNc6mFUGfM?Vi=d zlIIDEQGPpSHe+qQxg%-X%Xj=c_(?bgvU&3Yg3g4;XUZCqr>9Wam3u$*KZeUX~(#|!n+U&PvcF7n8SIAvrZ zGSAwP;#{^nGS_W`Pe!%~N`Howyd0P&wqY?bYC_eaYD7M(^&iIjaUBXjU)mX0k}$~P z!C6LK4WIp10au1$aMF)@841^2_<#Sz05=f;0s;a70|EsD0|WvB0|5a60ssR95fTLx zArLVJ6e2P~AR{v{LQxeYG(#3)B{e`qQd4pN+5iXv0|5aD0RWxNX@qK@WW!UaB!_>; zH=LSjrkilV$aw|W&&2!*_@Cwp=4iw1H8lqO>-Rr8JDRe6Tc2EQ>~9~~`X6KHeUGo7 z=;~?-uM_)c<`}-y`+q|F!|CP;<_|;cf1$4vQCnM#+dCgHvF=t@5Nq@F`g0nb#3yr? zk59z>4^cNaH#h!4n>&wZpQI7>PU6l(mghB$K_giH&FFoPq4q!3^Xd4XiT)viemzWI zczre?^d5)U{{Tbr>8}#cyV3oR=wHvL=4;>4eMg}_JwFqEBK~3bpW+{QG!$&;okD}~ zzy|QH%kAE>3lD4)$`|l|ulS#jQ9mBPh`#Xpd4te;A7lLit@%3Nflk+Q#NgVUxXZ?9 znWz_VF83O)Evd)uou3|y54W$KLk#n;O_{UALHUYyM)r1Q%nWp9{Q8gLPyYY}eY5fE zzleRmK=+5!#2=vhAL#B2j~z=GyKZ+QKQC3>4XwtsvE1Z9GrWBI0}r>a;%+y*{7w3Q zXoK???i2ET0Puf6{v`B2*Z$e~pX1SC_MSaI5q-oyn=nrhdLLu`CGojo^A8x>3!$<% zHew&pJPE%;?LQu$+vwi&@$2{yYA@U%^ao?m`~HLd5+Z}LeLPPdpW8nkhA-kDZ(qb; zaSy1TVDvu6{{X?%*XjHSe0qK$@@0d}{10EpsGMix(Z4=}+tZ)*109cF?StFX@*>0S z>-dA-A5r=bvHriA=64!*8WS0dA3-(XLp9({eKAJNf4Xbw-{{wf7t@H;)`9eId8w}- z(>}%rvI0tj31EE(yw{2NpO|9L$FJfKaSxzYBmres(&a3J(EEQ#dTMGZ9>j2#d%$(d zQB}7vrmR1NMuE*A4D>*=C*e+#+wJD`9J~Tv&)b%E{{Xc|(+BaHqZw!Yex*<--|c=s zx;GvDI%o!g^k_Hu(bV3*rC~gLeM516(KX=u{{UG+cy+^0flTb{)rP8u+EY*Y1`+rCz3Pj-ZmJ;UN3lPuxdU_;$3+#5Cl5E7NT!wr9C#VA zxF;d0z30dOMmEmx<;Me~;asf4kvI z1Ls+EsXKL|0lAU!K}(rw>HS+DIZqy=)ak*mZPM+(JFAQBP&lZDl{(e9?c8n>+tO$1%IS{kupV~c4oOaNDAoh$r{{T;rfb4E~ z^dFd?nA3ikT^Fh5AbRn>6jQI5Z-;t6V63;LWZ>lL?vK_R5cX~i2n-vGY!OLPO?h1> z7RFl_A2T5UT%g`1^ft|vbzXr^Q$3irQ)O<*{2&n*-Vr>_P7seoKJ$@>*g{8$wy5{- zCZOQsbKwgsOxx*$@hlE%()CO;Mf}#Qidd~(_uGAqWuFY!=I4X)8Pc`n7}V_VhlNZ}RoL3|uC!qta=Jluaa*P&-Q^FC#b z&xCQPARa$iJ}1oA<{XN~=KZFozeWxYPHnxaG5!+EKgvE8>A?>?oLg~kvYTsOPNIA_ zmN{99<6bqeb-bKa?Hb>xbjQW|)1-BV!#$gMhEU7114qSH(XB1Wq;R*9m(Z6fol~Y$ zne=4!3=FQHZwu5Dy(&ME84n%JPtK;E5O?H=o;M3daz7K|XA6RgG4pbsE|5re)9v z#Q54zwP2qTdCWmNEmXfs#4OnLXzRCVCxy73&*i_Nbjv(HR_OO&g|#|AOvmaCa@^ku zQT?akM^#`O;atz{zu|tVRc~a(=sim=uH`@YuUen@uUVh?uUU`q$67(Tb}LrwmiE$r ziO~8^9(iTHzlHixhoE$d)u&nLuhZ=2lcHVnx8vOvRX`Ac%c<9TLCmk|rMOr(P0vl$ z+L-sP^;dF#h!24;$0L!isC14Y?#1aHT;ZP8poRgI1L7(AfS+*OGtZdlmV&R9guSM! z-lNf92~I@|aNgtC3hD>&1m*Q^cSL8|^#1@8b3~5}vx|7w7t~&3#BpijXTv>Xv1el8 zJ;tYB>B(**Ch2-e#C(!r1)zy@5N#GoPa6_Kj!>%{IzHFk&w>xbgruSFWI?xuvS~K>AeQ- z#LZL6iasG#V^(EXQF?Qv{%0ZVZN+NayN{dEYRTtj7A__Y9Bj*TS2p3ls&WyC->3MS zFh>6XFrG7Fbj%FA>o`-@adI%P?LBT)wmuupY@83Pa_Z5FgN1}F>llF6evo3H24>*m zW?9PYUX6>3l$XQ`h!lIN6Q*7BJ$EMm06=G7Km3KWp&j(BN$)-<@lG?Y2U*L;wRwU+ z9vO8ykZf+VMnnh1+_xtlujE|JSNnR``&`^S+R=ekD=y8$r17?#oO{%@KK>6@>C2Nt z(p?!Ct75UCL&EPh7U+W(IxzF7uC+g-Az6t!!D;a91bAu9-M_ z1x_!-c?+SFjh%;=uFp0@qtxxdtAktB^G*xOfkjtF_{{Znga33?`QM?5+=74QCMzq<}ahR<5s;{DpCr`V* zfr{{Z&xq>}hEK0R$nSLYShM0BWi*Fe4A#TSOJE^}#kGqQQ{pU3hH6-~;Ppa5M8WEH zfpo&kOuSm|Q~C6_923cD!1*qxRn(6lO;7SCQNqWvtE;l?d$Fy0ZTN35lD;c0y{Ayd zs_NY=z)L!M8m;!0S6#|gvau<)u})4k`CS_R&O*ACM@Lk-N}TL^S$kO-c$Z?^!pUD) z>NV187v%VONs0;Ff2VW0U~WJexJHcJ-lGwrDzdb?w>a4~XMvRkkX%1MN7Dm)KuPgF zBkCXzlds|RD?>Ep3vnEcUlU=FIgY#1v?f=?w5TH*>#e@lvfqY%^9{F8o`1GneUjq>Xu|* zEe(N@S#E004%c-)rGzmpqN?*j`|Zhh_FeF4<_0l|U}p*JHR#98j==GzaP|2xxOUHM6ZpWz9tRVNF65b2rQaT1#dj(X0#AAAx* z4J?GtKu=>g?H5KqXUg>s-J8)e`o+mQwi&aX>+tSjH9jfj9I!(3t94$XUGGwK{!TZ6@O9%}qHEjGA0jZ= zm2qDul6aKl54Eeh+W!EJv-61ZS&7+*;>?O@L?ibI9$!=98+C|8$v?NLy-q~N{7;6L zT|Ez>`xbSByqxno7ykfPhw0T*mNrP2qMHey3a1=vAeW9vCqQl;QnjCn{$^iO#l+Wi zd_&UCr&_lO?ZvYn;rx!~7X!F|FrRsCt4bGcwz*VgE8Rv-^zt;ePD}mF!`-?zZ_4HI zVtkpP%0zt2ut@GzYFS!34>RI44MIHVokZkO*>Pb8WmNwFMU~%Kz{d}Cjb?#1r2F#iByXJfV+*N_wle-IV&eSaRGh#>JvfcX;N z=8QkXmKvHHjqTuBlA7Q$@BF@Pf?Iv<;krgbV&~%AXzKkjlq|=3tZmit3!Ye&9LDmH zk3%{N@#&beV{q$@Q>h)joUGVo;X4-U$1~m&z2ijP`BFZglb8+o} z_%Tn_I{J5UUTjRC;c4*ZRSv_Bhd^ z&gEWuK5xxojzPT4s*=U}tH{aeIc|d&GMt))m34ZJnD&WH_EBnEUN%*ER3*r)J&r{+ z_U^9Em~E(8g!WpLdAlpE;4O;PWab~6vDD<#z!=$k`(2oyK4+PzL<$VxG(;*m#a-?M z9Qe-4%NL7i4b78Diml+w_ue`r@e@wVp~lU?wh*&+Xy`kiG0x*LW9RU)oRlg#D}F;; zWruES7vwsU%`xll+Ngd_bWG`APfo%UklCEP<4oh?(>f{FO~ahLOFLGhE4Dp4aJ7Z4 zLa1+P)o<+JWEp$$xdg(yyN8bRm%Ps6P_s9~eKn8Leio$tt5w)JR?uj@Zl=2wFwqC8 z8}P6Bnz3@~%A{LZJMeR`t7=-Cx}QzI0^qGNZ0)@1dA})^u75BcfG1MJ?pMKavaZIo zSyn$#t3JOAy6(Fjx>kS6U#Gi2r_uM{@g;f!i~6Qzwj;-|leT=61Hqmk{Q7x zOMw$1(b?B}l-Sj{Ve@wZsfX9HYsRS}d_#_ZlD@OkAGBs!{i8Z6TzY01LS26D=e#z1 z&g86?a3@>pBPQWhcCc$_Lc5j-5Wl?cnY#Y~CaiW@*C_4TjeO`CJh5J*&r>;THMesg zBO|8eyvNC_BBi#V!G{x_hhJ&xx2bg7urA`?gE4LAU!IMb-^J|}=1Ak2N#YuHagR=6 zH6yYxWy{EGGcCBIhd{xdN4dyf#hEsH4n)6`4D$x(pFj_w@inQo*5hpIZKs}4GkKOB zu_wmj4c0_(1H(l++{&i4rtTpL(T8K)@;=27k33W~Z|#-Vja!nw&`lTtoO}pTwF1s2 zRby5)210&Cm6?=(XiQt2RyAK$>NS0<@dr^Ka(qO8?Ee7Badk6#)=o+q*4uk&xmTM6 z_;TJ~HB{|gJ2%eN@fN^zdZr^^MfJ{ulgBEqTRMz6R(IB^YK2!E4C^rMY{B(TF!&pQjv{P#C(-6vu4hw&M+~P&_e8M27Ta>z(IoK@4sq&zc;dkM z{^{7WO{MmNQBpQN?0@w#5Fua*Gh})6B>I1dzT#zrMW9rdSW?!SjgpGQ5s^`ilB3z2 zvDu{V(J`OvxJ{YO#2Gm&>^g>U_8nWPiB+1u)t~D}#&9Ak`@n^FEcXyWV<;eLedlvv2v~8On}vpiC(&aw zG1~%1w|2r_J3ya@a0z=W=}Iv*#_3kz=lm@7p=C z_k-BM?GNGs?GLp4L+v%_v+*C^FSw2R^#Ih=`m`Yg>V0~ic~7bx`f>5UPb$C`_K3ke z&gk(u4eoOqFt|H`g70SagzN(Z;;~V60DLC)ax>|6oylHV*xed04ES9xu1Y0=HWBAIEWYm3>GLF zg@Jv}Vjq3YO=_4u#J64Oz!4lB0z7UI2!^6Sf}$ghM?$AZ@F3j!pywBVk}U1+1$OWo zkOOQi;0!#VJ4_GbX;1R3>Ob_yv$c;~L0+;uf&T#KH0mGp!TY#mN?WytY@Ix5jI`kk z8=VCXSJ8GlTVj))Xz@zdky${NUy7kirXTRmT{tg*cB=#g>OZ)H-Uw#AK|DjP&l(

      nU$tQH@DWE##0AcWGp1Ip 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 YlHWf6tfgy7^z)_miHJ0D`t_H80n4rv`v3p{ From c38be08905a1b37138877db8a488c0deae47f50b Mon Sep 17 00:00:00 2001 From: Seil0 Date: Wed, 19 Jun 2019 16:00:37 +0200 Subject: [PATCH 80/88] some css style improvements --- src/main/resources/css/MainWindow.css | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/main/resources/css/MainWindow.css b/src/main/resources/css/MainWindow.css index 450ce25..3a799cc 100644 --- a/src/main/resources/css/MainWindow.css +++ b/src/main/resources/css/MainWindow.css @@ -120,16 +120,19 @@ * ScrollBar * * * ******************************************************************************/ + .scroll-bar:vertical, .scroll-bar:horizontal { + -fx-background-color: transparent; + } .scroll-bar:vertical > .track-background, .scroll-bar:horizontal > .track-background { - -fx-background-color: #F1F1F1; + -fx-background-color: transparent; -fx-background-insets: 0.0; } .scroll-bar:vertical > .thumb, .scroll-bar:horizontal > .thumb { - -fx-background-color: #BCBCBC; + -fx-background-color: #FFFFFF; -fx-background-insets: 0.0; - -fx-background-radius: 1.0; + -fx-background-radius: 15.0; } /* Up- and Down-Button Padding */ @@ -139,7 +142,8 @@ /* Left- and Right-Button Padding */ .scroll-bar:horizontal > .increment-button, .scroll-bar:horizontal > .decrement-button { - -fx-padding: 2 5 2 5; + -fx-background-color: transparent; + -fx-padding: 5 2 5 2; } .scroll-bar > .increment-button, .scroll-bar > .decrement-button, .scroll-bar:hover > .increment-button, .scroll-bar:hover > .decrement-button { @@ -162,12 +166,18 @@ /* Right Arrow */ .scroll-bar:horizontal > .increment-button > .increment-arrow { - -fx-shape: "M0 428l0 -428l214 214l-214 214z"; + /* -fx-shape: "M0 428l0 -428l214 214l-214 214z"; */ + -fx-background-color:transparent; + -fx-shape: " "; + -fx-padding: 0; } /* Left Arrow */ .scroll-bar:horizontal > .decrement-button > .decrement-arrow { - -fx-shape: "M214 0l0 428l-214 -214l214 -214z"; + /* -fx-shape: "M214 0l0 428l-214 -214l214 -214z"; */ + -fx-background-color:transparent; + -fx-shape: " "; + -fx-padding: 0; } /******************************************************************************* @@ -192,3 +202,4 @@ .scroll-pane > .viewport { -fx-background-color: transparent; } + From e84e7f9d20154ceb4a1116e8019874d5b091494c Mon Sep 17 00:00:00 2001 From: Seil0 Date: Wed, 19 Jun 2019 16:31:49 +0200 Subject: [PATCH 81/88] removed all table-mode stuff, minor ui tweaks * changed all icons to 48dp * the choisBox in SeriesView is now readable --- .../application/MainWindowController.java | 390 ++---------------- .../application/SeriesDetailView.java | 2 +- .../controller/OMDbAPIController.java | 4 +- .../HomeFlix/player/PlayerController.java | 28 +- src/main/resources/css/MainWindow.css | 8 +- src/main/resources/fxml/MainWindow.fxml | 45 -- src/main/resources/fxml/PlayerWindow.fxml | 34 +- src/main/resources/fxml/SeriesDetailView.fxml | 8 +- .../icons/baseline_fullscreen_black_48dp.png | Bin 0 -> 109 bytes .../baseline_fullscreen_exit_black_48dp.png | Bin 0 -> 111 bytes .../icons/baseline_pause_black_48dp.png | Bin 0 -> 107 bytes .../icons/baseline_stop_black_48dp.png | Bin 0 -> 107 bytes .../icons/ic_favorite_black_18dp_1x.png | Bin 214 -> 0 bytes .../ic_favorite_border_black_18dp_1x.png | Bin 261 -> 0 bytes .../icons/ic_fullscreen_black_24dp_1x.png | Bin 90 -> 0 bytes .../ic_fullscreen_exit_black_24dp_1x.png | Bin 93 -> 0 bytes .../icons/ic_pause_black_24dp_1x.png | Bin 81 -> 0 bytes .../icons/ic_play_arrow_black_18dp_1x.png | Bin 131 -> 0 bytes .../icons/ic_play_arrow_black_24dp_1x.png | Bin 150 -> 0 bytes .../icons/ic_play_arrow_white_18dp_1x.png | Bin 135 -> 0 bytes .../icons/ic_search_black_18dp_1x.png | Bin 215 -> 0 bytes .../icons/ic_skip_next_black_18dp_1x.png | Bin 128 -> 0 bytes .../icons/ic_skip_next_white_18dp_1x.png | Bin 134 -> 0 bytes .../icons/ic_skip_previous_black_18dp_1x.png | Bin 131 -> 0 bytes .../icons/ic_skip_previous_white_18dp_1x.png | Bin 138 -> 0 bytes .../resources/icons/ic_stop_black_24dp_1x.png | Bin 82 -> 0 bytes 26 files changed, 102 insertions(+), 417 deletions(-) create mode 100644 src/main/resources/icons/baseline_fullscreen_black_48dp.png create mode 100644 src/main/resources/icons/baseline_fullscreen_exit_black_48dp.png create mode 100755 src/main/resources/icons/baseline_pause_black_48dp.png create mode 100755 src/main/resources/icons/baseline_stop_black_48dp.png delete mode 100644 src/main/resources/icons/ic_favorite_black_18dp_1x.png delete mode 100644 src/main/resources/icons/ic_favorite_border_black_18dp_1x.png delete mode 100644 src/main/resources/icons/ic_fullscreen_black_24dp_1x.png delete mode 100644 src/main/resources/icons/ic_fullscreen_exit_black_24dp_1x.png delete mode 100644 src/main/resources/icons/ic_pause_black_24dp_1x.png delete mode 100644 src/main/resources/icons/ic_play_arrow_black_18dp_1x.png delete mode 100644 src/main/resources/icons/ic_play_arrow_black_24dp_1x.png delete mode 100644 src/main/resources/icons/ic_play_arrow_white_18dp_1x.png delete mode 100644 src/main/resources/icons/ic_search_black_18dp_1x.png delete mode 100644 src/main/resources/icons/ic_skip_next_black_18dp_1x.png delete mode 100644 src/main/resources/icons/ic_skip_next_white_18dp_1x.png delete mode 100644 src/main/resources/icons/ic_skip_previous_black_18dp_1x.png delete mode 100644 src/main/resources/icons/ic_skip_previous_white_18dp_1x.png delete mode 100644 src/main/resources/icons/ic_stop_black_24dp_1x.png diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index 5c5c88d..beeeb0f 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -38,7 +38,6 @@ import java.util.ResourceBundle; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -51,31 +50,21 @@ import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXColorPicker; import com.jfoenix.controls.JFXHamburger; import com.jfoenix.controls.JFXSlider; -import com.jfoenix.controls.JFXTextField; import com.jfoenix.controls.JFXToggleButton; import com.jfoenix.transitions.hamburger.HamburgerBackArrowBasicTransition; import javafx.animation.TranslateTransition; -import javafx.application.Platform; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.fxml.FXML; -import javafx.scene.Node; import javafx.scene.control.ChoiceBox; -import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; -import javafx.scene.control.MenuItem; import javafx.scene.control.ScrollPane; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.TreeItem; -import javafx.scene.control.TreeTableColumn; -import javafx.scene.control.TreeTableColumn.SortType; -import javafx.scene.control.TreeTableView; import javafx.scene.effect.BoxBlur; import javafx.scene.control.ScrollPane.ScrollBarPolicy; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; import javafx.scene.input.MouseEvent; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.FlowPane; @@ -83,9 +72,6 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import javafx.scene.text.Font; -import javafx.scene.text.FontWeight; -import javafx.scene.text.Text; -import javafx.scene.text.TextFlow; import javafx.stage.DirectoryChooser; import javafx.stage.FileChooser; import javafx.stage.Stage; @@ -101,21 +87,23 @@ import kellerkinder.HomeFlix.player.Player; public class MainWindowController { - // general/settings fxml elements + // general @FXML private AnchorPane mainAnchorPane; - @FXML private ScrollPane settingsScrollPane; - @FXML private JFXHamburger menuHam; @FXML private HBox topHBox; @FXML private VBox sideMenuVBox; - @FXML private TableView sourcesTable; + + @FXML private JFXHamburger menuHam; @FXML private JFXButton aboutBtn; @FXML private JFXButton settingsBtn; + + // settings + @FXML private ScrollPane settingsScrollPane; + @FXML private JFXButton updateBtn; @FXML private JFXButton addDirectoryBtn; @FXML private JFXButton addStreamSourceBtn; - @FXML private JFXToggleButton autoUpdateToggleBtn; @FXML private JFXToggleButton autoplayToggleBtn; @@ -125,7 +113,7 @@ public class MainWindowController { @FXML private ChoiceBox branchChoisBox = new ChoiceBox<>(); @FXML private JFXSlider fontsizeSlider; - + @FXML private Label homeflixSettingsLbl; @FXML private Label mainColorLbl; @FXML private Label fontsizeLbl; @@ -135,31 +123,10 @@ public class MainWindowController { @FXML private Label sourcesLbl; @FXML private Label versionLbl; + @FXML private TableView sourcesTable; @FXML private TreeItem sourceRoot = new TreeItem<>(new SourceDataType("", "")); @FXML private TableColumn sourceColumn; @FXML private TableColumn modeColumn; - - // table-mode - @FXML private AnchorPane tableModeAnchorPane; - @FXML private JFXTextField searchTextField; - - @FXML private TreeTableView filmsTreeTable; - @FXML private TreeTableColumn columnStreamUrl; - @FXML private TreeTableColumn columnTitle; - @FXML private TreeTableColumn columnFavorite; - @FXML private TreeTableColumn columnSeason; - @FXML private TreeTableColumn columnEpisode; - @FXML private TreeItem filmRoot = new TreeItem<>(new FilmTabelDataType("", "", "", "", false, null)); - - - @FXML private ScrollPane textScrollPane; - @FXML private TextFlow textFlow; - @FXML private ImageView posterImageView; - - @FXML private JFXButton playbtn; - @FXML private JFXButton openfolderbtn; - @FXML private JFXButton returnBtn; - @FXML private JFXButton forwardBtn; // poster-mode @FXML private ScrollPane posterModeScrollPane; @@ -177,27 +144,18 @@ public class MainWindowController { private boolean menuTrue = false; - private final String version = "0.7.0"; - private final String buildNumber = "169"; + private final String version = "0.7.90"; + private final String buildNumber = "171"; private final String versionName = "toothless dragon"; private String btnStyle; - private final int hashA = -647380320; - private int last; - private int indexTable; - private int indexList; - private int next; private FilmTabelDataType currentTableFilm = new FilmTabelDataType("", "", "", "", false, null); private ObservableList languages = FXCollections.observableArrayList("English (en_US)", "Deutsch (de_DE)"); private ObservableList branches = FXCollections.observableArrayList("stable", "beta"); - private ObservableList filterData = FXCollections.observableArrayList(); private ObservableList filmsList = FXCollections.observableArrayList(); private ObservableList posterEmenents = FXCollections.observableArrayList(); private static ObservableList sourcesList = FXCollections.observableArrayList(); - private MenuItem like = new MenuItem("like"); - private MenuItem dislike = new MenuItem("dislike"); - private ContextMenu menu = new ContextMenu(like, dislike); private LocalDate lastValidCache = LocalDate.now().minusDays(30); // current date - 30 days is the last valid cache date public MainWindowController() { @@ -232,12 +190,10 @@ public class MainWindowController { initUI(); initActions(); dbController.init(); - refreshAllFilms(); - // load sources list in gui + // load data list in gui addSourceToTable(); - - posterModeStartup(); // TODO testing DO NOT USE THIS!! + posterModeStartup(); } // Initialize general UI elements @@ -271,22 +227,6 @@ public class MainWindowController { * Tabel-Mode */ private void initTabel() { - - // film Table - filmsTreeTable.setRoot(filmRoot); - filmsTreeTable.setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY); - filmsTreeTable.setShowRoot(false); - - // write content into cell - columnStreamUrl.setCellValueFactory(cellData -> cellData.getValue().getValue().streamUrlProperty()); - columnTitle.setCellValueFactory(cellData -> cellData.getValue().getValue().titleProperty()); - columnSeason.setCellValueFactory(cellData -> cellData.getValue().getValue().seasonProperty()); - columnEpisode.setCellValueFactory(cellData -> cellData.getValue().getValue().episodeProperty()); - columnFavorite.setCellValueFactory(cellData -> cellData.getValue().getValue().imageProperty()); - - // context menu for treeTableViewfilm - filmsTreeTable.setContextMenu(menu); - // sourcesTreeTable sourceColumn.setCellValueFactory(cellData -> cellData.getValue().pathProperty()); modeColumn.setCellValueFactory(cellData -> cellData.getValue().modeProperty()); @@ -334,111 +274,10 @@ public class MainWindowController { fontsizeSlider.valueProperty().addListener(e -> { XMLController.setFontSize(fontsizeSlider.getValue()); - if (!getCurrentTitle().isEmpty()) { - setSelectedFilmInfo(dbController.readCache(getCurrentStreamUrl())); - } - xmlController.saveSettings(); - }); - - // Table-Mode actions - searchTextField.textProperty().addListener((e, oldValue, newValue) -> { - filmRoot.getChildren().clear(); - filterData.clear(); - filterData = filmsList.stream() - .filter(x -> x.getTitle().toLowerCase().contains(searchTextField.getText().toLowerCase())) - .collect(Collectors.toCollection(FXCollections::observableArrayList)); - - addFilmsToTable(filterData); - - if (searchTextField.getText().hashCode() == hashA) { - XMLController.setColor("000000"); - colorPicker.setValue(new Color(0, 0, 0, 1)); - applyColor(); - } - }); - - like.setOnAction(e -> { - dbController.like(getCurrentStreamUrl()); - filmsList.set(indexList, dbController.getStream(getCurrentStreamUrl())); - refreshTableElement(); - }); - - dislike.setOnAction(e -> { - dbController.dislike(getCurrentStreamUrl()); - filmsList.set(indexList, dbController.getStream(getCurrentStreamUrl())); - refreshTableElement(); - }); - - // FIXME fix bug when sort by ASCENDING, wrong order - columnFavorite.sortTypeProperty().addListener((e, paramT1, paramT2) -> { - filmRoot.getChildren().clear(); - filterData.clear(); - - if (paramT2.equals(SortType.DESCENDING)) { - // add favorites at the top - for (FilmTabelDataType film : filmsList) { - if (film.getFavorite()) { - filterData.add(0, film); - } else { - filterData.add(film); - } - } - } else { - // add favorites at the bottom - for (FilmTabelDataType film : filmsList) { - if (!film.getFavorite()) { - filterData.add(0, film); - } else { - filterData.add(film); - } - } - } - - addFilmsToTable(filterData); - }); - - // Change-listener for treeTableViewfilm - - filmsTreeTable.getSelectionModel().selectedItemProperty().addListener((e, paramT1, paramT2) -> { - if (filmsTreeTable.getSelectionModel().getSelectedItem() == null) { - return; - } - currentTableFilm = filmsTreeTable.getSelectionModel().getSelectedItem().getValue(); // set the current film object - indexTable = filmsTreeTable.getSelectionModel().getSelectedIndex(); // get selected items table index - last = indexTable - 1; - next = indexTable + 1; - - for (FilmTabelDataType film : filmsList) { - if (film.equals(currentTableFilm)) { - indexList = filmsList.indexOf(film); // get selected items list index - } - } - - if ((dbController.getCacheDate(getCurrentStreamUrl()).isAfter(lastValidCache) || getCurrentStreamUrl().contains("_rootNode")) - && dbController.searchCacheByURL(getCurrentStreamUrl())) { - LOGGER.info("loading from cache: " + getCurrentTitle()); - setSelectedFilmInfo(dbController.readCache(getCurrentStreamUrl())); - } else { - // this is not perfect! - new Thread(() -> { - Thread omdbAPIThread = new Thread(new OMDbAPIController(dbController, currentTableFilm, XMLController.getOmdbAPIKey())); - omdbAPIThread.setName("OMDbAPI"); - omdbAPIThread.start(); - - synchronized (omdbAPIThread) { - try { - omdbAPIThread.wait(); - } catch (InterruptedException e1) { - LOGGER.error(e1); - } - // update the GUI for the selected film - Platform.runLater(() -> { - setSelectedFilmInfo(dbController.readCache(getCurrentStreamUrl())); - }); - } - }).start(); - } + // TODO add functionality for postermode + + xmlController.saveSettings(); }); // Poster-Mode actions @@ -510,16 +349,6 @@ public class MainWindowController { } } - @FXML - private void returnBtnclicked() { - filmsTreeTable.getSelectionModel().select(last); - } - - @FXML - private void forwardBtnclicked() { - filmsTreeTable.getSelectionModel().select(next); - } - // general fxml actions @FXML private void aboutBtnAction() { @@ -585,11 +414,6 @@ public class MainWindowController { xmlController.saveSettings(); } - // refresh the selected child of the root node - private void refreshTableElement() { - filmRoot.getChildren().get(indexTable).setValue(filmsList.get(indexList)); - } - /** * refresh all films in filmsList and in filmsTable clear the FilmsList and * FilmRoot children, then update the database @@ -598,65 +422,6 @@ public class MainWindowController { filmsList.clear(); dbController.refreshDataBase(); // refreshes the database after a source path was added filmsList = dbController.getStreamsList(); // returns a list of all films stored in the database - - // refresh filmRoot in filmsTreeTable - filmRoot.getChildren().clear(); - addFilmsToTable(filmsList); - - // - } - - /** - * TODO rework! add data from a ObservableList to the films-table - * - * @param elementsList a list of elements you want to add - */ - public void addFilmsToTable(ObservableList elementsList) { - - for (FilmTabelDataType element : elementsList) { - - // only if the entry contains a season and a episode it's a valid series - if (!element.getSeason().isEmpty() && !element.getEpisode().isEmpty()) { - - // if there is any node check if it's the root node to the episode - // else there is no node at all so we must create a new - if (filmRoot.getChildren().size() > 0) { - for (int i = 0; i < filmRoot.getChildren().size(); i++) { - // if a root node exists, add element - // else create a new root node and add the element (if rootNode is the last - // node) (works since we edit the element!) - if (filmRoot.getChildren().get(i).getValue().getTitle().equals(element.getTitle())) { - TreeItem episodeNode = new TreeItem<>( - new FilmTabelDataType(element.getStreamUrl(), element.getTitle(), - element.getSeason(), element.getEpisode(), element.getFavorite(), - element.getImage())); - filmRoot.getChildren().get(i).getChildren().add(episodeNode); - } else if (filmRoot.getChildren().get(i).nextSibling() == null) { - TreeItem seriesRootNode = new TreeItem<>( - new FilmTabelDataType(element.getTitle() + "_rootNode", element.getTitle(), "", "", - element.getFavorite(), element.getImage())); - filmRoot.getChildren().add(seriesRootNode); - } - } - } else { - // add new root node - TreeItem seriesRootNode = new TreeItem<>( - new FilmTabelDataType(element.getTitle() + "_rootNode", element.getTitle(), "", "", - element.getFavorite(), element.getImage())); - filmRoot.getChildren().add(seriesRootNode); - - // add new element - TreeItem episodeNode = new TreeItem<>(new FilmTabelDataType( - element.getStreamUrl(), element.getTitle(), element.getSeason(), element.getEpisode(), - element.getFavorite(), element.getImage())); - filmRoot.getChildren().get(0).getChildren().add(episodeNode); - } - - } else { - // if season and episode are empty, we can assume the object is a film - filmRoot.getChildren().add(new TreeItem(element)); - } - } } // add a all elements of sourcesList to the sources table on the settings pane @@ -715,20 +480,12 @@ public class MainWindowController { btnStyle = "-fx-button-type: RAISED; -fx-background-color: #" + XMLController.getColor() + "; -fx-text-fill: WHITE;"; menuBtnStyle = "-fx-text-fill: WHITE;"; - playbtn.setGraphic(new ImageView(new Image("icons/ic_play_arrow_white_18dp_1x.png"))); - returnBtn.setGraphic(new ImageView(new Image("icons/ic_skip_previous_white_18dp_1x.png"))); - forwardBtn.setGraphic(new ImageView(new Image("icons/ic_skip_next_white_18dp_1x.png"))); - menuHam.getStyleClass().clear(); menuHam.getStyleClass().add("jfx-hamburgerW"); } else { btnStyle = "-fx-button-type: RAISED; -fx-background-color: #" + XMLController.getColor() + "; -fx-text-fill: BLACK;"; menuBtnStyle = "-fx-text-fill: BLACK;"; - playbtn.setGraphic(new ImageView(new Image("icons/ic_play_arrow_black_18dp_1x.png"))); - returnBtn.setGraphic(new ImageView(new Image("icons/ic_skip_previous_black_18dp_1x.png"))); - forwardBtn.setGraphic(new ImageView(new Image("icons/ic_skip_next_black_18dp_1x.png"))); - menuHam.getStyleClass().clear(); menuHam.getStyleClass().add("jfx-hamburgerB"); } @@ -736,16 +493,11 @@ public class MainWindowController { // boxes and TextFields sideMenuVBox.setStyle("-fx-background-color: #" + XMLController.getColor() + ";"); topHBox.setStyle("-fx-background-color: #" + XMLController.getColor() + ";"); - searchTextField.setFocusColor(Color.valueOf(XMLController.getColor())); // normal buttons addDirectoryBtn.setStyle(btnStyle); addStreamSourceBtn.setStyle(btnStyle); updateBtn.setStyle(btnStyle); - playbtn.setStyle(btnStyle); - openfolderbtn.setStyle(btnStyle); - returnBtn.setStyle(btnStyle); - forwardBtn.setStyle(btnStyle); // menu buttons settingsBtn.setStyle(menuBtnStyle); @@ -793,8 +545,6 @@ public class MainWindowController { aboutBtn.setText(XMLController.getLocalBundle().getString("info")); settingsBtn.setText(XMLController.getLocalBundle().getString("settings")); - searchTextField.setPromptText(XMLController.getLocalBundle().getString("tfSearch")); - openfolderbtn.setText(XMLController.getLocalBundle().getString("openFolder")); updateBtn.setText(XMLController.getLocalBundle().getString("checkUpdates")); addDirectoryBtn.setText(XMLController.getLocalBundle().getString("addDirectory")); addStreamSourceBtn.setText(XMLController.getLocalBundle().getString("addStreamSource")); @@ -805,69 +555,6 @@ public class MainWindowController { autoUpdateToggleBtn.setText(XMLController.getLocalBundle().getString("autoUpdate")); autoplayToggleBtn.setText(XMLController.getLocalBundle().getString("autoplay")); branchLbl.setText(XMLController.getLocalBundle().getString("branchLbl")); - columnStreamUrl.setText(XMLController.getLocalBundle().getString("columnStreamUrl")); - columnTitle.setText(XMLController.getLocalBundle().getString("columnName")); - columnFavorite.setText(XMLController.getLocalBundle().getString("columnFavorite")); - columnSeason.setText(XMLController.getLocalBundle().getString("columnSeason")); - columnEpisode.setText(XMLController.getLocalBundle().getString("columnEpisode")); - } - - private void setSelectedFilmInfo(String[] cacheData) { - Font font = Font.font("System", FontWeight.BOLD, (int) Math.round(XMLController.getFontSize())); - ObservableList textFlow = getTextFlow().getChildren(); - - // TODO this should move! *** - Text[] nameText = new Text[20]; - nameText[0] = new Text(XMLController.getLocalBundle().getString("title") + ": "); - nameText[1] = new Text(XMLController.getLocalBundle().getString("year") + ": "); - nameText[2] = new Text(XMLController.getLocalBundle().getString("rated") + ": "); - nameText[3] = new Text(XMLController.getLocalBundle().getString("released") + ": "); - nameText[4] = new Text(XMLController.getLocalBundle().getString("season") + ": "); - nameText[5] = new Text(XMLController.getLocalBundle().getString("episode") + ": "); - nameText[6] = new Text(XMLController.getLocalBundle().getString("runtime") + ": "); - nameText[7] = new Text(XMLController.getLocalBundle().getString("genre") + ": "); - nameText[8] = new Text(XMLController.getLocalBundle().getString("directors") + ": "); - nameText[9] = new Text(XMLController.getLocalBundle().getString("writers") + ": "); - nameText[10] = new Text(XMLController.getLocalBundle().getString("actors") + ": "); - nameText[11] = new Text(XMLController.getLocalBundle().getString("plot") + ": "); - nameText[12] = new Text(XMLController.getLocalBundle().getString("language") + ": "); - nameText[13] = new Text(XMLController.getLocalBundle().getString("country") + ": "); - nameText[14] = new Text(XMLController.getLocalBundle().getString("awards") + ": "); - nameText[15] = new Text(XMLController.getLocalBundle().getString("metascore") + ": "); - nameText[16] = new Text(XMLController.getLocalBundle().getString("imdbRating") + ": "); - nameText[17] = new Text(XMLController.getLocalBundle().getString("type") + ": "); - nameText[18] = new Text(XMLController.getLocalBundle().getString("boxOffice") + ": "); - nameText[19] = new Text(XMLController.getLocalBundle().getString("website") + ": "); - // *** - - // set the correct font for the nameText - for (Text text : nameText) { - text.setFont(font); - } - - // clear the textFlow and add the new text - textFlow.clear(); - - for (int i = 0; i < 20; i++) { - // if the cacheData exists and they are not empty add the text - if(cacheData[i] != null && cacheData[i].length() > 0) { - textFlow.addAll(nameText[i], new Text(cacheData[i] + "\n")); - } - } - - getTextFlow().setStyle("-fx-font-size : " + ((int) Math.round(XMLController.getFontSize()) + 1) + "px;"); - - // add the image - try { - if (new File(cacheData[20]).isFile()) { - posterImageView.setImage(new Image(new File(cacheData[20]).toURI().toString())); - } else { - posterImageView.setImage(new Image(cacheData[20])); - } - } catch (Exception e) { - posterImageView.setImage(new Image("icons/Homeflix_Poster.png")); - LOGGER.error("No Poster found, useing default."); - } } // if AutoUpdate, then check for updates @@ -905,7 +592,8 @@ public class MainWindowController { private void posterModeStartup() { checkAllPosters(); - addGUIElements(); + addAllPosters(); + checkCache(); } /** @@ -918,7 +606,7 @@ public class MainWindowController { for (FilmTabelDataType entry : dbController.getAllNotCachedEntries()) { System.out.println(entry.getStreamUrl() + " is NOT cached!"); - Runnable OMDbAPIWorker = new OMDbAPIController(dbController, entry, XMLController.getOmdbAPIKey()); + Runnable OMDbAPIWorker = new OMDbAPIController(entry, XMLController.getOmdbAPIKey()); executor.execute(OMDbAPIWorker); } executor.shutdown(); @@ -940,7 +628,7 @@ public class MainWindowController { /** * add all cached films/series to the PosterMode GUI */ - private void addGUIElements() { + private void addAllPosters() { // refresh the posterModeElements list posterEmenents.clear(); posterEmenents = dbController.getPosterElementsList(); // returns a list of all PosterElements stored in the database @@ -949,19 +637,16 @@ public class MainWindowController { for (PosterModeElement element : posterEmenents) { element.getButton().addEventHandler(MouseEvent.MOUSE_CLICKED, (event) -> { enableBlur(); // blur the FlowPane + System.out.println("selected: " + element.getStreamURL()); // if the selected element is a file it's a film, else a series - if (new File(element.getStreamURL()).isFile()) { + if (new File(element.getStreamURL()).isFile() || element.getStreamURL().contains("http")) { filmDetailViewController.setFilm(element.getStreamURL()); filmDetailViewController.showPane(); } else { -// filmDetailViewController.setFilm(element.getStreamURL()); -// filmDetailViewController.showPane(); seriesDetailViewController.setSeries(element.getStreamURL()); seriesDetailViewController.showPane(); } - - System.out.println("selected: " + element.getStreamURL()); }); } @@ -971,6 +656,25 @@ public class MainWindowController { System.out.println("added gui elements"); } + // TODO can this be done in dbController? + /** + * check if the cache is to old, if so update asynchron + */ + private void checkCache() { + ExecutorService executor = Executors.newFixedThreadPool(5); + + // TODO if filmlist is not used anymore, it cann be removed + for(FilmTabelDataType entry : filmsList) { + if (dbController.getCacheDate(entry.getStreamUrl()).isBefore(lastValidCache)) { + System.out.println(entry.getTitle() + " chached on: " + dbController.getCacheDate(entry.getStreamUrl())); + Runnable OMDbAPIWorker = new OMDbAPIController(entry, XMLController.getOmdbAPIKey()); + executor.execute(OMDbAPIWorker); + } + } + + executor.shutdown(); + } + private void enableBlur() { BoxBlur boxBlur = new BoxBlur(); boxBlur.setWidth(9); @@ -997,22 +701,10 @@ public class MainWindowController { return currentTableFilm.getStreamUrl(); } - public int getIndexTable() { - return indexTable; - } - - public int getIndexList() { - return indexList; - } - public static ObservableList getSourcesList() { return sourcesList; } - public TextFlow getTextFlow() { - return textFlow; - } - public JFXButton getUpdateBtn() { return updateBtn; } diff --git a/src/main/java/kellerkinder/HomeFlix/application/SeriesDetailView.java b/src/main/java/kellerkinder/HomeFlix/application/SeriesDetailView.java index 7e0bf5b..dbdf814 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/SeriesDetailView.java +++ b/src/main/java/kellerkinder/HomeFlix/application/SeriesDetailView.java @@ -67,7 +67,7 @@ public class SeriesDetailView { public void initialize() { dbController = DBController.getInstance(); seriesDVPane.setStyle("-fx-background-color: rgba(89,89,89,0.9);"); - scrollPaneEpisodes.setStyle("-fx-background-color: transparent;"); + scrollPaneEpisodes.setStyle("-fx-background-color: transparent;"); scrollPaneEpisodes.setHbarPolicy(ScrollBarPolicy.ALWAYS); cbSeason.getSelectionModel().selectedIndexProperty().addListener((e, oldValue, newValue) -> { diff --git a/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java b/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java index a746558..af156ec 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java @@ -55,10 +55,10 @@ public class OMDbAPIController implements Runnable { * @param currentTableFilm the current film object * @param omdbAPIKey the omdbAPI key */ - public OMDbAPIController(DBController dbController, FilmTabelDataType currentTableFilm, String omdbAPIKey) { - this.dbController = dbController; + public OMDbAPIController(FilmTabelDataType currentTableFilm, String omdbAPIKey) { this.currentTableFilm = currentTableFilm; this.omdbAPIKey = omdbAPIKey; + dbController = DBController.getInstance(); } @Override diff --git a/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java b/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java index 54e1db8..39eed19 100644 --- a/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java +++ b/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java @@ -65,6 +65,10 @@ public class PlayerController { @FXML private JFXButton playBtn; @FXML private JFXButton fullscreenBtn; @FXML private JFXButton nextEpBtn; + + @FXML private ImageView stopIcon; + @FXML private ImageView playIcon; + @FXML private ImageView fullscreenIcon; private Player player; private Media media; @@ -82,11 +86,10 @@ public class PlayerController { private boolean showControls = true; private boolean autoplay; - private ImageView stop_black = new ImageView(new Image("icons/ic_stop_black_24dp_1x.png")); - private ImageView play_arrow_black = new ImageView(new Image("icons/ic_play_arrow_black_24dp_1x.png")); - private ImageView pause_black = new ImageView(new Image("icons/ic_pause_black_24dp_1x.png")); - private ImageView fullscreen_black = new ImageView(new Image("icons/ic_fullscreen_black_24dp_1x.png")); - private ImageView fullscreen_exit_black = new ImageView(new Image("icons/ic_fullscreen_exit_black_24dp_1x.png")); + private Image playArrow = new Image("icons/baseline_play_arrow_black_48dp.png"); + private Image pause = new Image("icons/baseline_pause_black_48dp.png"); + private Image fullscreen = new Image("icons/baseline_fullscreen_black_48dp.png"); + private Image fullscreenExit = new Image("icons/baseline_fullscreen_exit_black_48dp.png"); /** * create a new PlayerWindow object @@ -130,9 +133,8 @@ public class PlayerController { initMediaPlayer(); // set the control elements to the correct value - stopBtn.setGraphic(stop_black); - playBtn.setGraphic(pause_black); - fullscreenBtn.setGraphic(fullscreen_exit_black); + playIcon.setImage(pause); + fullscreenIcon.setImage(fullscreenExit); timeSlider.setValue(0); } @@ -176,7 +178,7 @@ public class PlayerController { // if we are 120ms to the end stop the media mediaPlayer.stop(); DBController.getInstance().setCurrentTime(film.getStreamUrl(), 0); // reset old video start time - playBtn.setGraphic(play_arrow_black); + playIcon.setImage(playArrow); } else { if (nextEpBtn.isVisible()) nextEpBtn.setVisible(false); @@ -262,10 +264,10 @@ public class PlayerController { void fullscreenBtnAction(ActionEvent event) { if (player.getStage().isFullScreen()) { player.getStage().setFullScreen(false); - fullscreenBtn.setGraphic(fullscreen_black); + fullscreenIcon.setImage(fullscreen); } else { player.getStage().setFullScreen(true); - fullscreenBtn.setGraphic(fullscreen_exit_black); + fullscreenIcon.setImage(fullscreenExit); } } @@ -273,10 +275,10 @@ public class PlayerController { void playBtnAction(ActionEvent event) { if (mediaPlayer.getStatus().equals(Status.PLAYING)) { mediaPlayer.pause(); - playBtn.setGraphic(play_arrow_black); + playIcon.setImage(playArrow); } else { mediaPlayer.play(); - playBtn.setGraphic(pause_black); + playIcon.setImage(pause); } } diff --git a/src/main/resources/css/MainWindow.css b/src/main/resources/css/MainWindow.css index 3a799cc..0aff210 100644 --- a/src/main/resources/css/MainWindow.css +++ b/src/main/resources/css/MainWindow.css @@ -129,7 +129,13 @@ -fx-background-insets: 0.0; } -.scroll-bar:vertical > .thumb, .scroll-bar:horizontal > .thumb { +.scroll-bar:vertical > .thumb { + -fx-background-color: #BCBCBC; + -fx-background-insets: 0.0; + -fx-background-radius: 15.0; +} + +.scroll-bar:horizontal > .thumb { -fx-background-color: #FFFFFF; -fx-background-insets: 0.0; -fx-background-radius: 15.0; diff --git a/src/main/resources/fxml/MainWindow.fxml b/src/main/resources/fxml/MainWindow.fxml index e710025..239c5d2 100644 --- a/src/main/resources/fxml/MainWindow.fxml +++ b/src/main/resources/fxml/MainWindow.fxml @@ -4,7 +4,6 @@ - @@ -13,58 +12,14 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/resources/fxml/PlayerWindow.fxml b/src/main/resources/fxml/PlayerWindow.fxml index cfbdd27..40a58b4 100644 --- a/src/main/resources/fxml/PlayerWindow.fxml +++ b/src/main/resources/fxml/PlayerWindow.fxml @@ -3,13 +3,15 @@ + + - + @@ -25,15 +27,39 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/fxml/SeriesDetailView.fxml b/src/main/resources/fxml/SeriesDetailView.fxml index 555c79f..be1a12b 100644 --- a/src/main/resources/fxml/SeriesDetailView.fxml +++ b/src/main/resources/fxml/SeriesDetailView.fxml @@ -122,12 +122,16 @@ - + - + + + + + diff --git a/src/main/resources/icons/baseline_fullscreen_black_48dp.png b/src/main/resources/icons/baseline_fullscreen_black_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..917e418a93386a61338d7c2588985ed9805841b8 GIT binary patch literal 109 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}a}t6Hgb%kP61PS2uD3C0q}_*T{?$jEqLr9g*H)i0){4eU%4|1&-`+It_UpTX1B&t;uc GLK6VvuOPhu literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/baseline_fullscreen_exit_black_48dp.png b/src/main/resources/icons/baseline_fullscreen_exit_black_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..5fc3166ac68a482f4b8c92622584292a5c3085a2 GIT binary patch literal 111 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}a}tGfx-CkP61PR~5M!6hvGc-`Bs) z->55*v&s0jOND2QL#yTPx!fE}PTJn}WICX(A9-NFX{L$)8RM24-3FS#;OXk; Jvd$@?2>|5vARzz% literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/baseline_pause_black_48dp.png b/src/main/resources/icons/baseline_pause_black_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..16d26b3363290ca3732ec6cad758f8f80e505da3 GIT binary patch literal 107 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDH3?y^UWFG-iYymzYu0R?HmZtAK52P4Ng8YIR y9G=}s19F5tT^vIy7?T$WJ@|j%e_}@P6-I_O0rQi$1J3t@^mw}ZxvXpK{{6!)-S*OcGq*Cn!3~ZKRy-bCIy+5 zaM-=xq^i2i^;m=WtsM8H|C8n1CbQq#Xw7>>;_Xci!|!g!M=nTz6)ep=_r!}g`Bjfv zL|1L%B!LNng+IOgTAqYhKA+UVB-&?YDex#DP4{G%kX)B4w@CdbwmV;XQrE92+68nd NgQu&X%Q~loCIBYqRbl`D diff --git a/src/main/resources/icons/ic_favorite_border_black_18dp_1x.png b/src/main/resources/icons/ic_favorite_border_black_18dp_1x.png deleted file mode 100644 index 7edb675069f3b54a74cf4dfa518600d0a7184fb4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 261 zcmV+g0s8)lP)B3901UBO}ED6Ya_3~g01orN+PqKlxUQ)@R1i_JzE>0W+96yi|H!lNn|Tu8)o|1 z&P@NTIFE4rxj7AO=N7?|3vCUvdaU{O?$NVwsVO{p@aD@3z?4t7CQ!03gk3`zSNJdq ztP3*$>b3w(3pL0&EL)gLZ^X05VrPmi4!EHT9{u z?_E*VQ79WQ;!al$Z0IUfBn)c_dya%_WodF}LO50Oi_D2*nSb&FB$qu_tZB1&00000 LNkvXXu0mjf2bgML diff --git a/src/main/resources/icons/ic_fullscreen_black_24dp_1x.png b/src/main/resources/icons/ic_fullscreen_black_24dp_1x.png deleted file mode 100644 index 3553d6a5d6c410575bb658b48e95e6a500f49e2c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 90 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_*1MNb#UkP60RiI)%Td6K#U0-41e n)OimJ^3-g&q^R4Q!@v;bW`FSMsV|R!iWxjz{an^LB{Ts5YOxpV diff --git a/src/main/resources/icons/ic_fullscreen_exit_black_24dp_1x.png b/src/main/resources/icons/ic_fullscreen_exit_black_24dp_1x.png deleted file mode 100644 index c8394487c9c3e539520d57b899313ab0779ed6ab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 93 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_*16;Bt(kP60RhQ`1T@*ERyFt2`S q&y&E=&B# wc=B>S&sUzDB`$N=G(DB)Hcp&$#GH%4Jltn(&bImcfEF=$y85}Sb4q9e09LOpNdN!< diff --git a/src/main/resources/icons/ic_play_arrow_white_18dp_1x.png b/src/main/resources/icons/ic_play_arrow_white_18dp_1x.png deleted file mode 100644 index cae85ab67ad6d2bf814dfd750e71d3f2218ee45f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 135 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+0wn(&ce?|m{5)M8Lp07OCrDU)nBOkp%KyJw zIE;l!`+v7^*q8tRcmF@xBHDQ1-~UxDqJRJY|66~xKI~AJ7N_Pd_Y=Fy|4-(ytYKZK jQ1Rn_x79_bY6gb=Hc78ko_tmYn#|zo>gTe~DWM4f3@S8s diff --git a/src/main/resources/icons/ic_search_black_18dp_1x.png b/src/main/resources/icons/ic_search_black_18dp_1x.png deleted file mode 100644 index f0d4e97577508554557c9073d1607218a48a9215..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 215 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+0wn(&ce?|m=6Sj}hEy$!Cb2S zz#@wSU0t@Pto$0DvY0ANJfO+Jc_%@IDc0TTlGYW4ge=dxOQv65K9}b?`&LHRx8cR% ztCu6g?)Y}Lt=ngk@Z`Zl(KEKD3~YMen&xf}*ET;G_$_N8$FwfNh9@fAYVY{i6n=)3 zDh2b4Yjdk#-9FRh%B|g+o6h-co%%=f<|?zlxr?K|Z{I4PY&&VA`EOR~T>aDCXT#Nj PE@kj^^>bP0l+XkKwR2O_ diff --git a/src/main/resources/icons/ic_skip_next_black_18dp_1x.png b/src/main/resources/icons/ic_skip_next_black_18dp_1x.png deleted file mode 100644 index 327fd8d8af74d4c8d43a73b5a2d9f5fe5e930101..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 128 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+0wn(&ce?|m+&x_!Ln;`PC9XJ3Zj@}jf8YVb zvWqPJ#u~~W|1-N9h%&a?8>VN@ zdUhXI-v9mo!`&YJ|9`*1_u~Km^Z%<0hOsbZ|Kk=6`||()<^SuBXcR5HvFrSQVNT0G i5)Ty=)C|}K85lU`W}d0$oPPpnFoUP7pUXO@geCwHelqO< diff --git a/src/main/resources/icons/ic_skip_previous_black_18dp_1x.png b/src/main/resources/icons/ic_skip_previous_black_18dp_1x.png deleted file mode 100644 index 34c528d3cf9e84a0679632a39a8e9236f456e624..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 131 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+0wn(&ce?|mygXeTLn;`PC9WK3T*%mZ{?LNX zg={OH*bDj3d}tBCvVM_D+j$2$IoCrx*^O)d{}9<}WW##th}NPDRuUd9)^iF31o(W? e5)$~57#KRQX$kL`^k4oXYh3Ob6Mw<&;$VR?=W@% diff --git a/src/main/resources/icons/ic_stop_black_24dp_1x.png b/src/main/resources/icons/ic_stop_black_24dp_1x.png deleted file mode 100644 index 0588f0b4cf92eca1c16aa65c47550dddf8a39099..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 82 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_*1NlzEYkP60R1<9VK{}u{-2CEW7 em^%|h7#LjB`wN$O|Lg=RV(@hJb6Mw<&;$UT`4g=G From 989281ff8dfef95f4b52c33582bebe2bb8e90a85 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Fri, 21 Jun 2019 17:50:56 +0200 Subject: [PATCH 82/88] added SettingsView --- .../HomeFlix/application/SettingsView.java | 83 ++++++++++ src/main/resources/fxml/SettingsView.fxml | 154 ++++++++++++++++++ 2 files changed, 237 insertions(+) create mode 100644 src/main/java/kellerkinder/HomeFlix/application/SettingsView.java create mode 100644 src/main/resources/fxml/SettingsView.fxml diff --git a/src/main/java/kellerkinder/HomeFlix/application/SettingsView.java b/src/main/java/kellerkinder/HomeFlix/application/SettingsView.java new file mode 100644 index 0000000..d5e35bc --- /dev/null +++ b/src/main/java/kellerkinder/HomeFlix/application/SettingsView.java @@ -0,0 +1,83 @@ +package kellerkinder.HomeFlix.application; + +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXColorPicker; +import com.jfoenix.controls.JFXSlider; +import com.jfoenix.controls.JFXToggleButton; + +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.ChoiceBox; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.layout.AnchorPane; +import kellerkinder.HomeFlix.datatypes.SourceDataType; + +public class SettingsView { + + @FXML private ScrollPane settingsScrollPane; + @FXML private AnchorPane settingsAnchorPane; + + @FXML private Label homeflixSettingsLbl; + @FXML private Label mainColorLbl; + @FXML private Label fontsizeLbl; + @FXML private Label languageLbl; + @FXML private Label updateLbl; + @FXML private Label branchLbl; + @FXML private Label versionLbl; + @FXML private Label PlayerLbl; + @FXML private Label sourcesLbl; + + @FXML private JFXColorPicker colorPicker; + @FXML private JFXSlider fontsizeSlider; + + @FXML private ChoiceBox languageChoisBox; + @FXML private ChoiceBox branchChoisBox; + + @FXML private JFXButton updateBtn; + @FXML private JFXButton addStreamSourceBtn; + @FXML private JFXButton addDirectoryBtn; + + @FXML private JFXToggleButton autoUpdateToggleBtn; + @FXML private JFXToggleButton autoplayToggleBtn; + + @FXML private TableView sourcesTable; + @FXML private TableColumn sourceColumn; + @FXML private TableColumn modeColumn; + + public void initialize() { + System.out.println("geht"); + } + + @FXML void addDirectoryBtnAction(ActionEvent event) { + + } + + @FXML + void addStreamSourceBtnAction(ActionEvent event) { + + } + + @FXML + void autoUpdateToggleBtnAction(ActionEvent event) { + + } + + @FXML + void autoplayToggleBtnAction(ActionEvent event) { + + } + + @FXML + void colorPickerAction(ActionEvent event) { + + } + + @FXML + void updateBtnAction(ActionEvent event) { + + } + +} diff --git a/src/main/resources/fxml/SettingsView.fxml b/src/main/resources/fxml/SettingsView.fxml new file mode 100644 index 0000000..d5d8790 --- /dev/null +++ b/src/main/resources/fxml/SettingsView.fxml @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 9af3ad26bdb9676e82b6d00816ed9e0f87360dfa Mon Sep 17 00:00:00 2001 From: Seil0 Date: Fri, 21 Jun 2019 21:16:53 +0200 Subject: [PATCH 83/88] implemented settings as nested conroller --- .../HomeFlix/application/FilmDetailView.java | 22 ++ .../HomeFlix/application/Main.java | 120 +----- .../application/MainWindowController.java | 359 +++++------------- .../application/SeriesDetailView.java | 22 ++ .../HomeFlix/application/SettingsView.java | 238 +++++++++++- .../HomeFlix/controller/DBController.java | 4 +- .../controller/SourcesController.java | 3 - .../HomeFlix/controller/UpdateController.java | 27 +- .../Alerts/JFX2BtnCancelAlert.java | 9 +- src/main/resources/fxml/MainWindow.fxml | 122 +----- src/main/resources/fxml/SettingsView.fxml | 4 +- .../locals/HomeFlix-Local_de_DE.properties | 1 + .../locals/HomeFlix-Local_en_US.properties | 1 + 13 files changed, 405 insertions(+), 527 deletions(-) diff --git a/src/main/java/kellerkinder/HomeFlix/application/FilmDetailView.java b/src/main/java/kellerkinder/HomeFlix/application/FilmDetailView.java index 1dae333..5c23192 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/FilmDetailView.java +++ b/src/main/java/kellerkinder/HomeFlix/application/FilmDetailView.java @@ -1,3 +1,25 @@ +/** + * Project-HomeFlix + * + * Copyright 2016-2019 <@Seil0> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + */ + package kellerkinder.HomeFlix.application; import java.awt.Desktop; diff --git a/src/main/java/kellerkinder/HomeFlix/application/Main.java b/src/main/java/kellerkinder/HomeFlix/application/Main.java index e280d72..1b281aa 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/Main.java +++ b/src/main/java/kellerkinder/HomeFlix/application/Main.java @@ -19,55 +19,38 @@ * MA 02110-1301, USA. * */ + package kellerkinder.HomeFlix.application; import java.io.File; import java.io.IOException; -import java.util.Locale; -import java.util.ResourceBundle; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.kellerkinder.Alerts.JFX2BtnCancelAlert; import javafx.application.Application; -import javafx.event.ActionEvent; -import javafx.event.EventHandler; import javafx.fxml.FXMLLoader; import javafx.scene.Scene; import javafx.scene.image.Image; import javafx.scene.layout.AnchorPane; -import javafx.stage.DirectoryChooser; -import javafx.stage.FileChooser; import javafx.stage.Stage; import kellerkinder.HomeFlix.controller.XMLController; public class Main extends Application { - private Stage primaryStage; private Scene scene; private AnchorPane pane; private MainWindowController mainWindowController; private static XMLController xmlController; - private ResourceBundle bundle; private static Logger LOGGER; + public static final String version = "0.7.90"; + public static final String buildNumber = "171"; + public static final String versionName = "toothless dragon"; @Override public void start(Stage primaryStage) throws IOException { - LOGGER.info("OS: " + XMLController.getOsName() + " " + XMLController.getOsVers() + " " + XMLController.getOsVers()); - LOGGER.info("Java: " + XMLController.getJavaVend() + " " + XMLController.getJavaVers()); - LOGGER.info("User: " + XMLController.getUserName() + " " + XMLController.getUserHome()); - - this.primaryStage = primaryStage; - mainWindow(); - } - - /** - * initialize the mainWindowController, GUI and load the saved settings or call addFirstSource - * initialize the primaryStage and set the file/directory paths - */ - private void mainWindow(){ + //initialize the mainWindowController and the primaryStage try { FXMLLoader loader = new FXMLLoader(); loader.setLocation(getClass().getResource("/fxml/MainWindow.fxml")); @@ -87,22 +70,8 @@ public class Main extends Application { mainWindowController = loader.getController(); //Link of FXMLController and controller class mainWindowController.init(); - - // startup checks - if (!XMLController.getConfigFile().exists()) { - XMLController.getDirHomeFlix().mkdir(); - System.out.println("config not found"); - - addFirstSource(); - xmlController.saveSettings(); - } - - if (!XMLController.getPosterCache().exists()) { - XMLController.getPosterCache().mkdir(); - } - } catch (IOException e) { - LOGGER.error(e); + LOGGER.error("Error while loading in Main", e); } } @@ -125,70 +94,23 @@ public class Main extends Application { logFile.delete(); LOGGER = LogManager.getLogger(Main.class.getName()); + LOGGER.info("OS: " + XMLController.getOsName() + " " + XMLController.getOsVers() + " " + XMLController.getOsVers()); + LOGGER.info("Java: " + XMLController.getJavaVend() + " " + XMLController.getJavaVers()); + LOGGER.info("User: " + XMLController.getUserName() + " " + XMLController.getUserHome()); + xmlController = new XMLController(); - launch(args); - } - - /** TODO this should move - * we need to get the path for the first source from the user and add it to - * sources.json, if the user ends the file-/directory-chooser the program will exit - */ - private void addFirstSource() { - switch (XMLController.getSysLocal()) { - case "en_US": - bundle = ResourceBundle.getBundle("locals.HomeFlix-Local", Locale.US); // us_english - break; - case "de_DE": - bundle = ResourceBundle.getBundle("locals.HomeFlix-Local", Locale.GERMAN); // de_german - break; - default: - bundle = ResourceBundle.getBundle("locals.HomeFlix-Local", Locale.US); // default local - break; + + if (XMLController.getConfigFile().exists()) { + xmlController.loadSettings(); + } else { + xmlController.saveSettings(); + } + + if (!XMLController.getPosterCache().exists()) { + XMLController.getPosterCache().mkdir(); } - JFX2BtnCancelAlert selectFirstSource = new JFX2BtnCancelAlert(bundle.getString("addSourceHeader"), - bundle.getString("addSourceBody"), - "-fx-button-type: RAISED; -fx-background-color: #ee3523; -fx-text-fill: BLACK;", - bundle.getString("addDirectory"), bundle.getString("addStreamSource"), - bundle.getString("cancelBtnText"), primaryStage); - - // directory action - EventHandler btn1Action = new EventHandler() { - @Override - public void handle(ActionEvent event) { - DirectoryChooser directoryChooser = new DirectoryChooser(); - directoryChooser.setTitle(bundle.getString("addDirectory")); - File selectedFolder = directoryChooser.showDialog(primaryStage); - if (selectedFolder != null && selectedFolder.exists()) { - mainWindowController.addSource(selectedFolder.getPath(), "local"); - selectFirstSource.getAlert().close(); - } else { - LOGGER.error("The selected folder dosen't exist!"); - System.exit(1); - } - } - }; - - // streaming action - EventHandler btn2Action = new EventHandler() { - @Override - public void handle(ActionEvent event) { - FileChooser fileChooser = new FileChooser(); - fileChooser.setTitle("addStreamSource"); - File selectedFile = fileChooser.showOpenDialog(primaryStage); - if (selectedFile != null && selectedFile.exists()) { - mainWindowController.addSource(selectedFile.getPath(), "stream"); - selectFirstSource.getAlert().close(); - } else { - LOGGER.error("The selected file dosen't exist!"); - System.exit(1); - } - } - }; - - selectFirstSource.setBtn1Action(btn1Action); - selectFirstSource.setBtn2Action(btn2Action); - selectFirstSource.showAndWait(); + launch(args); } - + } diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index beeeb0f..fd173de 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -41,28 +41,21 @@ import java.util.concurrent.TimeUnit; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.kellerkinder.Alerts.JFX2BtnCancelAlert; import org.kellerkinder.Alerts.JFXInfoAlert; import com.eclipsesource.json.Json; import com.eclipsesource.json.JsonArray; import com.eclipsesource.json.JsonObject; import com.jfoenix.controls.JFXButton; -import com.jfoenix.controls.JFXColorPicker; import com.jfoenix.controls.JFXHamburger; -import com.jfoenix.controls.JFXSlider; -import com.jfoenix.controls.JFXToggleButton; import com.jfoenix.transitions.hamburger.HamburgerBackArrowBasicTransition; import javafx.animation.TranslateTransition; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.fxml.FXML; -import javafx.scene.control.ChoiceBox; -import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; -import javafx.scene.control.TableColumn; -import javafx.scene.control.TableView; -import javafx.scene.control.TreeItem; import javafx.scene.effect.BoxBlur; import javafx.scene.control.ScrollPane.ScrollBarPolicy; import javafx.scene.input.MouseEvent; @@ -70,8 +63,6 @@ import javafx.scene.layout.AnchorPane; import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; -import javafx.scene.paint.Color; -import javafx.scene.text.Font; import javafx.stage.DirectoryChooser; import javafx.stage.FileChooser; import javafx.stage.Stage; @@ -82,7 +73,6 @@ import kellerkinder.HomeFlix.controller.UpdateController; import kellerkinder.HomeFlix.controller.XMLController; import kellerkinder.HomeFlix.datatypes.FilmTabelDataType; import kellerkinder.HomeFlix.datatypes.PosterModeElement; -import kellerkinder.HomeFlix.datatypes.SourceDataType; import kellerkinder.HomeFlix.player.Player; public class MainWindowController { @@ -99,34 +89,7 @@ public class MainWindowController { @FXML private JFXButton settingsBtn; // settings - @FXML private ScrollPane settingsScrollPane; - - @FXML private JFXButton updateBtn; - @FXML private JFXButton addDirectoryBtn; - @FXML private JFXButton addStreamSourceBtn; - @FXML private JFXToggleButton autoUpdateToggleBtn; - @FXML private JFXToggleButton autoplayToggleBtn; - - @FXML private JFXColorPicker colorPicker; - - @FXML private ChoiceBox languageChoisBox = new ChoiceBox<>(); - @FXML private ChoiceBox branchChoisBox = new ChoiceBox<>(); - - @FXML private JFXSlider fontsizeSlider; - - @FXML private Label homeflixSettingsLbl; - @FXML private Label mainColorLbl; - @FXML private Label fontsizeLbl; - @FXML private Label languageLbl; - @FXML private Label updateLbl; - @FXML private Label branchLbl; - @FXML private Label sourcesLbl; - @FXML private Label versionLbl; - - @FXML private TableView sourcesTable; - @FXML private TreeItem sourceRoot = new TreeItem<>(new SourceDataType("", "")); - @FXML private TableColumn sourceColumn; - @FXML private TableColumn modeColumn; + @FXML private SettingsView settingsViewController; // poster-mode @FXML private ScrollPane posterModeScrollPane; @@ -144,18 +107,11 @@ public class MainWindowController { private boolean menuTrue = false; - private final String version = "0.7.90"; - private final String buildNumber = "171"; - private final String versionName = "toothless dragon"; private String btnStyle; private FilmTabelDataType currentTableFilm = new FilmTabelDataType("", "", "", "", false, null); - private ObservableList languages = FXCollections.observableArrayList("English (en_US)", "Deutsch (de_DE)"); - private ObservableList branches = FXCollections.observableArrayList("stable", "beta"); - private ObservableList filmsList = FXCollections.observableArrayList(); private ObservableList posterEmenents = FXCollections.observableArrayList(); - private static ObservableList sourcesList = FXCollections.observableArrayList(); private LocalDate lastValidCache = LocalDate.now().minusDays(30); // current date - 30 days is the last valid cache date public MainWindowController() { @@ -171,66 +127,36 @@ public class MainWindowController { return instance; } - @FXML public void initialize() { instance = this; xmlController = new XMLController(); dbController = DBController.getInstance(); + + if (!new File(XMLController.getDirHomeFlix() + "/sources.json").exists()) { + XMLController.getDirHomeFlix().mkdir(); + LOGGER.warn("sources file not found"); + + addFirstSource(); + xmlController.saveSettings(); + } } public void init() { - LOGGER.info("Initializing Project-HomeFlix build " + buildNumber); - - xmlController.loadSettings(); // load settings - checkAutoUpdate(); + LOGGER.info("Initializing Project-HomeFlix build " + Main.buildNumber); // initialize the GUI and the DBController primaryStage = (Stage) mainAnchorPane.getScene().getWindow(); // set primary stage for dialogs - initTabel(); - initUI(); + posterModeScrollPane.setVbarPolicy(ScrollBarPolicy.ALWAYS); + setLocalUI(); + applyColor(); // TODO only on first start + initActions(); dbController.init(); // load data list in gui - addSourceToTable(); posterModeStartup(); - } - - // Initialize general UI elements - private void initUI() { - //JFXScrollPane.smoothScrolling(posterModeScrollPane); - posterModeScrollPane.setVbarPolicy(ScrollBarPolicy.ALWAYS); - settingsScrollPane.setVbarPolicy(ScrollBarPolicy.ALWAYS); - versionLbl.setText("Version: " + version + " (Build: " + buildNumber + ")"); - fontsizeSlider.setValue(XMLController.getFontSize()); - colorPicker.setValue(Color.valueOf(XMLController.getColor())); - - updateBtn.setFont(Font.font("System", 12)); - autoUpdateToggleBtn.setSelected(XMLController.isAutoUpdate()); - autoplayToggleBtn.setSelected(XMLController.isAutoplay()); - languageChoisBox.setItems(languages); - branchChoisBox.setItems(branches); - - if (XMLController.isUseBeta()) { - branchChoisBox.getSelectionModel().select(1); - } else { - branchChoisBox.getSelectionModel().select(0); - } - - setLocalUI(); - applyColor(); - } - - /** - * Initialize the tables (treeTableViewfilm and sourcesTable) only needed for - * Tabel-Mode - */ - private void initTabel() { - // sourcesTreeTable - sourceColumn.setCellValueFactory(cellData -> cellData.getValue().pathProperty()); - modeColumn.setCellValueFactory(cellData -> cellData.getValue().modeProperty()); - sourcesTable.setItems(sourcesList); + checkAutoUpdate(); // TODO async } // Initializing the actions @@ -250,37 +176,12 @@ public class MainWindowController { burgerTask.play(); menuTrue = true; } - if (settingsScrollPane.isVisible()) { - settingsScrollPane.setVisible(false); - } - }); - - languageChoisBox.getSelectionModel().selectedIndexProperty().addListener((e, oldValue, newValue) -> { - String local = languageChoisBox.getItems().get((int) newValue).toString(); - local = local.substring(local.length() - 6, local.length() - 1); // reading only en_US from English (en_US) - XMLController.setUsrLocal(local); - setLocalUI(); - xmlController.saveSettings(); - }); - - branchChoisBox.getSelectionModel().selectedIndexProperty().addListener((e, oldValue, newValue) -> { - if (branchChoisBox.getItems().get((int) newValue).toString() == "beta") { - XMLController.setUseBeta(true); - } else { - XMLController.setUseBeta(false); - } - xmlController.saveSettings(); - }); - - fontsizeSlider.valueProperty().addListener(e -> { - XMLController.setFontSize(fontsizeSlider.getValue()); - // TODO add functionality for postermode - - xmlController.saveSettings(); - }); - - // Poster-Mode actions + if (settingsViewController.isVisible()) { + settingsViewController.setVisible(false); + } + });primaryStage.setMinHeight(600.00 + 34); // 34 -> window decoration + primaryStage.setMinWidth(1130.00); } // Table-Mode fxml actions @@ -293,7 +194,7 @@ public class MainWindowController { } if (isSupportedFormat(currentTableFilm)) { - new Player(getCurrentStreamUrl()); + new Player(currentTableFilm.getStreamUrl()); } else { LOGGER.error("using fallback player!"); if (System.getProperty("os.name").contains("Linux")) { @@ -317,7 +218,7 @@ public class MainWindowController { vlcInfoAlert.showAndWait(); } else { try { - new ProcessBuilder("vlc", getCurrentStreamUrl()).start(); + new ProcessBuilder("vlc", currentTableFilm.getStreamUrl()).start(); } catch (IOException e) { LOGGER.warn("An error has occurred while opening the file!", e); } @@ -325,7 +226,7 @@ public class MainWindowController { } else if (System.getProperty("os.name").contains("Windows") || System.getProperty("os.name").contains("Mac OS X")) { try { - Desktop.getDesktop().open(new File(getCurrentStreamUrl())); + Desktop.getDesktop().open(new File(currentTableFilm.getStreamUrl())); } catch (IOException e) { LOGGER.warn("An error has occurred while opening the file!", e); } @@ -335,119 +236,78 @@ public class MainWindowController { } } } - - @FXML - private void openfolderbtnclicked() { - File dest = new File(getCurrentStreamUrl()).getParentFile(); - - if (!System.getProperty("os.name").contains("Linux")) { - try { - Desktop.getDesktop().open(dest); - } catch (IOException e) { - e.printStackTrace(); - } - } - } // general fxml actions @FXML private void aboutBtnAction() { - String bodyText = "Project HomeFlix \nVersion: " + version + " (Build: " + buildNumber + ") \"" + versionName - + "\" \n" + XMLController.getLocalBundle().getString("infoText"); + String bodyText = "Project HomeFlix \nVersion: " + Main.version + " (Build: " + Main.buildNumber + ") \"" + + Main.versionName + "\" \n" + XMLController.getLocalBundle().getString("infoText"); JFXInfoAlert infoAlert = new JFXInfoAlert("Project HomeFlix", bodyText, btnStyle, primaryStage); infoAlert.showAndWait(); } @FXML private void settingsBtnclicked() { - settingsScrollPane.setVisible(!settingsScrollPane.isVisible()); + settingsViewController.setVisible(!settingsViewController.isVisible()); } - - @FXML - private void addDirectoryBtnAction() { - DirectoryChooser directoryChooser = new DirectoryChooser(); - directoryChooser.setTitle(XMLController.getLocalBundle().getString("addDirectory")); - File selectedFolder = directoryChooser.showDialog(primaryStage); - if (selectedFolder != null && selectedFolder.exists()) { - addSource(selectedFolder.getPath(), "local"); - } else { - LOGGER.error("The selected folder dosen't exist!"); - } - } - - @FXML - private void addStreamSourceBtnAction() { - FileChooser fileChooser = new FileChooser(); - fileChooser.setTitle(XMLController.getLocalBundle().getString("addStreamSource")); - File selectedFile = fileChooser.showOpenDialog(primaryStage); - if (selectedFile != null && selectedFile.exists()) { - addSource(selectedFile.getPath(), "stream"); - } else { - LOGGER.error("The selected file dosen't exist!"); - } - } - - @FXML - private void colorPickerAction() { - XMLController.setColor(colorPicker.getValue().toString().substring(2, 10)); - xmlController.saveSettings(); - applyColor(); - } - - @FXML - private void updateBtnAction() { - updateController = new UpdateController(this, buildNumber, XMLController.isUseBeta()); - Thread updateThread = new Thread(updateController); - updateThread.setName("Updater"); - updateThread.start(); - } - - @FXML - private void autoUpdateToggleBtnAction() { - XMLController.setAutoUpdate(!XMLController.isAutoUpdate()); - xmlController.saveSettings(); - } - - @FXML - private void autoplayToggleBtnAction() { - XMLController.setAutoplay(!XMLController.isAutoplay()); - xmlController.saveSettings(); - } - + /** - * refresh all films in filmsList and in filmsTable clear the FilmsList and - * FilmRoot children, then update the database + * we need to get the path for the first source from the user and add it to + * sources.json, if the user ends the file-/directory-chooser the program will exit */ - private void refreshAllFilms() { - filmsList.clear(); - dbController.refreshDataBase(); // refreshes the database after a source path was added - filmsList = dbController.getStreamsList(); // returns a list of all films stored in the database - } + private void addFirstSource() { + JFX2BtnCancelAlert selectFirstSource = new JFX2BtnCancelAlert( + XMLController.getLocalBundle().getString("addSourceHeader"), + XMLController.getLocalBundle().getString("addSourceBody"), + "-fx-button-type: RAISED; -fx-background-color: #ee3523; -fx-text-fill: BLACK;", + XMLController.getLocalBundle().getString("addDirectory"), + XMLController.getLocalBundle().getString("addStreamSource"), + XMLController.getLocalBundle().getString("cancelBtnText"), primaryStage); - // add a all elements of sourcesList to the sources table on the settings pane - public void addSourceToTable() { - for (SourceDataType source : sourcesList) { - sourceRoot.getChildren().add(new TreeItem(source)); // add data to root-node - } + // directory action + selectFirstSource.setBtn1Action(e -> { + DirectoryChooser directoryChooser = new DirectoryChooser(); + directoryChooser.setTitle(XMLController.getLocalBundle().getString("addDirectory")); + File selectedFolder = directoryChooser.showDialog(primaryStage); + if (selectedFolder != null && selectedFolder.exists()) { + selectFirstSource.getAlert().close(); + writeSource(selectedFolder.getPath(), "local"); + } else { + LOGGER.error("The selected folder dosen't exist!"); + System.exit(1); + } + }); + + // streaming action + selectFirstSource.setBtn2Action(e -> { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle(XMLController.getLocalBundle().getString("addStreamSource")); + File selectedFile = fileChooser.showOpenDialog(primaryStage); + if (selectedFile != null && selectedFile.exists()) { + selectFirstSource.getAlert().close(); + writeSource(selectedFile.getPath(), "stream"); + } else { + LOGGER.error("The selected file dosen't exist!"); + System.exit(1); + } + }); + + selectFirstSource.showAndWait(); } /** - * add a source to the newsources list + * add a source to the sources file * * @param path to the source * @param mode of the source (local or streaming) */ - public void addSource(String path, String mode) { + void writeSource(String path, String mode) { JsonArray newsources = null; - + try { // read old array File oldSources = new File(XMLController.getDirHomeFlix() + "/sources.json"); - if (oldSources.exists()) { - newsources = Json.parse(new FileReader(XMLController.getDirHomeFlix() + "/sources.json")).asArray(); - } else { - newsources = Json.array(); - } + newsources = oldSources.exists() ? Json.parse(new FileReader(XMLController.getDirHomeFlix() + "/sources.json")).asArray() : Json.array(); // add new source JsonObject source = Json.object().add("path", path).add("mode", mode); @@ -456,37 +316,28 @@ public class MainWindowController { newsources.writeTo(writer); writer.close(); } catch (IOException e) { - LOGGER.error(e); + LOGGER.error("Error while writing sources file!", e); } - - // clear old sources list/table - getSourcesList().clear(); - sourceRoot.getChildren().clear(); - - refreshAllFilms(); // refresh the FilmsList - checkAllPosters(); // check if there is anything to cache } /** * set the color of the GUI-Elements if usedColor is less than checkColor set * text fill white, else black */ - private void applyColor() { + void applyColor() { String menuBtnStyle; BigInteger usedColor = new BigInteger(XMLController.getColor(), 16); BigInteger checkColor = new BigInteger("78909cff", 16); + menuHam.getStyleClass().clear(); + if (usedColor.compareTo(checkColor) == -1) { btnStyle = "-fx-button-type: RAISED; -fx-background-color: #" + XMLController.getColor() + "; -fx-text-fill: WHITE;"; menuBtnStyle = "-fx-text-fill: WHITE;"; - - menuHam.getStyleClass().clear(); menuHam.getStyleClass().add("jfx-hamburgerW"); } else { btnStyle = "-fx-button-type: RAISED; -fx-background-color: #" + XMLController.getColor() + "; -fx-text-fill: BLACK;"; menuBtnStyle = "-fx-text-fill: BLACK;"; - - menuHam.getStyleClass().clear(); menuHam.getStyleClass().add("jfx-hamburgerB"); } @@ -494,14 +345,11 @@ public class MainWindowController { sideMenuVBox.setStyle("-fx-background-color: #" + XMLController.getColor() + ";"); topHBox.setStyle("-fx-background-color: #" + XMLController.getColor() + ";"); - // normal buttons - addDirectoryBtn.setStyle(btnStyle); - addStreamSourceBtn.setStyle(btnStyle); - updateBtn.setStyle(btnStyle); - // menu buttons settingsBtn.setStyle(menuBtnStyle); aboutBtn.setStyle(menuBtnStyle); + + settingsViewController.updateColor(btnStyle); } // slide in in 400ms @@ -524,37 +372,26 @@ public class MainWindowController { /** * set the local based on the languageChoisBox selection */ - private void setLocalUI() { + void setLocalUI() { + // TODO switch expressions switch (XMLController.getUsrLocal()) { case "en_US": XMLController.setLocalBundle(ResourceBundle.getBundle("locals.HomeFlix-Local", Locale.US)); // us_English - languageChoisBox.getSelectionModel().select(0); break; case "de_DE": XMLController.setLocalBundle(ResourceBundle.getBundle("locals.HomeFlix-Local", Locale.GERMAN)); // German - languageChoisBox.getSelectionModel().select(1); break; default: XMLController.setLocalBundle(ResourceBundle.getBundle("locals.HomeFlix-Local", Locale.US)); // default local - languageChoisBox.getSelectionModel().select(0); break; } + settingsViewController.updateGUILocal(); filmDetailViewController.updateGUILocal(); seriesDetailViewController.updateGUILocal(); aboutBtn.setText(XMLController.getLocalBundle().getString("info")); settingsBtn.setText(XMLController.getLocalBundle().getString("settings")); - updateBtn.setText(XMLController.getLocalBundle().getString("checkUpdates")); - addDirectoryBtn.setText(XMLController.getLocalBundle().getString("addDirectory")); - addStreamSourceBtn.setText(XMLController.getLocalBundle().getString("addStreamSource")); - homeflixSettingsLbl.setText(XMLController.getLocalBundle().getString("homeflixSettingsLbl")); - mainColorLbl.setText(XMLController.getLocalBundle().getString("mainColorLbl")); - fontsizeLbl.setText(XMLController.getLocalBundle().getString("fontsizeLbl")); - languageLbl.setText(XMLController.getLocalBundle().getString("languageLbl")); - autoUpdateToggleBtn.setText(XMLController.getLocalBundle().getString("autoUpdate")); - autoplayToggleBtn.setText(XMLController.getLocalBundle().getString("autoplay")); - branchLbl.setText(XMLController.getLocalBundle().getString("branchLbl")); } // if AutoUpdate, then check for updates @@ -563,7 +400,7 @@ public class MainWindowController { if (XMLController.isAutoUpdate()) { try { LOGGER.info("AutoUpdate: looking for updates on startup ..."); - updateController = new UpdateController(this, buildNumber, XMLController.isUseBeta()); + updateController = new UpdateController(settingsViewController); // TODO this is will crash Thread updateThread = new Thread(updateController); updateThread.setName("Updater"); updateThread.start(); @@ -599,7 +436,7 @@ public class MainWindowController { /** * check if all posters are cached, if not cache the missing ones */ - private void checkAllPosters() { + void checkAllPosters() { // get all not cached entries, none of them should have a cached poster ExecutorService executor = Executors.newFixedThreadPool(5); @@ -621,14 +458,14 @@ public class MainWindowController { } // update all elements from the database - refreshAllFilms(); + dbController.refreshDataBase(); // refreshes the database after a source path was added System.out.println("finished refresh"); } /** * add all cached films/series to the PosterMode GUI */ - private void addAllPosters() { + void addAllPosters() { // refresh the posterModeElements list posterEmenents.clear(); posterEmenents = dbController.getPosterElementsList(); // returns a list of all PosterElements stored in the database @@ -637,7 +474,6 @@ public class MainWindowController { for (PosterModeElement element : posterEmenents) { element.getButton().addEventHandler(MouseEvent.MOUSE_CLICKED, (event) -> { enableBlur(); // blur the FlowPane - System.out.println("selected: " + element.getStreamURL()); // if the selected element is a file it's a film, else a series if (new File(element.getStreamURL()).isFile() || element.getStreamURL().contains("http")) { @@ -656,15 +492,14 @@ public class MainWindowController { System.out.println("added gui elements"); } - // TODO can this be done in dbController? + // TODO can this be done in dbController? with dbController.refreshDataBase(); /** * check if the cache is to old, if so update asynchron */ private void checkCache() { ExecutorService executor = Executors.newFixedThreadPool(5); - // TODO if filmlist is not used anymore, it cann be removed - for(FilmTabelDataType entry : filmsList) { + for(FilmTabelDataType entry : dbController.getStreamsList()) { if (dbController.getCacheDate(entry.getStreamUrl()).isBefore(lastValidCache)) { System.out.println(entry.getTitle() + " chached on: " + dbController.getCacheDate(entry.getStreamUrl())); Runnable OMDbAPIWorker = new OMDbAPIController(entry, XMLController.getOmdbAPIKey()); @@ -686,26 +521,4 @@ public class MainWindowController { public void disableBlur() { posterModeFlowPane.setEffect(null); } - - // getter and setter - - public FilmTabelDataType getCurrentTableFilm() { - return currentTableFilm; - } - - public String getCurrentTitle() { - return currentTableFilm.getTitle(); - } - - public String getCurrentStreamUrl() { - return currentTableFilm.getStreamUrl(); - } - - public static ObservableList getSourcesList() { - return sourcesList; - } - - public JFXButton getUpdateBtn() { - return updateBtn; - } } diff --git a/src/main/java/kellerkinder/HomeFlix/application/SeriesDetailView.java b/src/main/java/kellerkinder/HomeFlix/application/SeriesDetailView.java index dbdf814..4e70597 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/SeriesDetailView.java +++ b/src/main/java/kellerkinder/HomeFlix/application/SeriesDetailView.java @@ -1,3 +1,25 @@ +/** + * Project-HomeFlix + * + * Copyright 2016-2019 <@Seil0> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + */ + package kellerkinder.HomeFlix.application; import java.awt.Desktop; diff --git a/src/main/java/kellerkinder/HomeFlix/application/SettingsView.java b/src/main/java/kellerkinder/HomeFlix/application/SettingsView.java index d5e35bc..bfc7694 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/SettingsView.java +++ b/src/main/java/kellerkinder/HomeFlix/application/SettingsView.java @@ -1,10 +1,47 @@ +/** + * Project-HomeFlix + * + * Copyright 2016-2019 <@Seil0> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + */ + package kellerkinder.HomeFlix.application; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.eclipsesource.json.Json; +import com.eclipsesource.json.JsonArray; +import com.eclipsesource.json.JsonObject; +import com.eclipsesource.json.JsonValue; import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXColorPicker; import com.jfoenix.controls.JFXSlider; import com.jfoenix.controls.JFXToggleButton; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.ChoiceBox; @@ -12,7 +49,14 @@ import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; +import javafx.scene.control.ScrollPane.ScrollBarPolicy; import javafx.scene.layout.AnchorPane; +import javafx.scene.paint.Color; +import javafx.stage.DirectoryChooser; +import javafx.stage.FileChooser; +import kellerkinder.HomeFlix.controller.DBController; +import kellerkinder.HomeFlix.controller.UpdateController; +import kellerkinder.HomeFlix.controller.XMLController; import kellerkinder.HomeFlix.datatypes.SourceDataType; public class SettingsView { @@ -47,37 +91,209 @@ public class SettingsView { @FXML private TableColumn sourceColumn; @FXML private TableColumn modeColumn; + private XMLController xmlController; + private static final Logger LOGGER = LogManager.getLogger(FilmDetailView.class.getName()); + private ObservableList languages = FXCollections.observableArrayList("English (en_US)", "Deutsch (de_DE)"); + private ObservableList branches = FXCollections.observableArrayList("stable", "beta"); + private static ObservableList sourcesList = FXCollections.observableArrayList(); + public void initialize() { - System.out.println("geht"); + xmlController = new XMLController(); + + // initialize the GUI elements + settingsScrollPane.setVbarPolicy(ScrollBarPolicy.ALWAYS); + versionLbl.setText("Version: " + Main.version + " (Build: " + Main.buildNumber + ")"); + fontsizeSlider.setValue(XMLController.getFontSize()); + colorPicker.setValue(Color.valueOf(XMLController.getColor())); + + autoUpdateToggleBtn.setSelected(XMLController.isAutoUpdate()); + autoplayToggleBtn.setSelected(XMLController.isAutoplay()); + languageChoisBox.setItems(languages); + branchChoisBox.setItems(branches); + + branchChoisBox.getSelectionModel().select(XMLController.isUseBeta() ? 1 : 0); + // TODO switch expressions + switch (XMLController.getUsrLocal()) { + case "en_US": + languageChoisBox.getSelectionModel().select(0); + break; + case "de_DE": + languageChoisBox.getSelectionModel().select(1); + break; + default: + languageChoisBox.getSelectionModel().select(0); + break; + } + + // initialize the sources table + sourceColumn.setCellValueFactory(cellData -> cellData.getValue().pathProperty()); + modeColumn.setCellValueFactory(cellData -> cellData.getValue().modeProperty()); + sourcesTable.setItems(sourcesList); + + initActions(); + loadInitSources(); } - @FXML void addDirectoryBtnAction(ActionEvent event) { + private void initActions() { + languageChoisBox.getSelectionModel().selectedIndexProperty().addListener((e, oldValue, newValue) -> { + String local = languageChoisBox.getItems().get((int) newValue).toString(); + local = local.substring(local.length() - 6, local.length() - 1); // reading only en_US from English (en_US) + XMLController.setUsrLocal(local); + xmlController.saveSettings(); + MainWindowController.getInstance().setLocalUI(); + }); + + branchChoisBox.getSelectionModel().selectedIndexProperty().addListener((e, oldValue, newValue) -> { + if (branchChoisBox.getItems().get((int) newValue).toString() == "beta") { + XMLController.setUseBeta(true); + } else { + XMLController.setUseBeta(false); + } + xmlController.saveSettings(); + }); + + fontsizeSlider.valueProperty().addListener(e -> { + XMLController.setFontSize(fontsizeSlider.getValue()); + xmlController.saveSettings(); + + // TODO add functionality for postermode + }); + } + + @FXML + private void addDirectoryBtnAction(ActionEvent event) { + DirectoryChooser directoryChooser = new DirectoryChooser(); + directoryChooser.setTitle(XMLController.getLocalBundle().getString("addDirectory")); + File selectedFolder = directoryChooser.showDialog(settingsScrollPane.getScene().getWindow()); + if (selectedFolder != null && selectedFolder.exists()) { + addSource(selectedFolder.getPath(), "local"); + } else { + LOGGER.error("The selected folder dosen't exist!"); + } } @FXML - void addStreamSourceBtnAction(ActionEvent event) { - + private void addStreamSourceBtnAction(ActionEvent event) { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle(XMLController.getLocalBundle().getString("addStreamSource")); + File selectedFile = fileChooser.showOpenDialog(settingsScrollPane.getScene().getWindow()); + if (selectedFile != null && selectedFile.exists()) { + addSource(selectedFile.getPath(), "stream"); + } else { + LOGGER.error("The selected file dosen't exist!"); + } } @FXML - void autoUpdateToggleBtnAction(ActionEvent event) { - + private void autoUpdateToggleBtnAction(ActionEvent event) { + XMLController.setAutoUpdate(!XMLController.isAutoUpdate()); + xmlController.saveSettings(); } @FXML - void autoplayToggleBtnAction(ActionEvent event) { - + private void autoplayToggleBtnAction(ActionEvent event) { + XMLController.setAutoplay(!XMLController.isAutoplay()); + xmlController.saveSettings(); } @FXML - void colorPickerAction(ActionEvent event) { - + private void colorPickerAction(ActionEvent event) { + XMLController.setColor(colorPicker.getValue().toString().substring(2, 10)); + xmlController.saveSettings(); + MainWindowController.getInstance().applyColor(); } @FXML - void updateBtnAction(ActionEvent event) { + private void updateBtnAction(ActionEvent event) { + Thread updateThread = new Thread(new UpdateController(this)); + updateThread.setName("Updater"); + updateThread.start(); + } + + /** TODO can this be done async? + * add a source to the sources file and load all new streams + * @param path to the source + * @param mode of the source (local or streaming) + */ + void addSource(String path, String mode) { + JsonArray newsources = null; + try {settingsScrollPane.setVbarPolicy(ScrollBarPolicy.ALWAYS); + // read old array + File oldSources = new File(XMLController.getDirHomeFlix() + "/sources.json"); + newsources = oldSources.exists() ? Json.parse(new FileReader(XMLController.getDirHomeFlix() + "/sources.json")).asArray() : Json.array(); + + // add new source + JsonObject source = Json.object().add("path", path).add("mode", mode); + newsources.add(source); + Writer writer = new FileWriter(XMLController.getDirHomeFlix() + "/sources.json"); + newsources.writeTo(writer); + writer.close(); + } catch (IOException e) { + LOGGER.error(e); + } + + // update the sourcesTable + sourcesList.add(new SourceDataType(path, mode)); + + DBController.getInstance().refreshDataBase(); // refreshes the database after a source path was added + MainWindowController.getInstance().checkAllPosters(); // check if there is anything new to cache + MainWindowController.getInstance().addAllPosters(); + } + + // add a all elements of sourcesList to the sources table on the settings pane + private void loadInitSources() { + try { + // create a JsonArray, containing all sources, add each source to the mwc, get all films from it + JsonArray sources = Json.parse(new FileReader(XMLController.getDirHomeFlix() + "/sources.json")).asArray(); + for (JsonValue source : sources) { + String path = source.asObject().getString("path", ""); + String mode = source.asObject().getString("mode", ""); + sourcesList.add(new SourceDataType(path, mode)); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void updateColor(String btnStyle) { + updateBtn.setStyle(btnStyle); + addDirectoryBtn.setStyle(btnStyle); + addStreamSourceBtn.setStyle(btnStyle); + + autoUpdateToggleBtn.setToggleColor(Color.valueOf(XMLController.getColor())); + autoUpdateToggleBtn.setToggleLineColor(Color.valueOf(XMLController.getColor())); + autoplayToggleBtn.setToggleColor(Color.valueOf(XMLController.getColor())); + autoplayToggleBtn.setToggleLineColor(Color.valueOf(XMLController.getColor())); + } + + public void updateGUILocal() { + homeflixSettingsLbl.setText(XMLController.getLocalBundle().getString("homeflixSettingsLbl")); + mainColorLbl.setText(XMLController.getLocalBundle().getString("mainColorLbl")); + fontsizeLbl.setText(XMLController.getLocalBundle().getString("fontsizeLbl")); + languageLbl.setText(XMLController.getLocalBundle().getString("languageLbl")); + branchLbl.setText(XMLController.getLocalBundle().getString("branchLbl")); + sourcesLbl.setText(XMLController.getLocalBundle().getString("sourcesLbl")); + + updateBtn.setText(XMLController.getLocalBundle().getString("checkUpdates")); + addDirectoryBtn.setText(XMLController.getLocalBundle().getString("addDirectory")); + addStreamSourceBtn.setText(XMLController.getLocalBundle().getString("addStreamSource")); + + autoUpdateToggleBtn.setText(XMLController.getLocalBundle().getString("autoUpdate")); + autoplayToggleBtn.setText(XMLController.getLocalBundle().getString("autoplay")); + } + + public void setVisible(boolean visible) { + settingsScrollPane.setVisible(visible); + } + + public boolean isVisible() { + return settingsScrollPane.isVisible(); + } + + public JFXButton getUpdateBtn() { + return updateBtn; } } diff --git a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java index c06e846..8b0d4ee 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java @@ -52,8 +52,8 @@ public class DBController { private static DBController instance = null; private String DB_PATH; - private Image favorite_black = new Image("icons/ic_favorite_black_18dp_1x.png"); - private Image favorite_border_black = new Image("icons/ic_favorite_border_black_18dp_1x.png"); + private Image favorite_black = new Image("icons/baseline_favorite_black_48dp.png"); // TODO this should be removed + private Image favorite_border_black = new Image("icons/baseline_favorite_border_black_48dp.png"); // TODO this too private List databaseStreams = new ArrayList(); // contains all films stored in the database private List sourceStreams = new ArrayList(); // contains all films from the sources private Connection connection = null; diff --git a/src/main/java/kellerkinder/HomeFlix/controller/SourcesController.java b/src/main/java/kellerkinder/HomeFlix/controller/SourcesController.java index 7e5c213..76dc8fc 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/SourcesController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/SourcesController.java @@ -37,9 +37,7 @@ import com.eclipsesource.json.JsonArray; import com.eclipsesource.json.JsonObject; import com.eclipsesource.json.JsonValue; -import kellerkinder.HomeFlix.application.MainWindowController; import kellerkinder.HomeFlix.datatypes.DatabaseDataType; -import kellerkinder.HomeFlix.datatypes.SourceDataType; public class SourcesController { @@ -61,7 +59,6 @@ public class SourcesController { for (JsonValue source : sources) { String path = source.asObject().getString("path", ""); String mode = source.asObject().getString("mode", ""); - MainWindowController.getSourcesList().add(new SourceDataType(path, mode)); // TODO if (mode.equals("local")) addLocalSource(path); diff --git a/src/main/java/kellerkinder/HomeFlix/controller/UpdateController.java b/src/main/java/kellerkinder/HomeFlix/controller/UpdateController.java index 3c7caf1..ace49cd 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/UpdateController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/UpdateController.java @@ -41,12 +41,12 @@ import com.eclipsesource.json.JsonObject; import com.eclipsesource.json.JsonValue; import javafx.application.Platform; -import kellerkinder.HomeFlix.application.MainWindowController; +import kellerkinder.HomeFlix.application.Main; +import kellerkinder.HomeFlix.application.SettingsView; public class UpdateController implements Runnable { - private MainWindowController mainWindowController; - private int buildNumber; + private SettingsView settingsViewController; private int updateBuildNumber; // tag_name from gitea private String apiOutput; @SuppressWarnings("unused") @@ -57,7 +57,6 @@ public class UpdateController implements Runnable { private String giteaApiRelease = "https://git.mosad.xyz/api/v1/repos/Seil0/Project-HomeFlix/releases"; private URL giteaApiUrl; - private boolean useBeta; private static final Logger LOGGER = LogManager.getLogger(UpdateController.class.getName()); /** @@ -66,17 +65,15 @@ public class UpdateController implements Runnable { * @param buildNumber the buildNumber of the used HomeFlix version * @param useBeta if the updater should query the beta channel */ - public UpdateController(MainWindowController mwc, String buildNumber, boolean useBeta) { - mainWindowController = mwc; - this.buildNumber = Integer.parseInt(buildNumber); - this.useBeta = useBeta; + public UpdateController(SettingsView svc) { + settingsViewController = svc; } @Override public void run() { - LOGGER.info("beta:" + useBeta + "; checking for updates ..."); + LOGGER.info("beta:" + XMLController.isUseBeta() + "; checking for updates ..."); Platform.runLater(() -> { - mainWindowController.getUpdateBtn().setText(XMLController.getLocalBundle().getString("updateBtnChecking")); + settingsViewController.getUpdateBtn().setText(XMLController.getLocalBundle().getString("updateBtnChecking")); }); try { @@ -96,7 +93,7 @@ public class UpdateController implements Runnable { JsonObject objectAsset = object.asObject().get("assets").asArray().get(0).asObject(); for(JsonValue objectIt : objectArray) { - if(objectIt.asObject().getBoolean("prerelease", false) == useBeta) { + if(objectIt.asObject().getBoolean("prerelease", false) == XMLController.isUseBeta()) { // we found the needed release either beta or not object = objectIt; objectAsset = objectIt.asObject().get("assets").asArray().get(0).asObject(); @@ -108,20 +105,20 @@ public class UpdateController implements Runnable { updateName = object.asObject().getString("name", ""); updateChanges = object.asObject().getString("body", ""); - LOGGER.info("Build: " + buildNumber + ", Update: " + updateBuildNumber); + LOGGER.info("Build: " + Main.buildNumber + ", Update: " + updateBuildNumber); /** * Compare the program BuildNumber with the current BuildNumber * if buildNumber < updateBuildNumber then perform a update */ - if (buildNumber >= updateBuildNumber) { + if (Integer.parseInt(Main.buildNumber) >= updateBuildNumber) { Platform.runLater(() -> { - mainWindowController.getUpdateBtn().setText(XMLController.getLocalBundle().getString("updateBtnNoUpdateAvailable")); + settingsViewController.getUpdateBtn().setText(XMLController.getLocalBundle().getString("updateBtnNoUpdateAvailable")); }); LOGGER.info("no update available"); } else { Platform.runLater(() -> { - mainWindowController.getUpdateBtn().setText(XMLController.getLocalBundle().getString("updateBtnUpdateAvailable")); + settingsViewController.getUpdateBtn().setText(XMLController.getLocalBundle().getString("updateBtnUpdateAvailable")); }); LOGGER.info("update available"); browserDownloadUrl = objectAsset.getString("browser_download_url", ""); diff --git a/src/main/java/org/kellerkinder/Alerts/JFX2BtnCancelAlert.java b/src/main/java/org/kellerkinder/Alerts/JFX2BtnCancelAlert.java index 9fc68d6..2821b11 100644 --- a/src/main/java/org/kellerkinder/Alerts/JFX2BtnCancelAlert.java +++ b/src/main/java/org/kellerkinder/Alerts/JFX2BtnCancelAlert.java @@ -90,7 +90,7 @@ public class JFX2BtnCancelAlert { JFXButton cancelBtn = new JFXButton(); cancelBtn.setText(cancelText); cancelBtn.addEventHandler(ActionEvent.ACTION, (e)-> alert.close()); - cancelBtn.addEventHandler(ActionEvent.ACTION, (e)-> System.exit(0)); + cancelBtn.addEventHandler(ActionEvent.ACTION, (e)-> System.exit(0)); // TODO only on first start cancelBtn.setButtonType(com.jfoenix.controls.JFXButton.ButtonType.RAISED); cancelBtn.setPrefHeight(32); cancelBtn.setStyle(btnStyle); @@ -99,6 +99,13 @@ public class JFX2BtnCancelAlert { content.setActions(btnOne, btnTwo, cancelBtn); content.setHeading(new Text(headingText)); content.setBody(new Text(bodyText)); + + // TODO only on first start + Stage stage = (Stage) alert.getDialogPane().getScene().getWindow(); + stage.setMinWidth(416); + stage.setMinHeight(162); + stage.setOnCloseRequest(event -> System.exit(0)); + alert.setContent(content); alert.showAndWait(); } diff --git a/src/main/resources/fxml/MainWindow.fxml b/src/main/resources/fxml/MainWindow.fxml index 239c5d2..9a243b2 100644 --- a/src/main/resources/fxml/MainWindow.fxml +++ b/src/main/resources/fxml/MainWindow.fxml @@ -1,17 +1,9 @@ - - - - - - - - @@ -31,119 +23,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/src/main/resources/fxml/SettingsView.fxml b/src/main/resources/fxml/SettingsView.fxml index d5d8790..62f05e6 100644 --- a/src/main/resources/fxml/SettingsView.fxml +++ b/src/main/resources/fxml/SettingsView.fxml @@ -16,9 +16,9 @@ - + - + diff --git a/src/main/resources/locals/HomeFlix-Local_de_DE.properties b/src/main/resources/locals/HomeFlix-Local_de_DE.properties index 5363deb..1b7279d 100644 --- a/src/main/resources/locals/HomeFlix-Local_de_DE.properties +++ b/src/main/resources/locals/HomeFlix-Local_de_DE.properties @@ -22,6 +22,7 @@ updateBtnNoUpdateAvailable = Kein Update verf\u00FCgbar autoUpdate = beim Start nach Updates suchen: autoplay = autoplay branchLbl = Updatezweig +sourcesLbl = Quellen #column translations columnStreamUrl = Datei Name diff --git a/src/main/resources/locals/HomeFlix-Local_en_US.properties b/src/main/resources/locals/HomeFlix-Local_en_US.properties index c303d44..a2166c7 100644 --- a/src/main/resources/locals/HomeFlix-Local_en_US.properties +++ b/src/main/resources/locals/HomeFlix-Local_en_US.properties @@ -22,6 +22,7 @@ updateBtnNoUpdateAvailable = no update available autoUpdate = check at startup for updates: autoplay = autoplay branchLbl = Branch +sourcesLbl = Quellen #column translations columnStreamUrl = File Name From 5488bece2d544be0ab1da5a629998c528f742e9c Mon Sep 17 00:00:00 2001 From: Seil0 Date: Sat, 22 Jun 2019 21:04:43 +0200 Subject: [PATCH 84/88] minor bugfixes and clean up * fixed a bug that prevented posters from showing at the first start * added the ability to start a series from the last watched episode --- .../HomeFlix/application/FilmDetailView.java | 45 +------- .../HomeFlix/application/Main.java | 12 +-- .../application/MainWindowController.java | 85 ++------------- .../application/SeriesDetailView.java | 3 +- .../HomeFlix/application/SettingsView.java | 26 ++--- .../HomeFlix/controller/DBController.java | 46 ++++---- .../controller/OMDbAPIController.java | 4 +- .../HomeFlix/datatypes/FilmTabelDataType.java | 102 ++++-------------- .../kellerkinder/HomeFlix/player/Player.java | 91 +++++++++++++--- .../Alerts/JFX2BtnCancelAlert.java | 16 +-- 10 files changed, 161 insertions(+), 269 deletions(-) diff --git a/src/main/java/kellerkinder/HomeFlix/application/FilmDetailView.java b/src/main/java/kellerkinder/HomeFlix/application/FilmDetailView.java index 5c23192..8c02475 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/FilmDetailView.java +++ b/src/main/java/kellerkinder/HomeFlix/application/FilmDetailView.java @@ -23,10 +23,8 @@ package kellerkinder.HomeFlix.application; import java.awt.Desktop; -import java.io.BufferedReader; import java.io.File; import java.io.IOException; -import java.io.InputStreamReader; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -217,53 +215,12 @@ public class FilmDetailView { MainWindowController.getInstance().disableBlur(); // disable blur } - // TODO rework private void playFilm() { if(new File(currentStreamURL).isDirectory()) { return; } - if (Player.isSupportedFormat(currentStreamURL)) { - new Player(currentStreamURL); - } else { - LOGGER.error("using fallback player!"); - if (System.getProperty("os.name").contains("Linux")) { - String line; - String output = ""; - Process p; - try { - p = Runtime.getRuntime().exec("which vlc"); - BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream())); - while ((line = input.readLine()) != null) { - output = line; - } - LOGGER.info("which vlc: " + output); - input.close(); - } catch (IOException e1) { - e1.printStackTrace(); - } - if (output.contains("which: no vlc") || output == "") { -// JFXInfoAlert vlcInfoAlert = new JFXInfoAlert("Info", -// XMLController.getLocalBundle().getString("vlcNotInstalled"), btnStyle, primaryStage); -// vlcInfoAlert.showAndWait(); - } else { - try { - new ProcessBuilder("vlc", currentStreamURL).start(); - } catch (IOException e) { - LOGGER.warn("An error has occurred while opening the file!", e); - } - } - - } else if (System.getProperty("os.name").contains("Windows") || System.getProperty("os.name").contains("Mac OS X")) { - try { - Desktop.getDesktop().open(new File(currentStreamURL)); - } catch (IOException e) { - LOGGER.warn("An error has occurred while opening the file!", e); - } - } else { - LOGGER.error(System.getProperty("os.name") + ", OS is not supported, please contact a developer! "); - } - } + new Player(currentStreamURL); } } diff --git a/src/main/java/kellerkinder/HomeFlix/application/Main.java b/src/main/java/kellerkinder/HomeFlix/application/Main.java index 1b281aa..6496fec 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/Main.java +++ b/src/main/java/kellerkinder/HomeFlix/application/Main.java @@ -43,8 +43,8 @@ public class Main extends Application { private MainWindowController mainWindowController; private static XMLController xmlController; private static Logger LOGGER; - public static final String version = "0.7.90"; - public static final String buildNumber = "171"; + public static final String version = "0.7.91"; + public static final String buildNumber = "173"; public static final String versionName = "toothless dragon"; @Override @@ -100,11 +100,11 @@ public class Main extends Application { xmlController = new XMLController(); - if (XMLController.getConfigFile().exists()) { - xmlController.loadSettings(); - } else { - xmlController.saveSettings(); + if (!XMLController.getConfigFile().exists()) { + xmlController.saveSettings(); // save the settings file with default values if it doesn't exist } + + xmlController.loadSettings(); if (!XMLController.getPosterCache().exists()) { XMLController.getPosterCache().mkdir(); diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index fd173de..4c8eb0c 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -22,16 +22,12 @@ package kellerkinder.HomeFlix.application; -import java.awt.Desktop; -import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; -import java.io.InputStreamReader; import java.io.Writer; import java.math.BigInteger; -import java.net.URLConnection; import java.time.LocalDate; import java.util.Locale; import java.util.ResourceBundle; @@ -73,7 +69,6 @@ import kellerkinder.HomeFlix.controller.UpdateController; import kellerkinder.HomeFlix.controller.XMLController; import kellerkinder.HomeFlix.datatypes.FilmTabelDataType; import kellerkinder.HomeFlix.datatypes.PosterModeElement; -import kellerkinder.HomeFlix.player.Player; public class MainWindowController { @@ -109,8 +104,6 @@ public class MainWindowController { private String btnStyle; - private FilmTabelDataType currentTableFilm = new FilmTabelDataType("", "", "", "", false, null); - private ObservableList posterEmenents = FXCollections.observableArrayList(); private LocalDate lastValidCache = LocalDate.now().minusDays(30); // current date - 30 days is the last valid cache date @@ -184,59 +177,6 @@ public class MainWindowController { primaryStage.setMinWidth(1130.00); } - // Table-Mode fxml actions - @FXML - private void playbtnclicked() { - if (currentTableFilm.getStreamUrl().length() > 0) { - if (currentTableFilm.getStreamUrl().contains("_rootNode")) { - LOGGER.info("rootNode found, getting last watched episode"); - currentTableFilm = dbController.getLastWatchedEpisode(currentTableFilm.getTitle()); - } - - if (isSupportedFormat(currentTableFilm)) { - new Player(currentTableFilm.getStreamUrl()); - } else { - LOGGER.error("using fallback player!"); - if (System.getProperty("os.name").contains("Linux")) { - String line; - String output = ""; - Process p; - try { - p = Runtime.getRuntime().exec("which vlc"); - BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream())); - while ((line = input.readLine()) != null) { - output = line; - } - LOGGER.info("which vlc: " + output); - input.close(); - } catch (IOException e1) { - e1.printStackTrace(); - } - if (output.contains("which: no vlc") || output == "") { - JFXInfoAlert vlcInfoAlert = new JFXInfoAlert("Info", - XMLController.getLocalBundle().getString("vlcNotInstalled"), btnStyle, primaryStage); - vlcInfoAlert.showAndWait(); - } else { - try { - new ProcessBuilder("vlc", currentTableFilm.getStreamUrl()).start(); - } catch (IOException e) { - LOGGER.warn("An error has occurred while opening the file!", e); - } - } - - } else if (System.getProperty("os.name").contains("Windows") || System.getProperty("os.name").contains("Mac OS X")) { - try { - Desktop.getDesktop().open(new File(currentTableFilm.getStreamUrl())); - } catch (IOException e) { - LOGGER.warn("An error has occurred while opening the file!", e); - } - } else { - LOGGER.error(System.getProperty("os.name") + ", OS is not supported, please contact a developer! "); - } - } - } - } - // general fxml actions @FXML private void aboutBtnAction() { @@ -272,6 +212,7 @@ public class MainWindowController { if (selectedFolder != null && selectedFolder.exists()) { selectFirstSource.getAlert().close(); writeSource(selectedFolder.getPath(), "local"); + settingsViewController.loadInitSources(); } else { LOGGER.error("The selected folder dosen't exist!"); System.exit(1); @@ -286,6 +227,7 @@ public class MainWindowController { if (selectedFile != null && selectedFile.exists()) { selectFirstSource.getAlert().close(); writeSource(selectedFile.getPath(), "stream"); + settingsViewController.loadInitSources(); } else { LOGGER.error("The selected file dosen't exist!"); System.exit(1); @@ -411,20 +353,9 @@ public class MainWindowController { } } - /** - * check if a film is supported by the HomeFlixPlayer or not this is the case if - * the mime type is mp4 - * - * @param entry the film you want to check - * @return true if so, false if not - */ - private boolean isSupportedFormat(FilmTabelDataType film) { - String mimeType = URLConnection.guessContentTypeFromName(film.getStreamUrl()); - return mimeType != null && (mimeType.contains("mp4") || mimeType.contains("vp6")); - } /** - * Poser Mode WIP + * Poser Mode */ private void posterModeStartup() { @@ -437,13 +368,15 @@ public class MainWindowController { * check if all posters are cached, if not cache the missing ones */ void checkAllPosters() { + dbController.refreshDataBase(); // refreshes the database after a source path was added + // get all not cached entries, none of them should have a cached poster ExecutorService executor = Executors.newFixedThreadPool(5); for (FilmTabelDataType entry : dbController.getAllNotCachedEntries()) { System.out.println(entry.getStreamUrl() + " is NOT cached!"); - Runnable OMDbAPIWorker = new OMDbAPIController(entry, XMLController.getOmdbAPIKey()); + Runnable OMDbAPIWorker = new OMDbAPIController(entry); executor.execute(OMDbAPIWorker); } executor.shutdown(); @@ -456,9 +389,7 @@ public class MainWindowController { } catch (InterruptedException e) { LOGGER.error(e); } - - // update all elements from the database - dbController.refreshDataBase(); // refreshes the database after a source path was added + System.out.println("finished refresh"); } @@ -502,7 +433,7 @@ public class MainWindowController { for(FilmTabelDataType entry : dbController.getStreamsList()) { if (dbController.getCacheDate(entry.getStreamUrl()).isBefore(lastValidCache)) { System.out.println(entry.getTitle() + " chached on: " + dbController.getCacheDate(entry.getStreamUrl())); - Runnable OMDbAPIWorker = new OMDbAPIController(entry, XMLController.getOmdbAPIKey()); + Runnable OMDbAPIWorker = new OMDbAPIController(entry); executor.execute(OMDbAPIWorker); } } diff --git a/src/main/java/kellerkinder/HomeFlix/application/SeriesDetailView.java b/src/main/java/kellerkinder/HomeFlix/application/SeriesDetailView.java index 4e70597..018dab3 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/SeriesDetailView.java +++ b/src/main/java/kellerkinder/HomeFlix/application/SeriesDetailView.java @@ -48,6 +48,7 @@ import javafx.util.Duration; import kellerkinder.HomeFlix.controller.DBController; import kellerkinder.HomeFlix.controller.XMLController; import kellerkinder.HomeFlix.datatypes.SeriresDVEpisode; +import kellerkinder.HomeFlix.player.Player; public class SeriesDetailView { @@ -121,7 +122,7 @@ public class SeriesDetailView { @FXML private void btnPlayAction() { -// playFilm(); // TODO + new Player(dbController.getLastWatchedEpisode(currentStreamURL)); } @FXML diff --git a/src/main/java/kellerkinder/HomeFlix/application/SettingsView.java b/src/main/java/kellerkinder/HomeFlix/application/SettingsView.java index bfc7694..d1062a6 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/SettingsView.java +++ b/src/main/java/kellerkinder/HomeFlix/application/SettingsView.java @@ -243,18 +243,20 @@ public class SettingsView { } // add a all elements of sourcesList to the sources table on the settings pane - private void loadInitSources() { - try { - // create a JsonArray, containing all sources, add each source to the mwc, get all films from it - JsonArray sources = Json.parse(new FileReader(XMLController.getDirHomeFlix() + "/sources.json")).asArray(); - for (JsonValue source : sources) { - String path = source.asObject().getString("path", ""); - String mode = source.asObject().getString("mode", ""); - sourcesList.add(new SourceDataType(path, mode)); - } - } catch (Exception e) { - e.printStackTrace(); - } + void loadInitSources() { + if(new File(XMLController.getDirHomeFlix() + "/sources.json").exists()) { + try { + // create a JsonArray, containing all sources, add each source to the mwc, get all films from it + JsonArray sources = Json.parse(new FileReader(XMLController.getDirHomeFlix() + "/sources.json")).asArray(); + for (JsonValue source : sources) { + String path = source.asObject().getString("path", ""); + String mode = source.asObject().getString("mode", ""); + sourcesList.add(new SourceDataType(path, mode)); + } + } catch (Exception e) { + e.printStackTrace(); + } + } } public void updateColor(String btnStyle) { diff --git a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java index 8b0d4ee..d0ba3db 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java @@ -42,7 +42,6 @@ import org.apache.logging.log4j.Logger; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.scene.image.Image; -import javafx.scene.image.ImageView; import kellerkinder.HomeFlix.datatypes.DatabaseDataType; import kellerkinder.HomeFlix.datatypes.FilmTabelDataType; import kellerkinder.HomeFlix.datatypes.OMDbAPIResponseDataType; @@ -52,8 +51,6 @@ public class DBController { private static DBController instance = null; private String DB_PATH; - private Image favorite_black = new Image("icons/baseline_favorite_black_48dp.png"); // TODO this should be removed - private Image favorite_border_black = new Image("icons/baseline_favorite_border_black_48dp.png"); // TODO this too private List databaseStreams = new ArrayList(); // contains all films stored in the database private List sourceStreams = new ArrayList(); // contains all films from the sources private Connection connection = null; @@ -161,10 +158,8 @@ public class DBController { ResultSet rs = stmt.executeQuery("SELECT * FROM films ORDER BY title"); while (rs.next()) { - ImageView imageView = rs.getBoolean("favorite") ? new ImageView(favorite_black) : new ImageView(favorite_border_black); filmsList.add(new FilmTabelDataType(rs.getString("streamUrl"), - rs.getString("title"), rs.getString("season"), rs.getString("episode") ,rs.getBoolean("favorite"), - imageView)); + rs.getString("title"), rs.getString("season"), rs.getString("episode"))); } stmt.close(); rs.close(); @@ -189,10 +184,8 @@ public class DBController { ResultSet rs = ps.executeQuery(); while (rs.next()) { - ImageView imageView = rs.getBoolean("favorite") ? new ImageView(favorite_black) : new ImageView(favorite_border_black); film = new FilmTabelDataType(rs.getString("streamUrl"), - rs.getString("title"), rs.getString("season"), rs.getString("episode"), rs.getBoolean("favorite"), - imageView); + rs.getString("title"), rs.getString("season"), rs.getString("episode")); } rs.close(); ps.close(); @@ -597,8 +590,7 @@ public class DBController { ResultSet rs = stmt.executeQuery("SELECT * FROM films WHERE cached = 0"); while (rs.next()) { notCachedEntries.add(new FilmTabelDataType(rs.getString("streamUrl"), - rs.getString("title"), rs.getString("season"), rs.getString("episode"), rs.getBoolean("favorite"), - new ImageView(favorite_border_black))); + rs.getString("title"), rs.getString("season"), rs.getString("episode"))); } stmt.close(); rs.close(); @@ -682,7 +674,8 @@ public class DBController { /** * get the next episode of a series * @param title title of the film - * @param episode episode currently played + * @param episode current episode + * @param season season of the current episode * @return {@link FilmTabelDataType} the next episode as object */ public FilmTabelDataType getNextEpisode(String title, int episode, int season) { @@ -710,8 +703,7 @@ public class DBController { // at this point we have found the correct episode nextFilm = new FilmTabelDataType(rs.getString("streamUrl"), rs.getString("title"), - rs.getString("season"), rs.getString("episode"), rs.getBoolean("favorite"), - new ImageView()); + rs.getString("season"), rs.getString("episode")); rs.close(); ps.close(); @@ -723,33 +715,31 @@ public class DBController { /** TODO rework, we should save the next episode in the root entry * get the last watched episode - * @param title the title of the series - * @return the last watched episode as {@link FilmTabelDataType} object + * @param streamURL URL of the stream + * @return the last watched episodes URL */ - public FilmTabelDataType getLastWatchedEpisode(String title) { - LOGGER.info("last watched episode of: " + title); - FilmTabelDataType nextFilm = null; + public String getLastWatchedEpisode(String streamURL) { + LOGGER.info("last watched episode in: " + streamURL); + String lastEp = null; double lastCurrentTime = 0; try { - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT * FROM films WHERE title = \"" + title + "\";"); + PreparedStatement ps = connection.prepareStatement("SELECT * FROM films WHERE streamUrl LIKE ?"); + ps.setString(1, streamURL + "/%"); + ResultSet rs = ps.executeQuery(); while (rs.next()) { - nextFilm = new FilmTabelDataType(rs.getString("streamUrl"), rs.getString("title"), - rs.getString("season"), rs.getString("episode"), rs.getBoolean("favorite"), - new ImageView()); // get the first episode where currentTime != 0 - if (rs.getDouble("currentTime") > lastCurrentTime) { - break; + if (rs.getDouble("currentTime") > lastCurrentTime || lastEp == null) { + lastEp = rs.getString("streamUrl"); } } rs.close(); - stmt.close(); + ps.close(); } catch (Exception e) { LOGGER.error("Ups! error while getting the last watched episode!", e); } - return nextFilm; + return lastEp; } /** diff --git a/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java b/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java index af156ec..e017ede 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java @@ -55,9 +55,9 @@ public class OMDbAPIController implements Runnable { * @param currentTableFilm the current film object * @param omdbAPIKey the omdbAPI key */ - public OMDbAPIController(FilmTabelDataType currentTableFilm, String omdbAPIKey) { + public OMDbAPIController(FilmTabelDataType currentTableFilm) { this.currentTableFilm = currentTableFilm; - this.omdbAPIKey = omdbAPIKey; + omdbAPIKey = XMLController.getOmdbAPIKey(); dbController = DBController.getInstance(); } diff --git a/src/main/java/kellerkinder/HomeFlix/datatypes/FilmTabelDataType.java b/src/main/java/kellerkinder/HomeFlix/datatypes/FilmTabelDataType.java index d774532..53ab588 100644 --- a/src/main/java/kellerkinder/HomeFlix/datatypes/FilmTabelDataType.java +++ b/src/main/java/kellerkinder/HomeFlix/datatypes/FilmTabelDataType.java @@ -20,20 +20,11 @@ */ package kellerkinder.HomeFlix.datatypes; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; -import javafx.scene.image.ImageView; - public class FilmTabelDataType { - private final StringProperty streamUrl = new SimpleStringProperty(); - private final StringProperty title = new SimpleStringProperty(); - private final StringProperty season = new SimpleStringProperty(); - private final StringProperty episode = new SimpleStringProperty(); - private final BooleanProperty favorite = new SimpleBooleanProperty(); - private final SimpleObjectProperty image = new SimpleObjectProperty<>(); + private String streamUrl; + private String title; + private String season; + private String episode; /** * tableData is the data-type of tree-table-view @@ -41,93 +32,44 @@ public class FilmTabelDataType { * @param title title of the film * @param season season if it's a series * @param episode episode if it's a series - * @param favorite indicator for favorites, used for sorting the items - * @param cached indicator for caching status - * @param image favorite icon */ - public FilmTabelDataType(final String streamUrl, final String title, final String season, final String episode, - final boolean favorite, final ImageView image) { - this.streamUrl.set(streamUrl); - this.title.set(title); - this.season.set(season); - this.episode.set(episode); - this.favorite.set(favorite); - this.image.set(image); + public FilmTabelDataType(String streamUrl, String title, String season, String episode) { + this.streamUrl = streamUrl; + this.title = title; + this.season = season; + this.episode = episode; } - public StringProperty streamUrlProperty(){ + public String getStreamUrl() { return streamUrl; } - public StringProperty titleProperty(){ + public String getTitle() { return title; } - - public StringProperty seasonProperty(){ + + public String getSeason() { return season; } - public StringProperty episodeProperty(){ + public String getEpisode() { return episode; } - public BooleanProperty favoriteProperty(){ - return favorite; - } - - public SimpleObjectProperty imageProperty(){ - return image; - } - - - public final String getStreamUrl() { - return streamUrlProperty().get(); - } - - public final String getTitle() { - return titleProperty().get(); + public void setStreamUrl(String streamUrl) { + this.streamUrl = streamUrl; } - public final String getSeason() { - return seasonProperty().get(); - } - - public final String getEpisode() { - return episodeProperty().get(); - } - - public final boolean getFavorite() { - return favoriteProperty().get(); - } - - public final ImageView getImage() { - return imageProperty().get(); - } - - - public final void setStreamUrl(String streamUrl) { - streamUrlProperty().set(streamUrl); + public void setTitle(String title) { + this.title = title; } - public final void setTitle(String title) { - titleProperty().set(title); - } - - public final void setSeason(String season) { - seasonProperty().set(season); + public void setSeason(String season) { + this.season = season; } - public final void setEpisode(String season) { - episodeProperty().set(season); + public void setEpisode(String episode) { + this.episode = episode; } - - public final void setFavorite(boolean favorite) { - favoriteProperty().set(favorite); - } - - public final void setImage(ImageView image) { - imageProperty().set(image); - } - } diff --git a/src/main/java/kellerkinder/HomeFlix/player/Player.java b/src/main/java/kellerkinder/HomeFlix/player/Player.java index 0f34e23..4d3c297 100644 --- a/src/main/java/kellerkinder/HomeFlix/player/Player.java +++ b/src/main/java/kellerkinder/HomeFlix/player/Player.java @@ -22,8 +22,16 @@ package kellerkinder.HomeFlix.player; +import java.awt.Desktop; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; import java.net.URLConnection; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + import javafx.fxml.FXMLLoader; import javafx.scene.Scene; import javafx.scene.image.Image; @@ -31,13 +39,13 @@ import javafx.scene.layout.AnchorPane; import javafx.stage.Stage; import kellerkinder.HomeFlix.application.Main; import kellerkinder.HomeFlix.controller.DBController; - public class Player { private PlayerController playerController; private Stage stage; private AnchorPane pane; private Scene scene; + private static final Logger LOGGER = LogManager.getLogger(Player.class.getName()); // TODO move the players choose logic to a separate static class @@ -46,6 +54,20 @@ public class Player { * @param currentTableFilm the currently selected film */ public Player(String streamURL) { + + if (isSupportedFormat(streamURL)) { + hfPlayer(streamURL); + } else { + legacyPlayer(streamURL); + } + + } + + /** + * start the integrated player + * @param streamURL + */ + private void hfPlayer(String streamURL) { playerController = new PlayerController(this, streamURL); try { @@ -72,6 +94,61 @@ public class Player { e.printStackTrace(); } } + + /** + * + */ + private void legacyPlayer(String streamURL) { + LOGGER.warn("using fallback player!"); + if (System.getProperty("os.name").contains("Linux")) { + String line; + String output = ""; + Process p; + try { + p = Runtime.getRuntime().exec("which vlc"); + BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream())); + while ((line = input.readLine()) != null) { + output = line; + } + LOGGER.info("which vlc: " + output); + input.close(); + } catch (IOException e1) { + e1.printStackTrace(); + } + if (output.contains("which: no vlc") || output == "") { +// JFXInfoAlert vlcInfoAlert = new JFXInfoAlert("Info", +// XMLController.getLocalBundle().getString("vlcNotInstalled"), btnStyle, primaryStage); +// vlcInfoAlert.showAndWait(); + } else { + try { + new ProcessBuilder("vlc", streamURL).start(); + } catch (IOException e) { + LOGGER.warn("An error has occurred while opening the file!", e); + } + } + + } else if (System.getProperty("os.name").contains("Windows") || System.getProperty("os.name").contains("Mac OS X")) { + try { + Desktop.getDesktop().open(new File(streamURL)); + } catch (IOException e) { + LOGGER.warn("An error has occurred while opening the file!", e); + } + } else { + LOGGER.error(System.getProperty("os.name") + ", OS is not supported, please contact a developer! "); + } + } + + /** + * check if a film is supported by the HomeFlixPlayer or not this is the case if + * the mime type is mp4 + * + * @param streamURL URL of the stream you want to check + * @return true if so, false if not + */ + public static boolean isSupportedFormat(String streamURL) { + String mimeType = URLConnection.guessContentTypeFromName(streamURL); + return mimeType != null && (mimeType.contains("mp4") || mimeType.contains("vp6")); + } public Stage getStage() { return stage; @@ -81,16 +158,4 @@ public class Player { return scene; } - /** - * check if a film is supported by the HomeFlixPlayer or not this is the case if - * the mime type is mp4 - * - * @param entry the film you want to check - * @return true if so, false if not - */ - public static boolean isSupportedFormat(String streamURL) { - String mimeType = URLConnection.guessContentTypeFromName(streamURL); - return mimeType != null && (mimeType.contains("mp4") || mimeType.contains("vp6")); - } - } diff --git a/src/main/java/org/kellerkinder/Alerts/JFX2BtnCancelAlert.java b/src/main/java/org/kellerkinder/Alerts/JFX2BtnCancelAlert.java index 2821b11..cab44da 100644 --- a/src/main/java/org/kellerkinder/Alerts/JFX2BtnCancelAlert.java +++ b/src/main/java/org/kellerkinder/Alerts/JFX2BtnCancelAlert.java @@ -90,7 +90,7 @@ public class JFX2BtnCancelAlert { JFXButton cancelBtn = new JFXButton(); cancelBtn.setText(cancelText); cancelBtn.addEventHandler(ActionEvent.ACTION, (e)-> alert.close()); - cancelBtn.addEventHandler(ActionEvent.ACTION, (e)-> System.exit(0)); // TODO only on first start + cancelBtn.setButtonType(com.jfoenix.controls.JFXButton.ButtonType.RAISED); cancelBtn.setPrefHeight(32); cancelBtn.setStyle(btnStyle); @@ -100,11 +100,15 @@ public class JFX2BtnCancelAlert { content.setHeading(new Text(headingText)); content.setBody(new Text(bodyText)); - // TODO only on first start - Stage stage = (Stage) alert.getDialogPane().getScene().getWindow(); - stage.setMinWidth(416); - stage.setMinHeight(162); - stage.setOnCloseRequest(event -> System.exit(0)); + // only on first start + if (stage == null) { + cancelBtn.addEventHandler(ActionEvent.ACTION, (e)-> System.exit(0)); + Stage stage = (Stage) alert.getDialogPane().getScene().getWindow(); + stage.setMinWidth(416); + stage.setMinHeight(162); + stage.setOnCloseRequest(event -> System.exit(0)); + } + alert.setContent(content); alert.showAndWait(); From 9f8a7c0f4c6e40fcd64c3a6f1942aa4f4ada7ba9 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Sun, 23 Jun 2019 15:23:03 +0200 Subject: [PATCH 85/88] fixed scrollpane jumping to the top after returning from detailview * use plot=full to get a better description --- .../HomeFlix/application/MainWindowController.java | 9 ++++----- .../HomeFlix/controller/OMDbAPIController.java | 5 +++-- src/main/resources/fxml/MainWindow.fxml | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index 4c8eb0c..ec20f68 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -158,6 +158,7 @@ public class MainWindowController { // general actions HamburgerBackArrowBasicTransition burgerTask = new HamburgerBackArrowBasicTransition(menuHam); menuHam.addEventHandler(MouseEvent.MOUSE_PRESSED, (e) -> { + if (menuTrue) { sideMenuSlideOut(); burgerTask.setRate(-1.0); @@ -173,8 +174,7 @@ public class MainWindowController { if (settingsViewController.isVisible()) { settingsViewController.setVisible(false); } - });primaryStage.setMinHeight(600.00 + 34); // 34 -> window decoration - primaryStage.setMinWidth(1130.00); + }); } // general fxml actions @@ -296,7 +296,6 @@ public class MainWindowController { // slide in in 400ms private void sideMenuSlideIn() { - sideMenuVBox.setVisible(true); TranslateTransition translateTransition = new TranslateTransition(Duration.millis(400), sideMenuVBox); translateTransition.setFromX(-150); translateTransition.setToX(0); @@ -374,7 +373,7 @@ public class MainWindowController { ExecutorService executor = Executors.newFixedThreadPool(5); for (FilmTabelDataType entry : dbController.getAllNotCachedEntries()) { - System.out.println(entry.getStreamUrl() + " is NOT cached!"); +// System.out.println(entry.getStreamUrl() + " is NOT cached!"); Runnable OMDbAPIWorker = new OMDbAPIController(entry); executor.execute(OMDbAPIWorker); @@ -432,7 +431,7 @@ public class MainWindowController { for(FilmTabelDataType entry : dbController.getStreamsList()) { if (dbController.getCacheDate(entry.getStreamUrl()).isBefore(lastValidCache)) { - System.out.println(entry.getTitle() + " chached on: " + dbController.getCacheDate(entry.getStreamUrl())); +// System.out.println(entry.getTitle() + " chached on: " + dbController.getCacheDate(entry.getStreamUrl())); Runnable OMDbAPIWorker = new OMDbAPIController(entry); executor.execute(OMDbAPIWorker); } diff --git a/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java b/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java index e017ede..54b0852 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/OMDbAPIController.java @@ -165,10 +165,11 @@ public class OMDbAPIController implements Runnable { apiUrl = new URL(URL + omdbAPIKey + "&t=" + title.replace(" ", "%20") + "&Season=" + currentTableFilm.getSeason() - + "&Episode=" + currentTableFilm.getEpisode()); + + "&Episode=" + currentTableFilm.getEpisode() + + "&plot=full"); } else { apiUrl = new URL(URL + omdbAPIKey + "&t=" - + title.replace(" ", "%20")); + + title.replace(" ", "%20") + "&plot=full"); } BufferedReader ina = new BufferedReader(new InputStreamReader(apiUrl.openStream())); diff --git a/src/main/resources/fxml/MainWindow.fxml b/src/main/resources/fxml/MainWindow.fxml index 9a243b2..a8b5461 100644 --- a/src/main/resources/fxml/MainWindow.fxml +++ b/src/main/resources/fxml/MainWindow.fxml @@ -33,7 +33,7 @@ - + From d8062513e9a3b3ba8c94cd9928d12632c1e9cf97 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Sun, 23 Jun 2019 15:26:05 +0200 Subject: [PATCH 86/88] fixed animation coordinates for 9f8a7c0f4c --- .../HomeFlix/application/MainWindowController.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index ec20f68..2aba921 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -297,16 +297,16 @@ public class MainWindowController { // slide in in 400ms private void sideMenuSlideIn() { TranslateTransition translateTransition = new TranslateTransition(Duration.millis(400), sideMenuVBox); - translateTransition.setFromX(-150); - translateTransition.setToX(0); + translateTransition.setFromX(0); + translateTransition.setToX(150); translateTransition.play(); } // slide out in 400ms private void sideMenuSlideOut() { TranslateTransition translateTransition = new TranslateTransition(Duration.millis(400), sideMenuVBox); - translateTransition.setFromX(0); - translateTransition.setToX(-150); + translateTransition.setFromX(150); + translateTransition.setToX(0); translateTransition.play(); } From 75a80535a757cc5bc6bc82168067a5b5b0ec746d Mon Sep 17 00:00:00 2001 From: Seil0 Date: Thu, 27 Jun 2019 13:30:44 +0200 Subject: [PATCH 87/88] reworked the UpdateController --- .../application/MainWindowController.java | 17 +-- .../HomeFlix/application/SettingsView.java | 15 ++- .../HomeFlix/controller/UpdateController.java | 110 ++++++++---------- 3 files changed, 63 insertions(+), 79 deletions(-) diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index 2aba921..65e6090 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -52,8 +52,8 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.ScrollPane; -import javafx.scene.effect.BoxBlur; import javafx.scene.control.ScrollPane.ScrollBarPolicy; +import javafx.scene.effect.BoxBlur; import javafx.scene.input.MouseEvent; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.FlowPane; @@ -95,7 +95,6 @@ public class MainWindowController { private static MainWindowController instance = null; private DBController dbController; - private UpdateController updateController; private XMLController xmlController; private Stage primaryStage; private static final Logger LOGGER = LogManager.getLogger(MainWindowController.class.getName()); @@ -337,18 +336,10 @@ public class MainWindowController { // if AutoUpdate, then check for updates private void checkAutoUpdate() { + LOGGER.info("AutoUpdate: looking for updates on startup ..."); - if (XMLController.isAutoUpdate()) { - try { - LOGGER.info("AutoUpdate: looking for updates on startup ..."); - updateController = new UpdateController(settingsViewController); // TODO this is will crash - Thread updateThread = new Thread(updateController); - updateThread.setName("Updater"); - updateThread.start(); - updateThread.join(); - } catch (InterruptedException e) { - e.printStackTrace(); - } + if (XMLController.isAutoUpdate() && UpdateController.isUpdateAvailable()) { + UpdateController.update(); } } diff --git a/src/main/java/kellerkinder/HomeFlix/application/SettingsView.java b/src/main/java/kellerkinder/HomeFlix/application/SettingsView.java index d1062a6..6a5aa61 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/SettingsView.java +++ b/src/main/java/kellerkinder/HomeFlix/application/SettingsView.java @@ -47,9 +47,9 @@ import javafx.fxml.FXML; import javafx.scene.control.ChoiceBox; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; +import javafx.scene.control.ScrollPane.ScrollBarPolicy; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; -import javafx.scene.control.ScrollPane.ScrollBarPolicy; import javafx.scene.layout.AnchorPane; import javafx.scene.paint.Color; import javafx.stage.DirectoryChooser; @@ -206,9 +206,16 @@ public class SettingsView { @FXML private void updateBtnAction(ActionEvent event) { - Thread updateThread = new Thread(new UpdateController(this)); - updateThread.setName("Updater"); - updateThread.start(); + + + new UpdateController(); + if (UpdateController.isUpdateAvailable()) { + updateBtn.setText(XMLController.getLocalBundle().getString("updateBtnUpdateAvailable")); + UpdateController.update(); + } else { + updateBtn.setText(XMLController.getLocalBundle().getString("updateBtnNoUpdateAvailable")); + } + } /** TODO can this be done async? diff --git a/src/main/java/kellerkinder/HomeFlix/controller/UpdateController.java b/src/main/java/kellerkinder/HomeFlix/controller/UpdateController.java index ace49cd..7e1cf2c 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/UpdateController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/UpdateController.java @@ -40,58 +40,79 @@ import com.eclipsesource.json.JsonArray; import com.eclipsesource.json.JsonObject; import com.eclipsesource.json.JsonValue; -import javafx.application.Platform; import kellerkinder.HomeFlix.application.Main; -import kellerkinder.HomeFlix.application.SettingsView; -public class UpdateController implements Runnable { +public class UpdateController { - private SettingsView settingsViewController; - private int updateBuildNumber; // tag_name from gitea - private String apiOutput; + private static int updateBuildNumber; // tag_name from gitea @SuppressWarnings("unused") - private String updateName; + private static String updateName; @SuppressWarnings("unused") - private String updateChanges; - private String browserDownloadUrl; // update download link - private String giteaApiRelease = "https://git.mosad.xyz/api/v1/repos/Seil0/Project-HomeFlix/releases"; - private URL giteaApiUrl; + private static String updateChanges; + private static String browserDownloadUrl; // update download link + private static final String giteaApiRelease = "https://git.mosad.xyz/api/v1/repos/Seil0/Project-HomeFlix/releases"; private static final Logger LOGGER = LogManager.getLogger(UpdateController.class.getName()); /** * updater for Project HomeFlix based on cemu_UIs, checks for Updates and download it - * @param mwc the MainWindowController object - * @param buildNumber the buildNumber of the used HomeFlix version - * @param useBeta if the updater should query the beta channel */ - public UpdateController(SettingsView svc) { - settingsViewController = svc; + public UpdateController() { + } - @Override - public void run() { + public static void update() { + if (browserDownloadUrl != null) { + LOGGER.info("download link: " + browserDownloadUrl); + + Runnable run = () -> { + try { + // open new HTTP connection, ProgressMonitorInputStream for downloading the data + // FIXME the download progress dialog is not showing! + HttpURLConnection connection = (HttpURLConnection) new URL(browserDownloadUrl).openConnection(); + ProgressMonitorInputStream pmis = new ProgressMonitorInputStream(null, "Downloading...", connection.getInputStream()); + ProgressMonitor pm = pmis.getProgressMonitor(); + pm.setMillisToDecideToPopup(0); + pm.setMillisToPopup(0); + pm.setMinimum(0);// set beginning of the progress bar to 0 + pm.setMaximum(connection.getContentLength());// set the end to the file length + FileUtils.copyInputStreamToFile(pmis, new File("ProjectHomeFlix_update.jar")); // download update + LOGGER.info("update download successful, restarting ..."); + org.apache.commons.io.FileUtils.copyFile(new File("ProjectHomeFlix_update.jar"), new File("ProjectHomeFlix.jar")); + org.apache.commons.io.FileUtils.deleteQuietly(new File("ProjectHomeFlix_update.jar")); // delete update + new ProcessBuilder("java", "-jar", "ProjectHomeFlix.jar").start(); // start the new application + System.exit(0); // close the current application + } catch (IOException e) { + LOGGER.info("could not download update files", e); + } + }; + + Thread t = new Thread(run); + t.start(); + + } + } + + + public static boolean isUpdateAvailable() { LOGGER.info("beta:" + XMLController.isUseBeta() + "; checking for updates ..."); - Platform.runLater(() -> { - settingsViewController.getUpdateBtn().setText(XMLController.getLocalBundle().getString("updateBtnChecking")); - }); + String apiOutput = ""; try { - giteaApiUrl = new URL(giteaApiRelease); + URL giteaApiUrl = new URL(giteaApiRelease); BufferedReader ina = new BufferedReader(new InputStreamReader(giteaApiUrl.openStream())); apiOutput = ina.readLine(); ina.close(); } catch (Exception e) { - Platform.runLater(() -> { LOGGER.error("could not check update version", e); - }); } JsonArray objectArray = Json.parse("{\"items\": " + apiOutput + "}").asObject().get("items").asArray(); JsonValue object = objectArray.get(0).asObject(); // set to the latest release as default JsonObject objectAsset = object.asObject().get("assets").asArray().get(0).asObject(); + // TODO rework stream() for(JsonValue objectIt : objectArray) { if(objectIt.asObject().getBoolean("prerelease", false) == XMLController.isUseBeta()) { // we found the needed release either beta or not @@ -104,46 +125,11 @@ public class UpdateController implements Runnable { updateBuildNumber = Integer.parseInt(object.asObject().getString("tag_name", "")); updateName = object.asObject().getString("name", ""); updateChanges = object.asObject().getString("body", ""); + browserDownloadUrl = objectAsset.getString("browser_download_url", ""); LOGGER.info("Build: " + Main.buildNumber + ", Update: " + updateBuildNumber); - /** - * Compare the program BuildNumber with the current BuildNumber - * if buildNumber < updateBuildNumber then perform a update - */ - if (Integer.parseInt(Main.buildNumber) >= updateBuildNumber) { - Platform.runLater(() -> { - settingsViewController.getUpdateBtn().setText(XMLController.getLocalBundle().getString("updateBtnNoUpdateAvailable")); - }); - LOGGER.info("no update available"); - } else { - Platform.runLater(() -> { - settingsViewController.getUpdateBtn().setText(XMLController.getLocalBundle().getString("updateBtnUpdateAvailable")); - }); - LOGGER.info("update available"); - browserDownloadUrl = objectAsset.getString("browser_download_url", ""); - LOGGER.info("download link: " + browserDownloadUrl); - try { - // open new HTTP connection, ProgressMonitorInputStream for downloading the data - // FIXME the download progress dialog is not showing! - HttpURLConnection connection = (HttpURLConnection) new URL(browserDownloadUrl).openConnection(); - ProgressMonitorInputStream pmis = new ProgressMonitorInputStream(null, "Downloading...", connection.getInputStream()); - ProgressMonitor pm = pmis.getProgressMonitor(); - pm.setMillisToDecideToPopup(0); - pm.setMillisToPopup(0); - pm.setMinimum(0);// set beginning of the progress bar to 0 - pm.setMaximum(connection.getContentLength());// set the end to the file length - FileUtils.copyInputStreamToFile(pmis, new File("ProjectHomeFlix_update.jar")); // download update - LOGGER.info("update download successful, restarting ..."); - org.apache.commons.io.FileUtils.copyFile(new File("ProjectHomeFlix_update.jar"), new File("ProjectHomeFlix.jar")); - org.apache.commons.io.FileUtils.deleteQuietly(new File("ProjectHomeFlix_update.jar")); // delete update - new ProcessBuilder("java", "-jar", "ProjectHomeFlix.jar").start(); // start the new application - System.exit(0); // close the current application - } catch (IOException e) { - Platform.runLater(() -> { - LOGGER.info("could not download update files", e); - }); - } - } + return Integer.parseInt(Main.buildNumber) < updateBuildNumber; + } } From ae20909012ca35fb1367f647817ebff7054a7856 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Thu, 4 Jul 2019 12:39:19 +0200 Subject: [PATCH 88/88] update version to 0.8.0 --- src/main/java/kellerkinder/HomeFlix/application/Main.java | 2 +- src/main/resources/css/MainWindow.css | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/kellerkinder/HomeFlix/application/Main.java b/src/main/java/kellerkinder/HomeFlix/application/Main.java index 6496fec..da229a7 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/Main.java +++ b/src/main/java/kellerkinder/HomeFlix/application/Main.java @@ -43,7 +43,7 @@ public class Main extends Application { private MainWindowController mainWindowController; private static XMLController xmlController; private static Logger LOGGER; - public static final String version = "0.7.91"; + public static final String version = "0.8.0"; public static final String buildNumber = "173"; public static final String versionName = "toothless dragon"; diff --git a/src/main/resources/css/MainWindow.css b/src/main/resources/css/MainWindow.css index 0aff210..e17c481 100644 --- a/src/main/resources/css/MainWindow.css +++ b/src/main/resources/css/MainWindow.css @@ -17,7 +17,7 @@ /******************************************************************************* * * - * TreeTable * + * TreeTable * * * ******************************************************************************/ @@ -120,6 +120,7 @@ * ScrollBar * * * ******************************************************************************/ + .scroll-bar:vertical, .scroll-bar:horizontal { -fx-background-color: transparent; }

      =fb4A}R(ts8YEOxo{{R^!48?TG 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̕K+y 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≺^geH>{?R&AkPt$@+(GZ<}_ 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 AYybcN From 512b715c1aafb48441543c5c1073242e2bf0e50c Mon Sep 17 00:00:00 2001 From: Seil0 Date: Tue, 18 Jun 2019 01:25:46 +0200 Subject: [PATCH 79/88] first working version of the PosterMode, enabled by default --- .../application/MainWindowController.java | 17 +- .../application/SeriesDetailView.java | 206 ++++++++++++++++++ .../HomeFlix/controller/DBController.java | 32 ++- .../HomeFlix/datatypes/SeriresDVEpisode.java | 67 ++++++ .../kellerkinder/HomeFlix/player/Player.java | 2 + src/main/resources/css/MainWindow.css | 6 +- src/main/resources/fxml/MainWindow.fxml | 1 + src/main/resources/fxml/SeriesDetailView.fxml | 65 +----- .../icons/2QjWeU6mfvx6sWF2z9CLb4kdRnR.jpg | Bin 6694 -> 0 bytes 9 files changed, 326 insertions(+), 70 deletions(-) create mode 100644 src/main/java/kellerkinder/HomeFlix/datatypes/SeriresDVEpisode.java delete mode 100644 src/main/resources/icons/2QjWeU6mfvx6sWF2z9CLb4kdRnR.jpg diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index 3e88eae..5c5c88d 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -165,8 +165,8 @@ public class MainWindowController { @FXML private ScrollPane posterModeScrollPane; @FXML private FlowPane posterModeFlowPane; - // FilmDetailView @FXML private FilmDetailView filmDetailViewController; + @FXML private SeriesDetailView seriesDetailViewController; private static MainWindowController instance = null; private DBController dbController; @@ -264,12 +264,6 @@ public class MainWindowController { setLocalUI(); applyColor(); - -// BoxBlur boxBlur = new BoxBlur(); -// boxBlur.setWidth(9); -// boxBlur.setHeight(7); -// boxBlur.setIterations(3); -// posterModeFlowPane.setEffect(boxBlur); } /** @@ -421,7 +415,7 @@ public class MainWindowController { } } - if ((dbController.getCached(getCurrentStreamUrl()).isAfter(lastValidCache) || getCurrentStreamUrl().contains("_rootNode")) + if ((dbController.getCacheDate(getCurrentStreamUrl()).isAfter(lastValidCache) || getCurrentStreamUrl().contains("_rootNode")) && dbController.searchCacheByURL(getCurrentStreamUrl())) { LOGGER.info("loading from cache: " + getCurrentTitle()); setSelectedFilmInfo(dbController.readCache(getCurrentStreamUrl())); @@ -795,6 +789,7 @@ public class MainWindowController { } filmDetailViewController.updateGUILocal(); + seriesDetailViewController.updateGUILocal(); aboutBtn.setText(XMLController.getLocalBundle().getString("info")); settingsBtn.setText(XMLController.getLocalBundle().getString("settings")); @@ -960,8 +955,10 @@ public class MainWindowController { filmDetailViewController.setFilm(element.getStreamURL()); filmDetailViewController.showPane(); } else { - filmDetailViewController.setFilm(element.getStreamURL()); - filmDetailViewController.showPane(); +// filmDetailViewController.setFilm(element.getStreamURL()); +// filmDetailViewController.showPane(); + seriesDetailViewController.setSeries(element.getStreamURL()); + seriesDetailViewController.showPane(); } System.out.println("selected: " + element.getStreamURL()); diff --git a/src/main/java/kellerkinder/HomeFlix/application/SeriesDetailView.java b/src/main/java/kellerkinder/HomeFlix/application/SeriesDetailView.java index 9de452d..7e0bf5b 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/SeriesDetailView.java +++ b/src/main/java/kellerkinder/HomeFlix/application/SeriesDetailView.java @@ -1,10 +1,216 @@ package kellerkinder.HomeFlix.application; +import java.awt.Desktop; +import java.io.File; +import java.io.IOException; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.jfoenix.controls.JFXButton; + +import javafx.animation.FadeTransition; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import javafx.fxml.FXML; +import javafx.scene.control.ChoiceBox; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.ScrollPane.ScrollBarPolicy; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.HBox; +import javafx.scene.text.Text; +import javafx.util.Duration; +import kellerkinder.HomeFlix.controller.DBController; +import kellerkinder.HomeFlix.controller.XMLController; +import kellerkinder.HomeFlix.datatypes.SeriresDVEpisode; public class SeriesDetailView { @FXML private AnchorPane seriesDVPane; + @FXML private ScrollPane scrollPaneEpisodes; + + @FXML private HBox hBoxEpisodes; + + @FXML private Label lblTitle; + @FXML private Label lblYear; + @FXML private Label lblScore; + + @FXML private Label lblCrew; + @FXML private Label lblDirectors; + @FXML private Label lblDirectorsInfo; + @FXML private Label lblWriters; + @FXML private Label lblWritersInfo; + @FXML private Label lblActors; + @FXML private Label lblActorsInfo; + + @FXML private JFXButton btnWishlist; + @FXML private JFXButton btnFavourite; + @FXML private JFXButton btnHide; + @FXML private JFXButton btnPlay; + @FXML private JFXButton btnDirectory; + + @FXML private ImageView wishlistIcon; + @FXML private ImageView favoriteIcon; + @FXML private ImageView imgPoster; + + @FXML private Text textPlot; + + @FXML private ChoiceBox cbSeason; + + private DBController dbController; + private static final Logger LOGGER = LogManager.getLogger(SeriesDetailView.class.getName()); + private String currentStreamURL; + + public void initialize() { + dbController = DBController.getInstance(); + seriesDVPane.setStyle("-fx-background-color: rgba(89,89,89,0.9);"); + scrollPaneEpisodes.setStyle("-fx-background-color: transparent;"); + scrollPaneEpisodes.setHbarPolicy(ScrollBarPolicy.ALWAYS); + + cbSeason.getSelectionModel().selectedIndexProperty().addListener((e, oldValue, newValue) -> { + addEpisodes(newValue.intValue() + 1); + }); + } + + @FXML + private void btnWishlistAction() { + + } + + @FXML + private void btnFavouriteAction() { + dbController.toggleFavoriteState(currentStreamURL); + + // update the favorite icon + if(dbController.getFavoriteState(currentStreamURL) == 1) { + favoriteIcon.setImage(new Image("icons/baseline_favorite_black_48dp.png")); + } else { + favoriteIcon.setImage(new Image("icons/baseline_favorite_border_black_48dp.png")); + } + } + + @FXML + private void btnHideAction() { + hidePane(); + } + + @FXML + private void btnPlayAction() { +// playFilm(); // TODO + } + + @FXML + private void btnDirectoryAction() { + File dest = new File(currentStreamURL).getParentFile(); + if (!System.getProperty("os.name").contains("Linux")) { + try { + Desktop.getDesktop().open(dest); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + /** + * set the cached data of a series to the SeriesDetailView + * @param streamURL URL of the series root directory + */ + public void setSeries(String streamURL) { + currentStreamURL = streamURL; + String[] cacheInfo = dbController.readCache(streamURL); // get the cache data from the database + + // add the cache data to the GUI + lblTitle.setText(cacheInfo[0]); + lblYear.setText("(" + cacheInfo[1] + ")"); + lblScore.setText(XMLController.getLocalBundle().getString("score") + ": " + cacheInfo[15] + "%"); + + textPlot.setText(cacheInfo[11]); + + lblDirectors.setText(cacheInfo[8]); + lblWriters.setText(cacheInfo[9]); + lblActors.setText(cacheInfo[10]); + + try { + imgPoster.setImage(new Image(new File(cacheInfo[20]).toURI().toString())); + } catch (Exception e) { + imgPoster.setImage(new Image("icons/Homeflix_Poster.png")); + LOGGER.error("No Poster found, useing default."); + } + + // set the favorite correct icon + if(dbController.getFavoriteState(streamURL) == 1) { + favoriteIcon.setImage(new Image("icons/baseline_favorite_black_48dp.png")); + } else { + favoriteIcon.setImage(new Image("icons/baseline_favorite_border_black_48dp.png")); + } + + // add all seasons to the ChoiceBox + ObservableList seasons = FXCollections.observableArrayList(); + for (int i = 1; i <= new File(currentStreamURL).listFiles().length; i++) { + seasons.add("Season " + i); + } + cbSeason.getItems().clear(); + cbSeason.setItems(seasons); + cbSeason.getSelectionModel().select(0); + + // add all episodes for season 1 + addEpisodes(1); + } + + /** + * update the text of all static GUI elements of FilmDeatilView + */ + public void updateGUILocal() { + lblCrew.setText(XMLController.getLocalBundle().getString("crew")); + lblDirectorsInfo.setText(XMLController.getLocalBundle().getString("directors")); + lblWritersInfo.setText(XMLController.getLocalBundle().getString("writers")); + lblActorsInfo.setText(XMLController.getLocalBundle().getString("actors")); + } + + /** + * show the FilmDVpane + */ + public void showPane() { + seriesDVPane.setVisible(true); + FadeTransition fadeIn = new FadeTransition(Duration.millis(300), seriesDVPane); + fadeIn.setFromValue(0.3); + fadeIn.setToValue(1.0); + fadeIn.play(); + } + + /** + * hide the FilmDVpane + */ + private void hidePane() { + FadeTransition fadeOut = new FadeTransition(Duration.millis(200), seriesDVPane); + fadeOut.setFromValue(1.0); + fadeOut.setToValue(0.3); + fadeOut.play(); + + seriesDVPane.setVisible(false); + + MainWindowController.getInstance().disableBlur(); // disable blur + } + + /** + * add all episodes of one season to the ScrollPane + * @param season the season to add + */ + private void addEpisodes(int season) { + hBoxEpisodes.getChildren().clear(); + for (String[] episodePoster : dbController.getEpisodes(new File(currentStreamURL).getName(), season)) { + Image poster; + try { + poster = new Image(episodePoster[2].length() > 0 ? new File(episodePoster[2]).toURI().toString() : "icons/Homeflix_Poster.png"); + } catch (Exception e) { + poster = new Image("icons/Homeflix_Poster.png"); + } + hBoxEpisodes.getChildren().add(new SeriresDVEpisode(episodePoster[0], episodePoster[1], poster)); + } + } + } diff --git a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java index 0312c29..c06e846 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java @@ -452,7 +452,7 @@ public class DBController { * get the cached date for the film * @param streamUrl URL of the film */ - public LocalDate getCached(String streamUrl) { + public LocalDate getCacheDate(String streamUrl) { LocalDate cacheDate = LocalDate.now().minusDays(31); try { PreparedStatement ps = connection.prepareStatement("SELECT cached FROM films WHERE streamUrl = ?"); @@ -651,6 +651,34 @@ public class DBController { } } + /** + * return the cache data for all episodes of a series season + * @param title the title of the series + * @param season the season you want + * @return a ArrayList of cache data (String Array [21]) + */ + public ArrayList getEpisodes(String title, int season) { + ArrayList episodePosters =new ArrayList<>(); + try { + // try to get a all episode of one season + PreparedStatement ps = connection.prepareStatement("SELECT streamUrl, episode FROM films WHERE title = ? AND season = ?"); + ps.setString(1, title); + ps.setString(2, Integer.toString(season)); + ResultSet rs = ps.executeQuery(); + + // for each episode load cache data + while (rs.next()) { + String[] cacheData = readCache(rs.getString("streamUrl")); + String[] ret = {rs.getString("streamUrl"), rs.getString("episode"), cacheData[20]}; + episodePosters.add(ret); + } + } catch(Exception e) { + LOGGER.error("Ups! error while getting episodes of a season!", e); + } + + return episodePosters; + } + /** * get the next episode of a series * @param title title of the film @@ -662,7 +690,7 @@ public class DBController { try { // try to get a new episode of the current season - PreparedStatement ps = connection.prepareStatement("SELECT * FROM films WHERE title = ? AND season = ? AND episode = ?"); + PreparedStatement ps = connection.prepareStatement("SELECT * FROM films WHERE title = ? AND season = ? AND episode = ?"); ps.setString(1, title); ps.setString(2, Integer.toString(season)); ps.setString(3, Integer.toString(episode + 1)); diff --git a/src/main/java/kellerkinder/HomeFlix/datatypes/SeriresDVEpisode.java b/src/main/java/kellerkinder/HomeFlix/datatypes/SeriresDVEpisode.java new file mode 100644 index 0000000..a181884 --- /dev/null +++ b/src/main/java/kellerkinder/HomeFlix/datatypes/SeriresDVEpisode.java @@ -0,0 +1,67 @@ +package kellerkinder.HomeFlix.datatypes; + +import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.AnchorPane; +import kellerkinder.HomeFlix.player.Player; + +public class SeriresDVEpisode extends AnchorPane { + + private String streamURL; + private Label label = new Label(); + private ImageView imageView = new ImageView(); + + public SeriresDVEpisode() { + super.getChildren().addAll(imageView, label); + super.prefWidth(200); + super.prefHeight(112); + + + imageView.setPreserveRatio(true); + imageView.setFitHeight(112); + + label.setStyle("-fx-text-fill: #ffffff; -fx-font-size: 14pt ; -fx-font-weight: bold;"); + super.setTopAnchor(label, 3.0); + super.setLeftAnchor(label, 7.0); + } + + public SeriresDVEpisode(String streamURL, String episode, Image poster) { + this(); + + this.streamURL = streamURL; + + label.setText(episode); + imageView.setImage(poster); + imageView.addEventHandler(MouseEvent.MOUSE_PRESSED, (e) -> { + // Always play with the integrated Player TODO + new Player(streamURL); + }); + } + + public String getStreamURL() { + return streamURL; + } + + public Label getLabel() { + return label; + } + + public ImageView getImageView() { + return imageView; + } + + public void setStreamURL(String streamURL) { + this.streamURL = streamURL; + } + + public void setLabel(Label label) { + this.label = label; + } + + public void setImageView(ImageView imageView) { + this.imageView = imageView; + } + +} diff --git a/src/main/java/kellerkinder/HomeFlix/player/Player.java b/src/main/java/kellerkinder/HomeFlix/player/Player.java index a49be2c..0f34e23 100644 --- a/src/main/java/kellerkinder/HomeFlix/player/Player.java +++ b/src/main/java/kellerkinder/HomeFlix/player/Player.java @@ -39,6 +39,8 @@ public class Player { private AnchorPane pane; private Scene scene; + // TODO move the players choose logic to a separate static class + /** * generate a new PlayerWindow * @param currentTableFilm the currently selected film diff --git a/src/main/resources/css/MainWindow.css b/src/main/resources/css/MainWindow.css index 26b7275..450ce25 100644 --- a/src/main/resources/css/MainWindow.css +++ b/src/main/resources/css/MainWindow.css @@ -187,4 +187,8 @@ .scroll-pane .corner { -fx-background-insets: 0; -} \ No newline at end of file +} + +.scroll-pane > .viewport { + -fx-background-color: transparent; +} diff --git a/src/main/resources/fxml/MainWindow.fxml b/src/main/resources/fxml/MainWindow.fxml index 3400524..e710025 100644 --- a/src/main/resources/fxml/MainWindow.fxml +++ b/src/main/resources/fxml/MainWindow.fxml @@ -75,6 +75,7 @@ + diff --git a/src/main/resources/fxml/SeriesDetailView.fxml b/src/main/resources/fxml/SeriesDetailView.fxml index c7cf83e..555c79f 100644 --- a/src/main/resources/fxml/SeriesDetailView.fxml +++ b/src/main/resources/fxml/SeriesDetailView.fxml @@ -14,7 +14,7 @@ - + @@ -118,66 +118,17 @@ - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + + diff --git a/src/main/resources/icons/2QjWeU6mfvx6sWF2z9CLb4kdRnR.jpg b/src/main/resources/icons/2QjWeU6mfvx6sWF2z9CLb4kdRnR.jpg deleted file mode 100644 index 4394dc5d8cd9adca189a3d8187d330e2820e7b74..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6694 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

    =fb4A}R(ts8YEOxo{{R^!48?TG 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̕K+y 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≺^geH>{?R&AkPt$@+(GZ<}_ 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 AYybcN literal 0 HcmV?d00001 From 656c22d48a56dd2b12bea9374476cbed87a520e7 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Sun, 16 Jun 2019 21:44:42 +0200 Subject: [PATCH 77/88] added FilmDetailView * implemented the PosterMode basics * added some new 48dp icons --- .../HomeFlix/application/FilmDetailView.java | 173 ++++++++++++++++- .../application/MainWindowController.java | 66 +++++-- .../HomeFlix/controller/DBController.java | 44 +++++ src/main/resources/fxml/FilmDetailView.fxml | 175 +++++++++++++++++- .../icons/baseline_favorite_black_48dp.png | Bin 0 -> 429 bytes .../baseline_favorite_border_black_48dp.png | Bin 0 -> 652 bytes .../icons/baseline_folder_black_48dp.png | Bin 0 -> 194 bytes ...aseline_keyboard_arrow_down_black_48dp.png | Bin 0 -> 223 bytes ...aseline_keyboard_arrow_down_white_48dp.png | Bin 0 -> 224 bytes .../icons/baseline_list_black_48dp.png | Bin 0 -> 110 bytes .../icons/baseline_play_arrow_black_48dp.png | Bin 0 -> 199 bytes .../locals/HomeFlix-Local_de_DE.properties | 8 +- .../locals/HomeFlix-Local_en_US.properties | 10 +- 13 files changed, 443 insertions(+), 33 deletions(-) create mode 100755 src/main/resources/icons/baseline_favorite_black_48dp.png create mode 100755 src/main/resources/icons/baseline_favorite_border_black_48dp.png create mode 100755 src/main/resources/icons/baseline_folder_black_48dp.png create mode 100755 src/main/resources/icons/baseline_keyboard_arrow_down_black_48dp.png create mode 100755 src/main/resources/icons/baseline_keyboard_arrow_down_white_48dp.png create mode 100755 src/main/resources/icons/baseline_list_black_48dp.png create mode 100755 src/main/resources/icons/baseline_play_arrow_black_48dp.png diff --git a/src/main/java/kellerkinder/HomeFlix/application/FilmDetailView.java b/src/main/java/kellerkinder/HomeFlix/application/FilmDetailView.java index 9db14e6..e8f4299 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/FilmDetailView.java +++ b/src/main/java/kellerkinder/HomeFlix/application/FilmDetailView.java @@ -1,43 +1,198 @@ package kellerkinder.HomeFlix.application; +import java.awt.Desktop; +import java.io.File; +import java.io.IOException; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + import com.jfoenix.controls.JFXButton; +import javafx.animation.FadeTransition; import javafx.fxml.FXML; import javafx.scene.control.Label; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.AnchorPane; import javafx.scene.text.Text; +import javafx.util.Duration; +import kellerkinder.HomeFlix.controller.DBController; +import kellerkinder.HomeFlix.controller.XMLController; public class FilmDetailView { @FXML private AnchorPane filmDVPane; + @FXML private Label lblTitle; @FXML private Label lblYear; @FXML private Label lblScore; - @FXML private JFXButton btnWhishlist; + @FXML private Label lblCrew; + @FXML private Label lblDirectors; + @FXML private Label lblDirectorsInfo; + @FXML private Label lblWriters; + @FXML private Label lblWritersInfo; + @FXML private Label lblActors; + @FXML private Label lblActorsInfo; + + @FXML private Label lblInfo; + @FXML private Label lblRuntimeInfo; + @FXML private Label lblRuntime; + @FXML private Label lblLanguageInfo; + @FXML private Label lblLanguage; + @FXML private Label lblRevenueInfo; + @FXML private Label lblRevenue; + @FXML private Label lblRatingInfo; + @FXML private Label lblRating; + + @FXML private JFXButton btnWishlist; @FXML private JFXButton btnFavourite; + @FXML private JFXButton btnHide; + @FXML private JFXButton btnPlay; + @FXML private JFXButton btnDirectory; + + @FXML private ImageView wishListIcon; + @FXML private ImageView favoriteIcon; + @FXML private ImageView imgPoster; @FXML private Text textPlot; - public void initialize() { - System.out.println("init nested controller"); - filmDVPane.setStyle("-fx-background-color: rgba(89,89,89,0.9);"); - btnWhishlist.setGraphic(new ImageView(new Image("icons/ic_play_arrow_black_18dp_1x.png"))); - } + private DBController dbController; + private static final Logger LOGGER = LogManager.getLogger(FilmDetailView.class.getName()); + private String currentStreamURL; - public void foo() { - System.out.println("test"); + public void initialize() { + dbController = DBController.getInstance(); + filmDVPane.setStyle("-fx-background-color: rgba(89,89,89,0.9);"); } @FXML - private void btnWhishlistAction() { + private void btnWishlistAction() { } @FXML private void btnFavouriteAction() { + dbController.toggleFavoriteState(currentStreamURL); + + // update the favorite icon + if(dbController.getFavoriteState(currentStreamURL) == 1) { + favoriteIcon.setImage(new Image("icons/baseline_favorite_black_48dp.png")); + } else { + favoriteIcon.setImage(new Image("icons/baseline_favorite_border_black_48dp.png")); + } + } + + @FXML + private void btnHideAction() { + hidePane(); + } + + @FXML + private void btnPlayAction() { + playFilm(); + } + + @FXML + private void btnDirectoryAction() { + File dest = new File(currentStreamURL).getParentFile(); + + if (!System.getProperty("os.name").contains("Linux")) { + try { + Desktop.getDesktop().open(dest); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + /** + * set the cached data of a stream to the FilmDetailView + * @param streamURL URL of the stream + */ + public void setFilm(String streamURL) { + currentStreamURL = streamURL; + String[] cacheInfo = dbController.readCache(streamURL); // get the cache data from the database + + // add the cache data to the GUI + lblTitle.setText(cacheInfo[0]); + lblYear.setText("(" + cacheInfo[1] + ")"); + lblScore.setText(XMLController.getLocalBundle().getString("score") + ": " + cacheInfo[15] + "%"); + + textPlot.setText(cacheInfo[11]); + + lblDirectors.setText(cacheInfo[8]); + lblWriters.setText(cacheInfo[9]); + lblActors.setText(cacheInfo[10]); + + lblRuntime.setText(cacheInfo[6]); + lblLanguage.setText(cacheInfo[12]); + lblRevenue.setText(cacheInfo[18]); + lblRating.setText(cacheInfo[2]); + + try { + if (new File(cacheInfo[20]).isFile()) { + imgPoster.setImage(new Image(new File(cacheInfo[20]).toURI().toString())); + } else { + imgPoster.setImage(new Image(cacheInfo[20])); + } + } catch (Exception e) { + imgPoster.setImage(new Image("icons/Homeflix_Poster.png")); + LOGGER.error("No Poster found, useing default."); + } + + // set the favorite correct icon + if(dbController.getFavoriteState(streamURL) == 1) { + favoriteIcon.setImage(new Image("icons/baseline_favorite_black_48dp.png")); + } else { + favoriteIcon.setImage(new Image("icons/baseline_favorite_border_black_48dp.png")); + } + + } + + /** + * update the text of all static GUI elements of FilmDeatilView + */ + public void updateGUILocal() { + lblCrew.setText(XMLController.getLocalBundle().getString("crew")); + lblDirectorsInfo.setText(XMLController.getLocalBundle().getString("directors")); + lblWritersInfo.setText(XMLController.getLocalBundle().getString("writers")); + lblActorsInfo.setText(XMLController.getLocalBundle().getString("actors")); + + lblInfo.setText(XMLController.getLocalBundle().getString("info")); + lblRuntimeInfo.setText(XMLController.getLocalBundle().getString("runtime")); + lblLanguageInfo.setText(XMLController.getLocalBundle().getString("language")); + lblRevenueInfo.setText(XMLController.getLocalBundle().getString("boxOffice")); + lblRatingInfo.setText(XMLController.getLocalBundle().getString("rated")); + } + + /** + * show the FilmDVpane + */ + public void showPane() { + filmDVPane.setVisible(true); + FadeTransition fadeIn = new FadeTransition(Duration.millis(300), filmDVPane); + fadeIn.setFromValue(0.3); + fadeIn.setToValue(1.0); + fadeIn.play(); + } + + /** + * hide the FilmDVpane + */ + private void hidePane() { + FadeTransition fadeOut = new FadeTransition(Duration.millis(200), filmDVPane); + fadeOut.setFromValue(1.0); + fadeOut.setToValue(0.3); + fadeOut.play(); + + filmDVPane.setVisible(false); + + MainWindowController.getInstance().disableBlur(); // disable blur + } + + private void playFilm() { } diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index e8febb7..e9f324e 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -168,6 +168,7 @@ public class MainWindowController { // FilmDetailView @FXML private FilmDetailView filmDetailViewController; + private static MainWindowController instance = null; private DBController dbController; private UpdateController updateController; private XMLController xmlController; @@ -195,16 +196,26 @@ public class MainWindowController { private ObservableList posterEmenents = FXCollections.observableArrayList(); private static ObservableList sourcesList = FXCollections.observableArrayList(); private MenuItem like = new MenuItem("like"); - private MenuItem dislike = new MenuItem("dislike"); // TODO one option (like or dislike) + private MenuItem dislike = new MenuItem("dislike"); private ContextMenu menu = new ContextMenu(like, dislike); private LocalDate lastValidCache = LocalDate.now().minusDays(30); // current date - 30 days is the last valid cache date public MainWindowController() { // the constructor } + + public static MainWindowController getInstance() { + if (instance == null) { + LOGGER.error("There was a fatal error: instance is null!"); + instance = new MainWindowController(); + } + + return instance; + } @FXML public void initialize() { + instance = this; xmlController = new XMLController(); dbController = DBController.getInstance(); } @@ -226,7 +237,7 @@ public class MainWindowController { // load sources list in gui addSourceToTable(); - posterModeStartup(); // TODO testing DO NOT USE THIS!! +// posterModeStartup(); // TODO testing DO NOT USE THIS!! } // Initialize general UI elements @@ -254,11 +265,11 @@ public class MainWindowController { setLocalUI(); applyColor(); - BoxBlur boxBlur = new BoxBlur(); - boxBlur.setWidth(9); - boxBlur.setHeight(7); - boxBlur.setIterations(3); - posterModeFlowPane.setEffect(boxBlur); +// BoxBlur boxBlur = new BoxBlur(); +// boxBlur.setWidth(9); +// boxBlur.setHeight(7); +// boxBlur.setIterations(3); +// posterModeFlowPane.setEffect(boxBlur); } /** @@ -491,7 +502,7 @@ public class MainWindowController { } } } - + @FXML private void openfolderbtnclicked() { File dest = new File(getCurrentStreamUrl()).getParentFile(); @@ -508,7 +519,6 @@ public class MainWindowController { @FXML private void returnBtnclicked() { filmsTreeTable.getSelectionModel().select(last); - filmDetailViewController.foo(); // TODO } @FXML @@ -783,6 +793,9 @@ public class MainWindowController { languageChoisBox.getSelectionModel().select(0); break; } + + filmDetailViewController.updateGUILocal(); + aboutBtn.setText(XMLController.getLocalBundle().getString("info")); settingsBtn.setText(XMLController.getLocalBundle().getString("settings")); searchTextField.setPromptText(XMLController.getLocalBundle().getString("tfSearch")); @@ -818,8 +831,8 @@ public class MainWindowController { nameText[5] = new Text(XMLController.getLocalBundle().getString("episode") + ": "); nameText[6] = new Text(XMLController.getLocalBundle().getString("runtime") + ": "); nameText[7] = new Text(XMLController.getLocalBundle().getString("genre") + ": "); - nameText[8] = new Text(XMLController.getLocalBundle().getString("director") + ": "); - nameText[9] = new Text(XMLController.getLocalBundle().getString("writer") + ": "); + nameText[8] = new Text(XMLController.getLocalBundle().getString("directors") + ": "); + nameText[9] = new Text(XMLController.getLocalBundle().getString("writers") + ": "); nameText[10] = new Text(XMLController.getLocalBundle().getString("actors") + ": "); nameText[11] = new Text(XMLController.getLocalBundle().getString("plot") + ": "); nameText[12] = new Text(XMLController.getLocalBundle().getString("language") + ": "); @@ -916,6 +929,7 @@ public class MainWindowController { executor.shutdown(); // TODO show loading screen + // we might need this as otherwise it would load before all tasks are finished try { executor.awaitTermination(1, TimeUnit.MINUTES); @@ -936,11 +950,41 @@ public class MainWindowController { posterEmenents.clear(); posterEmenents = dbController.getPosterElementsList(); // returns a list of all PosterElements stored in the database + // add button onAction + for (PosterModeElement element : posterEmenents) { + element.getButton().addEventHandler(MouseEvent.MOUSE_CLICKED, (event) -> { + enableBlur(); // blur the FlowPane + + // if the selected element is a file it's a film, else a series + if (new File(element.getStreamURL()).isFile()) { + filmDetailViewController.setFilm(element.getStreamURL()); + filmDetailViewController.showPane(); + } else { + filmDetailViewController.setFilm(element.getStreamURL()); + filmDetailViewController.showPane(); + } + + System.out.println("selected: " + element.getStreamURL()); + }); + } + posterModeFlowPane.getChildren().clear(); // remove all GUIElements from the posterModeFlowPane posterModeFlowPane.getChildren().addAll(posterEmenents); // add all films/series as new GUIElements to the posterModeFlowPane System.out.println("added gui elements"); } + + private void enableBlur() { + BoxBlur boxBlur = new BoxBlur(); + boxBlur.setWidth(9); + boxBlur.setHeight(7); + boxBlur.setIterations(3); + posterModeFlowPane.setEffect(boxBlur); + } + + public void disableBlur() { + posterModeFlowPane.setEffect(null); + } // getter and setter diff --git a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java index 3ecd905..0312c29 100644 --- a/src/main/java/kellerkinder/HomeFlix/controller/DBController.java +++ b/src/main/java/kellerkinder/HomeFlix/controller/DBController.java @@ -353,6 +353,50 @@ public class DBController { } } + /** + * return the favorite state for a stream + * @param streamURL URL of the stream + * @return 0 if it's not a favorite, else 1 + */ + public int getFavoriteState(String streamURL) { + int favoriteState = 0; + PreparedStatement ps; + try { + ps = connection.prepareStatement("SELECT favorite FROM films WHERE streamUrl = ?"); + ps.setString(1, streamURL); + ResultSet rs = ps.executeQuery(); + + while (rs.next()) { + favoriteState = rs.getInt("favorite"); + } + + rs.close(); + ps.close(); + } catch (SQLException e) { + LOGGER.error("Ups! an error occured!", e); + } + + return favoriteState; + } + + public void toggleFavoriteState(String streamURL) { + PreparedStatement ps; + try { + if (getFavoriteState(streamURL) == 0) { + ps = connection.prepareStatement("UPDATE films SET favorite = 1 WHERE streamUrl = ?"); + } else { + ps = connection.prepareStatement("UPDATE films SET favorite = 0 WHERE streamUrl = ?"); + } + + ps.setString(1, streamURL); + ps.executeUpdate(); + connection.commit(); + ps.close(); + } catch (SQLException e) { + LOGGER.error("Ups! an error occured!", e); + } + } + /** * update the database entry for the given film, favorite = 0 * @param streamUrl URL of the film diff --git a/src/main/resources/fxml/FilmDetailView.fxml b/src/main/resources/fxml/FilmDetailView.fxml index 3faf40e..0674e8f 100644 --- a/src/main/resources/fxml/FilmDetailView.fxml +++ b/src/main/resources/fxml/FilmDetailView.fxml @@ -2,16 +2,18 @@ + + - + @@ -20,7 +22,7 @@ - @@ -50,12 +69,152 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - diff --git a/src/main/resources/icons/baseline_favorite_black_48dp.png b/src/main/resources/icons/baseline_favorite_black_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..41caa7ebba76bc34c30331db90c1a0ec6e975afb GIT binary patch literal 429 zcmV;e0aE^nP)_%lY+#L4yVj zkd<~h@2Rf--ngm7NaaRpamy=R{(9z|-Bu`MrejK?tl#z;0bry({)n!>W5;gi^kt!y6C+gJOU)1TXNsu}sGIfO1WSt^YohD1P zij1{NoEI6pAn{sc?6pLv$XKUDhsaom#8Z*6rxKS%#;!>05EHj$x=s+!=lXtGYr)UZaW$>rUqj(wtu4k+ME6L;tk1q^dXG@fV1C}^B^ z(O|zUP@G8~iQ@TYg$7M6{t53b)HqW;5e0f`rvEhE(*m!bq6t$x5H)&WiY86eE~?aS zq6|j6D$-mvS|&r&dsdsFvii4jf>8hf XLGa<15zUyF00000NkvXXu0mjf2U5hs literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/baseline_favorite_border_black_48dp.png b/src/main/resources/icons/baseline_favorite_border_black_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..a696cfd15fb3f8b6c4a61893c6fa3dc8a46ceabf GIT binary patch literal 652 zcmV;70(1R|P)A!H%%s(j6N?OsgAa2tW%`{7ztwsyV=clLNU@`rL0HenkwbQiX1BC z8XDhJDG)0HWiM&GRi$vO2vI3-(YUBe#j&D*O1X%}CRG}Y6&+Q|W;A-K(oL+mpi+9F z5hF)=4V+a;GNRG3TdpOZIOm*7*p04IGXFT7(M*m0NgfPrke2FB+y&pM(X%m>Nt)g{ zct!_tQ;obwASP6*vGD_L1T@=Ig|T2o*A5nA~9_3fY} sQM3JvIJ~9`9zXKf{a1+Lu{|Fc;vY?^@$}Q540IHOr>mdKI;Vst096o1q5uE@ literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/baseline_keyboard_arrow_down_black_48dp.png b/src/main/resources/icons/baseline_keyboard_arrow_down_black_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..f984862cd070abc9c760527a310400fd47932327 GIT binary patch literal 223 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0DWu7jMAr*{o&o;8P1PHi2WEK%B zlCR z)LGI}W=xHZP0Fl?pQkTYjPm|*&!cdH{C|7hi$c5Zwm28{* literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/baseline_keyboard_arrow_down_white_48dp.png b/src/main/resources/icons/baseline_keyboard_arrow_down_white_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..f31e4c8e4b0b7bb3a69297e0832386526db3022b GIT binary patch literal 224 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0D<(@8%Ar*{o&oZ*L1c*3XWET+< ze5kLiaADGIM+TOI?h`IbDK%I(R|@#vd&2x)=RsUy`JCc&7lTglaz*{*m$epN`p4nq z5kK{^FER{gLW&x@BpaIJ)<^_Qe5E8G*c>s@aFNp1IRV0f$}>-FKI&9^O8@zv&#ubx zCT)8XHwryzK9F@vT1>j}K-xKJG5zX$J3J;_cRjR6Pv}|cM1%F*XVsnj^wQ2*KEES< Xahs}9rs_?Q9~eAc{an^LB{Ts5^8r+r literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/baseline_list_black_48dp.png b/src/main/resources/icons/baseline_list_black_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..3351e6fbd862df92c866a3143c54fd07fcc5746d GIT binary patch literal 110 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCQ%@JikP61PS2l7oFmNzCevgl; zj&=;X<4r literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/baseline_play_arrow_black_48dp.png b/src/main/resources/icons/baseline_play_arrow_black_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..30967bc288ed3f74c90ae186e25eaa9c00d2c162 GIT binary patch literal 199 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0DK2I0NkP61P(+mY03w#tUz?%N6oc x?yd_C_bEJ%wP2C_c1it2fqfjyO+C*^A}(r1mptpL_5oeS;OXk;vd$@?2>`K|MZN$4 literal 0 HcmV?d00001 diff --git a/src/main/resources/locals/HomeFlix-Local_de_DE.properties b/src/main/resources/locals/HomeFlix-Local_de_DE.properties index f93973c..5363deb 100644 --- a/src/main/resources/locals/HomeFlix-Local_de_DE.properties +++ b/src/main/resources/locals/HomeFlix-Local_de_DE.properties @@ -43,8 +43,8 @@ season = Staffel episode = Episode runtime = Laufzeit genre = Gener -director = Regisseur -writer = Autor +directors = Regisseur +writers = Autoren actors = Schauspieler plot = Beschreibung language = Original Sprache @@ -60,3 +60,7 @@ website = Webseite addSourceHeader = Neue Quelle hinzuf\u00FCgen addSourceBody = HomeFlix konnte keine Quelle finden. \nFüge eine loakels Verzeichniss oder eine Sreaming Datei als neue Quelle hinzu. cancelBtnText = Abbrechen + +#DetailView +crew = Haupt-Crew +score = Benutzerbewertung diff --git a/src/main/resources/locals/HomeFlix-Local_en_US.properties b/src/main/resources/locals/HomeFlix-Local_en_US.properties index a3db43f..c303d44 100644 --- a/src/main/resources/locals/HomeFlix-Local_en_US.properties +++ b/src/main/resources/locals/HomeFlix-Local_en_US.properties @@ -41,10 +41,10 @@ rated = Rating released = published on season = Season episode = Episode -runtime = Duration +runtime = Runtime genre = Gener -director = Director -writer = Writer +directors = Directors +writers = Writers actors = Actors plot = Plot language = Language @@ -60,3 +60,7 @@ website = Website addSourceHeader = add a new source addSourceBody = HomeFlix was not able to load a source. \nAdd a new local directory oa a streaming file as new source. cancelBtnText = cancel + +#DetailView +crew = Featured Crew +score = User Score From 693650fece796226a15290253618a110e69cd969 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Mon, 17 Jun 2019 00:44:44 +0200 Subject: [PATCH 78/88] added first layout for SeriesDetailView --- .../HomeFlix/application/FilmDetailView.java | 50 ++++- .../HomeFlix/application/Main.java | 2 +- .../application/MainWindowController.java | 4 +- .../application/SeriesDetailView.java | 10 + .../kellerkinder/HomeFlix/player/Player.java | 32 ++- .../HomeFlix/player/PlayerController.java | 4 +- src/main/resources/fxml/FilmDetailView.fxml | 12 +- src/main/resources/fxml/MainWindow.fxml | 4 +- src/main/resources/fxml/SeriesDetailView.fxml | 212 ++++++++++++++++++ .../icons/2QjWeU6mfvx6sWF2z9CLb4kdRnR.jpg | Bin 0 -> 6694 bytes ...e_keyboard_arrow_down_white_48dp_48x16.png | Bin 0 -> 290 bytes src/main/resources/icons/spider-man.jpg | Bin 242839 -> 0 bytes 12 files changed, 302 insertions(+), 28 deletions(-) create mode 100644 src/main/java/kellerkinder/HomeFlix/application/SeriesDetailView.java create mode 100644 src/main/resources/fxml/SeriesDetailView.fxml create mode 100644 src/main/resources/icons/2QjWeU6mfvx6sWF2z9CLb4kdRnR.jpg create mode 100644 src/main/resources/icons/baseline_keyboard_arrow_down_white_48dp_48x16.png delete mode 100644 src/main/resources/icons/spider-man.jpg diff --git a/src/main/java/kellerkinder/HomeFlix/application/FilmDetailView.java b/src/main/java/kellerkinder/HomeFlix/application/FilmDetailView.java index e8f4299..1dae333 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/FilmDetailView.java +++ b/src/main/java/kellerkinder/HomeFlix/application/FilmDetailView.java @@ -1,8 +1,10 @@ package kellerkinder.HomeFlix.application; import java.awt.Desktop; +import java.io.BufferedReader; import java.io.File; import java.io.IOException; +import java.io.InputStreamReader; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -19,6 +21,7 @@ import javafx.scene.text.Text; import javafx.util.Duration; import kellerkinder.HomeFlix.controller.DBController; import kellerkinder.HomeFlix.controller.XMLController; +import kellerkinder.HomeFlix.player.Player; public class FilmDetailView { @@ -52,7 +55,7 @@ public class FilmDetailView { @FXML private JFXButton btnPlay; @FXML private JFXButton btnDirectory; - @FXML private ImageView wishListIcon; + @FXML private ImageView wishlistIcon; @FXML private ImageView favoriteIcon; @FXML private ImageView imgPoster; @@ -192,8 +195,53 @@ public class FilmDetailView { MainWindowController.getInstance().disableBlur(); // disable blur } + // TODO rework private void playFilm() { + if(new File(currentStreamURL).isDirectory()) { + return; + } + if (Player.isSupportedFormat(currentStreamURL)) { + new Player(currentStreamURL); + } else { + LOGGER.error("using fallback player!"); + if (System.getProperty("os.name").contains("Linux")) { + String line; + String output = ""; + Process p; + try { + p = Runtime.getRuntime().exec("which vlc"); + BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream())); + while ((line = input.readLine()) != null) { + output = line; + } + LOGGER.info("which vlc: " + output); + input.close(); + } catch (IOException e1) { + e1.printStackTrace(); + } + if (output.contains("which: no vlc") || output == "") { +// JFXInfoAlert vlcInfoAlert = new JFXInfoAlert("Info", +// XMLController.getLocalBundle().getString("vlcNotInstalled"), btnStyle, primaryStage); +// vlcInfoAlert.showAndWait(); + } else { + try { + new ProcessBuilder("vlc", currentStreamURL).start(); + } catch (IOException e) { + LOGGER.warn("An error has occurred while opening the file!", e); + } + } + + } else if (System.getProperty("os.name").contains("Windows") || System.getProperty("os.name").contains("Mac OS X")) { + try { + Desktop.getDesktop().open(new File(currentStreamURL)); + } catch (IOException e) { + LOGGER.warn("An error has occurred while opening the file!", e); + } + } else { + LOGGER.error(System.getProperty("os.name") + ", OS is not supported, please contact a developer! "); + } + } } } diff --git a/src/main/java/kellerkinder/HomeFlix/application/Main.java b/src/main/java/kellerkinder/HomeFlix/application/Main.java index 2d580c6..e280d72 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/Main.java +++ b/src/main/java/kellerkinder/HomeFlix/application/Main.java @@ -72,7 +72,7 @@ public class Main extends Application { FXMLLoader loader = new FXMLLoader(); loader.setLocation(getClass().getResource("/fxml/MainWindow.fxml")); pane = (AnchorPane) loader.load(); - primaryStage.setMinHeight(600.00); + primaryStage.setMinHeight(600.00 + 34); // 34 -> window decoration primaryStage.setMinWidth(1130.00); //primaryStage.setResizable(false); primaryStage.setTitle("Project HomeFlix"); diff --git a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java index e9f324e..3e88eae 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java +++ b/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java @@ -237,7 +237,7 @@ public class MainWindowController { // load sources list in gui addSourceToTable(); -// posterModeStartup(); // TODO testing DO NOT USE THIS!! + posterModeStartup(); // TODO testing DO NOT USE THIS!! } // Initialize general UI elements @@ -460,7 +460,7 @@ public class MainWindowController { } if (isSupportedFormat(currentTableFilm)) { - new Player(getCurrentTableFilm()); + new Player(getCurrentStreamUrl()); } else { LOGGER.error("using fallback player!"); if (System.getProperty("os.name").contains("Linux")) { diff --git a/src/main/java/kellerkinder/HomeFlix/application/SeriesDetailView.java b/src/main/java/kellerkinder/HomeFlix/application/SeriesDetailView.java new file mode 100644 index 0000000..9de452d --- /dev/null +++ b/src/main/java/kellerkinder/HomeFlix/application/SeriesDetailView.java @@ -0,0 +1,10 @@ +package kellerkinder.HomeFlix.application; + +import javafx.fxml.FXML; +import javafx.scene.layout.AnchorPane; + +public class SeriesDetailView { + + @FXML private AnchorPane seriesDVPane; + +} diff --git a/src/main/java/kellerkinder/HomeFlix/player/Player.java b/src/main/java/kellerkinder/HomeFlix/player/Player.java index b585d36..a49be2c 100644 --- a/src/main/java/kellerkinder/HomeFlix/player/Player.java +++ b/src/main/java/kellerkinder/HomeFlix/player/Player.java @@ -22,16 +22,15 @@ package kellerkinder.HomeFlix.player; -import javafx.event.EventHandler; +import java.net.URLConnection; + import javafx.fxml.FXMLLoader; import javafx.scene.Scene; import javafx.scene.image.Image; import javafx.scene.layout.AnchorPane; import javafx.stage.Stage; -import javafx.stage.WindowEvent; import kellerkinder.HomeFlix.application.Main; import kellerkinder.HomeFlix.controller.DBController; -import kellerkinder.HomeFlix.datatypes.FilmTabelDataType; public class Player { @@ -44,8 +43,8 @@ public class Player { * generate a new PlayerWindow * @param currentTableFilm the currently selected film */ - public Player(FilmTabelDataType currentTableFilm) { - playerController = new PlayerController(this, currentTableFilm); + public Player(String streamURL) { + playerController = new PlayerController(this, streamURL); try { FXMLLoader fxmlLoader = new FXMLLoader(); @@ -57,13 +56,10 @@ public class Player { stage.setScene(scene); stage.setTitle("HomeFlix"); stage.getIcons().add(new Image(Main.class.getResourceAsStream("/icons/Homeflix_Icon_64x64.png"))); - stage.setOnCloseRequest(new EventHandler() { - public void handle(WindowEvent we) { - DBController.getInstance().setCurrentTime(currentTableFilm.getStreamUrl(), - playerController.getCurrentTime()); - playerController.getMediaPlayer().stop(); - stage.close(); - } + stage.setOnCloseRequest(event -> { + DBController.getInstance().setCurrentTime(streamURL, playerController.getCurrentTime()); + playerController.getMediaPlayer().stop(); + stage.close(); }); playerController.init(); @@ -83,4 +79,16 @@ public class Player { return scene; } + /** + * check if a film is supported by the HomeFlixPlayer or not this is the case if + * the mime type is mp4 + * + * @param entry the film you want to check + * @return true if so, false if not + */ + public static boolean isSupportedFormat(String streamURL) { + String mimeType = URLConnection.guessContentTypeFromName(streamURL); + return mimeType != null && (mimeType.contains("mp4") || mimeType.contains("vp6")); + } + } diff --git a/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java b/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java index cb63d4a..54e1db8 100644 --- a/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java +++ b/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java @@ -94,9 +94,9 @@ public class PlayerController { * @param player the player object (needed for closing action) * @param film the film object */ - public PlayerController(Player player, FilmTabelDataType film) { + public PlayerController(Player player, String streamURL) { this.player = player; - this.film = film; + this.film = DBController.getInstance().getStream(streamURL); } /** diff --git a/src/main/resources/fxml/FilmDetailView.fxml b/src/main/resources/fxml/FilmDetailView.fxml index 0674e8f..8034899 100644 --- a/src/main/resources/fxml/FilmDetailView.fxml +++ b/src/main/resources/fxml/FilmDetailView.fxml @@ -2,7 +2,6 @@ - @@ -89,7 +88,7 @@ - - - + - + diff --git a/src/main/resources/fxml/SeriesDetailView.fxml b/src/main/resources/fxml/SeriesDetailView.fxml new file mode 100644 index 0000000..c7cf83e --- /dev/null +++ b/src/main/resources/fxml/SeriesDetailView.fxml @@ -0,0 +1,212 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/icons/2QjWeU6mfvx6sWF2z9CLb4kdRnR.jpg b/src/main/resources/icons/2QjWeU6mfvx6sWF2z9CLb4kdRnR.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4394dc5d8cd9adca189a3d8187d330e2820e7b74 GIT binary patch literal 6694 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