biskuteri-cafe-JKomasto2/LoginWindow.java
Snowyfox e6fea4c061 Fixed bug when redraft makes no changes
(Before this, JKomasto and sometimes the Mastodon web client would get '411 Record Not Found' when submitting the same text after deleting and redrafting. Presumably the Mastodon server caches both whether an idempotency key was fulfilled and which post it leads to, and for some reason it looks up the second and fails.)
2022-05-31 03:39:56 -04:00

590 lines
14 KiB
Java

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JTextField;
import javax.swing.JPasswordField;
import javax.swing.JTextArea;
import javax.swing.JCheckBox;
import javax.swing.JOptionPane;
import javax.swing.BorderFactory;
import javax.swing.border.Border;
import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Cursor;
import java.awt.Desktop;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import cafe.biskuteri.hinoki.Tree;
import java.net.URI;
import java.io.IOException;
class
LoginWindow extends JFrame {
private JKomasto
primaire;
private MastodonApi
api;
// - -%- -
private LoginComponent
display;
private boolean
serverContacted = false,
haveAppCredentials = false,
haveAccessToken = false,
haveAccountDetails = false;
// ---%-@-%---
public void
updateStatusDisplay()
{
char prefix1 = serverContacted ? '✓' : '✕';
char prefix2 = haveAppCredentials ? '✓' : '✕';
char prefix3 = haveAccessToken ? '✓' : '✕';
char prefix4 = haveAccountDetails ? '✓' : '✕';
StringBuilder b = new StringBuilder();
b.append(prefix1 + " Have connection to server\n");
b.append(prefix2 + " Have app credentials\n");
b.append(prefix3 + " Have access token\n");
b.append(prefix4 + " Have account details\n");
display.setText(b.toString());
display.paintImmediately(display.getBounds(null));
}
public void
useCache()
{
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
try
{
api.loadCache();
haveAppCredentials = true;
haveAccessToken = true;
display.setInstanceUrl(api.getInstanceUrl());
updateStatusDisplay();
}
catch (IOException eIo)
{
JOptionPane.showMessageDialog(
this,
"We couldn't get login details from the cache.."
+ "\n" + eIo.getClass() + ": " + eIo.getMessage()
);
display.setAutoLoginToggled(false);
}
display.setCursor(null);
if (!haveAccessToken) return;
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
api.getAccountDetails(new RequestListener() {
public void
connectionFailed(IOException eIo)
{
JOptionPane.showMessageDialog(
LoginWindow.this,
"Tried to get account details, failed.."
+ "\n" + eIo.getMessage()
);
}
public void
requestFailed(int httpCode, Tree<String> json)
{
JOptionPane.showMessageDialog(
LoginWindow.this,
"Tried to get account details, failed.."
+ "\n" + json.get("error").value
+ "\n(HTTP error code: " + httpCode + ")"
);
}
public void
requestSucceeded(Tree<String> json)
{
api.setAccountDetails(json);
serverContacted = true;
haveAccountDetails = true;
updateStatusDisplay();
}
});
display.setCursor(null);
if (!haveAccountDetails) return;
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
try
{
api.saveToCache();
}
catch (IOException eIo)
{
JOptionPane.showMessageDialog(
this,
"We couldn't save login details into the cache.."
+ "\n" + eIo.getClass() + ": " + eIo.getMessage()
);
}
display.setCursor(null);
primaire.finishedLogin();
}
public void
useInstanceUrl()
{
if (display.isAutoLoginToggled()) { useCache(); return; }
String url = display.getInstanceUrl();
if (url.trim().isEmpty()) {
// Should we show an error dialog..?
display.setInstanceUrl("");
return;
}
if (!hasProtocol(url)) {
url = "https://" + url;
display.setInstanceUrl(url);
display.paintImmediately(display.getBounds(null));
}
serverContacted = false;
haveAppCredentials = false;
haveAccessToken = false;
haveAccountDetails = false;
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
api.testUrlConnection(url, new RequestListener() {
public void
connectionFailed(IOException eIo)
{
JOptionPane.showMessageDialog(
LoginWindow.this,
"Tried to connect to URL, failed.."
+ "\n" + eIo.getClass() + ": " + eIo.getMessage()
);
}
public void
requestFailed(int httpCode, Tree<String> response)
{
JOptionPane.showMessageDialog(
LoginWindow.this,
"Tried to connect to URL, failed.."
+ "\n" + response.get("body").value
);
}
public void
requestSucceeded(Tree<String> response)
{
serverContacted = true;
updateStatusDisplay();
}
});
display.setCursor(null);
if (!serverContacted) return;
api.setInstanceUrl(url);
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
api.getAppCredentials(new RequestListener() {
public void
connectionFailed(IOException eIo)
{
JOptionPane.showMessageDialog(
LoginWindow.this,
"Tried to get app credentials, failed.."
+ "\n" + eIo.getMessage()
);
}
public void
requestFailed(int httpCode, Tree<String> json)
{
JOptionPane.showMessageDialog(
LoginWindow.this,
"Tried to get app credentials, failed.."
+ "\n" + json.get("error").value
+ "\n(HTTP error code: " + httpCode + ")"
);
}
public void
requestSucceeded(Tree<String> json)
{
api.setAppCredentials(json);
haveAppCredentials = true;
updateStatusDisplay();
}
});
display.setCursor(null);
if (!haveAppCredentials) return;
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
URI uri = api.getAuthorisationURL();
final String MESSAGE1 =
"We will need you to login through a web browser,\n"
+ "and they will give you an authorisation code\n"
+ "that you will paste here. Sorry..!";
boolean supported =
Desktop.isDesktopSupported()
&& Desktop.getDesktop().isSupported(Desktop.Action.BROWSE);
if (supported)
{
JOptionPane.showMessageDialog(
LoginWindow.this,
MESSAGE1,
"Authorisation code renewal",
JOptionPane.INFORMATION_MESSAGE
);
try { Desktop.getDesktop().browse(uri); }
catch (IOException eIo) { supported = false; }
}
if (!supported)
{
final String MESSAGE2 =
"\nWe cannot use Desktop.browse(URI) on your\n"
+ "computer.. You'll have to open your web\n"
+ "browser yourself, and copy this URL in.";
JTextField field = new JTextField();
field.setText(uri.toString());
field.setPreferredSize(new Dimension(120, 32));
field.selectAll();
JOptionPane.showMessageDialog(
LoginWindow.this,
new Object[] { MESSAGE1, MESSAGE2, field },
"Authorisation code renewal",
JOptionPane.INFORMATION_MESSAGE
);
}
display.receiveAuthorisationCode();
display.setCursor(null);
}
public void
useAuthorisationCode()
{
String code = display.getAuthorisationCode();
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
api.getAccessToken(code, new RequestListener() {
public void
connectionFailed(IOException eIo)
{
JOptionPane.showMessageDialog(
LoginWindow.this,
"Tried to get app token, failed.."
+ "\n" + eIo.getMessage()
);
}
public void
requestFailed(int httpCode, Tree<String> json)
{
JOptionPane.showMessageDialog(
LoginWindow.this,
"Tried to get app token, failed.."
+ "\n" + json.get("error").value
+ "\n(HTTP error code: " + httpCode + ")"
);
}
public void
requestSucceeded(Tree<String> json)
{
api.setAccessToken(json);
haveAccessToken = true;
updateStatusDisplay();
}
});
display.setCursor(null);
if (!haveAccessToken) return;
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
api.getAccountDetails(new RequestListener() {
public void
connectionFailed(IOException eIo)
{
JOptionPane.showMessageDialog(
LoginWindow.this,
"Tried to get account details, failed.."
+ "\n" + eIo.getMessage()
);
}
public void
requestFailed(int httpCode, Tree<String> json)
{
JOptionPane.showMessageDialog(
LoginWindow.this,
"Tried to get account details, failed.."
+ "\n" + json.get("error").value
+ "\n(HTTP error code: " + httpCode + ")"
);
}
public void
requestSucceeded(Tree<String> json)
{
api.setAccountDetails(json);
haveAccountDetails = true;
updateStatusDisplay();
}
});
display.setCursor(null);
if (!haveAccountDetails) return;
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
try
{
api.saveToCache();
}
catch (IOException eIo)
{
JOptionPane.showMessageDialog(
this,
"We couldn't save login details into the cache.."
+ "\n" + eIo.getClass() + ": " + eIo.getMessage()
);
}
display.setCursor(null);
primaire.finishedLogin();
}
// - -%- -
public static boolean
hasProtocol(String url) { return url.matches("^.+://.*"); }
// ---%-@-%---
LoginWindow(JKomasto primaire)
{
super("JKomasto - Login");
this.primaire = primaire;
this.api = primaire.getMastodonApi();
setLocationByPlatform(true);
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
display = new LoginComponent(this);
updateStatusDisplay();
display.setPreferredSize(new Dimension(320, 280));
setContentPane(display);
pack();
setIconImage(primaire.getProgramIcon());
}
}
class
LoginComponent extends JPanel
implements ActionListener {
private LoginWindow
primaire;
// - -%- -
private JTextArea
statusDisplay;
private JTextField
instanceUrlField,
authorisationCodeField;
private JLabel
instanceUrlLabel,
authorisationCodeLabel;
private JButton
instanceUrlButton,
authorisationCodeButton;
private JPanel
labelArea,
forField,
accountsPanel;
private JCheckBox
autoLoginToggle;
// ---%-@-%---
public String
getInstanceUrl()
{
return instanceUrlField.getText();
}
public void
setInstanceUrl(String url)
{
instanceUrlField.setText(url);
}
public String
getAuthorisationCode()
{
return authorisationCodeField.getText();
}
public void
setText(String status)
{
statusDisplay.setText(status);
}
public void
setAutoLoginToggled(boolean a) { autoLoginToggle.setSelected(a); }
public boolean
isAutoLoginToggled() { return autoLoginToggle.isSelected(); }
public void
actionPerformed(ActionEvent eA)
{
if (eA.getSource() == instanceUrlButton)
primaire.useInstanceUrl();
if (eA.getSource() == authorisationCodeButton)
primaire.useAuthorisationCode();
}
public void
receiveInstanceUrl()
{
labelArea.remove(authorisationCodeLabel);
forField.remove(authorisationCodeButton);
accountsPanel.remove(authorisationCodeField);
labelArea.add(instanceUrlLabel, BorderLayout.NORTH);
forField.add(instanceUrlButton, BorderLayout.EAST);
accountsPanel.add(instanceUrlField);
revalidate();
}
public void
receiveAuthorisationCode()
{
labelArea.remove(instanceUrlLabel);
forField.remove(instanceUrlButton);
accountsPanel.remove(instanceUrlField);
labelArea.add(authorisationCodeLabel, BorderLayout.NORTH);
forField.add(authorisationCodeButton, BorderLayout.EAST);
accountsPanel.add(authorisationCodeField);
revalidate();
}
// ---%-@-%---
LoginComponent(LoginWindow primaire)
{
this.primaire = primaire;
Border b1, b2, bi, bo, be;
b1 = BorderFactory.createEtchedBorder();
b2 = BorderFactory.createEmptyBorder(8, 8, 8, 8);
bi = BorderFactory.createCompoundBorder(b1, b2);
bo = BorderFactory.createEmptyBorder(8, 8, 8, 8);
be = BorderFactory.createEmptyBorder();
Font f1, f2;
f1 = new Font("Dialog", Font.PLAIN, 12);
f2 = new Font("Dialog", Font.PLAIN, 16);
// We can't use swing.Box for layout,
// it's malfunctioning for some reason.
instanceUrlField = new JTextField();
instanceUrlField.setFont(f1);
String s1 = "Enter an instance URL!";
instanceUrlLabel = new JLabel(s1);
instanceUrlLabel.setLabelFor(instanceUrlField);
instanceUrlLabel.setFont(f1);
instanceUrlButton = new JButton("Connect");
instanceUrlButton.setFont(f1);
instanceUrlButton.setMnemonic(KeyEvent.VK_C);
instanceUrlButton.addActionListener(this);
authorisationCodeField = new JPasswordField();
authorisationCodeField.setFont(f1);
String s2 = "Paste the authorisation code!";
authorisationCodeLabel = new JLabel(s2);
authorisationCodeLabel.setLabelFor(authorisationCodeField);
authorisationCodeLabel.setFont(f1);
authorisationCodeButton = new JButton("Login");
authorisationCodeButton.setFont(f1);
authorisationCodeButton.setMnemonic(KeyEvent.VK_L);
authorisationCodeButton.addActionListener(this);
labelArea = new JPanel();
labelArea.setLayout(new BorderLayout());
forField = new JPanel();
forField.setOpaque(false);
forField.setLayout(new BorderLayout());
forField.add(labelArea, BorderLayout.WEST);
String s3 = "Automatically login if possible";
autoLoginToggle = new JCheckBox(s3);
autoLoginToggle.setMnemonic(KeyEvent.VK_A);
autoLoginToggle.setBorder(be);
autoLoginToggle.setFont(f1);
JPanel optionsPanel = new JPanel();
optionsPanel.setOpaque(false);
optionsPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
optionsPanel.setBorder(bi);
optionsPanel.add(autoLoginToggle);
accountsPanel = new JPanel();
accountsPanel.setOpaque(false);
accountsPanel.setLayout(new BorderLayout(0, 4));
accountsPanel.add(instanceUrlField);
accountsPanel.add(forField, BorderLayout.SOUTH);
statusDisplay = new JTextArea();
statusDisplay.setEditable(false);
statusDisplay.setBackground(null);
statusDisplay.setBorder(bi);
statusDisplay.setFont(f2);
receiveInstanceUrl();
setLayout(new BorderLayout(0, 8));
add(accountsPanel, BorderLayout.NORTH);
add(optionsPanel, BorderLayout.CENTER);
add(statusDisplay, BorderLayout.SOUTH);
setBorder(bo);
}
}