diff --git a/.idea/misc.xml b/.idea/misc.xml index 001e756..11d3355 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -8,7 +8,5 @@ - - - + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index e45147d..df8284a 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ UTF-8 -5.10.2 + 5.10.2 @@ -24,8 +24,13 @@ javafx-fxml 21 + + org.openjfx + javafx-media + 21 + - + org.junit.jupiter junit-jupiter-api ${junit.version} diff --git a/src/main/java/sbu/javafx/Launcher.java b/src/main/java/sbu/javafx/Launcher.java index c288d10..852cc6d 100644 --- a/src/main/java/sbu/javafx/Launcher.java +++ b/src/main/java/sbu/javafx/Launcher.java @@ -2,7 +2,11 @@ package sbu.javafx; import javafx.application.Application; -public class Launcher{ - public static void main(String[] args) { - // TODO: Launch the JavaFX application by calling Application.launch(Main.class) } +public class Launcher +{ + public static void main(String[] args) + { + // TODO: Launch the JavaFX application by calling Application.launch(Main.class) + Application.launch(MusicApp.class, args); + } } diff --git a/src/main/java/sbu/javafx/MusicApp.java b/src/main/java/sbu/javafx/MusicApp.java index a50badc..022a1c2 100644 --- a/src/main/java/sbu/javafx/MusicApp.java +++ b/src/main/java/sbu/javafx/MusicApp.java @@ -3,21 +3,43 @@ package sbu.javafx; import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.scene.Scene; +import javafx.scene.image.Image; import javafx.stage.Stage; import java.io.IOException; +import java.util.Objects; -public class MusicApp extends Application { +public class MusicApp extends Application +{ @Override public void start(Stage stage) throws IOException { - FXMLLoader fxmlLoader = new FXMLLoader(MusicApp.class.getResource("App-view.fxml")); - Scene scene = new Scene(fxmlLoader.load()); + + FXMLLoader fxmlLoader = new FXMLLoader( + Objects.requireNonNull(MusicApp.class.getResource("App-view.fxml"), + "Cannot find App-view.fxml in resources (src/main/resources/sbu/javafx/)") + ); + + Scene scene = new Scene(fxmlLoader.load(), 520, 300); stage.setTitle("Music Player"); stage.setScene(scene); - //TODO: add icon to the stage + + try + { + Image icon = new Image( + Objects.requireNonNull(MusicApp.class.getResourceAsStream("icon.png"), + "Cannot find icon.png in resources (src/main/resources/sbu/javafx/)") + ); + stage.getIcons().add(icon); + } + catch (Exception ignored) {} + stage.show(); } -} + public static void main(String[] args) + { + launch(args); + } +} \ No newline at end of file diff --git a/src/main/java/sbu/javafx/MusicController.java b/src/main/java/sbu/javafx/MusicController.java index 5da27a9..67205d5 100644 --- a/src/main/java/sbu/javafx/MusicController.java +++ b/src/main/java/sbu/javafx/MusicController.java @@ -1,31 +1,381 @@ package sbu.javafx; -import javafx.fxml.FXML; +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.control.Button; import javafx.scene.control.Label; -import javafx.scene.input.MouseEvent; -import javafx.scene.layout.VBox; +import javafx.scene.control.Slider; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.*; +import javafx.scene.media.Media; +import javafx.scene.media.MediaPlayer; +import javafx.stage.Stage; +import javafx.util.Duration; +import java.io.IOException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; +import javafx.scene.Parent; -public class MusicController { +public class MusicController +{ - // TODO: Define your UI components using the @FXML annotation. + @FXML private VBox songContainer; + @FXML private Label nowPlayingLabel; + @FXML private Label currentTrackTitle; + @FXML private Label timeLabel; + @FXML private Slider progressSlider; + @FXML private Slider volumeSlider; + @FXML private Button playPauseButton; + @FXML private Button repeatButton; + @FXML private Button shuffleButton; + @FXML private Button likeButton; + @FXML private StackPane visualizerContainer; - //TODO: Implement the logic to play a song. This method should update the UI to show which song is currently playing. + private MediaPlayer mediaPlayer; + private int currentSongIndex = 0; + private boolean isRepeat = false; + private boolean isShuffle = false; + private boolean isLiked = false; + private final Random random = new Random(); + private Timeline visualizerAnimation; + + private final String[] songFiles = { + "Maison.mp3", + "KUSURA_BAKMA.mp3", + "Hiting_Motion.mp3", + "Lost_in_the_haze.mp3", + "North.mp3", + "Longing.mp3", + "After_Math.mp3", + "On_The_Roads.mp3", + "Bumpup.mp3", + "SEVMEYİ_DENEMEDİN.mp3" + + }; + + private final Map songDisplayNames = new HashMap<>(); + private static final ObservableList playlist = FXCollections.observableArrayList(); + private static final ObservableList favorites = FXCollections.observableArrayList(); @FXML - public void handlePlayAction() { - //Update labels, change button icons, etc. + public void initialize() + { + for (int i = 0; i < songFiles.length; i++) + { + createSongCard(i); + } + setupVolumeControl(); + createVisualizer(); } - //TODO: Logic to add a specific song to the user's playlist. + private void createVisualizer() + { + HBox bars = new HBox(3); + bars.setAlignment(Pos.BOTTOM_CENTER); + for (int i = 0; i < 4; i++) + { + Region bar = new Region(); + bar.setPrefWidth(4); + bar.setPrefHeight(5); + bar.setStyle("-fx-background-color: #1DB954; -fx-background-radius: 2;"); + bars.getChildren().add(bar); + } + visualizerContainer.getChildren().add(bars); - public void addToPlaylist(String songName) { - //Add the song to a list or update a ListView. + visualizerAnimation = new Timeline(); + for (Node node : bars.getChildren()) + { + double randomHeight = 10 + Math.random() * 15; + KeyFrame kf = new KeyFrame(Duration.millis(150 + Math.random() * 100), + new KeyValue(((Region)node).prefHeightProperty(), randomHeight) + ); + visualizerAnimation.getKeyFrames().add(kf); + } + visualizerAnimation.setCycleCount(Timeline.INDEFINITE); + visualizerAnimation.setAutoReverse(true); } - // TODO:This method should open a new window or change the current scene to display the "Playlist" page. + private void updateLikeButton(String displayName) + { + if (displayName == null || likeButton == null) + { + if (likeButton != null) + { + likeButton.setText("♡"); + likeButton.getStyleClass().remove("btn-heart-liked"); + } + return; + } - public void openPlaylistWindow() { - //Create a new stage and show the playlist UI. + if (favorites.contains(displayName)) + { + likeButton.setText("♥"); + if (!likeButton.getStyleClass().contains("btn-heart-liked")) + { + likeButton.getStyleClass().add("btn-heart-liked"); + } + } else + { + likeButton.setText("♡"); + likeButton.getStyleClass().remove("btn-heart-liked"); + } } -} + + @FXML + private void toggleLike() + { + String currentSongName = nowPlayingLabel.getText(); + if (currentSongName.equals("None") || currentSongName.equals("Not Playing")) return; + + isLiked = !isLiked; + if (isLiked) + { + likeButton.setText("♥"); + likeButton.setStyle("-fx-text-fill: #ff4d4d; -fx-font-size: 16px;"); + if (!favorites.contains(currentSongName)) favorites.add(currentSongName); + } else + { + likeButton.setText("♡"); + likeButton.setStyle("-fx-text-fill: inherit; -fx-font-size: 16px;"); + favorites.remove(currentSongName); + } + } + + private void createSongCard(int index) + { + String fileName = songFiles[index]; + String displayName = cleanFileName(fileName); + + HBox card = new HBox(14); + card.setAlignment(Pos.CENTER_LEFT); + card.getStyleClass().add("song-card"); + + + ImageView coverView = createCoverImageView(fileName); + + VBox infoBox = new VBox(4); + Label titleLabel = new Label(displayName); + titleLabel.getStyleClass().add("song-title"); + + + Label subtitleLabel = new Label("00:00"); + subtitleLabel.getStyleClass().add("song-subtitle"); + + + subtitleLabel.getStyleClass().add("song-meta"); + infoBox.getChildren().addAll(titleLabel, subtitleLabel); + + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + + Button cardLikeBtn = new Button(favorites.contains(displayName) ? "♥" : "♡"); + cardLikeBtn.getStyleClass().add("btn-ghost"); + cardLikeBtn.setStyle("-fx-font-size: 16px; -fx-text-fill: #ff4d6d;"); + cardLikeBtn.setOnAction(event -> + { + if (favorites.contains(displayName)) + { + favorites.remove(displayName); + cardLikeBtn.setText("♡"); + } else { + favorites.add(displayName); + cardLikeBtn.setText("♥"); + } + updateLikeButton(displayName); + }); + + Button addButton = new Button("+"); + addButton.getStyleClass().add("btn-ghost"); + addButton.setStyle("-fx-font-size: 18px;"); + addButton.setOnAction(event -> + { + if (!playlist.contains(displayName)) + { + playlist.add(displayName); + } + }); + + Button playButton = new Button("▶"); + playButton.getStyleClass().add("btn-green-small"); + playButton.setOnAction(event -> + { + currentSongIndex = index; + playMedia(fileName); + }); + + cardLikeBtn.setOnMouseClicked(e -> e.consume()); + addButton.setOnMouseClicked(e -> e.consume()); + playButton.setOnMouseClicked(e -> e.consume()); + + card.getChildren().addAll(coverView, infoBox, spacer, cardLikeBtn, addButton, playButton); + songContainer.getChildren().add(card); + } + + private void playMedia(String fileName) + { + if (mediaPlayer != null) + { + mediaPlayer.stop(); mediaPlayer.dispose(); + } + + URL resource = getClass().getResource("/music/" + fileName); + if (resource == null) return; + + Media media = new Media(resource.toExternalForm()); + mediaPlayer = new MediaPlayer(media); + mediaPlayer.volumeProperty().bind(volumeSlider.valueProperty()); + + mediaPlayer.setOnReady(() -> { + String name = cleanFileName(fileName); + nowPlayingLabel.setText(name); + currentTrackTitle.setText(name); + playPauseButton.setText("Pause"); + visualizerAnimation.play(); + + isLiked = favorites.contains(name); + likeButton.setText(isLiked ? "♥" : "♡"); + likeButton.setStyle(isLiked ? "-fx-text-fill: #ff4d4d; -fx-font-size: 16px;" : ""); + }); + + mediaPlayer.currentTimeProperty().addListener((obs, old, cur) -> + { + timeLabel.setText(formatDuration(cur) + " / " + formatDuration(media.getDuration())); + if (!progressSlider.isValueChanging()) + { + progressSlider.setValue((cur.toSeconds() / media.getDuration().toSeconds()) * 100); + } + }); + + mediaPlayer.setOnEndOfMedia(this::nextTrack); + mediaPlayer.play(); + } + + private ImageView createCoverImageView(String mp3FileName) + { + String baseName = mp3FileName.substring(0, mp3FileName.lastIndexOf(".")); + String[] extensions = {".jpg", ".jpeg", ".png", ".webp"}; + for (String ext : extensions) + { + URL url = getClass().getResource("/music/" + baseName + ext); + if (url != null) + { + ImageView iv = new ImageView(new Image(url.toExternalForm())); + iv.setFitWidth(50); iv.setFitHeight(50); iv.setPreserveRatio(true); + return iv; + } + } + return new ImageView(); + } + + private void loadMetadata(String fileName, Label metaLabel) + { + URL resource = getClass().getResource("/music/" + fileName); + if (resource == null) return; + + Media media = new Media(resource.toExternalForm()); + MediaPlayer tempPlayer = new MediaPlayer(media); + + tempPlayer.setOnReady(() -> + { + metaLabel.setText(formatDuration(media.getDuration())); + tempPlayer.dispose(); + }); + } + + @FXML private void nextTrack() + { + if (isShuffle) currentSongIndex = random.nextInt(songFiles.length); + else currentSongIndex = (currentSongIndex + 1) % songFiles.length; + playMedia(songFiles[currentSongIndex]); + } + + @FXML private void previousTrack() + { + currentSongIndex = (currentSongIndex - 1 + songFiles.length) % songFiles.length; + playMedia(songFiles[currentSongIndex]); + } + + @FXML private void togglePlayPause() + { + if (mediaPlayer == null) return; + if (mediaPlayer.getStatus() == MediaPlayer.Status.PLAYING) + { + mediaPlayer.pause(); + playPauseButton.setText("Play"); + visualizerAnimation.pause(); + } else + { + mediaPlayer.play(); + playPauseButton.setText("Pause"); + visualizerAnimation.play(); + } + } + + @FXML private void toggleRepeat() + { + isRepeat = !isRepeat; repeatButton.setStyle(isRepeat ? "-fx-text-fill: #1DB954;" : ""); + } + + @FXML private void toggleShuffle() + { + isShuffle = !isShuffle; shuffleButton.setStyle(isShuffle ? "-fx-text-fill: #1DB954;" : ""); + } + + private void setupVolumeControl() + { + volumeSlider.setValue(0.5); + } + + private String cleanFileName(String f) + { + return f.replace(".mp3", "").replace("_", " ").replace("-", " "); + } + + private String formatDuration(Duration d) + { + int s = (int) Math.floor(d.toSeconds()); return String.format("%02d:%02d", s / 60, s % 60); + } + + + @FXML + private void goToPlaylistScene(ActionEvent event) throws IOException + { + FXMLLoader loader = new FXMLLoader(getClass().getResource("Playlist-view.fxml")); + Parent root = loader.load(); + + PlaylistController controller = loader.getController(); + controller.setPlaylist(playlist); + controller.setPreviousScene(((Node) event.getSource()).getScene()); + + Stage stage = (Stage) ((Node) event.getSource()).getScene().getWindow(); + stage.setTitle("Playlist"); + stage.setScene(new Scene(root)); + } + + @FXML + private void goToFavoritesScene(ActionEvent event) throws IOException + { + FXMLLoader loader = new FXMLLoader(getClass().getResource("Playlist-view.fxml")); + Parent root = loader.load(); + + PlaylistController controller = loader.getController(); + controller.setPlaylist(favorites); + controller.setPreviousScene(((Node) event.getSource()).getScene()); + + Stage stage = (Stage) ((Node) event.getSource()).getScene().getWindow(); + stage.setTitle("Favorites"); + stage.setScene(new Scene(root)); + } +} \ No newline at end of file diff --git a/src/main/java/sbu/javafx/PlaylistController.java b/src/main/java/sbu/javafx/PlaylistController.java new file mode 100644 index 0000000..cd39887 --- /dev/null +++ b/src/main/java/sbu/javafx/PlaylistController.java @@ -0,0 +1,70 @@ +package sbu.javafx; + +import javafx.collections.ObservableList; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.Scene; +import javafx.scene.control.ListView; +import javafx.stage.Stage; + +public class PlaylistController +{ + + @FXML + private ListView playlistListView; + + private ObservableList playlist; + private Scene previousScene; + + /** + * Called from MusicController after loading Playlist-view.fxml + */ + public void setPlaylist(ObservableList playlist) + { + this.playlist = playlist; + + if (playlistListView != null) + { + playlistListView.setItems(playlist); + } + } + + /** + * Save the previous scene so we can go back to it. + */ + public void setPreviousScene(Scene previousScene) + { + this.previousScene = previousScene; + } + + @FXML + private void removeSelected(ActionEvent e) + { + if (playlistListView == null) return; + + String selected = playlistListView.getSelectionModel().getSelectedItem(); + if (selected == null) return; + + if (playlist != null) + { + playlist.remove(selected); + } else + { + playlistListView.getItems().remove(selected); + } + } + + @FXML + private void goBack(ActionEvent e) + { + if (playlistListView == null) return; + + Stage stage = (Stage) playlistListView.getScene().getWindow(); + if (previousScene != null) + { + stage.setScene(previousScene); + stage.setTitle("Music Player"); + stage.show(); + } + } +} \ No newline at end of file diff --git a/src/main/resources/music/After_Math.jpg b/src/main/resources/music/After_Math.jpg new file mode 100644 index 0000000..05b5b87 Binary files /dev/null and b/src/main/resources/music/After_Math.jpg differ diff --git a/src/main/resources/music/After_Math.mp3 b/src/main/resources/music/After_Math.mp3 new file mode 100644 index 0000000..edd66f0 Binary files /dev/null and b/src/main/resources/music/After_Math.mp3 differ diff --git a/src/main/resources/music/Bumpup.jpg b/src/main/resources/music/Bumpup.jpg new file mode 100644 index 0000000..ca83c0c Binary files /dev/null and b/src/main/resources/music/Bumpup.jpg differ diff --git a/src/main/resources/music/Bumpup.mp3 b/src/main/resources/music/Bumpup.mp3 new file mode 100644 index 0000000..8a940aa Binary files /dev/null and b/src/main/resources/music/Bumpup.mp3 differ diff --git a/src/main/resources/music/Hiting_Motion.jpg b/src/main/resources/music/Hiting_Motion.jpg new file mode 100644 index 0000000..b96e0cf Binary files /dev/null and b/src/main/resources/music/Hiting_Motion.jpg differ diff --git a/src/main/resources/music/Hiting_Motion.mp3 b/src/main/resources/music/Hiting_Motion.mp3 new file mode 100644 index 0000000..43a4031 Binary files /dev/null and b/src/main/resources/music/Hiting_Motion.mp3 differ diff --git a/src/main/resources/music/KUSURA_BAKMA.jpg b/src/main/resources/music/KUSURA_BAKMA.jpg new file mode 100644 index 0000000..820fa38 Binary files /dev/null and b/src/main/resources/music/KUSURA_BAKMA.jpg differ diff --git a/src/main/resources/music/KUSURA_BAKMA.mp3 b/src/main/resources/music/KUSURA_BAKMA.mp3 new file mode 100644 index 0000000..d1308e3 Binary files /dev/null and b/src/main/resources/music/KUSURA_BAKMA.mp3 differ diff --git a/src/main/resources/music/Longing.jpg b/src/main/resources/music/Longing.jpg new file mode 100644 index 0000000..26afa7b Binary files /dev/null and b/src/main/resources/music/Longing.jpg differ diff --git a/src/main/resources/music/Longing.mp3 b/src/main/resources/music/Longing.mp3 new file mode 100644 index 0000000..917959a Binary files /dev/null and b/src/main/resources/music/Longing.mp3 differ diff --git a/src/main/resources/music/Lost_in_the_haze.jpg b/src/main/resources/music/Lost_in_the_haze.jpg new file mode 100644 index 0000000..a2c0aaa Binary files /dev/null and b/src/main/resources/music/Lost_in_the_haze.jpg differ diff --git a/src/main/resources/music/Lost_in_the_haze.mp3 b/src/main/resources/music/Lost_in_the_haze.mp3 new file mode 100644 index 0000000..ff3749d Binary files /dev/null and b/src/main/resources/music/Lost_in_the_haze.mp3 differ diff --git a/src/main/resources/music/Maison.jpg b/src/main/resources/music/Maison.jpg new file mode 100644 index 0000000..31e7eb7 Binary files /dev/null and b/src/main/resources/music/Maison.jpg differ diff --git a/src/main/resources/music/Maison.mp3 b/src/main/resources/music/Maison.mp3 new file mode 100644 index 0000000..0c84e51 Binary files /dev/null and b/src/main/resources/music/Maison.mp3 differ diff --git a/src/main/resources/music/North.jpg b/src/main/resources/music/North.jpg new file mode 100644 index 0000000..c3bac63 Binary files /dev/null and b/src/main/resources/music/North.jpg differ diff --git a/src/main/resources/music/North.mp3 b/src/main/resources/music/North.mp3 new file mode 100644 index 0000000..dd9d20e Binary files /dev/null and b/src/main/resources/music/North.mp3 differ diff --git a/src/main/resources/music/On_The_Roads.jpg b/src/main/resources/music/On_The_Roads.jpg new file mode 100644 index 0000000..b628b84 Binary files /dev/null and b/src/main/resources/music/On_The_Roads.jpg differ diff --git a/src/main/resources/music/On_The_Roads.mp3 b/src/main/resources/music/On_The_Roads.mp3 new file mode 100644 index 0000000..8dc92c6 Binary files /dev/null and b/src/main/resources/music/On_The_Roads.mp3 differ diff --git a/src/main/resources/music/SEVMEYİ_DENEMEDİN.jpg b/src/main/resources/music/SEVMEYİ_DENEMEDİN.jpg new file mode 100644 index 0000000..eb6b5c5 Binary files /dev/null and b/src/main/resources/music/SEVMEYİ_DENEMEDİN.jpg differ diff --git a/src/main/resources/music/SEVMEYİ_DENEMEDİN.mp3 b/src/main/resources/music/SEVMEYİ_DENEMEDİN.mp3 new file mode 100644 index 0000000..28f40b5 Binary files /dev/null and b/src/main/resources/music/SEVMEYİ_DENEMEDİN.mp3 differ diff --git a/src/main/resources/sbu/javafx/App-view.fxml b/src/main/resources/sbu/javafx/App-view.fxml index 306c8d3..32e2264 100644 --- a/src/main/resources/sbu/javafx/App-view.fxml +++ b/src/main/resources/sbu/javafx/App-view.fxml @@ -1,8 +1,65 @@ - - + + - + - + + + + + +
+ + + + + + + + + + + + + + + + + + + +
+ diff --git a/src/main/resources/sbu/javafx/Playlist-view.fxml b/src/main/resources/sbu/javafx/Playlist-view.fxml new file mode 100644 index 0000000..751f317 --- /dev/null +++ b/src/main/resources/sbu/javafx/Playlist-view.fxml @@ -0,0 +1,26 @@ + + + + + + + + + +