opsu-dance/src/itdelatrisu/opsu/downloads/DownloadNode.java
Jeffrey Han 70c70fd812 Implemented an internal beatmap downloader (using Bloodcat).
The downloads menu can be accessed through the button on the right side of the main menu.  The downloader supports searching and concurrent downloads (NOTE: this is limited by the download server!).  Double-click any search result to begin downloading it to the SongPacks directory; cancel the download by hitting the red 'x' in the upper-right corner.  A confirmation will appear if trying to quit opsu! while downloads are running.

New classes:
- Download: represents an individual download from a remote address to a local path, and provides status and progress information; downloads files using Java NIO.
- DownloadNode: holds a Download object as well as additional beatmap fields, and handles drawing.
- DownloadList: manages the current list of downloads.
- DownloadsMenu: game state controller.
- DownloadServer: interface for a beatmap download server.
- BloodcatServer: implements DownloadServer using Bloodcat.
- ReadableByteChannelWrapper: wrapper for ReadableByteChannel that tracks progress.

Added images:
- "downloads" image by @kouyang.
- "search-background" image from "Minimalist Miku" skin (listed in credits).
- "delete" icon by Visual Pharm (https://www.iconfinder.com/icons/27842/) under CC BY-ND 3.0.

Other changes:
- Added up/down/left/right Expand directions to MenuButton class.
- Removed width/height parameters from OsuParser (leftovers).

Signed-off-by: Jeffrey Han <itdelatrisu@gmail.com>
2015-02-01 02:10:17 -05:00

397 lines
13 KiB
Java

/*
* opsu! - an open-source osu! client
* Copyright (C) 2014, 2015 Jeffrey Han
*
* opsu! is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* opsu! is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
*/
package itdelatrisu.opsu.downloads;
import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.downloads.Download.Status;
import itdelatrisu.opsu.states.DownloadsMenu;
import java.io.File;
import org.newdawn.slick.Color;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Image;
/**
* Node containing song data and a Download object.
*/
public class DownloadNode {
/** The associated Download object. */
private Download download;
/** Beatmap set ID. */
private int beatmapSetID;
/** Last updated date string. */
private String date;
/** Song title. */
private String title, titleUnicode;
/** Song artist. */
private String artist, artistUnicode;
/** Beatmap creator. */
private String creator;
/** Button drawing values. */
private static float buttonBaseX, buttonBaseY, buttonWidth, buttonHeight, buttonOffset;
/** Information drawing values. */
private static float infoBaseX, infoBaseY, infoWidth, infoHeight;
/** Container dimensions. */
private static int containerWidth, containerHeight;
/** Button background colors. */
public static final Color
BG_NORMAL = new Color(0, 0, 0, 0.25f),
BG_HOVER = new Color(0, 0, 0, 0.5f),
BG_FOCUS = new Color(0, 0, 0, 0.75f);
/**
* Initializes the base coordinates for drawing.
* @param width the container width
* @param height the container height
*/
public static void init(int width, int height) {
containerWidth = width;
containerHeight = height;
// download result buttons
buttonBaseX = width * 0.024f;
buttonBaseY = height * 0.2f;
buttonWidth = width * 0.7f;
buttonHeight = Utils.FONT_MEDIUM.getLineHeight() * 2f;
buttonOffset = buttonHeight * 1.1f;
// download info
infoBaseX = width * 0.75f;
infoBaseY = height * 0.07f + Utils.FONT_LARGE.getLineHeight() * 2f;
infoWidth = width * 0.25f;
infoHeight = Utils.FONT_DEFAULT.getLineHeight() * 2.4f;
}
/**
* Returns true if the coordinates are within the bounds of the
* download result button at the given index.
* @param cx the x coordinate
* @param cy the y coordinate
* @param index the index (to offset the button from the topmost button)
*/
public static boolean resultContains(float cx, float cy, int index) {
float y = buttonBaseY + (index * buttonOffset);
return ((cx > buttonBaseX && cx < buttonBaseX + buttonWidth) &&
(cy > y && cy < y + buttonHeight));
}
/**
* Returns true if the coordinates are within the bounds of the
* download result button area.
* @param cx the x coordinate
* @param cy the y coordinate
*/
public static boolean resultAreaContains(float cx, float cy) {
return ((cx > buttonBaseX && cx < buttonBaseX + buttonWidth) &&
(cy > buttonBaseY && cy < buttonBaseY + buttonOffset * DownloadsMenu.MAX_RESULT_BUTTONS));
}
/**
* Returns true if the coordinates are within the bounds of the
* download information button at the given index.
* @param cx the x coordinate
* @param cy the y coordinate
* @param index the index (to offset the button from the topmost button)
*/
public static boolean downloadContains(float cx, float cy, int index) {
float y = infoBaseY + (index * infoHeight);
return ((cx > infoBaseX && cx <= containerWidth) &&
(cy > y && cy < y + infoHeight));
}
/**
* Returns true if the coordinates are within the bounds of the
* download action icon at the given index.
* @param cx the x coordinate
* @param cy the y coordinate
* @param index the index (to offset the button from the topmost button)
*/
public static boolean downloadIconContains(float cx, float cy, int index) {
int iconWidth = GameImage.DELETE.getImage().getWidth();
float edgeX = infoBaseX + infoWidth * 0.985f;
float y = infoBaseY + (index * infoHeight);
float marginY = infoHeight * 0.04f;
return ((cx > edgeX - iconWidth && cx < edgeX) &&
(cy > y + marginY && cy < y + marginY + iconWidth));
}
/**
* Returns true if the coordinates are within the bounds of the
* download information button area.
* @param cx the x coordinate
* @param cy the y coordinate
*/
public static boolean downloadAreaContains(float cx, float cy) {
return ((cx > infoBaseX && cx <= containerWidth) &&
(cy > infoBaseY && cy < infoBaseY + infoHeight * DownloadsMenu.MAX_DOWNLOADS_SHOWN));
}
/**
* Returns true if the coordinates are within the bounds of the
* previous page icon.
* @param cx the x coordinate
* @param cy the y coordinate
*/
public static boolean prevPageContains(float cx, float cy) {
Image img = GameImage.MUSIC_PREVIOUS.getImage();
return ((cx > buttonBaseX && cx < buttonBaseX + img.getWidth()) &&
(cy > buttonBaseY - img.getHeight() && cy < buttonBaseY));
}
/**
* Returns true if the coordinates are within the bounds of the
* next page icon.
* @param cx the x coordinate
* @param cy the y coordinate
*/
public static boolean nextPageContains(float cx, float cy) {
Image img = GameImage.MUSIC_NEXT.getImage();
return ((cx > buttonBaseX + buttonWidth - img.getWidth() && cx < buttonBaseX + buttonWidth) &&
(cy > buttonBaseY - img.getHeight() && cy < buttonBaseY));
}
/**
* Draws the scroll bar for the download result buttons.
* @param g the graphics context
* @param index the start button index
* @param total the total number of buttons
*/
public static void drawResultScrollbar(Graphics g, int index, int total) {
float scrollbarWidth = containerWidth * 0.00347f;
float heightRatio = 0.0016f * (total * total) - 0.0705f * total + 0.9965f;
float scrollbarHeight = containerHeight * heightRatio;
float heightDiff = buttonHeight + buttonOffset * (DownloadsMenu.MAX_RESULT_BUTTONS - 1) - scrollbarHeight;
float offsetY = heightDiff * ((float) index / (total - DownloadsMenu.MAX_RESULT_BUTTONS));
g.setColor(BG_NORMAL);
g.fillRect(buttonBaseX + buttonWidth * 1.005f, buttonBaseY, scrollbarWidth, buttonOffset * DownloadsMenu.MAX_RESULT_BUTTONS);
g.setColor(Color.white);
g.fillRect(buttonBaseX + buttonWidth * 1.005f, buttonBaseY + offsetY, scrollbarWidth, scrollbarHeight);
}
/**
* Draws the scroll bar for the download information area.
* @param g the graphics context
* @param index the start index
* @param total the total number of downloads
*/
public static void drawDownloadScrollbar(Graphics g, int index, int total) {
float scrollbarWidth = containerWidth * 0.00347f;
float heightRatio = 0.0016f * (total * total) - 0.0705f * total + 0.9965f;
float scrollbarHeight = containerHeight * heightRatio;
float heightDiff = infoHeight + infoHeight * (DownloadsMenu.MAX_DOWNLOADS_SHOWN - 1) - scrollbarHeight;
float offsetY = heightDiff * ((float) index / (total - DownloadsMenu.MAX_DOWNLOADS_SHOWN));
g.setColor(BG_NORMAL);
g.fillRect(infoBaseX + infoWidth - scrollbarWidth, infoBaseY, scrollbarWidth, infoHeight * DownloadsMenu.MAX_DOWNLOADS_SHOWN);
g.setColor(Color.white);
g.fillRect(infoBaseX + infoWidth - scrollbarWidth, infoBaseY + offsetY, scrollbarWidth, scrollbarHeight);
}
/**
* Draws the page number text and previous/next page icons.
* @param page the current page number
* @param prev whether to draw the previous page icon
* @param next whether to draw the next page icon
*/
public static void drawPageIcons(int page, boolean prev, boolean next) {
String pageText = String.format("Page %d", page);
Utils.FONT_BOLD.drawString(
buttonBaseX + (buttonWidth - Utils.FONT_BOLD.getWidth("Page 1")) / 2f,
buttonBaseY - Utils.FONT_BOLD.getLineHeight() * 1.3f, pageText, Color.white);
if (prev) {
Image prevImg = GameImage.MUSIC_PREVIOUS.getImage();
prevImg.draw(buttonBaseX, buttonBaseY - prevImg.getHeight());
}
if (next) {
Image nextImg = GameImage.MUSIC_NEXT.getImage();
nextImg.draw(buttonBaseX + buttonWidth - nextImg.getWidth(), buttonBaseY - nextImg.getHeight());
}
}
/**
* Constructor.
*/
public DownloadNode(int beatmapSetID, String date, String title,
String titleUnicode, String artist, String artistUnicode, String creator) {
this.beatmapSetID = beatmapSetID;
this.date = date;
this.title = title;
this.titleUnicode = titleUnicode;
this.artist = artist;
this.artistUnicode = artistUnicode;
this.creator = creator;
}
/**
* Creates a download object for this node.
* @param server the server to download from
* @see #getDownload()
*/
public void createDownload(DownloadServer server) {
if (download == null) {
String path = String.format("%s%c%d", Options.getOSZDir(), File.separatorChar, beatmapSetID);
String rename = String.format("%d %s - %s.osz", beatmapSetID, artist, title);
this.download = new Download(server.getURL(beatmapSetID), path, rename);
}
}
/**
* Returns the associated download object, or null if none.
* @see #createDownload(DownloadServer)
*/
public Download getDownload() { return download; }
/**
* Clears the associated download object, if any.
* @see #createDownload(DownloadServer)
*/
public void clearDownload() { download = null; }
/**
* Returns the beatmap set ID.
*/
public int getID() { return beatmapSetID; }
/**
* Returns the last updated date.
*/
public String getDate() { return date; }
/**
* Returns the song title.
* If configured, the Unicode string will be returned instead.
*/
public String getTitle() {
return (Options.useUnicodeMetadata() && titleUnicode != null && !titleUnicode.isEmpty()) ? titleUnicode : title;
}
/**
* Returns the song artist.
* If configured, the Unicode string will be returned instead.
*/
public String getArtist() {
return (Options.useUnicodeMetadata() && artistUnicode != null && !artistUnicode.isEmpty()) ? artistUnicode : artist;
}
/**
* Returns the song creator.
*/
public String getCreator() { return creator; }
/**
* Draws the download result as a rectangular button.
* @param g the graphics context
* @param index the index (to offset the button from the topmost button)
* @param hover true if the mouse is hovering over this button
* @param focus true if the button is focused
*/
public void drawResult(Graphics g, int index, boolean hover, boolean focus) {
float textX = buttonBaseX + buttonWidth * 0.02f;
float edgeX = buttonBaseX + buttonWidth * 0.985f;
float y = buttonBaseY + index * buttonOffset;
float marginY = buttonHeight * 0.04f;
Download dl = DownloadList.get().getDownload(beatmapSetID);
// rectangle outline
g.setColor((focus) ? BG_FOCUS : (hover) ? BG_HOVER : BG_NORMAL);
g.fillRect(buttonBaseX, y, buttonWidth, buttonHeight);
// download progress
if (dl != null) {
float progress = dl.getProgress();
if (progress > 0f) {
g.setColor(Utils.COLOR_GREEN);
g.fillRect(buttonBaseX, y, buttonWidth * progress / 100f, buttonHeight);
}
}
// text
Utils.FONT_BOLD.drawString(
textX, y + marginY,
String.format("%s - %s%s", getArtist(), getTitle(),
(dl != null) ? String.format(" [%s]", dl.getStatus().getName()) : ""), Color.white);
Utils.FONT_DEFAULT.drawString(
textX, y + marginY + Utils.FONT_BOLD.getLineHeight(),
String.format("Last updated: %s", date), Color.white);
Utils.FONT_DEFAULT.drawString(
edgeX - Utils.FONT_DEFAULT.getWidth(creator), y + marginY,
creator, Color.white);
}
/**
* Draws the download information.
* @param g the graphics context
* @param index the index (to offset from the topmost position)
* @param id the list index
* @param hover true if the mouse is hovering over this button
*/
public void drawDownload(Graphics g, int index, int id, boolean hover) {
if (download == null) {
ErrorHandler.error("Trying to draw download information for button without Download object.", null, false);
return;
}
float textX = infoBaseX + infoWidth * 0.02f;
float edgeX = infoBaseX + infoWidth * 0.985f;
float y = infoBaseY + index * infoHeight;
float marginY = infoHeight * 0.04f;
// rectangle outline
g.setColor((id % 2 == 0) ? BG_FOCUS : BG_NORMAL);
g.fillRect(infoBaseX, y, infoWidth, infoHeight);
// text
String info;
Status status = download.getStatus();
float progress = download.getProgress();
if (progress < 0f)
info = status.getName();
else if (status == Download.Status.WAITING)
info = String.format("%s...", status.getName());
else
info = String.format("%s: %.1f%% (%s/%s)", status.getName(), progress,
Utils.bytesToString(download.readSoFar()), Utils.bytesToString(download.contentLength()));
Utils.FONT_BOLD.drawString(textX, y + marginY, getTitle(), Color.white);
Utils.FONT_DEFAULT.drawString(textX, y + marginY + Utils.FONT_BOLD.getLineHeight(), info, Color.white);
// 'x' button
if (hover) {
Image img = GameImage.DELETE.getImage();
img.draw(edgeX - img.getWidth(), y + marginY);
}
}
@Override
public String toString() {
return String.format("[%d] %s - %s (by %s)", beatmapSetID, getArtist(), getTitle(), creator);
}
}