Merge pull request #24 from 4ilo/master
Get and display world record from speedrun.com
This commit is contained in:
commit
26c712aaf2
|
@ -23,6 +23,7 @@ dependencies {
|
||||||
compile fileTree(dir: 'lib', include: ['*.jar'])
|
compile fileTree(dir: 'lib', include: ['*.jar'])
|
||||||
compile 'com.1stleg:jnativehook:2.0.2'
|
compile 'com.1stleg:jnativehook:2.0.2'
|
||||||
compile 'com.thoughtworks.xstream:xstream:1.4.4'
|
compile 'com.thoughtworks.xstream:xstream:1.4.4'
|
||||||
|
compile group: 'org.json', name: 'json', version: '20180130'
|
||||||
}
|
}
|
||||||
|
|
||||||
macAppBundle {
|
macAppBundle {
|
||||||
|
|
32
src/main/java/org/fenix/WorldRecord/Category.java
Normal file
32
src/main/java/org/fenix/WorldRecord/Category.java
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
32
src/main/java/org/fenix/WorldRecord/Game.java
Normal file
32
src/main/java/org/fenix/WorldRecord/Game.java
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
58
src/main/java/org/fenix/WorldRecord/JSONReader.java
Normal file
58
src/main/java/org/fenix/WorldRecord/JSONReader.java
Normal file
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
299
src/main/java/org/fenix/WorldRecord/RecordDialog.java
Normal file
299
src/main/java/org/fenix/WorldRecord/RecordDialog.java
Normal file
|
@ -0,0 +1,299 @@
|
||||||
|
package org.fenix.WorldRecord;
|
||||||
|
|
||||||
|
import org.fenix.llanfair.dialog.*;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.awt.event.*;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog window to select a world record on speedrun.com
|
||||||
|
* @author 4ilo 2018
|
||||||
|
*/
|
||||||
|
public class RecordDialog extends JDialog
|
||||||
|
{
|
||||||
|
private JLabel searchLabel = new JLabel("Search game:");
|
||||||
|
private JButton searchButton = new JButton("Search");
|
||||||
|
private JTextField searchField = new JTextField();
|
||||||
|
|
||||||
|
private DefaultComboBoxModel<Game> gameListModel = new DefaultComboBoxModel<>();
|
||||||
|
private JComboBox<Game> games = new JComboBox<>(gameListModel);
|
||||||
|
private JLabel gamesLabel = new JLabel("Games:");
|
||||||
|
|
||||||
|
private DefaultComboBoxModel<Category> categoryListModel = new DefaultComboBoxModel<>();
|
||||||
|
private JComboBox<Category> categories = new JComboBox<>(categoryListModel);
|
||||||
|
private JLabel categoriesLabel = new JLabel("Categories:");
|
||||||
|
|
||||||
|
private JButton close = new JButton("Close");
|
||||||
|
private JButton ok = new JButton("Ok");
|
||||||
|
private JLabel worldRecord = new JLabel("Unknown World Record");
|
||||||
|
|
||||||
|
private ActionListener categoryListener;
|
||||||
|
private ActionListener gameListener;
|
||||||
|
|
||||||
|
private Category category;
|
||||||
|
private EditRun editRun;
|
||||||
|
|
||||||
|
|
||||||
|
public RecordDialog(EditRun editRun)
|
||||||
|
{
|
||||||
|
this.editRun = editRun;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
JPanel categoriesPanel = new JPanel(new GridLayout(1,2));
|
||||||
|
{
|
||||||
|
categoriesPanel.add(categoriesLabel);
|
||||||
|
categoriesPanel.add(categories);
|
||||||
|
|
||||||
|
categories.setEnabled(false);
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search the game title on speedrun.com
|
||||||
|
* @param name The name of the game
|
||||||
|
*/
|
||||||
|
private void searchGame(String name)
|
||||||
|
{
|
||||||
|
this.resetFields();
|
||||||
|
|
||||||
|
ArrayList<Game> games = new ArrayList<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
games = WorldRecordParser.searchGames(name);
|
||||||
|
} catch (IOException e)
|
||||||
|
{
|
||||||
|
showError();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setGames(games);
|
||||||
|
this.addGameListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the categories for a game on speedrun.com
|
||||||
|
* @param game A game object received from the game search
|
||||||
|
*/
|
||||||
|
private void getCategories(Game game)
|
||||||
|
{
|
||||||
|
ArrayList<Category> categories = new ArrayList<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
categories = WorldRecordParser.getCategories(game);
|
||||||
|
} catch (IOException e)
|
||||||
|
{
|
||||||
|
showError();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setCategories(categories);
|
||||||
|
this.addCategoryListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the world record time and owner from speedrun.com
|
||||||
|
* @param category A category object received from the category search
|
||||||
|
*/
|
||||||
|
private void getWorldRecord(Category category)
|
||||||
|
{
|
||||||
|
String worldRecord = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
worldRecord = WorldRecordParser.getRecord(category);
|
||||||
|
} catch (IOException e)
|
||||||
|
{
|
||||||
|
showError();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.worldRecord.setText(worldRecord);
|
||||||
|
this.category = category;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append the games in the array to the games combobox
|
||||||
|
* @param games A ArrayList of game objects
|
||||||
|
*/
|
||||||
|
private void setGames(ArrayList<Game> games)
|
||||||
|
{
|
||||||
|
this.resetFields();
|
||||||
|
|
||||||
|
for(Game game: games)
|
||||||
|
{
|
||||||
|
gameListModel.addElement(game);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.games.setEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append the categories in the array to the categories checkbox
|
||||||
|
* @param categories A ArrayList of Category objects
|
||||||
|
*/
|
||||||
|
private void setCategories(ArrayList<Category> categories)
|
||||||
|
{
|
||||||
|
categoryListModel.removeAllElements();
|
||||||
|
|
||||||
|
for(Category category: categories)
|
||||||
|
{
|
||||||
|
categoryListModel.addElement(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.categories.setEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the dialog without saving
|
||||||
|
*/
|
||||||
|
private void close()
|
||||||
|
{
|
||||||
|
this.setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the dialog and send a signal to the parent window
|
||||||
|
*/
|
||||||
|
private void actionOk()
|
||||||
|
{
|
||||||
|
this.category = ((Category) categories.getSelectedItem());
|
||||||
|
this.setVisible(false);
|
||||||
|
|
||||||
|
this.editRun.recordSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the changeListener to the categories combobox
|
||||||
|
*/
|
||||||
|
private void addCategoryListener()
|
||||||
|
{
|
||||||
|
categoryListener = new ActionListener() {
|
||||||
|
public void actionPerformed(ActionEvent e)
|
||||||
|
{
|
||||||
|
getWorldRecord((Category) categories.getSelectedItem());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
categories.addActionListener(categoryListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the changeListener to the games combobox
|
||||||
|
*/
|
||||||
|
private void addGameListener()
|
||||||
|
{
|
||||||
|
|
||||||
|
gameListener = new ActionListener() {
|
||||||
|
public void actionPerformed(ActionEvent e)
|
||||||
|
{
|
||||||
|
getCategories((Game) games.getSelectedItem());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
games.addActionListener(gameListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the comboboxes
|
||||||
|
*/
|
||||||
|
private void resetFields()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
categories.removeActionListener(categoryListener);
|
||||||
|
games.removeActionListener(gameListener);
|
||||||
|
} catch (Exception e){}
|
||||||
|
|
||||||
|
categories.setEnabled(false);
|
||||||
|
games.setEnabled(false);
|
||||||
|
|
||||||
|
gameListModel.removeAllElements();
|
||||||
|
categoryListModel.removeAllElements();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void showError()
|
||||||
|
{
|
||||||
|
JOptionPane.showMessageDialog(null, "Could not connect to speedrun.com",
|
||||||
|
"Error", JOptionPane.ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the dialog
|
||||||
|
*/
|
||||||
|
public void display()
|
||||||
|
{
|
||||||
|
setAlwaysOnTop(true);
|
||||||
|
setModalityType(ModalityType.APPLICATION_MODAL);
|
||||||
|
pack();
|
||||||
|
setVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the selected category object
|
||||||
|
* @return Category
|
||||||
|
*/
|
||||||
|
public Category getCategory()
|
||||||
|
{
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the selected record string
|
||||||
|
* @return String
|
||||||
|
*/
|
||||||
|
public String getRecordString()
|
||||||
|
{
|
||||||
|
return worldRecord.getText();
|
||||||
|
}
|
||||||
|
}
|
132
src/main/java/org/fenix/WorldRecord/WorldRecordParser.java
Normal file
132
src/main/java/org/fenix/WorldRecord/WorldRecordParser.java
Normal file
|
@ -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<Game> searchGames(String name) throws IOException
|
||||||
|
{
|
||||||
|
String url = "https://www.speedrun.com/api/v1/games?name=" + name;
|
||||||
|
JSONArray json_games;
|
||||||
|
ArrayList<Game> 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<Category> getCategories(Game game) throws IOException
|
||||||
|
{
|
||||||
|
String url = "https://www.speedrun.com/api/v1/games/" + game.getId() + "/categories";
|
||||||
|
|
||||||
|
ArrayList<Category> 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 += String.format("%02d:", hours);
|
||||||
|
|
||||||
|
time += String.format("%02d:%02d", minutes, seconds);
|
||||||
|
|
||||||
|
if(time_seconds % 1 != 0)
|
||||||
|
{
|
||||||
|
int miliseconds = Math.round((time_seconds%1) * 1000);
|
||||||
|
time += "." + miliseconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
}
|
|
@ -109,6 +109,7 @@ public enum Language {
|
||||||
setting_footer_multiline,
|
setting_footer_multiline,
|
||||||
setting_footer_deltaLabels,
|
setting_footer_deltaLabels,
|
||||||
setting_footer_sumOfBest,
|
setting_footer_sumOfBest,
|
||||||
|
setting_footer_worldRecord,
|
||||||
|
|
||||||
// Accuracy
|
// Accuracy
|
||||||
accuracy_seconds,
|
accuracy_seconds,
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package org.fenix.llanfair;
|
package org.fenix.llanfair;
|
||||||
|
|
||||||
import com.thoughtworks.xstream.annotations.XStreamOmitField;
|
import com.thoughtworks.xstream.annotations.XStreamOmitField;
|
||||||
|
import org.fenix.WorldRecord.Category;
|
||||||
|
import org.fenix.WorldRecord.WorldRecordParser;
|
||||||
import org.fenix.llanfair.config.Settings;
|
import org.fenix.llanfair.config.Settings;
|
||||||
import org.fenix.utils.TableModelSupport;
|
import org.fenix.utils.TableModelSupport;
|
||||||
import org.fenix.utils.config.Configuration;
|
import org.fenix.utils.config.Configuration;
|
||||||
|
@ -104,7 +106,7 @@ public class Run implements TableModel, Serializable {
|
||||||
|
|
||||||
public static final String COMPLETED_ATTEMPT_COUNTER_PROPERTY = "run.completedAttemptCounter";
|
public static final String COMPLETED_ATTEMPT_COUNTER_PROPERTY = "run.completedAttemptCounter";
|
||||||
|
|
||||||
|
public static final String RECORD_CATEGORY_PROPERTY = "run.record.category";
|
||||||
|
|
||||||
public static final String DELAYED_START_PROPERTY = "run.delayedStart";
|
public static final String DELAYED_START_PROPERTY = "run.delayedStart";
|
||||||
|
|
||||||
|
@ -185,6 +187,8 @@ public class Run implements TableModel, Serializable {
|
||||||
@XStreamOmitField
|
@XStreamOmitField
|
||||||
private int sessionAttempts;
|
private int sessionAttempts;
|
||||||
|
|
||||||
|
private Category recordCategory;
|
||||||
|
|
||||||
// ----------------------------------------------------------- CONSTRUCTORS
|
// ----------------------------------------------------------- CONSTRUCTORS
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -598,6 +602,23 @@ public class Run implements TableModel, Serializable {
|
||||||
|
|
||||||
public int getSessionAttempts() { return sessionAttempts; }
|
public int getSessionAttempts() { return sessionAttempts; }
|
||||||
|
|
||||||
|
public String getRecordString() {
|
||||||
|
String recordString;
|
||||||
|
|
||||||
|
try {
|
||||||
|
recordString = WorldRecordParser.getRecord(this.recordCategory);
|
||||||
|
} catch (Exception e) {
|
||||||
|
recordString = "Unknown World Record";
|
||||||
|
}
|
||||||
|
|
||||||
|
return recordString;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Category getRecordCategory()
|
||||||
|
{
|
||||||
|
return recordCategory;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------- SETTERS
|
// ---------------------------------------------------------------- SETTERS
|
||||||
|
|
||||||
public<T> T getSetting( String key ) {
|
public<T> T getSetting( String key ) {
|
||||||
|
@ -648,6 +669,15 @@ public class Run implements TableModel, Serializable {
|
||||||
pcSupport.firePropertyChange(DELAYED_START_PROPERTY, old, delayedStart);
|
pcSupport.firePropertyChange(DELAYED_START_PROPERTY, old, delayedStart);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setRecordCategory(Category category)
|
||||||
|
{
|
||||||
|
Category old = this.recordCategory;
|
||||||
|
|
||||||
|
this.recordCategory = category;
|
||||||
|
|
||||||
|
pcSupport.firePropertyChange(RECORD_CATEGORY_PROPERTY, old, name);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inserts the given segment at the end. If it's the first segment being
|
* Inserts the given segment at the end. If it's the first segment being
|
||||||
* added, the run becomes {@link State#READY}, meaning it can be started.
|
* added, the run becomes {@link State#READY}, meaning it can be started.
|
||||||
|
@ -1155,6 +1185,9 @@ public class Run implements TableModel, Serializable {
|
||||||
if ( configuration == null ) {
|
if ( configuration == null ) {
|
||||||
configuration = new Configuration();
|
configuration = new Configuration();
|
||||||
}
|
}
|
||||||
|
if(recordCategory == null) {
|
||||||
|
recordCategory = new Category("", "");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -124,6 +124,7 @@ public class Settings {
|
||||||
public static final Property<Boolean> footerMultiline = new Property<>( "footer.multiline" );
|
public static final Property<Boolean> footerMultiline = new Property<>( "footer.multiline" );
|
||||||
public static final Property<Boolean> footerShowDeltaLabels = new Property<>( "footer.deltaLabels" );
|
public static final Property<Boolean> footerShowDeltaLabels = new Property<>( "footer.deltaLabels" );
|
||||||
public static final Property<Boolean> footerShowSumOfBest = new Property<>( "footer.sumOfBest" );
|
public static final Property<Boolean> footerShowSumOfBest = new Property<>( "footer.sumOfBest" );
|
||||||
|
public static final Property<Boolean> footerShowWorldRecord = new Property<>("footer.worldRecord");
|
||||||
|
|
||||||
private static Configuration global = null;
|
private static Configuration global = null;
|
||||||
private static Run run = null;
|
private static Run run = null;
|
||||||
|
@ -294,6 +295,7 @@ public class Settings {
|
||||||
setDefault( footerMultiline.key, true, force );
|
setDefault( footerMultiline.key, true, force );
|
||||||
setDefault( footerShowDeltaLabels.key, true, force );
|
setDefault( footerShowDeltaLabels.key, true, force );
|
||||||
setDefault( footerShowSumOfBest.key, false, force );
|
setDefault( footerShowSumOfBest.key, false, force );
|
||||||
|
setDefault( footerShowWorldRecord.key, true, force);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -4,6 +4,8 @@ import org.fenix.llanfair.*;
|
||||||
import org.fenix.llanfair.config.Accuracy;
|
import org.fenix.llanfair.config.Accuracy;
|
||||||
import org.fenix.utils.gui.GBC;
|
import org.fenix.utils.gui.GBC;
|
||||||
|
|
||||||
|
import org.fenix.WorldRecord.*;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import javax.swing.event.ListSelectionEvent;
|
import javax.swing.event.ListSelectionEvent;
|
||||||
import javax.swing.event.ListSelectionListener;
|
import javax.swing.event.ListSelectionListener;
|
||||||
|
@ -107,6 +109,16 @@ implements ActionListener, ListSelectionListener {
|
||||||
|
|
||||||
private JTextField runDelayedStart;
|
private JTextField runDelayedStart;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* World record data from spedrun.com
|
||||||
|
*/
|
||||||
|
private JLabel recordLabel;
|
||||||
|
private JButton selectRecord;
|
||||||
|
private RecordDialog recordSelector;
|
||||||
|
private Category recordCategory;
|
||||||
|
private JLabel recordString;
|
||||||
|
|
||||||
|
|
||||||
// ----------------------------------------------------------- CONSTRUCTEURS
|
// ----------------------------------------------------------- CONSTRUCTEURS
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -159,6 +171,20 @@ implements ActionListener, ListSelectionListener {
|
||||||
moveUp = new JButton(Llanfair.getResources().getIcon("ARROW_UP.png"));
|
moveUp = new JButton(Llanfair.getResources().getIcon("ARROW_UP.png"));
|
||||||
moveDown = new JButton(Llanfair.getResources().getIcon("ARROW_DOWN.png"));
|
moveDown = new JButton(Llanfair.getResources().getIcon("ARROW_DOWN.png"));
|
||||||
segmented = new JCheckBox("" + Language.ED_SEGMENTED, run.isSegmented());
|
segmented = new JCheckBox("" + Language.ED_SEGMENTED, run.isSegmented());
|
||||||
|
recordLabel = new JLabel("World record");
|
||||||
|
selectRecord = new JButton("Select record");
|
||||||
|
recordCategory = run.getRecordCategory();
|
||||||
|
|
||||||
|
if(!recordCategory.getId().equals("")) {
|
||||||
|
try {
|
||||||
|
recordString = new JLabel(WorldRecordParser.getRecord(recordCategory));
|
||||||
|
} catch(Exception e) {
|
||||||
|
RecordDialog.showError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
recordString = new JLabel();
|
||||||
|
}
|
||||||
|
|
||||||
placeComponents();
|
placeComponents();
|
||||||
setBehavior();
|
setBehavior();
|
||||||
|
@ -186,10 +212,14 @@ implements ActionListener, ListSelectionListener {
|
||||||
add(moveUp, GBC.grid(3, 5).insets(0, 4).anchor(GBC.FIRST_LINE_START));
|
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(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));
|
||||||
|
add(recordString, GBC.grid(1,8).insets(4,0,0,4).anchor(GBC.LINE_START));
|
||||||
|
|
||||||
JPanel controls = new JPanel();
|
JPanel controls = new JPanel();
|
||||||
controls.add(save);
|
controls.add(save);
|
||||||
controls.add(cancel);
|
controls.add(cancel);
|
||||||
add(controls, GBC.grid(0, 7, 4, 1));
|
add(controls, GBC.grid(0, 8, 4, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -227,6 +257,7 @@ implements ActionListener, ListSelectionListener {
|
||||||
moveDown.addActionListener(this);
|
moveDown.addActionListener(this);
|
||||||
cancel.addActionListener(this);
|
cancel.addActionListener(this);
|
||||||
save.addActionListener(this);
|
save.addActionListener(this);
|
||||||
|
selectRecord.addActionListener(this);
|
||||||
|
|
||||||
// Insertion des délégués de rendus et d’édition.
|
// Insertion des délégués de rendus et d’édition.
|
||||||
segments.setDefaultRenderer(Icon.class, new IconRenderer());
|
segments.setDefaultRenderer(Icon.class, new IconRenderer());
|
||||||
|
@ -301,6 +332,8 @@ implements ActionListener, ListSelectionListener {
|
||||||
long delayedStart = parseDelayedStartTime(runDelayedStart.getText());
|
long delayedStart = parseDelayedStartTime(runDelayedStart.getText());
|
||||||
run.setDelayedStart(delayedStart == -1 ? 0 : delayedStart);
|
run.setDelayedStart(delayedStart == -1 ? 0 : delayedStart);
|
||||||
|
|
||||||
|
run.setRecordCategory(recordCategory);
|
||||||
|
|
||||||
dispose();
|
dispose();
|
||||||
|
|
||||||
} else if (source.equals(cancel)) {
|
} else if (source.equals(cancel)) {
|
||||||
|
@ -317,6 +350,9 @@ implements ActionListener, ListSelectionListener {
|
||||||
int selected = segments.getSelectedRow();
|
int selected = segments.getSelectedRow();
|
||||||
run.moveSegmentDown(selected);
|
run.moveSegmentDown(selected);
|
||||||
segments.setRowSelectionInterval(selected + 1, selected + 1);
|
segments.setRowSelectionInterval(selected + 1, selected + 1);
|
||||||
|
} else if (source.equals(selectRecord)) {
|
||||||
|
recordSelector = new RecordDialog(this);
|
||||||
|
recordSelector.display();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -339,6 +375,13 @@ implements ActionListener, ListSelectionListener {
|
||||||
moveDown.setEnabled(enabled && selected < run.getRowCount() - 1);
|
moveDown.setEnabled(enabled && selected < run.getRowCount() - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void recordSet() {
|
||||||
|
this.recordCategory = recordSelector.getCategory();
|
||||||
|
this.recordString.setText(recordSelector.getRecordString());
|
||||||
|
|
||||||
|
this.recordSelector.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------- CLASSE INTERNE
|
// ---------------------------------------------------------- CLASSE INTERNE
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -30,6 +30,7 @@ public class TabComponents extends SettingsTab
|
||||||
SCB_SETTINGS.add(Settings.footerShowBestTime);
|
SCB_SETTINGS.add(Settings.footerShowBestTime);
|
||||||
SCB_SETTINGS.add(Settings.footerMultiline);
|
SCB_SETTINGS.add(Settings.footerMultiline);
|
||||||
SCB_SETTINGS.add(Settings.footerVerbose);
|
SCB_SETTINGS.add(Settings.footerVerbose);
|
||||||
|
SCB_SETTINGS.add(Settings.footerShowWorldRecord);
|
||||||
SCB_SETTINGS.add(Settings.footerShowSumOfBest);
|
SCB_SETTINGS.add(Settings.footerShowSumOfBest);
|
||||||
SCB_SETTINGS.add(Settings.coreShowSegmentName);
|
SCB_SETTINGS.add(Settings.coreShowSegmentName);
|
||||||
SCB_SETTINGS.add(Settings.coreShowSplitTime);
|
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.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.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.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(
|
footerPanel.setBorder(
|
||||||
BorderFactory.createTitledBorder("" + Language.FOOTER)
|
BorderFactory.createTitledBorder("" + Language.FOOTER)
|
||||||
);
|
);
|
||||||
|
|
|
@ -29,6 +29,7 @@ class Footer extends JPanel {
|
||||||
private static final int TEXT = 0x04;
|
private static final int TEXT = 0x04;
|
||||||
private static final int BEST = 0x08;
|
private static final int BEST = 0x08;
|
||||||
private static final int VERBOSE = 0x10;
|
private static final int VERBOSE = 0x10;
|
||||||
|
private static final int WORLD_RECORD = 0x20;
|
||||||
|
|
||||||
private static final int INSET = 3;
|
private static final int INSET = 3;
|
||||||
|
|
||||||
|
@ -62,6 +63,8 @@ class Footer extends JPanel {
|
||||||
private boolean resize;
|
private boolean resize;
|
||||||
private Dimension preferredSize;
|
private Dimension preferredSize;
|
||||||
|
|
||||||
|
private JLabel worldRecord;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a default panel displaying informations for the given run.
|
* Creates a default panel displaying informations for the given run.
|
||||||
*
|
*
|
||||||
|
@ -88,6 +91,8 @@ class Footer extends JPanel {
|
||||||
preferredSize = null;
|
preferredSize = null;
|
||||||
resize = false;
|
resize = false;
|
||||||
|
|
||||||
|
worldRecord = new JLabel();
|
||||||
|
|
||||||
setRun(run);
|
setRun(run);
|
||||||
setOpaque(false);
|
setOpaque(false);
|
||||||
|
|
||||||
|
@ -130,6 +135,7 @@ class Footer extends JPanel {
|
||||||
boolean ftVerbose = Settings.footerVerbose.get();
|
boolean ftVerbose = Settings.footerVerbose.get();
|
||||||
boolean ftTwoLines = Settings.footerMultiline.get();
|
boolean ftTwoLines = Settings.footerMultiline.get();
|
||||||
boolean ftSumOfBest = Settings.footerShowSumOfBest.get();
|
boolean ftSumOfBest = Settings.footerShowSumOfBest.get();
|
||||||
|
boolean ftWorldRecord = Settings.footerShowWorldRecord.get();
|
||||||
|
|
||||||
int height = Math.max(timeH, labelH);
|
int height = Math.max(timeH, labelH);
|
||||||
int width = prevW + timeW + smtmW + INSET * 2;
|
int width = prevW + timeW + smtmW + INSET * 2;
|
||||||
|
@ -155,6 +161,10 @@ class Footer extends JPanel {
|
||||||
if (ftSumOfBest) {
|
if (ftSumOfBest) {
|
||||||
height += labelH;
|
height += labelH;
|
||||||
}
|
}
|
||||||
|
if (ftWorldRecord) {
|
||||||
|
height += labelH;
|
||||||
|
}
|
||||||
|
|
||||||
preferredSize = new Dimension(width, height);
|
preferredSize = new Dimension(width, height);
|
||||||
setMinimumSize(new Dimension(50, height));
|
setMinimumSize(new Dimension(50, height));
|
||||||
resize = false;
|
resize = false;
|
||||||
|
@ -220,7 +230,8 @@ class Footer extends JPanel {
|
||||||
|
|
||||||
updateVisibility(BEST);
|
updateVisibility(BEST);
|
||||||
forceResize();
|
forceResize();
|
||||||
} else if (Settings.footerShowDeltaLabels.equals(property)) {
|
} else if (Settings.footerShowDeltaLabels.equals(property)
|
||||||
|
|| Settings.footerShowWorldRecord.equals(property)) {
|
||||||
updateVisibility(TEXT);
|
updateVisibility(TEXT);
|
||||||
forceResize();
|
forceResize();
|
||||||
} else if (Settings.footerVerbose.equals(property)) {
|
} else if (Settings.footerVerbose.equals(property)) {
|
||||||
|
@ -242,6 +253,8 @@ class Footer extends JPanel {
|
||||||
updateValues(TIME | TEXT);
|
updateValues(TIME | TEXT);
|
||||||
updateSize();
|
updateSize();
|
||||||
forceResize();
|
forceResize();
|
||||||
|
} else if (Run.RECORD_CATEGORY_PROPERTY.equals(property)) {
|
||||||
|
updateValues(WORLD_RECORD);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -328,11 +341,20 @@ class Footer extends JPanel {
|
||||||
);
|
);
|
||||||
panelSumOfBest.setOpaque(false);
|
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(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(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(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(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(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) {
|
private void updateVisibility(int identifier) {
|
||||||
|
@ -352,6 +374,7 @@ class Footer extends JPanel {
|
||||||
labelDelta.setVisible(ftLabels && !ftVerbose);
|
labelDelta.setVisible(ftLabels && !ftVerbose);
|
||||||
labelDeltaBest.setVisible(ftLabels);
|
labelDeltaBest.setVisible(ftLabels);
|
||||||
labelSumOfBest.setVisible(Settings.footerShowSumOfBest.get());
|
labelSumOfBest.setVisible(Settings.footerShowSumOfBest.get());
|
||||||
|
worldRecord.setVisible(Settings.footerShowWorldRecord.get());
|
||||||
}
|
}
|
||||||
if ((identifier & VERBOSE) == VERBOSE) {
|
if ((identifier & VERBOSE) == VERBOSE) {
|
||||||
boolean ftVerbose = Settings.footerVerbose.get();
|
boolean ftVerbose = Settings.footerVerbose.get();
|
||||||
|
@ -416,6 +439,7 @@ class Footer extends JPanel {
|
||||||
labelBest.setForeground(color);
|
labelBest.setForeground(color);
|
||||||
labelDeltaBest.setForeground(color);
|
labelDeltaBest.setForeground(color);
|
||||||
labelSumOfBest.setForeground(color);
|
labelSumOfBest.setForeground(color);
|
||||||
|
worldRecord.setForeground(color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -445,6 +469,7 @@ class Footer extends JPanel {
|
||||||
labelBest.setFont(Settings.coreFont.get());
|
labelBest.setFont(Settings.coreFont.get());
|
||||||
labelDeltaBest.setFont(Settings.coreFont.get());
|
labelDeltaBest.setFont(Settings.coreFont.get());
|
||||||
labelSumOfBest.setFont(Settings.coreFont.get());
|
labelSumOfBest.setFont(Settings.coreFont.get());
|
||||||
|
worldRecord.setFont(Settings.coreFont.get());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -563,6 +588,9 @@ class Footer extends JPanel {
|
||||||
labelDeltaBest.setText("" + Language.LB_FT_DELTA_BEST);
|
labelDeltaBest.setText("" + Language.LB_FT_DELTA_BEST);
|
||||||
labelSumOfBest.setText("" + Language.LB_FT_SUM_OF_BEST);
|
labelSumOfBest.setText("" + Language.LB_FT_SUM_OF_BEST);
|
||||||
}
|
}
|
||||||
|
if((identifier & WORLD_RECORD) == WORLD_RECORD) {
|
||||||
|
worldRecord.setText(run.getRecordString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateSize() {
|
private void updateSize() {
|
||||||
|
|
|
@ -90,6 +90,7 @@ setting_footer_bestTime = Best Time
|
||||||
setting_footer_multiline = Display on Two Lines
|
setting_footer_multiline = Display on Two Lines
|
||||||
setting_footer_deltaLabels = Delta Labels
|
setting_footer_deltaLabels = Delta Labels
|
||||||
setting_footer_sumOfBest = Sum of Best
|
setting_footer_sumOfBest = Sum of Best
|
||||||
|
setting_footer_worldRecord = World Record
|
||||||
|
|
||||||
# Accuracy
|
# Accuracy
|
||||||
accuracy_seconds = Seconds
|
accuracy_seconds = Seconds
|
||||||
|
|
Reference in a new issue