develop #1
@@ -8,7 +8,5 @@
|
|||||||
</list>
|
</list>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_23" default="true" project-jdk-name="23" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" project-jdk-name="21" project-jdk-type="JavaSDK" />
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
|
||||||
</component>
|
|
||||||
</project>
|
</project>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<junit.version>5.10.2</junit.version> </properties>
|
<junit.version>5.10.2</junit.version> </properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -24,8 +24,13 @@
|
|||||||
<artifactId>javafx-fxml</artifactId>
|
<artifactId>javafx-fxml</artifactId>
|
||||||
<version>21</version>
|
<version>21</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openjfx</groupId>
|
||||||
|
<artifactId>javafx-media</artifactId>
|
||||||
|
<version>21</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.junit.jupiter</groupId>
|
<groupId>org.junit.jupiter</groupId>
|
||||||
<artifactId>junit-jupiter-api</artifactId>
|
<artifactId>junit-jupiter-api</artifactId>
|
||||||
<version>${junit.version}</version>
|
<version>${junit.version}</version>
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ package sbu.javafx;
|
|||||||
|
|
||||||
import javafx.application.Application;
|
import javafx.application.Application;
|
||||||
|
|
||||||
public class Launcher{
|
public class Launcher
|
||||||
public static void main(String[] args) {
|
{
|
||||||
// TODO: Launch the JavaFX application by calling Application.launch(Main.class) }
|
public static void main(String[] args)
|
||||||
|
{
|
||||||
|
// TODO: Launch the JavaFX application by calling Application.launch(Main.class)
|
||||||
|
Application.launch(MusicApp.class, args);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,21 +3,43 @@ package sbu.javafx;
|
|||||||
import javafx.application.Application;
|
import javafx.application.Application;
|
||||||
import javafx.fxml.FXMLLoader;
|
import javafx.fxml.FXMLLoader;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public class MusicApp extends Application {
|
public class MusicApp extends Application
|
||||||
|
{
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void start(Stage stage) throws IOException
|
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.setTitle("Music Player");
|
||||||
stage.setScene(scene);
|
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();
|
stage.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
public static void main(String[] args)
|
||||||
|
{
|
||||||
|
launch(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,31 +1,381 @@
|
|||||||
package sbu.javafx;
|
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.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.control.Label;
|
||||||
import javafx.scene.input.MouseEvent;
|
import javafx.scene.control.Slider;
|
||||||
import javafx.scene.layout.VBox;
|
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<String, String> songDisplayNames = new HashMap<>();
|
||||||
|
private static final ObservableList<String> playlist = FXCollections.observableArrayList();
|
||||||
|
private static final ObservableList<String> favorites = FXCollections.observableArrayList();
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
public void handlePlayAction() {
|
public void initialize()
|
||||||
//Update labels, change button icons, etc.
|
{
|
||||||
|
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) {
|
visualizerAnimation = new Timeline();
|
||||||
//Add the song to a list or update a ListView.
|
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() {
|
if (favorites.contains(displayName))
|
||||||
//Create a new stage and show the playlist UI.
|
{
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String> playlistListView;
|
||||||
|
|
||||||
|
private ObservableList<String> playlist;
|
||||||
|
private Scene previousScene;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called from MusicController after loading Playlist-view.fxml
|
||||||
|
*/
|
||||||
|
public void setPlaylist(ObservableList<String> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 275 KiB |
@@ -1,8 +1,65 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
<?import javafx.scene.layout.VBox?>
|
<?import javafx.scene.control.*?>
|
||||||
<VBox xmlns="http://javafx.com/fxml" alignment="CENTER" spacing="10">
|
<?import javafx.scene.layout.*?>
|
||||||
|
|
||||||
<!-- TODO: Add your code here! -->
|
<BorderPane xmlns="http://javafx.com/javafx/21"
|
||||||
|
xmlns:fx="http://javafx.com/fxml/1"
|
||||||
|
fx:controller="sbu.javafx.MusicController"
|
||||||
|
stylesheets="@spotify.css">
|
||||||
|
|
||||||
</VBox>
|
<top>
|
||||||
|
<HBox styleClass="header" spacing="12" alignment="CENTER_LEFT" style="-fx-padding: 15;">
|
||||||
|
<Label text="Music Player" styleClass="h1"/>
|
||||||
|
<Region HBox.hgrow="ALWAYS"/>
|
||||||
|
<Label text="Now Playing:" styleClass="muted"/>
|
||||||
|
<Label fx:id="nowPlayingLabel" text="None" styleClass="song-title"/>
|
||||||
|
</HBox>
|
||||||
|
</top>
|
||||||
|
|
||||||
|
<center>
|
||||||
|
<VBox spacing="12" style="-fx-padding: 18;">
|
||||||
|
|
||||||
|
<ScrollPane fitToWidth="true"
|
||||||
|
style="-fx-background-color: transparent; -fx-background: transparent;"
|
||||||
|
VBox.vgrow="ALWAYS">
|
||||||
|
<VBox fx:id="songContainer"
|
||||||
|
spacing="12"
|
||||||
|
style="-fx-background-color: transparent;" />
|
||||||
|
</ScrollPane>
|
||||||
|
|
||||||
|
<VBox spacing="10" styleClass="song-card">
|
||||||
|
|
||||||
|
<HBox alignment="CENTER" spacing="20">
|
||||||
|
<HBox alignment="CENTER_LEFT" spacing="12" prefWidth="250">
|
||||||
|
<StackPane fx:id="visualizerContainer" prefWidth="30" prefHeight="30" />
|
||||||
|
<VBox alignment="CENTER_LEFT">
|
||||||
|
<Label fx:id="currentTrackTitle" text="Not Playing" styleClass="song-title" style="-fx-font-size: 13;"/>
|
||||||
|
<Label fx:id="timeLabel" text="00:00 / 00:00" styleClass="song-meta"/>
|
||||||
|
</VBox>
|
||||||
|
</HBox>
|
||||||
|
|
||||||
|
<Slider fx:id="progressSlider" min="0" max="100" value="0" HBox.hgrow="ALWAYS"/>
|
||||||
|
</HBox>
|
||||||
|
|
||||||
|
<HBox spacing="15" alignment="CENTER">
|
||||||
|
<Button fx:id="shuffleButton" text="Shuffle" onAction="#toggleShuffle" styleClass="btn-ghost"/>
|
||||||
|
<Button text="Prev" onAction="#previousTrack" styleClass="btn-ghost"/>
|
||||||
|
<Button fx:id="likeButton" text="♡" onAction="#toggleLike" styleClass="btn-ghost" style="-fx-font-size: 16px;"/>
|
||||||
|
<Button fx:id="playPauseButton" text="Pause" onAction="#togglePlayPause" styleClass="btn-green" prefWidth="80"/>
|
||||||
|
<Button text="Next" onAction="#nextTrack" styleClass="btn-ghost"/>
|
||||||
|
<Button fx:id="repeatButton" text="Repeat" onAction="#toggleRepeat" styleClass="btn-ghost"/>
|
||||||
|
</HBox>
|
||||||
|
</VBox>
|
||||||
|
|
||||||
|
<HBox spacing="10" alignment="CENTER" style="-fx-padding: 5;">
|
||||||
|
<Label text="Vol:" styleClass="song-meta"/>
|
||||||
|
<Slider fx:id="volumeSlider" min="0" max="100" value="50" prefWidth="150"/>
|
||||||
|
<Region HBox.hgrow="ALWAYS" />
|
||||||
|
<Button text="View Favorites" styleClass="btn-ghost" onAction="#goToFavoritesScene" style="-fx-margin-right: 10;"/>
|
||||||
|
<Button text="View Playlist" styleClass="btn-ghost" onAction="#goToPlaylistScene"/>
|
||||||
|
</HBox>
|
||||||
|
|
||||||
|
</VBox>
|
||||||
|
</center>
|
||||||
|
</BorderPane>
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.scene.control.*?>
|
||||||
|
<?import javafx.scene.layout.*?>
|
||||||
|
|
||||||
|
<BorderPane xmlns="http://javafx.com/javafx/21"
|
||||||
|
xmlns:fx="http://javafx.com/fxml/1"
|
||||||
|
fx:controller="sbu.javafx.PlaylistController"
|
||||||
|
stylesheets="@spotify.css">
|
||||||
|
|
||||||
|
<top>
|
||||||
|
<HBox styleClass="header" spacing="10" alignment="CENTER_LEFT">
|
||||||
|
<Button text="← Back" styleClass="btn-ghost" onAction="#goBack"/>
|
||||||
|
<Label text="Playlist" styleClass="h1"/>
|
||||||
|
</HBox>
|
||||||
|
</top>
|
||||||
|
|
||||||
|
<center>
|
||||||
|
<VBox spacing="12" style="-fx-padding: 18;">
|
||||||
|
<ListView fx:id="playlistListView" VBox.vgrow="ALWAYS"/>
|
||||||
|
<HBox alignment="CENTER_RIGHT">
|
||||||
|
<Button text="Remove Selected" onAction="#removeSelected"/>
|
||||||
|
</HBox>
|
||||||
|
</VBox>
|
||||||
|
</center>
|
||||||
|
</BorderPane>
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
.root {
|
||||||
|
-fx-font-family: "Segoe UI", "Inter", "Roboto", "Arial";
|
||||||
|
-fx-background-color: #121212;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
-fx-background-color: #181818;
|
||||||
|
-fx-padding: 14 18 14 18;
|
||||||
|
-fx-border-color: #242424;
|
||||||
|
-fx-border-width: 0 0 1 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h1 {
|
||||||
|
-fx-text-fill: #FFFFFF;
|
||||||
|
-fx-font-size: 20px;
|
||||||
|
-fx-font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.muted {
|
||||||
|
-fx-text-fill: #B3B3B3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.song-card {
|
||||||
|
-fx-background-color: #181818;
|
||||||
|
-fx-background-radius: 12;
|
||||||
|
-fx-padding: 12 14 12 14;
|
||||||
|
-fx-border-color: #242424;
|
||||||
|
-fx-border-radius: 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.song-card:hover {
|
||||||
|
-fx-background-color: #202020;
|
||||||
|
}
|
||||||
|
|
||||||
|
.song-title {
|
||||||
|
-fx-text-fill: #FFFFFF;
|
||||||
|
-fx-font-size: 14px;
|
||||||
|
-fx-font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.song-meta {
|
||||||
|
-fx-text-fill: #B3B3B3;
|
||||||
|
-fx-font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
-fx-background-radius: 999;
|
||||||
|
-fx-padding: 8 14 8 14;
|
||||||
|
-fx-font-weight: 600;
|
||||||
|
-fx-cursor: hand;
|
||||||
|
-fx-background-color: #2A2A2A;
|
||||||
|
-fx-text-fill: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
-fx-background-color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-green {
|
||||||
|
-fx-background-color: #1DB954;
|
||||||
|
-fx-text-fill: #0B0B0B;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-green:hover {
|
||||||
|
-fx-background-color: #22C55E;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-ghost {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-border-color: #2A2A2A;
|
||||||
|
-fx-border-width: 1;
|
||||||
|
-fx-text-fill: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-ghost:hover {
|
||||||
|
-fx-background-color: #1E1E1E;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-view {
|
||||||
|
-fx-background-color: #121212;
|
||||||
|
-fx-control-inner-background: #121212;
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
-fx-padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-cell {
|
||||||
|
-fx-background-color: #181818;
|
||||||
|
-fx-text-fill: #FFFFFF;
|
||||||
|
-fx-padding: 10 12 10 12;
|
||||||
|
-fx-border-color: #242424;
|
||||||
|
-fx-border-width: 0 0 1 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-cell:filled:selected {
|
||||||
|
-fx-background-color: #1F1F1F;
|
||||||
|
}
|
||||||
|
.btn-green-small {
|
||||||
|
-fx-background-color: #1db954;
|
||||||
|
-fx-text-fill: black;
|
||||||
|
-fx-background-radius: 50;
|
||||||
|
-fx-padding: 5 10;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-green-small:hover {
|
||||||
|
-fx-background-color: #1ed760;
|
||||||
|
-fx-scale-x: 1.1;
|
||||||
|
-fx-scale-y: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-heart-liked {
|
||||||
|
-fx-text-fill: #ff4d6d !important;
|
||||||
|
}
|
||||||