70c70fd812
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>
553 lines
16 KiB
Java
553 lines
16 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.states;
|
|
|
|
import itdelatrisu.opsu.GameImage;
|
|
import itdelatrisu.opsu.Opsu;
|
|
import itdelatrisu.opsu.Utils;
|
|
import itdelatrisu.opsu.audio.SoundController;
|
|
import itdelatrisu.opsu.audio.SoundEffect;
|
|
import itdelatrisu.opsu.downloads.BloodcatServer;
|
|
import itdelatrisu.opsu.downloads.Download;
|
|
import itdelatrisu.opsu.downloads.DownloadList;
|
|
import itdelatrisu.opsu.downloads.DownloadNode;
|
|
import itdelatrisu.opsu.downloads.DownloadServer;
|
|
|
|
import java.io.IOException;
|
|
|
|
import org.newdawn.slick.Color;
|
|
import org.newdawn.slick.GameContainer;
|
|
import org.newdawn.slick.Graphics;
|
|
import org.newdawn.slick.Input;
|
|
import org.newdawn.slick.SlickException;
|
|
import org.newdawn.slick.gui.TextField;
|
|
import org.newdawn.slick.state.BasicGameState;
|
|
import org.newdawn.slick.state.StateBasedGame;
|
|
import org.newdawn.slick.state.transition.FadeInTransition;
|
|
import org.newdawn.slick.state.transition.FadeOutTransition;
|
|
|
|
/**
|
|
* Downloads menu.
|
|
*/
|
|
public class DownloadsMenu extends BasicGameState {
|
|
/** The max number of search result buttons to be shown at a time. */
|
|
public static final int MAX_RESULT_BUTTONS = 10;
|
|
|
|
/** The max number of downloads to be shown at a time. */
|
|
public static final int MAX_DOWNLOADS_SHOWN = 11;
|
|
|
|
/** Delay time, in milliseconds, between each search. */
|
|
private static final int SEARCH_DELAY = 700;
|
|
|
|
/** Delay time, in milliseconds, for double-clicking focused result. */
|
|
private static final int FOCUS_DELAY = 250;
|
|
|
|
/** Minimum time, in milliseconds, that must elapse between queries. */
|
|
private static final int MIN_REQUEST_INTERVAL = 300;
|
|
|
|
/** The beatmap download server. */
|
|
private DownloadServer server = new BloodcatServer();
|
|
|
|
/** The current list of search results. */
|
|
private DownloadNode[] resultList;
|
|
|
|
/** Current focused (selected) result. */
|
|
private int focusResult = -1;
|
|
|
|
/** Delay time, in milliseconds, for double-clicking focused result. */
|
|
private int focusTimer = 0;
|
|
|
|
/** Current start result button (topmost entry). */
|
|
private int startResult = 0;
|
|
|
|
/** Total number of results for current query. */
|
|
private int totalResults = 0;
|
|
|
|
/** Page of current query results. */
|
|
private int page = 1;
|
|
|
|
/** Total number of results across pages seen so far. */
|
|
private int pageResultTotal = 0;
|
|
|
|
/** Page navigation. */
|
|
private enum Page { RESET, CURRENT, PREVIOUS, NEXT };
|
|
|
|
/** Page direction for next query. */
|
|
private Page pageDir = Page.RESET;
|
|
|
|
/** Whether to only show ranked maps. */
|
|
private boolean rankedOnly = true;
|
|
|
|
/** Current start download index. */
|
|
private int startDownloadIndex = 0;
|
|
|
|
/** Query thread. */
|
|
private Thread queryThread;
|
|
|
|
/** The search textfield. */
|
|
private TextField search;
|
|
|
|
/**
|
|
* Delay timer, in milliseconds, before running another search.
|
|
* This is overridden by character entry (reset) and 'esc'/'enter' (immediate search).
|
|
*/
|
|
private int searchTimer;
|
|
|
|
/** Information text to display based on the search query. */
|
|
private String searchResultString;
|
|
|
|
/** Whether or not the search timer has been manually reset; reset after search delay passes. */
|
|
private boolean searchTimerReset = false;
|
|
|
|
/** The last search query. */
|
|
private String lastQuery;
|
|
|
|
/** Page direction for last query. */
|
|
private Page lastQueryDir = Page.RESET;
|
|
|
|
/** Number of active requests. */
|
|
private int activeRequests = 0;
|
|
|
|
/** "Ranked only?" checkbox coordinates. */
|
|
private float rankedBoxX, rankedBoxY, rankedBoxLength;
|
|
|
|
// game-related variables
|
|
private StateBasedGame game;
|
|
private Input input;
|
|
private int state;
|
|
|
|
public DownloadsMenu(int state) {
|
|
this.state = state;
|
|
}
|
|
|
|
@Override
|
|
public void init(GameContainer container, StateBasedGame game)
|
|
throws SlickException {
|
|
this.game = game;
|
|
this.input = container.getInput();
|
|
|
|
int width = container.getWidth();
|
|
int height = container.getHeight();
|
|
|
|
// search
|
|
searchTimer = SEARCH_DELAY;
|
|
searchResultString = "Type to search!";
|
|
search = new TextField(
|
|
container, Utils.FONT_DEFAULT,
|
|
(int) (width * 0.024f), (int) (height * 0.05f) + Utils.FONT_LARGE.getLineHeight(),
|
|
(int) (width * 0.35f), Utils.FONT_MEDIUM.getLineHeight()
|
|
);
|
|
search.setBackgroundColor(DownloadNode.BG_NORMAL);
|
|
search.setBorderColor(Color.white);
|
|
search.setTextColor(Color.white);
|
|
search.setConsumeEvents(false);
|
|
search.setMaxLength(255);
|
|
|
|
// ranked only?
|
|
rankedBoxX = search.getX() + search.getWidth() * 1.2f;
|
|
rankedBoxY = search.getY();
|
|
rankedBoxLength = search.getHeight();
|
|
}
|
|
|
|
@Override
|
|
public void render(GameContainer container, StateBasedGame game, Graphics g)
|
|
throws SlickException {
|
|
int width = container.getWidth();
|
|
int height = container.getHeight();
|
|
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
|
|
|
|
// background
|
|
GameImage.SEARCH_BG.getImage().draw();
|
|
|
|
// title
|
|
Utils.FONT_LARGE.drawString(width * 0.024f, height * 0.04f, "Download Beatmaps!", Color.white);
|
|
|
|
// search
|
|
g.setColor(Color.white);
|
|
search.render(container, g);
|
|
Utils.FONT_BOLD.drawString(
|
|
search.getX() + search.getWidth() * 0.01f, search.getY() + search.getHeight() * 1.3f,
|
|
searchResultString, Color.white
|
|
);
|
|
|
|
// ranked only?
|
|
if (rankedOnly)
|
|
g.fillRect(rankedBoxX, rankedBoxY, rankedBoxLength, rankedBoxLength);
|
|
else
|
|
g.drawRect(rankedBoxX, rankedBoxY, rankedBoxLength, rankedBoxLength);
|
|
Utils.FONT_MEDIUM.drawString(rankedBoxX + rankedBoxLength * 1.5f, rankedBoxY, "Show ranked maps only?", Color.white);
|
|
|
|
// search results
|
|
DownloadNode[] nodes = resultList;
|
|
if (nodes != null) {
|
|
for (int i = 0; i < MAX_RESULT_BUTTONS; i++) {
|
|
int index = startResult + i;
|
|
if (index >= nodes.length)
|
|
break;
|
|
nodes[index].drawResult(g, i, DownloadNode.resultContains(mouseX, mouseY, i), (index == focusResult));
|
|
}
|
|
|
|
// scroll bar
|
|
if (nodes.length > MAX_RESULT_BUTTONS)
|
|
DownloadNode.drawResultScrollbar(g, startResult, nodes.length);
|
|
|
|
// pages
|
|
if (nodes.length > 0)
|
|
DownloadNode.drawPageIcons(page, (page > 1), (pageResultTotal < totalResults));
|
|
}
|
|
|
|
// downloads
|
|
float downloadsX = width * 0.75f, downloadsY = search.getY();
|
|
g.setColor(DownloadNode.BG_NORMAL);
|
|
g.fillRect(downloadsX, downloadsY,
|
|
width * 0.25f, height - downloadsY * 2f);
|
|
Utils.FONT_LARGE.drawString(downloadsX + width * 0.015f, downloadsY + height * 0.015f, "Downloads", Color.white);
|
|
int downloadsSize = DownloadList.get().size();
|
|
if (downloadsSize > 0) {
|
|
for (int i = 0; i < MAX_DOWNLOADS_SHOWN; i++) {
|
|
int index = startDownloadIndex + i;
|
|
if (index >= downloadsSize)
|
|
break;
|
|
DownloadList.get().getNode(index).drawDownload(g, i, index, DownloadNode.downloadContains(mouseX, mouseY, i));
|
|
}
|
|
|
|
// scroll bar
|
|
if (downloadsSize > MAX_DOWNLOADS_SHOWN)
|
|
DownloadNode.drawDownloadScrollbar(g, startDownloadIndex, downloadsSize);
|
|
}
|
|
|
|
Utils.getBackButton().draw();
|
|
Utils.drawVolume(g);
|
|
Utils.drawFPS();
|
|
Utils.drawCursor();
|
|
}
|
|
|
|
@Override
|
|
public void update(GameContainer container, StateBasedGame game, int delta)
|
|
throws SlickException {
|
|
Utils.updateCursor(delta);
|
|
Utils.updateVolumeDisplay(delta);
|
|
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
|
|
Utils.getBackButton().hoverUpdate(delta, mouseX, mouseY);
|
|
|
|
// focus timer
|
|
if (focusResult != -1 && focusTimer < FOCUS_DELAY)
|
|
focusTimer += delta;
|
|
|
|
// search
|
|
search.setFocus(true);
|
|
searchTimer += delta;
|
|
if (searchTimer >= SEARCH_DELAY) {
|
|
searchTimer = 0;
|
|
searchTimerReset = false;
|
|
|
|
final String query = search.getText().trim().toLowerCase();
|
|
if (lastQuery == null || !query.equals(lastQuery)) {
|
|
lastQuery = query;
|
|
lastQueryDir = pageDir;
|
|
|
|
if (queryThread != null && queryThread.isAlive())
|
|
queryThread.interrupt();
|
|
|
|
// execute query
|
|
queryThread = new Thread() {
|
|
@Override
|
|
public void run() {
|
|
activeRequests++;
|
|
|
|
// check page direction
|
|
Page lastPageDir = pageDir;
|
|
pageDir = Page.RESET;
|
|
int lastPageSize = (resultList != null) ? resultList.length : 0;
|
|
int newPage = page;
|
|
if (lastPageDir == Page.RESET)
|
|
newPage = 1;
|
|
else if (lastPageDir == Page.NEXT)
|
|
newPage++;
|
|
else if (lastPageDir == Page.PREVIOUS)
|
|
newPage--;
|
|
try {
|
|
DownloadNode[] nodes = server.resultList(query, newPage, rankedOnly);
|
|
if (activeRequests - 1 == 0) {
|
|
// update page total
|
|
page = newPage;
|
|
if (nodes != null) {
|
|
if (lastPageDir == Page.NEXT)
|
|
pageResultTotal += nodes.length;
|
|
else if (lastPageDir == Page.PREVIOUS)
|
|
pageResultTotal -= lastPageSize;
|
|
else if (lastPageDir == Page.RESET)
|
|
pageResultTotal = nodes.length;
|
|
} else
|
|
pageResultTotal = 0;
|
|
|
|
resultList = nodes;
|
|
totalResults = server.totalResults();
|
|
focusResult = -1;
|
|
startResult = 0;
|
|
if (nodes == null)
|
|
searchResultString = "An error has occurred.";
|
|
else {
|
|
if (query.isEmpty())
|
|
searchResultString = "Type to search!";
|
|
else if (totalResults == 0)
|
|
searchResultString = "No results found.";
|
|
else
|
|
searchResultString = String.format("%d result%s found!",
|
|
totalResults, (totalResults == 1) ? "" : "s");
|
|
}
|
|
}
|
|
} catch (IOException e) {
|
|
searchResultString = "Could not establish connection to server.";
|
|
} finally {
|
|
activeRequests--;
|
|
queryThread = null;
|
|
}
|
|
}
|
|
};
|
|
queryThread.start();
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public int getID() { return state; }
|
|
|
|
@Override
|
|
public void mousePressed(int button, int x, int y) {
|
|
// check mouse button
|
|
if (button != Input.MOUSE_LEFT_BUTTON)
|
|
return;
|
|
|
|
// back
|
|
if (Utils.getBackButton().contains(x, y)) {
|
|
SoundController.playSound(SoundEffect.MENUBACK);
|
|
((MainMenu) game.getState(Opsu.STATE_MAINMENU)).reset();
|
|
game.enterState(Opsu.STATE_MAINMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
|
return;
|
|
}
|
|
|
|
// ranked only?
|
|
if ((x > rankedBoxX && x < rankedBoxX + rankedBoxLength) &&
|
|
(y > rankedBoxY && y < rankedBoxY + rankedBoxLength)) {
|
|
rankedOnly = !rankedOnly;
|
|
lastQuery = null;
|
|
pageDir = Page.CURRENT;
|
|
resetSearchTimer();
|
|
return;
|
|
}
|
|
|
|
// search results
|
|
DownloadNode[] nodes = resultList;
|
|
if (nodes != null) {
|
|
if (DownloadNode.resultAreaContains(x, y)) {
|
|
for (int i = 0; i < MAX_RESULT_BUTTONS; i++) {
|
|
int index = startResult + i;
|
|
if (index >= nodes.length)
|
|
break;
|
|
if (DownloadNode.resultContains(x, y, i)) {
|
|
if (index == focusResult) {
|
|
if (focusTimer >= FOCUS_DELAY) {
|
|
// too slow for double-click
|
|
focusTimer = 0;
|
|
} else {
|
|
// start download
|
|
DownloadNode node = nodes[index];
|
|
if (!DownloadList.get().contains(node.getID())) {
|
|
DownloadList.get().addNode(node);
|
|
node.createDownload(server);
|
|
node.getDownload().start();
|
|
}
|
|
}
|
|
} else {
|
|
// set focus
|
|
focusResult = index;
|
|
focusTimer = 0;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// pages
|
|
if (nodes.length > 0) {
|
|
if (page > 1 && DownloadNode.prevPageContains(x, y)) {
|
|
if (lastQueryDir == Page.PREVIOUS && queryThread != null && queryThread.isAlive())
|
|
; // don't send consecutive requests
|
|
else {
|
|
pageDir = Page.PREVIOUS;
|
|
lastQuery = null;
|
|
resetSearchTimer();
|
|
}
|
|
return;
|
|
}
|
|
if (pageResultTotal < totalResults && DownloadNode.nextPageContains(x, y)) {
|
|
if (lastQueryDir == Page.NEXT && queryThread != null && queryThread.isAlive())
|
|
; // don't send consecutive requests
|
|
else {
|
|
pageDir = Page.NEXT;
|
|
lastQuery = null;
|
|
resetSearchTimer();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// downloads
|
|
if (!DownloadList.get().isEmpty() && DownloadNode.downloadAreaContains(x, y)) {
|
|
for (int i = 0, n = DownloadList.get().size(); i < MAX_DOWNLOADS_SHOWN; i++) {
|
|
int index = startDownloadIndex + i;
|
|
if (index >= n)
|
|
break;
|
|
if (DownloadNode.downloadIconContains(x, y, i)) {
|
|
DownloadNode node = DownloadList.get().getNode(index);
|
|
Download dl = node.getDownload();
|
|
switch (dl.getStatus()) {
|
|
case CANCELLED:
|
|
case COMPLETE:
|
|
case ERROR:
|
|
node.clearDownload();
|
|
DownloadList.get().remove(index);
|
|
break;
|
|
case DOWNLOADING:
|
|
case WAITING:
|
|
dl.cancel();
|
|
break;
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void mouseWheelMoved(int newValue) {
|
|
int shift = (newValue < 0) ? 1 : -1;
|
|
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
|
|
scrollLists(mouseX, mouseY, shift);
|
|
}
|
|
|
|
@Override
|
|
public void mouseDragged(int oldx, int oldy, int newx, int newy) {
|
|
// check mouse button
|
|
if (!input.isMouseButtonDown(Input.MOUSE_RIGHT_BUTTON) &&
|
|
!input.isMouseButtonDown(Input.MOUSE_LEFT_BUTTON))
|
|
return;
|
|
|
|
int diff = newy - oldy;
|
|
if (diff == 0)
|
|
return;
|
|
int shift = (diff < 0) ? 1 : -1;
|
|
scrollLists(oldx, oldy, shift);
|
|
}
|
|
|
|
@Override
|
|
public void keyPressed(int key, char c) {
|
|
switch (key) {
|
|
case Input.KEY_ESCAPE:
|
|
if (!search.getText().isEmpty()) {
|
|
// clear search text
|
|
search.setText("");
|
|
pageDir = Page.RESET;
|
|
resetSearchTimer();
|
|
} else {
|
|
// return to main menu
|
|
SoundController.playSound(SoundEffect.MENUBACK);
|
|
((MainMenu) game.getState(Opsu.STATE_MAINMENU)).reset();
|
|
game.enterState(Opsu.STATE_MAINMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
|
}
|
|
break;
|
|
case Input.KEY_ENTER:
|
|
if (!search.getText().isEmpty()) {
|
|
pageDir = Page.RESET;
|
|
resetSearchTimer();
|
|
}
|
|
break;
|
|
case Input.KEY_F5:
|
|
lastQuery = null;
|
|
pageDir = Page.CURRENT;
|
|
resetSearchTimer();
|
|
break;
|
|
case Input.KEY_F12:
|
|
Utils.takeScreenShot();
|
|
break;
|
|
default:
|
|
// wait for user to finish typing
|
|
if (Character.isLetterOrDigit(c) || key == Input.KEY_BACK) {
|
|
searchTimer = 0;
|
|
pageDir = Page.RESET;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void enter(GameContainer container, StateBasedGame game)
|
|
throws SlickException {
|
|
Utils.getBackButton().setScale(1f);
|
|
focusResult = -1;
|
|
startResult = 0;
|
|
startDownloadIndex = 0;
|
|
pageDir = Page.RESET;
|
|
}
|
|
|
|
/**
|
|
* Resets the search timer, but respects the minimum request interval.
|
|
*/
|
|
private void resetSearchTimer() {
|
|
if (!searchTimerReset) {
|
|
if (searchTimer < MIN_REQUEST_INTERVAL)
|
|
searchTimer = SEARCH_DELAY - MIN_REQUEST_INTERVAL;
|
|
else
|
|
searchTimer = SEARCH_DELAY;
|
|
searchTimerReset = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Processes a shift in the search result and downloads list start indices,
|
|
* if the mouse coordinates are within the area bounds.
|
|
* @param cx the x coordinate
|
|
* @param cy the y coordinate
|
|
* @param shift the number of indices to shift
|
|
*/
|
|
private void scrollLists(int cx, int cy, int shift) {
|
|
// search results
|
|
if (DownloadNode.resultAreaContains(cx, cy)) {
|
|
DownloadNode[] nodes = resultList;
|
|
if (nodes != null && nodes.length >= MAX_RESULT_BUTTONS) {
|
|
int newStartResult = startResult + shift;
|
|
if (newStartResult >= 0 && newStartResult + MAX_RESULT_BUTTONS <= nodes.length)
|
|
startResult = newStartResult;
|
|
}
|
|
}
|
|
|
|
// downloads
|
|
else if (DownloadNode.downloadAreaContains(cx, cy)) {
|
|
if (DownloadList.get().size() >= MAX_DOWNLOADS_SHOWN) {
|
|
int newStartDownloadIndex = startDownloadIndex + shift;
|
|
if (newStartDownloadIndex >= 0 && newStartDownloadIndex + MAX_DOWNLOADS_SHOWN <= DownloadList.get().size())
|
|
startDownloadIndex = newStartDownloadIndex;
|
|
}
|
|
}
|
|
}
|
|
}
|