From 2bc005f031d47f2effa2c937312878fe46629783 Mon Sep 17 00:00:00 2001 From: Markil3 <75867393+Markil3@users.noreply.github.com> Date: Fri, 9 Jul 2021 13:26:05 -0600 Subject: [PATCH] Adds working links for the collections and such. --- .../regis/universe_player/ClickListener.java | 33 ++ .../edu/regis/universe_player/data/Album.java | 2 - .../universe_player/data/CollectionType.java | 28 ++ .../data/SimpleSongProvider.java | 2 +- .../player/CollectionList.java | 315 ++++++++++++++++++ .../universe_player/player/Collections.java | 91 ++++- .../universe_player/player/Interface.java | 102 +++++- .../player/SongDisplayListener.java | 35 ++ .../src/main/resources/gui/icons/artist.png | Bin 0 -> 7789 bytes .../src/main/resources/gui/icons/artist.svg | 78 +++++ 10 files changed, 667 insertions(+), 19 deletions(-) create mode 100644 interface/src/main/java/edu/regis/universe_player/ClickListener.java create mode 100644 interface/src/main/java/edu/regis/universe_player/data/CollectionType.java create mode 100644 interface/src/main/java/edu/regis/universe_player/player/CollectionList.java create mode 100644 interface/src/main/java/edu/regis/universe_player/player/SongDisplayListener.java create mode 100644 interface/src/main/resources/gui/icons/artist.png create mode 100644 interface/src/main/resources/gui/icons/artist.svg 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 0000000000000000000000000000000000000000..9df7c904b8d1e634b4a153ee2f9feaa32c9d87c9 GIT binary patch literal 7789 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 + + + + + + + image/svg+xml + + + + + + + + + + + +