package chatty.gui.components.admin;
import chatty.gui.MainGui;
import static chatty.gui.components.admin.AdminDialog.SMALL_BUTTON_INSETS;
import static chatty.gui.components.admin.AdminDialog.hideableLabel;
import static chatty.gui.components.admin.AdminDialog.makeGbc;
import chatty.util.DateTime;
import chatty.util.StringUtil;
import chatty.util.api.ChannelInfo;
import chatty.util.api.CommunitiesManager;
import chatty.util.api.CommunitiesManager.Community;
import chatty.util.api.TwitchApi;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
/**
*
* @author tduva
*/
public class StatusPanel extends JPanel {
/**
* Set to not updating the channel info after this time (re-enable buttons).
* This is also done when channel info is received, so when it was set
* successfully it will be set to not updating immediately. This basicially
* is only for the case of error.
*/
private static final int PUT_RESULT_DELAY = 5000;
private final JTextArea status = new JTextArea();
private final JTextField game = new JTextField(20);
private final JTextField community = new JTextField(20);
private final JButton update = new JButton("Update");
private final JLabel updated = new JLabel("No info loaded");
private final JLabel putResult = new JLabel("...");
private final JButton selectGame = new JButton("Select game");
private final JButton removeGame = new JButton("Remove");
private final JButton selectCommunity = new JButton("Select community");
private final JButton removeCommunity = new JButton("Remove");
private final JButton reloadButton = new JButton("reload");
private final JButton historyButton = new JButton("Presets");
private final JButton addToHistoryButton = new JButton("Fav");
private final SelectGameDialog selectGameDialog;
private final SelectCommunityDialog selectCommunityDialog;
private final StatusHistoryDialog statusHistoryDialog;
private final MainGui main;
private final TwitchApi api;
private String currentChannel;
private boolean statusEdited;
private Community currentCommunity = Community.EMPTY;
private long infoLastLoaded;
private boolean loading;
private boolean loadingStatus;
private boolean loadingCommunity;
private String statusLoadError;
private String communityLoadError;
private String statusPutResult;
private String communityPutResult;
private long lastPutResult = -1;
public StatusPanel(AdminDialog parent, MainGui main, TwitchApi api) {
this.main = main;
this.api = api;
selectGameDialog = new SelectGameDialog(main, api);
selectCommunityDialog = new SelectCommunityDialog(main, api);
statusHistoryDialog = new StatusHistoryDialog(parent, main.getStatusHistory());
GridBagConstraints gbc;
setLayout(new GridBagLayout());
JPanel presetPanel = new JPanel();
presetPanel.setLayout(new GridBagLayout());
historyButton.setMargin(SMALL_BUTTON_INSETS);
historyButton.setToolTipText("Open status presets containing favorites "
+ "and status history");
historyButton.setMnemonic(KeyEvent.VK_P);
gbc = makeGbc(2, 0, 1, 1);
gbc.insets = new Insets(5, 5, 5, -1);
gbc.anchor = GridBagConstraints.EAST;
presetPanel.add(historyButton, gbc);
addToHistoryButton.setMargin(SMALL_BUTTON_INSETS);
addToHistoryButton.setToolTipText("Add current status to favorites");
addToHistoryButton.setMnemonic(KeyEvent.VK_F);
gbc = makeGbc(3, 0, 1, 1);
gbc.insets = new Insets(5, 0, 5, 5);
gbc.anchor = GridBagConstraints.EAST;
presetPanel.add(addToHistoryButton, gbc);
updated.setHorizontalAlignment(JLabel.CENTER);
gbc = makeGbc(1,0,1,1);
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.weightx = 1;
gbc.anchor = GridBagConstraints.EAST;
gbc.insets = new Insets(5, 10, 5, 5);
presetPanel.add(updated, gbc);
reloadButton.setMargin(SMALL_BUTTON_INSETS);
reloadButton.setIcon(new ImageIcon(AdminDialog.class.getResource("view-refresh.png")));
reloadButton.setMnemonic(KeyEvent.VK_R);
gbc = makeGbc(0,0,1,1);
gbc.anchor = GridBagConstraints.EAST;
presetPanel.add(reloadButton, gbc);
gbc = makeGbc(0, 1, 3, 1);
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.insets = new Insets(0,0,0,0);
add(presetPanel, gbc);
status.setLineWrap(true);
status.setWrapStyleWord(true);
status.setRows(2);
status.setMargin(new Insets(2,3,3,2));
status.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
statusEdited();
}
@Override
public void removeUpdate(DocumentEvent e) {
statusEdited();
}
@Override
public void changedUpdate(DocumentEvent e) {
statusEdited();
}
});
gbc = makeGbc(0,2,3,1);
gbc.fill = GridBagConstraints.BOTH;
gbc.weightx = 1;
gbc.weighty = 1;
add(new JScrollPane(status), gbc);
game.setEditable(false);
gbc = makeGbc(0,3,1,1);
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.weightx = 1;
add(game, gbc);
selectGame.setMargin(SMALL_BUTTON_INSETS);
selectGame.setMnemonic(KeyEvent.VK_G);
gbc = makeGbc(1,3,1,1);
gbc.fill = GridBagConstraints.HORIZONTAL;
add(selectGame, gbc);
removeGame.setMargin(SMALL_BUTTON_INSETS);
gbc = makeGbc(2,3,1,1);
gbc.fill = GridBagConstraints.HORIZONTAL;
add(removeGame,gbc);
community.setEditable(false);
gbc = makeGbc(0,4,1,1);
gbc.fill = GridBagConstraints.HORIZONTAL;
add(community, gbc);
selectCommunity.setMargin(SMALL_BUTTON_INSETS);
gbc = makeGbc(1,4,1,1);
gbc.fill = GridBagConstraints.HORIZONTAL;
add(selectCommunity, gbc);
removeCommunity.setMargin(SMALL_BUTTON_INSETS);
gbc = makeGbc(2,4,1,1);
gbc.fill = GridBagConstraints.HORIZONTAL;
add(removeCommunity, gbc);
gbc = makeGbc(0,5,3,1);
gbc.fill = GridBagConstraints.HORIZONTAL;
update.setMnemonic(KeyEvent.VK_U);
add(update, gbc);
gbc = makeGbc(0,6,3,1);
add(putResult,gbc);
ActionListener actionListener = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (e.getSource() == update) {
if (currentChannel != null && !currentChannel.isEmpty()) {
loadingStatus = true;
loadingCommunity = true;
setLoading(true);
ChannelInfo info = new ChannelInfo(currentChannel, status.getText(), game.getText());
main.putChannelInfo(info);
putCommunity();
addCurrentToHistory();
}
} else if (e.getSource() == reloadButton) {
getChannelInfo();
} else if (e.getSource() == selectGame) {
selectGameDialog.setLocationRelativeTo(StatusPanel.this);
String result = selectGameDialog.open(game.getText());
if (result != null) {
game.setText(result);
statusEdited();
}
} else if (e.getSource() == removeGame) {
game.setText("");
statusEdited();
} else if (e.getSource() == selectCommunity) {
selectCommunityDialog.setLocationRelativeTo(StatusPanel.this);
CommunitiesManager.Community result = selectCommunityDialog.open(currentCommunity);
if (result != null) {
setCommunity(result);
statusEdited();
}
} else if (e.getSource() == removeCommunity) {
setCommunity(null);
statusEdited();
} else if (e.getSource() == historyButton) {
StatusHistoryEntry result = statusHistoryDialog.showDialog(game.getText());
if (result != null) {
// A null value means that value shouldn't be used to
// change the info, it would be empty otherwise
if (result.title != null) {
status.setText(result.title);
}
if (result.game != null) {
game.setText(result.game);
}
if (result.community != null) {
setCommunity(result.community);
}
}
} else if (e.getSource() == addToHistoryButton) {
addCurrentToFavorites();
}
}
};
reloadButton.addActionListener(actionListener);
selectGame.addActionListener(actionListener);
removeGame.addActionListener(actionListener);
selectCommunity.addActionListener(actionListener);
removeCommunity.addActionListener(actionListener);
historyButton.addActionListener(actionListener);
addToHistoryButton.addActionListener(actionListener);
update.addActionListener(actionListener);
}
public void changeChannel(String channel) {
currentChannel = channel;
status.setText("");
game.setText("");
setCommunity(null);
// This will reset last loaded anyway
getChannelInfo();
}
private void setCommunity(Community c) {
if (c == null) {
currentCommunity = Community.EMPTY;
community.setText(null);
} else {
currentCommunity = c;
community.setText(c.toString());
}
}
private void putCommunity() {
final String channel = currentChannel;
api.setCommunity(currentChannel, currentCommunity.getId(), error -> {
if (currentChannel.equals(channel)) {
if (error != null) {
communityPutResult = "Failed setting community.";
} else {
communityPutResult = "Community updated.";
}
loadingCommunity = false;
checkLoadingDone();
}
});
}
/**
* Channel Info received, which happens when Channel Info is requested
* or when a new status was successfully set.
*
* @param stream Then stream the info is for
* @param info The channel info
*/
public void setChannelInfo(String stream, ChannelInfo info, TwitchApi.RequestResultCode result) {
if (stream.equals(this.currentChannel)) {
if (result == TwitchApi.RequestResultCode.SUCCESS) {
status.setText(info.getStatus());
game.setText(info.getGame());
} else {
infoLastLoaded = -1;
if (result == TwitchApi.RequestResultCode.NOT_FOUND) {
statusLoadError = "Channel not found";
} else {
statusLoadError = "";
}
}
loadingStatus = false;
checkLoadingDone();
}
}
/**
* Sets the result text of a attempted status update, but doesn't set it
* back to "not loading" state, which is done when the channel info is
* returned (which is also contained in the response for this action)
*
* @param result
*/
public void setPutResult(TwitchApi.RequestResultCode result) {
if (result == TwitchApi.RequestResultCode.SUCCESS) {
statusPutResult = "Info updated.";
} else {
if (result == TwitchApi.RequestResultCode.ACCESS_DENIED) {
statusPutResult = "Changing info: Access denied";
updated.setText("Error: Access denied");
} else if (result == TwitchApi.RequestResultCode.FAILED) {
statusPutResult = "Changing info: Unknown error";
updated.setText("Error: Unknown error");
} else if (result == TwitchApi.RequestResultCode.NOT_FOUND) {
statusPutResult = "Changing info: Channel not found.";
updated.setText("Error: Channel not found.");
} else if (result == TwitchApi.RequestResultCode.INVALID_STREAM_STATUS) {
statusPutResult = "Changing info: Invalid title/game (possibly bad language)";
updated.setText("Error: Invalid title/game");
}
}
lastPutResult = System.currentTimeMillis();
loadingStatus = false;
checkLoadingDone();
}
/**
* Changes the text of the putResult label.
*
* @param result
*/
protected void setPutResult(String result) {
hideableLabel(putResult, result);
}
/**
* Request Channel Info from the API.
*/
private void getChannelInfo() {
loadingStatus = true;
loadingCommunity = true;
statusLoadError = null;
communityLoadError = null;
setLoading(true);
main.getChannelInfo(currentChannel);
final String channel = currentChannel;
api.getCommunityForChannel(currentChannel, (r, e) -> {
if (currentChannel.equals(channel)) {
if (r == null) {
communityLoadError = e == null ? "" : e;
} else {
setCommunity(r);
}
loadingCommunity = false;
checkLoadingDone();
}
updateCommunityName(r);
});
}
private void checkLoadingDone() {
if (!loadingStatus && !loadingCommunity) {
statusEdited = false;
updated.setText("Info last loaded: just now");
if (statusPutResult != null || communityPutResult != null) {
setPutResult(statusPutResult+" / "+communityPutResult);
statusPutResult = null;
communityPutResult = null;
}
if (statusLoadError != null || communityLoadError != null) {
infoLastLoaded = -1;
String error = getError(statusLoadError, "Status");
error = StringUtil.append(error, ", ", getError(communityLoadError, "Community"));
if (error.isEmpty()) {
error = "Unkonwn Error";
}
updated.setText("Loading failed: "+error);
statusLoadError = null;
communityLoadError = null;
} else {
infoLastLoaded = System.currentTimeMillis();
}
setLoading(false);
}
}
private static String getError(String message, String type) {
if (message != null) {
if (!message.isEmpty()) {
return message;
}
}
return "";
}
/**
* Set the dialog loading state, enabling or disabling controls.
*
* @param loading
*/
private void setLoading(boolean loading) {
if (loading) {
updated.setText("Loading..");
lastPutResult = -1;
}
update.setEnabled(!loading);
selectGame.setEnabled(!loading);
removeGame.setEnabled(!loading);
selectCommunity.setEnabled(!loading);
removeCommunity.setEnabled(!loading);
reloadButton.setEnabled(!loading);
historyButton.setEnabled(!loading);
addToHistoryButton.setEnabled(!loading);
this.loading = loading;
}
public void update() {
if (!loading && infoLastLoaded > 0) {
long timePassed = System.currentTimeMillis() - infoLastLoaded;
updated.setText("Info last loaded: "
+ DateTime.duration(timePassed, 1, 0) + " ago"
+ (statusEdited ? " (edited)" : ""));
}
if (loading && lastPutResult > 0) {
long ago = System.currentTimeMillis() - lastPutResult;
if (ago > PUT_RESULT_DELAY) {
setLoading(false);
}
}
}
/**
* This should be done from an up-to-date source, like a direct response
* from the API. It might not be necessary, but just in case a Community
* gets renamed at some point, at least this way it's possible to change it
* for existing status/favorite entries.
*
* @param c
*/
protected void updateCommunityName(Community c) {
if (c == null) {
return;
}
main.getStatusHistory().updateCommunityName(c);
Map<String, String> communities = main.getCommunityFavorites();
if (communities.containsKey(c.getId())) {
communities.put(c.getId(), c.getName());
main.setCommunityFavorites(communities);
}
}
private void statusEdited() {
statusEdited = true;
}
public String getStatusHistorySorting() {
return statusHistoryDialog.getSortOrder();
}
public void setStatusHistorySorting(String order) {
statusHistoryDialog.setSortOrder(order);
}
/**
* Adds the current status to the preset history
*/
private void addCurrentToHistory() {
String currentTitle = status.getText().trim();
String currentGame = game.getText();
if (main.getSaveStatusHistorySetting()
|| main.getStatusHistory().isFavorite(currentTitle, currentGame, currentCommunity)) {
main.getStatusHistory().addUsed(currentTitle, currentGame, currentCommunity);
}
}
/**
* Adds the current status to the preset favorites
*/
private void addCurrentToFavorites() {
main.getStatusHistory().addFavorite(status.getText().trim(), game.getText(), currentCommunity);
}
}