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