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>
This commit is contained in:
parent
cb5a7d6a4b
commit
70c70fd812
|
@ -16,9 +16,10 @@ The Java Runtime Environment (JRE) must be installed in order to run opsu!.
|
|||
The download page is located [here](https://www.java.com/en/download/).
|
||||
|
||||
### Beatmaps
|
||||
opsu! also requires beatmaps to run, which are available for download on the
|
||||
opsu! requires beatmaps to run, which are available for download on the
|
||||
[osu! website](https://osu.ppy.sh/p/beatmaplist) and mirror sites such as
|
||||
[osu!Mirror](https://osu.yas-online.net/) or [Bloodcat](http://bloodcat.com/osu/).
|
||||
Beatmaps can also be downloaded directly through opsu! in the downloads menu.
|
||||
|
||||
If osu! is already installed, this application will attempt to load songs
|
||||
directly from the osu! program folder. Otherwise, place songs in the generated
|
||||
|
|
5
pom.xml
5
pom.xml
|
@ -138,5 +138,10 @@
|
|||
<artifactId>sqlite-jdbc</artifactId>
|
||||
<version>3.8.7</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.json</groupId>
|
||||
<artifactId>json</artifactId>
|
||||
<version>20140107</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
BIN
res/delete.png
Normal file
BIN
res/delete.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
BIN
res/downloads.png
Normal file
BIN
res/downloads.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.2 KiB |
BIN
res/search-background.jpg
Normal file
BIN
res/search-background.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 228 KiB |
|
@ -19,6 +19,7 @@
|
|||
package itdelatrisu.opsu;
|
||||
|
||||
import itdelatrisu.opsu.audio.MusicController;
|
||||
import itdelatrisu.opsu.downloads.DownloadList;
|
||||
|
||||
import org.newdawn.slick.AppGameContainer;
|
||||
import org.newdawn.slick.Game;
|
||||
|
@ -110,4 +111,13 @@ public class Container extends AppGameContainer {
|
|||
throw e; // re-throw exception
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exit() {
|
||||
// show confirmation dialog if any downloads are active
|
||||
if (DownloadList.get().hasActiveDownloads() && DownloadList.showExitConfirmation())
|
||||
return;
|
||||
|
||||
super.exit();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -465,6 +465,26 @@ public enum GameImage {
|
|||
return img.getScaledCopy((h * 0.15f) / img.getHeight());
|
||||
}
|
||||
},
|
||||
DOWNLOADS ("downloads", "png", false, false) {
|
||||
@Override
|
||||
protected Image process_sub(Image img, int w, int h) {
|
||||
return img.getScaledCopy((h * 0.45f) / img.getHeight());
|
||||
}
|
||||
},
|
||||
SEARCH_BG ("search-background", "png|jpg", false, true) {
|
||||
@Override
|
||||
protected Image process_sub(Image img, int w, int h) {
|
||||
img.setAlpha(0.8f);
|
||||
return img.getScaledCopy(w, h);
|
||||
}
|
||||
},
|
||||
DELETE ("delete", "png", false, false) {
|
||||
@Override
|
||||
protected Image process_sub(Image img, int w, int h) {
|
||||
int lineHeight = Utils.FONT_DEFAULT.getLineHeight();
|
||||
return img.getScaledCopy(lineHeight, lineHeight);
|
||||
}
|
||||
},
|
||||
HISTORY ("history", "png", false, false) {
|
||||
@Override
|
||||
protected Image process_sub(Image img, int w, int h) {
|
||||
|
@ -573,10 +593,7 @@ public enum GameImage {
|
|||
* @param type the file types (separated by '|')
|
||||
*/
|
||||
GameImage(String filename, String type) {
|
||||
this.filename = filename;
|
||||
this.type = getType(type);
|
||||
this.skinnable = true;
|
||||
this.preload = true;
|
||||
this(filename, type, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -586,11 +603,8 @@ public enum GameImage {
|
|||
* @param type the file types (separated by '|')
|
||||
*/
|
||||
GameImage(String filename, String filenameFormat, String type) {
|
||||
this.filename = filename;
|
||||
this(filename, type, true, true);
|
||||
this.filenameFormat = filenameFormat;
|
||||
this.type = getType(type);
|
||||
this.skinnable = true;
|
||||
this.preload = true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -50,7 +50,7 @@ public class MenuButton {
|
|||
private Expand dir = Expand.CENTER;
|
||||
|
||||
/** Scaled expansion directions (for hovering). */
|
||||
public enum Expand { CENTER, UP_RIGHT, UP_LEFT, DOWN_RIGHT, DOWN_LEFT; }
|
||||
public enum Expand { CENTER, UP, RIGHT, LEFT, DOWN, UP_RIGHT, UP_LEFT, DOWN_RIGHT, DOWN_LEFT; }
|
||||
|
||||
/**
|
||||
* Creates a new button from an Image.
|
||||
|
@ -137,7 +137,7 @@ public class MenuButton {
|
|||
public void draw() {
|
||||
if (img != null) {
|
||||
if (imgL == null) {
|
||||
Image imgScaled = img.getScaledCopy(scale);
|
||||
Image imgScaled = (scale == 1f) ? img : img.getScaledCopy(scale);
|
||||
imgScaled.setAlpha(img.getAlpha());
|
||||
imgScaled.draw(x - xRadius, y - yRadius);
|
||||
} else {
|
||||
|
@ -229,9 +229,13 @@ public class MenuButton {
|
|||
// offset by difference between normal/scaled image dimensions
|
||||
xOffset = (int) ((scale - 1f) * img.getWidth());
|
||||
yOffset = (int) ((scale - 1f) * img.getHeight());
|
||||
if (dir == Expand.DOWN_RIGHT || dir == Expand.UP_RIGHT)
|
||||
if (dir == Expand.UP || dir == Expand.DOWN)
|
||||
xOffset = 0; // no horizontal offset
|
||||
if (dir == Expand.RIGHT || dir == Expand.LEFT)
|
||||
yOffset = 0; // no vertical offset
|
||||
if (dir == Expand.RIGHT || dir == Expand.DOWN_RIGHT || dir == Expand.UP_RIGHT)
|
||||
xOffset *= -1; // flip x for right
|
||||
if (dir == Expand.DOWN_LEFT || dir == Expand.DOWN_RIGHT)
|
||||
if (dir == Expand.DOWN || dir == Expand.DOWN_LEFT || dir == Expand.DOWN_RIGHT)
|
||||
yOffset *= -1; // flip y for down
|
||||
}
|
||||
this.xRadius = ((img.getWidth() * scale) + xOffset) / 2f;
|
||||
|
|
|
@ -19,6 +19,8 @@
|
|||
package itdelatrisu.opsu;
|
||||
|
||||
import itdelatrisu.opsu.audio.MusicController;
|
||||
import itdelatrisu.opsu.downloads.DownloadList;
|
||||
import itdelatrisu.opsu.states.DownloadsMenu;
|
||||
import itdelatrisu.opsu.states.Game;
|
||||
import itdelatrisu.opsu.states.GamePauseMenu;
|
||||
import itdelatrisu.opsu.states.GameRanking;
|
||||
|
@ -62,7 +64,8 @@ public class Opsu extends StateBasedGame {
|
|||
STATE_GAME = 4,
|
||||
STATE_GAMEPAUSEMENU = 5,
|
||||
STATE_GAMERANKING = 6,
|
||||
STATE_OPTIONSMENU = 7;
|
||||
STATE_OPTIONSMENU = 7,
|
||||
STATE_DOWNLOADSMENU = 8;
|
||||
|
||||
/** Server socket for restricting the program to a single instance. */
|
||||
private static ServerSocket SERVER_SOCKET;
|
||||
|
@ -85,6 +88,7 @@ public class Opsu extends StateBasedGame {
|
|||
addState(new GamePauseMenu(STATE_GAMEPAUSEMENU));
|
||||
addState(new GameRanking(STATE_GAMERANKING));
|
||||
addState(new OptionsMenu(STATE_OPTIONSMENU));
|
||||
addState(new DownloadsMenu(STATE_DOWNLOADSMENU));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -194,6 +198,10 @@ public class Opsu extends StateBasedGame {
|
|||
return false;
|
||||
}
|
||||
|
||||
// show confirmation dialog if any downloads are active
|
||||
if (DownloadList.get().hasActiveDownloads() && DownloadList.showExitConfirmation())
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -204,6 +212,9 @@ public class Opsu extends StateBasedGame {
|
|||
// close scores database
|
||||
ScoreDB.closeConnection();
|
||||
|
||||
// cancel all downloads
|
||||
DownloadList.get().cancelAllDownloads();
|
||||
|
||||
// close server socket
|
||||
if (SERVER_SOCKET != null) {
|
||||
try {
|
||||
|
|
|
@ -55,10 +55,8 @@ public class OsuParser {
|
|||
/**
|
||||
* Invokes parser for each OSU file in a root directory.
|
||||
* @param root the root directory (search has depth 1)
|
||||
* @param width the container width
|
||||
* @param height the container height
|
||||
*/
|
||||
public static void parseAllFiles(File root, int width, int height) {
|
||||
public static void parseAllFiles(File root) {
|
||||
// create a new OsuGroupList
|
||||
OsuGroupList.create();
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ package itdelatrisu.opsu;
|
|||
|
||||
import itdelatrisu.opsu.audio.SoundController;
|
||||
import itdelatrisu.opsu.audio.SoundEffect;
|
||||
import itdelatrisu.opsu.downloads.DownloadNode;
|
||||
|
||||
import java.awt.Font;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
@ -71,7 +72,8 @@ public class Utils {
|
|||
COLOR_ORANGE_OBJECT = new Color(255, 200, 32),
|
||||
COLOR_YELLOW_ALPHA = new Color(255, 255, 0, 0.4f),
|
||||
COLOR_WHITE_FADE = new Color(255, 255, 255, 1f),
|
||||
COLOR_RED_HOVER = new Color(255, 112, 112);
|
||||
COLOR_RED_HOVER = new Color(255, 112, 112),
|
||||
COLOR_GREEN = new Color(137, 201, 79);
|
||||
|
||||
/** The default map colors, used when a map does not provide custom colors. */
|
||||
public static final Color[] DEFAULT_COMBO = {
|
||||
|
@ -201,6 +203,9 @@ public class Utils {
|
|||
// initialize score data buttons
|
||||
ScoreData.init(width, height);
|
||||
|
||||
// initialize download nodes
|
||||
DownloadNode.init(width, height);
|
||||
|
||||
// back button
|
||||
Image back = GameImage.MENU_BACK.getImage();
|
||||
backButton = new MenuButton(back,
|
||||
|
@ -685,4 +690,18 @@ public class Utils {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a human-readable representation of a given number of bytes.
|
||||
* @param bytes the number of bytes
|
||||
* @return the string representation
|
||||
* @author aioobe (http://stackoverflow.com/a/3758880)
|
||||
*/
|
||||
public static String bytesToString(long bytes) {
|
||||
if (bytes < 1024)
|
||||
return bytes + " B";
|
||||
int exp = (int) (Math.log(bytes) / Math.log(1024));
|
||||
char pre = "KMGTPE".charAt(exp - 1);
|
||||
return String.format("%.1f %cB", bytes / Math.pow(1024, exp), pre);
|
||||
}
|
||||
}
|
134
src/itdelatrisu/opsu/downloads/BloodcatServer.java
Normal file
134
src/itdelatrisu/opsu/downloads/BloodcatServer.java
Normal file
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* 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 java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
* Download server: http://bloodcat.com/osu/
|
||||
*/
|
||||
public class BloodcatServer implements DownloadServer {
|
||||
/** Formatted download URL: {@code beatmapSetID} */
|
||||
private static final String DOWNLOAD_URL = "http://bloodcat.com/osu/s/%d";
|
||||
|
||||
/** Formatted search URL: {@code query,rankedOnly,page} */
|
||||
private static final String SEARCH_URL = "http://bloodcat.com/osu/?q=%s&m=b&c=%s&g=&d=0&s=date&o=0&p=%d&mod=json";
|
||||
|
||||
/** Total result count from the last query. */
|
||||
private int totalResults = -1;
|
||||
|
||||
/** Constructor. */
|
||||
public BloodcatServer() {}
|
||||
|
||||
@Override
|
||||
public String getURL(int beatmapSetID) {
|
||||
return String.format(DOWNLOAD_URL, beatmapSetID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DownloadNode[] resultList(String query, int page, boolean rankedOnly) throws IOException {
|
||||
DownloadNode[] nodes = null;
|
||||
try {
|
||||
// read JSON
|
||||
String search = String.format(SEARCH_URL, URLEncoder.encode(query, "UTF-8"), rankedOnly ? "0" : "", page);
|
||||
JSONObject json = readJsonFromUrl(new URL(search));
|
||||
if (json == null) {
|
||||
this.totalResults = -1;
|
||||
return null;
|
||||
}
|
||||
|
||||
// parse result list
|
||||
JSONArray arr = json.getJSONArray("results");
|
||||
nodes = new DownloadNode[arr.length()];
|
||||
for (int i = 0; i < nodes.length; i++) {
|
||||
JSONObject item = arr.getJSONObject(i);
|
||||
nodes[i] = new DownloadNode(
|
||||
item.getInt("id"), item.getString("date"),
|
||||
item.getString("title"), item.isNull("titleUnicode") ? null : item.getString("titleUnicode"),
|
||||
item.getString("artist"), item.isNull("artistUnicode") ? null : item.getString("artistUnicode"),
|
||||
item.getString("creator")
|
||||
);
|
||||
}
|
||||
|
||||
// store total result count
|
||||
this.totalResults = json.getInt("resultCount");
|
||||
} catch (MalformedURLException | UnsupportedEncodingException e) {
|
||||
ErrorHandler.error(String.format("Problem loading result list for query '%s'.", query), e, true);
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int totalResults() { return totalResults; }
|
||||
|
||||
/**
|
||||
* Returns a JSON object from a URL.
|
||||
* @param url the remote URL
|
||||
* @return the JSON object
|
||||
* @author Roland Illig (http://stackoverflow.com/a/4308662)
|
||||
*/
|
||||
public static JSONObject readJsonFromUrl(URL url) throws IOException {
|
||||
// open connection
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setConnectTimeout(Download.CONNECTION_TIMEOUT);
|
||||
conn.setReadTimeout(Download.READ_TIMEOUT);
|
||||
conn.setUseCaches(false);
|
||||
try {
|
||||
conn.connect();
|
||||
} catch (SocketTimeoutException e) {
|
||||
ErrorHandler.error("Connection to server timed out.", e, false);
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (Thread.interrupted())
|
||||
return null;
|
||||
|
||||
// read JSON
|
||||
JSONObject json = null;
|
||||
try (InputStream in = conn.getInputStream()) {
|
||||
BufferedReader rd = new BufferedReader(new InputStreamReader(in));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
int c;
|
||||
while ((c = rd.read()) != -1)
|
||||
sb.append((char) c);
|
||||
json = new JSONObject(sb.toString());
|
||||
} catch (SocketTimeoutException e) {
|
||||
ErrorHandler.error("Connection to server timed out.", e, false);
|
||||
throw e;
|
||||
} catch (JSONException e1) {
|
||||
ErrorHandler.error("Failed to create JSON object.", e1, true);
|
||||
}
|
||||
return json;
|
||||
}
|
||||
}
|
256
src/itdelatrisu/opsu/downloads/Download.java
Normal file
256
src/itdelatrisu/opsu/downloads/Download.java
Normal file
|
@ -0,0 +1,256 @@
|
|||
/*
|
||||
* 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 java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
|
||||
/**
|
||||
* File download.
|
||||
*/
|
||||
public class Download {
|
||||
/** Connection timeout, in ms. */
|
||||
public static final int CONNECTION_TIMEOUT = 5000;
|
||||
|
||||
/** Read timeout, in ms. */
|
||||
public static final int READ_TIMEOUT = 10000;
|
||||
|
||||
/** Download statuses. */
|
||||
public enum Status {
|
||||
WAITING ("Waiting"),
|
||||
DOWNLOADING ("Downloading"),
|
||||
COMPLETE ("Complete"),
|
||||
CANCELLED ("Cancelled"),
|
||||
ERROR ("Error");
|
||||
|
||||
/** The status name. */
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param name the status name.
|
||||
*/
|
||||
Status(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the status name.
|
||||
*/
|
||||
public String getName() { return name; }
|
||||
}
|
||||
|
||||
/** The local path. */
|
||||
private String localPath;
|
||||
|
||||
/** The local path to rename the file to when finished. */
|
||||
private String rename;
|
||||
|
||||
/** The download URL. */
|
||||
private URL url;
|
||||
|
||||
/** The readable byte channel. */
|
||||
private ReadableByteChannelWrapper rbc;
|
||||
|
||||
/** The file output stream. */
|
||||
private FileOutputStream fos;
|
||||
|
||||
/** The size of the download. */
|
||||
private int contentLength = -1;
|
||||
|
||||
/** The download status. */
|
||||
private Status status = Status.WAITING;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param remoteURL the download URL
|
||||
* @param localPath the path to save the download
|
||||
*/
|
||||
public Download(String remoteURL, String localPath) {
|
||||
this(remoteURL, localPath, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param remoteURL the download URL
|
||||
* @param localPath the path to save the download
|
||||
* @param rename the file name to rename the download to when complete
|
||||
*/
|
||||
public Download(String remoteURL, String localPath, String rename) {
|
||||
try {
|
||||
this.url = new URL(remoteURL);
|
||||
} catch (MalformedURLException e) {
|
||||
this.status = Status.ERROR;
|
||||
ErrorHandler.error(String.format("Bad download URL: '%s'", remoteURL), e, true);
|
||||
return;
|
||||
}
|
||||
this.localPath = localPath;
|
||||
this.rename = rename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the download from the "waiting" status.
|
||||
*/
|
||||
public void start() {
|
||||
if (status != Status.WAITING)
|
||||
return;
|
||||
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
// open connection, get content length
|
||||
HttpURLConnection conn = null;
|
||||
try {
|
||||
conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setConnectTimeout(CONNECTION_TIMEOUT);
|
||||
conn.setReadTimeout(READ_TIMEOUT);
|
||||
conn.setUseCaches(false);
|
||||
contentLength = conn.getContentLength();
|
||||
} catch (IOException e) {
|
||||
status = Status.ERROR;
|
||||
ErrorHandler.error("Failed to open connection.", e, false);
|
||||
return;
|
||||
}
|
||||
|
||||
// download file
|
||||
try (
|
||||
InputStream in = conn.getInputStream();
|
||||
ReadableByteChannel readableByteChannel = Channels.newChannel(in);
|
||||
FileOutputStream fileOutputStream = new FileOutputStream(localPath);
|
||||
) {
|
||||
rbc = new ReadableByteChannelWrapper(readableByteChannel);
|
||||
fos = fileOutputStream;
|
||||
status = Status.DOWNLOADING;
|
||||
fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
|
||||
if (status == Status.DOWNLOADING) { // not interrupted
|
||||
status = Status.COMPLETE;
|
||||
rbc.close();
|
||||
fos.close();
|
||||
if (rename != null) {
|
||||
Path source = new File(localPath).toPath();
|
||||
Files.move(source, source.resolveSibling(rename), StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
status = Status.ERROR;
|
||||
ErrorHandler.error("Failed to start download.", e, false);
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the download status.
|
||||
*/
|
||||
public Status getStatus() { return status; }
|
||||
|
||||
/**
|
||||
* Returns true if transfers are currently taking place.
|
||||
*/
|
||||
public boolean isTransferring() {
|
||||
return (rbc != null && rbc.isOpen() && fos != null && fos.getChannel().isOpen());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the download is active.
|
||||
*/
|
||||
public boolean isActive() {
|
||||
return (status == Status.WAITING || status == Status.DOWNLOADING);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the download content in bytes, or -1 if not calculated
|
||||
* (or if an error has occurred).
|
||||
*/
|
||||
public int contentLength() { return contentLength; }
|
||||
|
||||
/**
|
||||
* Returns the download completion percentage, or -1f if an error has occurred.
|
||||
*/
|
||||
public float getProgress() {
|
||||
switch (status) {
|
||||
case WAITING:
|
||||
return 0f;
|
||||
case COMPLETE:
|
||||
return 100f;
|
||||
case DOWNLOADING:
|
||||
if (rbc != null && fos != null && contentLength > 0)
|
||||
return (float) rbc.getReadSoFar() / (float) contentLength * 100f;
|
||||
else
|
||||
return 0f;
|
||||
case CANCELLED:
|
||||
case ERROR:
|
||||
default:
|
||||
return -1f;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of bytes read so far.
|
||||
*/
|
||||
public long readSoFar() {
|
||||
switch (status) {
|
||||
case COMPLETE:
|
||||
return contentLength;
|
||||
case DOWNLOADING:
|
||||
if (rbc != null)
|
||||
return rbc.getReadSoFar();
|
||||
// else fall through
|
||||
case WAITING:
|
||||
case CANCELLED:
|
||||
case ERROR:
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the download, if running.
|
||||
*/
|
||||
public void cancel() {
|
||||
try {
|
||||
this.status = Status.CANCELLED;
|
||||
boolean transferring = isTransferring();
|
||||
if (rbc != null && rbc.isOpen())
|
||||
rbc.close();
|
||||
if (fos != null && fos.getChannel().isOpen())
|
||||
fos.close();
|
||||
if (transferring) {
|
||||
File f = new File(localPath);
|
||||
if (f.isFile())
|
||||
f.delete();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
this.status = Status.ERROR;
|
||||
ErrorHandler.error("Failed to cancel download.", e, true);
|
||||
}
|
||||
}
|
||||
}
|
144
src/itdelatrisu/opsu/downloads/DownloadList.java
Normal file
144
src/itdelatrisu/opsu/downloads/DownloadList.java
Normal file
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* 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 java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.UIManager;
|
||||
|
||||
import org.newdawn.slick.util.Log;
|
||||
|
||||
/**
|
||||
* Maintains the current downloads list.
|
||||
*/
|
||||
public class DownloadList {
|
||||
/** The single instance of this class. */
|
||||
private static DownloadList list = new DownloadList();
|
||||
|
||||
/** Current list of downloads. */
|
||||
private List<DownloadNode> nodes;
|
||||
|
||||
/** The map of beatmap set IDs to DownloadNodes for the current downloads. */
|
||||
private Map<Integer, DownloadNode> map;
|
||||
|
||||
/**
|
||||
* Returns the single instance of this class.
|
||||
*/
|
||||
public static DownloadList get() { return list; }
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
private DownloadList() {
|
||||
nodes = new ArrayList<DownloadNode>();
|
||||
map = new HashMap<Integer, DownloadNode>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the DownloadNode at an index.
|
||||
*/
|
||||
public DownloadNode getNode(int index) { return nodes.get(index); }
|
||||
|
||||
/**
|
||||
* Gets the Download for a beatmap set ID, or null if not in the list.
|
||||
*/
|
||||
public Download getDownload(int beatmapSetID) {
|
||||
DownloadNode node = map.get(beatmapSetID);
|
||||
return (node == null) ? null : node.getDownload();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the doownloads list.
|
||||
*/
|
||||
public int size() { return nodes.size(); }
|
||||
|
||||
/**
|
||||
* Returns {@code true} if this list contains no elements.
|
||||
*/
|
||||
public boolean isEmpty() { return nodes.isEmpty(); }
|
||||
|
||||
/**
|
||||
* Returns {@code true} if this list contains the beatmap set ID.
|
||||
*/
|
||||
public boolean contains(int beatmapSetID) { return map.containsKey(beatmapSetID); }
|
||||
|
||||
/**
|
||||
* Adds a DownloadNode to the list.
|
||||
*/
|
||||
public void addNode(DownloadNode node) {
|
||||
nodes.add(node);
|
||||
map.put(node.getID(), node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a DownloadNode from the list.
|
||||
*/
|
||||
public void remove(DownloadNode node) { remove(nodes.indexOf(node)); }
|
||||
|
||||
/**
|
||||
* Removes a DownloadNode from the list at the given index.
|
||||
*/
|
||||
public void remove(int index) {
|
||||
DownloadNode node = nodes.remove(index);
|
||||
map.remove(node.getID());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the list contains any downloads that are active.
|
||||
*/
|
||||
public boolean hasActiveDownloads() {
|
||||
for (DownloadNode node: nodes) {
|
||||
Download dl = node.getDownload();
|
||||
if (dl != null && dl.isActive())
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels all downloads.
|
||||
*/
|
||||
public void cancelAllDownloads() {
|
||||
for (DownloadNode node : nodes) {
|
||||
Download dl = node.getDownload();
|
||||
if (dl != null && dl.isActive())
|
||||
dl.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a confirmation dialog (used before exiting the game).
|
||||
* @return true if user selects "yes", false otherwise
|
||||
*/
|
||||
public static boolean showExitConfirmation() {
|
||||
try {
|
||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||
} catch (Exception e) {
|
||||
Log.error("Could not set system look and feel for Container.", e);
|
||||
}
|
||||
int n = JOptionPane.showConfirmDialog(null,
|
||||
"Beatmap downloads are in progress.\nAre you sure you want to quit opsu!?",
|
||||
"Warning", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
|
||||
return (n != JOptionPane.YES_OPTION);
|
||||
}
|
||||
}
|
396
src/itdelatrisu/opsu/downloads/DownloadNode.java
Normal file
396
src/itdelatrisu/opsu/downloads/DownloadNode.java
Normal file
|
@ -0,0 +1,396 @@
|
|||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
52
src/itdelatrisu/opsu/downloads/DownloadServer.java
Normal file
52
src/itdelatrisu/opsu/downloads/DownloadServer.java
Normal file
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 java.io.IOException;
|
||||
|
||||
/**
|
||||
* Interface for beatmap download servers.
|
||||
*/
|
||||
public interface DownloadServer {
|
||||
/**
|
||||
* Returns a web address to download the given beatmap.
|
||||
* @param beatmapSetID the beatmap set ID
|
||||
* @return the URL string
|
||||
*/
|
||||
public String getURL(int beatmapSetID);
|
||||
|
||||
/**
|
||||
* Returns a list of results for a given search query, or null if the
|
||||
* list could not be created.
|
||||
* @param query the search query
|
||||
* @param page the result page (starting at 1)
|
||||
* @param rankedOnly whether to only show ranked maps
|
||||
* @return the result array
|
||||
* @throws IOException if any connection problem occurs
|
||||
*/
|
||||
public DownloadNode[] resultList(String query, int page, boolean rankedOnly) throws IOException;
|
||||
|
||||
/**
|
||||
* Returns the total number of results for the last search query.
|
||||
* This will differ from the the size of the array returned by
|
||||
* {@link #resultList(String, int, boolean)} if multiple pages exist.
|
||||
* @return the result count, or -1 if no query
|
||||
*/
|
||||
public int totalResults();
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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 java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
|
||||
/**
|
||||
* Wrapper for a ReadableByteChannel that stores the number of bytes read.
|
||||
* @author par (http://stackoverflow.com/a/11068356)
|
||||
*/
|
||||
public class ReadableByteChannelWrapper implements ReadableByteChannel {
|
||||
/** The wrapped ReadableByteChannel. */
|
||||
private ReadableByteChannel rbc;
|
||||
|
||||
/** The number of bytes read. */
|
||||
private long bytesRead;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param rbc the ReadableByteChannel to wrap
|
||||
*/
|
||||
public ReadableByteChannelWrapper(ReadableByteChannel rbc) {
|
||||
this.rbc = rbc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException { rbc.close(); }
|
||||
|
||||
@Override
|
||||
public boolean isOpen() { return rbc.isOpen(); }
|
||||
|
||||
@Override
|
||||
public int read(ByteBuffer bb) throws IOException {
|
||||
int bytes;
|
||||
if ((bytes = rbc.read(bb)) > 0)
|
||||
bytesRead += bytes;
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of bytes read so far.
|
||||
*/
|
||||
public long getReadSoFar() { return bytesRead; }
|
||||
}
|
552
src/itdelatrisu/opsu/states/DownloadsMenu.java
Normal file
552
src/itdelatrisu/opsu/states/DownloadsMenu.java
Normal file
|
@ -0,0 +1,552 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,6 +21,7 @@ package itdelatrisu.opsu.states;
|
|||
import itdelatrisu.opsu.ErrorHandler;
|
||||
import itdelatrisu.opsu.GameImage;
|
||||
import itdelatrisu.opsu.MenuButton;
|
||||
import itdelatrisu.opsu.MenuButton.Expand;
|
||||
import itdelatrisu.opsu.Opsu;
|
||||
import itdelatrisu.opsu.Options;
|
||||
import itdelatrisu.opsu.OsuFile;
|
||||
|
@ -75,6 +76,9 @@ public class MainMenu extends BasicGameState {
|
|||
/** Music control buttons. */
|
||||
private MenuButton musicPlay, musicPause, musicNext, musicPrevious;
|
||||
|
||||
/** Button linking to Downloads menu. */
|
||||
private MenuButton downloadsButton;
|
||||
|
||||
/** Button linking to repository. */
|
||||
private MenuButton repoButton;
|
||||
|
||||
|
@ -138,6 +142,12 @@ public class MainMenu extends BasicGameState {
|
|||
musicNext.setHoverScale(1.5f);
|
||||
musicPrevious.setHoverScale(1.5f);
|
||||
|
||||
// initialize downloads button
|
||||
Image dlImg = GameImage.DOWNLOADS.getImage();
|
||||
downloadsButton = new MenuButton(dlImg, width - dlImg.getWidth() / 2f, height / 2f);
|
||||
downloadsButton.setHoverDir(Expand.LEFT);
|
||||
downloadsButton.setHoverScale(1.05f);
|
||||
|
||||
// initialize repository button
|
||||
if (Desktop.isDesktopSupported()) { // only if a webpage can be opened
|
||||
Image repoImg = GameImage.REPOSITORY.getImage();
|
||||
|
@ -173,6 +183,9 @@ public class MainMenu extends BasicGameState {
|
|||
g.fillRect(0, height * 8 / 9f, width, height / 9f);
|
||||
Utils.COLOR_BLACK_ALPHA.a = oldAlpha;
|
||||
|
||||
// draw downloads button
|
||||
downloadsButton.draw();
|
||||
|
||||
// draw buttons
|
||||
if (logoTimer > 0) {
|
||||
playButton.draw();
|
||||
|
@ -239,6 +252,7 @@ public class MainMenu extends BasicGameState {
|
|||
exitButton.hoverUpdate(delta, mouseX, mouseY);
|
||||
if (repoButton != null)
|
||||
repoButton.hoverUpdate(delta, mouseX, mouseY);
|
||||
downloadsButton.hoverUpdate(delta, mouseX, mouseY);
|
||||
musicPlay.hoverUpdate(delta, mouseX, mouseY);
|
||||
musicPause.hoverUpdate(delta, mouseX, mouseY);
|
||||
if (musicPlay.contains(mouseX, mouseY))
|
||||
|
@ -319,6 +333,8 @@ public class MainMenu extends BasicGameState {
|
|||
musicPrevious.setScale(1f);
|
||||
if (repoButton != null && !repoButton.contains(mouseX, mouseY))
|
||||
repoButton.setScale(1f);
|
||||
if (!downloadsButton.contains(mouseX, mouseY))
|
||||
downloadsButton.setScale(1f);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -355,6 +371,12 @@ public class MainMenu extends BasicGameState {
|
|||
MusicController.setPosition(0);
|
||||
}
|
||||
|
||||
// downloads button actions
|
||||
else if (downloadsButton.contains(x, y)) {
|
||||
SoundController.playSound(SoundEffect.MENUHIT);
|
||||
game.enterState(Opsu.STATE_DOWNLOADSMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
||||
}
|
||||
|
||||
// repository button actions
|
||||
else if (repoButton != null && repoButton.contains(x, y)) {
|
||||
try {
|
||||
|
@ -437,5 +459,6 @@ public class MainMenu extends BasicGameState {
|
|||
musicPause.setScale(1f);
|
||||
musicNext.setScale(1f);
|
||||
musicPrevious.setScale(1f);
|
||||
downloadsButton.setScale(1f);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -620,7 +620,7 @@ public class SongMenu extends BasicGameState {
|
|||
// invoke unpacker and parser
|
||||
File beatmapDir = Options.getBeatmapDir();
|
||||
OszUnpacker.unpackAllFiles(Options.getOSZDir(), beatmapDir);
|
||||
OsuParser.parseAllFiles(beatmapDir, container.getWidth(), container.getHeight());
|
||||
OsuParser.parseAllFiles(beatmapDir);
|
||||
|
||||
// initialize song list
|
||||
if (OsuGroupList.get().size() > 0) {
|
||||
|
|
|
@ -93,8 +93,6 @@ public class Splash extends BasicGameState {
|
|||
finished = true;
|
||||
} else {
|
||||
// load resources in a new thread
|
||||
final int width = container.getWidth();
|
||||
final int height = container.getHeight();
|
||||
thread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
@ -104,7 +102,7 @@ public class Splash extends BasicGameState {
|
|||
OszUnpacker.unpackAllFiles(Options.getOSZDir(), beatmapDir);
|
||||
|
||||
// parse song directory
|
||||
OsuParser.parseAllFiles(beatmapDir, width, height);
|
||||
OsuParser.parseAllFiles(beatmapDir);
|
||||
|
||||
// load sounds
|
||||
SoundController.init();
|
||||
|
|
Loading…
Reference in New Issue
Block a user