diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..8306744
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/java/sbu/javafx/Launcher.java b/src/main/java/sbu/javafx/Launcher.java
index b66ae9b..a3ffd1f 100644
--- a/src/main/java/sbu/javafx/Launcher.java
+++ b/src/main/java/sbu/javafx/Launcher.java
@@ -4,7 +4,6 @@ import javafx.application.Application;
public class Launcher{
public static void main(String[] args) {
- // TODO: Launch the JavaFX application by calling Application.launch(Main.class)
Application.launch(MusicApp.class);
}
}
diff --git a/src/main/java/sbu/javafx/MusicApp.java b/src/main/java/sbu/javafx/MusicApp.java
index a0c39e6..e6fd5e6 100644
--- a/src/main/java/sbu/javafx/MusicApp.java
+++ b/src/main/java/sbu/javafx/MusicApp.java
@@ -5,19 +5,20 @@ import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
-import java.io.IOException;
-
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());
+ public void start(Stage stage) throws Exception {
+ FXMLLoader loader = new FXMLLoader(
+ getClass().getResource("App-view.fxml"));
+ Scene scene = new Scene(loader.load());
stage.setTitle("Music Player");
stage.setScene(scene);
- stage.show();
stage.setResizable(false);
+ 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..092ecec 100644
--- a/src/main/java/sbu/javafx/MusicController.java
+++ b/src/main/java/sbu/javafx/MusicController.java
@@ -1,31 +1,387 @@
package sbu.javafx;
-import javafx.fxml.FXML;
+
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
-import javafx.scene.control.Label;
-import javafx.scene.input.MouseEvent;
+import javafx.fxml.FXML;
+import javafx.scene.control.*;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.media.Media;
+import javafx.scene.media.MediaPlayer;
import javafx.scene.layout.VBox;
+import javafx.stage.FileChooser;
+import javafx.util.Duration;
+
+import java.io.File;
+import java.util.*;
public class MusicController {
- // TODO: Define your UI components using the @FXML annotation.
+ // ========================================= fxml Fields ============================================
+ // player panel
+ @FXML private ImageView albumCover;
+ @FXML private Label songTitle, artistName;
+ @FXML private Label currentTime, totalTime;
+ @FXML private Button playPauseBtn, shuffleBtn, repeatBtn;
+ @FXML private Button libraryTabBtn, playlistTabBtn;
+ @FXML private Slider progressSlider, volumeSlider;
- //TODO: Implement the logic to play a song. This method should update the UI to show which song is currently playing.
+ // library and playlists panel
+ @FXML private VBox libraryPane, playlistsPane, playlistsListPane, playlistDetailPane;
+ @FXML private ListView libraryView;
+ @FXML private ListView playlistsView;
+ @FXML private ListView playlistDetailView;
+ @FXML private Label libraryCount, playlistDetailName;
+
+ // my fields
+ private MediaPlayer mediaPlayer;
+ private ObservableList library = FXCollections.observableArrayList();
+ private HashMap> playlists = new HashMap<>();
+
+ private List playQueue = new ArrayList<>();
+ private int currentIndex = -1;
+ private boolean isPlaying = false;
+ private boolean isShuffle = false;
+ private boolean isRepeat = false;
+
+ // ========================================= Methods ============================================
+ public void initialize(){ // runs without calling
+ libraryView.setItems(library);
+
+ // volume
+ volumeSlider.valueProperty().addListener((obs, oldVal, newVal) -> {
+ if(mediaPlayer != null){
+ mediaPlayer.setVolume(newVal.doubleValue());
+ }
+ });
+
+ // progress slider - seek functionality
+ progressSlider.setOnMouseReleased(e -> {
+ if(mediaPlayer != null){
+ mediaPlayer.seek(Duration.seconds(progressSlider.getValue()));
+ }
+ });
+ progressSlider.setMax(100);
+
+ // library - double click to play
+ libraryView.setOnMouseClicked(e -> {
+ if(e.getClickCount() == 2){ // double click
+ Song selectedSong = libraryView.getSelectionModel().getSelectedItem();
+ if(selectedSong != null){
+ playQueue = new ArrayList<>(library);
+ currentIndex = playQueue.indexOf(selectedSong);
+ playSong(selectedSong);
+ }
+ }
+ });
+
+ // playlist detail - double click to play
+ playlistDetailView.setOnMouseClicked(e -> {
+ if(e.getClickCount() == 2){ // double click
+ Song selectedSong = playlistDetailView.getSelectionModel().getSelectedItem();
+ if(selectedSong != null){
+ playQueue = new ArrayList<>(playlistDetailView.getItems());
+ currentIndex = playQueue.indexOf(selectedSong);
+ playSong(selectedSong);
+ }
+ }
+ });
+
+ // playlists - double click to open
+ playlistsView.setOnMouseClicked(e -> {
+ if(e.getClickCount() == 2){ // double click
+ String selectedPlaylist = playlistsView.getSelectionModel().getSelectedItem();
+ if(selectedPlaylist != null){
+ openPlayList(selectedPlaylist);
+ }
+ }
+ });
+
+ setupLibraryItems();
+
+ }
+
+ // ========================================= Playback ============================================
+ private void playSong(Song song) {
+ if(mediaPlayer != null){
+ mediaPlayer.stop();
+ mediaPlayer.dispose(); // file not in use
+ }
+
+ File file = new File(song.getFilePath());
+ if (!file.exists()) {
+ System.out.println("FILE NOT FOUND!");
+ return;
+ }
+
+ Media media = new Media(file.toURI().toString());
+ mediaPlayer = new MediaPlayer(media);
+ mediaPlayer.setVolume(volumeSlider.getValue());
+
+ // UI changes
+ songTitle.setText(song.getTitle());
+ artistName.setText(song.getArtist());
+ loadCover(song);
+
+ // progress bar
+ mediaPlayer.currentTimeProperty().addListener((obs, oldVal, newVal) -> {
+ if(!progressSlider.isValueChanging()){
+ progressSlider.setValue(newVal.toSeconds());
+ currentTime.setText(formatTime(newVal.toSeconds()));
+ }
+ });
+
+ mediaPlayer.setOnReady(() -> {
+ double total= mediaPlayer.getTotalDuration().toSeconds();
+ progressSlider.setMax(total);
+ totalTime.setText(formatTime(total));
+ });
+
+ mediaPlayer.setOnEndOfMedia(() -> {
+ if(isRepeat){
+ mediaPlayer.seek(Duration.ZERO);
+ mediaPlayer.play();
+ }
+ else{
+ handleNext();
+ }
+ });
+ mediaPlayer.play();
+ isPlaying = true;
+ playPauseBtn.setText("⏸");
+ libraryView.refresh();
+ }
+
+ private void loadCover(Song song){
+ try{
+ if(song.getCoverPath() != null && !song.getCoverPath().isEmpty()){
+ albumCover.setImage(new Image(
+ new File(song.getCoverPath()).toURI().toString()
+ ));
+ }
+ else{
+ albumCover.setImage(new Image(
+ Objects.requireNonNull(getClass().getResourceAsStream("/sbu/images/default-cover.png"))
+ ));
+ }
+ } catch(Exception e){
+ albumCover.setImage(null);
+ }
+ }
+
+ private String formatTime(double seconds) {
+ int minute = (int) seconds / 60;
+ int second = (int) seconds % 60;
+ return String.format("%d:%02d", minute, second);
+ }
+
+ // ========================================= Control buttons ============================================
+ @FXML
+ public void handlePlayPause() {
+ if(mediaPlayer == null) return;
+
+ if(isPlaying){
+ mediaPlayer.pause();
+ playPauseBtn.setText("▶");
+ }
+ else{
+ mediaPlayer.play();
+ playPauseBtn.setText("⏸");
+ }
+ isPlaying = !isPlaying;
+ }
@FXML
- public void handlePlayAction() {
- //Update labels, change button icons, etc.
+ public void handleNext() {
+ if(playQueue.isEmpty()) return;
+ if(isShuffle){
+ currentIndex = (int) (Math.random() * playQueue.size());
+ }
+ else{
+ currentIndex = (currentIndex + 1) % playQueue.size(); // when we reach end of the queue it goes to the first one
+ }
+ playSong(playQueue.get(currentIndex));
}
- //TODO: Logic to add a specific song to the user's playlist.
-
- public void addToPlaylist(String songName) {
- //Add the song to a list or update a ListView.
+ @FXML
+ public void handlePrev() {
+ if(playQueue.isEmpty()) return;
+ currentIndex = (currentIndex - 1 + playQueue.size()) % playQueue.size();
+ playSong(playQueue.get(currentIndex));
}
- // TODO:This method should open a new window or change the current scene to display the "Playlist" page.
-
- public void openPlaylistWindow() {
- //Create a new stage and show the playlist UI.
+ @FXML
+ public void handleShuffle() {
+ isShuffle = !isShuffle;
+ if(isShuffle) shuffleBtn.getStyleClass().add("active-btn");
+ else shuffleBtn.getStyleClass().remove("active-btn");
}
+
+ @FXML
+ public void handleRepeat() {
+ isRepeat = !isRepeat;
+ if (isRepeat) repeatBtn.getStyleClass().add("active-btn");
+ else repeatBtn.getStyleClass().remove("active-btn");
+ }
+
+ // ========================================= Library ============================================
+ @FXML
+ public void handleAddSong() {
+ FileChooser fileChooser = new FileChooser();
+ fileChooser.setTitle("Add Songs");
+ fileChooser.getExtensionFilters().add(
+ new FileChooser.ExtensionFilter("Audio Files", "*.mp3", "*.wav", "*.aac", "*.flac")
+ );
+
+ List files = fileChooser.showOpenMultipleDialog(null);
+ if(files == null) return;
+ for(File file : files){
+ String name = file.getName();
+ Song song = new Song(name, "Unknown", "Unknown", file.getAbsolutePath(), "", 0);
+ library.add(song);
+ }
+ libraryCount.setText(library.size() + " Songs");
+ }
+
+ private void setupLibraryItems() {
+ libraryView.setCellFactory(lv -> new ListCell<>() {
+ private final ImageView cover = new ImageView();
+ private final Label index = new Label();
+ private final Label title = new Label();
+ private final Label artist = new Label();
+ private final Label duration = new Label();
+ private final VBox info = new VBox(2, title, artist);
+ private final javafx.scene.layout.HBox root = new javafx.scene.layout.HBox(10, index, cover, info, duration);
+
+ // Context Menu
+ private final ContextMenu contextMenu = new ContextMenu();
+ private final Menu addToPlaylistMenu = new Menu("Add to Playlist");
+
+ {
+ cover.setFitWidth(36);
+ cover.setFitHeight(36);
+ cover.setPreserveRatio(true);
+ index.setStyle("-fx-text-fill: #9E9E9E; -fx-font-size: 11px; -fx-min-width: 20px;");
+ title.setStyle("-fx-font-size: 13px; -fx-text-fill: #212121;");
+ artist.setStyle("-fx-font-size: 11px; -fx-text-fill: #757575;");
+ duration.setStyle("-fx-font-size: 11px; -fx-text-fill: #9E9E9E;");
+ javafx.scene.layout.HBox.setHgrow(info, javafx.scene.layout.Priority.ALWAYS);
+ root.setAlignment(javafx.geometry.Pos.CENTER_LEFT);
+ root.setPadding(new javafx.geometry.Insets(4, 8, 4, 8));
+
+ // Context Menu
+ contextMenu.getItems().add(addToPlaylistMenu);
+ contextMenu.setOnShowing(e -> {
+ addToPlaylistMenu.getItems().clear();
+ if (playlists.isEmpty()) {
+ MenuItem none = new MenuItem("No playlists yet");
+ none.setDisable(true);
+ addToPlaylistMenu.getItems().add(none);
+ } else {
+ for (String name : playlists.keySet()) {
+ MenuItem item = new MenuItem(name);
+ item.setOnAction(ev -> {
+ Song song = getItem();
+ if (song != null) playlists.get(name).add(song);
+ });
+ addToPlaylistMenu.getItems().add(item);
+ }
+ }
+ });
+ }
+
+ @Override
+ protected void updateItem(Song song, boolean empty) {
+ super.updateItem(song, empty);
+ if (empty || song == null) {
+ setGraphic(null);
+ setContextMenu(null);
+ } else {
+ index.setText(String.valueOf(getIndex() + 1));
+ title.setText(song.getTitle());
+ artist.setText(song.getArtist());
+ duration.setText(song.getFormattedDuration());
+
+ try {
+ if (song.getCoverPath() != null && !song.getCoverPath().isEmpty()) {
+ cover.setImage(new Image(new File(song.getCoverPath()).toURI().toString(), true));
+ } else {
+ cover.setImage(new Image(
+ Objects.requireNonNull(getClass().getResourceAsStream("/sbu/images/default-cover.png"))));
+ }
+ } catch (Exception e) {
+ cover.setImage(null);
+ }
+
+ if (getIndex() == currentIndex && playQueue.equals(new ArrayList<>(library))) {
+ title.setStyle("-fx-font-size: 13px; -fx-text-fill: #1DB954; -fx-font-weight: bold;");
+ } else {
+ title.setStyle("-fx-font-size: 13px; -fx-text-fill: #212121;");
+ }
+
+ setGraphic(root);
+ setContextMenu(contextMenu);
+ }
+ }
+ });
+ }
+
+ // ========================================= Playlists ============================================
+ @FXML
+ public void handleNewPlaylist() {
+ TextInputDialog dialog = new TextInputDialog();
+ dialog.setTitle("New Playlist");
+ dialog.setHeaderText(null);
+ dialog.setContentText("Playlist name: ");
+ dialog.showAndWait().ifPresent(name -> {
+ if(!name.isBlank() && !playlists.containsKey(name)){
+ playlists.put(name, FXCollections.observableArrayList());
+ playlistsView.setItems( FXCollections.observableArrayList(playlists.keySet()) );
+ }
+ });
+ }
+
+ @FXML
+ private void openPlayList(String name) {
+ playlistDetailName.setText(name);
+ playlistDetailView.setItems(playlists.get(name));
+ playlistsListPane.setVisible(false);
+ playlistsListPane.setManaged(false);
+ playlistDetailPane.setVisible(true);
+ playlistDetailPane.setManaged(true);
+ }
+
+ @FXML
+ public void handleBackToPlaylists() {
+ playlistDetailPane.setVisible(false);
+ playlistDetailPane.setManaged(false);
+ playlistsListPane.setVisible(true);
+ playlistsListPane.setManaged(true);
+ }
+
+ // switching between tabs (library and playlists)
+ @FXML
+ public void showLibrary() {
+ libraryPane.setVisible(true);
+ libraryPane.setManaged(true);
+ playlistsPane.setVisible(false);
+ playlistsPane.setManaged(false);
+
+ libraryTabBtn.getStyleClass().add("tab-active");
+ playlistTabBtn.getStyleClass().remove("tab-active");
+ }
+
+ @FXML
+ private void showPlaylists() {
+ playlistsPane.setVisible(true);
+ playlistsPane.setManaged(true);
+ libraryPane.setVisible(false);
+ libraryPane.setManaged(false);
+
+ playlistTabBtn.getStyleClass().add("tab-active");
+ libraryTabBtn.getStyleClass().remove("tab-active");
+ }
+
}
diff --git a/src/main/java/sbu/javafx/Playlist.java b/src/main/java/sbu/javafx/Playlist.java
new file mode 100644
index 0000000..8ddc989
--- /dev/null
+++ b/src/main/java/sbu/javafx/Playlist.java
@@ -0,0 +1,47 @@
+package sbu.javafx;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class Playlist {
+
+ private String name;
+ private List songs = new ArrayList<>();
+
+ // Constructor
+ public Playlist(String name){
+ this.name = name;
+ }
+
+ // ========================================= getters & setters ============================================
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public List getSongs() {
+ return songs;
+ }
+
+ // ========================================= Methods ============================================
+ public void addSong(Song song){
+ if(song != null && !songs.contains(song))
+ songs.add(song);
+ }
+
+ public void removeSong(Song song){
+ songs.remove(song);
+ }
+
+ public int size(){
+ return songs.size();
+ }
+
+ @Override
+ public String toString() {
+ return name + " (" + songs.size() + " songs";
+ }
+}
diff --git a/src/main/java/sbu/javafx/Song.java b/src/main/java/sbu/javafx/Song.java
index 5fedfea..7f4dc26 100644
--- a/src/main/java/sbu/javafx/Song.java
+++ b/src/main/java/sbu/javafx/Song.java
@@ -60,7 +60,7 @@ public class Song {
}
// ========================================= Methods ============================================
- public String formattedDuration(){
+ public String getFormattedDuration(){
int minutes = duration / 60;
int seconds = duration % 60;
return String.format("%d:%02d)", minutes, seconds);
diff --git a/src/main/resources/sbu/images/default-cover.png b/src/main/resources/sbu/images/default-cover.png
new file mode 100644
index 0000000..2f4b39a
Binary files /dev/null and b/src/main/resources/sbu/images/default-cover.png differ