Project-HomeFlix/src/main/java/kellerkinder/HomeFlix/application/MainWindowController.java

525 lines
17 KiB
Java

/**
* 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;
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;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
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.JFXHamburger;
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.ScrollPane;
import javafx.scene.effect.BoxBlur;
import javafx.scene.control.ScrollPane.ScrollBarPolicy;
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.stage.DirectoryChooser;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
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.FilmTabelDataType;
import kellerkinder.HomeFlix.datatypes.PosterModeElement;
import kellerkinder.HomeFlix.player.Player;
public class MainWindowController {
// general
@FXML private AnchorPane mainAnchorPane;
@FXML private HBox topHBox;
@FXML private VBox sideMenuVBox;
@FXML private JFXHamburger menuHam;
@FXML private JFXButton aboutBtn;
@FXML private JFXButton settingsBtn;
// settings
@FXML private SettingsView settingsViewController;
// poster-mode
@FXML private ScrollPane posterModeScrollPane;
@FXML private FlowPane posterModeFlowPane;
@FXML private FilmDetailView filmDetailViewController;
@FXML private SeriesDetailView seriesDetailViewController;
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());
private boolean menuTrue = false;
private String btnStyle;
private FilmTabelDataType currentTableFilm = new FilmTabelDataType("", "", "", "", false, null);
private ObservableList<PosterModeElement> posterEmenents = FXCollections.observableArrayList();
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;
}
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 " + Main.buildNumber);
// initialize the GUI and the DBController
primaryStage = (Stage) mainAnchorPane.getScene().getWindow(); // set primary stage for dialogs
posterModeScrollPane.setVbarPolicy(ScrollBarPolicy.ALWAYS);
setLocalUI();
applyColor(); // TODO only on first start
initActions();
dbController.init();
// load data list in gui
posterModeStartup();
checkAutoUpdate(); // TODO async
}
// Initializing the actions
private void initActions() {
// general actions
HamburgerBackArrowBasicTransition burgerTask = new HamburgerBackArrowBasicTransition(menuHam);
menuHam.addEventHandler(MouseEvent.MOUSE_PRESSED, (e) -> {
if (menuTrue) {
sideMenuSlideOut();
burgerTask.setRate(-1.0);
burgerTask.play();
menuTrue = false;
} else {
sideMenuSlideIn();
burgerTask.setRate(1.0);
burgerTask.play();
menuTrue = true;
}
if (settingsViewController.isVisible()) {
settingsViewController.setVisible(false);
}
});primaryStage.setMinHeight(600.00 + 34); // 34 -> window decoration
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() {
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() {
settingsViewController.setVisible(!settingsViewController.isVisible());
}
/**
* 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() {
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);
// 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 sources file
*
* @param path to the source
* @param mode of the source (local or streaming)
*/
void writeSource(String path, String mode) {
JsonArray newsources = null;
try {
// 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("Error while writing sources file!", e);
}
}
/**
* set the color of the GUI-Elements if usedColor is less than checkColor set
* text fill white, else black
*/
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().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().add("jfx-hamburgerB");
}
// boxes and TextFields
sideMenuVBox.setStyle("-fx-background-color: #" + XMLController.getColor() + ";");
topHBox.setStyle("-fx-background-color: #" + XMLController.getColor() + ";");
// menu buttons
settingsBtn.setStyle(menuBtnStyle);
aboutBtn.setStyle(menuBtnStyle);
settingsViewController.updateColor(btnStyle);
}
// slide in in 400ms
private void sideMenuSlideIn() {
sideMenuVBox.setVisible(true);
TranslateTransition translateTransition = new TranslateTransition(Duration.millis(400), sideMenuVBox);
translateTransition.setFromX(-150);
translateTransition.setToX(0);
translateTransition.play();
}
// slide out in 400ms
private void sideMenuSlideOut() {
TranslateTransition translateTransition = new TranslateTransition(Duration.millis(400), sideMenuVBox);
translateTransition.setFromX(0);
translateTransition.setToX(-150);
translateTransition.play();
}
/**
* set the local based on the languageChoisBox selection
*/
void setLocalUI() {
// TODO switch expressions
switch (XMLController.getUsrLocal()) {
case "en_US":
XMLController.setLocalBundle(ResourceBundle.getBundle("locals.HomeFlix-Local", Locale.US)); // us_English
break;
case "de_DE":
XMLController.setLocalBundle(ResourceBundle.getBundle("locals.HomeFlix-Local", Locale.GERMAN)); // German
break;
default:
XMLController.setLocalBundle(ResourceBundle.getBundle("locals.HomeFlix-Local", Locale.US)); // default local
break;
}
settingsViewController.updateGUILocal();
filmDetailViewController.updateGUILocal();
seriesDetailViewController.updateGUILocal();
aboutBtn.setText(XMLController.getLocalBundle().getString("info"));
settingsBtn.setText(XMLController.getLocalBundle().getString("settings"));
}
// if AutoUpdate, then check for updates
private void checkAutoUpdate() {
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();
}
}
}
/**
* 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
*/
private void posterModeStartup() {
checkAllPosters();
addAllPosters();
checkCache();
}
/**
* check if all posters are cached, if not cache the missing ones
*/
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(entry, XMLController.getOmdbAPIKey());
executor.execute(OMDbAPIWorker);
}
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);
} 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");
}
/**
* add all cached films/series to the PosterMode GUI
*/
void addAllPosters() {
// refresh the posterModeElements list
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() || element.getStreamURL().contains("http")) {
filmDetailViewController.setFilm(element.getStreamURL());
filmDetailViewController.showPane();
} else {
seriesDetailViewController.setSeries(element.getStreamURL());
seriesDetailViewController.showPane();
}
});
}
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");
}
// 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);
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());
executor.execute(OMDbAPIWorker);
}
}
executor.shutdown();
}
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);
}
}