Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8185543f88 | ||
|
|
dc9dd22920 | ||
|
|
ee9ef1ec90 | ||
|
|
0e4d96b3a7 | ||
|
|
90d8487c6e | ||
|
|
517414cdf4 | ||
|
|
a5e70479ed | ||
|
|
2099a1a9f3 | ||
|
|
305aef7473 |
Generated
+1
-1
@@ -8,7 +8,7 @@
|
||||
</list>
|
||||
</option>
|
||||
</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" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+7
@@ -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>
|
||||
@@ -1,72 +1,123 @@
|
||||
# Seventh Assignment — JavaFX Music Player Project
|
||||
# 🎵 Music Player
|
||||
|
||||
This is a starter JavaFX application for a simple Music Player interface. Your goal is to design the UI and connect it to the provided controller logic to create basic music player interactions.
|
||||
A minimal JavaFX music player with playlist support and dark mode.
|
||||
|
||||
## 📋 Assignment Objectives
|
||||
---
|
||||
|
||||
### Complete the following features:
|
||||
## Features
|
||||
|
||||
**1. Design the Music Player UI**
|
||||
- Play, pause, skip, and seek through songs
|
||||
- Shuffle and repeat modes
|
||||
- Volume control
|
||||
- Album art display (loaded from file metadata)
|
||||
- Library tab — manage all your songs
|
||||
- Playlists tab — create custom playlists and add songs to them
|
||||
- Dark mode toggle
|
||||
- Right-click on any song to add it to a playlist
|
||||
|
||||
Create a layout for a simple music player interface. Your UI should display at least **three songs**, each containing:
|
||||
---
|
||||
|
||||
- Song title
|
||||
- Artist name
|
||||
- Duration
|
||||
- A **Play** button
|
||||
- An **Add to Playlist** button
|
||||
## Project Structure
|
||||
|
||||
**2. Connect UI Components to the Controller**
|
||||
```
|
||||
MusicPlayer/
|
||||
├── src/
|
||||
│ └── main/
|
||||
│ ├── java/sbu/javafx/
|
||||
│ │ ├── MainApp.java
|
||||
│ │ ├── MusicController.java
|
||||
│ │ ├── Song.java
|
||||
│ │ └── Playlist.java
|
||||
│ └── resources/sbu/javafx/
|
||||
│ ├── player.fxml
|
||||
│ └── style.css
|
||||
└── pom.xml
|
||||
```
|
||||
|
||||
Link your UI elements to the provided `MusicController` class:
|
||||
---
|
||||
|
||||
- Connect buttons to their corresponding controller methods.
|
||||
- Ensure that clicking buttons triggers the correct actions.
|
||||
## Requirements
|
||||
|
||||
**3. Display the Currently Playing Song**
|
||||
- Java 17+
|
||||
- JavaFX 17+
|
||||
- JAudioTagger 3.0.1 (for reading song metadata)
|
||||
|
||||
When a user clicks a **Play** button:
|
||||
---
|
||||
|
||||
- Update a label in the UI showing the currently playing song (e.g., "Now Playing: Song Title").
|
||||
## Dependencies (pom.xml)
|
||||
|
||||
**4. Implement a Playlist**
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-controls</artifactId>
|
||||
<version>17</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-fxml</artifactId>
|
||||
<version>17</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-media</artifactId>
|
||||
<version>17</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.jthink</groupId>
|
||||
<artifactId>jaudiotagger</artifactId>
|
||||
<version>3.0.1</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
When the **Add to Playlist** button is clicked:
|
||||
---
|
||||
|
||||
- Add the selected song to a playlist data structure (e.g., an `ArrayList`).
|
||||
## How to Run
|
||||
|
||||
**5. Create a Playlist Page**
|
||||
```bash
|
||||
mvn clean javafx:run
|
||||
```
|
||||
|
||||
Add a **"View Playlist"** button to the main UI:
|
||||
---
|
||||
|
||||
- When clicked, switch to a **new scene** that displays the playlist.
|
||||
- Use `stage.setScene(...)` to switch between scenes.
|
||||
- Show the playlist content using a `ListView` or any other layout.
|
||||
## How to Use
|
||||
|
||||
### Adding Songs
|
||||
1. Go to the **Library** tab
|
||||
2. Click **+ Add Song**
|
||||
3. Select one or more audio files (mp3, wav, aac, flac)
|
||||
4. Songs will appear in the library with their metadata
|
||||
|
||||
## 🎨 UI Design Reference
|
||||
### Playing Songs
|
||||
- Double-click any song in the library to start playing
|
||||
- Use the control buttons to play/pause, skip, or go back
|
||||
- Drag the progress bar to seek
|
||||
|
||||
To help you get started, refer to the layout below. Your application should have a similar, clean, and simple structure:
|
||||
### Playlists
|
||||
1. Go to the **Playlists** tab
|
||||
2. Click **+ New** to create a playlist
|
||||
3. Go back to the Library tab
|
||||
4. Right-click any song and select **Add to Playlist**
|
||||
5. Double-click a playlist to open it
|
||||
6. Double-click a song inside the playlist to play it
|
||||
|
||||

