Added a queue system.

This commit is contained in:
Markil3
2021-08-03 13:11:36 -06:00
parent 8386c9575c
commit 52262b5668
12 changed files with 1912 additions and 352 deletions

View File

@@ -0,0 +1,351 @@
/*
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
*/
package com.wordpress.tips4java;
import javax.swing.*;
import javax.swing.border.BevelBorder;
import java.awt.*;
/**
* A JPanel containing default options for scrolling.
*
* @author Rob Camick, https://tips4java.wordpress.com/2009/12/20/scrollable-panel/
*/
public class ScrollablePanel extends JPanel implements Scrollable, SwingConstants
{
public enum ScrollableSizeHint
{
NONE,
FIT,
STRETCH;
}
public enum IncrementType
{
PERCENT,
PIXELS;
}
private ScrollableSizeHint scrollableHeight = ScrollableSizeHint.NONE;
private ScrollableSizeHint scrollableWidth = ScrollableSizeHint.NONE;
private IncrementInfo horizontalBlock;
private IncrementInfo horizontalUnit;
private IncrementInfo verticalBlock;
private IncrementInfo verticalUnit;
/**
* Default constructor that uses a FlowLayout
*/
public ScrollablePanel()
{
this(new FlowLayout());
}
/**
* Constuctor for specifying the LayoutManager of the panel.
*
* @param layout the LayountManger for the panel
*/
public ScrollablePanel(LayoutManager layout)
{
super(layout);
IncrementInfo block = new IncrementInfo(IncrementType.PERCENT, 100);
IncrementInfo unit = new IncrementInfo(IncrementType.PERCENT, 10);
setScrollableBlockIncrement(HORIZONTAL, block);
setScrollableBlockIncrement(VERTICAL, block);
setScrollableUnitIncrement(HORIZONTAL, unit);
setScrollableUnitIncrement(VERTICAL, unit);
}
/**
* Get the height ScrollableSizeHint enum
*
* @return the ScrollableSizeHint enum for the height
*/
public ScrollableSizeHint getScrollableHeight()
{
return scrollableHeight;
}
/**
* Set the ScrollableSizeHint enum for the height. The enum is used to
* determine the boolean value that is returned by the
* getScrollableTracksViewportHeight() method. The valid values are:
* <p>
* ScrollableSizeHint.NONE - return "false", which causes the height
* of the panel to be used when laying out the children
* ScrollableSizeHint.FIT - return "true", which causes the height of
* the viewport to be used when laying out the children
* ScrollableSizeHint.STRETCH - return "true" when the viewport height
* is greater than the height of the panel, "false" otherwise.
*
* @param scrollableHeight as represented by the ScrollableSizeHint enum.
*/
public void setScrollableHeight(ScrollableSizeHint scrollableHeight)
{
this.scrollableHeight = scrollableHeight;
revalidate();
}
/**
* Get the width ScrollableSizeHint enum
*
* @return the ScrollableSizeHint enum for the width
*/
public ScrollableSizeHint getScrollableWidth()
{
return scrollableWidth;
}
/**
* Set the ScrollableSizeHint enum for the width. The enum is used to
* determine the boolean value that is returned by the
* getScrollableTracksViewportWidth() method. The valid values are:
* <p>
* ScrollableSizeHint.NONE - return "false", which causes the width
* of the panel to be used when laying out the children
* ScrollableSizeHint.FIT - return "true", which causes the width of
* the viewport to be used when laying out the children
* ScrollableSizeHint.STRETCH - return "true" when the viewport width
* is greater than the width of the panel, "false" otherwise.
*
* @param scrollableWidth as represented by the ScrollableSizeHint enum.
*/
public void setScrollableWidth(ScrollableSizeHint scrollableWidth)
{
this.scrollableWidth = scrollableWidth;
revalidate();
}
/**
* Get the block IncrementInfo for the specified orientation
*
* @return the block IncrementInfo for the specified orientation
*/
public IncrementInfo getScrollableBlockIncrement(int orientation)
{
return orientation == SwingConstants.HORIZONTAL ? horizontalBlock : verticalBlock;
}
/**
* Specify the information needed to do block scrolling.
*
* @param orientation specify the scrolling orientation. Must be either:
* SwingContants.HORIZONTAL or SwingContants.VERTICAL.
* @param amount a value used with the IncrementType to determine the
* scrollable amount
* @paran type specify how the amount parameter in the calculation of
* the scrollable amount. Valid values are:
* IncrementType.PERCENT - treat the amount as a % of the viewport size
* IncrementType.PIXEL - treat the amount as the scrollable amount
*/
public void setScrollableBlockIncrement(int orientation, IncrementType type, int amount)
{
IncrementInfo info = new IncrementInfo(type, amount);
setScrollableBlockIncrement(orientation, info);
}
/**
* Specify the information needed to do block scrolling.
*
* @param orientation specify the scrolling orientation. Must be either:
* SwingContants.HORIZONTAL or SwingContants.VERTICAL.
* @param info An IncrementInfo object containing information of how to
* calculate the scrollable amount.
*/
public void setScrollableBlockIncrement(int orientation, IncrementInfo info)
{
switch (orientation)
{
case SwingConstants.HORIZONTAL:
horizontalBlock = info;
break;
case SwingConstants.VERTICAL:
verticalBlock = info;
break;
default:
throw new IllegalArgumentException("Invalid orientation: " + orientation);
}
}
/**
* Get the unit IncrementInfo for the specified orientation
*
* @return the unit IncrementInfo for the specified orientation
*/
public IncrementInfo getScrollableUnitIncrement(int orientation)
{
return orientation == SwingConstants.HORIZONTAL ? horizontalUnit : verticalUnit;
}
/**
* Specify the information needed to do unit scrolling.
*
* @param orientation specify the scrolling orientation. Must be either:
* SwingContants.HORIZONTAL or SwingContants.VERTICAL.
* @param amount a value used with the IncrementType to determine the
* scrollable amount
* @paran type specify how the amount parameter in the calculation of
* the scrollable amount. Valid values are:
* IncrementType.PERCENT - treat the amount as a % of the viewport size
* IncrementType.PIXEL - treat the amount as the scrollable amount
*/
public void setScrollableUnitIncrement(int orientation, IncrementType type, int amount)
{
IncrementInfo info = new IncrementInfo(type, amount);
setScrollableUnitIncrement(orientation, info);
}
/**
* Specify the information needed to do unit scrolling.
*
* @param orientation specify the scrolling orientation. Must be either:
* SwingContants.HORIZONTAL or SwingContants.VERTICAL.
* @param info An IncrementInfo object containing information of how to
* calculate the scrollable amount.
*/
public void setScrollableUnitIncrement(int orientation, IncrementInfo info)
{
switch (orientation)
{
case SwingConstants.HORIZONTAL:
horizontalUnit = info;
break;
case SwingConstants.VERTICAL:
verticalUnit = info;
break;
default:
throw new IllegalArgumentException("Invalid orientation: " + orientation);
}
}
// Implement Scrollable interface
public Dimension getPreferredScrollableViewportSize()
{
return getPreferredSize();
}
public int getScrollableUnitIncrement(
Rectangle visible, int orientation, int direction)
{
switch (orientation)
{
case SwingConstants.HORIZONTAL:
return getScrollableIncrement(horizontalUnit, visible.width);
case SwingConstants.VERTICAL:
return getScrollableIncrement(verticalUnit, visible.height);
default:
throw new IllegalArgumentException("Invalid orientation: " + orientation);
}
}
public int getScrollableBlockIncrement(
Rectangle visible, int orientation, int direction)
{
switch (orientation)
{
case SwingConstants.HORIZONTAL:
return getScrollableIncrement(horizontalBlock, visible.width);
case SwingConstants.VERTICAL:
return getScrollableIncrement(verticalBlock, visible.height);
default:
throw new IllegalArgumentException("Invalid orientation: " + orientation);
}
}
protected int getScrollableIncrement(IncrementInfo info, int distance)
{
if (info.getIncrement() == IncrementType.PIXELS)
{
return info.getAmount();
}
else
{
return distance * info.getAmount() / 100;
}
}
public boolean getScrollableTracksViewportWidth()
{
if (scrollableWidth == ScrollableSizeHint.NONE)
{
return false;
}
if (scrollableWidth == ScrollableSizeHint.FIT)
{
return true;
}
// STRETCH sizing, use the greater of the panel or viewport width
if (getParent() instanceof JViewport)
{
return (((JViewport) getParent()).getWidth() > getPreferredSize().width);
}
return false;
}
public boolean getScrollableTracksViewportHeight()
{
if (scrollableHeight == ScrollableSizeHint.NONE)
{
return false;
}
if (scrollableHeight == ScrollableSizeHint.FIT)
{
return true;
}
// STRETCH sizing, use the greater of the panel or viewport height
if (getParent() instanceof JViewport)
{
return (((JViewport) getParent()).getHeight() > getPreferredSize().height);
}
return false;
}
/**
* Helper class to hold the information required to calculate the scroll amount.
*/
static class IncrementInfo
{
private IncrementType type;
private int amount;
public IncrementInfo(IncrementType type, int amount)
{
this.type = type;
this.amount = amount;
}
public IncrementType getIncrement()
{
return type;
}
public int getAmount()
{
return amount;
}
public String toString()
{
return
"com.ScrollablePanel[" +
type + ", " +
amount + "]";
}
}
}

View File

@@ -443,22 +443,25 @@ public class LocalSongProvider implements SongProvider<LocalSong>
{ {
try try
{ {
state = getDb().createStatement(); synchronized (db)
result = state.executeQuery("SELECT mod FROM local_songs WHERE file='" + this.file.getAbsolutePath() + "';");
if (result.next())
{ {
if (result.getLong(1) >= this.file.lastModified()) state = getDb().createStatement();
result = state.executeQuery("SELECT mod FROM local_songs WHERE file='" + this.file.getAbsolutePath().replaceAll("'", "''") + "';");
if (result.next())
{ {
/* if (result.getLong(1) >= this.file.lastModified())
* No modifications needed {
*/ /*
updatedSongs++; * No modifications needed
triggerUpdateListeners(); */
return; updatedSongs++;
} triggerUpdateListeners();
else return;
{ }
state.executeUpdate("UPDATE local_songs SET mod = " + this.file.lastModified() + " WHERE file='" + this.file.getAbsolutePath() + "';"); else
{
state.executeUpdate("UPDATE local_songs SET mod = " + this.file.lastModified() + " WHERE file='" + this.file.getAbsolutePath().replaceAll("'", "''") + "';");
}
} }
} }
currentFolder = file.getPath(); currentFolder = file.getPath();
@@ -609,147 +612,153 @@ public class LocalSongProvider implements SongProvider<LocalSong>
/* /*
* Update album information. * Update album information.
*/ */
result = state.executeQuery("SELECT album FROM local_albums WHERE album='" + albumTitle + "';"); synchronized (db)
if (!result.next())
{ {
state.executeUpdate("INSERT INTO local_albums (album) VALUES ('" + albumTitle + "');"); /*
} * This part in particular is prone to thread-safety issues.
result = state.executeQuery("SELECT artists FROM local_albums WHERE album='" + albumTitle + "';"); */
if (result.getString("artists") == null && albumArtist != null) result = state.executeQuery("SELECT album FROM local_albums WHERE album='" + albumTitle + "';");
{ if (!result.next())
state.executeUpdate("UPDATE local_albums SET artists='" + Arrays.stream(albumArtist.split(";")).map(String::trim).collect(Collectors.joining(";")) + "' WHERE album='" + albumTitle + "';");
}
// TODO - Can we get year metadata?
result = state.executeQuery("SELECT genres FROM local_albums WHERE album='" + albumTitle + "';");
if (result.getString("genres") == null && genre != null)
{
state.executeUpdate("UPDATE local_albums SET genres='" + Arrays.stream(genre.split(";")).map(String::trim).collect(Collectors.joining(";")) + "' WHERE album='" + albumTitle + "';");
}
result = state.executeQuery("SELECT tracks FROM local_albums WHERE album='" + albumTitle + "';");
if (result.getInt("tracks") == 0 && track != null && track[1] > 0)
{
state.executeUpdate("UPDATE local_albums SET tracks=" + track[1] + " WHERE album='" + albumTitle + "';");
}
result = state.executeQuery("SELECT discs FROM local_albums WHERE album='" + albumTitle + "';");
if (result.getInt("discs") == 0 && disc != null && disc[1] > 0)
{
state.executeUpdate("UPDATE local_albums SET tracks=" + disc[1] + " WHERE album='" + albumTitle + "';");
}
/*
* Create the song
*/
result = state.executeQuery("SELECT title FROM local_songs WHERE file='" + file.getAbsolutePath() + "';");
if (result.next())
{
logger.debug("Updating song cache for {} ({})", title, file);
StringBuilder sql = new StringBuilder("UPDATE local_songs SET ");
sql.append("codec='").append(codec).append("', ");
sql.append("type='").append(codec).append("', ");
if (title != null && !title.isEmpty())
{ {
sql.append("title='").append(title).append("', "); state.executeUpdate("INSERT INTO local_albums (album) VALUES ('" + albumTitle + "');");
} }
else result = state.executeQuery("SELECT artists FROM local_albums WHERE album='" + albumTitle + "';");
if (result.getString("artists") == null && albumArtist != null)
{ {
sql.append("title='").append(file.getName()).append("', "); state.executeUpdate("UPDATE local_albums SET artists='" + Arrays.stream(albumArtist.split(";")).map(String::trim).collect(Collectors.joining(";")) + "' WHERE album='" + albumTitle + "';");
} }
if (artist != null && !artist.isEmpty()) // TODO - Can we get year metadata?
result = state.executeQuery("SELECT genres FROM local_albums WHERE album='" + albumTitle + "';");
if (result.getString("genres") == null && genre != null)
{ {
sql.append("artists='").append(Optional.of(artist).map(s -> s.split(";")).stream().flatMap(Arrays::stream).map(String::trim).collect(Collectors.joining(";"))).append("', "); state.executeUpdate("UPDATE local_albums SET genres='" + Arrays.stream(genre.split(";")).map(String::trim).collect(Collectors.joining(";")) + "' WHERE album='" + albumTitle + "';");
} }
else result = state.executeQuery("SELECT tracks FROM local_albums WHERE album='" + albumTitle + "';");
if (result.getInt("tracks") == 0 && track != null && track[1] > 0)
{ {
sql.append("artists=NULL, "); state.executeUpdate("UPDATE local_albums SET tracks=" + track[1] + " WHERE album='" + albumTitle + "';");
} }
if (track != null && track[0] > 0) result = state.executeQuery("SELECT discs FROM local_albums WHERE album='" + albumTitle + "';");
if (result.getInt("discs") == 0 && disc != null && disc[1] > 0)
{ {
sql.append("track=").append(track[0]).append(", "); state.executeUpdate("UPDATE local_albums SET tracks=" + disc[1] + " WHERE album='" + albumTitle + "';");
}
else
{
sql.append("track=NULL, ");
}
if (disc != null && disc[0] > 0)
{
sql.append("disc=").append(disc[0]).append(", ");
}
else
{
sql.append("disc=NULL, ");
}
if (duration != 0)
{
sql.append("duration=").append(duration).append(", ");
}
else
{
sql.append("duration=NULL, ");
}
if (albumTitle != null && !albumTitle.isEmpty())
{
sql.append("album='").append(albumTitle).append("', ");
}
else
{
sql.append("album=NULL, ");
}
sql.append("mod=").append(file.lastModified());
sql.append(" WHERE file='").append(file.getAbsolutePath()).append("';");
state.executeUpdate(sql.toString());
}
else
{
logger.debug("Caching song {} ({})", title, file);
StringBuilder sql = new StringBuilder("INSERT INTO local_songs ");
StringBuilder columns = new StringBuilder("(");
StringBuilder values = new StringBuilder("(");
columns.append("file,");
values.append('\'').append(file.getAbsolutePath()).append("',");
columns.append("codec,");
values.append('\'').append(codec).append("',");
columns.append("type,");
values.append('\'').append(type).append("',");
if (title != null && !title.isEmpty())
{
columns.append("title,");
values.append('\'').append(title).append("',");
}
if (artist != null && !artist.isEmpty())
{
columns.append("artists,");
values.append('\'').append(Optional.of(artist).map(s -> s.split(";")).stream().flatMap(Arrays::stream).map(String::trim).collect(Collectors.joining(";"))).append("',");
}
if (track != null && track[0] > 0)
{
columns.append("track,");
values.append(track[0]).append(",");
}
if (disc != null && disc[0] > 0)
{
columns.append("disc,");
values.append(disc[0]).append(",");
}
if (duration > 0)
{
columns.append("duration,");
values.append(duration).append(",");
}
if (albumTitle != null && !albumTitle.isEmpty())
{
columns.append("album,");
values.append('\'').append(albumTitle).append("',");
} }
columns.append("mod"); /*
values.append(file.lastModified()); * Create the song
*/
columns.append(") VALUES "); result = state.executeQuery("SELECT title FROM local_songs WHERE file='" + file.getAbsolutePath().replaceAll("'", "''") + "';");
values.append(");"); if (result.next())
sql.append(columns); {
sql.append(values); logger.debug("Updating song cache for {} ({})", title, file);
state.executeUpdate(sql.toString()); StringBuilder sql = new StringBuilder("UPDATE local_songs SET ");
sql.append("codec='").append(codec).append("', ");
sql.append("type='").append(codec).append("', ");
if (title != null && !title.isEmpty())
{
sql.append("title='").append(title).append("', ");
}
else
{
sql.append("title='").append(file.getName()).append("', ");
}
if (artist != null && !artist.isEmpty())
{
sql.append("artists='").append(Optional.of(artist).map(s -> s.split(";")).stream().flatMap(Arrays::stream).map(String::trim).collect(Collectors.joining(";"))).append("', ");
}
else
{
sql.append("artists=NULL, ");
}
if (track != null && track[0] > 0)
{
sql.append("track=").append(track[0]).append(", ");
}
else
{
sql.append("track=NULL, ");
}
if (disc != null && disc[0] > 0)
{
sql.append("disc=").append(disc[0]).append(", ");
}
else
{
sql.append("disc=NULL, ");
}
if (duration != 0)
{
sql.append("duration=").append(duration).append(", ");
}
else
{
sql.append("duration=NULL, ");
}
if (albumTitle != null && !albumTitle.isEmpty())
{
sql.append("album='").append(albumTitle).append("', ");
}
else
{
sql.append("album=NULL, ");
}
sql.append("mod=").append(file.lastModified());
sql.append(" WHERE file='").append(file.getAbsolutePath().replaceAll("'", "''")).append("';");
state.executeUpdate(sql.toString());
}
else
{
logger.debug("Caching song {} ({})", title, file);
StringBuilder sql = new StringBuilder("INSERT INTO local_songs ");
StringBuilder columns = new StringBuilder("(");
StringBuilder values = new StringBuilder("(");
columns.append("file,");
values.append('\'').append(file.getAbsolutePath().replaceAll("'", "''")).append("',");
columns.append("codec,");
values.append('\'').append(codec).append("',");
columns.append("type,");
values.append('\'').append(type).append("',");
if (title != null && !title.isEmpty())
{
columns.append("title,");
values.append('\'').append(title).append("',");
}
if (artist != null && !artist.isEmpty())
{
columns.append("artists,");
values.append('\'').append(Optional.of(artist).map(s -> s.split(";")).stream().flatMap(Arrays::stream).map(String::trim).collect(Collectors.joining(";"))).append("',");
}
if (track != null && track[0] > 0)
{
columns.append("track,");
values.append(track[0]).append(",");
}
if (disc != null && disc[0] > 0)
{
columns.append("disc,");
values.append(disc[0]).append(",");
}
if (duration > 0)
{
columns.append("duration,");
values.append(duration).append(",");
}
if (albumTitle != null && !albumTitle.isEmpty())
{
columns.append("album,");
values.append('\'').append(albumTitle).append("',");
}
columns.append("mod");
values.append(file.lastModified());
columns.append(") VALUES ");
values.append(");");
sql.append(columns);
sql.append(values);
state.executeUpdate(sql.toString());
}
} }
updatedSongs++; updatedSongs++;
@@ -828,83 +837,86 @@ public class LocalSongProvider implements SongProvider<LocalSong>
/* /*
* Check if the table exists * Check if the table exists
*/ */
state = getDb().createStatement(); synchronized (db)
result = state.executeQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='LOCAL_ALBUMS';");
if (!result.next())
{ {
logger.debug("Creating album table."); state = getDb().createStatement();
/* result = state.executeQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='LOCAL_ALBUMS';");
* Create the table if (!result.next())
*/
state.executeUpdate("CREATE TABLE LOCAL_ALBUMS" +
"(ALBUM TEXT PRIMARY KEY NOT NULL," +
"ARTISTS TEXT," +
"YEAR INTEGER," +
"GENRES TEXT," +
"TRACKS INTEGER," +
"DISCS INTEGER);");
}
else
{
result = state.executeQuery("SELECT * FROM LOCAL_ALBUMS;");
while (result.next())
{ {
album = albums.get(result.getString("album")); logger.debug("Creating album table.");
if (album == null) /*
{ * Create the table
album = new Album(); */
album.name = result.getString("album"); state.executeUpdate("CREATE TABLE LOCAL_ALBUMS" +
albums.put(album.name, album); "(ALBUM TEXT PRIMARY KEY NOT NULL," +
} "ARTISTS TEXT," +
album.artists = Optional.ofNullable(result.getString("artists")).map(s -> s.split(";")).orElse(new String[0]); "YEAR INTEGER," +
album.year = result.getInt("year"); "GENRES TEXT," +
album.genres = Optional.ofNullable(result.getString("genres")).map(s -> s.split(";")).orElse(new String[0]); "TRACKS INTEGER," +
album.totalTracks = result.getInt("tracks"); "DISCS INTEGER);");
album.totalDiscs = result.getInt("discs");
} }
} else
result = state.executeQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='LOCAL_SONGS';");
if (!result.next())
{
logger.debug("Creating song table.");
/*
* Create the table
*/
state.executeUpdate("CREATE TABLE LOCAL_SONGS" +
"(FILE TEXT PRIMARY KEY NOT NULL," +
"CODEC CHAR(5)," +
"TYPE CHAR(5)," +
"TITLE TEXT," +
"ARTISTS TEXT," +
"TRACK INTEGER," +
"DISC INTEGER," +
"DURATION BIGINT," +
"ALBUM TEXT," +
"MOD BIGINT);");
}
else
{
result = state.executeQuery("SELECT * FROM LOCAL_SONGS;");
while (result.next())
{ {
song = songs.get(new File(result.getString("file"))); result = state.executeQuery("SELECT * FROM LOCAL_ALBUMS;");
if (song == null) while (result.next())
{ {
song = new LocalSong(); album = albums.get(result.getString("album"));
song.file = new File(result.getString("file")); if (album == null)
songs.put(song.file, song); {
album = new Album();
album.name = result.getString("album");
albums.put(album.name, album);
}
album.artists = Optional.ofNullable(result.getString("artists")).map(s -> s.split(";")).orElse(new String[0]);
album.year = result.getInt("year");
album.genres = Optional.ofNullable(result.getString("genres")).map(s -> s.split(";")).orElse(new String[0]);
album.totalTracks = result.getInt("tracks");
album.totalDiscs = result.getInt("discs");
} }
song.codec = result.getString("codec");
song.type = result.getString("type");
song.title = result.getString("title");
song.artists = Optional.ofNullable(result.getString("artists")).map(s -> s.split(";")).orElse(new String[0]);
song.trackNum = result.getInt("track");
song.disc = result.getInt("disc");
song.duration = result.getLong("duration");
song.album = Optional.ofNullable(result.getString("album")).map(albums::get).orElse(null);
} }
result = state.executeQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='LOCAL_SONGS';");
if (!result.next())
{
logger.debug("Creating song table.");
/*
* Create the table
*/
state.executeUpdate("CREATE TABLE LOCAL_SONGS" +
"(FILE TEXT PRIMARY KEY NOT NULL," +
"CODEC CHAR(5)," +
"TYPE CHAR(5)," +
"TITLE TEXT," +
"ARTISTS TEXT," +
"TRACK INTEGER," +
"DISC INTEGER," +
"DURATION BIGINT," +
"ALBUM TEXT," +
"MOD BIGINT);");
}
else
{
result = state.executeQuery("SELECT * FROM LOCAL_SONGS;");
while (result.next())
{
song = songs.get(new File(result.getString("file")));
if (song == null)
{
song = new LocalSong();
song.file = new File(result.getString("file"));
songs.put(song.file, song);
}
song.codec = result.getString("codec");
song.type = result.getString("type");
song.title = result.getString("title");
song.artists = Optional.ofNullable(result.getString("artists")).map(s -> s.split(";")).orElse(new String[0]);
song.trackNum = result.getInt("track");
song.disc = result.getInt("disc");
song.duration = result.getLong("duration");
song.album = Optional.ofNullable(result.getString("album")).map(albums::get).orElse(null);
}
}
state.close();
} }
state.close();
} }
catch (SQLException e) catch (SQLException e)
{ {

View File

@@ -0,0 +1,469 @@
/*
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
*/
package edu.regis.universeplayer.data;
import java.util.*;
/**
* A song queue contains a list of songs that are to play, along with a way to
* order them
*
* @author William Hubbard
* @version 0.1
*/
public class Queue extends ArrayList<Song>
{
private static Queue INSTANCE;
public static Queue getInstance()
{
if (INSTANCE == null)
{
INSTANCE = new Queue();
}
return INSTANCE;
}
private final ArrayList<Song> queueOrder = new ArrayList<>();
private int currentIndex;
private boolean shuffle;
private boolean repeat;
private final Random random = new Random();
private final LinkedList<SongChangeListener> songListeners = new LinkedList<>();
private final LinkedList<QueueChangeListener> queueListeners = new LinkedList<>();
public Queue()
{
}
/**
* Obtains the song scheduled to play.
*
* @return The currently playing song.
*/
public Song getCurrentSong()
{
if (this.currentIndex < 0 || this.currentIndex >= this.queueOrder.size())
{
return null;
}
return this.queueOrder.get(currentIndex);
}
/**
* Obtains the song index scheduled to play.
*
* @return The currently playing song index.
*/
public int getCurrentIndex()
{
return this.indexOf(this.getCurrentSong());
}
/**
* Checks whether this queue will loop after the end of the song.
*
* @return True if the queue loops, false otherwise.
*/
public boolean isRepeating()
{
return this.repeat;
}
/**
* Sets whether this queue should loop after the last song plays.
*
* @param repeat - Whether the queue loops.
*/
public void setRepeat(boolean repeat)
{
this.repeat = repeat;
}
/**
* Checks whether this queue is in shuffle mode.
*
* @return True if the queue shuffles, false otherwise.
*/
public boolean isShuffling()
{
return this.shuffle;
}
/**
* Sets whether this queue should be shuffled or not.
*
* @param shuffle - Whether or not the queue shuffles.
*/
public void setShuffle(boolean shuffle)
{
this.shuffle = shuffle;
this.queueOrder.clear();
}
/**
* Skips to the next song.
*
* @return The new song, or null if we finished the list.
*/
public Song skipNext()
{
if (++this.currentIndex >= this.queueOrder.size())
{
if (this.repeat)
{
this.currentIndex = 0;
/*
* Reshuffle the queue as needed.
*/
this.queueOrder.clear();
this.getOrder();
this.triggerSongChangeListeners();
return this.getCurrentSong();
}
else
{
/*
* Make sure we aren't infinitely increasing the current index.
*/
this.currentIndex = this.queueOrder.size() - 1;
this.triggerSongChangeListeners();
return null;
}
}
this.triggerSongChangeListeners();
return this.getCurrentSong();
}
/**
* Skips to the previous song.
*
* @return The previous song, or the first one if we go underboard.
*/
public Song skipPrev()
{
if (--this.currentIndex < 0)
{
if (this.repeat)
{
this.currentIndex = this.queueOrder.size() - 1;
}
else
{
this.currentIndex = 0;
}
}
this.triggerSongChangeListeners();
return this.getCurrentSong();
}
/**
* Skips to the song at the provided song index.
*
* @param index - The new index. This is not relational to the queue.
* @return The new song, or null if we finished the list.
*/
public Song skipToSong(int index)
{
Song song = this.get(index);
this.currentIndex = this.queueOrder.indexOf(song);
this.triggerSongChangeListeners();
return this.getCurrentSong();
}
private ArrayList<Song> getOrder()
{
LinkedList<Song> indexesRemaining;
if (this.queueOrder.isEmpty())
{
this.queueOrder.addAll(this);
if (this.shuffle)
{
indexesRemaining = new LinkedList<>(this.queueOrder);
this.queueOrder.clear();
while (indexesRemaining.size() > 0)
{
this.queueOrder.add(indexesRemaining.remove(random.nextInt(indexesRemaining.size())));
}
}
}
return this.queueOrder;
}
@Override
public boolean add(Song song)
{
int newIndex;
if (super.add(song))
{
if (this.shuffle)
{
newIndex = random.nextInt(this.queueOrder.size());
if (newIndex <= this.currentIndex)
{
this.currentIndex++;
}
this.queueOrder.add(newIndex, song);
}
else
{
this.queueOrder.add(song);
}
this.triggerQueueChangeListeners();
return true;
}
return false;
}
@Override
public void add(int index, Song song)
{
int newIndex;
super.add(index, song);
if (this.shuffle)
{
newIndex = this.random.nextInt(this.queueOrder.size());
}
else
{
newIndex = index;
}
if (newIndex <= this.currentIndex)
{
this.currentIndex++;
}
this.queueOrder.add(newIndex, song);
this.triggerQueueChangeListeners();
}
@Override
public Song remove(int index)
{
Song removed = super.remove(index);
if (removed != null)
{
index = this.queueOrder.indexOf(removed);
if (index <= this.currentIndex)
{
this.currentIndex--;
}
this.queueOrder.remove(removed);
this.triggerQueueChangeListeners();
}
return removed;
}
@Override
public boolean remove(Object o)
{
int index = this.indexOf(o);
if (super.remove(o))
{
if (index <= this.currentIndex)
{
this.currentIndex--;
}
this.queueOrder.remove(o);
this.triggerQueueChangeListeners();
return true;
}
return false;
}
@Override
public void clear()
{
super.clear();
this.queueOrder.clear();
this.currentIndex = 0;
this.triggerQueueChangeListeners();
this.triggerSongChangeListeners();
}
@Override
public boolean addAll(Collection<? extends Song> c)
{
int newIndex;
int length = this.size();
if (super.addAll(c))
{
for (Song song : c)
{
if (this.shuffle)
{
newIndex = random.nextInt(this.queueOrder.size());
if (newIndex <= this.currentIndex)
{
this.currentIndex++;
}
this.queueOrder.add(newIndex, song);
}
else
{
this.queueOrder.add(song);
}
}
this.triggerQueueChangeListeners();
return true;
}
return false;
}
@Override
public boolean addAll(int index, Collection<? extends Song> c)
{
int newIndex;
int i = 0;
if (super.addAll(index, c))
{
for (Song song : c)
{
if (this.shuffle)
{
newIndex = random.nextInt(this.queueOrder.size());
}
else
{
newIndex = index + i++;
}
if (newIndex <= this.currentIndex)
{
this.currentIndex++;
}
this.queueOrder.add(newIndex, song);
}
this.triggerQueueChangeListeners();
return true;
}
return false;
}
@Override
protected void removeRange(int fromIndex, int toIndex)
{
for (int i = fromIndex; i < toIndex; i++)
{
this.queueOrder.remove(this.get(i));
if (i <= this.currentIndex)
{
this.currentIndex--;
}
}
super.removeRange(fromIndex, toIndex);
this.triggerQueueChangeListeners();
}
@Override
public boolean removeAll(Collection<?> c)
{
boolean success;
for (int i = 0, removed = 0; i <= this.currentIndex && i < this.size(); i++)
{
if (c.contains(this.get(i)))
{
this.currentIndex--;
}
}
this.queueOrder.removeAll(c);
success = super.removeAll(c);
this.triggerQueueChangeListeners();
return success;
}
@Override
public boolean retainAll(Collection<?> c)
{
boolean success;
for (int i = 0, removed = 0; i <= this.currentIndex && i < this.size(); i++)
{
if (!c.contains(this.get(i)))
{
this.currentIndex--;
}
}
this.queueOrder.retainAll(c);
success = super.retainAll(c);
this.triggerQueueChangeListeners();
return success;
}
/**
* Adds a listener for when the current song changes.
*
* @param listener - The listener to add.
*/
public void addSongChangeListener(SongChangeListener listener)
{
this.songListeners.add(listener);
}
/**
* Removes a listener for when the current song changes.
*
* @param listener - The listener to remove.
*/
public void removeSongChangeListener(SongChangeListener listener)
{
this.songListeners.remove(listener);
}
/**
* Tells all listeners that the song changed.
*/
protected void triggerSongChangeListeners()
{
this.songListeners.forEach(listener -> listener.onSongChange(this));
}
/**
* Adds a listener for when the queue contents changes.
*
* @param listener - The listener to add.
*/
public void addQueueChangeListener(QueueChangeListener listener)
{
this.queueListeners.add(listener);
}
/**
* Removes a listener for when the queue contents changes.
*
* @param listener - The listener to remove.
*/
public void removeQueueChangeListener(QueueChangeListener listener)
{
this.queueListeners.remove(listener);
}
/**
* Tells all listeners that the queue changed.
*/
protected void triggerQueueChangeListeners()
{
this.queueListeners.forEach(listener -> listener.onQueueChange(this));
}
public interface SongChangeListener extends EventListener
{
/**
* Called when the song of a queue changes.
*
* @param queue - The queue whose song changed.
*/
void onSongChange(Queue queue);
}
public interface QueueChangeListener extends EventListener
{
/**
* Called when the queue contents change.
*
* @param queue - The queue that changed.
*/
void onQueueChange(Queue queue);
}
}

View File

@@ -4,30 +4,38 @@
package edu.regis.universeplayer.player; package edu.regis.universeplayer.player;
import javax.swing.ImageIcon; import javax.swing.*;
import javax.swing.JLabel; import javax.swing.border.LineBorder;
import javax.swing.JPanel;
import javax.swing.SpringLayout;
import edu.regis.universeplayer.data.Album; import edu.regis.universeplayer.data.Album;
import java.awt.*;
import java.awt.event.*;
import java.util.Arrays;
/** /**
* This panel will display information on an album. * This panel will display information on an album.
*/ */
public class AlbumInfo extends JPanel public class AlbumInfo extends JButton
{ {
private Album album; public Album album;
private JLabel artLabel; public final JLabel artLabel;
private JLabel albumName; public final JLabel albumName;
private JLabel artists; public final JLabel artists;
private JLabel genres; public final JLabel genres;
private JLabel year; public final JLabel year;
public AlbumInfo() public AlbumInfo()
{ {
this.removeAll();
this.setContentAreaFilled(false);
this.setBorder(null);
SpringLayout infoLayout = new SpringLayout(); SpringLayout infoLayout = new SpringLayout();
this.setLayout(infoLayout); this.setLayout(infoLayout);
this.setFocusable(true);
this.setModel(new DefaultButtonModel());
this.artLabel = new JLabel(); this.artLabel = new JLabel();
this.add(this.artLabel); this.add(this.artLabel);
this.albumName = new JLabel("Album"); this.albumName = new JLabel("Album");
@@ -38,7 +46,7 @@ public class AlbumInfo extends JPanel
this.add(this.genres); this.add(this.genres);
this.year = new JLabel("20XX"); this.year = new JLabel("20XX");
this.add(this.year); this.add(this.year);
/* /*
* Set the layout information * Set the layout information
*/ */
@@ -52,27 +60,54 @@ public class AlbumInfo extends JPanel
infoLayout.putConstraint(SpringLayout.WEST, genres, 5, SpringLayout.EAST, artLabel); infoLayout.putConstraint(SpringLayout.WEST, genres, 5, SpringLayout.EAST, artLabel);
infoLayout.putConstraint(SpringLayout.NORTH, year, 5, SpringLayout.SOUTH, genres); infoLayout.putConstraint(SpringLayout.NORTH, year, 5, SpringLayout.SOUTH, genres);
infoLayout.putConstraint(SpringLayout.WEST, year, 5, SpringLayout.EAST, artLabel); infoLayout.putConstraint(SpringLayout.WEST, year, 5, SpringLayout.EAST, artLabel);
// infoLayout.putConstraint(SpringLayout.EAST, this, 0, SpringLayout.EAST, albumName);
infoLayout.putConstraint(SpringLayout.EAST, this, 0, SpringLayout.EAST, artists); int maxLength = -1;
// infoLayout.putConstraint(SpringLayout.EAST, this, 0, SpringLayout.EAST, genres); JLabel longest = null;
// infoLayout.putConstraint(SpringLayout.EAST, this, 0, SpringLayout.EAST, year); for (JLabel label : Arrays.asList(albumName, artists, genres, year))
{
if (label.getWidth() > maxLength)
{
longest = label;
maxLength = longest.getWidth();
}
}
infoLayout.putConstraint(SpringLayout.EAST, this, 0, SpringLayout.EAST, longest);
infoLayout.putConstraint(SpringLayout.SOUTH, this, 5, SpringLayout.EAST, artLabel); infoLayout.putConstraint(SpringLayout.SOUTH, this, 5, SpringLayout.EAST, artLabel);
while (this.getMouseListeners().length > 0)
{
this.removeMouseListener(this.getMouseListeners()[0]);
}
this.addFocusListener(new FocusAdapter()
{
@Override
public void focusGained(FocusEvent e)
{
((AlbumInfo) e.getComponent()).setBorder(new LineBorder(Color.GRAY, 1));
}
@Override
public void focusLost(FocusEvent e)
{
((AlbumInfo) e.getComponent()).setBorder(null);
}
});
} }
public AlbumInfo(Album album) public AlbumInfo(Album album)
{ {
this(); this();
this.updateInfo(album); this.updateInfo(album);
} }
public void updateInfo(Album album) public void updateInfo(Album album)
{ {
final int ART_SIZE = 128; final int ART_SIZE = 128;
ImageIcon icon; ImageIcon icon;
StringBuilder builder; StringBuilder builder;
this.album = album; this.album = album;
if (album.art != null) if (album.art != null)
{ {
icon = album.art; icon = album.art;
@@ -80,13 +115,13 @@ public class AlbumInfo extends JPanel
else else
{ {
icon = new ImageIcon(this.getClass() icon = new ImageIcon(this.getClass()
.getResource("/gui/icons/defaultart.png"), "Default"); .getResource("/gui/icons/defaultart.png"), "Default");
} }
icon.setImage(icon.getImage().getScaledInstance(ART_SIZE, ART_SIZE, 0)); icon.setImage(icon.getImage().getScaledInstance(ART_SIZE, ART_SIZE, 0));
this.artLabel.setIcon(icon); this.artLabel.setIcon(icon);
this.albumName.setText(album.name); this.albumName.setText(album.name);
builder = new StringBuilder(); builder = new StringBuilder();
if (album.artists != null && album.artists.length >= 1) if (album.artists != null && album.artists.length >= 1)
{ {
@@ -103,7 +138,7 @@ public class AlbumInfo extends JPanel
} }
} }
this.artists.setText(builder.toString()); this.artists.setText(builder.toString());
builder = new StringBuilder(); builder = new StringBuilder();
if (album.genres != null && album.genres.length >= 1) if (album.genres != null && album.genres.length >= 1)
{ {
@@ -120,7 +155,7 @@ public class AlbumInfo extends JPanel
} }
} }
this.genres.setText(builder.toString()); this.genres.setText(builder.toString());
this.year.setText(String.valueOf(album.year)); this.year.setText(String.valueOf(album.year));
} }
} }

View File

@@ -4,14 +4,16 @@
package edu.regis.universeplayer.player; package edu.regis.universeplayer.player;
import java.awt.FlowLayout; import java.awt.*;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.util.*; import java.util.*;
import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.swing.ImageIcon; import javax.swing.*;
import javax.swing.JLabel;
import javax.swing.JPanel;
import com.wordpress.tips4java.ScrollablePanel;
import edu.regis.universeplayer.ClickListener; import edu.regis.universeplayer.ClickListener;
import edu.regis.universeplayer.data.Album; import edu.regis.universeplayer.data.Album;
import edu.regis.universeplayer.data.CollectionType; import edu.regis.universeplayer.data.CollectionType;
@@ -24,7 +26,7 @@ import edu.regis.universeplayer.data.SongProvider;
* @author William Hubbard * @author William Hubbard
* @version 0.1 * @version 0.1
*/ */
public class CollectionList extends JPanel public class CollectionList extends ScrollablePanel
{ {
/** /**
* The type of collections being displayed. * The type of collections being displayed.
@@ -33,7 +35,7 @@ public class CollectionList extends JPanel
/** /**
* A link between the JLabel and the object they point towards. * A link between the JLabel and the object they point towards.
*/ */
private Map<JLabel, Object> labelMap = new HashMap<>(); private Map<JButton, Object> labelMap = new HashMap<>();
/** /**
* A list of all things interested in knowing when we click a collection. * A list of all things interested in knowing when we click a collection.
@@ -49,6 +51,16 @@ public class CollectionList extends JPanel
FlowLayout layout = new FlowLayout(); FlowLayout layout = new FlowLayout();
this.setLayout(layout); this.setLayout(layout);
this.setFocusCycleRoot(true);
// this.setFocusable(true);
this.addFocusListener(new FocusAdapter()
{
@Override
public void focusGained(FocusEvent e)
{
labelMap.keySet().stream().findFirst().ifPresent(JButton::requestFocusInWindow);
}
});
} }
/** /**
@@ -105,12 +117,13 @@ public class CollectionList extends JPanel
{ {
final int ART_SIZE = 128; final int ART_SIZE = 128;
JLabel artistLabel; JButton artistLabel;
ImageIcon icon; ImageIcon icon;
for (String artist : artists) for (String artist : artists)
{ {
artistLabel = new JLabel(); artistLabel = new JButton();
setButtonLook(artistLabel);
// TODO - Maybe add some sort of artist image lookup? // TODO - Maybe add some sort of artist image lookup?
// if (album.art != null) // if (album.art != null)
// { // {
@@ -126,7 +139,7 @@ public class CollectionList extends JPanel
artistLabel.setText(artist); artistLabel.setText(artist);
artistLabel.setHorizontalTextPosition(JLabel.CENTER); artistLabel.setHorizontalTextPosition(JLabel.CENTER);
artistLabel.setVerticalTextPosition(JLabel.BOTTOM); artistLabel.setVerticalTextPosition(JLabel.BOTTOM);
artistLabel.addMouseListener((ClickListener) mouseEvent -> { artistLabel.addActionListener(mouseEvent -> {
if (album) if (album)
{ {
this.triggerSongDisplayListeners(SongProvider.INSTANCE this.triggerSongDisplayListeners(SongProvider.INSTANCE
@@ -155,12 +168,13 @@ public class CollectionList extends JPanel
{ {
final int ART_SIZE = 128; final int ART_SIZE = 128;
JLabel albumLabel; JButton albumLabel;
ImageIcon icon; ImageIcon icon;
for (Album album : albums) for (Album album : albums)
{ {
albumLabel = new JLabel(); albumLabel = new JButton();
setButtonLook(albumLabel);
icon = Objects.requireNonNullElseGet(album.art, () -> new ImageIcon(this.getClass() icon = Objects.requireNonNullElseGet(album.art, () -> new ImageIcon(this.getClass()
.getResource("/gui/icons/defaultart.png"), "Default")); .getResource("/gui/icons/defaultart.png"), "Default"));
icon.setImage(icon.getImage().getScaledInstance(ART_SIZE, ART_SIZE, 0)); icon.setImage(icon.getImage().getScaledInstance(ART_SIZE, ART_SIZE, 0));
@@ -168,7 +182,7 @@ public class CollectionList extends JPanel
albumLabel.setText(album.name); albumLabel.setText(album.name);
albumLabel.setHorizontalTextPosition(JLabel.CENTER); albumLabel.setHorizontalTextPosition(JLabel.CENTER);
albumLabel.setVerticalTextPosition(JLabel.BOTTOM); albumLabel.setVerticalTextPosition(JLabel.BOTTOM);
albumLabel.addMouseListener((ClickListener) mouseEvent -> this.triggerSongDisplayListeners(new ArrayList<>(SongProvider.INSTANCE albumLabel.addActionListener(mouseEvent -> this.triggerSongDisplayListeners(new ArrayList<>(SongProvider.INSTANCE
.getSongsFromAlbum(album)))); .getSongsFromAlbum(album))));
this.add(albumLabel); this.add(albumLabel);
this.labelMap.put(albumLabel, album); this.labelMap.put(albumLabel, album);
@@ -184,12 +198,13 @@ public class CollectionList extends JPanel
{ {
// final int ART_SIZE = 128; // final int ART_SIZE = 128;
JLabel genreLabel; JButton genreLabel;
// ImageIcon icon; // ImageIcon icon;
for (String genre : genres) for (String genre : genres)
{ {
genreLabel = new JLabel(); genreLabel = new JButton();
setButtonLook(genreLabel);
// TODO - Maybe add some sort of artist image lookup? // TODO - Maybe add some sort of artist image lookup?
// if (album.art != null) // if (album.art != null)
// { // {
@@ -205,7 +220,7 @@ public class CollectionList extends JPanel
genreLabel.setText(genre); genreLabel.setText(genre);
genreLabel.setHorizontalTextPosition(JLabel.CENTER); genreLabel.setHorizontalTextPosition(JLabel.CENTER);
genreLabel.setVerticalTextPosition(JLabel.BOTTOM); genreLabel.setVerticalTextPosition(JLabel.BOTTOM);
genreLabel.addMouseListener((ClickListener) mouseEvent -> this.triggerSongDisplayListeners(SongProvider.INSTANCE genreLabel.addActionListener(mouseEvent -> this.triggerSongDisplayListeners(SongProvider.INSTANCE
.getAlbumsFromGenre(genre).stream() .getAlbumsFromGenre(genre).stream()
.flatMap(album2 -> SongProvider.INSTANCE.getSongsFromAlbum(album2) .flatMap(album2 -> SongProvider.INSTANCE.getSongsFromAlbum(album2)
.stream()) .stream())
@@ -223,13 +238,14 @@ public class CollectionList extends JPanel
private void addYears(List<Integer> years) private void addYears(List<Integer> years)
{ {
// final int ART_SIZE = 128; // final int ART_SIZE = 128;
JLabel yearLabel; JButton yearLabel;
// ImageIcon icon; // ImageIcon icon;
for (Integer year : years) for (Integer year : years)
{ {
yearLabel = new JLabel(); yearLabel = new JButton();
setButtonLook(yearLabel);
// TODO - Maybe add some sort of artist image lookup? // TODO - Maybe add some sort of artist image lookup?
// if (album.art != null) // if (album.art != null)
// { // {
@@ -245,7 +261,7 @@ public class CollectionList extends JPanel
yearLabel.setText(year.toString()); yearLabel.setText(year.toString());
yearLabel.setHorizontalTextPosition(JLabel.CENTER); yearLabel.setHorizontalTextPosition(JLabel.CENTER);
yearLabel.setVerticalTextPosition(JLabel.BOTTOM); yearLabel.setVerticalTextPosition(JLabel.BOTTOM);
yearLabel.addMouseListener((ClickListener) mouseEvent -> this.triggerSongDisplayListeners(SongProvider.INSTANCE yearLabel.addActionListener(mouseEvent -> this.triggerSongDisplayListeners(SongProvider.INSTANCE
.getAlbumsFromYear(year).stream() .getAlbumsFromYear(year).stream()
.flatMap(album2 -> SongProvider.INSTANCE.getSongsFromAlbum(album2) .flatMap(album2 -> SongProvider.INSTANCE.getSongsFromAlbum(album2)
.stream()) .stream())
@@ -255,6 +271,15 @@ public class CollectionList extends JPanel
} }
} }
private void setButtonLook(JButton button)
{
button.setFocusPainted(true);
button.setMargin(new Insets(0, 0, 0, 0));
button.setContentAreaFilled(false);
button.setBorderPainted(false);
button.setOpaque(false);
}
/** /**
* Adds a listener for when the displayed songs should change. * Adds a listener for when the displayed songs should change.
* *

View File

@@ -4,14 +4,15 @@
package edu.regis.universeplayer.player; package edu.regis.universeplayer.player;
import java.awt.Color; import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.LinkedList; import java.util.LinkedList;
import javax.swing.BoxLayout; import javax.swing.*;
import javax.swing.JLabel; import javax.swing.border.LineBorder;
import javax.swing.JPanel; import javax.swing.plaf.LabelUI;
import edu.regis.universeplayer.ClickListener; import edu.regis.universeplayer.ClickListener;
import edu.regis.universeplayer.data.CollectionType; import edu.regis.universeplayer.data.CollectionType;
@@ -36,37 +37,85 @@ public class Collections extends JPanel
*/ */
public Collections() public Collections()
{ {
JLabel label; JButton defaultLabel;
JButton label;
BoxLayout layout = new BoxLayout(this, BoxLayout.Y_AXIS); BoxLayout layout = new BoxLayout(this, BoxLayout.Y_AXIS);
this.setLayout(layout); this.setLayout(layout);
this.setFocusable(true);
this.setFocusCycleRoot(true);
this.add(label = new JLabel("All Songs")); this.add(defaultLabel = label = this.createButton("All Songs"));
label.setForeground(Color.BLUE); label.setMnemonic('A');
label.addMouseListener((ClickListener) mouseEvent -> this label.addActionListener(mouseEvent -> this
.triggerSongDisplayListeners(new ArrayList<>(SongProvider.INSTANCE.getSongs()))); .triggerSongDisplayListeners(new ArrayList<>(SongProvider.INSTANCE.getSongs())));
this.add(label = new JLabel("Artists")); this.add(label = this.createButton("Artists"));
label.setForeground(Color.BLUE); label.setMnemonic('T');
label.addMouseListener((ClickListener) mouseEvent -> this label.addActionListener(mouseEvent -> this
.triggerCollectionDisplayListeners(CollectionType.albumArtist, SongProvider.INSTANCE .triggerCollectionDisplayListeners(CollectionType.albumArtist, SongProvider.INSTANCE
.getAlbumArtists())); .getAlbumArtists()));
this.add(label = new JLabel("Albums")); this.add(label = this.createButton("Albums"));
label.setForeground(Color.BLUE); label.setMnemonic('B');
label.addMouseListener((ClickListener) mouseEvent -> this label.addActionListener(mouseEvent -> this
.triggerCollectionDisplayListeners(CollectionType.album, SongProvider.INSTANCE .triggerCollectionDisplayListeners(CollectionType.album, SongProvider.INSTANCE
.getAlbums())); .getAlbums()));
this.add(label = new JLabel("Genres")); this.add(label = this.createButton("Genres"));
label.setForeground(Color.BLUE); label.setMnemonic('G');
label.addMouseListener((ClickListener) mouseEvent -> this label.addActionListener(mouseEvent -> this
.triggerCollectionDisplayListeners(CollectionType.genre, SongProvider.INSTANCE .triggerCollectionDisplayListeners(CollectionType.genre, SongProvider.INSTANCE
.getGenres())); .getGenres()));
this.add(label = new JLabel("Years")); this.add(label = this.createButton("Years"));
label.setForeground(Color.BLUE); label.setMnemonic('Y');
label.addMouseListener((ClickListener) mouseEvent -> this label.addActionListener(mouseEvent -> this
.triggerCollectionDisplayListeners(CollectionType.year, SongProvider.INSTANCE .triggerCollectionDisplayListeners(CollectionType.year, SongProvider.INSTANCE
.getYears())); .getYears()));
this.add(new JLabel("\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF")); this.add(new JLabel("\u23AF".repeat(6)));
this.add(label = new JLabel("Playlists")); this.add(label = this.createButton("Playlists"));
label.setMnemonic('P');
addMouseListener(new MouseAdapter()
{
@Override
public void mouseClicked(MouseEvent e)
{
defaultLabel.requestFocusInWindow();
}
});
addFocusListener(new FocusAdapter()
{
@Override
public void focusGained(FocusEvent e)
{
int index = -1;
Component[] children = ((Container) e.getComponent()).getComponents();
for (int i = 0, l = children.length; index == -1 && i < l; i++)
{
if (children[i] == e.getOppositeComponent())
{
index = i;
}
}
/*
* Only auto-switch focus if the previous component was not from
* within.
*/
if (index == -1)
{
defaultLabel.requestFocusInWindow();
}
}
});
}
private JButton createButton(String text)
{
JButton label = new JButton(text);
label.setFocusPainted(true);
label.setMargin(new Insets(0, 0, 0, 0));
label.setContentAreaFilled(false);
label.setBorderPainted(false);
label.setOpaque(false);
label.setForeground(Color.BLUE); label.setForeground(Color.BLUE);
return label;
} }
/** /**

View File

@@ -0,0 +1,14 @@
/*
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
*/
package edu.regis.universeplayer.player;
import java.awt.*;
public class FocusManager extends DefaultKeyboardFocusManager
{
public FocusManager()
{
}
}

View File

@@ -6,25 +6,21 @@ package edu.regis.universeplayer.player;
import edu.regis.universeplayer.Player; import edu.regis.universeplayer.Player;
import edu.regis.universeplayer.browser.Browser; import edu.regis.universeplayer.browser.Browser;
import edu.regis.universeplayer.data.CollectionType; import edu.regis.universeplayer.data.Queue;
import edu.regis.universeplayer.data.Song; import edu.regis.universeplayer.data.*;
import edu.regis.universeplayer.data.SongProvider;
import edu.regis.universeplayer.data.UpdateListener;
import net.harawata.appdirs.AppDirsFactory; import net.harawata.appdirs.AppDirsFactory;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.swing.*; import javax.swing.*;
import java.awt.*; import java.awt.*;
import java.awt.event.ComponentEvent; import java.awt.event.*;
import java.awt.event.ComponentListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.Set;
import java.util.concurrent.Future; import java.util.concurrent.Future;
/** /**
@@ -33,7 +29,7 @@ import java.util.concurrent.Future;
* @author William Hubbard * @author William Hubbard
* @version 0.1 * @version 0.1
*/ */
public class Interface extends JFrame implements SongDisplayListener, ComponentListener, WindowListener, PlaybackCommandListener, UpdateListener public class Interface extends JFrame implements SongDisplayListener, ComponentListener, WindowListener, PlaybackCommandListener, UpdateListener, FocusListener
{ {
private static final Logger logger = LoggerFactory.getLogger(Interface.class); private static final Logger logger = LoggerFactory.getLogger(Interface.class);
@@ -44,6 +40,10 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
* A reference to the panel containing links to different collection views. * A reference to the panel containing links to different collection views.
*/ */
private final Collections collectionTypes; private final Collections collectionTypes;
/**
* A reference to the panel showing the queue.
*/
private final QueueList queueList;
/** /**
* A reference to the central view showing a list of songs. * A reference to the central view showing a list of songs.
*/ */
@@ -81,8 +81,10 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
*/ */
logger.info("Starting application"); logger.info("Starting application");
inter = new Interface(); inter = new Interface();
inter.pack(); inter.setSize(700, 500);
SongProvider.INSTANCE.addUpdateListener(inter);
inter.setVisible(true); inter.setVisible(true);
try try
{ {
@@ -118,8 +120,6 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
logger.error("Could not open browser background", e); logger.error("Could not open browser background", e);
JOptionPane.showMessageDialog(inter, e, "Could not open browser background", JOptionPane.ERROR_MESSAGE); JOptionPane.showMessageDialog(inter, e, "Could not open browser background", JOptionPane.ERROR_MESSAGE);
} }
SongProvider.INSTANCE.addUpdateListener(inter);
} }
catch (Throwable e) catch (Throwable e)
{ {
@@ -179,14 +179,27 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
this.setTitle("Universal Music Player"); this.setTitle("Universal Music Player");
this.getContentPane().setLayout(new BorderLayout()); this.getContentPane().setLayout(new BorderLayout());
this.setFocusable(true);
this.setFocusCycleRoot(true);
this.getContentPane() this.getContentPane()
.add(this.collectionTypes = new Collections(), BorderLayout.LINE_START); .add(this.collectionTypes = new Collections(), BorderLayout.LINE_START);
this.collectionTypes.addFocusListener(this);
this.collectionTypes.addSongDisplayListener(this); this.collectionTypes.addSongDisplayListener(this);
this.controls = new PlayerControls(); this.controls = new PlayerControls();
this.controls.addFocusListener(this);
controls.addCommandListener(this); controls.addCommandListener(this);
this.getContentPane().add(controls, BorderLayout.PAGE_END); this.getContentPane().add(controls, BorderLayout.PAGE_END);
this.queueList = new QueueList();
this.queueList.addFocusListener(this);
Queue.getInstance().addQueueChangeListener(this.queueList);
Queue.getInstance().addSongChangeListener(this.queueList);
this.getContentPane().add(queueList, BorderLayout.LINE_END);
this.songList = new SongList(); this.songList = new SongList();
this.songList.addFocusListener(this);
this.collectionList = new CollectionList(); this.collectionList = new CollectionList();
this.collectionList.addSongDisplayListener(this); this.collectionList.addSongDisplayListener(this);
@@ -196,7 +209,13 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
this.addComponentListener(this); this.addComponentListener(this);
this.addWindowListener(this); this.addWindowListener(this);
this.addFocusListener(this);
((SortingFocusTraversalPolicy) this.getFocusTraversalPolicy()).setImplicitDownCycleTraversal(true);
this.setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, Set.of(AWTKeyStroke.getAWTKeyStroke(KeyEvent.VK_DOWN, 0), AWTKeyStroke.getAWTKeyStroke(KeyEvent.VK_RIGHT, 0)));
this.setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, Set.of(AWTKeyStroke.getAWTKeyStroke(KeyEvent.VK_UP, 0), AWTKeyStroke.getAWTKeyStroke(KeyEvent.VK_LEFT, 0)));
this.setFocusTraversalKeys(KeyboardFocusManager.UP_CYCLE_TRAVERSAL_KEYS, Set.of(AWTKeyStroke.getAWTKeyStroke(KeyEvent.VK_TAB, 0)));
this.setFocusTraversalKeys(KeyboardFocusManager.DOWN_CYCLE_TRAVERSAL_KEYS, Set.of(AWTKeyStroke.getAWTKeyStroke(KeyEvent.VK_TAB, KeyEvent.SHIFT_DOWN_MASK)));
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.updateSongs(SongProvider.INSTANCE.getSongs()); this.updateSongs(SongProvider.INSTANCE.getSongs());
@@ -207,11 +226,12 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
{ {
this.songList.listAlbums(songs); this.songList.listAlbums(songs);
this.centerView.setViewportView(this.songList); this.centerView.setViewportView(this.songList);
this.songList.setPreferredSize(new Dimension(this.centerView.getViewport() // this.songList.setPreferredSize(new Dimension(this.centerView.getViewport()
.getExtentSize().width, Integer.MAX_VALUE)); // .getExtentSize().width, Integer.MAX_VALUE));
this.songList.setPreferredSize(new Dimension(this.centerView.getViewport() this.songList.setPreferredSize(new Dimension(this.centerView.getViewport()
.getExtentSize().width, this.songList .getExtentSize().width, this.songList
.getMinimumSize().height)); .getMinimumSize().height));
this.centerView.validate();
} }
@Override @Override
@@ -219,11 +239,12 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
{ {
this.collectionList.listCollection(type, collections); this.collectionList.listCollection(type, collections);
this.centerView.setViewportView(this.collectionList); this.centerView.setViewportView(this.collectionList);
this.collectionList.setPreferredSize(new Dimension(this.centerView.getViewport() // this.collectionList.setPreferredSize(new Dimension(this.centerView.getViewport()
.getExtentSize().width, Integer.MAX_VALUE)); // .getExtentSize().width, Integer.MAX_VALUE));
this.collectionList.setPreferredSize(new Dimension(this.centerView.getViewport() this.collectionList.setPreferredSize(new Dimension(this.centerView.getViewport()
.getExtentSize().width, this.collectionList .getExtentSize().width, this.collectionList
.getMinimumSize().height)); .getMinimumSize().height));
this.centerView.validate();
} }
@Override @Override
@@ -309,15 +330,11 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
@Override @Override
public void onCommand(PlaybackCommand command, Object data) public void onCommand(PlaybackCommand command, Object data)
{ {
Player player; Player player = null;
if (this.currentPlayer >= 0 && this.currentPlayer < this.players.size()) if (this.currentPlayer >= 0 && this.currentPlayer < this.players.size())
{ {
player = this.players.get(this.currentPlayer); player = this.players.get(this.currentPlayer);
} }
else
{
throw new NullPointerException("No player available");
}
switch (command) switch (command)
{ {
case PLAY -> { case PLAY -> {
@@ -328,10 +345,8 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
} }
case PAUSE -> { case PAUSE -> {
} }
case NEXT -> { case NEXT -> Queue.getInstance().skipNext();
} case PREVIOUS -> Queue.getInstance().skipPrev();
case PREVIOUS -> {
}
case SEEK -> { case SEEK -> {
} }
} }
@@ -350,4 +365,85 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
this.updateSongs(SongProvider.INSTANCE.getSongs()); this.updateSongs(SongProvider.INSTANCE.getSongs());
} }
} }
@Override
public void focusGained(FocusEvent e)
{
Component parent;
if (e.getOppositeComponent() == null)
{
collectionTypes.requestFocusInWindow();
return;
}
parent = e.getOppositeComponent().getParent();
if (e.getComponent() == this)
{
if (parent == collectionTypes)
{
centerView.getViewport().getView().requestFocusInWindow();
}
else if (parent == songList || parent == collectionList)
{
queueList.requestFocusInWindow();
}
else if (parent == queueList.songList || parent == queueList.header)
{
controls.requestFocusInWindow();
}
else if (parent == controls)
{
collectionTypes.requestFocusInWindow();
}
else
{
logger.warn("Unrecognized parent {}", parent);
collectionTypes.requestFocusInWindow();
}
}
/*
* We are transfering backwards from an inner element
*/
else
{
int index = -1;
Component[] children = ((Container) e.getComponent()).getComponents();
for (int i = 0, l = children.length; index == -1 && i < l; i++)
{
if (children[i] == e.getOppositeComponent())
{
index = i;
}
}
if (index != -1)
{
parent = e.getComponent();
if (parent == collectionTypes)
{
centerView.getViewport().getView().requestFocusInWindow();
}
else if (parent == songList || parent == collectionList)
{
queueList.requestFocusInWindow();
}
else if (parent == controls)
{
controls.requestFocusInWindow();
}
else if (parent == queueList.songList || parent == queueList.header)
{
collectionTypes.requestFocusInWindow();
}
else
{
logger.warn("Unrecognized parent {}", parent);
collectionTypes.requestFocusInWindow();
}
}
}
}
@Override
public void focusLost(FocusEvent e)
{
}
} }

View File

@@ -4,8 +4,9 @@
package edu.regis.universeplayer.player; package edu.regis.universeplayer.player;
import java.awt.Dimension; import java.awt.*;
import java.awt.FlowLayout; import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.util.LinkedList; import java.util.LinkedList;
import javax.swing.*; import javax.swing.*;
@@ -40,6 +41,8 @@ public class PlayerControls extends JPanel
SpringLayout layout = new SpringLayout(); SpringLayout layout = new SpringLayout();
this.setLayout(layout); this.setLayout(layout);
this.setFocusable(true);
this.setFocusCycleRoot(false);
buttonLayout = new FlowLayout(); buttonLayout = new FlowLayout();
buttonCont = new JPanel(buttonLayout); buttonCont = new JPanel(buttonLayout);
@@ -82,6 +85,31 @@ public class PlayerControls extends JPanel
this.updateProgress.setStringPainted(true); this.updateProgress.setStringPainted(true);
this.setUpdateProgress(0, 0, null); this.setUpdateProgress(0, 0, null);
this.add(this.updateProgress); this.add(this.updateProgress);
this.addFocusListener(new FocusAdapter()
{
@Override
public void focusGained(FocusEvent e)
{
int index = -1;
Component[] children = ((Container) e.getComponent()).getComponents();
for (int i = 0, l = children.length; index == -1 && i < l; i++)
{
if (children[i] == e.getOppositeComponent())
{
index = i;
}
}
/*
* Only auto-switch focus if the previous component was not from
* within.
*/
if (index == -1)
{
prevButton.requestFocusInWindow();
}
}
});
layout.putConstraint(SpringLayout.NORTH, buttonCont, 0, SpringLayout.NORTH, this); layout.putConstraint(SpringLayout.NORTH, buttonCont, 0, SpringLayout.NORTH, this);
layout.putConstraint(SpringLayout.WEST, buttonCont, 0, SpringLayout.WEST, this); layout.putConstraint(SpringLayout.WEST, buttonCont, 0, SpringLayout.WEST, this);

View File

@@ -0,0 +1,224 @@
/*
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
*/
package edu.regis.universeplayer.player;
import com.wordpress.tips4java.ScrollablePanel;
import edu.regis.universeplayer.ClickListener;
import edu.regis.universeplayer.data.Queue;
import edu.regis.universeplayer.data.Song;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.*;
import javax.swing.border.BevelBorder;
import javax.swing.border.LineBorder;
import java.awt.*;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.util.Formatter;
public class QueueList extends JPanel implements Queue.SongChangeListener, Queue.QueueChangeListener
{
private static final Logger logger = LoggerFactory.getLogger(QueueList.class);
private final JScrollPane scroll;
final JPanel header;
final ScrollablePanel songList;
private int currentHighlight = 0;
private JButton clearButton;
public QueueList()
{
GridBagConstraints c = new GridBagConstraints();
this.setLayout(new BorderLayout());
this.setFocusable(true);
// this.setFocusCycleRoot(true);
header = new JPanel(new GridBagLayout());
// header.setFocusCycleRoot(true);
c.fill = GridBagConstraints.HORIZONTAL;
c.gridx = 0;
c.gridy = 0;
c.gridwidth = 1;
header.add(new JLabel(" ".repeat(20) + "Queue" + " ".repeat(20)), c);
this.clearButton = new JButton("Clear");
this.clearButton.addActionListener(e -> {
Queue.getInstance().clear();
});
c.gridy = 1;
c.gridwidth = 1;
header.add(clearButton, c);
header.addFocusListener(new FocusAdapter()
{
@Override
public void focusGained(FocusEvent e)
{
int index = -1;
Component[] children = ((Container) e.getComponent()).getComponents();
for (int i = 0, l = children.length; index == -1 && i < l; i++)
{
if (children[i] == e.getOppositeComponent())
{
index = i;
}
}
/*
* Only auto-switch focus if the previous component was not from
* within.
*/
if (index == -1)
{
clearButton.requestFocusInWindow();
}
}
});
this.add(header, BorderLayout.NORTH);
this.songList = new ScrollablePanel(new GridLayout(0, 2));
/*
* Don't focus on here until we have elements.
*/
this.songList.setFocusable(false);
this.songList.setFocusCycleRoot(true);
this.songList.addFocusListener(new FocusAdapter()
{
@Override
public void focusGained(FocusEvent e)
{
int index = -1;
Component[] children = ((Container) e.getComponent()).getComponents();
for (int i = 0, l = children.length; index == -1 && i < l; i++)
{
if (children[i] == e.getOppositeComponent())
{
index = i;
}
}
/*
* Only auto-switch focus if the previous component was not from
* within.
*/
if (index == -1)
{
songList.getComponents()[0].requestFocusInWindow();
}
else
{
header.requestFocusInWindow();
}
}
});
this.songList.setScrollableWidth(ScrollablePanel.ScrollableSizeHint.FIT);
this.scroll = new JScrollPane(this.songList);
// this.scroll.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
// this.scroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
this.add(this.scroll, BorderLayout.CENTER);
this.addFocusListener(new FocusAdapter()
{
@Override
public void focusGained(FocusEvent e)
{
int index = -1;
Component[] children = ((Container) e.getComponent()).getComponents();
for (int i = 0, l = children.length; index == -1 && i < l; i++)
{
if (children[i] == e.getOppositeComponent())
{
index = i;
}
}
/*
* Only auto-switch focus if the previous component was not from
* within.
*/
if (index == -1)
{
header.requestFocusInWindow();
}
}
});
}
@Override
public void onSongChange(Queue queue)
{
if (this.currentHighlight >= 0)
{
this.songList.getComponent(this.currentHighlight * 2).setForeground(Color.BLACK);
}
this.currentHighlight = queue.getCurrentIndex();
if (this.currentHighlight >= 0)
{
this.songList.getComponent(this.currentHighlight * 2).setForeground(Color.BLUE);
}
}
@Override
public void onQueueChange(Queue queue)
{
Formatter dateForm;
JButton songLabel;
JLabel durationLabel;
GridBagConstraints c;
this.songList.removeAll();
int i = 0;
for (Song song : queue)
{
songLabel = new JButton(song.title);
songLabel.setFocusPainted(true);
songLabel.setMargin(new Insets(0, 0, 0, 0));
songLabel.setContentAreaFilled(false);
songLabel.setBorderPainted(false);
songLabel.setOpaque(false);
songLabel.setHorizontalAlignment(JLabel.LEFT);
int songIndex = i;
songLabel.addMouseListener((ClickListener) e -> {
if (e.getClickCount() == 2)
{
queue.skipToSong(songIndex);
}
});
dateForm = new Formatter();
durationLabel = new JLabel(dateForm.format("%1$tM:%1$tS", song.duration).toString());
durationLabel.setHorizontalAlignment(JLabel.RIGHT);
durationLabel.addMouseListener((ClickListener) e -> {
if (e.getClickCount() == 2)
{
queue.skipToSong(songIndex);
}
});
dateForm.close();
c = new GridBagConstraints();
c.gridx = 0;
c.gridy = i;
c.anchor = GridBagConstraints.WEST;
c.insets = new Insets(0, 0, 0, 5);
c.fill = GridBagConstraints.HORIZONTAL;
this.songList.add(songLabel);
c = new GridBagConstraints();
c.gridx = 1;
c.gridy = i;
c.anchor = GridBagConstraints.EAST;
c.insets = new Insets(0, 0, 0, 0);
this.songList.add(durationLabel);
songLabel.setForeground(Color.BLACK);
i++;
}
this.songList.setFocusable(queue.size() > 0);
this.currentHighlight = queue.getCurrentIndex();
if (this.currentHighlight >= 0)
{
this.songList.getComponent(queue.getCurrentIndex() * 2).setForeground(Color.BLUE);
}
this.validate();
}
}

