From 512b715c1aafb48441543c5c1073242e2bf0e50c Mon Sep 17 00:00:00 2001 From: Seil0 Date: Tue, 18 Jun 2019 01:25:46 +0200 Subject: [PATCH] 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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

nU$tQH@DWE##0AcWGp1Ip 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 YlHWf6tfgy7^z)_miHJ0D`t_H80n4rv`v3p{