Files
HW-07-JavaFX/src/main/java/sbu/javafx/MusicController.java
T

449 lines
16 KiB
Java

package sbu.javafx;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
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 org.jaudiotagger.audio.AudioFile;
import org.jaudiotagger.audio.AudioFileIO;
import org.jaudiotagger.audio.AudioHeader;
import org.jaudiotagger.tag.FieldKey;
import org.jaudiotagger.tag.Tag;
import org.jaudiotagger.tag.images.Artwork;
import java.io.File;
import java.io.FileOutputStream;
import java.util.*;
public class MusicController {
// ========================================= 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;
// library and playlists panel
@FXML private VBox libraryPane, playlistsPane, playlistsListPane, playlistDetailPane;
@FXML private ListView<Song> libraryView;
@FXML private ListView<String> playlistsView;
@FXML private ListView<Song> playlistDetailView;
@FXML private Label libraryCount, playlistDetailName;
// my fields
private MediaPlayer mediaPlayer;
private ObservableList<Song> library = FXCollections.observableArrayList();
private HashMap<String, ObservableList<Song>> playlists = new HashMap<>();
private List<Song> 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);
}
private Song createSongFromFile(File file){
String filePath = file.getAbsolutePath();
String title = file.getName();
String artist = "Unknown";
String album = "Unknown";
String coverPath = "";
int duration = 0;
try{
AudioFile audioFile = AudioFileIO.read(file);
Tag tag = audioFile.getTag();
AudioHeader header = audioFile.getAudioHeader();
if(tag != null){
// title
String t = tag.getFirst(FieldKey.TITLE);
if(t != null && !t.isBlank())
title = t;
// artist
String a = tag.getFirst(FieldKey.ARTIST);
if(a != null && !a.isBlank())
artist = a;
// album
String al = tag.getFirst(FieldKey.ALBUM);
if(al != null && !al.isBlank())
album = al;
// cover
List<Artwork> artworks = tag.getArtworkList();
if(artworks != null && !artworks.isEmpty()){
byte[] imageData = artworks.get(0).getBinaryData();
File coverFile = new File(
System.getProperty("java.io.tmpdir"),
title.replaceAll("\\s+", "_") + "_cover.jpg"
);
try(FileOutputStream fos = new FileOutputStream(coverFile)){
fos.write(imageData);
coverPath = coverFile.getAbsolutePath();
}
}
}
if(header != null)
duration = header.getTrackLength();
} catch(Exception e){
System.out.println("Cannot read metadata, " + e.getMessage());
}
return new Song(title, artist, album, filePath, coverPath, duration);
}
// ========================================= 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 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));
}
@FXML
public void handlePrev() {
if(playQueue.isEmpty()) return;
currentIndex = (currentIndex - 1 + playQueue.size()) % playQueue.size();
playSong(playQueue.get(currentIndex));
}
@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<File> files = fileChooser.showOpenMultipleDialog(null);
if(files == null) return;
for(File file : files){
Song song = createSongFromFile(file);
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");
}
}