Fonctionne

This commit is contained in:
ExostFlash 2025-12-19 13:01:24 +01:00
parent aad4686d48
commit 0008be71bd
17 changed files with 1285 additions and 53 deletions

1
.idea/gradle.xml generated
View file

@ -5,6 +5,7 @@
<option name="linkedExternalProjectsSettings"> <option name="linkedExternalProjectsSettings">
<GradleProjectSettings> <GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="ms-17" />
<option name="modules"> <option name="modules">
<set> <set>
<option value="$PROJECT_DIR$" /> <option value="$PROJECT_DIR$" />

View file

@ -9,9 +9,6 @@ plugins {
group = "dev.mecdu" group = "dev.mecdu"
version = "1.0-SNAPSHOT" version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
val junitVersion = "5.12.1" val junitVersion = "5.12.1"
@ -32,7 +29,7 @@ application {
javafx { javafx {
version = "17.0.14" version = "17.0.14"
modules = listOf("javafx.controls", "javafx.fxml") modules = listOf("javafx.controls", "javafx.fxml", "javafx.media", "javafx.web")
} }
dependencies { dependencies {
@ -44,6 +41,10 @@ dependencies {
compileOnly("org.projectlombok:lombok:+") compileOnly("org.projectlombok:lombok:+")
annotationProcessor("org.projectlombok:lombok:1.+") annotationProcessor("org.projectlombok:lombok:1.+")
implementation("com.google.code.gson:gson:+") implementation("com.google.code.gson:gson:+")
implementation("com.sparkjava:spark-core:2.9.4")
implementation("org.openjfx:javafx-media:17.0.14")
implementation("com.github.Vatuu:discord-rpc:1.6.2")
implementation("com.github.Vatuu:discord-rpc-binaries:3.4.0")
} }
tasks.withType<Test> { tasks.withType<Test> {

View file

@ -1 +1,9 @@
rootProject.name = "javafx" rootProject.name = "javafx"
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}

View file

@ -0,0 +1,108 @@
package dev.mecdu.javafx;
import static spark.Spark.*;
import java.io.IOException;
import java.net.URI;
import java.net.URLEncoder;
import java.awt.Desktop;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import com.google.gson.Gson;
import javafx.application.Platform;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse.BodyHandlers;
import java.util.Map;
public class DiscordAuth {
private static final String CLIENT_ID = "1434628241376804904";
private static final String CLIENT_SECRET = "99XMgIsYZOWGv2t309u8UQ-waNugiTGu";
private static final String REDIRECT_URI = "http://localhost:4567/callback";
private static final Gson gson = new Gson();
private static Runnable onLoginSuccess;
public static void setOnLoginSuccess(Runnable callback) {
onLoginSuccess = callback;
}
public static void startLogin() throws IOException {
port(4567); // port pour Spark
// Ouvre Discord login dans le navigateur
String url = "https://discord.com/api/oauth2/authorize?client_id=" + CLIENT_ID +
"&redirect_uri=" + URLEncoder.encode(REDIRECT_URI, "UTF-8") +
"&response_type=code&scope=identify%20email";
Desktop.getDesktop().browse(URI.create(url));
// Serveur Spark pour récupérer le code
get("/callback", (req, res) -> {
String code = req.queryParams("code");
System.out.println("Code reçu : " + code);
// Échange du code contre le token
String accessToken = exchangeCodeForToken(code);
// Récupération info utilisateur
Map<String, Object> userData = fetchUserData(accessToken);
String username = (String) userData.get("username");
// Sauvegarde session
SessionManager.setUser(accessToken, username);
if (onLoginSuccess != null) {
Platform.runLater(onLoginSuccess); // JavaFX UI thread
}
res.type("text/html");
return "<h1>Connexion reussie !</h1><p>Vous pouvez fermer cette fenetre.</p>";
});
}
private static String exchangeCodeForToken(String code) throws IOException {
try {
String form = "client_id=" + CLIENT_ID +
"&client_secret=" + CLIENT_SECRET +
"&grant_type=authorization_code" +
"&code=" + code +
"&redirect_uri=" + URLEncoder.encode(REDIRECT_URI, "UTF-8");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://discord.com/api/oauth2/token"))
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(BodyPublishers.ofString(form))
.build();
HttpClient client = HttpClient.newHttpClient();
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
Map<String, Object> map = gson.fromJson(response.body(), Map.class);
return (String) map.get("access_token");
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
private static Map<String, Object> fetchUserData(String token) throws IOException {
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://discord.com/api/users/@me"))
.header("Authorization", "Bearer " + token)
.GET()
.build();
HttpClient client = HttpClient.newHttpClient();
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
return gson.fromJson(response.body(), Map.class);
} catch (Exception e) {
e.printStackTrace();
return Map.of("username", "Inconnu");
}
}
}

View file

@ -0,0 +1,123 @@
package dev.mecdu.javafx;
import net.arikia.dev.drpc.DiscordEventHandlers;
import net.arikia.dev.drpc.DiscordRPC;
import net.arikia.dev.drpc.DiscordRichPresence;
import java.time.Instant;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* Gestion du Rich Presence Discord
* Compatible avec java-discord-rpc (MinnDevelopment)
*/
public class DiscordPresenceManager {
private static DiscordPresenceManager instance;
private final String applicationId;
private final ScheduledExecutorService executor;
private boolean watching = false;
private Instant watchStart;
private String title = "";
private String state = "";
private String details = "";
private DiscordPresenceManager(String applicationId) {
this.applicationId = applicationId;
executor = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "DiscordRPC-Callback");
t.setDaemon(true);
return t;
});
System.out.println("[DiscordPresenceManager] Constructeur appelé avec applicationId=" + applicationId);
}
public static synchronized DiscordPresenceManager getInstance(String appId) {
if (instance == null)
instance = new DiscordPresenceManager(appId);
return instance;
}
public void init() {
DiscordEventHandlers handlers = new DiscordEventHandlers.Builder()
.setReadyEventHandler(user -> System.out.println("Discord RPC prêt : " + user.username))
.build();
DiscordRPC.discordInitialize(applicationId, handlers, true);
System.out.println("[DiscordPresenceManager] discordInitialize appelé");
executor.scheduleAtFixedRate(() -> {
try {
DiscordRPC.discordRunCallbacks();
// Log pour vérifier l'appel régulier
// System.out.println("[DiscordPresenceManager] discordRunCallbacks appelé");
} catch (Exception e) {
e.printStackTrace();
}
}, 0, 2, TimeUnit.SECONDS);
System.out.println("[DiscordPresenceManager] Callback executor lancé");
}
public void setViewing(String title) {
this.watching = false;
this.title = title;
this.state = "Consulte";
this.details = title;
updatePresence();
System.out.println("[DiscordPresenceManager] setViewing appelé : title=" + title);
}
public void startWatching(String title, Integer season, Integer episode) {
this.watching = true;
this.watchStart = Instant.now();
this.title = title;
if (season != null && episode != null)
this.state = String.format("Regarde — S%02dE%02d", season, episode);
else
this.state = "Regarde";
this.details = title;
updatePresence();
System.out.println("[DiscordPresenceManager] startWatching appelé : title=" + title + ", season=" + season + ", episode=" + episode);
}
public void stopWatching() {
this.watching = false;
this.state = "Pause";
updatePresence();
System.out.println("[DiscordPresenceManager] stopWatching appelé");
}
private void updatePresence() {
DiscordRichPresence.Builder builder = new DiscordRichPresence.Builder(state)
.setDetails(details)
.setBigImage("cover", title);
if (watching && watchStart != null) {
builder.setStartTimestamps(watchStart.getEpochSecond());
}
DiscordRichPresence presence = builder.build();
DiscordRPC.discordUpdatePresence(presence);
System.out.println("[DiscordPresenceManager] updatePresence appelé : state=" + state + ", details=" + details + ", watching=" + watching + ", watchStart=" + watchStart);
}
public void dispose() {
try {
executor.shutdownNow();
DiscordRPC.discordShutdown();
System.out.println("[DiscordPresenceManager] dispose appelé");
} catch (Exception e) {
e.printStackTrace();
}
}
}

View file

@ -16,8 +16,11 @@ public class HomeApplication extends Application {
FXMLLoader fxmlLoader = new FXMLLoader(HomeApplication.class.getResource("home.fxml")); FXMLLoader fxmlLoader = new FXMLLoader(HomeApplication.class.getResource("home.fxml"));
Parent root = fxmlLoader.load(); Parent root = fxmlLoader.load();
// Récupère le contrôleur si tu veux lui passer des infos // Init Discord RPC
// HomeController controller = loader.getController(); String discordAppId = "1434628241376804904";
dev.mecdu.javafx.DiscordPresenceManager discordManager = dev.mecdu.javafx.DiscordPresenceManager.getInstance(discordAppId);
discordManager.init();
discordManager.setViewing("Streamify");
Scene scene = new Scene(root, 1000, 700); Scene scene = new Scene(root, 1000, 700);
stage.setTitle("Streamify 🎬"); stage.setTitle("Streamify 🎬");

View file

@ -0,0 +1,31 @@
package dev.mecdu.javafx;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.scene.Node;
import java.io.IOException;
public class SceneLoader {
public static void switchTo(String fxmlPath, Node triggerNode, Object controllerData) {
try {
FXMLLoader loader = new FXMLLoader(SceneLoader.class.getResource(fxmlPath));
Parent root = loader.load();
// send donné controller
if (controllerData != null && loader.getController() instanceof TransferableData target) {
target.onDataReceived(controllerData);
}
Stage stage = (Stage) triggerNode.getScene().getWindow();
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
} catch (IOException e) {
e.printStackTrace();
}
}
}

View file

@ -0,0 +1,61 @@
package dev.mecdu.javafx;
import javafx.event.ActionEvent;
import java.util.prefs.Preferences;
public class SessionManager {
private static final Preferences prefs = Preferences.userNodeForPackage(SessionManager.class);
private static String accessToken;
private static String username;
// Charger la session au démarrage
static {
accessToken = prefs.get("discord_token", null);
username = prefs.get("discord_username", null);
}
public static void setUser(String token, String name) {
accessToken = token;
username = name;
// Sauvegarde persistante
prefs.put("discord_token", token);
prefs.put("discord_username", name);
}
public static String getToken() {
return accessToken;
}
public static String getUsername() {
return username;
}
public static boolean isLoggedIn() {
return accessToken != null;
}
public static void logout() {
accessToken = null;
username = null;
prefs.remove("discord_token");
prefs.remove("discord_username");
}
public static void login(Runnable onSuccess) {
try {
// Définir ce qui doit se passer après login
DiscordAuth.setOnLoginSuccess(() -> {
onSuccess.run();
});
// Lance la connexion
DiscordAuth.startLogin();
} catch (Exception e) {
e.printStackTrace();
}
}
}

View file

@ -0,0 +1,5 @@
package dev.mecdu.javafx;
public interface TransferableData {
void onDataReceived(Object data);
}

View file

@ -4,6 +4,10 @@ import com.google.gson.annotations.SerializedName;
import lombok.Data; import lombok.Data;
import java.util.UUID; import java.util.UUID;
import java.util.Map;
import lombok.Getter;
import lombok.Setter;
@Data @Data
public class Program { public class Program {
@ -69,8 +73,22 @@ public class Program {
tv tv
} }
@Data
public static class SeasonInfo {
@SerializedName("s_active")
private int sActive;
@SerializedName("nb_seasons")
private int nbSeasons;
@SerializedName("nb_episodes")
private int nbEpisodes;
}
private Map<String, SeasonInfo> seasons;
@Override @Override
public String toString() { public String toString() {
return title; // utile si tu ne mets pas de cellFactory return title;
} }
} }

View file

@ -1,8 +0,0 @@
package dev.mecdu.javafx.model;
import lombok.Data;
@Data
public class Programs {
private Program[] programs;
}

View file

@ -1,14 +1,19 @@
package dev.mecdu.javafx.ui; package dev.mecdu.javafx.ui;
import dev.mecdu.javafx.DiscordAuth;
import dev.mecdu.javafx.SessionManager;
import dev.mecdu.javafx.api.StreamifyApiClient; import dev.mecdu.javafx.api.StreamifyApiClient;
import dev.mecdu.javafx.model.Program; import dev.mecdu.javafx.model.Program;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.Initializable; import javafx.fxml.Initializable;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.control.TextField;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import java.net.URL; import java.net.URL;
@ -20,11 +25,31 @@ import java.util.stream.Collectors;
import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toList;
public class HomeController implements Initializable { public class HomeController implements Initializable {
@FXML
private javafx.scene.image.ImageView logoHome;
@FXML
private Button btnVoirPlus;
private static final String DISCORD_APP_ID = "1434628241376804904";
private static dev.mecdu.javafx.DiscordPresenceManager discordManager;
public VBox proSection; public VBox proSection;
public VBox tvSection; public VBox tvSection;
public VBox moviesSection; public VBox moviesSection;
@FXML @FXML
private TextField searchField;
public ImageView randomImage;
public Label randomTitle;
public StackPane randomContainer;
public VBox randomInfoBox;
public Button btnLeft;
public Button btnRight;
public Button btnConnexion;
public Button btnLogout;
public Label randomGenre;
public Label randomYear;
public Label randomDuration;
public Label randomDescription;
@FXML
private ListView<Program> movieList; private ListView<Program> movieList;
@FXML @FXML
@ -45,14 +70,107 @@ public class HomeController implements Initializable {
@FXML @FXML
private ScrollPane moviesCarousel; private ScrollPane moviesCarousel;
@FXML
private void onLeftHover() { btnLeft.setOpacity(1.0); }
@FXML
private void onLeftExit() { btnLeft.setOpacity(0.7); }
@FXML
private void onRightHover() { btnRight.setOpacity(1.0); }
@FXML
private void onRightExit() { btnRight.setOpacity(0.7); }
private int currentIndex = 0;
private List<Program> bannerShows = Collections.emptyList();
private final StreamifyApiClient apiClient = new StreamifyApiClient(); private final StreamifyApiClient apiClient = new StreamifyApiClient();
@Override @Override
public void initialize(URL url, ResourceBundle resourceBundle) { public void initialize(URL url, ResourceBundle resourceBundle) {
if (logoHome != null) {
logoHome.getStyleClass().add("logo");
try {
logoHome.setImage(new Image(getClass().getResourceAsStream("/dev/mecdu/javafx/logo.png")));
} catch (Exception e) {
System.err.println("[ERROR] Impossible de charger le logo Streamify : " + e.getMessage());
}
}
// Discord RPC : statut général Streamify
if (discordManager == null) {
discordManager = dev.mecdu.javafx.DiscordPresenceManager.getInstance(DISCORD_APP_ID);
}
discordManager.setViewing("Streamify");
if (SessionManager.isLoggedIn()) {
updateAuthButtons();
System.out.println("Connecté en tant que : " + SessionManager.getUsername());
} else {
updateAuthButtons();
}
loadTVShows(); loadTVShows();
} }
@FXML
private void onSearch(ActionEvent event) {
String q = null;
if (searchField != null) q = searchField.getText();
if (q == null || q.trim().isEmpty()) {
// si rien, je recharge l'accueil (comme avant)
loadTVShows();
return;
}
performSearch(q.trim());
}
@FXML
private void onSearch(ActionEvent event) {
String q = null;
if (searchField != null) q = searchField.getText();
if (q == null || q.trim().isEmpty()) {
// empty -> reload default
loadTVShows();
return;
}
performSearch(q.trim());
}
private void performSearch(String query) {
new Thread(() -> {
try {
List<Program> all = apiClient.getPrograms();
String q = query.toLowerCase();
List<Program> filtered = all.stream()
.filter(p -> p != null)
.filter(p -> {
String t = p.getTitle() != null ? p.getTitle().toLowerCase() : "";
String tf = p.getTitleFR() != null ? p.getTitleFR().toLowerCase() : "";
String genres = p.getGenders() != null ? p.getGenders().toLowerCase() : "";
String actors = p.getActors() != null ? p.getActors().toLowerCase() : "";
return t.contains(q) || tf.contains(q) || genres.contains(q) || actors.contains(q);
})
.collect(toList());
Platform.runLater(() -> {
// hide other sections and show results in moviesContainer
proSection.setVisible(false);
proSection.setManaged(false);
tvSection.setVisible(false);
tvSection.setManaged(false);
moviesSection.setVisible(true);
moviesSection.setManaged(true);
// display results
shows(filtered, moviesContainer);
});
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
private void loadTVShows() { private void loadTVShows() {
new Thread(() -> { new Thread(() -> {
try { try {
@ -68,7 +186,7 @@ public class HomeController implements Initializable {
Collections.shuffle(randomShows); Collections.shuffle(randomShows);
randomShows = randomShows.stream() randomShows = randomShows.stream()
.limit(10) .limit(11)
.toList(); .toList();
// filtrer seulement les prochainements et limiter à 10 // filtrer seulement les prochainements et limiter à 10
@ -79,7 +197,7 @@ public class HomeController implements Initializable {
Collections.reverse(proShows); Collections.reverse(proShows);
proShows = proShows.stream() proShows = proShows.stream()
.limit(10) .limit(11)
.toList(); .toList();
// filtrer seulement les séries TV et limiter à 10 // filtrer seulement les séries TV et limiter à 10
@ -91,7 +209,7 @@ public class HomeController implements Initializable {
Collections.reverse(tvShows); Collections.reverse(tvShows);
tvShows = tvShows.stream() tvShows = tvShows.stream()
.limit(10) .limit(11)
.toList(); .toList();
// filtrer seulement les films MOVIES et limiter à 10 // filtrer seulement les films MOVIES et limiter à 10
@ -103,7 +221,7 @@ public class HomeController implements Initializable {
Collections.reverse(moviesShows); Collections.reverse(moviesShows);
moviesShows = moviesShows.stream() moviesShows = moviesShows.stream()
.limit(10) .limit(11)
.toList(); .toList();
List<Program> finalRandomShows = randomShows; List<Program> finalRandomShows = randomShows;
@ -112,6 +230,7 @@ public class HomeController implements Initializable {
List<Program> finalMoviesShows = moviesShows; List<Program> finalMoviesShows = moviesShows;
Platform.runLater(() -> { Platform.runLater(() -> {
randomShows(finalRandomShows);
if (finalProShows.isEmpty()) { if (finalProShows.isEmpty()) {
proSection.setVisible(false); proSection.setVisible(false);
@ -133,6 +252,66 @@ public class HomeController implements Initializable {
}).start(); }).start();
} }
private void randomShows(List<Program> shows) {
if (shows.isEmpty()) return;
bannerShows = shows;
currentIndex = 0;
showBanner(bannerShows.get(currentIndex));
btnLeft.setOnAction(e -> showPreviousBanner());
btnRight.setOnAction(e -> showNextBanner());
if (btnVoirPlus != null) {
btnVoirPlus.setOnAction(e -> onVoirPlus(null));
}
}
@FXML
private void onVoirPlus(ActionEvent event) {
if (bannerShows != null && !bannerShows.isEmpty()) {
Program p = bannerShows.get(currentIndex);
dev.mecdu.javafx.SceneLoader.switchTo("/dev/mecdu/javafx/program.fxml", btnVoirPlus, p);
}
}
@FXML
private void onLogoHomeClick(javafx.scene.input.MouseEvent event) {
// Recharge la page d'accueil
dev.mecdu.javafx.SceneLoader.switchTo("/dev/mecdu/javafx/home.fxml", logoHome, null);
}
private void showBanner(Program p) {
if (p == null) return;
// Crée l'image sans taille forcée
Image image = new Image(p.getLandscapeLink(), true);
// Applique l'image et garde le ratio
randomImage.setImage(image);
randomImage.setPreserveRatio(true);
randomImage.setFitWidth(1280); // Largeur max
randomImage.setFitHeight(450); // Hauteur max sans déformation
randomTitle.setText(p.getTitle());
randomGenre.setText(p.getGenders() != null ? p.getGenders() : "Inconnu");
randomYear.setText(String.valueOf(p.getReleasedAt() != 0 ? p.getReleasedAt() : "N/A"));
randomDuration.setText(p.getLengths() != null ? p.getLengths() : "");
randomDescription.setText(p.getResum() != null ? p.getResum() : "Aucune description disponible.");
}
private void showNextBanner() {
if (bannerShows.isEmpty()) return;
currentIndex = (currentIndex + 1) % bannerShows.size();
showBanner(bannerShows.get(currentIndex));
}
private void showPreviousBanner() {
if (bannerShows.isEmpty()) return;
currentIndex = (currentIndex - 1 + bannerShows.size()) % bannerShows.size();
showBanner(bannerShows.get(currentIndex));
}
private void shows(List<Program> finalShows, HBox container) { private void shows(List<Program> finalShows, HBox container) {
container.getChildren().clear(); container.getChildren().clear();
for (Program p : finalShows) { for (Program p : finalShows) {
@ -152,8 +331,44 @@ public class HomeController implements Initializable {
card.getChildren().addAll(imageView, title); card.getChildren().addAll(imageView, title);
card.getStyleClass().add("card"); card.getStyleClass().add("card");
card.setOnMouseClicked(e -> {
System.out.println("Film cliqué : " + p.getTitle() + " (id = " + p.getId() + ")");
dev.mecdu.javafx.SceneLoader.switchTo("/dev/mecdu/javafx/program.fxml", card, p);
});
container.getChildren().add(card); container.getChildren().add(card);
} }
} }
public void login(ActionEvent actionEvent) {
SessionManager.login(() -> updateAuthButtons());
}
public void logout(ActionEvent actionEvent) {
SessionManager.logout();
updateAuthButtons();
}
private void updateAuthButtons() {
if (SessionManager.isLoggedIn()) {
showOnly(btnLogout, btnConnexion, btnLogout);
} else {
showOnly(btnConnexion, btnConnexion, btnLogout);
}
}
private void showOnly(Button visibleButton, Button... buttons) {
for (Button b : buttons) {
if (b == visibleButton) {
b.setVisible(true);
b.setManaged(true);
} else {
b.setVisible(false);
b.setManaged(false);
}
}
}
} }

View file

@ -0,0 +1,399 @@
package dev.mecdu.javafx.ui;
import dev.mecdu.javafx.SessionManager;
import dev.mecdu.javafx.TransferableData;
import dev.mecdu.javafx.model.Program;
import dev.mecdu.javafx.DiscordPresenceManager;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ComboBox;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
import javafx.scene.web.WebView;
import javafx.scene.web.WebEngine;
import javafx.scene.Node;
import javafx.scene.media.MediaView;
import javafx.scene.media.MediaPlayer;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ProgramController implements TransferableData {
@FXML
private javafx.scene.image.ImageView logoProg;
@FXML
private ComboBox<String> seasonCombo;
@FXML
private ComboBox<Integer> episodeCombo;
public ImageView imageView;
public Button btnLogout;
@FXML
private Button btnWatch;
@FXML
private Button btnTrailer;
public Label titleLabel;
public Label yearLabel;
public Label durationLabel;
public Label genreLabel;
public Label descriptionLabel;
public Button btnConnexion;
public AnchorPane videoContainer;
private Program program;
private static final String DISCORD_APP_ID = "1434628241376804904";
private static DiscordPresenceManager discordManager;
@FXML
public void initialize() {
if (logoProg != null) {
logoProg.getStyleClass().add("logo");
try {
// je charge le logo ici (petit hack pour éviter NPE)
logoProg.setImage(new javafx.scene.image.Image(getClass().getResourceAsStream("/dev/mecdu/javafx/logo.png")));
} catch (Exception e) {
System.err.println("[ERROR] Impossible de charger le logo Streamify : " + e.getMessage());
}
}
if (discordManager == null) {
discordManager = DiscordPresenceManager.getInstance(DISCORD_APP_ID);
discordManager.init();
}
// sélecteurs visible TV & Co
if (seasonCombo != null) {
seasonCombo.setVisible(false);
seasonCombo.setManaged(false);
}
if (episodeCombo != null) {
episodeCombo.setVisible(false);
episodeCombo.setManaged(false);
}
// Regarder cacher si pas Co
if (btnWatch != null) {
btnWatch.setVisible(false);
btnWatch.setManaged(false);
}
if (btnTrailer != null) {
btnTrailer.setVisible(true);
btnTrailer.setManaged(true);
}
}
@Override
public void onDataReceived(Object data) {
if (data instanceof Program p) {
this.program = p;
// afficher les infos basiques et mettre à jour les contrôles
displayProgram();
// Discord RPC : je dis que je consulte la fiche
discordManager.setViewing(program.getTitle());
// mettre à jour boutons / combos selon si c'est une série et si je suis connecté
updateControlsVisibility();
}
}
private void displayProgram() {
titleLabel.setText(program.getTitle());
descriptionLabel.setText(program.getResum());
imageView.setImage(new Image(program.getLandscapeLink()));
}
private void initSeasonEpisodeSelectors() {
try {
if (program == null || seasonCombo == null || episodeCombo == null) return;
Map<String, Program.SeasonInfo> seasons = program.getSeasons();
boolean isTv = "tv".equalsIgnoreCase(program.getTypes());
boolean isLogged = SessionManager.isLoggedIn();
if (isTv && isLogged && seasons != null && !seasons.isEmpty()) {
// afficher contrôles
seasonCombo.setVisible(true);
seasonCombo.setManaged(true);
episodeCombo.setVisible(true);
episodeCombo.setManaged(true);
List<String> keys = new ArrayList<>(seasons.keySet());
keys.sort(Comparator.comparingInt(Integer::parseInt));
ObservableList<String> seasonItems = FXCollections.observableArrayList();
for (String k : keys) {
Program.SeasonInfo info = seasons.get(k);
if (info != null) {
seasonItems.add(k);
}
}
seasonCombo.setItems(seasonItems);
if (!seasonItems.isEmpty()) {
seasonCombo.getSelectionModel().select(0);
populateEpisodesForSelectedSeason();
}
} else {
// masquer contrôles si pas Co
seasonCombo.setVisible(false);
seasonCombo.setManaged(false);
episodeCombo.setVisible(false);
episodeCombo.setManaged(false);
}
} catch (Exception e) {
System.err.println("[ERROR] initSeasonEpisodeSelectors: " + e.getMessage());
}
}
/**
* Met à jour la visibilité des contrôles de lecture et des sélecteurs en fonction
* du type du programme et de l'état de connexion.
*/
private void updateControlsVisibility() {
boolean logged = SessionManager.isLoggedIn();
// bouton Regarder visible seulement si connecté
if (btnWatch != null) {
btnWatch.setVisible(logged);
btnWatch.setManaged(logged);
}
// bouton Bande-annonce : ne pas afficher si c'est un film ET qu'on n'est pas connecté
boolean isTv = program != null && "tv".equalsIgnoreCase(program.getTypes());
if (btnTrailer != null) {
boolean trailerVisible = logged || isTv; // visible si connecté ou si c'est une série
btnTrailer.setVisible(trailerVisible);
btnTrailer.setManaged(trailerVisible);
}
// sélecteurs saison/épisode : déléguer à la méthode existante qui vérifie aussi la connexion
initSeasonEpisodeSelectors();
}
@FXML
private void onSeasonSelected(ActionEvent event) {
populateEpisodesForSelectedSeason();
}
private void populateEpisodesForSelectedSeason() {
try {
String selectedSeason = seasonCombo.getValue();
if (selectedSeason == null || selectedSeason.isEmpty()) return;
Map<String, Program.SeasonInfo> seasons = program.getSeasons();
Program.SeasonInfo info = seasons.get(selectedSeason);
if (info == null) return;
int nbEpisodes = info.getNbEpisodes();
ObservableList<Integer> epItems = FXCollections.observableArrayList();
// remplir 1..nbEpisodes
for (int i = 1; i <= nbEpisodes; i++) epItems.add(i);
episodeCombo.setItems(epItems);
if (!epItems.isEmpty()) episodeCombo.getSelectionModel().select(0);
} catch (Exception e) {
System.err.println("[ERROR] populateEpisodesForSelectedSeason: " + e.getMessage());
}
}
public void onWatch(ActionEvent actionEvent) {
try {
String videoUrl = "https://api-stream.apps.lehub.tf/api/v1.1/program/"
+ program.getTypes() + "/" + program.getUuid();
Integer selectedSeason = null;
Integer selectedEpisode = null;
if ("tv".equalsIgnoreCase(program.getTypes())) {
if (seasonCombo != null && episodeCombo != null) {
try {
String s = seasonCombo.getValue();
Integer e = episodeCombo.getValue();
if (s != null && !s.isEmpty()) selectedSeason = Integer.parseInt(s);
if (e != null) selectedEpisode = e;
} catch (Exception ex) {
// ignore parse errors, fallback to null
}
}
if (selectedSeason != null && selectedEpisode != null) {
videoUrl = videoUrl + "/" + selectedSeason + "/" + selectedEpisode;
}
}
// debug rapide : url construite
System.out.println("[INFO] URL vidéo : " + videoUrl);
WebView webView = new WebView();
WebEngine engine = webView.getEngine();
String html =
"<html>" +
"<body style='margin:0; background:black;'>" +
"<video id='player' controls autoplay style='width:100%; height:100%;'>" +
"<source src='" + videoUrl + "' type='video/webm'>" +
"<source src='" + videoUrl + "' type='video/mp4'>" +
"Votre navigateur ne supporte pas la lecture vidéo." +
"</video>" +
"</body>" +
"</html>";
// charge le lecteur HTML5 dans le WebView
engine.loadContent(html);
// insérer le webview dans le conteneur (et enlever ancien contenu)
videoContainer.getChildren().clear();
AnchorPane.setTopAnchor(webView, 0.0);
AnchorPane.setBottomAnchor(webView, 0.0);
AnchorPane.setLeftAnchor(webView, 0.0);
AnchorPane.setRightAnchor(webView, 0.0);
videoContainer.getChildren().add(webView);
// Discord RPC : visionnage film ou série
if ("tv".equalsIgnoreCase(program.getTypes())) {
discordManager.startWatching(program.getTitle(), selectedSeason, selectedEpisode);
} else {
discordManager.startWatching(program.getTitle(), null, null);
}
System.out.println("[INFO] Lecteur WebView prêt.");
} catch (Exception e) {
e.printStackTrace();
System.err.println("[ERROR] Impossible de charger la vidéo : " + e.getMessage());
}
}
public void onBack(ActionEvent actionEvent) {}
public void login(ActionEvent actionEvent) { SessionManager.login(this::updateAuthButtons); }
public void logout(ActionEvent actionEvent) { SessionManager.logout(); updateAuthButtons(); }
public void onHome(ActionEvent actionEvent) {}
@FXML
private void onTrailer(ActionEvent event) {
try {
if (program == null || program.getTrailerLink() == null) return;
String trailerUrl = program.getTrailerLink();
// stop any existing playback
stopPlayback();
WebView webView = new WebView();
WebEngine engine = webView.getEngine();
// Try to extract YouTube ID and embed clean player
String ytId = extractYoutubeId(trailerUrl);
if (ytId != null) {
String html = "<html><body style='margin:0;background:black;'>" +
"<iframe width='100%' height='100%' src='https://www.youtube.com/embed/" + ytId + "?autoplay=1&rel=0' " +
"frameborder='0' allow='autoplay; encrypted-media' allowfullscreen></iframe>" +
"</body></html>";
engine.loadContent(html);
} else {
// fallback to loading the URL directly
engine.load(trailerUrl);
}
videoContainer.getChildren().clear();
AnchorPane.setTopAnchor(webView, 0.0);
AnchorPane.setBottomAnchor(webView, 0.0);
AnchorPane.setLeftAnchor(webView, 0.0);
AnchorPane.setRightAnchor(webView, 0.0);
videoContainer.getChildren().add(webView);
} catch (Exception e) {
System.err.println("[ERROR] Impossible d'ouvrir la bande-annonce: " + e.getMessage());
// fallback : tenter d'ouvrir dans le navigateur système
try {
java.awt.Desktop.getDesktop().browse(new java.net.URI(program.getTrailerLink()));
} catch (Exception ex) {
System.err.println("[ERROR] fallback open trailer failed: " + ex.getMessage());
}
}
}
/**
* Extract a YouTube video ID from common URL formats.
* Returns null if no ID could be found.
*/
private String extractYoutubeId(String url) {
if (url == null) return null;
try {
// common patterns: v=ID, youtu.be/ID, embed/ID
Pattern p = Pattern.compile("(?:v=|youtu\\.be/|embed/|v/)([A-Za-z0-9_-]{11})");
Matcher m = p.matcher(url);
if (m.find()) {
return m.group(1);
}
} catch (Exception e) {
// ignore
}
return null;
}
@FXML
private void onLogoHomeClick(javafx.scene.input.MouseEvent event) {
// Stop any playback before leaving the page
stopPlayback();
// Retour à la home
dev.mecdu.javafx.SceneLoader.switchTo("/dev/mecdu/javafx/home.fxml", logoProg, null);
}
/**
* Stoppe toute lecture présente dans le conteneur vidéo (WebView HTML5 video or MediaView).
* Appelé avant de quitter la page pour éviter que le son continue en arrière-plan.
*/
private void stopPlayback() {
try {
if (videoContainer == null) return;
// iterate on a copy to avoid concurrent modification
List<Node> children = new ArrayList<>(videoContainer.getChildren());
for (Node n : children) {
if (n instanceof WebView) {
WebView w = (WebView) n;
try {
// Try to pause HTML5 video with JS if present
w.getEngine().executeScript("var v = document.getElementById('player'); if(v){v.pause(); v.removeAttribute('src');}" );
} catch (Exception ex) {
// ignore JS errors
}
}
if (n instanceof MediaView) {
MediaView mv = (MediaView) n;
MediaPlayer mp = mv.getMediaPlayer();
if (mp != null) {
try { mp.stop(); } catch (Exception e) { /* ignore */ }
}
}
}
videoContainer.getChildren().clear();
} catch (Exception e) {
System.err.println("[ERROR] stopPlayback: " + e.getMessage());
}
}
private void updateAuthButtons() {
boolean loggedIn = SessionManager.isLoggedIn();
btnConnexion.setVisible(!loggedIn);
btnConnexion.setManaged(!loggedIn);
btnLogout.setVisible(loggedIn);
btnLogout.setManaged(loggedIn);
// Re-évaluer l'affichage des contrôles (sélecteurs / boutons) lorsque l'état de session change
updateControlsVisibility();
}
}

View file

@ -9,6 +9,12 @@ module dev.mecdu.javafx {
requires java.net.http; requires java.net.http;
requires java.desktop; requires java.desktop;
requires javafx.graphics; requires javafx.graphics;
requires spark.core;
requires java.prefs;
requires javafx.media;
requires javafx.base;
requires discord.rpc;
requires javafx.web;
opens dev.mecdu.javafx to javafx.fxml; opens dev.mecdu.javafx to javafx.fxml;
exports dev.mecdu.javafx; exports dev.mecdu.javafx;

View file

@ -1,14 +1,53 @@
.logo {
-fx-cursor: hand;
-fx-effect: dropshadow(gaussian, #e50914, 10, 0.2, 0, 0);
-fx-background-radius: 10;
-fx-padding: 5px;
-fx-background-color: transparent;
-fx-transition: scale 0.2s;
}
.logo:hover {
-fx-scale-x: 1.08;
-fx-scale-y: 1.08;
-fx-effect: dropshadow(gaussian, #e50914, 20, 0.4, 0, 0);
}
.label { .label {
-fx-max-height: 40px; /* environ 2 lignes */ -fx-max-height: 20px;
-fx-text-overrun: ellipsis; -fx-text-overrun: ellipsis;
} }
.card { .card {
-fx-alignment: center; -fx-alignment: center;
-fx-cursor: hand; -fx-cursor: hand;
} }
.card:hover { .card:hover {
-fx-scale-x: 1.05; -fx-scale-x: 1.05;
-fx-scale-y: 1.05; -fx-scale-y: 1.05;
-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.6), 10, 0, 0, 0); -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.6), 10, 0, 0, 0);
}
.banner-description {
-fx-text-fill: #e0e0e0;
-fx-font-size: 16px;
-fx-padding: 10px;
-fx-background-color: rgba(50, 50, 50, 0.5);
-fx-background-radius: 8;
-fx-wrap-text: true;
}
.banner-description .text {
-fx-text-alignment: left;
-fx-line-spacing: 2px;
-fx-max-height: 20em;
-fx-ellipsis-string: "...";
-fx-text-overrun: ellipsis-word;
}
.banner-description-prog {
-fx-text-fill: #e0e0e0;
-fx-font-size: 16px;
-fx-padding: 10px;
-fx-background-color: rgba(50, 50, 50, 0.5);
-fx-background-radius: 8;
-fx-wrap-text: true;
} }

View file

@ -2,79 +2,170 @@
<?import javafx.geometry.Insets?> <?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?> <?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?> <?import javafx.scene.layout.*?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.shape.Rectangle?>
<?import javafx.scene.paint.LinearGradient?>
<?import javafx.scene.paint.Stop?>
<BorderPane xmlns="http://javafx.com/javafx" <BorderPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml" xmlns:fx="http://javafx.com/fxml"
fx:controller="dev.mecdu.javafx.ui.HomeController" fx:controller="dev.mecdu.javafx.ui.HomeController"
style="-fx-background-color: #141414;" style="-fx-background-color: #141414;"
prefWidth="1280" prefHeight="900"> prefWidth="1280" prefHeight="900"
stylesheets="@home.css">
<!-- HEADER FIXE -->
<top> <top>
<BorderPane style="-fx-background-color: #000000; -fx-padding: 15 30 15 30;"> <BorderPane style="-fx-background-color: #000000; -fx-padding: 15 30 15 30;">
<left> <left>
<Label text="STREAMIFY" <ImageView fx:id="logoHome" fitHeight="38" fitWidth="180" pickOnBounds="true"
style="-fx-text-fill: #E50914; -fx-font-size: 28px; -fx-font-weight: bold;"/> style="-fx-cursor: hand;" preserveRatio="true"
onMouseClicked="#onLogoHomeClick"/>
</left> </left>
<center> <center>
<HBox spacing="25" alignment="CENTER"> <HBox spacing="25" alignment="CENTER">
<Label text="Accueil" style="-fx-text-fill: white; -fx-font-size: 16px;"/>
<Label text="Séries" style="-fx-text-fill: white; -fx-font-size: 16px;"/> <TextField fx:id="searchField" promptText="Rechercher..." style="-fx-pref-width: 200px;" onAction="#onSearch"/>
<Label text="Films" style="-fx-text-fill: white; -fx-font-size: 16px;"/>
<Label text="Nouveautés" style="-fx-text-fill: white; -fx-font-size: 16px;"/>
<Label text="Ma liste" style="-fx-text-fill: white; -fx-font-size: 16px;"/>
</HBox> </HBox>
</center> </center>
<right>
<StackPane>
<Button fx:id="btnConnexion" text="Connexion"
style="-fx-background-color: #E50914; -fx-text-fill: white; -fx-font-size: 18px;"
onAction="#login"/>
<Button fx:id="btnLogout" text="Déconnexion"
style="-fx-background-color: #E50914; -fx-text-fill: white; -fx-font-size: 18px;"
onAction="#logout"/>
</StackPane>
</right>
</BorderPane> </BorderPane>
</top> </top>
<!-- CONTENU SCROLLABLE -->
<center> <center>
<ScrollPane fitToWidth="true" hbarPolicy="NEVER" vbarPolicy="AS_NEEDED" <ScrollPane fitToWidth="true" hbarPolicy="NEVER" vbarPolicy="AS_NEEDED"
style="-fx-background: #ffffff; -fx-border-color: transparent;"> style="-fx-background: #141414; -fx-border-color: transparent;">
<VBox spacing="40" alignment="TOP_CENTER"> <VBox spacing="30" style="-fx-padding: 20;">
<padding> <StackPane fx:id="randomContainer" prefHeight="450" style="-fx-background-color: black;">
<Insets top="20" right="40" bottom="20" left="40"/>
</padding> <ImageView fx:id="randomImage"
fitWidth="1280"
fitHeight="450"
preserveRatio="true"
smooth="true"
style="-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.3), 20, 0.3, 0, 0);"/>
<Rectangle width="980" height="450">
<fill>
<LinearGradient startX="0" startY="0" endX="0" endY="1">
<stops>
<Stop offset="0" color="#00000060"/>
<Stop offset="0.5" color="#00000080"/>
<Stop offset="1" color="#000000"/>
</stops>
</LinearGradient>
</fill>
</Rectangle>
<VBox fx:id="randomInfoBox"
alignment="CENTER_LEFT"
spacing="15"
maxWidth="800"
StackPane.alignment="CENTER_LEFT"
style="-fx-padding: 0 0 0 60;">
<Label fx:id="randomTitle"
text="Titre du film"
style="-fx-text-fill: white; -fx-font-size: 40px; -fx-font-weight: bold;"
wrapText="true"
maxWidth="700"/>
<HBox spacing="15">
<Label fx:id="randomGenre"
text="Action"
style="-fx-text-fill: #CCCCCC; -fx-font-size: 16px;"/>
<Label text="•" style="-fx-text-fill: #CCCCCC; -fx-font-size: 16px;"/>
<Label fx:id="randomYear"
text="2024"
style="-fx-text-fill: #CCCCCC; -fx-font-size: 16px;"/>
<Label text="•" style="-fx-text-fill: #CCCCCC; -fx-font-size: 16px;"/>
<Label fx:id="randomDuration"
text="1h45"
style="-fx-text-fill: #CCCCCC; -fx-font-size: 16px;"/>
</HBox>
<Label fx:id="randomDescription"
text="Description du film ici. Une aventure incroyable pleine démotions et daction."
wrapText="true"
maxWidth="700"
styleClass="banner-description"/>
<HBox spacing="15" style="-fx-padding: 10 0 0 0;">
<Button fx:id="btnVoirPlus" text="▶ Voir plus"
style="-fx-background-color: #E50914;
-fx-text-fill: white;
-fx-font-size: 16px;
-fx-font-weight: bold;
-fx-padding: 10 20;
-fx-background-radius: 8;"
onAction="#onVoirPlus"/>
</HBox>
</VBox>
<Button fx:id="btnLeft" text=""
StackPane.alignment="CENTER_LEFT"
translateX="15"
style="-fx-background-color: transparent;
-fx-text-fill: white;
-fx-font-size: 36px;
-fx-font-weight: bold;
-fx-opacity: 0.7;"
onMouseEntered="#onLeftHover"
onMouseExited="#onLeftExit"/>
<Button fx:id="btnRight" text=""
StackPane.alignment="CENTER_RIGHT"
translateX="-15"
style="-fx-background-color: transparent;
-fx-text-fill: white;
-fx-font-size: 36px;
-fx-font-weight: bold;
-fx-opacity: 0.7;"
onMouseEntered="#onRightHover"
onMouseExited="#onRightExit"/>
</StackPane>
<!-- Carrousel Prochainement -->
<VBox spacing="10" fx:id="proSection"> <VBox spacing="10" fx:id="proSection">
<Label text="🔜 Prochainement" <Label text="🔜 Prochainement"
style="-fx-text-fill: black; -fx-font-size: 22px; -fx-font-weight: bold;"/> style="-fx-text-fill: white; -fx-font-size: 22px; -fx-font-weight: bold;"/>
<ScrollPane fx:id="proCarousel" hbarPolicy="ALWAYS" vbarPolicy="NEVER" <ScrollPane fx:id="proCarousel" hbarPolicy="AS_NEEDED" vbarPolicy="NEVER"
prefHeight="270" fitToHeight="false" fitToWidth="true" prefHeight="270" fitToHeight="false" fitToWidth="true"
style="-fx-background-color: transparent;"> style="-fx-background-color: transparent;">
<HBox fx:id="proContainer" spacing="10" style="-fx-padding: 10;"/> <HBox fx:id="proContainer" spacing="10" style="-fx-padding: 10;"/>
</ScrollPane> </ScrollPane>
</VBox> </VBox>
<!-- Carrousel Séries -->
<VBox spacing="10" fx:id="tvSection"> <VBox spacing="10" fx:id="tvSection">
<Label text="📺 Séries récentes" <Label text="📺 Séries récentes"
style="-fx-text-fill: black; -fx-font-size: 22px; -fx-font-weight: bold;"/> style="-fx-text-fill: white; -fx-font-size: 22px; -fx-font-weight: bold;"/>
<ScrollPane fx:id="tvCarousel" hbarPolicy="ALWAYS" vbarPolicy="NEVER" <ScrollPane fx:id="tvCarousel" hbarPolicy="AS_NEEDED" vbarPolicy="NEVER"
prefHeight="270" fitToHeight="false" fitToWidth="true" prefHeight="270" fitToHeight="false" fitToWidth="true"
style="-fx-background-color: transparent;"> style="-fx-background-color: transparent;">
<HBox fx:id="tvContainer" spacing="10" style="-fx-padding: 10;"/> <HBox fx:id="tvContainer" spacing="10" style="-fx-padding: 10;"/>
</ScrollPane> </ScrollPane>
</VBox> </VBox>
<!-- Carrousel Films -->
<VBox spacing="10" fx:id="moviesSection"> <VBox spacing="10" fx:id="moviesSection">
<Label text="🎥 Derniers films" <Label text="🎥 Derniers films"
style="-fx-text-fill: black; -fx-font-size: 22px; -fx-font-weight: bold;"/> style="-fx-text-fill: white; -fx-font-size: 22px; -fx-font-weight: bold;"/>
<ScrollPane fx:id="moviesCarousel" hbarPolicy="ALWAYS" vbarPolicy="NEVER" <ScrollPane fx:id="moviesCarousel" hbarPolicy="AS_NEEDED" vbarPolicy="NEVER"
prefHeight="270" fitToHeight="false" fitToWidth="true" prefHeight="270" fitToHeight="false" fitToWidth="true"
style="-fx-background-color: transparent;"> style="-fx-background-color: transparent;">
<HBox fx:id="moviesContainer" spacing="10" style="-fx-padding: 10;"/> <HBox fx:id="moviesContainer" spacing="10" style="-fx-padding: 10;"/>
</ScrollPane> </ScrollPane>
</VBox> </VBox>
</VBox> </VBox>
</ScrollPane> </ScrollPane>
</center> </center>
<!-- FOOTER -->
<bottom> <bottom>
<HBox alignment="CENTER" style="-fx-background-color: #000000; -fx-padding: 15;"> <HBox alignment="CENTER" style="-fx-background-color: #000000; -fx-padding: 15;">
<Label text="© 2025 Streamify — Inspiré par Netflix" <Label text="© 2025 Streamify — Inspiré par Netflix"

View file

@ -0,0 +1,131 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?>
<?import javafx.scene.control.ComboBox?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.paint.LinearGradient?>
<?import javafx.scene.paint.Stop?>
<?import javafx.scene.shape.Rectangle?>
<?import javafx.scene.media.MediaView?>
<BorderPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="dev.mecdu.javafx.ui.ProgramController"
style="-fx-background-color: #141414;"
prefWidth="1280" prefHeight="900"
stylesheets="@home.css">
<top>
<BorderPane style="-fx-background-color: #000000; -fx-padding: 15 30 15 30;">
<left>
<ImageView fx:id="logoProg" fitHeight="38" fitWidth="180" pickOnBounds="true"
style="-fx-cursor: hand;" preserveRatio="true"
onMouseClicked="#onLogoHomeClick"/>
</left>
<center>
<HBox spacing="25" alignment="CENTER">
<TextField promptText="Rechercher..." style="-fx-pref-width: 200px;"/>
</HBox>
</center>
<right>
<StackPane>
<Button fx:id="btnConnexion" text="Connexion"
style="-fx-background-color: #E50914; -fx-text-fill: white; -fx-font-size: 18px;"
onAction="#login"/>
<Button fx:id="btnLogout" text="Déconnexion"
style="-fx-background-color: #E50914; -fx-text-fill: white; -fx-font-size: 18px;"
onAction="#logout"/>
</StackPane>
</right>
</BorderPane>
</top>
<center>
<ScrollPane fitToWidth="true" hbarPolicy="NEVER" vbarPolicy="AS_NEEDED"
style="-fx-background: transparent; -fx-border-color: transparent;">
<VBox spacing="30" style="-fx-padding: 30;">
<StackPane prefHeight="450" style="-fx-background-color: black; -fx-background-radius: 12;">
<ImageView fx:id="imageView"
fitWidth="1280"
fitHeight="450"
preserveRatio="true"
smooth="true"/>
<Rectangle width="1280" height="450">
<fill>
<LinearGradient startX="0" startY="0" endX="0" endY="1">
<stops>
<Stop offset="0" color="#00000060"/>
<Stop offset="0.5" color="#00000080"/>
<Stop offset="1" color="#000000"/>
</stops>
</LinearGradient>
</fill>
</Rectangle>
<VBox alignment="BOTTOM_LEFT" spacing="15" maxWidth="900"
StackPane.alignment="BOTTOM_LEFT"
style="-fx-padding: 0 0 60 60;">
<Label fx:id="titleLabel"
text="Titre du film"
style="-fx-text-fill: white; -fx-font-size: 44px; -fx-font-weight: bold;"
wrapText="true" maxWidth="900"/>
<HBox spacing="15">
<Label fx:id="yearLabel" text="2024" style="-fx-text-fill: #CCCCCC; -fx-font-size: 16px;"/>
<Label text="•" style="-fx-text-fill: #CCCCCC; -fx-font-size: 16px;"/>
<Label fx:id="genreLabel" text="Action" style="-fx-text-fill: #CCCCCC; -fx-font-size: 16px;"/>
<Label text="•" style="-fx-text-fill: #CCCCCC; -fx-font-size: 16px;"/>
<Label fx:id="durationLabel" text="1h45" style="-fx-text-fill: #CCCCCC; -fx-font-size: 16px;"/>
</HBox>
</VBox>
</StackPane>
<VBox spacing="10" style="-fx-padding: 20; -fx-background-color: rgba(60,60,60,0.4); -fx-background-radius: 10;">
<Label text="Synopsis"
style="-fx-text-fill: white; -fx-font-size: 22px; -fx-font-weight: bold;"/>
<Label fx:id="descriptionLabel"
text="Description du film ici."
wrapText="true"
style="-fx-text-fill: #E0E0E0; -fx-font-size: 16px;"
styleClass="banner-description-prog"/>
</VBox>
<VBox spacing="10">
<HBox spacing="10" alignment="CENTER_LEFT">
<Label text="Saison:" style="-fx-text-fill: white; -fx-font-size: 14px;"/>
<ComboBox fx:id="seasonCombo" prefWidth="160" onAction="#onSeasonSelected" />
<Label text="Épisode:" style="-fx-text-fill: white; -fx-font-size: 14px;"/>
<ComboBox fx:id="episodeCombo" prefWidth="120" />
</HBox>
<HBox spacing="15" alignment="CENTER_LEFT">
<Button fx:id="btnWatch" text="▶ Regarder"
onAction="#onWatch"
style="-fx-background-color: #E50914;
-fx-text-fill: white;
-fx-font-size: 18px;
-fx-font-weight: bold;
-fx-padding: 10 25;
-fx-background-radius: 10;"/>
<Button fx:id="btnTrailer" text="Bande-annonce"
onAction="#onTrailer"
style="-fx-background-color: transparent; -fx-border-color: #E50914; -fx-text-fill: white; -fx-font-size: 16px; -fx-padding: 8 20; -fx-background-radius: 10; -fx-border-radius: 10;"/>
</HBox>
</VBox>
<AnchorPane fx:id="videoContainer" prefWidth="800" prefHeight="450">
<MediaView fx:id="mediaView"/>
</AnchorPane>
</VBox>
</ScrollPane>
</center>
<bottom>
<HBox alignment="CENTER" style="-fx-background-color: #000000; -fx-padding: 15;">
<Label text="© 2025 Streamify — Inspiré par Netflix"
style="-fx-text-fill: #888; -fx-font-size: 12px;"/>
</HBox>
</bottom>
</BorderPane>