diff --git a/interface/src/main/java/edu/regis/universe_player/ClickListener.java b/interface/src/main/java/edu/regis/universe_player/ClickListener.java new file mode 100644 index 0000000..a5442a0 --- /dev/null +++ b/interface/src/main/java/edu/regis/universe_player/ClickListener.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2021 William Hubbard. All Rights Reserved. + */ + +package edu.regis.universe_player; + +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; + +/** + * A utility class designed to shortcut adding click listeners to AWT objects. + * + * @author William Hubbard + * @version 0.1 + */ +public interface ClickListener extends MouseListener +{ + default void mousePressed(MouseEvent var1) + { + } + + default void mouseReleased(MouseEvent var1) + { + } + + default void mouseEntered(MouseEvent var1) + { + } + + default void mouseExited(MouseEvent var1) + { + } +} diff --git a/interface/src/main/java/edu/regis/universe_player/data/Album.java b/interface/src/main/java/edu/regis/universe_player/data/Album.java index 2cf0718..eed8545 100644 --- a/interface/src/main/java/edu/regis/universe_player/data/Album.java +++ b/interface/src/main/java/edu/regis/universe_player/data/Album.java @@ -4,8 +4,6 @@ package edu.regis.universe_player.data; -import java.awt.Image; - import javax.swing.ImageIcon; public class Album implements Comparable diff --git a/interface/src/main/java/edu/regis/universe_player/data/CollectionType.java b/interface/src/main/java/edu/regis/universe_player/data/CollectionType.java new file mode 100644 index 0000000..6c45eed --- /dev/null +++ b/interface/src/main/java/edu/regis/universe_player/data/CollectionType.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2021 William Hubbard. All Rights Reserved. + */ + +package edu.regis.universe_player.data; + +/** + * Lists all of the type of collections possible. + */ +public enum CollectionType +{ + album(Album.class), artist(String.class), albumArtist(String.class), genre(String.class), year(Integer.class); + + /** + * The object class for this type. + */ + public Class objectType; + + /** + * Creates a collection type. + * + * @param type - The class this type uses. + */ + CollectionType(Class type) + { + this.objectType = type; + } +} diff --git a/interface/src/main/java/edu/regis/universe_player/data/SimpleSongProvider.java b/interface/src/main/java/edu/regis/universe_player/data/SimpleSongProvider.java index 7dfbe05..824b5c0 100644 --- a/interface/src/main/java/edu/regis/universe_player/data/SimpleSongProvider.java +++ b/interface/src/main/java/edu/regis/universe_player/data/SimpleSongProvider.java @@ -32,7 +32,7 @@ public class SimpleSongProvider implements SongProvider String albumName, albumArtist; Album album; for (int albumNum = 0, numAlbums = random - .nextInt(5) + 5; albumNum < numAlbums; albumNum++) + .nextInt(20) + 20; albumNum < numAlbums; albumNum++) { builder = new StringBuilder(); for (int i = 0, l = random.nextInt(10) + 10; i < l; i++) diff --git a/interface/src/main/java/edu/regis/universe_player/player/CollectionList.java b/interface/src/main/java/edu/regis/universe_player/player/CollectionList.java new file mode 100644 index 0000000..6afab05 --- /dev/null +++ b/interface/src/main/java/edu/regis/universe_player/player/CollectionList.java @@ -0,0 +1,315 @@ +/* + * Copyright (c) 2021 William Hubbard. All Rights Reserved. + */ + +package edu.regis.universe_player.player; + +import java.awt.FlowLayout; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.GridLayout; +import java.awt.Insets; +import java.awt.event.MouseEvent; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import javax.swing.ImageIcon; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import edu.regis.universe_player.ClickListener; +import edu.regis.universe_player.data.Album; +import edu.regis.universe_player.data.CollectionType; +import edu.regis.universe_player.data.Song; +import edu.regis.universe_player.data.SongProvider; + +/** + * This panel will list all the song collections on display (albums, artists, genres, etc.) + * + * @author William Hubbard + * @version 0.1 + */ +public class CollectionList extends JPanel +{ + /** + * The type of collections being displayed. + */ + private CollectionType type; + /** + * A link between the JLabel and the object they point towards. + */ + private Map labelMap = new HashMap<>(); + + /** + * A list of all things interested in knowing when we click a collection. + */ + private LinkedList listeners = new LinkedList<>(); + + /** + * Creates a collections list view. + */ + public CollectionList() + { + super(); + + FlowLayout layout = new FlowLayout(); + this.setLayout(layout); + } + + /** + * Updates the collections currently listed, sorted by album. + * + * @param type - The type of collection we are displaying. + * @param objects - The collection to display. + */ + public void listCollection(CollectionType type, Collection objects) + { + Class fType = objects.stream().filter(Objects::nonNull).map(Object::getClass).findFirst() + .orElse(null); + if (objects.isEmpty() || fType == null) + { + /* + * We really don't need error checking here, and we couldn't get it working anyway. + */ + this.labelMap.clear(); + this.removeAll(); + return; + } + if (!type.objectType.isAssignableFrom(fType)) + { + throw new ClassCastException("Can't assign " + type.objectType + .getName() + " from " + fType + .getName()); + } + + this.labelMap.clear(); + this.removeAll(); + this.type = type; + switch (type) + { + case album: + this.addAlbums(objects.stream().sorted().map(ob -> (Album) ob) + .collect(Collectors.toList())); + break; + case artist: + this.addArtists(objects.stream().sorted().map(ob -> (String) ob) + .collect(Collectors.toList()), false); + break; + case albumArtist: + this.addArtists(objects.stream().sorted().map(ob -> (String) ob) + .collect(Collectors.toList()), true); + break; + case genre: + this.addGenres(objects.stream().sorted().map(ob -> (String) ob) + .collect(Collectors.toList())); + break; + case year: + this.addYears(objects.stream().sorted().map(ob -> (Integer) ob) + .collect(Collectors.toList())); + break; + } + } + + /** + * Updates the display to show a list of artists + * + * @param artists - The list of artists to display. + * @param album - Whether the list should be treated as song artists or album artists. + */ + private void addArtists(List artists, boolean album) + { + final int ART_SIZE = 128; + + JLabel artistLabel; + ImageIcon icon; + + for (String artist : artists) + { + artistLabel = new JLabel(); + // TODO - Maybe add some sort of artist image lookup? +// if (album.art != null) +// { +// icon = album.art; +// } +// else +// { + icon = new ImageIcon(this.getClass() + .getResource("/gui/icons/artist.png"), "Default"); +// } + icon.setImage(icon.getImage().getScaledInstance(ART_SIZE, ART_SIZE, 0)); + artistLabel.setIcon(icon); + artistLabel.setText(artist); + artistLabel.setHorizontalTextPosition(JLabel.CENTER); + artistLabel.setVerticalTextPosition(JLabel.BOTTOM); + artistLabel.addMouseListener((ClickListener) mouseEvent -> { + if (album) + { + this.triggerSongDisplayListeners(SongProvider.INSTANCE + .getAlbumsFromArtist(artist).stream() + .flatMap(album2 -> SongProvider.INSTANCE.getSongsFromAlbum(album2) + .stream()) + .collect(Collectors.toList())); + } + else + { + this.triggerSongDisplayListeners(SongProvider.INSTANCE + .getSongsFromArtist(artist)); + } + }); + this.add(artistLabel); + this.labelMap.put(artistLabel, artist); + } + } + + /** + * Updates the display to show a list of albums. + * + * @param albums - The list of albums to display. + */ + private void addAlbums(List albums) + { + final int ART_SIZE = 128; + + JLabel albumLabel; + ImageIcon icon; + + for (Album album : albums) + { + albumLabel = new JLabel(); + if (album.art != null) + { + icon = album.art; + } + else + { + icon = new ImageIcon(this.getClass() + .getResource("/gui/icons/defaultart.png"), "Default"); + } + icon.setImage(icon.getImage().getScaledInstance(ART_SIZE, ART_SIZE, 0)); + albumLabel.setIcon(icon); + albumLabel.setText(album.name); + albumLabel.setHorizontalTextPosition(JLabel.CENTER); + albumLabel.setVerticalTextPosition(JLabel.BOTTOM); + albumLabel.addMouseListener((ClickListener) mouseEvent -> this.triggerSongDisplayListeners(SongProvider.INSTANCE + .getSongsFromAlbum(album))); + this.add(albumLabel); + this.labelMap.put(albumLabel, album); + } + } + + /** + * Updates the display to show a list of genres. + * + * @param genres - The list of genres to display. + */ + private void addGenres(List genres) + { +// final int ART_SIZE = 128; + + JLabel genreLabel; +// ImageIcon icon; + + for (String genre : genres) + { + genreLabel = new JLabel(); + // TODO - Maybe add some sort of artist image lookup? +// if (album.art != null) +// { +// icon = album.art; +// } +// else +// { +// icon = new ImageIcon(this.getClass() +// .getResource("/gui/icons/defaultart.png"), "Default"); +// } +// icon.setImage(icon.getImage().getScaledInstance(ART_SIZE, ART_SIZE, 0)); +// albumLabel.setIcon(icon); + genreLabel.setText(genre); + genreLabel.setHorizontalTextPosition(JLabel.CENTER); + genreLabel.setVerticalTextPosition(JLabel.BOTTOM); + genreLabel.addMouseListener((ClickListener) mouseEvent -> this.triggerSongDisplayListeners(SongProvider.INSTANCE + .getAlbumsFromGenre(genre).stream() + .flatMap(album2 -> SongProvider.INSTANCE.getSongsFromAlbum(album2) + .stream()) + .collect(Collectors.toList()))); + this.add(genreLabel); + this.labelMap.put(genreLabel, genre); + } + } + + /** + * Updates the display to show a list of release years. + * + * @param years - The list of genres to display. + */ + private void addYears(List years) + { +// final int ART_SIZE = 128; + + JLabel yearLabel; +// ImageIcon icon; + + for (Integer year : years) + { + yearLabel = new JLabel(); + // TODO - Maybe add some sort of artist image lookup? +// if (album.art != null) +// { +// icon = album.art; +// } +// else +// { +// icon = new ImageIcon(this.getClass() +// .getResource("/gui/icons/defaultart.png"), "Default"); +// } +// icon.setImage(icon.getImage().getScaledInstance(ART_SIZE, ART_SIZE, 0)); +// albumLabel.setIcon(icon); + yearLabel.setText(year.toString()); + yearLabel.setHorizontalTextPosition(JLabel.CENTER); + yearLabel.setVerticalTextPosition(JLabel.BOTTOM); + yearLabel.addMouseListener((ClickListener) mouseEvent -> this.triggerSongDisplayListeners(SongProvider.INSTANCE + .getAlbumsFromYear(year).stream() + .flatMap(album2 -> SongProvider.INSTANCE.getSongsFromAlbum(album2) + .stream()) + .collect(Collectors.toList()))); + this.add(yearLabel); + this.labelMap.put(yearLabel, year); + } + } + + /** + * Adds a listener for when the displayed songs should change. + * + * @param listener - The listener to add. + */ + public void addSongDisplayListener(SongDisplayListener listener) + { + this.listeners.add(listener); + } + + /** + * Adds a listener for when the displayed songs should change. + * + * @param listener - The listener to add. + */ + public void removeSongDisplayListener(SongDisplayListener listener) + { + this.listeners.remove(listener); + } + + /** + * Triggers all the song display listeners. + */ + protected void triggerSongDisplayListeners(Collection songs) + { + for (SongDisplayListener listener : this.listeners) + { + listener.updateSongs(songs); + } + } +} diff --git a/interface/src/main/java/edu/regis/universe_player/player/Collections.java b/interface/src/main/java/edu/regis/universe_player/player/Collections.java index 573e813..4b444ba 100644 --- a/interface/src/main/java/edu/regis/universe_player/player/Collections.java +++ b/interface/src/main/java/edu/regis/universe_player/player/Collections.java @@ -5,11 +5,21 @@ package edu.regis.universe_player.player; import java.awt.Color; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; import javax.swing.BoxLayout; import javax.swing.JLabel; import javax.swing.JPanel; +import edu.regis.universe_player.ClickListener; +import edu.regis.universe_player.data.CollectionType; +import edu.regis.universe_player.data.Song; +import edu.regis.universe_player.data.SongProvider; + /** * This panel links to various song collections the player has set up. * @@ -18,31 +28,88 @@ import javax.swing.JPanel; */ public class Collections extends JPanel { - JLabel allSongs; - JLabel artists; - JLabel albums; - JLabel genres; - JLabel years; - JLabel playlists; + /** + * A list of all things interested in knowing when we click a collection. + */ + private LinkedList listeners = new LinkedList<>(); + /** + * Creates a collections list view. + */ public Collections() { JLabel label; BoxLayout layout = new BoxLayout(this, BoxLayout.Y_AXIS); this.setLayout(layout); - this.add(this.allSongs = label = new JLabel("All Songs")); + this.add(label = new JLabel("All Songs")); label.setForeground(Color.BLUE); - this.add(this.artists = label = new JLabel("Artists")); + label.addMouseListener((ClickListener) mouseEvent -> this + .triggerSongDisplayListeners(SongProvider.INSTANCE.getSongs())); + this.add(label = new JLabel("Artists")); label.setForeground(Color.BLUE); - this.add(this.albums = label = new JLabel("Albums")); + label.addMouseListener((ClickListener) mouseEvent -> this + .triggerCollectionDisplayListeners(CollectionType.albumArtist, SongProvider.INSTANCE + .getAlbumArtists())); + this.add(label = new JLabel("Albums")); label.setForeground(Color.BLUE); - this.add(this.genres = label = new JLabel("Genres")); + label.addMouseListener((ClickListener) mouseEvent -> this + .triggerCollectionDisplayListeners(CollectionType.album, SongProvider.INSTANCE + .getAlbums())); + this.add(label = new JLabel("Genres")); label.setForeground(Color.BLUE); - this.add(this.years = label = new JLabel("Years")); + label.addMouseListener((ClickListener) mouseEvent -> this + .triggerCollectionDisplayListeners(CollectionType.genre, SongProvider.INSTANCE + .getGenres())); + this.add(label = new JLabel("Years")); label.setForeground(Color.BLUE); + label.addMouseListener((ClickListener) mouseEvent -> this + .triggerCollectionDisplayListeners(CollectionType.year, SongProvider.INSTANCE + .getYears())); this.add(new JLabel("\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF")); - this.add(this.playlists = label = new JLabel("Playlists")); + this.add(label = new JLabel("Playlists")); label.setForeground(Color.BLUE); } + + /** + * Adds a listener for when the displayed songs should change. + * + * @param listener - The listener to add. + */ + public void addSongDisplayListener(SongDisplayListener listener) + { + this.listeners.add(listener); + } + + /** + * Adds a listener for when the displayed songs should change. + * + * @param listener - The listener to add. + */ + public void removeSongDisplayListener(SongDisplayListener listener) + { + this.listeners.remove(listener); + } + + /** + * Triggers all the song display listeners. + */ + protected void triggerSongDisplayListeners(Collection songs) + { + for (SongDisplayListener listener : this.listeners) + { + listener.updateSongs(songs); + } + } + + /** + * Triggers all the song display listeners. + */ + protected void triggerCollectionDisplayListeners(CollectionType type, Collection collection) + { + for (SongDisplayListener listener : this.listeners) + { + listener.updateCollections(type, collection); + } + } } diff --git a/interface/src/main/java/edu/regis/universe_player/player/Interface.java b/interface/src/main/java/edu/regis/universe_player/player/Interface.java index d4b0370..1c39b78 100644 --- a/interface/src/main/java/edu/regis/universe_player/player/Interface.java +++ b/interface/src/main/java/edu/regis/universe_player/player/Interface.java @@ -5,17 +5,42 @@ package edu.regis.universe_player.player; import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.util.Collection; import javax.swing.JFrame; import javax.swing.JScrollPane; +import edu.regis.universe_player.data.CollectionType; +import edu.regis.universe_player.data.Song; + /** * The Interface class serves as the primary GUI that the player interacts with. + * * @author William Hubbard * @version 0.1 */ -public class Interface extends JFrame +public class Interface extends JFrame implements SongDisplayListener, ComponentListener { + /** + * A reference to the panel containing links to different collection views. + */ + private Collections collectionTypes; + /** + * A reference to the central view showing a list of songs. + */ + private SongList songList; + /** + * A reference to the central view showing a list of collections. + */ + private CollectionList collectionList; + /** + * A reference to the central view scroll pane. + */ + private JScrollPane centerView; + public static void main(String[] args) { Interface inter = new Interface(); @@ -23,17 +48,86 @@ public class Interface extends JFrame inter.setVisible(true); } + /** + * Creates an interface + */ public Interface() { super(); this.setTitle("Universal Music Player"); this.getContentPane().setLayout(new BorderLayout()); - this.getContentPane().add(new Collections(), BorderLayout.LINE_START); + this.getContentPane() + .add(this.collectionTypes = new Collections(), BorderLayout.LINE_START); + this.collectionTypes.addSongDisplayListener(this); this.getContentPane().add(new PlayerControls(), BorderLayout.PAGE_END); - JScrollPane songList = new JScrollPane(new SongList()); - this.getContentPane().add(songList, BorderLayout.CENTER); + this.songList = new SongList(); + this.collectionList = new CollectionList(); + this.collectionList.addSongDisplayListener(this); + + this.centerView = new JScrollPane(this.songList); + this.getContentPane().add(this.centerView, BorderLayout.CENTER); + this.componentResized(null); + + this.addComponentListener(this); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } + + @Override + public void updateSongs(Collection songs) + { + this.songList.listAlbums(songs); + this.centerView.setViewportView(this.songList); + this.songList.setPreferredSize(new Dimension(this.centerView.getViewport() + .getExtentSize().width, Integer.MAX_VALUE)); + this.songList.setPreferredSize(new Dimension(this.centerView.getViewport() + .getExtentSize().width, this.songList + .getMinimumSize().height)); + } + + @Override + public void updateCollections(CollectionType type, Collection collections) + { + this.collectionList.listCollection(type, collections); + this.centerView.setViewportView(this.collectionList); + this.collectionList.setPreferredSize(new Dimension(this.centerView.getViewport() + .getExtentSize().width, Integer.MAX_VALUE)); + this.collectionList.setPreferredSize(new Dimension(this.centerView.getViewport() + .getExtentSize().width, this.collectionList + .getMinimumSize().height)); + } + + @Override + public void componentResized(ComponentEvent event) + { + this.songList.setPreferredSize(new Dimension(this.centerView.getViewport() + .getExtentSize().width, Integer.MAX_VALUE)); + this.songList.setPreferredSize(new Dimension(this.centerView.getViewport() + .getExtentSize().width, this.songList + .getMinimumSize().height)); + this.collectionList.setPreferredSize(new Dimension(this.centerView.getViewport() + .getExtentSize().width, Integer.MAX_VALUE)); + this.collectionList.setPreferredSize(new Dimension(this.centerView.getViewport() + .getExtentSize().width, this.collectionList + .getMinimumSize().height)); + } + + @Override + public void componentMoved(ComponentEvent event) + { + + } + + @Override + public void componentShown(ComponentEvent event) + { + + } + + @Override + public void componentHidden(ComponentEvent event) + { + + } } \ No newline at end of file diff --git a/interface/src/main/java/edu/regis/universe_player/player/SongDisplayListener.java b/interface/src/main/java/edu/regis/universe_player/player/SongDisplayListener.java new file mode 100644 index 0000000..a9f02f7 --- /dev/null +++ b/interface/src/main/java/edu/regis/universe_player/player/SongDisplayListener.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2021 William Hubbard. All Rights Reserved. + */ + +package edu.regis.universe_player.player; + +import java.util.Collection; +import java.util.EventListener; + +import edu.regis.universe_player.data.CollectionType; +import edu.regis.universe_player.data.Song; + +/** + * A callback that is triggered whenever one element believes that the interface center display needs to be updated. + * + * @author William Hubbard + * @version 0.1 + */ +public interface SongDisplayListener extends EventListener +{ + /** + * Called to display a list of songs, sorted by album. + * + * @param songs - The songs to display. + */ + void updateSongs(Collection songs); + + /** + * Called to display a list of collections + * + * @param type - The type of collections to display. + * @param collections - The collections to display + */ + void updateCollections(CollectionType type, Collection collections); +} diff --git a/interface/src/main/resources/gui/icons/artist.png b/interface/src/main/resources/gui/icons/artist.png new file mode 100644 index 0000000..9df7c90 Binary files /dev/null and b/interface/src/main/resources/gui/icons/artist.png differ diff --git a/interface/src/main/resources/gui/icons/artist.svg b/interface/src/main/resources/gui/icons/artist.svg new file mode 100644 index 0000000..7ed18c3 --- /dev/null +++ b/interface/src/main/resources/gui/icons/artist.svg @@ -0,0 +1,78 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + +