Added "favorites" and "last played" beatmap groups, and more sorts.

- New sorts: by date added, and most played.
- Sorts are moved to a dropdown menu.
- Tabs are now groupings (all songs, last played, favorites).
- Add/remove "favorite" beatmaps in the right-click menu.
- Beatmap database is now updateable like score database (no longer drops/recreates on every update).
- Various bug fixes.

Signed-off-by: Jeffrey Han <itdelatrisu@gmail.com>
This commit is contained in:
Jeffrey Han
2016-12-22 05:26:09 -05:00
parent 4446487575
commit ed06a8b0ac
12 changed files with 601 additions and 133 deletions

View File

@@ -68,6 +68,18 @@ public class Beatmap implements Comparable<Beatmap> {
/** The star rating. */
public double starRating = -1;
/** The timestamp this beatmap was first loaded. */
public long dateAdded = 0;
/** Whether this beatmap is marked as a "favorite". */
public boolean favorite = false;
/** Total number of times this beatmap has been played. */
public int playCount = 0;
/** The last time this beatmap was played (timestamp). */
public long lastPlayed = 0;
/**
* [General]
*/
@@ -501,4 +513,12 @@ public class Beatmap implements Comparable<Beatmap> {
String[] rgb = s.split(",");
this.sliderBorder = new Color(new Color(Integer.parseInt(rgb[0]), Integer.parseInt(rgb[1]), Integer.parseInt(rgb[2])));
}
/**
* Increments the play counter and last played time.
*/
public void incrementPlayCounter() {
this.playCount++;
this.lastPlayed = System.currentTimeMillis();
}
}

View File

@@ -0,0 +1,179 @@
package itdelatrisu.opsu.beatmap;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.ui.MenuButton;
import itdelatrisu.opsu.ui.UI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.PriorityQueue;
import org.newdawn.slick.Image;
/**
* Beatmap groups.
*/
public enum BeatmapGroup {
/** All beatmaps (no filter). */
ALL (0, "All Songs", null),
/** Most recently played beatmaps. */
RECENT (1, "Last Played", "Your recently played beatmaps will appear in this list!") {
/** Number of elements to show. */
private static final int K = 20;
/** Returns the latest "last played" time in a beatmap set. */
private long lastPlayed(BeatmapSet set) {
long max = 0;
for (Beatmap beatmap : set) {
if (beatmap.lastPlayed > max)
max = beatmap.lastPlayed;
}
return max;
}
@Override
public ArrayList<BeatmapSetNode> filter(ArrayList<BeatmapSetNode> list) {
// find top K elements
PriorityQueue<BeatmapSetNode> pq = new PriorityQueue<BeatmapSetNode>(K, new Comparator<BeatmapSetNode>() {
@Override
public int compare(BeatmapSetNode v, BeatmapSetNode w) {
return Long.compare(lastPlayed(v.getBeatmapSet()), lastPlayed(w.getBeatmapSet()));
}
});
for (BeatmapSetNode node : list) {
long timestamp = lastPlayed(node.getBeatmapSet());
if (timestamp == 0)
continue; // skip unplayed beatmaps
if (pq.size() < K || timestamp > lastPlayed(pq.peek().getBeatmapSet())) {
if (pq.size() == K)
pq.poll();
pq.add(node);
}
}
// return as list
ArrayList<BeatmapSetNode> filteredList = new ArrayList<BeatmapSetNode>();
for (BeatmapSetNode node : pq)
filteredList.add(node);
return filteredList;
}
},
/** "Favorite" beatmaps. */
FAVORITE (2, "Favorites", "Right-click a beatmap to add it to your Favorites!") {
@Override
public ArrayList<BeatmapSetNode> filter(ArrayList<BeatmapSetNode> list) {
// find "favorite" beatmaps
ArrayList<BeatmapSetNode> filteredList = new ArrayList<BeatmapSetNode>();
for (BeatmapSetNode node : list) {
if (node.getBeatmapSet().isFavorite())
filteredList.add(node);
}
return filteredList;
}
};
/** The ID of the group (used for tab positioning). */
private final int id;
/** The name of the group. */
private final String name;
/** The message to display if this list is empty. */
private final String emptyMessage;
/** The tab associated with the group (displayed in Song Menu screen). */
private MenuButton tab;
/** Total number of groups. */
private static final int SIZE = values().length;
/** Array of BeatmapGroup objects in reverse order. */
public static final BeatmapGroup[] VALUES_REVERSED;
static {
VALUES_REVERSED = values();
Collections.reverse(Arrays.asList(VALUES_REVERSED));
}
/** Current group. */
private static BeatmapGroup currentGroup = ALL;
/**
* Returns the current group.
* @return the current group
*/
public static BeatmapGroup current() { return currentGroup; }
/**
* Sets a new group.
* @param group the new group
*/
public static void set(BeatmapGroup group) { currentGroup = group; }
/**
* Constructor.
* @param id the ID of the group (for tab positioning)
* @param name the group name
* @param emptyMessage the message to display if this list is empty
*/
BeatmapGroup(int id, String name, String emptyMessage) {
this.id = id;
this.name = name;
this.emptyMessage = emptyMessage;
}
/**
* Returns the message to display if this list is empty.
* @return the message, or null if none
*/
public String getEmptyMessage() { return emptyMessage; }
/**
* Returns a filtered list of beatmap set nodes.
* @param list the unfiltered list
* @return the filtered list
*/
public ArrayList<BeatmapSetNode> filter(ArrayList<BeatmapSetNode> list) {
return list;
}
/**
* Initializes the tab.
* @param containerWidth the container width
* @param bottomY the bottom y coordinate
*/
public void init(int containerWidth, float bottomY) {
Image tab = GameImage.MENU_TAB.getImage();
int tabWidth = tab.getWidth();
float buttonX = containerWidth / 2f;
float tabOffset = (containerWidth - buttonX - tabWidth) / (SIZE - 1);
if (tabOffset > tabWidth) { // prevent tabs from being spaced out
tabOffset = tabWidth;
buttonX = (containerWidth * 0.99f) - (tabWidth * SIZE);
}
this.tab = new MenuButton(tab,
(buttonX + (tabWidth / 2f)) + (id * tabOffset),
bottomY - (tab.getHeight() / 2f)
);
}
/**
* Checks if the coordinates are within the image bounds.
* @param x the x coordinate
* @param y the y coordinate
* @return true if within bounds
*/
public boolean contains(float x, float y) { return tab.contains(x, y); }
/**
* Draws the tab.
* @param selected whether the tab is selected (white) or not (red)
* @param isHover whether to include a hover effect (unselected only)
*/
public void draw(boolean selected, boolean isHover) {
UI.drawTab(tab.getX(), tab.getY(), name, selected, isHover);
}
}

View File

@@ -120,6 +120,7 @@ public class BeatmapParser {
// parse directories
BeatmapSetNode lastNode = null;
long timestamp = System.currentTimeMillis();
for (File dir : dirs) {
currentDirectoryIndex++;
if (!dir.isDirectory())
@@ -163,6 +164,7 @@ public class BeatmapParser {
// add to parsed beatmap list
if (beatmap != null) {
beatmap.dateAdded = timestamp;
beatmaps.add(beatmap);
parsedBeatmaps.add(beatmap);
}

View File

@@ -19,6 +19,7 @@
package itdelatrisu.opsu.beatmap;
import itdelatrisu.opsu.GameMod;
import itdelatrisu.opsu.db.BeatmapDB;
import java.text.DecimalFormat;
import java.text.NumberFormat;
@@ -185,4 +186,26 @@ public class BeatmapSet implements Iterable<Beatmap> {
return false;
}
/**
* Returns whether this beatmap set is a "favorite".
*/
public boolean isFavorite() {
for (Beatmap map : beatmaps) {
if (map.favorite)
return true;
}
return false;
}
/**
* Sets the "favorite" status of this beatmap set.
* @param flag whether this beatmap set should have "favorite" status
*/
public void setFavorite(boolean flag) {
for (Beatmap map : beatmaps) {
map.favorite = flag;
BeatmapDB.updateFavoriteStatus(map);
}
}
}

View File

@@ -54,6 +54,9 @@ public class BeatmapSetList {
/** Total number of beatmaps (i.e. Beatmap objects). */
private int mapCount = 0;
/** List containing all nodes in the current group. */
private ArrayList<BeatmapSetNode> groupNodes;
/** Current list of nodes (subset of parsedNodes, used for searches). */
private ArrayList<BeatmapSetNode> nodes;
@@ -97,7 +100,7 @@ public class BeatmapSetList {
* This does not erase any parsed nodes.
*/
public void reset() {
nodes = parsedNodes;
nodes = groupNodes = BeatmapGroup.current().filter(parsedNodes);
expandedIndex = -1;
expandedStartNode = expandedEndNode = null;
lastQuery = "";
@@ -168,6 +171,7 @@ public class BeatmapSetList {
Beatmap beatmap = beatmapSet.get(0);
nodes.remove(index);
parsedNodes.remove(eCur);
groupNodes.remove(eCur);
mapCount -= beatmapSet.size();
if (beatmap.beatmapSetID > 0)
MSIDdb.remove(beatmap.beatmapSetID);
@@ -407,7 +411,7 @@ public class BeatmapSetList {
return;
// sort the list
Collections.sort(nodes, BeatmapSortOrder.getSort().getComparator());
Collections.sort(nodes, BeatmapSortOrder.current().getComparator());
expandedIndex = -1;
expandedStartNode = expandedEndNode = null;
@@ -444,7 +448,7 @@ public class BeatmapSetList {
// if empty query, reset to original list
if (query.isEmpty() || terms.isEmpty()) {
nodes = parsedNodes;
nodes = groupNodes;
return true;
}
@@ -472,14 +476,14 @@ public class BeatmapSetList {
String type = condType.remove();
String operator = condOperator.remove();
float value = condValue.remove();
for (BeatmapSetNode node : parsedNodes) {
for (BeatmapSetNode node : groupNodes) {
if (node.getBeatmapSet().matches(type, operator, value))
nodes.add(node);
}
} else {
// normal term
String term = terms.remove();
for (BeatmapSetNode node : parsedNodes) {
for (BeatmapSetNode node : groupNodes) {
if (node.getBeatmapSet().matches(term))
nodes.add(node);
}

View File

@@ -18,28 +18,19 @@
package itdelatrisu.opsu.beatmap;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.ui.MenuButton;
import itdelatrisu.opsu.ui.UI;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import org.newdawn.slick.Image;
/**
* Beatmap sorting orders.
*/
public enum BeatmapSortOrder {
TITLE (0, "Title", new TitleOrder()),
ARTIST (1, "Artist", new ArtistOrder()),
CREATOR (2, "Creator", new CreatorOrder()),
BPM (3, "BPM", new BPMOrder()),
LENGTH (4, "Length", new LengthOrder());
/** The ID of the sort (used for tab positioning). */
private final int id;
TITLE ("Title", new TitleOrder()),
ARTIST ("Artist", new ArtistOrder()),
CREATOR ("Creator", new CreatorOrder()),
BPM ("BPM", new BPMOrder()),
LENGTH ("Length", new LengthOrder()),
DATE ("Date Added", new DateOrder()),
PLAYS ("Most Played", new PlayOrder());
/** The name of the sort. */
private final String name;
@@ -47,19 +38,6 @@ public enum BeatmapSortOrder {
/** The comparator for the sort. */
private final Comparator<BeatmapSetNode> comparator;
/** The tab associated with the sort (displayed in Song Menu screen). */
private MenuButton tab;
/** Total number of sorts. */
private static final int SIZE = values().length;
/** Array of BeatmapSortOrder objects in reverse order. */
public static final BeatmapSortOrder[] VALUES_REVERSED;
static {
VALUES_REVERSED = values();
Collections.reverse(Arrays.asList(VALUES_REVERSED));
}
/** Current sort. */
private static BeatmapSortOrder currentSort = TITLE;
@@ -67,13 +45,13 @@ public enum BeatmapSortOrder {
* Returns the current sort.
* @return the current sort
*/
public static BeatmapSortOrder getSort() { return currentSort; }
public static BeatmapSortOrder current() { return currentSort; }
/**
* Sets a new sort.
* @param sort the new sort
*/
public static void setSort(BeatmapSortOrder sort) { BeatmapSortOrder.currentSort = sort; }
public static void set(BeatmapSortOrder sort) { currentSort = sort; }
/**
* Compares two BeatmapSetNode objects by title.
@@ -135,37 +113,57 @@ public enum BeatmapSortOrder {
}
}
/**
* Compares two BeatmapSetNode objects by date added.
* Uses the latest beatmap added in each set for comparison.
*/
private static class DateOrder implements Comparator<BeatmapSetNode> {
@Override
public int compare(BeatmapSetNode v, BeatmapSetNode w) {
long vMax = 0, wMax = 0;
for (Beatmap beatmap : v.getBeatmapSet()) {
if (beatmap.dateAdded > vMax)
vMax = beatmap.dateAdded;
}
for (Beatmap beatmap : w.getBeatmapSet()) {
if (beatmap.dateAdded > wMax)
wMax = beatmap.dateAdded;
}
return Long.compare(vMax, wMax);
}
}
/**
* Compares two BeatmapSetNode objects by total plays
* (summed across all beatmaps in each set).
*/
private static class PlayOrder implements Comparator<BeatmapSetNode> {
@Override
public int compare(BeatmapSetNode v, BeatmapSetNode w) {
int vTotal = 0, wTotal = 0;
for (Beatmap beatmap : v.getBeatmapSet())
vTotal += beatmap.playCount;
for (Beatmap beatmap : w.getBeatmapSet())
wTotal += beatmap.playCount;
return Integer.compare(vTotal, wTotal);
}
}
/**
* Constructor.
* @param id the ID of the sort (for tab positioning)
* @param name the sort name
* @param comparator the comparator for the sort
*/
BeatmapSortOrder(int id, String name, Comparator<BeatmapSetNode> comparator) {
this.id = id;
BeatmapSortOrder(String name, Comparator<BeatmapSetNode> comparator) {
this.name = name;
this.comparator = comparator;
}
/**
* Initializes the sort tab.
* @param containerWidth the container width
* @param bottomY the bottom y coordinate
* Returns the sort name.
* @return the name
*/
public void init(int containerWidth, float bottomY) {
Image tab = GameImage.MENU_TAB.getImage();
int tabWidth = tab.getWidth();
float buttonX = containerWidth / 2f;
float tabOffset = (containerWidth - buttonX - tabWidth) / (SIZE - 1);
if (tabOffset > tabWidth) { // prevent tabs from being spaced out
tabOffset = tabWidth;
buttonX = (containerWidth * 0.99f) - (tabWidth * SIZE);
}
this.tab = new MenuButton(tab,
(buttonX + (tabWidth / 2f)) + (id * tabOffset),
bottomY - (tab.getHeight() / 2f)
);
}
public String getName() { return name; }
/**
* Returns the comparator for the sort.
@@ -173,20 +171,6 @@ public enum BeatmapSortOrder {
*/
public Comparator<BeatmapSetNode> getComparator() { return comparator; }
/**
* Checks if the coordinates are within the image bounds.
* @param x the x coordinate
* @param y the y coordinate
* @return true if within bounds
*/
public boolean contains(float x, float y) { return tab.contains(x, y); }
/**
* Draws the sort tab.
* @param selected whether the tab is selected (white) or not (red)
* @param isHover whether to include a hover effect (unselected only)
*/
public void draw(boolean selected, boolean isHover) {
UI.drawTab(tab.getX(), tab.getY(), name, selected, isHover);
}
@Override
public String toString() { return name; }
}