diff --git a/src/main/java/org/example/App.java b/src/main/java/org/example/App.java index b1ff66f3..44da0bd3 100644 --- a/src/main/java/org/example/App.java +++ b/src/main/java/org/example/App.java @@ -2,7 +2,23 @@ import javafx.application.Application; +/** + * Application entry point for the myPod application. + * + *
This class contains the {@code main} method and is responsible for + * bootstrapping the JavaFX runtime. It delegates control to the + * {@link MyPod} JavaFX {@link Application} implementation.
+ * + *No application logic is handled here; this class exists solely to + * provide a clean and explicit startup entry point.
+ */ public class App { + + /** + * Launches the JavaFX application. + * + * @param args command-line arguments passed to the application + */ public static void main(String[] args) { Application.launch(MyPod.class, args); } diff --git a/src/main/java/org/example/DatabaseInitializer.java b/src/main/java/org/example/DatabaseInitializer.java index 923f2291..c98771f6 100644 --- a/src/main/java/org/example/DatabaseInitializer.java +++ b/src/main/java/org/example/DatabaseInitializer.java @@ -10,6 +10,19 @@ import java.util.List; +/** + * Initializes and populates the application database. + * + *This class is responsible for:
+ *The initializer is designed to be idempotent: data is only inserted + * if the database is empty or missing required entities.
+ */ public class DatabaseInitializer { private static final Logger logger = LoggerFactory.getLogger(DatabaseInitializer.class); @@ -21,6 +34,15 @@ public class DatabaseInitializer { private final ArtistRepository artistRepo; private final PlaylistRepository playlistRepo; + /** + * Creates a new database initializer. + * + * @param apiClient client used to fetch data from the iTunes API + * @param songRepo repository for {@link Song} entities + * @param albumRepo repository for {@link Album} entities + * @param artistRepo repository for {@link Artist} entities + * @param playlistRepo repository for {@link Playlist} entities + */ public DatabaseInitializer(ItunesApiClient apiClient, SongRepository songRepo, AlbumRepository albumRepo, ArtistRepository artistRepo, PlaylistRepository playlistRepo) { this.apiClient = apiClient; this.songRepo = songRepo; @@ -29,9 +51,21 @@ public DatabaseInitializer(ItunesApiClient apiClient, SongRepository songRepo, A this.playlistRepo = playlistRepo; } + /** + * Initializes the database with music data and default playlists. + * + *If the song table is empty, a predefined set of artist searches + * is executed against the iTunes API. The resulting artists, albums, + * and songs are persisted while avoiding duplicates.
+ * + *The method also ensures that required default playlists + * ("Library" and "Favorites") exist.
+ * + * @throws RuntimeException if data fetching or persistence fails + */ public void init() { - // Check if there is data already, fill if empty - if (songRepo.count() == 0) { + // Check if database is populated, populate if empty + if (songRepo.count() == 0) { // Limited artist set due to project scope ListThis class programmatically configures Hibernate without relying + * on a {@code persistence.xml} file. All JPA entities are discovered + * automatically via classpath scanning.
+ * + *The factory supports additional configuration properties that + * can be supplied at runtime.
+ */ public class EntityManagerFactoryProvider { + /** + * Creates and configures an {@link EntityManagerFactory}. + * + *The method scans the specified entity package for classes + * annotated with {@link jakarta.persistence.Entity}, registers them + * with Hibernate, and applies the provided JDBC and Hibernate + * configuration properties.
+ * + * @param jdbcUrl JDBC connection URL + * @param username database username + * @param password database password + * @param extraProps additional Hibernate configuration properties + * @return a fully initialized {@link EntityManagerFactory} + */ public static EntityManagerFactory create( String jdbcUrl, String username, @@ -32,6 +56,15 @@ public static EntityManagerFactory create( return cfg.createEntityManagerFactory(); } + /** + * Scans the classpath for JPA entity classes. + * + *All classes annotated with {@link Entity} within the specified + * package are discovered and loaded.
+ * + * @param pkg base package to scan + * @return list of entity classes + */ private static ListThis class is responsible for executing HTTP requests against + * Apple's public iTunes Search API and mapping the results into + * {@link ItunesDTO} objects.
+ * + *It performs basic response validation and result filtering + * to ensure that only relevant data is returned.
+ */ +public class ItunesApiClient { private static final Logger logger = LoggerFactory.getLogger(ItunesApiClient.class); - private final HttpClient client; private final ObjectMapper mapper; + /** + * Creates a new iTunes API client. + * + *Initializes an {@link HttpClient} and configures a Jackson + * {@link ObjectMapper} with Java Time support.
+ */ public ItunesApiClient() { this.client = HttpClient.newHttpClient(); this.mapper = new ObjectMapper(); mapper.registerModule(new JavaTimeModule()); } + /** + * Searches for songs by artist name using the iTunes Search API. + * + *The search results are filtered so that only songs whose + * artist name matches the provided term (after normalization) + * are returned.
+ * + * @param term artist search term + * @return list of matching {@link ItunesDTO} objects + * @throws Exception if the HTTP request or JSON parsing fails + */ public ListThe normalization process converts the string to lowercase, + * collapses whitespace and plus characters, and trims leading + * and trailing spaces.
+ * + * @param s input string + * @return normalized string, or an empty string if {@code s} is {@code null} + */ public String normalize(String s) { if (s == null) { return ""; diff --git a/src/main/java/org/example/ItunesDTO.java b/src/main/java/org/example/ItunesDTO.java index 96f5849f..5452c2f9 100644 --- a/src/main/java/org/example/ItunesDTO.java +++ b/src/main/java/org/example/ItunesDTO.java @@ -5,6 +5,14 @@ import java.net.URL; import java.time.LocalDate; +/** + * Data Transfer Object representing a single song result + * returned from the iTunes Search API. + * + *This record is designed to map directly to the JSON structure + * returned by the API and is used as an intermediate representation + * before converting the data into domain entities.
+ */ @JsonIgnoreProperties(ignoreUnknown = true) public record ItunesDTO(Long artistId, Long collectionId, @@ -20,6 +28,11 @@ public record ItunesDTO(Long artistId, URL artworkUrl100, String previewUrl) { + /** + * Extracts the release year from the release date. + * + * @return release year, or {@code 0} if the release date is unknown + */ public int releaseYear() { return releaseDate != null ? releaseDate.getYear() : 0; } diff --git a/src/main/java/org/example/ItunesPlayList.java b/src/main/java/org/example/ItunesPlayList.java index c0692afa..08d65760 100644 --- a/src/main/java/org/example/ItunesPlayList.java +++ b/src/main/java/org/example/ItunesPlayList.java @@ -21,95 +21,142 @@ import java.util.List; /** - * Huvudklass för GUI:t. Hanterar visning av bibliotek, spellistor och sökning. + * Main JavaFX UI class for playlist and library management. + * + *+ * This class is responsible for constructing and displaying the graphical + * user interface, including playlist navigation, song tables, search + * functionality, and context menus for playlist operations. + *
+ * + *+ * The UI communicates with the persistence layer exclusively through + * {@link PlaylistRepository}. + *
*/ public class ItunesPlayList { - private static final Logger logger = LoggerFactory.getLogger(ItunesPlayList.class); - private final PlaylistRepository pri; - private Runnable onUpdateCallback; + /** + * Creates a new {@code ItunesPlayList}. + * + * @param playlistRepository repository used for playlist persistence operations + */ + public ItunesPlayList(PlaylistRepository playlistRepository) { + this.pri = playlistRepository; + } + + /** + * Registers a callback that is invoked when the UI state changes and + * external components need to refresh. + * + * @param callback the callback to invoke on update + */ public void setOnUpdate(Runnable callback) { this.onUpdateCallback = callback; } + /** + * Triggers the registered update callback, if present. + */ private void refresh() { if (onUpdateCallback != null) { onUpdateCallback.run(); } } - public ItunesPlayList(PlaylistRepository playlistRepository) { - this.pri = playlistRepository; - } - - // --- DATAMODELL --- + // --------------------------------------------------------------------- + // Data model + // --------------------------------------------------------------------- - // En lista med alla playlist som finns i databasen + /** + * Observable list containing all playlists loaded from the database. + */ private final ObservableList+ * This method initializes all UI components, loads playlists asynchronously, + * and wires event handlers for user interaction. + *
*/ public void showLibrary() { - Stage stage = new Stage(); - // Lägg till existerande playlist i vår lokala lista (non-blocking) + // Load playlists asynchronously to avoid blocking the JavaFX thread new Thread(() -> { - List+ * System playlists such as the main library and favorites cannot be renamed. + *
*/ private void renameSelectedPlaylist() { Playlist sel = sourceList.getSelectionModel().getSelectedItem(); @@ -452,7 +513,7 @@ private void renameSelectedPlaylist() { sel.setName(newName); sourceList.refresh(); } catch (IllegalStateException ex) { - logger.error("renameSelectedPlaylist: failed to rename", ex); + logger.error("renameSelectedPlaylist: failed to rename ", ex); new Alert(Alert.AlertType.ERROR, "Failed to rename: " + ex.getMessage()).showAndWait(); } } @@ -461,49 +522,68 @@ private void renameSelectedPlaylist() { } /** - * Tar bort vald spellista (men tillåter inte att man tar bort "Bibliotek" eller "Favoriter"). + * Deletes the currently selected playlist. + * + *+ * System playlists (e.g. Library and Favorites) cannot be deleted. + *
*/ private void deleteSelectedPlaylist() { Playlist sel = sourceList.getSelectionModel().getSelectedItem(); if (sel != null && sel.getId() != null && !sel.getId().equals(1L) && !sel.getId().equals(2L)) { - pri.deletePlaylist(sel); - allPlaylistList.remove(sel); - - refresh(); + try { + pri.deletePlaylist(sel); + allPlaylistList.remove(sel); + refresh(); + } catch (Exception ex) { + logger.error("deleteSelectedPlaylist: failed to delete", ex); + new Alert(Alert.AlertType.ERROR, "Failed to delete playlist: " + ex.getMessage()).showAndWait(); + } } } /** - * Tar bort vald låt från den aktiva spellistan (ej från huvudbiblioteket "Bibliotek"). + * Removes the selected song from the currently active playlist. + * + *+ * Songs cannot be removed directly from the main library. + *
*/ private void removeSelectedSong() { Song sel = songTable.getSelectionModel().getSelectedItem(); Playlist list = sourceList.getSelectionModel().getSelectedItem(); - // Skydd: Man får inte ta bort låtar direkt från biblioteket i denna vy - if (sel != null && list != null && list.getId() != null && !list.getId().equals(1L)) { - pri.removeSong(list, sel); - list.getSongs().remove(sel); - songTable.getItems().remove(sel); - refresh(); + // You cannot remove song from Library + if (sel != null && list != null && list.getId() != null && !list.getId().equals(1L)) { + try { + pri.removeSong(list, sel); + list.getSongs().remove(sel); + songTable.getItems().remove(sel); + refresh(); + } catch (Exception ex) { + logger.error("removeSelectedSong: failed to remove", ex); + new Alert(Alert.AlertType.ERROR, "Failed to remove song: " + ex.getMessage()).showAndWait(); + } } } /** - * Visar en popup-meny för att lägga till vald låt i en annan spellista. + * Displays a context menu allowing the user to add the selected song + * to another playlist. + * + * @param anchor the UI element used as the menu anchor */ private void addSelectedSong(Button anchor) { Song sel = songTable.getSelectionModel().getSelectedItem(); - if (sel == null) return; // Ingen låt vald + if (sel == null) return; ContextMenu menu = new ContextMenu(); for (Playlist pl : allPlaylistList) { if (pl.getId() != null && pl.getId().equals(1L)) - continue; // Man kan inte lägga till i "Bibliotek" (det är källan) + continue; // You cannot add song to Library MenuItem itm = new MenuItem(pl.getName()); itm.setOnAction(e -> { - // Om låten inte redan finns i listan, lägg till den if (!pri.isSongInPlaylist(pl, sel)) { try { pri.addSong(pl, sel); @@ -517,7 +597,7 @@ private void addSelectedSong(Button anchor) { }); menu.getItems().add(itm); } - // Visa menyn vid knappen + var bounds = anchor.localToScreen(anchor.getBoundsInLocal()); menu.show(anchor, bounds.getMinX(), bounds.getMaxY()); } diff --git a/src/main/java/org/example/MyPod.java b/src/main/java/org/example/MyPod.java index 202fe057..f1a5c198 100644 --- a/src/main/java/org/example/MyPod.java +++ b/src/main/java/org/example/MyPod.java @@ -47,87 +47,116 @@ import java.util.List; /** - * Huvudklassen för applikationen "MyPod". - * Denna klass bygger upp GUI:t (simulerar en iPod) och hanterar navigering. + * Main application class for {@code MyPod}. + *+ * This class is responsible for: + *
+ * {@link ObservableList} is used so JavaFX can react to changes if needed.
+ */
private final ObservableList
+ * Supported keys:
+ *
+ * This method clears the current screen, resets navigation state and
+ * populates the view based on the selected screen name.
+ *
+ * @param screenName the identifier of the screen to display
*/
private void showScreen(String screenName) {
- screenContent.getChildren().clear(); // Rensa skärmen
- menuLabels.clear(); // Rensa listan med menyval
- isMainMenu = false; // Vi är inte i huvudmenyn längre
- selectedIndex = 0; // Återställ markör till toppen
+ screenContent.getChildren().clear();
+ menuLabels.clear();
+ isMainMenu = false;
+ selectedIndex = 0; // Reset marker to top
currentScreenName = screenName;
- // Rubrik
+
Label screenTitle = new Label(screenName);
screenTitle.getStyleClass().add("screen-title");
screenContent.getChildren().add(screenTitle);
- // Fyll på med rätt data beroende på vad användaren valde
switch (screenName) {
case "Songs" -> {
if (songs != null && !songs.isEmpty()) {
@@ -424,16 +461,19 @@ private void showScreen(String screenName) {
} else addMenuItem("No playlists found");
}
}
- updateMenu(); // Uppdatera så första valet är markerat
+
+ updateMenu();
}
/**
- * Hjälpmetod för att lägga till en rad i listan på skärmen.
+ * Adds a static text-based menu entry to the current screen.
+ *
+ * @param text the text to display in the menu
*/
private void addMenuItem(String text) {
ObjectLabel stringLabel = new ObjectLabel(new Label(text), null);
stringLabel.label().getStyleClass().add("menu-item");
- stringLabel.label().setMaxWidth(Double.MAX_VALUE); // Gör att raden fyller hela bredden (snyggare markering)
+ stringLabel.label().setMaxWidth(Double.MAX_VALUE);
if ("Edit Playlists".equals(text)) {
stringLabel.label().setStyle("-fx-font-weight: bold; -fx-underline: true;");
@@ -444,19 +484,24 @@ private void addMenuItem(String text) {
}
/**
- * Hjälpmetod för att lägga till en rad i listan på skärmen som pekar på ett object
+ * Adds a menu entry that represents a domain object.
+ *
+ * The object's name is displayed and the object itself is stored
+ * for later selection handling.
+ *
+ * @param object the domain object associated with this menu entry
*/
private void addMenuItem(DBObject object) {
ObjectLabel objectLabel = new ObjectLabel(new Label(object.getName()), object);
objectLabel.label().getStyleClass().add("menu-item");
- objectLabel.label().setMaxWidth(Double.MAX_VALUE); // Gör att raden fyller hela bredden (snyggare markering)
+ objectLabel.label().setMaxWidth(Double.MAX_VALUE);
menuLabels.add(objectLabel);
screenContent.getChildren().add(objectLabel.label());
}
/**
- * Visar huvudmenyn igen.
+ * Displays the main menu and resets all navigation state.
*/
private void showMainMenu() {
screenContent.getChildren().clear();
@@ -472,15 +517,18 @@ private void showMainMenu() {
for (String item : mainMenu) {
addMenuItem(item);
}
- updateMenu();
+ updateMenu();
}
/**
- * Vad som händer när man trycker Enter på en låt/artist.
+ * Handles user selection when the ENTER key is pressed.
+ *
+ * Behavior depends on the currently active screen and the selected item.
+ *
+ * @param selection the selected menu entry
*/
private void handleSelection(ObjectLabel selection) {
- // Här kan du lägga till logik för att spela låten eller öppna albumet
System.out.println("User selected: " + selection.getText());
if ("Artists".equals(currentScreenName)) {
@@ -494,16 +542,13 @@ private void handleSelection(ObjectLabel selection) {
openMusicPlayer();
return;
}
-
if (selection.object() == null) {
return;
}
-
Playlist selectedPlaylist = playlists.stream()
.filter(p -> p.getId()
.equals(selection.object().getId()))
.findFirst().orElse(null);
-
if (selectedPlaylist != null) {
openPlaylist(selectedPlaylist);
}
@@ -515,6 +560,11 @@ private void handleSelection(ObjectLabel selection) {
}
}
+ /**
+ * Opens a playlist and displays its contained songs.
+ *
+ * @param p the playlist to open
+ */
private void openPlaylist(Playlist p) {
Playlist updatedPlaylist = playlistRepo.findById(p.getId());
@@ -542,15 +592,16 @@ private void openPlaylist(Playlist p) {
} else {
addMenuItem("No songs found");
}
- updateMenu();
+ updateMenu();
}
/**
- * Öppnar det externa fönstret "ItunesPlayList".
+ * Opens the external playlist management window.
+ *
+ * When playlists are modified, the current view is refreshed accordingly.
*/
private void openMusicPlayer() {
-
if (this.playlists == null) {
this.playlists = new ArrayList<>();
}
@@ -582,11 +633,15 @@ private void openMusicPlayer() {
itunesPlayList.showLibrary();
}
+ /**
+ * Displays all albums belonging to the selected artist.
+ *
+ * @param selection the selected artist entry
+ */
private void showArtistAlbums(ObjectLabel selection) {
screenContent.getChildren().clear();
menuLabels.clear();
selectedIndex = 0;
-
currentScreenName = "ArtistAlbums";
Label titleLabel = new Label(selection.getText());
@@ -611,12 +666,15 @@ private void showArtistAlbums(ObjectLabel selection) {
updateMenu();
}
-
+ /**
+ * Displays all songs belonging to the selected album.
+ *
+ * @param selection the selected album entry
+ */
private void showAlbumSongs(ObjectLabel selection) {
screenContent.getChildren().clear();
menuLabels.clear();
selectedIndex = 0;
-
currentScreenName = "AlbumSongs";
Label titleLabel = new Label(selection.getText());
@@ -641,28 +699,30 @@ private void showAlbumSongs(ObjectLabel selection) {
updateMenu();
}
-
+ /**
+ * Displays the "Now Playing" screen and starts playback of a song preview.
+ *
+ * @param selection the selected song entry
+ */
private void showNowPlaying(ObjectLabel selection) {
screenContent.getChildren().clear();
menuLabels.clear();
selectedIndex = 0;
currentScreenName = "NowPlaying";
- Song currentSong = null;
- if (songs != null && selection.object() != null) {
- currentSong = songs.stream()
- .filter(s -> s.getId().equals(selection.object().getId()))
- .findFirst()
- .orElse(null);
+ if (selection.object() == null) {
+ return;
}
- // Skapa elementen och tilldela klasser
+ Song currentSong = (Song) selection.object();
+
+ // Header
Label header = new Label("▶ NOW PLAYING");
header.getStyleClass().add("now-playing-header");
+ // Album art
ImageView albumArtView = new ImageView();
-
- if (currentSong != null && currentSong.getAlbum() != null) {
+ if (currentSong.getAlbum() != null) {
Image cover = currentSong.getAlbum().getCoverImage();
if (cover != null) {
albumArtView.setImage(cover);
@@ -679,12 +739,13 @@ private void showNowPlaying(ObjectLabel selection) {
-fx-background-color: white;
""");
- Label titleLabel = new Label(selection.getText());
+ // Song title
+ Label titleLabel = new Label(currentSong.getName());
titleLabel.getStyleClass().add("now-playing-title");
titleLabel.setWrapText(true);
String artistName;
- if (currentSong != null && currentSong.getAlbum() != null && currentSong.getAlbum().getArtist() != null) {
+ if (currentSong.getAlbum() != null && currentSong.getAlbum().getArtist() != null) {
artistName = currentSong.getAlbum().getArtist().getName();
} else {
artistName = "Unknown Artist";
@@ -693,7 +754,7 @@ private void showNowPlaying(ObjectLabel selection) {
artistLabel.getStyleClass().add("now-playing-artist");
String albumName;
- if (currentSong != null && currentSong.getAlbum() != null) {
+ if (currentSong.getAlbum() != null) {
albumName = currentSong.getAlbum().getName();
} else {
albumName = "Unknown Album";
@@ -701,10 +762,11 @@ private void showNowPlaying(ObjectLabel selection) {
Label albumLabel = new Label(albumName);
albumLabel.getStyleClass().add("now-playing-album");
+ // Progress bar
progressBar = new ProgressBar(0);
progressBar.getStyleClass().add("ipod-progress-bar");
- // --- Volume overlay (positioned on top of progress bar) ---
+ // Volume overlay (positioned on top of progress bar)
ensureVolumeBarExists();
volumeBar.setOpacity(0); // start hidden
@@ -712,22 +774,26 @@ private void showNowPlaying(ObjectLabel selection) {
StackPane progressStack = new StackPane(progressBar, volumeBar);
progressStack.setAlignment(Pos.CENTER);
- // Layout-behållaren
VBox layout = new VBox(3);
layout.getStyleClass().add("now-playing-container");
- layout.getChildren().addAll(header, albumArtView, titleLabel, artistLabel, albumLabel, progressStack);
layout.setAlignment(Pos.CENTER);
+ layout.getChildren().addAll(header, albumArtView, titleLabel, artistLabel, albumLabel, progressStack);
screenContent.getChildren().add(layout);
- if (currentSong != null) {
- String previewUrl = currentSong.getPreviewUrl();
- if (previewUrl != null && !previewUrl.isBlank()) {
- playPreview(previewUrl);
- }
+ // Play preview
+ String previewUrl = currentSong.getPreviewUrl();
+ if (previewUrl != null && !previewUrl.isBlank()) {
+ playPreview(previewUrl);
}
}
+ /**
+ * Plays a song preview from the given URL and binds playback progress
+ * to the progress bar.
+ *
+ * @param url preview stream URL
+ */
private void playPreview(String url) {
try {
if (mediaPlayer != null) {
@@ -763,42 +829,52 @@ private void playPreview(String url) {
mediaPlayer.play();
} catch (Exception e) {
- logger.error("playPreview: Could not play preview:", e);
+ logger.error("playPreview: Could not play preview: ", e);
}
}
+ /**
+ * Lazily creates the volume overlay progress bar if it does not exist.
+ */
private void ensureVolumeBarExists() {
if (volumeBar == null) {
- volumeBar = new ProgressBar(0.5); // standard volym 50%
+ volumeBar = new ProgressBar(0.5); // Standard volume set to 50%
volumeBar.getStyleClass().add("ipod-volume-bar");
- volumeBar.setOpacity(0); // hidden by default
+ volumeBar.setOpacity(0); // Hidden by default
}
}
/**
- * Initierar databasen och hämtar all data.
- * OBS: Denna körs i en bakgrundstråd (via Task i start-metoden).
+ * Initializes the database and loads all required data into memory.
+ *
+ * This method is executed on a background thread.
*/
private void initializeData() {
try {
- EntityManagerFactory emf = PersistenceManager.getEntityManagerFactory();
DatabaseInitializer initializer = new DatabaseInitializer(apiClient, songRepo, albumRepo, artistRepo, playlistRepo);
- initializer.init(); // Fyll databasen om den är tom
+ initializer.init();
- // Hämta data till minnet
this.songs = songRepo.findAll();
this.artists = artistRepo.findAll();
this.albums = albumRepo.findAll();
this.playlists = playlistRepo.findAll();
} catch (Exception e) {
- logger.error("initializeData: Failed to load data", e);
+ logger.error("initializeData: Failed to load data ", e);
}
}
+ /**
+ * Wrapper record binding a UI label to an optional domain object.
+ *
+ * Used to distinguish static menu items from selectable entities.
+ */
private record ObjectLabel(
Label label,
- DBObject object) { // object is null for static menu items like "Edit Playlists"
+ DBObject object) { // Object is null for static menu items like "Edit Playlists"
+ /**
+ * @return the text displayed by this menu item
+ */
public String getText() {
return label.getText();
}
diff --git a/src/main/java/org/example/PersistenceManager.java b/src/main/java/org/example/PersistenceManager.java
index 783cdcc5..6439c413 100644
--- a/src/main/java/org/example/PersistenceManager.java
+++ b/src/main/java/org/example/PersistenceManager.java
@@ -4,6 +4,15 @@
import java.util.Map;
+/**
+ * Central access point for the application's {@link EntityManagerFactory}.
+ *
+ * This class initializes a single, shared {@link EntityManagerFactory}
+ * instance using {@link EntityManagerFactoryProvider} and exposes it
+ * for use throughout the application. The factory is automatically closed when the JVM shuts down. An {@code Album} is a persistent JPA entity that belongs to an
+ * {@link Artist} and contains one or more {@link Song} entities. It also stores optional album artwork as a binary large object (BLOB),
+ * which is converted to a JavaFX {@link Image} when displayed in the UI. Album instances are typically created from iTunes API data via
+ * {@link #fromDTO(ItunesDTO, Artist)}. This factory method extracts album-related data and attempts
+ * to download and persist album artwork. If artwork cannot be loaded,
+ * {@code null} is stored and a default image is used by the UI. If no cover is stored or if decoding fails, a default placeholder
+ * image bundled with the application is returned. An {@code Artist} is the top-level domain object in the music model and
+ * owns one or more {@link Album} entities. Artists are typically created
+ * from external data sources (e.g. iTunes API) and persisted using JPA. Entity identity is based solely on the database identifier. A {@code Playlist} groups multiple {@link Song} entities using a
+ * many-to-many relationship. Playlists are mutable and can have songs
+ * added or removed dynamically. Entity identity is based solely on the generated database identifier. A {@code Song} belongs to a single {@link Album} and may appear in
+ * zero or more {@link Playlist} entities. Songs are typically created from external metadata sources
+ * such as the iTunes API. Entity identity is based solely on the song identifier. This interface defines the contract for album-related
+ * persistence operations, independent of the underlying
+ * persistence technology. Implementations are responsible for handling database
+ * access and entity lifecycle management.
+ * Provides persistence operations for {@link Album} entities, including
+ * existence checks, basic CRUD functionality, and simple query methods.
+ *
+ * All operations are executed using an {@link EntityManagerFactory}, with
+ * transactions managed internally where required.
+ * This interface defines the contract for artist-related
+ * persistence operations, independent of the underlying
+ * persistence technology. Implementations are responsible for handling database
+ * access and entity lifecycle management.
+ * Handles persistence and retrieval of {@link Artist} entities, providing
+ * basic CRUD operations and simple aggregate queries.
+ * This interface defines the contract for playlist-related
+ * persistence operations, independent of the underlying
+ * persistence technology. Implementations are responsible for handling database
+ * access and entity lifecycle management.
+ * This repository is responsible for all persistence operations related to
+ * {@link Playlist} entities, including creation, retrieval, modification,
+ * and deletion, as well as managing associations between playlists and songs.
+ *
+ * All write operations are executed within transactions. Read operations
+ * use dedicated {@code EntityManager} instances to ensure proper resource handling.
+ *
+ * {@code DISTINCT} is used to avoid duplicate playlists caused by join fetching.
+ * This interface defines the contract for song-related
+ * persistence operations, independent of the underlying
+ * persistence technology. Implementations are responsible for handling database
+ * access and entity lifecycle management.
+ * Provides persistence and query operations for {@link Song} entities,
+ * including lookups by artist and album.
+ *
+ * Certain queries eagerly fetch related album and artist entities to
+ * avoid lazy loading issues in the presentation layer.
+ *
+ * Album and artist associations are eagerly fetched.
+ * This class provides a shared test setup for all repository test cases.
+ * It initializes an in-memory (or test-specific) persistence context and
+ * populates it with a consistent set of test entities. Concrete repository test classes should extend this class in order
+ * to reuse the same test data and repository instances. This ensures that every test is executed against a fresh and
+ * predictable database state. The dataset includes multiple artists, albums, and songs
+ * to support repository queries, filtering, and relationship testing. This class is intended exclusively for use in automated tests. It creates
+ * an in-memory H2 database with a schema lifecycle tailored for testing
+ * (create-drop), ensuring full isolation between test runs. The {@link EntityManagerFactory} is initialized on first access and
+ * reused for the duration of a test suite. It should be explicitly closed
+ * after each test (or test class) to guarantee a clean persistence state. This class is a static utility and should never be instantiated. The factory is configured to use an in-memory H2 database with
+ * automatic schema creation and teardown. The database remains alive
+ * for the duration of the JVM to support multiple transactions per test. This method should be called after each test or test suite to ensure
+ * a clean shutdown of the persistence context and prevent state leakage
+ * between tests.
+ *
*/
private void setupNavigation(Scene scene) {
scene.setOnKeyPressed(event -> {
@@ -281,9 +321,7 @@ private void setupNavigation(Scene scene) {
showScreen("Artists");
} else if ("PlaylistSongs".equals(currentScreenName)) {
showScreen("Playlists");
- }
- // ESCAPE fungerar som "Back"-knapp
- else {
+ } else {
showMainMenu();
}
event.consume();
@@ -291,33 +329,23 @@ private void setupNavigation(Scene scene) {
}
int totalItems = menuLabels.size();
- if (totalItems == 0) return; // Gör inget om listan är tom
+ if (totalItems == 0) return;
int newIndex = selectedIndex;
- // Navigera NER
if (event.getCode() == KeyCode.DOWN) {
- // Modulo (%) gör att om vi trycker ner på sista elementet, hamnar vi på 0 igen.
- newIndex = (selectedIndex + 1) % totalItems;
- }
- // Navigera UPP
- else if (event.getCode() == KeyCode.UP) {
- // Matematisk formel för att loopa bakåt (från 0 till sista)
- newIndex = (selectedIndex - 1 + totalItems) % totalItems;
- }
- // Välj (ENTER)
- else if (event.getCode() == KeyCode.ENTER) {
+ newIndex = (selectedIndex + 1) % totalItems; // %, if scrolling past last line, get back to 0
+ } else if (event.getCode() == KeyCode.UP) {
+ newIndex = (selectedIndex - 1 + totalItems) % totalItems; // Formula for looping backwards, from 0 to last
+ } else if (event.getCode() == KeyCode.ENTER) {
if (isMainMenu) {
- // Om vi är i huvudmenyn, gå in i en undermeny (t.ex. "Songs")
showScreen(mainMenu.get(selectedIndex));
} else {
- // Om vi är i en lista, hantera valet (spela låt etc)
handleSelection(menuLabels.get(selectedIndex));
}
return;
}
- // Om indexet ändrades, uppdatera grafiken
if (newIndex != selectedIndex) {
selectedIndex = newIndex;
updateMenu();
@@ -325,16 +353,24 @@ else if (event.getCode() == KeyCode.ENTER) {
});
}
+ /**
+ * Adjusts playback volume and displays the volume overlay.
+ *
+ * @param delta positive or negative volume delta
+ */
private void adjustVolume(double delta) {
if (mediaPlayer == null) return;
+ ensureVolumeBarExists();
double newVolume = Math.max(0, Math.min(1, mediaPlayer.getVolume() + delta));
mediaPlayer.setVolume(newVolume);
-
volumeBar.setProgress(newVolume);
showVolumeOverlay();
}
+ /**
+ * Displays the volume overlay temporarily using fade animations.
+ */
private void showVolumeOverlay() {
if (volumeHideTimer != null) {
volumeHideTimer.stop();
@@ -354,21 +390,21 @@ private void showVolumeOverlay() {
}
/**
- * Uppdaterar visuellt vilken rad som är vald (lägger till CSS-klass).
+ * Updates menu selection styling and ensures the selected item is visible.
*/
private void updateMenu() {
for (int i = 0; i < menuLabels.size(); i++) {
if (i == selectedIndex) {
- menuLabels.get(i).label().getStyleClass().add("selected-item"); // Gör texten markerad
- ensureVisible(menuLabels.get(i).label()); // Se till att scrollbaren flyttas så vi ser valet
+ menuLabels.get(i).label().getStyleClass().add("selected-item");
+ ensureVisible(menuLabels.get(i).label());
} else {
- menuLabels.get(i).label().getStyleClass().remove("selected-item"); // Ta bort markering
+ menuLabels.get(i).label().getStyleClass().remove("selected-item");
}
}
}
/**
- * Avancerad metod för att automatiskt scrolla ScrollPane till det markerade elementet.
+ * Automatically scrolls the {@link ScrollPane} so the given label is visible.
*/
private void ensureVisible(Label node) {
Platform.runLater(() -> {
@@ -376,31 +412,32 @@ private void ensureVisible(Label node) {
double viewportHeight = scrollPane.getViewportBounds().getHeight();
double nodeY = node.getBoundsInParent().getMinY();
- // Om innehållet är högre än skärmen, räkna ut var vi ska scrolla
if (contentHeight > viewportHeight) {
- // Beräkna positionen (0.0 är toppen, 1.0 är botten)
- double scrollTarget = nodeY / (contentHeight - viewportHeight);
- // Sätt värdet men tvinga det att vara mellan 0 och 1
- scrollPane.setVvalue(Math.min(1.0, Math.max(0.0, scrollTarget)));
+ double scrollTarget = nodeY / (contentHeight - viewportHeight); // Calculate position, 0.0 top, 1.0 bottom
+ scrollPane.setVvalue(Math.min(1.0, Math.max(0.0, scrollTarget))); // Set value between 0.0 and 1.0
}
});
}
/**
- * Visar en undermeny (Songs, Artists etc).
+ * Displays a secondary screen such as Songs, Artists, Albums or Playlists.
+ *