Compare commits

6 Commits
Author SHA1 Message Date
Matin-Ardestani 0e4d96b3a7 Add dark mode 2026-06-10 15:35:26 +03:30
Matin-Ardestani 90d8487c6e Implement radius for covers 2026-06-10 15:17:03 +03:30
Matin-Ardestani 517414cdf4 Fix cover and artist bug 2026-06-10 15:08:14 +03:30
Matin-Ardestani a5e70479ed Implement first draft of the app 2026-06-10 14:51:18 +03:30
Matin-Ardestani 2099a1a9f3 Implement Song class 2026-06-09 22:59:45 +03:30
Matin-Ardestani 305aef7473 Implement first draft app view 2026-06-09 22:58:53 +03:30
11 changed files with 1212 additions and 49 deletions
+1 -1
View File
@@ -8,7 +8,7 @@
</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" default="true" project-jdk-name="25" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" /> <output url="file://$PROJECT_DIR$/out" />
</component> </component>
</project> </project>
Generated
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+29 -23
View File
@@ -11,32 +11,50 @@
<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>
<maven.compiler.release>23</maven.compiler.release>
<javafx.version>23</javafx.version>
</properties>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>org.openjfx</groupId> <groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId> <artifactId>javafx-controls</artifactId>
<version>21</version> <version>${javafx.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.openjfx</groupId> <groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId> <artifactId>javafx-fxml</artifactId>
<version>21</version> <version>${javafx.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-media</artifactId>
<version>${javafx.version}</version>
</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>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId> <artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version> <version>${junit.version}</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependencies> </dependency>
<dependency>
<groupId>net.jthink</groupId>
<artifactId>jaudiotagger</artifactId>
<version>3.0.1</version>
</dependency>
</dependencies>
<build> <build>
<plugins> <plugins>
@@ -45,30 +63,18 @@
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version> <version>3.13.0</version>
<configuration> <configuration>
<source>23</source> <release>${maven.compiler.release}</release>
<target>23</target>
</configuration> </configuration>
</plugin> </plugin>
<plugin> <plugin>
<groupId>org.openjfx</groupId> <groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId> <artifactId>javafx-maven-plugin</artifactId>
<version>0.0.8</version> <version>0.0.8</version>
<executions> <configuration>
<execution> <mainClass>sbu.javafx/sbu.javafx.MusicApp</mainClass>
<!-- Default configuration for running with: mvn clean javafx:run --> </configuration>
<id>default-cli</id>
<configuration>
<mainClass>sbu.javafx/sbu.javafx.MusicApp</mainClass>
<launcher>app</launcher>
<jlinkZipName>app</jlinkZipName>
<jlinkImageName>app</jlinkImageName>
<noManPages>true</noManPages>
<stripDebug>true</stripDebug>
<noHeaderFiles>true</noHeaderFiles>
</configuration>
</execution>
</executions>
</plugin> </plugin>
</plugins> </plugins>
</build> </build>
</project> </project>
+2 -1
View File
@@ -4,5 +4,6 @@ import javafx.application.Application;
public class Launcher{ public class Launcher{
public static void main(String[] args) { public static void main(String[] args) {
// TODO: Launch the JavaFX application by calling Application.launch(Main.class) } Application.launch(MusicApp.class);
}
} }
+11 -8
View File
@@ -3,21 +3,24 @@ 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;
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 Exception {
{ FXMLLoader loader = new FXMLLoader(
FXMLLoader fxmlLoader = new FXMLLoader(MusicApp.class.getResource("App-view.fxml")); getClass().getResource("App-view.fxml"));
Scene scene = new Scene(fxmlLoader.load()); Scene scene = new Scene(loader.load());
stage.setTitle("Music Player"); stage.setTitle("Music Player");
stage.getIcons().add(new Image("/sbu/images/default-cover.png"));
stage.setScene(scene); stage.setScene(scene);
//TODO: add icon to the stage stage.setResizable(false);
stage.show(); stage.show();
} }
} public static void main(String[] args) {
launch(args);
}
}
+464 -13
View File
@@ -1,31 +1,482 @@
package sbu.javafx; package sbu.javafx;
import javafx.fxml.FXML;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent; import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.control.Button;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent; import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
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.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 javafx.scene.shape.Rectangle;
import java.awt.*;
import java.io.File;
import java.io.FileOutputStream;
import java.util.*;
import java.util.List;
public class MusicController { 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<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;
// theme button
@FXML private Button themeBtn;
private boolean isDarkMode = 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();
Rectangle clip = new Rectangle(160, 160);
clip.setArcWidth(24);
clip.setArcHeight(24);
albumCover.setClip(clip);
}
// ========================================= 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 @FXML
public void handlePlayAction() { public void handleNext() {
//Update labels, change button icons, etc. 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. @FXML
public void handlePrev() {
public void addToPlaylist(String songName) { if(playQueue.isEmpty()) return;
//Add the song to a list or update a ListView. 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. @FXML
public void handleShuffle() {
isShuffle = !isShuffle;
if(isShuffle) shuffleBtn.getStyleClass().add("active-btn");
else shuffleBtn.getStyleClass().remove("active-btn");
}
public void openPlaylistWindow() { @FXML
//Create a new stage and show the playlist UI. 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);
Rectangle coverClip = new Rectangle(36, 36);
coverClip.setArcWidth(8);
coverClip.setArcHeight(8);
cover.setClip(coverClip);
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");
}
// theme controller
@FXML
public void handleThemeToggle() {
isDarkMode = !isDarkMode;
javafx.scene.Scene scene = themeBtn.getScene();
if (isDarkMode) {
scene.getRoot().getStyleClass().add("dark");
themeBtn.setText("☀️");
} else {
scene.getRoot().getStyleClass().remove("dark");
themeBtn.setText("🌙");
}
} }
} }
+47
View File
@@ -0,0 +1,47 @@
package sbu.javafx;
import java.util.ArrayList;
import java.util.List;
public class Playlist {
private String name;
private List<Song> 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<Song> 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";
}
}
+73
View File
@@ -0,0 +1,73 @@
package sbu.javafx;
public class Song {
private String title;
private String artist;
private String album;
private String filePath;
private String coverPath;
private int duration; // seconds
// Constructor
public Song(String title, String artist, String album, String filePath, String coverPath, int duration){
this.title = title;
this.artist = artist;
this.album = album;
this.filePath = filePath;
this.coverPath = coverPath;
this.duration = duration;
}
// ========================================= Getters ============================================
public int getDuration() {
return duration;
}
public String getCoverPath() {
return coverPath;
}
public String getFilePath() {
return filePath;
}
public String getAlbum() {
return album;
}
public String getArtist() {
return artist;
}
public String getTitle() {
return title;
}
// ========================================= Setters ============================================
public void setTitle(String title) {
this.title = title;
}
public void setArtist(String artist) {
this.artist = artist;
}
public void setAlbum(String album) {
this.album = album;
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public void setCoverPath(String coverPath) {
this.coverPath = coverPath;
}
public void setDuration(int duration) {
this.duration = duration;
}
// ========================================= Methods ============================================
public String getFormattedDuration(){
int minutes = duration / 60;
int seconds = duration % 60;
return String.format("%d:%02d)", minutes, seconds);
}
@Override
public String toString() {
return title + " - " + artist;
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

+195 -3
View File
@@ -1,8 +1,200 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.VBox?> <?import javafx.scene.layout.VBox?>
<VBox xmlns="http://javafx.com/fxml" alignment="CENTER" spacing="10"> <?import javafx.scene.layout.HBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Slider?>
<?import javafx.scene.control.ListView?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.StackPane?>
<!-- TODO: Add your code here! --> <HBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="sbu.javafx.MusicController"
stylesheets="@style.css"
spacing="0"
prefWidth="700"
prefHeight="420">
</VBox> <!-- ===== Left: Player ===== -->
<VBox styleClass="player-panel"
alignment="CENTER"
spacing="20"
prefWidth="320">
<!-- Dark Mode Toggle -->
<HBox alignment="TOP_RIGHT" fillHeight="false">
<Button fx:id="themeBtn" text="🌙" styleClass="theme-btn"
onAction="#handleThemeToggle"/>
</HBox>
<padding>
<Insets top="32" right="28" bottom="32" left="28"/>
</padding>
<!-- Album Art -->
<StackPane styleClass="cover-container" alignment="CENTER">
<ImageView fx:id="albumCover"
fitWidth="160"
fitHeight="160"
preserveRatio="true"
styleClass="album-cover"/>
</StackPane>
<!-- Song Info -->
<VBox alignment="CENTER" spacing="6">
<Label fx:id="songTitle" text="Song Title" styleClass="song-title"/>
<Label fx:id="artistName" text="Artist" styleClass="artist-name"/>
</VBox>
<!-- Progress -->
<VBox spacing="4" fillWidth="true">
<Slider fx:id="progressSlider" styleClass="progress-slider"/>
<HBox>
<Label fx:id="currentTime" text="0:00" styleClass="time-label"/>
<HBox HBox.hgrow="ALWAYS"/>
<Label fx:id="totalTime" text="0:00" styleClass="time-label"/>
</HBox>
</VBox>
<!-- Controls -->
<HBox alignment="CENTER" spacing="16">
<Button fx:id="shuffleBtn" text="⇄" styleClass="control-btn"
onAction="#handleShuffle"/>
<Button text="⏮" styleClass="control-btn"
onAction="#handlePrev"/>
<Button fx:id="playPauseBtn" text="▶" styleClass="play-btn"
onAction="#handlePlayPause"/>
<Button text="⏭" styleClass="control-btn"
onAction="#handleNext"/>
<Button fx:id="repeatBtn" text="⟳" styleClass="control-btn"
onAction="#handleRepeat"/>
</HBox>
<!-- Volume -->
<HBox alignment="CENTER" spacing="8">
<Label text="🔈" styleClass="time-label"/>
<Slider fx:id="volumeSlider"
prefWidth="160"
min="0" max="1" value="0.7"
styleClass="volume-slider"/>
<Label text="🔊" styleClass="time-label"/>
</HBox>
</VBox>
<!-- ===== Right: Playlist Panel ===== -->
<VBox styleClass="playlist-panel"
spacing="0"
HBox.hgrow="ALWAYS">
<!-- Tab Buttons -->
<HBox styleClass="tab-bar" spacing="0">
<Button fx:id="libraryTabBtn"
text="🎵 Library"
styleClass="tab-btn, tab-active"
maxWidth="Infinity"
HBox.hgrow="ALWAYS"
onAction="#showLibrary"/>
<Button fx:id="playlistTabBtn"
text="📋 Playlists"
styleClass="tab-btn"
maxWidth="Infinity"
HBox.hgrow="ALWAYS"
onAction="#showPlaylists"/>
</HBox>
<!-- StackPane: two panels on top of each other -->
<StackPane VBox.vgrow="ALWAYS">
<!-- ===== Tab 1: Library ===== -->
<VBox fx:id="libraryPane"
spacing="10"
styleClass="tab-content">
<padding>
<Insets top="16" right="16" bottom="16" left="16"/>
</padding>
<!-- Header -->
<HBox alignment="CENTER_LEFT" spacing="10">
<Label fx:id="libraryCount"
text="0 Songs"
styleClass="tab-header"
HBox.hgrow="ALWAYS"/>
<Button text="+ Add Song"
styleClass="add-btn"
onAction="#handleAddSong"/>
</HBox>
<!-- List -->
<ListView fx:id="libraryView"
VBox.vgrow="ALWAYS"
styleClass="playlist"/>
</VBox>
<!-- ===== Tab 2: Playlists ===== -->
<VBox fx:id="playlistsPane"
spacing="10"
styleClass="tab-content"
visible="false"
managed="false">
<padding>
<Insets top="16" right="16" bottom="16" left="16"/>
</padding>
<!-- Header -->
<HBox alignment="CENTER_LEFT" spacing="10">
<Label text="My Playlists"
styleClass="tab-header"
HBox.hgrow="ALWAYS"/>
<Button text="+ New"
styleClass="add-btn"
onAction="#handleNewPlaylist"/>
</HBox>
<!-- Playlists list or selected playlist content -->
<StackPane VBox.vgrow="ALWAYS">
<!-- Playlists list -->
<VBox fx:id="playlistsListPane" spacing="8">
<ListView fx:id="playlistsView"
VBox.vgrow="ALWAYS"
styleClass="playlist"/>
</VBox>
<!-- Inside a playlist -->
<VBox fx:id="playlistDetailPane"
spacing="10"
visible="false"
managed="false">
<!-- Back + Playlist Name -->
<HBox alignment="CENTER_LEFT" spacing="10">
<Button text="← Back"
styleClass="back-btn"
onAction="#handleBackToPlaylists"/>
<Label fx:id="playlistDetailName"
text="Playlist Name"
styleClass="tab-header"/>
</HBox>
<ListView fx:id="playlistDetailView"
VBox.vgrow="ALWAYS"
styleClass="playlist"/>
</VBox>
</StackPane>
</VBox>
</StackPane>
</VBox>
</HBox>
+383
View File
@@ -0,0 +1,383 @@
/* ===== Root ===== */
.root {
-fx-background-color: #F5F5F5;
-fx-font-family: "Segoe UI", sans-serif;
}
/* ===== Player Panel ===== */
.player-panel {
-fx-background-color: #FFFFFF;
-fx-border-color: #E0E0E0;
-fx-border-width: 0 1 0 0;
}
/* ===== Album Cover ===== */
.cover-container {
-fx-background-color: #EEEEEE;
-fx-background-radius: 12px;
-fx-min-width: 160px;
-fx-min-height: 160px;
-fx-max-width: 160px;
-fx-max-height: 160px;
}
.album-cover {
-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.15), 10, 0, 0, 2);
}
/* ===== Song Info ===== */
.song-title {
-fx-font-size: 18px;
-fx-font-weight: bold;
-fx-text-fill: #212121;
}
.artist-name {
-fx-font-size: 13px;
-fx-text-fill: #757575;
}
/* ===== Progress Slider ===== */
.progress-slider .track {
-fx-background-color: #E0E0E0;
-fx-pref-height: 4px;
}
.progress-slider .thumb {
-fx-background-color: #212121;
-fx-pref-width: 12px;
-fx-pref-height: 12px;
-fx-background-radius: 6px;
}
.progress-slider:focused .thumb {
-fx-effect: none;
}
/* ===== Time Labels ===== */
.time-label {
-fx-font-size: 11px;
-fx-text-fill: #9E9E9E;
}
/* ===== Control Buttons ===== */
.control-btn {
-fx-background-color: transparent;
-fx-font-size: 16px;
-fx-cursor: hand;
-fx-padding: 6 10 6 10;
-fx-background-radius: 50%;
}
.control-btn:hover {
-fx-background-color: #F0F0F0;
}
.control-btn:pressed {
-fx-background-color: #E0E0E0;
}
/* ===== Play Button ===== */
.play-btn {
-fx-background-color: #212121;
-fx-text-fill: #FFFFFF;
-fx-font-size: 18px;
-fx-cursor: hand;
-fx-min-width: 48px;
-fx-min-height: 48px;
-fx-background-radius: 50%;
-fx-padding: 0;
}
.play-btn:hover {
-fx-background-color: #424242;
}
.play-btn:pressed {
-fx-background-color: #616161;
}
/* ===== Active state for shuffle/repeat ===== */
.active-btn {
-fx-text-fill: #1DB954;
}
/* ===== Volume Slider ===== */
.volume-slider .track {
-fx-background-color: #E0E0E0;
-fx-pref-height: 3px;
}
.volume-slider .thumb {
-fx-background-color: #212121;
-fx-pref-width: 10px;
-fx-pref-height: 10px;
-fx-background-radius: 5px;
}
.volume-slider:focused .thumb {
-fx-effect: none;
}
/* ===== Playlist Panel ===== */
.playlist-panel {
-fx-background-color: #FAFAFA;
}
/* ===== ListView ===== */
.playlist {
-fx-background-color: transparent;
-fx-border-color: transparent;
-fx-padding: 0;
}
.playlist .list-cell {
-fx-background-color: transparent;
-fx-font-size: 13px;
-fx-text-fill: #424242;
-fx-padding: 10 12 10 12;
-fx-border-color: transparent transparent #F0F0F0 transparent;
-fx-border-width: 1px;
-fx-cursor: hand;
}
.playlist .list-cell:hover {
-fx-background-color: #F0F0F0;
}
.playlist .list-cell:selected {
-fx-background-color: #EEEEEE;
-fx-text-fill: #212121;
-fx-font-weight: bold;
}
/* Scrollbar */
.playlist .scroll-bar:vertical {
-fx-pref-width: 6px;
}
.playlist .scroll-bar:vertical .thumb {
-fx-background-color: #BDBDBD;
-fx-background-radius: 3px;
}
.playlist .scroll-bar:vertical .track {
-fx-background-color: transparent;
}
.playlist .scroll-bar:vertical .increment-button,
.playlist .scroll-bar:vertical .decrement-button {
-fx-pref-height: 0;
-fx-opacity: 0;
}
/* ===== Tab Bar ===== */
.tab-bar {
-fx-background-color: #F0F0F0;
-fx-border-color: transparent transparent #E0E0E0 transparent;
-fx-border-width: 1px;
}
.tab-btn {
-fx-background-color: transparent;
-fx-font-size: 13px;
-fx-text-fill: #757575;
-fx-cursor: hand;
-fx-padding: 12 0 12 0;
-fx-background-radius: 0;
-fx-border-color: transparent;
}
.tab-btn:hover {
-fx-background-color: #E8E8E8;
-fx-text-fill: #424242;
}
.tab-active {
-fx-text-fill: #212121;
-fx-font-weight: bold;
-fx-border-color: transparent transparent #212121 transparent;
-fx-border-width: 0 0 2 0;
}
/* ===== Tab Content ===== */
.tab-content {
-fx-background-color: #FAFAFA;
}
.tab-header {
-fx-font-size: 13px;
-fx-font-weight: bold;
-fx-text-fill: #424242;
}
/* ===== Add Button ===== */
.add-btn {
-fx-background-color: #212121;
-fx-text-fill: #FFFFFF;
-fx-font-size: 12px;
-fx-cursor: hand;
-fx-padding: 6 12 6 12;
-fx-background-radius: 6px;
}
.add-btn:hover {
-fx-background-color: #424242;
}
/* ===== Back Button ===== */
.back-btn {
-fx-background-color: transparent;
-fx-text-fill: #757575;
-fx-font-size: 12px;
-fx-cursor: hand;
-fx-padding: 4 8 4 8;
-fx-background-radius: 6px;
}
.back-btn:hover {
-fx-background-color: #EEEEEE;
-fx-text-fill: #212121;
}
/* ===== Theme Button ===== */
.theme-btn {
-fx-background-color: transparent;
-fx-font-size: 16px;
-fx-cursor: hand;
-fx-padding: 4 8 4 8;
-fx-background-radius: 6px;
}
.theme-btn:hover {
-fx-background-color: #F0F0F0;
}
/* ========================================= */
/* ===== Dark Mode ===== */
/* ========================================= */
.dark .root {
-fx-background-color: #121212;
}
.dark .player-panel {
-fx-background-color: #1E1E1E;
-fx-border-color: #333333;
}
.dark .playlist-panel {
-fx-background-color: #121212;
}
.dark .cover-container {
-fx-background-color: #2A2A2A;
}
.dark .song-title {
-fx-text-fill: #FFFFFF;
}
.dark .artist-name {
-fx-text-fill: #AAAAAA;
}
.dark .time-label {
-fx-text-fill: #666666;
}
.dark .progress-slider .track {
-fx-background-color: #333333;
}
.dark .progress-slider .thumb {
-fx-background-color: #FFFFFF;
}
.dark .volume-slider .track {
-fx-background-color: #333333;
}
.dark .volume-slider .thumb {
-fx-background-color: #FFFFFF;
}
.dark .play-btn {
-fx-background-color: #FFFFFF;
-fx-text-fill: #000000;
}
.dark .play-btn:hover {
-fx-background-color: #DDDDDD;
}
.dark .control-btn {
-fx-text-fill: #FFFFFF;
}
.dark .control-btn:hover {
-fx-background-color: #2A2A2A;
}
.dark .theme-btn:hover {
-fx-background-color: #2A2A2A;
}
.dark .tab-bar {
-fx-background-color: #1A1A1A;
-fx-border-color: #333333;
}
.dark .tab-btn {
-fx-text-fill: #9E9E9E;
}
.dark .tab-active {
-fx-text-fill: #FFFFFF;
-fx-border-color: transparent transparent #FFFFFF transparent;
}
.dark .tab-content {
-fx-background-color: #121212;
}
.dark .tab-header {
-fx-text-fill: #FFFFFF;
}
.dark .playlist .list-cell {
-fx-background-color: transparent;
-fx-text-fill: #CCCCCC;
-fx-border-color: transparent transparent #222222 transparent;
}
.dark .playlist .list-cell:hover {
-fx-background-color: #1E1E1E;
}
.dark .playlist .list-cell:selected {
-fx-background-color: #2A2A2A;
-fx-text-fill: #FFFFFF;
}
.dark .playlist .scroll-bar:vertical .thumb {
-fx-background-color: #444444;
}
.dark .add-btn {
-fx-background-color: #FFFFFF;
-fx-text-fill: #000000;
}
.dark .add-btn:hover {
-fx-background-color: #DDDDDD;
}
.dark .back-btn {
-fx-text-fill: #9E9E9E;
}
.dark .back-btn:hover {
-fx-background-color: #2A2A2A;
-fx-text-fill: #FFFFFF;
}