View File

@@ -4,29 +4,25 @@
package edu.regis.universeplayer.player; package edu.regis.universeplayer.player;
import java.awt.GridBagConstraints; import com.wordpress.tips4java.ScrollablePanel;
import java.awt.GridBagLayout; import edu.regis.universeplayer.ClickListener;
import java.awt.Insets; import edu.regis.universeplayer.data.Queue;
import java.util.Collection; import edu.regis.universeplayer.data.*;
import java.util.HashMap;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.swing.JLabel;
import javax.swing.JPanel;
import edu.regis.universeplayer.data.Album;
import edu.regis.universeplayer.data.Song;
import edu.regis.universeplayer.data.SongProvider;
/** /**
* This panel will list all the songs that are to be currently displayed. * This panel will list all the songs that are to be currently displayed.
*/ */
public class SongList extends JPanel public class SongList extends ScrollablePanel
{ {
private Map<Album, List<Song>> currentAlbums; private Map<Album, List<Song>> currentAlbums;
private Map<JLabel, Song> labelMap = new HashMap<>(); private Map<JComponent, Song> labelMap = new HashMap<>();
private Map<AlbumInfo, Album> artMap = new HashMap<>(); private Map<AlbumInfo, Album> artMap = new HashMap<>();
public SongList() public SongList()
@@ -34,10 +30,37 @@ public class SongList extends JPanel
super(); super();
GridBagLayout layout = new GridBagLayout(); GridBagLayout layout = new GridBagLayout();
this.setFocusTraversalPolicyProvider(true);
this.setLayout(layout); this.setLayout(layout);
SongProvider<?> provider = SongProvider.INSTANCE; SongProvider<?> provider = SongProvider.INSTANCE;
this.listAlbums(provider.getSongs()); this.listAlbums(provider.getSongs());
this.setScrollableWidth(ScrollableSizeHint.FIT);
this.setFocusTraversalPolicy(new SongListPolicy());
this.addFocusListener(new FocusAdapter()
{
@Override
public void focusGained(FocusEvent e)
{
int index = -1;
Component[] children = ((Container) e.getComponent()).getComponents();
for (int i = 0, l = children.length; index == -1 && i < l; i++)
{
if (children[i] == e.getOppositeComponent())
{
index = i;
}
}
if (index == -1)
{
artMap.keySet().stream().findFirst().ifPresent(albumInfo -> {
albumInfo.requestFocusInWindow();
scrollRectToVisible(albumInfo.getBounds());
});
}
}
});
} }
/** /**
@@ -50,15 +73,13 @@ public class SongList extends JPanel
Map<Album, List<Song>> albums = songs.stream().sorted().collect(Collectors Map<Album, List<Song>> albums = songs.stream().sorted().collect(Collectors
.groupingBy(song -> song.album, Collectors .groupingBy(song -> song.album, Collectors
.mapping(song -> (Song) song, Collectors.toList()))); .mapping(song -> (Song) song, Collectors.toList())));
GridBagConstraints c = new GridBagConstraints(), c2 = new GridBagConstraints(); GridBagConstraints c = new GridBagConstraints();
c.fill = GridBagConstraints.NONE; c.fill = GridBagConstraints.HORIZONTAL;
c.insets = new Insets(0, 0, 20, 0); int i = 0;
int i = 0, j;
AlbumInfo albumInfo;
List<Song> songCollection; List<Song> songCollection;
JPanel songList; JLabel songNum;
JLabel songNum, songTitle; JButton songTitle;
this.labelMap.clear(); this.labelMap.clear();
this.artMap.clear(); this.artMap.clear();
this.removeAll(); this.removeAll();
@@ -67,43 +88,276 @@ public class SongList extends JPanel
for (Album album : albums.keySet()) for (Album album : albums.keySet())
{ {
songCollection = albums.get(album); songCollection = albums.get(album);
albumInfo = new AlbumInfo(album); AlbumInfo albumInfo = new AlbumInfo(album);
c.gridx = 0; c.gridx = 0;
c.gridy = i; c.gridy = i;
c.anchor = GridBagConstraints.WEST; c.gridwidth = 1;
c.gridheight = songCollection.size();
c.weightx = 0;
c.anchor = GridBagConstraints.NORTHWEST;
c.insets = new Insets(0, 0, 20, 10);
List<Song> finalSongCollection = songCollection;
albumInfo.addMouseListener((ClickListener) e -> {
if (e.getClickCount() == 2)
{
Queue.getInstance().addAll(finalSongCollection);
}
});
albumInfo.albumName.addMouseListener((ClickListener) e -> {
if (e.getClickCount() == 2)
{
Container inter = this;
do
{
inter = inter.getParent();
}
while (!(inter instanceof Interface) && inter.getParent() != null);
if (inter instanceof Interface)
{
((Interface) inter).updateSongs(SongProvider.INSTANCE.getSongsFromAlbum(albumInfo.album));
}
}
});
albumInfo.artists.addMouseListener((ClickListener) e -> {
if (e.getClickCount() == 2)
{
Container inter = this;
do
{
inter = inter.getParent();
}
while (!(inter instanceof Interface) && inter.getParent() != null);
if (inter instanceof Interface)
{
((Interface) inter).updateCollections(CollectionType.album, Arrays.stream(albumInfo.album.artists).flatMap(s -> SongProvider.INSTANCE.getAlbumsFromArtist(s).stream()).collect(Collectors.toList()));
}
}
});
albumInfo.genres.addMouseListener((ClickListener) e -> {
if (e.getClickCount() == 2)
{
Container inter = this;
do
{
inter = inter.getParent();
}
while (!(inter instanceof Interface) && inter.getParent() != null);
if (inter instanceof Interface)
{
((Interface) inter).updateCollections(CollectionType.album, Arrays.stream(albumInfo.album.genres).flatMap(s -> SongProvider.INSTANCE.getAlbumsFromGenre(s).stream()).collect(Collectors.toList()));
}
}
});
albumInfo.year.addMouseListener((ClickListener) e -> {
if (e.getClickCount() == 2)
{
Container inter = this;
do
{
inter = inter.getParent();
}
while (!(inter instanceof Interface) && inter.getParent() != null);
if (inter instanceof Interface)
{
((Interface) inter).updateCollections(CollectionType.album, SongProvider.INSTANCE.getAlbumsFromYear(albumInfo.album.year));
}
}
});
this.add(albumInfo, c); this.add(albumInfo, c);
this.artMap.put(albumInfo, album); this.artMap.put(albumInfo, album);
songList = new JPanel(new GridBagLayout()); JButton firstSong = null;
j = 0;
for (Song song : songCollection) for (Song song : songCollection)
{ {
songNum = new JLabel(String.valueOf(song.trackNum)); songNum = new JLabel(String.valueOf(song.trackNum));
c2.gridx = 0; songNum.setFocusable(false);
c2.gridy = j; c.gridx = 1;
c2.anchor = GridBagConstraints.EAST; c.gridy = i;
c2.insets = new Insets(0, 0, 0, 0); c.gridheight = 1;
songList.add(songNum, c2); c.weightx = 0;
c.anchor = GridBagConstraints.NORTHEAST;
c.insets = new Insets(0, 0, 0, 0);
this.add(songNum, c);
this.labelMap.put(songNum, song); this.labelMap.put(songNum, song);
songTitle = new JLabel(song.title); songTitle = new JButton(song.title);
c2.gridx = 1; songTitle.setHorizontalAlignment(JButton.LEFT);
c2.gridy = j; songTitle.setFocusPainted(true);
c2.anchor = GridBagConstraints.WEST; songTitle.setMargin(new Insets(0, 0, 0, 0));
c2.insets = new Insets(0, 10, 0, 0); songTitle.setContentAreaFilled(false);
songList.add(songTitle, c2); songTitle.setBorderPainted(false);
songTitle.setOpaque(false);
songTitle.addActionListener(new AbstractAction()
{
@Override
public void actionPerformed(ActionEvent e)
{
Queue.getInstance().add(song);
Queue.getInstance().skipToSong(Queue.getInstance().size() - 1);
}
});
c.gridx = 2;
c.gridy = i;
c.weightx = 1.0;
c.anchor = GridBagConstraints.NORTHWEST;
c.insets = new Insets(0, 10, 0, 0);
this.add(songTitle, c);
this.labelMap.put(songTitle, song); this.labelMap.put(songTitle, song);
// TODO - Add song length or something // TODO - Add song length or something
j++; if (firstSong == null)
{
firstSong = songTitle;
JButton finalFirstSong = firstSong;
albumInfo.setAction(new AbstractAction()
{
@Override
public void actionPerformed(ActionEvent e)
{
finalFirstSong.requestFocusInWindow();
}
});
List<Song> finalSongCollection1 = songCollection;
albumInfo.addKeyListener(new KeyAdapter()
{
@Override
public void keyTyped(KeyEvent e)
{
if (e.getKeyCode() == KeyEvent.VK_ENTER)
{
Queue.getInstance().addAll(finalSongCollection1);
}
}
});
}
i++;
} }
c.gridx = 1;
c.anchor = GridBagConstraints.WEST;
this.add(songList, c);
// this.add(new JLabel(new ImageIcon(this.getClass().getResource("/gui/icons/defaultart.png"), "Default")), c); // this.add(new JLabel(new ImageIcon(this.getClass().getResource("/gui/icons/defaultart.png"), "Default")), c);
c.gridx = 0;
c.gridy = i++;
c.gridwidth = 3;
c.anchor = GridBagConstraints.NORTH;
this.add(new JSeparator(SwingConstants.HORIZONTAL), c);
i++; i++;
} }
} }
private class SongListPolicy extends FocusTraversalPolicy
{
@Override
public Component getComponentAfter(Container aContainer, Component aComponent)
{
Component[] children = aContainer.getComponents();
int index = -1;
for (int i = 0, l = aContainer.getComponentCount(); index == -1 && i < l; i++)
{
if (children[i] == aComponent)
{
index = i;
}
}
if (index != -1)
{
if (aComponent instanceof AlbumInfo)
{
for (int i = index + 1, l = aContainer.getComponentCount(); i < l; i++)
{
if (children[i] instanceof AlbumInfo)
{
scrollRectToVisible(children[i].getBounds());
return children[i];
}
}
}
else if (aComponent instanceof JButton)
{
for (int i = index + 1, l = aContainer.getComponentCount(); i < l; i++)
{
if (children[i].isFocusable())
{
scrollRectToVisible(children[i].getBounds());
return children[i];
}
}
}
}
return null;
}
@Override
public Component getComponentBefore(Container aContainer, Component aComponent)
{
Component[] children = aContainer.getComponents();
int index = -1;
for (int i = 0, l = aContainer.getComponentCount(); index == -1 && i < l; i++)
{
if (children[i] == aComponent)
{
index = i;
}
}
if (index != -1)
{
if (aComponent instanceof AlbumInfo)
{
for (int i = index - 1; i >= 0; i--)
{
if (children[i] instanceof AlbumInfo)
{
scrollRectToVisible(children[i].getBounds());
return children[i];
}
}
}
else if (aComponent instanceof JButton)
{
for (int i = index - 1; i >= 0; i--)
{
if (children[i].isFocusable())
{
scrollRectToVisible(children[i].getBounds());
return children[i];
}
}
}
}
return null;
}
@Override
public Component getFirstComponent(Container aContainer)
{
Component comp = Arrays.stream(aContainer.getComponents()).filter(a -> a instanceof AlbumInfo).findFirst().orElse(null);
if (comp != null)
{
scrollRectToVisible(comp.getBounds());
}
return comp;
}
@Override
public Component getLastComponent(Container aContainer)
{
Component[] matching = Arrays.stream(aContainer.getComponents()).filter(a -> a instanceof JButton).toArray(Component[]::new);
if (matching.length > 0)
{
scrollRectToVisible(matching[matching.length - 1].getBounds());
return matching[matching.length - 1];
}
else
{
return null;
}
}
@Override
public Component getDefaultComponent(Container aContainer)
{
return this.getFirstComponent(aContainer);
}
}
} }

View File

@@ -20,5 +20,8 @@
<Logger name="edu.regis.universeplayer.data.LocalSongProvider" level="debug"> <Logger name="edu.regis.universeplayer.data.LocalSongProvider" level="debug">
<AppenderRef ref="Console"/> <AppenderRef ref="Console"/>
</Logger> </Logger>
<Logger name="edu.regis.universeplayer.player.Interface" level="debug">
<AppenderRef ref="Console"/>
</Logger>
</Loggers> </Loggers>
</Configuration> </Configuration>