|
||||
### Dark Mode
|
||||
- Click the 🌙 button on the top right of the player panel to toggle dark mode
|
||||
|
||||
*(Note: This is a wireframe. You are encouraged to keep your implementation simple and focus on functionality.)*
|
||||
---
|
||||
|
||||
## Supported Formats
|
||||
|
||||
## ⭐ Bonus Tasks (Optional)
|
||||
| Format | Supported |
|
||||
|--------|-----------|
|
||||
| MP3 | ✅ |
|
||||
| WAV | ✅ |
|
||||
| AAC | ✅ |
|
||||
| FLAC | ✅ |
|
||||
|
||||
**The core requirements above do not include actual audio playback.** The following tasks are optional (Bonus) and should only be attempted if you have completed the core requirements.
|
||||
|
||||
**1. Play Functionality**
|
||||
|
||||
- Implement actual audio playback using JavaFX `Media` and `MediaPlayer` classes.
|
||||
|
||||
**2. Add Visual Feedback**
|
||||
|
||||
- When a song is playing, change the row style or update the **Play** button text to "Playing".
|
||||
|
||||
**3. Dark Mode / Light Mode**
|
||||
|
||||
- Add a toggle button to switch between two CSS stylesheets (e.g., `dark.css` and `light.css`) at runtime.
|
||||
|
||||
---
|
||||
|
||||
## Preview
|
||||

|
||||

|
||||

|
||||

|
||||
@@ -11,32 +11,50 @@
|
||||
|
||||
<properties>
|
||||
<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>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-controls</artifactId>
|
||||
<version>21</version>
|
||||
<version>${javafx.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-fxml</artifactId>
|
||||
<version>21</version>
|
||||
<version>${javafx.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-media</artifactId>
|
||||
<version>${javafx.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency> </dependencies>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>net.jthink</groupId>
|
||||
<artifactId>jaudiotagger</artifactId>
|
||||
<version>3.0.1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
@@ -45,30 +63,18 @@
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
<configuration>
|
||||
<source>23</source>
|
||||
<target>23</target>
|
||||
<release>${maven.compiler.release}</release>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-maven-plugin</artifactId>
|
||||
<version>0.0.8</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<!-- Default configuration for running with: mvn clean javafx:run -->
|
||||
<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>
|
||||
<configuration>
|
||||
<mainClass>sbu.javafx/sbu.javafx.MusicApp</mainClass>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
</project>
|
||||
|
||||
@@ -4,5 +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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,21 +3,24 @@ package sbu.javafx;
|
||||
import javafx.application.Application;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.image.Image;
|
||||
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.getIcons().add(new Image("/sbu/images/default-cover.png"));
|
||||
stage.setScene(scene);
|
||||
//TODO: add icon to the stage
|
||||
stage.setResizable(false);
|
||||
stage.show();
|
||||
}
|
||||
|
||||
}
|
||||
public static void main(String[] args) {
|
||||
launch(args);
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,482 @@
|
||||
package sbu.javafx;
|
||||
|
||||
import javafx.fxml.FXML;
|
||||
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
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.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.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 {
|
||||
|
||||
// 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
|
||||
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.
|
||||
@FXML
|
||||
public void handleShuffle() {
|
||||
isShuffle = !isShuffle;
|
||||
if(isShuffle) shuffleBtn.getStyleClass().add("active-btn");
|
||||
else shuffleBtn.getStyleClass().remove("active-btn");
|
||||
}
|
||||
|
||||
public void openPlaylistWindow() {
|
||||
//Create a new stage and show the playlist UI.
|
||||
@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);
|
||||
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("🌙");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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: 112 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
@@ -1,8 +1,200 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?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>
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user