From bcc1003d1d529e929f5011ef9978bd82c4f32d28 Mon Sep 17 00:00:00 2001 From: olivier Date: Mon, 12 Feb 2018 19:33:44 +0100 Subject: [PATCH] start adding world record info from speedrun.com to timer and settings. --- build.gradle | 1 + .../java/org/fenix/WorldRecord/Category.java | 32 +++ src/main/java/org/fenix/WorldRecord/Game.java | 32 +++ .../org/fenix/WorldRecord/JSONReader.java | 58 ++++++ .../org/fenix/WorldRecord/RecordDialog.java | 186 ++++++++++++++++++ .../fenix/WorldRecord/WorldRecordParser.java | 132 +++++++++++++ .../java/org/fenix/llanfair/Language.java | 1 + .../org/fenix/llanfair/config/Settings.java | 2 + .../org/fenix/llanfair/dialog/EditRun.java | 21 +- .../fenix/llanfair/dialog/TabComponents.java | 2 + .../java/org/fenix/llanfair/gui/Footer.java | 24 ++- src/main/resources/language.properties | 1 + 12 files changed, 490 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/fenix/WorldRecord/Category.java create mode 100644 src/main/java/org/fenix/WorldRecord/Game.java create mode 100644 src/main/java/org/fenix/WorldRecord/JSONReader.java create mode 100644 src/main/java/org/fenix/WorldRecord/RecordDialog.java create mode 100644 src/main/java/org/fenix/WorldRecord/WorldRecordParser.java diff --git a/build.gradle b/build.gradle index 2ef5baa..9800faf 100644 --- a/build.gradle +++ b/build.gradle @@ -23,6 +23,7 @@ dependencies { compile fileTree(dir: 'lib', include: ['*.jar']) compile 'com.1stleg:jnativehook:2.0.2' compile 'com.thoughtworks.xstream:xstream:1.4.4' + compile group: 'org.json', name: 'json', version: '20180130' } macAppBundle { diff --git a/src/main/java/org/fenix/WorldRecord/Category.java b/src/main/java/org/fenix/WorldRecord/Category.java new file mode 100644 index 0000000..269c9e8 --- /dev/null +++ b/src/main/java/org/fenix/WorldRecord/Category.java @@ -0,0 +1,32 @@ +package org.fenix.WorldRecord; + +/** + * Category class to represent a speedrun.com category + * @author 4ilo 2018 + */ +public class Category +{ + private String name = ""; + private String id = ""; + + public Category(String name, String id) + { + this.name = name; + this.id = id; + } + + public String getName() + { + return name; + } + + public String getId() + { + return id; + } + + public String toString() + { + return this.name; + } +} diff --git a/src/main/java/org/fenix/WorldRecord/Game.java b/src/main/java/org/fenix/WorldRecord/Game.java new file mode 100644 index 0000000..6c42749 --- /dev/null +++ b/src/main/java/org/fenix/WorldRecord/Game.java @@ -0,0 +1,32 @@ +package org.fenix.WorldRecord; + +/** + * Game class to represent a speedrun.com game + * @author 4ilo 2018 + */ +public class Game +{ + private String title; + private String id; + + public Game(String title, String id) + { + this.title = title; + this.id = id; + } + + public String getTitle() + { + return title; + } + + public String getId() + { + return id; + } + + public String toString() + { + return this.title; + } +} diff --git a/src/main/java/org/fenix/WorldRecord/JSONReader.java b/src/main/java/org/fenix/WorldRecord/JSONReader.java new file mode 100644 index 0000000..67c8c31 --- /dev/null +++ b/src/main/java/org/fenix/WorldRecord/JSONReader.java @@ -0,0 +1,58 @@ +package org.fenix.WorldRecord; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.*; +import java.net.URL; +import java.nio.charset.Charset; + +/** + * JSONReader class to fetch JSON from api + * @author 4ilo 2018 + */ +public class JSONReader +{ + /** + * Get the content of the given reader as a string + * @param reader a reader object + * @return String with the data of the reader + * @throws IOException + */ + private static String readAll(Reader reader) throws IOException + { + StringBuilder builder = new StringBuilder(); + int cp; + + while((cp = reader.read()) != -1) + { + builder.append((char) cp); + } + + return builder.toString(); + } + + /** + * Read the json from the given json-api url + * @param url the api url + * @return JSONObject with the data from the url + * @throws IOException + * @throws JSONException + */ + public static JSONObject readJsonFromUrl(String url) throws IOException, JSONException + { + InputStream stream = new URL(url).openStream(); + + try + { + BufferedReader reader = new BufferedReader(new InputStreamReader(stream, Charset.forName("UTF-8"))); + String jsonText = readAll(reader); + + return new JSONObject(jsonText); + + } finally + { + stream.close(); + } + } +} diff --git a/src/main/java/org/fenix/WorldRecord/RecordDialog.java b/src/main/java/org/fenix/WorldRecord/RecordDialog.java new file mode 100644 index 0000000..9fd64f9 --- /dev/null +++ b/src/main/java/org/fenix/WorldRecord/RecordDialog.java @@ -0,0 +1,186 @@ +package org.fenix.WorldRecord; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.IOException; +import java.util.ArrayList; + +public class RecordDialog extends JDialog +{ + private JLabel searchLabel = new JLabel("Search game:"); + private JTextField searchField = new JTextField(); + private JButton searchButton = new JButton("Search"); + + private JLabel gamesLabel = new JLabel("Games:"); + private DefaultComboBoxModel gameListModel = new DefaultComboBoxModel<>(); + private JComboBox games = new JComboBox<>(gameListModel); + + private JLabel categoriesLabel = new JLabel("Categories:"); + private DefaultComboBoxModel categoryListModel = new DefaultComboBoxModel<>(); + private JComboBox categories = new JComboBox<>(categoryListModel); + + private JLabel worldRecord = new JLabel(); + private JButton ok = new JButton("Ok"); + private JButton close = new JButton("Close"); + + private String category_id = ""; + + public RecordDialog() + { + JPanel searchPanel = new JPanel(new FlowLayout()); + { + searchField.setPreferredSize(new Dimension(200,30)); + + searchPanel.add(searchLabel); + searchPanel.add(searchField); + searchPanel.add(searchButton); + + searchButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + searchGame(searchField.getText()); + } + }); + } + JPanel gamesPanel = new JPanel(new GridLayout(1,2)); + { + gamesPanel.add(gamesLabel); + gamesPanel.add(games); + + games.setEnabled(false); + + games.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + getCategories((Game) games.getSelectedItem()); + } + }); + } + JPanel categoriesPanel = new JPanel(new GridLayout(1,2)); + { + categoriesPanel.add(categoriesLabel); + categoriesPanel.add(categories); + + categories.setEnabled(false); + + categories.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + getWorldRecord((Category) categories.getSelectedItem()); + } + }); + } + JPanel buttonPanel = new JPanel(new FlowLayout()); + { + buttonPanel.add(ok); + buttonPanel.add(close); + + ok.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + actionOk(); + } + }); + + close.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + close(); + } + }); + } + + getContentPane().setLayout(new BoxLayout(getContentPane(), BoxLayout.PAGE_AXIS)); + this.add(searchPanel); + this.add(gamesPanel); + this.add(categoriesPanel); + + this.add(worldRecord); + worldRecord.setAlignmentX(Component.CENTER_ALIGNMENT); + + this.add(buttonPanel); + + this.setTitle("World Record selection"); + pack(); + } + + private void searchGame(String name) + { + ArrayList games = new ArrayList<>(); + + try { + games = WorldRecordParser.searchGames(name); + } catch (IOException e) + { + System.out.println("fout"); + } + + this.setGames(games); + } + + private void getCategories(Game game) + { + ArrayList categories = new ArrayList<>(); + + try { + categories = WorldRecordParser.getCategories(game); + } catch (IOException e) + { + System.out.println("fout"); + } + + this.setCategories(categories); + } + + private void getWorldRecord(Category category) + { + String worldRecord = ""; + + try { + worldRecord = WorldRecordParser.getRecord(category); + } catch (IOException e) + { + System.out.println("fout"); + } + + this.worldRecord.setText(worldRecord); + this.category_id = category.getId(); + } + + private void setGames(ArrayList games) + { + gameListModel.removeAllElements(); + + for(Game game: games) + { + gameListModel.addElement(game); + } + + this.games.setEnabled(true); + } + + private void setCategories(ArrayList categories) + { + categoryListModel.removeAllElements(); + + for(Category category: categories) + { + categoryListModel.addElement(category); + } + + this.categories.setEnabled(true); + } + + private void close() + { + this.setVisible(false); + } + + private void actionOk() + { + this.category_id = ((Category) categories.getSelectedItem()).getId(); + this.setVisible(false); + } +} diff --git a/src/main/java/org/fenix/WorldRecord/WorldRecordParser.java b/src/main/java/org/fenix/WorldRecord/WorldRecordParser.java new file mode 100644 index 0000000..36e75a8 --- /dev/null +++ b/src/main/java/org/fenix/WorldRecord/WorldRecordParser.java @@ -0,0 +1,132 @@ +package org.fenix.WorldRecord; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.IOException; +import java.util.ArrayList; + +/** + * @author 4ilo 2018 + */ +public class WorldRecordParser +{ + /** + * Search the speedrun.com database for the game with the given name + * @param name The name of the game you want to search + * @return List of games + * @throws IOException + */ + public static ArrayList searchGames(String name) throws IOException + { + String url = "https://www.speedrun.com/api/v1/games?name=" + name; + JSONArray json_games; + ArrayList games = new ArrayList<>(); + + + JSONObject json = JSONReader.readJsonFromUrl(url); + json_games = json.getJSONArray("data"); + + + for(Object game: json_games) + { + JSONObject obj = (JSONObject) game; + games.add(new Game( + obj.getJSONObject("names").get("international").toString(), + obj.get("id").toString() + )); + } + + return games; + } + + /** + * Get the speedrun.com categories for the given game + * @param game WorldRecord.Game object received from a game search + * @return List of categories + * @throws IOException + */ + public static ArrayList getCategories(Game game) throws IOException + { + String url = "https://www.speedrun.com/api/v1/games/" + game.getId() + "/categories"; + + ArrayList categories = new ArrayList<>(); + + + JSONObject json = JSONReader.readJsonFromUrl(url); + JSONArray json_categories = json.getJSONArray("data"); + + for(Object category: json_categories) + { + JSONObject obj = (JSONObject) category; + categories.add(new Category( + obj.get("name").toString(), + obj.get("id").toString() + )); + } + + return categories; + } + + /** + * Get the world record string for the given speedrun.com category + * @param category WorldRecord.Category object received from a category search + * @return The world record string + * @throws IOException + */ + public static String getRecord(Category category) throws IOException + { + String url = "https://www.speedrun.com/api/v1/categories/" + category.getId() + "/records"; + + JSONObject json = JSONReader.readJsonFromUrl(url); + JSONArray json_runs = json.getJSONArray("data").getJSONObject(0).getJSONArray("runs"); + + JSONObject wr_run = json_runs.getJSONObject(0).getJSONObject("run"); + + String player_name = getPlayerName(wr_run.getJSONArray("players").getJSONObject(0)); + + return "World record: " + parseTime(wr_run.getJSONObject("times").getFloat("primary_t")) + " by " + player_name; + } + + /** + * Get the speedrun.com player name for the given player JSONObject + * @param player The player JSONObject extracted from a run object + * @return The players name + * @throws IOException + */ + private static String getPlayerName(JSONObject player) throws IOException + { + String uri = player.get("uri").toString(); + JSONObject json = JSONReader.readJsonFromUrl(uri); + return json.getJSONObject("data").getJSONObject("names").get("international").toString(); + } + + /** + * Parse a time in seconds and miliseconds to a HH:MM:SS.sss format + * @param time_seconds the time in seconds + * @return Time in HH:MM:SS.sss format + */ + private static String parseTime(float time_seconds) + { + int hours = (int) time_seconds / 3600; + int secLeft = (int) time_seconds - hours*3600; + + int minutes = secLeft / 60; + int seconds = secLeft - minutes * 60; + + String time = ""; + + if(hours != 0) + time += hours + ":"; + + time += minutes + ":" + seconds; + + if(time_seconds % 1 != 0) + { + int miliseconds = Math.round((time_seconds%1) * 1000); + time += "." + miliseconds; + } + + return time; + } +} diff --git a/src/main/java/org/fenix/llanfair/Language.java b/src/main/java/org/fenix/llanfair/Language.java index c28a32d..770fd39 100644 --- a/src/main/java/org/fenix/llanfair/Language.java +++ b/src/main/java/org/fenix/llanfair/Language.java @@ -109,6 +109,7 @@ public enum Language { setting_footer_multiline, setting_footer_deltaLabels, setting_footer_sumOfBest, + setting_footer_worldRecord, // Accuracy accuracy_seconds, diff --git a/src/main/java/org/fenix/llanfair/config/Settings.java b/src/main/java/org/fenix/llanfair/config/Settings.java index fd4a3f8..c56217b 100644 --- a/src/main/java/org/fenix/llanfair/config/Settings.java +++ b/src/main/java/org/fenix/llanfair/config/Settings.java @@ -124,6 +124,7 @@ public class Settings { public static final Property footerMultiline = new Property<>( "footer.multiline" ); public static final Property footerShowDeltaLabels = new Property<>( "footer.deltaLabels" ); public static final Property footerShowSumOfBest = new Property<>( "footer.sumOfBest" ); + public static final Property footerShowWorldRecord = new Property<>("footer.worldRecord"); private static Configuration global = null; private static Run run = null; @@ -294,6 +295,7 @@ public class Settings { setDefault( footerMultiline.key, true, force ); setDefault( footerShowDeltaLabels.key, true, force ); setDefault( footerShowSumOfBest.key, false, force ); + setDefault( footerShowWorldRecord.key, true, force); } /** diff --git a/src/main/java/org/fenix/llanfair/dialog/EditRun.java b/src/main/java/org/fenix/llanfair/dialog/EditRun.java index 23dc9ec..faeb841 100644 --- a/src/main/java/org/fenix/llanfair/dialog/EditRun.java +++ b/src/main/java/org/fenix/llanfair/dialog/EditRun.java @@ -4,6 +4,8 @@ import org.fenix.llanfair.*; import org.fenix.llanfair.config.Accuracy; import org.fenix.utils.gui.GBC; +import org.fenix.WorldRecord.*; + import javax.swing.*; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; @@ -107,6 +109,14 @@ implements ActionListener, ListSelectionListener { private JTextField runDelayedStart; + /** + * World record data from spedrun.com + */ + private JLabel recordLabel; + private JButton selectRecord; + private RecordDialog recordSelector; + + // ----------------------------------------------------------- CONSTRUCTEURS /** @@ -159,6 +169,9 @@ implements ActionListener, ListSelectionListener { moveUp = new JButton(Llanfair.getResources().getIcon("ARROW_UP.png")); moveDown = new JButton(Llanfair.getResources().getIcon("ARROW_DOWN.png")); segmented = new JCheckBox("" + Language.ED_SEGMENTED, run.isSegmented()); + recordLabel = new JLabel("World record"); + selectRecord = new JButton("Select record"); + recordSelector = new RecordDialog(); placeComponents(); setBehavior(); @@ -186,10 +199,13 @@ implements ActionListener, ListSelectionListener { add(moveUp, GBC.grid(3, 5).insets(0, 4).anchor(GBC.FIRST_LINE_START)); add(moveDown, GBC.grid(3, 6).insets(4, 4).anchor(GBC.FIRST_LINE_START)); + add(recordLabel, GBC.grid(0,7).insets(5,4,4,0).anchor(GBC.LINE_END)); + add(selectRecord, GBC.grid(1,7).insets(4,0,0,4).anchor(GBC.LINE_START)); + JPanel controls = new JPanel(); controls.add(save); controls.add(cancel); - add(controls, GBC.grid(0, 7, 4, 1)); + add(controls, GBC.grid(0, 8, 4, 1)); } /** @@ -227,6 +243,7 @@ implements ActionListener, ListSelectionListener { moveDown.addActionListener(this); cancel.addActionListener(this); save.addActionListener(this); + selectRecord.addActionListener(this); // Insertion des délégués de rendus et d’édition. segments.setDefaultRenderer(Icon.class, new IconRenderer()); @@ -317,6 +334,8 @@ implements ActionListener, ListSelectionListener { int selected = segments.getSelectedRow(); run.moveSegmentDown(selected); segments.setRowSelectionInterval(selected + 1, selected + 1); + } else if (source.equals(selectRecord)) { + recordSelector.setVisible(true); } } diff --git a/src/main/java/org/fenix/llanfair/dialog/TabComponents.java b/src/main/java/org/fenix/llanfair/dialog/TabComponents.java index 4f4f205..d973b1d 100644 --- a/src/main/java/org/fenix/llanfair/dialog/TabComponents.java +++ b/src/main/java/org/fenix/llanfair/dialog/TabComponents.java @@ -30,6 +30,7 @@ public class TabComponents extends SettingsTab SCB_SETTINGS.add(Settings.footerShowBestTime); SCB_SETTINGS.add(Settings.footerMultiline); SCB_SETTINGS.add(Settings.footerVerbose); + SCB_SETTINGS.add(Settings.footerShowWorldRecord); SCB_SETTINGS.add(Settings.footerShowSumOfBest); SCB_SETTINGS.add(Settings.coreShowSegmentName); SCB_SETTINGS.add(Settings.coreShowSplitTime); @@ -432,6 +433,7 @@ public class TabComponents extends SettingsTab footerPanel.add(checkBoxes.get(Settings.footerShowBestTime.getKey()), GBC.grid(1, 1).anchor(GBC.LINE_START)); footerPanel.add(checkBoxes.get(Settings.footerMultiline.getKey()), GBC.grid(1, 2).anchor(GBC.LINE_START)); footerPanel.add(checkBoxes.get(Settings.footerShowSumOfBest.getKey()), GBC.grid(0, 3).anchor(GBC.LINE_START)); + footerPanel.add(checkBoxes.get(Settings.footerShowWorldRecord.getKey()), GBC.grid(1,3).anchor(GBC.LINE_START)); footerPanel.setBorder( BorderFactory.createTitledBorder("" + Language.FOOTER) ); diff --git a/src/main/java/org/fenix/llanfair/gui/Footer.java b/src/main/java/org/fenix/llanfair/gui/Footer.java index 436c5e9..3215eb3 100644 --- a/src/main/java/org/fenix/llanfair/gui/Footer.java +++ b/src/main/java/org/fenix/llanfair/gui/Footer.java @@ -62,6 +62,8 @@ class Footer extends JPanel { private boolean resize; private Dimension preferredSize; + private JLabel worldRecord; + /** * Creates a default panel displaying informations for the given run. * @@ -88,6 +90,8 @@ class Footer extends JPanel { preferredSize = null; resize = false; + worldRecord = new JLabel("World record: 5 by xem92"); + setRun(run); setOpaque(false); @@ -130,6 +134,7 @@ class Footer extends JPanel { boolean ftVerbose = Settings.footerVerbose.get(); boolean ftTwoLines = Settings.footerMultiline.get(); boolean ftSumOfBest = Settings.footerShowSumOfBest.get(); + boolean ftWorldRecord = Settings.footerShowWorldRecord.get(); int height = Math.max(timeH, labelH); int width = prevW + timeW + smtmW + INSET * 2; @@ -155,6 +160,10 @@ class Footer extends JPanel { if (ftSumOfBest) { height += labelH; } + if (ftWorldRecord) { + height += labelH; + } + preferredSize = new Dimension(width, height); setMinimumSize(new Dimension(50, height)); resize = false; @@ -220,7 +229,8 @@ class Footer extends JPanel { updateVisibility(BEST); forceResize(); - } else if (Settings.footerShowDeltaLabels.equals(property)) { + } else if (Settings.footerShowDeltaLabels.equals(property) + || Settings.footerShowWorldRecord.equals(property)) { updateVisibility(TEXT); forceResize(); } else if (Settings.footerVerbose.equals(property)) { @@ -328,11 +338,20 @@ class Footer extends JPanel { ); panelSumOfBest.setOpaque(false); } + JPanel panelWorldRecord= new JPanel(new GridBagLayout()); + { + panelWorldRecord.add( + worldRecord, + GBC.grid(0, 0).insets(0, 0, 0, INSET) + ); + panelWorldRecord.setOpaque(false); + } add(timePanel, GBC.grid(0, 0).anchor(GBC.LINE_START).weight(0.5, 0.0)); add(deltaPanel, GBC.grid(1, 0).anchor(GBC.LINE_END).weight(0.5, 0.0)); add(panelBest, GBC.grid(0, 1).anchor(GBC.LINE_START).weight(0.5, 0.0)); add(panelDeltaBest, GBC.grid(1, 1).anchor(GBC.LINE_END).weight(0.5, 0.0)); add(panelSumOfBest, GBC.grid(0, 2).anchor(GBC.LINE_START).weight(0.5, 0.0)); + add(panelWorldRecord, GBC.grid(0,3, 2, 1).weight(0.5,0.0)); } private void updateVisibility(int identifier) { @@ -352,6 +371,7 @@ class Footer extends JPanel { labelDelta.setVisible(ftLabels && !ftVerbose); labelDeltaBest.setVisible(ftLabels); labelSumOfBest.setVisible(Settings.footerShowSumOfBest.get()); + worldRecord.setVisible(Settings.footerShowWorldRecord.get()); } if ((identifier & VERBOSE) == VERBOSE) { boolean ftVerbose = Settings.footerVerbose.get(); @@ -416,6 +436,7 @@ class Footer extends JPanel { labelBest.setForeground(color); labelDeltaBest.setForeground(color); labelSumOfBest.setForeground(color); + worldRecord.setForeground(color); } } @@ -445,6 +466,7 @@ class Footer extends JPanel { labelBest.setFont(Settings.coreFont.get()); labelDeltaBest.setFont(Settings.coreFont.get()); labelSumOfBest.setFont(Settings.coreFont.get()); + worldRecord.setFont(Settings.coreFont.get()); } } diff --git a/src/main/resources/language.properties b/src/main/resources/language.properties index 66d6ae2..84684fe 100644 --- a/src/main/resources/language.properties +++ b/src/main/resources/language.properties @@ -90,6 +90,7 @@ setting_footer_bestTime = Best Time setting_footer_multiline = Display on Two Lines setting_footer_deltaLabels = Delta Labels setting_footer_sumOfBest = Sum of Best +setting_footer_worldRecord = World Record # Accuracy accuracy_seconds = Seconds