Fonctionne
This commit is contained in:
parent
aad4686d48
commit
0008be71bd
17 changed files with 1285 additions and 53 deletions
1
.idea/gradle.xml
generated
1
.idea/gradle.xml
generated
|
|
@ -5,6 +5,7 @@
|
|||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="ms-17" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
|
|
|
|||
|
|
@ -9,9 +9,6 @@ plugins {
|
|||
group = "dev.mecdu"
|
||||
version = "1.0-SNAPSHOT"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
val junitVersion = "5.12.1"
|
||||
|
||||
|
|
@ -32,7 +29,7 @@ application {
|
|||
|
||||
javafx {
|
||||
version = "17.0.14"
|
||||
modules = listOf("javafx.controls", "javafx.fxml")
|
||||
modules = listOf("javafx.controls", "javafx.fxml", "javafx.media", "javafx.web")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
@ -44,6 +41,10 @@ dependencies {
|
|||
compileOnly("org.projectlombok:lombok:+")
|
||||
annotationProcessor("org.projectlombok:lombok:1.+")
|
||||
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> {
|
||||
|
|
|
|||
|
|
@ -1 +1,9 @@
|
|||
rootProject.name = "javafx"
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url = uri("https://jitpack.io") }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
108
src/main/java/dev/mecdu/javafx/DiscordAuth.java
Normal file
108
src/main/java/dev/mecdu/javafx/DiscordAuth.java
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
123
src/main/java/dev/mecdu/javafx/DiscordPresenceManager.java
Normal file
123
src/main/java/dev/mecdu/javafx/DiscordPresenceManager.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -16,8 +16,11 @@ public class HomeApplication extends Application {
|
|||
FXMLLoader fxmlLoader = new FXMLLoader(HomeApplication.class.getResource("home.fxml"));
|
||||
Parent root = fxmlLoader.load();
|
||||
|
||||
// Récupère le contrôleur si tu veux lui passer des infos
|
||||
// HomeController controller = loader.getController();
|
||||
// Init Discord RPC
|
||||
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);
|
||||
stage.setTitle("Streamify 🎬");
|
||||
|
|
|
|||
31
src/main/java/dev/mecdu/javafx/SceneLoader.java
Normal file
31
src/main/java/dev/mecdu/javafx/SceneLoader.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
61
src/main/java/dev/mecdu/javafx/SessionManager.java
Normal file
61
src/main/java/dev/mecdu/javafx/SessionManager.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
5
src/main/java/dev/mecdu/javafx/TransferableData.java
Normal file
5
src/main/java/dev/mecdu/javafx/TransferableData.java
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package dev.mecdu.javafx;
|
||||
|
||||
public interface TransferableData {
|
||||
void onDataReceived(Object data);
|
||||
}
|
||||
|
|
@ -4,6 +4,10 @@ import com.google.gson.annotations.SerializedName;
|
|||
import lombok.Data;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.Map;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Data
|
||||
public class Program {
|
||||
|
|
@ -69,8 +73,22 @@ public class Program {
|
|||
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
|
||||
public String toString() {
|
||||
return title; // utile si tu ne mets pas de cellFactory
|
||||
return title;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
package dev.mecdu.javafx.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class Programs {
|
||||
private Program[] programs;
|
||||
}
|
||||
|
|
@ -1,14 +1,19 @@
|
|||
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.model.Program;
|
||||
import javafx.application.Platform;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import java.net.URL;
|
||||
|
|
@ -20,11 +25,31 @@ import java.util.stream.Collectors;
|
|||
import static java.util.stream.Collectors.toList;
|
||||
|
||||
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 tvSection;
|
||||
public VBox moviesSection;
|
||||
@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;
|
||||
|
||||
@FXML
|
||||
|
|
@ -45,14 +70,107 @@ public class HomeController implements Initializable {
|
|||
@FXML
|
||||
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();
|
||||
|
||||
@Override
|
||||
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();
|
||||
}
|
||||
|
||||
@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() {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
|
|
@ -68,7 +186,7 @@ public class HomeController implements Initializable {
|
|||
Collections.shuffle(randomShows);
|
||||
|
||||
randomShows = randomShows.stream()
|
||||
.limit(10)
|
||||
.limit(11)
|
||||
.toList();
|
||||
|
||||
// filtrer seulement les prochainements et limiter à 10
|
||||
|
|
@ -79,7 +197,7 @@ public class HomeController implements Initializable {
|
|||
Collections.reverse(proShows);
|
||||
|
||||
proShows = proShows.stream()
|
||||
.limit(10)
|
||||
.limit(11)
|
||||
.toList();
|
||||
|
||||
// filtrer seulement les séries TV et limiter à 10
|
||||
|
|
@ -91,7 +209,7 @@ public class HomeController implements Initializable {
|
|||
Collections.reverse(tvShows);
|
||||
|
||||
tvShows = tvShows.stream()
|
||||
.limit(10)
|
||||
.limit(11)
|
||||
.toList();
|
||||
|
||||
// filtrer seulement les films MOVIES et limiter à 10
|
||||
|
|
@ -103,7 +221,7 @@ public class HomeController implements Initializable {
|
|||
Collections.reverse(moviesShows);
|
||||
|
||||
moviesShows = moviesShows.stream()
|
||||
.limit(10)
|
||||
.limit(11)
|
||||
.toList();
|
||||
|
||||
List<Program> finalRandomShows = randomShows;
|
||||
|
|
@ -112,6 +230,7 @@ public class HomeController implements Initializable {
|
|||
List<Program> finalMoviesShows = moviesShows;
|
||||
|
||||
Platform.runLater(() -> {
|
||||
randomShows(finalRandomShows);
|
||||
|
||||
if (finalProShows.isEmpty()) {
|
||||
proSection.setVisible(false);
|
||||
|
|
@ -133,6 +252,66 @@ public class HomeController implements Initializable {
|
|||
}).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) {
|
||||
container.getChildren().clear();
|
||||
for (Program p : finalShows) {
|
||||
|
|
@ -152,8 +331,44 @@ public class HomeController implements Initializable {
|
|||
|
||||
card.getChildren().addAll(imageView, title);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
399
src/main/java/dev/mecdu/javafx/ui/ProgramController.java
Normal file
399
src/main/java/dev/mecdu/javafx/ui/ProgramController.java
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,12 @@ module dev.mecdu.javafx {
|
|||
requires java.net.http;
|
||||
requires java.desktop;
|
||||
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;
|
||||
exports dev.mecdu.javafx;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,18 @@
|
|||
.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 {
|
||||
-fx-max-height: 40px; /* environ 2 lignes */
|
||||
-fx-max-height: 20px;
|
||||
-fx-text-overrun: ellipsis;
|
||||
}
|
||||
|
||||
|
|
@ -10,5 +23,31 @@
|
|||
.card:hover {
|
||||
-fx-scale-x: 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;
|
||||
}
|
||||
|
|
@ -2,79 +2,170 @@
|
|||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?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"
|
||||
xmlns:fx="http://javafx.com/fxml"
|
||||
fx:controller="dev.mecdu.javafx.ui.HomeController"
|
||||
style="-fx-background-color: #141414;"
|
||||
prefWidth="1280" prefHeight="900">
|
||||
prefWidth="1280" prefHeight="900"
|
||||
stylesheets="@home.css">
|
||||
|
||||
<!-- HEADER FIXE -->
|
||||
<top>
|
||||
<BorderPane style="-fx-background-color: #000000; -fx-padding: 15 30 15 30;">
|
||||
<left>
|
||||
<Label text="STREAMIFY"
|
||||
style="-fx-text-fill: #E50914; -fx-font-size: 28px; -fx-font-weight: bold;"/>
|
||||
<ImageView fx:id="logoHome" fitHeight="38" fitWidth="180" pickOnBounds="true"
|
||||
style="-fx-cursor: hand;" preserveRatio="true"
|
||||
onMouseClicked="#onLogoHomeClick"/>
|
||||
</left>
|
||||
<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;"/>
|
||||
<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;"/>
|
||||
|
||||
<TextField fx:id="searchField" promptText="Rechercher..." style="-fx-pref-width: 200px;" onAction="#onSearch"/>
|
||||
</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>
|
||||
|
||||
<!-- CONTENU SCROLLABLE -->
|
||||
<center>
|
||||
<ScrollPane fitToWidth="true" hbarPolicy="NEVER" vbarPolicy="AS_NEEDED"
|
||||
style="-fx-background: #ffffff; -fx-border-color: transparent;">
|
||||
<VBox spacing="40" alignment="TOP_CENTER">
|
||||
<padding>
|
||||
<Insets top="20" right="40" bottom="20" left="40"/>
|
||||
</padding>
|
||||
style="-fx-background: #141414; -fx-border-color: transparent;">
|
||||
<VBox spacing="30" style="-fx-padding: 20;">
|
||||
<StackPane fx:id="randomContainer" prefHeight="450" style="-fx-background-color: black;">
|
||||
|
||||
<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 d’action."
|
||||
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">
|
||||
<Label text="🔜 Prochainement"
|
||||
style="-fx-text-fill: black; -fx-font-size: 22px; -fx-font-weight: bold;"/>
|
||||
<ScrollPane fx:id="proCarousel" hbarPolicy="ALWAYS" vbarPolicy="NEVER"
|
||||
style="-fx-text-fill: white; -fx-font-size: 22px; -fx-font-weight: bold;"/>
|
||||
<ScrollPane fx:id="proCarousel" hbarPolicy="AS_NEEDED" vbarPolicy="NEVER"
|
||||
prefHeight="270" fitToHeight="false" fitToWidth="true"
|
||||
style="-fx-background-color: transparent;">
|
||||
<HBox fx:id="proContainer" spacing="10" style="-fx-padding: 10;"/>
|
||||
</ScrollPane>
|
||||
</VBox>
|
||||
|
||||
<!-- Carrousel Séries -->
|
||||
<VBox spacing="10" fx:id="tvSection">
|
||||
<Label text="📺 Séries récentes"
|
||||
style="-fx-text-fill: black; -fx-font-size: 22px; -fx-font-weight: bold;"/>
|
||||
<ScrollPane fx:id="tvCarousel" hbarPolicy="ALWAYS" vbarPolicy="NEVER"
|
||||
style="-fx-text-fill: white; -fx-font-size: 22px; -fx-font-weight: bold;"/>
|
||||
<ScrollPane fx:id="tvCarousel" hbarPolicy="AS_NEEDED" vbarPolicy="NEVER"
|
||||
prefHeight="270" fitToHeight="false" fitToWidth="true"
|
||||
style="-fx-background-color: transparent;">
|
||||
<HBox fx:id="tvContainer" spacing="10" style="-fx-padding: 10;"/>
|
||||
</ScrollPane>
|
||||
</VBox>
|
||||
|
||||
<!-- Carrousel Films -->
|
||||
<VBox spacing="10" fx:id="moviesSection">
|
||||
<Label text="🎥 Derniers films"
|
||||
style="-fx-text-fill: black; -fx-font-size: 22px; -fx-font-weight: bold;"/>
|
||||
<ScrollPane fx:id="moviesCarousel" hbarPolicy="ALWAYS" vbarPolicy="NEVER"
|
||||
style="-fx-text-fill: white; -fx-font-size: 22px; -fx-font-weight: bold;"/>
|
||||
<ScrollPane fx:id="moviesCarousel" hbarPolicy="AS_NEEDED" vbarPolicy="NEVER"
|
||||
prefHeight="270" fitToHeight="false" fitToWidth="true"
|
||||
style="-fx-background-color: transparent;">
|
||||
<HBox fx:id="moviesContainer" spacing="10" style="-fx-padding: 10;"/>
|
||||
</ScrollPane>
|
||||
</VBox>
|
||||
|
||||
</VBox>
|
||||
</ScrollPane>
|
||||
</center>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<bottom>
|
||||
<HBox alignment="CENTER" style="-fx-background-color: #000000; -fx-padding: 15;">
|
||||
<Label text="© 2025 Streamify — Inspiré par Netflix"
|
||||
|
|
|
|||
131
src/main/resources/dev/mecdu/javafx/program.fxml
Normal file
131
src/main/resources/dev/mecdu/javafx/program.fxml
Normal 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>
|
||||
Loading…
Add table
Reference in a new issue