449 lines
16 KiB
Java
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");
|
|
}
|
|
|
|
}
|