Added changes made since.
All windows now somewhat functional. Added rudimentary HTML parser to deal with content.
@ -2,19 +2,33 @@
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JTextArea;
|
||||
import javax.swing.JTextField;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JComboBox;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.Box;
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.JOptionPane;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.Cursor;
|
||||
|
||||
import cafe.biskuteri.hinoki.Tree;
|
||||
import java.io.IOException;
|
||||
|
||||
class
|
||||
ComposeWindow extends JFrame {
|
||||
|
||||
private JKomasto
|
||||
primaire;
|
||||
|
||||
private MastodonApi
|
||||
api;
|
||||
|
||||
// - -%- -
|
||||
|
||||
private Composition
|
||||
composition;
|
||||
|
||||
@ -26,26 +40,61 @@ ComposeWindow extends JFrame {
|
||||
public void
|
||||
setComposition(Composition composition)
|
||||
{
|
||||
if (composition == null)
|
||||
{
|
||||
composition = new Composition();
|
||||
composition.text = "";
|
||||
composition.visibility = PostVisibility.MENTIONED;
|
||||
composition.replyToPostId = null;
|
||||
}
|
||||
|
||||
assert composition != null;
|
||||
this.composition = composition;
|
||||
syncDisplayToComposition();
|
||||
}
|
||||
|
||||
public void
|
||||
newComposition()
|
||||
{
|
||||
composition = new Composition();
|
||||
composition.text = "";
|
||||
composition.visibility = PostVisibility.MENTIONED;
|
||||
composition.replyToPostId = null;
|
||||
syncDisplayToComposition();
|
||||
}
|
||||
|
||||
public void
|
||||
submit()
|
||||
{
|
||||
syncCompositionToDisplay();
|
||||
display.setSubmitting(true);
|
||||
// Perform fancy submission here..
|
||||
api.submit(
|
||||
composition.text, composition.visibility,
|
||||
composition.replyToPostId,
|
||||
new RequestListener() {
|
||||
|
||||
public void
|
||||
connectionFailed(IOException eIo)
|
||||
{
|
||||
JOptionPane.showMessageDialog(
|
||||
ComposeWindow.this,
|
||||
"Tried to submit post, failed..."
|
||||
+ "\n" + eIo.getMessage()
|
||||
);
|
||||
}
|
||||
|
||||
public void
|
||||
requestFailed(int httpCode, Tree<String> json)
|
||||
{
|
||||
JOptionPane.showMessageDialog(
|
||||
ComposeWindow.this,
|
||||
"Tried to submit post, failed..."
|
||||
+ "\n" + json.get("error").value
|
||||
+ "(HTTP error code: " + httpCode + ")"
|
||||
);
|
||||
}
|
||||
|
||||
public void
|
||||
requestSucceeded(Tree<String> json)
|
||||
{
|
||||
newComposition();
|
||||
}
|
||||
|
||||
}
|
||||
);
|
||||
display.setSubmitting(false);
|
||||
// (悪) Are we going to block the EDT..?
|
||||
}
|
||||
|
||||
// - -%- -
|
||||
@ -54,6 +103,7 @@ ComposeWindow extends JFrame {
|
||||
syncDisplayToComposition()
|
||||
{
|
||||
display.setText(composition.text);
|
||||
display.setReplyToPostId(composition.replyToPostId);
|
||||
display.setVisibility(stringFor(composition.visibility));
|
||||
}
|
||||
|
||||
@ -63,18 +113,23 @@ ComposeWindow extends JFrame {
|
||||
composition.text = display.getText();
|
||||
composition.visibility =
|
||||
visibilityFrom(display.getVisibility());
|
||||
composition.replyToPostId = display.getReplyToPostId();
|
||||
}
|
||||
|
||||
// ---%-@-%---
|
||||
|
||||
ComposeWindow()
|
||||
ComposeWindow(JKomasto primaire)
|
||||
{
|
||||
super("Submit a new post");
|
||||
this.primaire = primaire;
|
||||
this.api = primaire.getMastodonApi();
|
||||
|
||||
getContentPane().setPreferredSize(new Dimension(360, 270));
|
||||
pack();
|
||||
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
|
||||
|
||||
display = new ComposeComponent(this);
|
||||
this.setComposition(null);
|
||||
newComposition();
|
||||
|
||||
setContentPane(display);
|
||||
}
|
||||
@ -87,7 +142,7 @@ ComposeWindow extends JFrame {
|
||||
switch (visibility)
|
||||
{
|
||||
case PUBLIC: return "Public";
|
||||
case LOCAL: return "Local";
|
||||
case UNLISTED: return "Unlisted";
|
||||
case FOLLOWERS: return "Followers";
|
||||
case MENTIONED: return "Mentioned";
|
||||
}
|
||||
@ -100,8 +155,8 @@ ComposeWindow extends JFrame {
|
||||
{
|
||||
if (string.equals("Public"))
|
||||
return PostVisibility.PUBLIC;
|
||||
if (string.equals("Local"))
|
||||
return PostVisibility.LOCAL;
|
||||
if (string.equals("Unlisted"))
|
||||
return PostVisibility.UNLISTED;
|
||||
if (string.equals("Followers"))
|
||||
return PostVisibility.FOLLOWERS;
|
||||
if (string.equals("Mentioned"))
|
||||
@ -126,17 +181,15 @@ implements ActionListener {
|
||||
private JTextArea
|
||||
text;
|
||||
|
||||
private JTextField
|
||||
reply;
|
||||
|
||||
private JComboBox<String>
|
||||
visibility;
|
||||
|
||||
private JButton
|
||||
submit;
|
||||
|
||||
// - -%- -
|
||||
|
||||
private static final Cursor
|
||||
WAIT_CURSOR = Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR);
|
||||
|
||||
// ---%-@-%---
|
||||
|
||||
public void
|
||||
@ -145,12 +198,18 @@ implements ActionListener {
|
||||
this.text.setText(text);
|
||||
}
|
||||
|
||||
public void
|
||||
setReplyToPostId(String postId)
|
||||
{
|
||||
this.reply.setText(postId);
|
||||
}
|
||||
|
||||
public void
|
||||
setVisibility(String visibility)
|
||||
{
|
||||
if (visibility.equals("Public"))
|
||||
this.visibility.setSelectedIndex(0);
|
||||
else if (visibility.equals("Local"))
|
||||
else if (visibility.equals("Unlisted"))
|
||||
this.visibility.setSelectedIndex(1);
|
||||
else if (visibility.equals("Followers"))
|
||||
this.visibility.setSelectedIndex(2);
|
||||
@ -164,6 +223,12 @@ implements ActionListener {
|
||||
return text.getText();
|
||||
}
|
||||
|
||||
public String
|
||||
getReplyToPostId()
|
||||
{
|
||||
return reply.getText();
|
||||
}
|
||||
|
||||
public String
|
||||
getVisibility()
|
||||
{
|
||||
@ -178,7 +243,7 @@ implements ActionListener {
|
||||
text.setEnabled(false);
|
||||
visibility.setEnabled(false);
|
||||
submit.setEnabled(false);
|
||||
setCursor(WAIT_CURSOR);
|
||||
setCursor(new Cursor(Cursor.WAIT_CURSOR));
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -201,10 +266,14 @@ implements ActionListener {
|
||||
this.primaire = primaire;
|
||||
|
||||
text = new JTextArea();
|
||||
text.setLineWrap(true);
|
||||
text.setWrapStyleWord(true);
|
||||
|
||||
reply = new JTextField();
|
||||
|
||||
visibility = new JComboBox<>(new String[] {
|
||||
"Public",
|
||||
"Local",
|
||||
"Unlisted",
|
||||
"Followers",
|
||||
"Mentioned"
|
||||
// Where should we be saving strings..
|
||||
@ -220,7 +289,14 @@ implements ActionListener {
|
||||
bottom.add(Box.createHorizontalStrut(8));
|
||||
bottom.add(submit);
|
||||
|
||||
JPanel top = new JPanel();
|
||||
top.setOpaque(false);
|
||||
top.setLayout(new BorderLayout(8, 0));
|
||||
top.add(new JLabel("In reply to: "), BorderLayout.WEST);
|
||||
top.add(reply);
|
||||
|
||||
setLayout(new BorderLayout(0, 8));
|
||||
add(top, BorderLayout.NORTH);
|
||||
add(text, BorderLayout.CENTER);
|
||||
add(bottom, BorderLayout.SOUTH);
|
||||
|
||||
|
@ -4,10 +4,60 @@ import javax.swing.JPanel;
|
||||
import javax.swing.JComponent;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Cursor;
|
||||
import java.util.List;
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
|
||||
class
|
||||
JKomasto {
|
||||
|
||||
private TimelineWindow
|
||||
timelineWindow;
|
||||
|
||||
private ComposeWindow
|
||||
composeWindow;
|
||||
|
||||
private PostWindow
|
||||
autoViewWindow;
|
||||
|
||||
private LoginWindow
|
||||
loginWindow;
|
||||
|
||||
private MastodonApi
|
||||
api;
|
||||
|
||||
// ---%-@-%---
|
||||
|
||||
public MastodonApi
|
||||
getMastodonApi() { return api; }
|
||||
|
||||
public void
|
||||
finishedLogin()
|
||||
{
|
||||
autoViewWindow.setCursor(new Cursor(Cursor.WAIT_CURSOR));
|
||||
timelineWindow.setCursor(new Cursor(Cursor.WAIT_CURSOR));
|
||||
|
||||
timelineWindow.showLatestPage();
|
||||
timelineWindow.setLocationByPlatform(true);
|
||||
timelineWindow.setVisible(true);
|
||||
|
||||
autoViewWindow.setTitle("Auto view - JKomasto");
|
||||
//autoViewWindow.setVisible(true);
|
||||
|
||||
loginWindow.dispose();
|
||||
autoViewWindow.setCursor(null);
|
||||
timelineWindow.setCursor(null);
|
||||
}
|
||||
|
||||
public PostWindow
|
||||
getAutoViewWindow() { return autoViewWindow; }
|
||||
|
||||
public ComposeWindow
|
||||
getComposeWindow() { return composeWindow; }
|
||||
|
||||
// ---%-@-%---
|
||||
|
||||
public static void
|
||||
main(String... args) { new JKomasto(); }
|
||||
|
||||
@ -16,27 +66,18 @@ JKomasto {
|
||||
public
|
||||
JKomasto()
|
||||
{
|
||||
new TimelineWindow().setVisible(true);
|
||||
//new ComposeWindow().setVisible(true);
|
||||
//new PostWindow().setVisible(true);
|
||||
}
|
||||
api = new MastodonApi();
|
||||
|
||||
// - -%- -
|
||||
timelineWindow = new TimelineWindow(this);
|
||||
composeWindow = new ComposeWindow(this);
|
||||
autoViewWindow = new PostWindow(this);
|
||||
loginWindow = new LoginWindow(this);
|
||||
|
||||
private static void
|
||||
test(JComponent component, int width, int height)
|
||||
{
|
||||
JFrame frame = new JFrame();
|
||||
frame.setTitle(component.getClass().getName());
|
||||
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
|
||||
frame.setLocationByPlatform(true);
|
||||
|
||||
JPanel panel = new JPanel();
|
||||
panel.setPreferredSize(new Dimension(width, height));
|
||||
panel.add(component);
|
||||
frame.setContentPane(panel);
|
||||
|
||||
frame.setVisible(true);
|
||||
composeWindow.dispose();
|
||||
autoViewWindow.dispose();
|
||||
timelineWindow.dispose();
|
||||
loginWindow.setLocationByPlatform(true);
|
||||
loginWindow.setVisible(true);
|
||||
}
|
||||
|
||||
}
|
||||
@ -47,7 +88,7 @@ enum
|
||||
PostVisibility {
|
||||
|
||||
PUBLIC,
|
||||
LOCAL,
|
||||
UNLISTED,
|
||||
FOLLOWERS,
|
||||
MENTIONED
|
||||
|
||||
@ -59,8 +100,8 @@ TimelineType {
|
||||
FEDERATED,
|
||||
LOCAL,
|
||||
HOME,
|
||||
MENTIONS,
|
||||
MESSAGES,
|
||||
NOTIFICATIONS,
|
||||
CONVERSATIONS,
|
||||
LIST
|
||||
|
||||
}
|
||||
@ -68,11 +109,14 @@ TimelineType {
|
||||
|
||||
|
||||
class
|
||||
Timeline {
|
||||
TimelinePage {
|
||||
|
||||
public TimelineType
|
||||
type;
|
||||
|
||||
public List<Post>
|
||||
posts;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -81,12 +125,13 @@ class
|
||||
Post {
|
||||
|
||||
public String
|
||||
text;
|
||||
text,
|
||||
contentWarning;
|
||||
|
||||
public String
|
||||
authorId, authorName;
|
||||
|
||||
public String
|
||||
public ZonedDateTime
|
||||
date;
|
||||
|
||||
public PostVisibility
|
||||
|
581
LoginWindow.java
Normal file
@ -0,0 +1,581 @@
|
||||
|
||||
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);
|
||||
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()
|
||||
{
|
||||
String url = display.getInstanceUrl();
|
||||
if (!hasProtocol(url)) {
|
||||
url = "https://" + url;
|
||||
display.setInstanceUrl(url);
|
||||
display.paintImmediately(display.getBounds(null));
|
||||
}
|
||||
serverContacted = false;
|
||||
haveAppCredentials = false;
|
||||
haveAccessToken = false;
|
||||
haveAccountDetails = false;
|
||||
|
||||
if (display.isAutoLoginToggled()) { useCache(); return; }
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
455
MastodonApi.java
Executable file
@ -0,0 +1,455 @@
|
||||
|
||||
import cafe.biskuteri.hinoki.Tree;
|
||||
import cafe.biskuteri.hinoki.JsonConverter;
|
||||
import cafe.biskuteri.hinoki.DSVTokeniser;
|
||||
import java.net.URL;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URLEncoder;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
|
||||
class
|
||||
MastodonApi {
|
||||
|
||||
private String
|
||||
instanceUrl;
|
||||
|
||||
private Tree<String>
|
||||
appCredentials,
|
||||
accessToken,
|
||||
accountDetails;
|
||||
|
||||
// - -%- -
|
||||
|
||||
private static final String
|
||||
SCOPES = "read+write";
|
||||
|
||||
// ---%-@-%---
|
||||
|
||||
public String
|
||||
getInstanceUrl() { return instanceUrl; }
|
||||
|
||||
public Tree<String>
|
||||
getAppCredentials() { return appCredentials; }
|
||||
|
||||
public Tree<String>
|
||||
getAccessToken() { return accessToken; }
|
||||
|
||||
public Tree<String>
|
||||
getAccountDetails() { return accountDetails; }
|
||||
|
||||
|
||||
public void
|
||||
setInstanceUrl(String a) { instanceUrl = a; }
|
||||
|
||||
public void
|
||||
setAppCredentials(Tree<String> a) { appCredentials = a; }
|
||||
|
||||
public void
|
||||
setAccessToken(Tree<String> a) { accessToken = a; }
|
||||
|
||||
public void
|
||||
setAccountDetails(Tree<String> a) { accountDetails = a; }
|
||||
|
||||
|
||||
public void
|
||||
testUrlConnection(String url, RequestListener handler)
|
||||
{
|
||||
try
|
||||
{
|
||||
URL endpoint = new URL(url);
|
||||
HttpURLConnection conn;
|
||||
conn = (HttpURLConnection)endpoint.openConnection();
|
||||
conn.connect();
|
||||
|
||||
returnResponseInTree(conn, handler);
|
||||
}
|
||||
catch (IOException eIo) { handler.connectionFailed(eIo); }
|
||||
}
|
||||
|
||||
|
||||
public void
|
||||
getAppCredentials(RequestListener handler)
|
||||
{
|
||||
assert instanceUrl != null;
|
||||
try {
|
||||
URL endpoint = new URL(instanceUrl + "/api/v1/apps");
|
||||
HttpURLConnection conn;
|
||||
conn = (HttpURLConnection)endpoint.openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setDoOutput(true);
|
||||
conn.connect();
|
||||
|
||||
OutputStreamWriter output;
|
||||
output = new OutputStreamWriter(conn.getOutputStream());
|
||||
output.write("client_name=JKomasto alpha");
|
||||
output.write("&redirect_uris=urn:ietf:wg:oauth:2.0:oob");
|
||||
output.write("&scopes=" + SCOPES);
|
||||
output.close();
|
||||
|
||||
doStandardJsonReturn(conn, handler);
|
||||
}
|
||||
catch (IOException eIo) { handler.connectionFailed(eIo); }
|
||||
}
|
||||
|
||||
public URI
|
||||
getAuthorisationURL()
|
||||
{
|
||||
assert instanceUrl != null;
|
||||
assert appCredentials != null;
|
||||
String clientId = appCredentials.get("client_id").value;
|
||||
|
||||
try
|
||||
{
|
||||
StringBuilder b = new StringBuilder();
|
||||
b.append(instanceUrl);
|
||||
b.append("/oauth/authorize");
|
||||
// Be careful of the spelling!!
|
||||
b.append("?response_type=code");
|
||||
b.append("&redirect_uri=urn:ietf:wg:oauth:2.0:oob");
|
||||
b.append("&scope=" + SCOPES);
|
||||
b.append("&client_id=" + clientId);
|
||||
|
||||
return new URI(b.toString());
|
||||
}
|
||||
catch (URISyntaxException eUs) { assert false; return null; }
|
||||
}
|
||||
|
||||
public void
|
||||
getAccessToken(String authorisationCode, RequestListener handler)
|
||||
{
|
||||
assert instanceUrl != null;
|
||||
assert appCredentials != null;
|
||||
String id = appCredentials.get("client_id").value;
|
||||
String secret = appCredentials.get("client_secret").value;
|
||||
|
||||
try
|
||||
{
|
||||
URL endpoint = new URL(instanceUrl + "/oauth/token");
|
||||
HttpURLConnection conn;
|
||||
conn = (HttpURLConnection)endpoint.openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setDoOutput(true);
|
||||
conn.connect();
|
||||
|
||||
OutputStreamWriter output;
|
||||
output = new OutputStreamWriter(conn.getOutputStream());
|
||||
output.write("client_id=" + id);
|
||||
output.write("&client_secret=" + secret);
|
||||
output.write("&redirect_uri=urn:ietf:wg:oauth:2.0:oob");
|
||||
output.write("&grant_type=authorization_code");
|
||||
output.write("&scope=" + SCOPES);
|
||||
output.write("&code=" + authorisationCode);
|
||||
output.close();
|
||||
|
||||
doStandardJsonReturn(conn, handler);
|
||||
}
|
||||
catch (IOException eIo) { handler.connectionFailed(eIo); }
|
||||
}
|
||||
|
||||
public void
|
||||
getAccountDetails(RequestListener handler)
|
||||
{
|
||||
assert accessToken != null;
|
||||
String token = accessToken.get("access_token").value;
|
||||
|
||||
try
|
||||
{
|
||||
String s = "/api/v1/accounts/verify_credentials";
|
||||
URL endpoint = new URL(instanceUrl + s);
|
||||
HttpURLConnection conn;
|
||||
conn = (HttpURLConnection)endpoint.openConnection();
|
||||
conn.setRequestProperty("Authorization", "Bearer " + token);
|
||||
conn.connect();
|
||||
|
||||
doStandardJsonReturn(conn, handler);
|
||||
}
|
||||
catch (IOException eIo) { handler.connectionFailed(eIo); }
|
||||
}
|
||||
|
||||
public void
|
||||
getTimelinePage(
|
||||
TimelineType type, int count, String maxId, String minId,
|
||||
RequestListener handler)
|
||||
{
|
||||
String token = accessToken.get("access_token").value;
|
||||
|
||||
String url = instanceUrl + "/api/v1";
|
||||
switch (type)
|
||||
{
|
||||
case FEDERATED:
|
||||
case LOCAL: url += "/timelines/public"; break;
|
||||
case HOME: url += "/timelines/home"; break;
|
||||
case NOTIFICATIONS:
|
||||
url += "/notifications";
|
||||
// Note that this endpoint returns Notifications,
|
||||
// not Statuses. But we uniformly return Tree<String>,
|
||||
// we expect the caller can handle it.
|
||||
break;
|
||||
case CONVERSATIONS: url += "/timelines/public"; break;
|
||||
default: assert false;
|
||||
}
|
||||
url += "?limit=" + count;
|
||||
if (maxId != null) url += "&max_id=" + maxId;
|
||||
if (minId != null) url += "&min_id=" + minId;
|
||||
// This is a GET endpoint, it rejects receiving
|
||||
// query params through the body.
|
||||
|
||||
try
|
||||
{
|
||||
URL endpoint = new URL(url);
|
||||
HttpURLConnection conn;
|
||||
conn = (HttpURLConnection)endpoint.openConnection();
|
||||
String s2 = "Bearer " + token;
|
||||
conn.setRequestProperty("Authorization", s2);
|
||||
conn.connect();
|
||||
|
||||
doStandardJsonReturn(conn, handler);
|
||||
}
|
||||
catch (IOException eIo) { handler.connectionFailed(eIo); }
|
||||
}
|
||||
|
||||
public void
|
||||
setPostFavourited(
|
||||
String postId, boolean favourited,
|
||||
RequestListener handler)
|
||||
{
|
||||
String token = accessToken.get("access_token").value;
|
||||
|
||||
String s1 = "/api/v1/statuses/" + postId;
|
||||
String s2 = favourited ? "/favourite" : "/unfavourite";
|
||||
String url = instanceUrl + s1 + s2;
|
||||
try
|
||||
{
|
||||
URL endpoint = new URL(url);
|
||||
HttpURLConnection conn;
|
||||
conn = (HttpURLConnection)endpoint.openConnection();
|
||||
String s3 = "Bearer " + token;
|
||||
conn.setRequestProperty("Authorization", s3);
|
||||
conn.setRequestMethod("POST");
|
||||
conn.connect();
|
||||
|
||||
doStandardJsonReturn(conn, handler);
|
||||
}
|
||||
catch (IOException eIo) { handler.connectionFailed(eIo); }
|
||||
}
|
||||
|
||||
public void
|
||||
setPostBoosted(
|
||||
String postId, boolean boosted,
|
||||
RequestListener handler)
|
||||
{
|
||||
String token = accessToken.get("access_token").value;
|
||||
|
||||
String s1 = "/api/v1/statuses/" + postId;
|
||||
String s2 = boosted ? "/reblog" : "/unreblog";
|
||||
String url = instanceUrl + s1 + s2;
|
||||
try
|
||||
{
|
||||
URL endpoint = new URL(url);
|
||||
HttpURLConnection conn;
|
||||
conn = (HttpURLConnection)endpoint.openConnection();
|
||||
String s3 = "Bearer " + token;
|
||||
conn.setRequestProperty("Authorization", s3);
|
||||
conn.setRequestMethod("POST");
|
||||
conn.connect();
|
||||
|
||||
doStandardJsonReturn(conn, handler);
|
||||
}
|
||||
catch (IOException eIo) { handler.connectionFailed(eIo); }
|
||||
}
|
||||
|
||||
public void
|
||||
submit(
|
||||
String text, PostVisibility visibility, String replyTo,
|
||||
RequestListener handler)
|
||||
{
|
||||
String token = accessToken.get("access_token").value;
|
||||
|
||||
String visibilityParam = "direct";
|
||||
switch (visibility) {
|
||||
case PUBLIC: visibilityParam = "public"; break;
|
||||
case UNLISTED: visibilityParam = "unlisted"; break;
|
||||
case FOLLOWERS: visibilityParam = "private"; break;
|
||||
case MENTIONED: visibilityParam = "direct"; break;
|
||||
default: assert false;
|
||||
}
|
||||
|
||||
String url = instanceUrl + "/api/v1/statuses";
|
||||
try
|
||||
{
|
||||
text = URLEncoder.encode(text, "UTF-8");
|
||||
|
||||
URL endpoint = new URL(url);
|
||||
HttpURLConnection conn;
|
||||
conn = (HttpURLConnection)endpoint.openConnection();
|
||||
String s1 = "Bearer " + token;
|
||||
conn.setRequestProperty("Authorization", s1);
|
||||
String s2 = Integer.toString(text.hashCode());
|
||||
conn.setRequestProperty("Idempotency-Key", s2);
|
||||
conn.setDoOutput(true);
|
||||
conn.setRequestMethod("POST");
|
||||
conn.connect();
|
||||
|
||||
OutputStreamWriter output;
|
||||
output = new OutputStreamWriter(conn.getOutputStream());
|
||||
output.write("status=" + text);
|
||||
output.write("&visibility=" + visibilityParam);
|
||||
if (replyTo != null) {
|
||||
output.write("&in_reply_to_id=" + replyTo);
|
||||
}
|
||||
output.close();
|
||||
|
||||
doStandardJsonReturn(conn, handler);
|
||||
}
|
||||
catch (IOException eIo) { handler.connectionFailed(eIo); }
|
||||
}
|
||||
|
||||
// - -%- -
|
||||
|
||||
private void
|
||||
doStandardJsonReturn(HttpURLConnection conn, RequestListener handler)
|
||||
throws IOException
|
||||
{
|
||||
InputStreamReader input;
|
||||
int code = conn.getResponseCode();
|
||||
if (code >= 300)
|
||||
{
|
||||
input = new InputStreamReader(conn.getErrorStream());
|
||||
Tree<String> response = JsonConverter.convert(input);
|
||||
input.close();
|
||||
handler.requestFailed(code, response);
|
||||
return;
|
||||
}
|
||||
|
||||
input = new InputStreamReader(conn.getInputStream());
|
||||
Tree<String> response = JsonConverter.convert(input);
|
||||
input.close();
|
||||
handler.requestSucceeded(response);
|
||||
}
|
||||
|
||||
private void
|
||||
returnResponseInTree(HttpURLConnection conn, RequestListener handler)
|
||||
throws IOException
|
||||
{
|
||||
InputStreamReader input;
|
||||
int code = conn.getResponseCode();
|
||||
if (code >= 300)
|
||||
{
|
||||
input = new InputStreamReader(conn.getErrorStream());
|
||||
Tree<String> response = fromPlain(input);
|
||||
input.close();
|
||||
handler.requestFailed(code, response);
|
||||
return;
|
||||
}
|
||||
|
||||
input = new InputStreamReader(conn.getInputStream());
|
||||
Tree<String> response = fromPlain(input);
|
||||
input.close();
|
||||
handler.requestSucceeded(response);
|
||||
}
|
||||
|
||||
// - -%- -
|
||||
|
||||
private static Tree<String>
|
||||
fromPlain(InputStreamReader r)
|
||||
throws IOException
|
||||
{
|
||||
StringBuilder b = new StringBuilder();
|
||||
int c; while ((c = r.read()) != -1) b.append((char)c);
|
||||
|
||||
Tree<String> leaf = new Tree<String>();
|
||||
leaf.key = "body";
|
||||
leaf.value = b.toString();
|
||||
Tree<String> doc = new Tree<String>();
|
||||
doc.add(leaf);
|
||||
return doc;
|
||||
}
|
||||
|
||||
// ---%-@-%---
|
||||
|
||||
public void
|
||||
loadCache()
|
||||
throws IOException
|
||||
{
|
||||
FileReader r = new FileReader(getCachePath());
|
||||
DSVTokeniser.Options o = new DSVTokeniser.Options();
|
||||
Tree<String> row1 = DSVTokeniser.tokenise(r, o);
|
||||
Tree<String> row2 = DSVTokeniser.tokenise(r, o);
|
||||
Tree<String> row3 = DSVTokeniser.tokenise(r, o);
|
||||
assert !row1.get(0).value.equals(o.endOfStreamValue);
|
||||
assert !row2.get(0).value.equals(o.endOfStreamValue);
|
||||
assert !row3.get(0).value.equals(o.endOfStreamValue);
|
||||
r.close();
|
||||
|
||||
// Prepare to bark like mad.
|
||||
boolean yes10 = !row1.get(0).value.equals(o.endOfStreamValue);
|
||||
boolean yes20 = !row2.get(0).value.equals(o.endOfStreamValue);
|
||||
boolean yes30 = !row3.get(0).value.equals(o.endOfStreamValue);
|
||||
boolean yes11 = row1.size() == 1;
|
||||
boolean yes21 = row2.size() == 2;
|
||||
boolean yes31 = row3.size() == 1;
|
||||
boolean all = yes10 & yes20 & yes30 & yes11 & yes21 & yes31;
|
||||
if (!all) {
|
||||
throw new IOException("Cache has invalid format!");
|
||||
}
|
||||
|
||||
setInstanceUrl(row1.get(0).value);
|
||||
appCredentials = new Tree<String>();
|
||||
appCredentials.add(new Tree<String>());
|
||||
appCredentials.add(new Tree<String>());
|
||||
appCredentials.get(0).key = "client_id";
|
||||
appCredentials.get(0).value = row2.get(0).value;
|
||||
appCredentials.get(1).key = "client_secret";
|
||||
appCredentials.get(1).value = row2.get(1).value;
|
||||
accessToken = new Tree<String>();
|
||||
accessToken.add(new Tree<String>());
|
||||
accessToken.get(0).key = "access_token";
|
||||
accessToken.get(0).value = row3.get(0).value;
|
||||
}
|
||||
|
||||
public void
|
||||
saveToCache()
|
||||
throws IOException
|
||||
{
|
||||
String f10 = instanceUrl;
|
||||
String f20 = appCredentials.get("client_id").value;
|
||||
String f21 = appCredentials.get("client_secret").value;
|
||||
String f30 = accessToken.get("access_token").value;
|
||||
|
||||
f10 = f10.replaceAll(":", "\\\\:") + "\n";
|
||||
f20 = f20.replaceAll(":", "\\\\:") + ":";
|
||||
f21 = f21.replaceAll(":", "\\\\:") + "\n";
|
||||
f30 = f30.replaceAll(":", "\\\\:") + "\n";
|
||||
|
||||
FileWriter w = new FileWriter(getCachePath());
|
||||
w.write(f10);
|
||||
w.write(f20);
|
||||
w.write(f21);
|
||||
w.write(f30);
|
||||
w.close();
|
||||
}
|
||||
|
||||
// - -%- -
|
||||
|
||||
private static String
|
||||
getCachePath()
|
||||
{
|
||||
String userHome = System.getProperty("user.home");
|
||||
String osName = System.getProperty("os.name");
|
||||
boolean isWindows = osName.contains("Windows");
|
||||
boolean isUnix = !isWindows;
|
||||
// We assume. If you're running JKomasto in classic Mac OS
|
||||
// for some reason, you should probably edit the code..
|
||||
String configDir = isWindows ? "AppData/Local" : ".config";
|
||||
String filename = "jkomasto.cache.dsv";
|
||||
String path = userHome + "/" + configDir + "/" + filename;
|
||||
return path;
|
||||
}
|
||||
|
||||
}
|
402
PostWindow.java
@ -7,21 +7,38 @@ import javax.swing.JMenuItem;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.Box;
|
||||
import javax.swing.BoxLayout;
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.border.Border;
|
||||
import javax.swing.JOptionPane;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Font;
|
||||
import java.awt.FontMetrics;
|
||||
import java.awt.Shape;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.GridLayout;
|
||||
import java.awt.Cursor;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.io.IOException;
|
||||
import cafe.biskuteri.hinoki.Tree;
|
||||
|
||||
|
||||
class
|
||||
PostWindow extends JFrame
|
||||
implements ActionListener {
|
||||
|
||||
private JKomasto
|
||||
primaire;
|
||||
|
||||
private MastodonApi
|
||||
api;
|
||||
|
||||
private Post
|
||||
post;
|
||||
|
||||
@ -33,24 +50,18 @@ implements ActionListener {
|
||||
private RepliesComponent
|
||||
repliesDisplay;
|
||||
|
||||
// - -%- -
|
||||
|
||||
private static final DateTimeFormatter
|
||||
DATE_FORMAT = DateTimeFormatter.ofPattern("d LLLL ''uu"),
|
||||
TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm");
|
||||
|
||||
// ---%-@-%---
|
||||
|
||||
public void
|
||||
setPost(Post post)
|
||||
showPost(Post post)
|
||||
{
|
||||
if (post == null)
|
||||
{
|
||||
post = new Post();
|
||||
post.text = "This is a sample post.";
|
||||
post.authorId = "snowyfox@biskuteri.cafe";
|
||||
post.authorName = "snowyfox";
|
||||
post.date = "0712hrs, 17 July";
|
||||
post.visibility = PostVisibility.MENTIONED;
|
||||
post.postId = "000000000";
|
||||
post.boosted = false;
|
||||
post.favourited = true;
|
||||
}
|
||||
|
||||
assert post != null;
|
||||
this.post = post;
|
||||
|
||||
List<RepliesComponent.Reply> replies = null;
|
||||
@ -77,10 +88,15 @@ implements ActionListener {
|
||||
replies.add(reply3);
|
||||
}
|
||||
|
||||
postDisplay.setAuthor(post.authorName);
|
||||
postDisplay.setTime(post.date);
|
||||
postDisplay.setAuthorName(post.authorName);
|
||||
postDisplay.setAuthorId(post.authorId);
|
||||
postDisplay.setDate(DATE_FORMAT.format(post.date));
|
||||
postDisplay.setTime(TIME_FORMAT.format(post.date));
|
||||
postDisplay.setText(post.text);
|
||||
postDisplay.setFavourited(post.favourited);
|
||||
postDisplay.setBoosted(post.boosted);
|
||||
repliesDisplay.setReplies(replies);
|
||||
repaint();
|
||||
}
|
||||
|
||||
public void
|
||||
@ -90,21 +106,98 @@ implements ActionListener {
|
||||
}
|
||||
|
||||
public void
|
||||
favourite()
|
||||
favourite(boolean favourited)
|
||||
{
|
||||
postDisplay.setCursor(new Cursor(Cursor.WAIT_CURSOR));
|
||||
postDisplay.setFavouriteBoostEnabled(false);
|
||||
postDisplay.paintImmediately(postDisplay.getBounds());
|
||||
RequestListener handler = new RequestListener() {
|
||||
|
||||
public void
|
||||
connectionFailed(IOException eIo)
|
||||
{
|
||||
JOptionPane.showMessageDialog(
|
||||
PostWindow.this,
|
||||
"Tried to favourite post, failed.."
|
||||
+ "\n" + eIo.getClass() + ": " + eIo.getMessage()
|
||||
);
|
||||
}
|
||||
|
||||
public void
|
||||
requestFailed(int httpCode, Tree<String> json)
|
||||
{
|
||||
JOptionPane.showMessageDialog(
|
||||
PostWindow.this,
|
||||
"Tried to favourite post, failed.."
|
||||
+ "\n" + json.get("error").value
|
||||
+ "\n(HTTP error code: " + httpCode + ")"
|
||||
);
|
||||
}
|
||||
|
||||
public void
|
||||
requestSucceeded(Tree<String> json)
|
||||
{
|
||||
post.favourited = favourited;
|
||||
}
|
||||
|
||||
};
|
||||
api.setPostFavourited(post.postId, favourited, handler);
|
||||
postDisplay.setFavouriteBoostEnabled(true);
|
||||
postDisplay.setCursor(null);
|
||||
}
|
||||
|
||||
public void
|
||||
boost()
|
||||
boost(boolean boosted)
|
||||
{
|
||||
postDisplay.setCursor(new Cursor(Cursor.WAIT_CURSOR));
|
||||
postDisplay.setFavouriteBoostEnabled(false);
|
||||
postDisplay.paintImmediately(postDisplay.getBounds());
|
||||
RequestListener handler = new RequestListener() {
|
||||
|
||||
public void
|
||||
connectionFailed(IOException eIo)
|
||||
{
|
||||
JOptionPane.showMessageDialog(
|
||||
PostWindow.this,
|
||||
"Tried to boost post, failed.."
|
||||
+ "\n" + eIo.getClass() + ": " + eIo.getMessage()
|
||||
);
|
||||
}
|
||||
|
||||
public void
|
||||
requestFailed(int httpCode, Tree<String> json)
|
||||
{
|
||||
JOptionPane.showMessageDialog(
|
||||
PostWindow.this,
|
||||
"Tried to boost post, failed.."
|
||||
+ "\n" + json.get("error").value
|
||||
+ "\n(HTTP error code: " + httpCode + ")"
|
||||
);
|
||||
}
|
||||
|
||||
public void
|
||||
requestSucceeded(Tree<String> json)
|
||||
{
|
||||
post.boosted = boosted;
|
||||
}
|
||||
|
||||
};
|
||||
api.setPostBoosted(post.postId, boosted, handler);
|
||||
postDisplay.setFavouriteBoostEnabled(true);
|
||||
postDisplay.setCursor(null);
|
||||
}
|
||||
|
||||
public void
|
||||
reply()
|
||||
{
|
||||
|
||||
ComposeWindow w = primaire.getComposeWindow();
|
||||
w.setLocation(getX(), getY() + 100);
|
||||
w.setVisible(true);
|
||||
Composition c = new Composition();
|
||||
c.text = "@" + post.authorId + " ";
|
||||
c.visibility = PostVisibility.PUBLIC;
|
||||
c.replyToPostId = post.postId;
|
||||
w.setComposition(c);
|
||||
}
|
||||
|
||||
public void
|
||||
@ -142,8 +235,11 @@ implements ActionListener {
|
||||
|
||||
// ---%-@-%---
|
||||
|
||||
PostWindow()
|
||||
PostWindow(JKomasto primaire)
|
||||
{
|
||||
this.primaire = primaire;
|
||||
this.api = primaire.getMastodonApi();
|
||||
|
||||
getContentPane().setPreferredSize(new Dimension(360, 270));
|
||||
pack();
|
||||
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
|
||||
@ -153,30 +249,20 @@ implements ActionListener {
|
||||
|
||||
repliesDisplay = new RepliesComponent();
|
||||
|
||||
setPost(null);
|
||||
|
||||
JMenu postMenu = new JMenu("Post");
|
||||
addToMenu(postMenu, "Favourite");
|
||||
addToMenu(postMenu, "Reply");
|
||||
JMenu displayMenu = new JMenu("Display");
|
||||
addToMenu(displayMenu, "Post");
|
||||
addToMenu(displayMenu, "Replies");
|
||||
JMenuBar menuBar = new JMenuBar();
|
||||
menuBar.add(postMenu);
|
||||
menuBar.add(displayMenu);
|
||||
//setJMenuBar(menuBar);
|
||||
Post samplePost = new Post();
|
||||
samplePost.text = "This is a sample post.";
|
||||
samplePost.authorId = "snowyfox@biskuteri.cafe";
|
||||
samplePost.authorName = "snowyfox";
|
||||
samplePost.date = ZonedDateTime.now();
|
||||
samplePost.visibility = PostVisibility.MENTIONED;
|
||||
samplePost.postId = "000000000";
|
||||
samplePost.boosted = false;
|
||||
samplePost.favourited = true;
|
||||
showPost(samplePost);
|
||||
|
||||
setContentPane(postDisplay);
|
||||
}
|
||||
|
||||
private void
|
||||
addToMenu(JMenu menu, String text)
|
||||
{
|
||||
JMenuItem item = new JMenuItem(text);
|
||||
item.addActionListener(this);
|
||||
menu.add(item);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -189,24 +275,40 @@ implements ActionListener {
|
||||
primaire;
|
||||
|
||||
private String
|
||||
author, time, text;
|
||||
authorName, authorId, date, time, text;
|
||||
|
||||
// - -%- -
|
||||
|
||||
private JButton
|
||||
profile,
|
||||
private TwoToggleButton
|
||||
favouriteBoost,
|
||||
replyMisc,
|
||||
nextPrev,
|
||||
nextPrev;
|
||||
|
||||
private JButton
|
||||
profile,
|
||||
media;
|
||||
|
||||
// ---%-@-%---
|
||||
|
||||
public void
|
||||
setAuthor(String author)
|
||||
setAuthorName(String authorName)
|
||||
{
|
||||
assert author != null;
|
||||
this.author = author;
|
||||
assert authorName != null;
|
||||
this.authorName = authorName;
|
||||
}
|
||||
|
||||
public void
|
||||
setAuthorId(String authorId)
|
||||
{
|
||||
assert authorId != null;
|
||||
this.authorId = authorId;
|
||||
}
|
||||
|
||||
public void
|
||||
setDate(String date)
|
||||
{
|
||||
assert date != null;
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
public void
|
||||
@ -223,124 +325,206 @@ implements ActionListener {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public void
|
||||
setFavourited(boolean a)
|
||||
{
|
||||
favouriteBoost.removeActionListener(this);
|
||||
favouriteBoost.setPrimaryToggled(a);
|
||||
favouriteBoost.addActionListener(this);
|
||||
}
|
||||
|
||||
public void
|
||||
setBoosted(boolean a)
|
||||
{
|
||||
favouriteBoost.removeActionListener(this);
|
||||
favouriteBoost.setSecondaryToggled(a);
|
||||
favouriteBoost.addActionListener(this);
|
||||
}
|
||||
|
||||
public void
|
||||
setFavouriteBoostEnabled(boolean a)
|
||||
{
|
||||
favouriteBoost.setEnabled(a);
|
||||
}
|
||||
|
||||
// - -%- -
|
||||
|
||||
public void
|
||||
actionPerformed(ActionEvent eA)
|
||||
{
|
||||
Object src = eA.getSource();
|
||||
|
||||
/*
|
||||
* Umm, ActionEvent is the wrong thing to listen for here.
|
||||
* We want mouse presses. The custom button would publish
|
||||
* events that mention which button it was.
|
||||
*
|
||||
* Default JButton ignores RMB.
|
||||
*/
|
||||
String command = eA.getActionCommand();
|
||||
|
||||
if (src == profile)
|
||||
{
|
||||
primaire.openAuthorProfile();
|
||||
return;
|
||||
}
|
||||
else if (src == favouriteBoost)
|
||||
{
|
||||
// Fancy!!
|
||||
}
|
||||
else if (src == replyMisc)
|
||||
{
|
||||
|
||||
}
|
||||
else if (src == nextPrev)
|
||||
if (src == favouriteBoost)
|
||||
{
|
||||
|
||||
if (command.equals("favouriteOn"))
|
||||
primaire.favourite(true);
|
||||
if (command.equals("favouriteOff"))
|
||||
primaire.favourite(false);
|
||||
if (command.equals("boostOn"))
|
||||
primaire.boost(true);
|
||||
if (command.equals("boostOff"))
|
||||
primaire.boost(false);
|
||||
return;
|
||||
}
|
||||
else if (src == media)
|
||||
|
||||
if (src == replyMisc)
|
||||
{
|
||||
if (command.startsWith("reply")) primaire.reply();
|
||||
return;
|
||||
}
|
||||
|
||||
if (src == nextPrev)
|
||||
{
|
||||
if (command.equals("next"))
|
||||
{
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (src == media)
|
||||
{
|
||||
primaire.openMedia();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
protected void
|
||||
paintComponent(Graphics g)
|
||||
{
|
||||
int lineHeight = 20;
|
||||
g.clearRect(0, 0, getWidth(), getHeight());
|
||||
|
||||
FontMetrics met = g.getFontMetrics();
|
||||
Font f1 = new Font("IPAGothic", Font.PLAIN, 16);
|
||||
Font f2 = new Font("IPAGothic", Font.PLAIN, 14);
|
||||
FontMetrics fm1 = g.getFontMetrics(f1);
|
||||
FontMetrics fm2 = g.getFontMetrics(f2);
|
||||
|
||||
int x1 = 56;
|
||||
int x2 = getWidth() - met.stringWidth(time);
|
||||
int y1 = lineHeight;
|
||||
int x1 = 60;
|
||||
int x4 = getWidth() - 10;
|
||||
int x2 = x4 - fm2.stringWidth(date);
|
||||
int x3 = x4 - fm1.stringWidth(time);
|
||||
int y1 = 10;
|
||||
int y2 = y1 + fm2.getHeight();
|
||||
int y3 = y2 + fm1.getHeight();
|
||||
int y4 = y3 + 8;
|
||||
|
||||
g.drawString(author, x1, y1);
|
||||
g.drawString(time, x2, y1);
|
||||
Shape defaultClip = g.getClip();
|
||||
g.setClip(x1, y1, Math.min(x2, x3) - 8 - x1, y4 - y1);
|
||||
// First time I've used this method..
|
||||
// Cause, clearRect is not working.
|
||||
g.setFont(f2);
|
||||
g.drawString(authorId, x1, y2);
|
||||
g.setFont(f1);
|
||||
g.drawString(authorName, x1, y3);
|
||||
g.setClip(defaultClip);
|
||||
|
||||
int y = y1;
|
||||
for (String line: split(text, 48)) {
|
||||
y += lineHeight;
|
||||
g.setFont(f2);
|
||||
g.drawString(date, x2, y2);
|
||||
g.setFont(f1);
|
||||
g.drawString(time, x3, y3);
|
||||
|
||||
int y = y4;
|
||||
for (String line: split(text, 40)) {
|
||||
y += fm1.getHeight();
|
||||
g.drawString(line, x1, y);
|
||||
}
|
||||
}
|
||||
|
||||
private List<String>
|
||||
// - -%- -
|
||||
|
||||
private static List<String>
|
||||
split(String string, int lineLength)
|
||||
{
|
||||
List<String> returnee = new ArrayList<>();
|
||||
|
||||
int start, max = string.length();
|
||||
for (start = 0; start < max; start += lineLength) {
|
||||
returnee.add(string.substring(
|
||||
start,
|
||||
Math.min(max, start + lineLength)
|
||||
));
|
||||
StringBuilder line = new StringBuilder();
|
||||
for (String word: string.split(" "))
|
||||
{
|
||||
if (word.length() >= lineLength) {
|
||||
word = word.substring(0, lineLength - 4) + "...";
|
||||
}
|
||||
if (word.equals("\n")) {
|
||||
returnee.add(empty(line));
|
||||
continue;
|
||||
}
|
||||
if (line.length() + word.length() > lineLength) {
|
||||
returnee.add(empty(line));
|
||||
}
|
||||
line.append(word);
|
||||
line.append(" ");
|
||||
}
|
||||
returnee.add(empty(line));
|
||||
|
||||
return returnee;
|
||||
}
|
||||
|
||||
private static String
|
||||
empty(StringBuilder b)
|
||||
{
|
||||
String s = b.toString();
|
||||
b.delete(0, b.length());
|
||||
return s;
|
||||
}
|
||||
|
||||
// ---%-@-%---
|
||||
|
||||
PostComponent(PostWindow primaire)
|
||||
{
|
||||
this.primaire = primaire;
|
||||
|
||||
author = time = text = "";
|
||||
authorName = authorId = time = text = "";
|
||||
|
||||
Dimension buttonSize = new Dimension(20, 40);
|
||||
|
||||
profile = new JButton("P");
|
||||
profile.setSize(40, 40);
|
||||
profile.setPreferredSize(buttonSize);
|
||||
profile.setMargin(null);
|
||||
profile.addActionListener(this);
|
||||
|
||||
favouriteBoost = new JButton("☆/B");
|
||||
favouriteBoost.setSize(40, 40);
|
||||
favouriteBoost = new TwoToggleButton("favourite", "boost");
|
||||
favouriteBoost.addActionListener(this);
|
||||
// We have to overload the buttons to accept RMBs.
|
||||
// Which perform the secondary functionality.
|
||||
|
||||
replyMisc = new JButton("R");
|
||||
replyMisc.setSize(40, 40);
|
||||
replyMisc = new TwoToggleButton("reply", "misc");
|
||||
replyMisc.addActionListener(this);
|
||||
|
||||
nextPrev = new JButton("↓/↑");
|
||||
nextPrev.setSize(40, 40);
|
||||
nextPrev = new TwoToggleButton("next", "prev");
|
||||
nextPrev.addActionListener(this);
|
||||
|
||||
media = new JButton("M");
|
||||
media.setSize(40, 40);
|
||||
media = new JButton("media");
|
||||
media.setPreferredSize(buttonSize);
|
||||
media.setMargin(null);
|
||||
media.addActionListener(this);
|
||||
|
||||
setLayout(null);
|
||||
profile.setLocation(8, 8); add(profile);
|
||||
favouriteBoost.setLocation(8, 8+48); add(favouriteBoost);
|
||||
replyMisc.setLocation(8, 8+48+48); add(replyMisc);
|
||||
nextPrev.setLocation(8, 8+48+48+48); add(nextPrev);
|
||||
media.setLocation(8, 8+48+48+48+48); add(media);
|
||||
Box ibuttons = Box.createVerticalBox();
|
||||
ibuttons.setOpaque(false);
|
||||
ibuttons.add(profile);
|
||||
ibuttons.add(Box.createVerticalStrut(8));
|
||||
ibuttons.add(favouriteBoost);
|
||||
ibuttons.add(Box.createVerticalStrut(8));
|
||||
ibuttons.add(replyMisc);
|
||||
ibuttons.add(Box.createVerticalStrut(8));
|
||||
ibuttons.add(nextPrev);
|
||||
ibuttons.add(Box.createVerticalStrut(8));
|
||||
ibuttons.add(media);
|
||||
ibuttons.setMaximumSize(ibuttons.getPreferredSize());
|
||||
Box buttons = Box.createVerticalBox();
|
||||
buttons.setOpaque(false);
|
||||
buttons.add(ibuttons);
|
||||
buttons.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
|
||||
|
||||
/*
|
||||
* Also, since it's a linear buttons now, we can just use
|
||||
* a Box or FlowLayout. Though we have to have a JPanel
|
||||
* for the left, and this text component on the right.
|
||||
* If we are custom rendering the text then we can stay
|
||||
* this way, with manual positioning.
|
||||
*/
|
||||
setLayout(new BorderLayout());
|
||||
add(buttons, BorderLayout.WEST);
|
||||
|
||||
setFont(getFont().deriveFont(14f));
|
||||
}
|
||||
@ -398,7 +582,7 @@ RepliesComponent extends JPanel {
|
||||
|
||||
ReplyPreviewComponent preview = previews[o];
|
||||
Reply reply = replies.get(o);
|
||||
preview.setAuthor(reply.author);
|
||||
preview.setAuthorName(reply.author);
|
||||
preview.setText(reply.text);
|
||||
preview.setVisible(true);
|
||||
}
|
||||
@ -492,7 +676,7 @@ ReplyPreviewComponent extends JButton {
|
||||
}
|
||||
|
||||
public void
|
||||
setAuthor(String author)
|
||||
setAuthorName(String author)
|
||||
{
|
||||
assert author != null;
|
||||
this.author = author;
|
||||
|
17
RequestListener.java
Executable file
@ -0,0 +1,17 @@
|
||||
|
||||
import cafe.biskuteri.hinoki.Tree;
|
||||
import java.io.IOException;
|
||||
|
||||
interface
|
||||
RequestListener {
|
||||
|
||||
void
|
||||
connectionFailed(IOException eIo);
|
||||
|
||||
void
|
||||
requestFailed(int httpCode, Tree<String> json);
|
||||
|
||||
void
|
||||
requestSucceeded(Tree<String> json);
|
||||
|
||||
}
|
169
RudimentaryHTMLParser.java
Executable file
@ -0,0 +1,169 @@
|
||||
|
||||
import cafe.biskuteri.hinoki.Tree;
|
||||
import java.io.StringReader;
|
||||
import java.io.Reader;
|
||||
import java.io.IOException;
|
||||
|
||||
class
|
||||
RudimentaryHTMLParser {
|
||||
|
||||
public static Tree<String>
|
||||
depthlessRead(String html)
|
||||
throws IOException
|
||||
{
|
||||
return pass2(pass1(html));
|
||||
}
|
||||
|
||||
// - -%- -
|
||||
|
||||
private static Tree<String>
|
||||
pass1(String html)
|
||||
throws IOException
|
||||
{
|
||||
Reader r = new StringReader(html);
|
||||
Tree<String> docu = new Tree<String>();
|
||||
StringBuilder text = new StringBuilder();
|
||||
StringBuilder htmlEscape = new StringBuilder();
|
||||
boolean quoted = false;
|
||||
int c; while ((c = r.read()) != -1)
|
||||
{
|
||||
if (c == '&' || htmlEscape.length() > 0)
|
||||
{
|
||||
htmlEscape.append((char)c);
|
||||
if (c == ';')
|
||||
{
|
||||
String s = empty(htmlEscape);
|
||||
if (quoted) text.append('\\');
|
||||
/*
|
||||
* If we're quoted (i.e. within unescaped
|
||||
* quotes), we're in a tag, add escaping
|
||||
* backslash for pass2 to work with.
|
||||
* Only necessary for the quotes, but,
|
||||
* might as well uniformly use.
|
||||
*/
|
||||
if (s.equals("<")) text.append('<');
|
||||
if (s.equals(">")) text.append('>');
|
||||
if (s.equals("&")) text.append('&');
|
||||
if (s.equals(""")) text.append('"');
|
||||
if (s.equals("'")) text.append('\'');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (c == '"')
|
||||
{
|
||||
text.append((char)c);
|
||||
quoted = !quoted;
|
||||
continue;
|
||||
}
|
||||
if (!quoted)
|
||||
{
|
||||
if (c == '<')
|
||||
{
|
||||
if (text.length() > 0)
|
||||
{
|
||||
Tree<String> node = new Tree<>();
|
||||
node.key = "text";
|
||||
node.value = empty(text);
|
||||
docu.add(node);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (c == '>')
|
||||
{
|
||||
Tree<String> node = new Tree<>();
|
||||
node.key = "tag";
|
||||
node.value = empty(text);
|
||||
docu.add(node);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
text.append((char)c);
|
||||
continue;
|
||||
}
|
||||
if (text.length() > 0)
|
||||
{
|
||||
Tree<String> node = new Tree<>();
|
||||
node.key = "text";
|
||||
node.value = empty(text);
|
||||
docu.add(node);
|
||||
}
|
||||
return docu;
|
||||
}
|
||||
|
||||
private static Tree<String>
|
||||
pass2(Tree<String> docu)
|
||||
throws IOException
|
||||
{
|
||||
for (Tree<String> node: docu.children)
|
||||
{
|
||||
if (node.key.equals("text")) continue;
|
||||
assert node.key.equals("tag");
|
||||
|
||||
Reader r = new StringReader(node.value);
|
||||
Tree<String> part = new Tree<String>();
|
||||
boolean escaped = false, quoted = false;
|
||||
StringBuilder field = new StringBuilder();
|
||||
int c; while ((c = r.read()) != -1)
|
||||
{
|
||||
if (escaped) {
|
||||
field.append((char)c);
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
if (c == '\\') {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
if (c == '"') {
|
||||
quoted = !quoted;
|
||||
continue;
|
||||
}
|
||||
if (quoted) {
|
||||
field.append((char)c);
|
||||
continue;
|
||||
}
|
||||
if (c == '=') {
|
||||
part.key = empty(field);
|
||||
continue;
|
||||
}
|
||||
if (c == ' ') {
|
||||
if (field.length() > 0) {
|
||||
boolean v = part.key != null;
|
||||
if (v) part.value = empty(field);
|
||||
else part.key = empty(field);
|
||||
node.add(part);
|
||||
part = new Tree<String>();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
field.append((char)c);
|
||||
}
|
||||
if (field.length() > 0) {
|
||||
boolean v = part.key != null;
|
||||
if (v) part.value = empty(field);
|
||||
else part.key = empty(field);
|
||||
node.add(part);
|
||||
}
|
||||
node.value = null;
|
||||
}
|
||||
return docu;
|
||||
}
|
||||
|
||||
private static String
|
||||
empty(StringBuilder b)
|
||||
{
|
||||
String s = b.toString();
|
||||
b.delete(0, b.length());
|
||||
return s;
|
||||
}
|
||||
|
||||
// ---%-@-%---
|
||||
|
||||
public static void
|
||||
main(String... args)
|
||||
{
|
||||
final String EX2 =
|
||||
"<p>2000s energy<br /><a href=\"http://www.pspad.com/en/screenshot.htm\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">http://www.</span><span class=\"\">pspad.com/en/screenshot.htm</span><span class=\"invisible\"></span></a></p>";
|
||||
}
|
||||
|
||||
}
|
@ -17,19 +17,36 @@ import java.awt.FlowLayout;
|
||||
import java.awt.Font;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Insets;
|
||||
import java.awt.Cursor;
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Deque;
|
||||
import java.util.LinkedList;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.MouseListener;
|
||||
import java.awt.event.MouseEvent;
|
||||
|
||||
import cafe.biskuteri.hinoki.Tree;
|
||||
import java.io.IOException;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.Period;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.time.format.DateTimeParseException;
|
||||
|
||||
class
|
||||
TimelineWindow extends JFrame
|
||||
implements ActionListener {
|
||||
|
||||
private Timeline
|
||||
timeline;
|
||||
private JKomasto
|
||||
primaire;
|
||||
|
||||
private MastodonApi
|
||||
api;
|
||||
|
||||
private TimelinePage
|
||||
page;
|
||||
|
||||
// - -%- -
|
||||
|
||||
@ -50,69 +67,178 @@ implements ActionListener {
|
||||
private JMenuItem
|
||||
flipToNewestPost;
|
||||
|
||||
private Deque<Deque<String>>
|
||||
postIDsSeen;
|
||||
// - -%- -
|
||||
|
||||
private static final int
|
||||
PREVIEW_COUNT = TimelineComponent.PREVIEW_COUNT;
|
||||
|
||||
// ---%-@-%---
|
||||
|
||||
public void
|
||||
setTimeline(Timeline timeline)
|
||||
setTimelineType(TimelineType type)
|
||||
{
|
||||
if (timeline == null)
|
||||
{
|
||||
timeline = new Timeline();
|
||||
timeline.type = TimelineType.LOCAL;
|
||||
}
|
||||
page.type = type;
|
||||
String s = type.toString();
|
||||
s = s.charAt(0) + s.substring(1).toLowerCase();
|
||||
setTitle(s + " - JKomasto");
|
||||
}
|
||||
|
||||
assert timeline != null;
|
||||
this.timeline = timeline;
|
||||
public void
|
||||
showLatestPage()
|
||||
{
|
||||
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
|
||||
api.getTimelinePage(
|
||||
page.type, PREVIEW_COUNT, null, null,
|
||||
new RequestListener() {
|
||||
|
||||
// Here, we should request for posts for the timeline.
|
||||
// And pass data to TimelineComponent.
|
||||
public void
|
||||
connectionFailed(IOException eIo)
|
||||
{
|
||||
String s = eIo.getClass().getName();
|
||||
setTitle(s + " - JKomasto");
|
||||
}
|
||||
|
||||
public void
|
||||
requestFailed(int httpCode, Tree<String> json)
|
||||
{
|
||||
setTitle(httpCode + " - JKomasto");
|
||||
// lol...
|
||||
}
|
||||
|
||||
public void
|
||||
requestSucceeded(Tree<String> json)
|
||||
{
|
||||
page.posts = toPosts(json);
|
||||
display.setPosts(page.posts);
|
||||
display.setNextPageAvailable(true);
|
||||
}
|
||||
|
||||
}
|
||||
);
|
||||
display.setCursor(null);
|
||||
}
|
||||
|
||||
public void
|
||||
nextPage()
|
||||
{
|
||||
/*
|
||||
* uhh, what if the ID we provide is invalid.
|
||||
* Or does Mastodon keep IDs as 'deleted'? That
|
||||
* would make things a lot simpler.
|
||||
*/
|
||||
assert page.posts != null;
|
||||
assert page.posts.size() != 0;
|
||||
Post last = page.posts.get(page.posts.size() - 1);
|
||||
|
||||
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
|
||||
api.getTimelinePage(
|
||||
page.type, PREVIEW_COUNT, last.postId, null,
|
||||
new RequestListener() {
|
||||
|
||||
public void
|
||||
connectionFailed(IOException eIo)
|
||||
{
|
||||
String s = eIo.getClass().getName();
|
||||
setTitle(s + " - JKomasto");
|
||||
}
|
||||
|
||||
public void
|
||||
requestFailed(int httpCode, Tree<String> json)
|
||||
{
|
||||
setTitle(httpCode + " - JKomasto");
|
||||
}
|
||||
|
||||
public void
|
||||
requestSucceeded(Tree<String> json)
|
||||
{
|
||||
page.posts = toPosts(json);
|
||||
display.setPosts(page.posts);
|
||||
display.setPreviousPageAvailable(true);
|
||||
}
|
||||
|
||||
}
|
||||
);
|
||||
display.setCursor(null);
|
||||
}
|
||||
|
||||
public void
|
||||
previousPage()
|
||||
{
|
||||
if (postIDsSeen.isEmpty())
|
||||
{
|
||||
// Just request the latest first page.
|
||||
return;
|
||||
}
|
||||
assert page.posts != null;
|
||||
assert page.posts.size() != 0;
|
||||
Post first = page.posts.get(0);
|
||||
|
||||
Deque<String> lastPagePostIDs = postIDsSeen.pop();
|
||||
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
|
||||
api.getTimelinePage(
|
||||
page.type, PREVIEW_COUNT, null, first.postId,
|
||||
new RequestListener() {
|
||||
|
||||
while (!lastPagePostIDs.isEmpty())
|
||||
{
|
||||
String idToTry = lastPagePostIDs.pop();
|
||||
public void
|
||||
connectionFailed(IOException eIo)
|
||||
{
|
||||
String s = eIo.getClass().getName();
|
||||
setTitle(s + " - JKomasto");
|
||||
}
|
||||
|
||||
// Fetch page for this ID.
|
||||
// If successful, render and return.
|
||||
}
|
||||
public void
|
||||
requestFailed(int httpCode, Tree<String> json)
|
||||
{
|
||||
setTitle(httpCode + " - JKomasto");
|
||||
}
|
||||
|
||||
// Didn't return from above. Our current page "replaced"
|
||||
// the previous page, so the previous page's predecessor
|
||||
// is our previous page. Try to flip to that.
|
||||
previousPage();
|
||||
public void
|
||||
requestSucceeded(Tree<String> json)
|
||||
{
|
||||
page.posts = toPosts(json);
|
||||
display.setPosts(page.posts);
|
||||
display.setNextPageAvailable(true);
|
||||
display.setPreviousPageAvailable(true);
|
||||
}
|
||||
|
||||
}
|
||||
);
|
||||
display.setCursor(null);
|
||||
|
||||
if (page.posts.size() < PREVIEW_COUNT) showLatestPage();
|
||||
}
|
||||
|
||||
// - -%- -
|
||||
|
||||
public void
|
||||
postSelected(Post post)
|
||||
{
|
||||
primaire.getAutoViewWindow().showPost(post);
|
||||
}
|
||||
|
||||
public void
|
||||
actionPerformed(ActionEvent eA)
|
||||
{
|
||||
Object src = eA.getSource();
|
||||
|
||||
if (src == openHome)
|
||||
{
|
||||
setTimelineType(TimelineType.HOME);
|
||||
showLatestPage();
|
||||
}
|
||||
if (src == openFederated)
|
||||
{
|
||||
setTimelineType(TimelineType.FEDERATED);
|
||||
showLatestPage();
|
||||
}
|
||||
if (src == openLocal)
|
||||
{
|
||||
setTimelineType(TimelineType.LOCAL);
|
||||
showLatestPage();
|
||||
}
|
||||
if (src == createPost)
|
||||
{
|
||||
primaire.getComposeWindow().setVisible(true);
|
||||
}
|
||||
if (src == openAutoPostView)
|
||||
{
|
||||
PostWindow w = primaire.getAutoViewWindow();
|
||||
w.setLocation(getX() + 10 + getWidth(), getY());
|
||||
w.setVisible(true);
|
||||
}
|
||||
if (src == flipToNewestPost)
|
||||
{
|
||||
showLatestPage();
|
||||
}
|
||||
if (src == quit)
|
||||
{
|
||||
/*
|
||||
@ -122,14 +248,99 @@ implements ActionListener {
|
||||
* background threads IIRC (and they can check
|
||||
* if the Swing thread is alive).
|
||||
*/
|
||||
dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// - -%- -
|
||||
|
||||
private static List<Post>
|
||||
toPosts(Tree<String> json)
|
||||
{
|
||||
List<Post> posts = new ArrayList<>();
|
||||
for (Tree<String> post: json.children)
|
||||
{
|
||||
Tree<String> account = post.get("account");
|
||||
Post addee = new Post();
|
||||
addee.postId = post.get("id").value;
|
||||
try {
|
||||
String s = post.get("created_at").value;
|
||||
addee.date = ZonedDateTime.parse(s);
|
||||
ZoneId z = ZoneId.systemDefault();
|
||||
addee.date = addee.date.withZoneSameInstant(z);
|
||||
}
|
||||
catch (DateTimeParseException eDt) {
|
||||
assert false;
|
||||
addee.date = ZonedDateTime.now();
|
||||
}
|
||||
try {
|
||||
StringBuilder b = new StringBuilder();
|
||||
Tree<String> nodes =
|
||||
RudimentaryHTMLParser
|
||||
.depthlessRead(post.get("content").value);
|
||||
for (Tree<String> node: nodes.children)
|
||||
{
|
||||
if (node.key.equals("tag"))
|
||||
{
|
||||
if (node.get(0).key.equals("br")) {
|
||||
b.append(" \n ");
|
||||
}
|
||||
if (node.get(0).key.equals("/p")) {
|
||||
b.append(" \n\n ");
|
||||
}
|
||||
}
|
||||
if (node.key.equals("text")) {
|
||||
b.append(node.value);
|
||||
}
|
||||
}
|
||||
addee.text = b.toString();
|
||||
}
|
||||
catch (IOException eIo) {
|
||||
eIo.printStackTrace();
|
||||
assert false;
|
||||
}
|
||||
String s = post.get("spoiler_text").value;
|
||||
if (!s.isEmpty()) addee.contentWarning = s;
|
||||
else addee.contentWarning = null;
|
||||
addee.authorId = account.get("acct").value;
|
||||
addee.authorName = account.get("username").value;
|
||||
String s2 = account.get("display_name").value;
|
||||
if (!s2.isEmpty()) addee.authorName = s2;
|
||||
String f = post.get("favourited").value;
|
||||
String b = post.get("reblogged").value;
|
||||
addee.favourited = f.equals("true");
|
||||
addee.boosted = b.equals("true");
|
||||
posts.add(addee);
|
||||
}
|
||||
return posts;
|
||||
}
|
||||
|
||||
private static String
|
||||
plainify(String html)
|
||||
{
|
||||
// Delete all tags.
|
||||
StringBuilder b = new StringBuilder();
|
||||
boolean in = false;
|
||||
for (char c: html.toCharArray()) switch (c) {
|
||||
case '<': in = true; break;
|
||||
case '>': in = false; break;
|
||||
default: if (!in) b.append(c);
|
||||
}
|
||||
String s = b.toString();
|
||||
s = s.replaceAll("<", "<");
|
||||
s = s.replaceAll(">", ">");
|
||||
s = s.replaceAll(" ", "");
|
||||
return s;
|
||||
}
|
||||
|
||||
// ---%-@-%---
|
||||
|
||||
TimelineWindow()
|
||||
TimelineWindow(JKomasto primaire)
|
||||
{
|
||||
getContentPane().setPreferredSize(new Dimension(300, 400));
|
||||
this.primaire = primaire;
|
||||
this.api = primaire.getMastodonApi();
|
||||
|
||||
getContentPane().setPreferredSize(new Dimension(320, 460));
|
||||
pack();
|
||||
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
|
||||
|
||||
@ -162,7 +373,13 @@ implements ActionListener {
|
||||
menuBar.add(timelineMenu);
|
||||
setJMenuBar(menuBar);
|
||||
|
||||
page = new TimelinePage();
|
||||
page.posts = new ArrayList<>();
|
||||
setTimelineType(TimelineType.FEDERATED);
|
||||
|
||||
display = new TimelineComponent(this);
|
||||
display.setNextPageAvailable(false);
|
||||
display.setPreviousPageAvailable(false);
|
||||
|
||||
setContentPane(display);
|
||||
}
|
||||
@ -173,7 +390,7 @@ implements ActionListener {
|
||||
|
||||
class
|
||||
TimelineComponent extends JPanel
|
||||
implements ActionListener {
|
||||
implements ActionListener, MouseListener {
|
||||
|
||||
private TimelineWindow
|
||||
primaire;
|
||||
@ -190,22 +407,49 @@ implements ActionListener {
|
||||
pageLabel;
|
||||
|
||||
private final List<PostPreviewComponent>
|
||||
postPreviews = new ArrayList<>();
|
||||
postPreviews;
|
||||
|
||||
private boolean
|
||||
hoverSelect;
|
||||
|
||||
// - -%- -
|
||||
|
||||
static final int
|
||||
PREVIEW_COUNT = 10;
|
||||
|
||||
// ---%-@-%---
|
||||
|
||||
public void
|
||||
setPosts(List<Post> posts)
|
||||
{
|
||||
if (posts == null)
|
||||
{
|
||||
posts = new ArrayList<>();
|
||||
// Insert sample timeline posts here
|
||||
}
|
||||
|
||||
this.posts.clear();
|
||||
this.posts.addAll(posts);
|
||||
syncPreviewsToPosts();
|
||||
|
||||
assert posts.size() <= postPreviews.size();
|
||||
for (int o = 0; o < posts.size(); ++o)
|
||||
{
|
||||
PostPreviewComponent c = postPreviews.get(o);
|
||||
Post p = posts.get(o);
|
||||
c.setTopLeft(p.authorName);
|
||||
{
|
||||
ZonedDateTime now = ZonedDateTime.now();
|
||||
long d = ChronoUnit.SECONDS.between(p.date, now);
|
||||
long s = Math.abs(d);
|
||||
if (s < 30) c.setTopRight("now");
|
||||
else if (s < 60) c.setTopRight(d + "s");
|
||||
else if (s < 3600) c.setTopRight((d / 60) + "m");
|
||||
else if (s < 86400) c.setTopRight((d / 3600) + "h");
|
||||
else c.setTopRight((d / 86400) + "d");
|
||||
}
|
||||
if (p.contentWarning != null)
|
||||
c.setBottom("(" + p.contentWarning + ")");
|
||||
else
|
||||
c.setBottom(p.text + " ");
|
||||
}
|
||||
for (int o = posts.size(); o < postPreviews.size(); ++o)
|
||||
{
|
||||
postPreviews.get(o).reset();
|
||||
}
|
||||
}
|
||||
|
||||
public void
|
||||
@ -227,20 +471,47 @@ implements ActionListener {
|
||||
prev.setEnabled(available);
|
||||
}
|
||||
|
||||
// - -%- -
|
||||
public void
|
||||
setHoverSelect(boolean a) { this.hoverSelect = a; }
|
||||
|
||||
private void
|
||||
syncPreviewsToPosts()
|
||||
|
||||
public void
|
||||
mouseEntered(MouseEvent eM)
|
||||
{
|
||||
for (PostPreviewComponent p: postPreviews)
|
||||
{
|
||||
p.setTopLeft("Top left");
|
||||
p.setTopRight("Top right");
|
||||
p.setBottom("Bottom");
|
||||
// (In reality we are supposed to map with posts)
|
||||
}
|
||||
if (!hoverSelect) return;
|
||||
mouseClicked(eM);
|
||||
}
|
||||
|
||||
// (知) First time I'm using one of these..!
|
||||
|
||||
public void
|
||||
mouseClicked(MouseEvent eM)
|
||||
{
|
||||
int offset = postPreviews.indexOf(eM.getSource());
|
||||
assert offset != -1;
|
||||
primaire.postSelected(posts.get(offset));
|
||||
postPreviews.get(offset).setSelected(true);
|
||||
repaint();
|
||||
}
|
||||
|
||||
public void
|
||||
mouseExited(MouseEvent eM)
|
||||
{
|
||||
if (!hoverSelect) return;
|
||||
int offset = postPreviews.indexOf(eM.getSource());
|
||||
assert offset != -1;
|
||||
postPreviews.get(offset).setSelected(false);
|
||||
repaint();
|
||||
}
|
||||
|
||||
public void
|
||||
mousePressed(MouseEvent eM) { }
|
||||
|
||||
public void
|
||||
mouseReleased(MouseEvent eM) { }
|
||||
|
||||
// - -%- -
|
||||
|
||||
public void
|
||||
actionPerformed(ActionEvent eA)
|
||||
{
|
||||
@ -262,10 +533,15 @@ implements ActionListener {
|
||||
{
|
||||
this.primaire = primaire;
|
||||
|
||||
postPreviews = new ArrayList<>(PREVIEW_COUNT);
|
||||
hoverSelect = true;
|
||||
|
||||
prev = new JButton("<");
|
||||
next = new JButton(">");
|
||||
prev.setEnabled(false);
|
||||
next.setEnabled(false);
|
||||
prev.addActionListener(this);
|
||||
next.addActionListener(this);
|
||||
|
||||
pageLabel = new JLabel("0");
|
||||
|
||||
@ -283,11 +559,13 @@ implements ActionListener {
|
||||
constraints.gridx = 0;
|
||||
constraints.weightx = 1;
|
||||
constraints.insets = new Insets(4, 0, 4, 0);
|
||||
for (int n = 8; n > 0; --n)
|
||||
for (int n = PREVIEW_COUNT; n > 0; --n)
|
||||
{
|
||||
PostPreviewComponent p = new PostPreviewComponent();
|
||||
centre.add(p, constraints);
|
||||
postPreviews.add(p);
|
||||
PostPreviewComponent c = new PostPreviewComponent();
|
||||
c.reset();
|
||||
c.addMouseListener(this);
|
||||
centre.add(c, constraints);
|
||||
postPreviews.add(c);
|
||||
}
|
||||
|
||||
setLayout(new BorderLayout());
|
||||
@ -295,8 +573,6 @@ implements ActionListener {
|
||||
add(bottom, BorderLayout.SOUTH);
|
||||
|
||||
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
|
||||
|
||||
setPosts(null);
|
||||
}
|
||||
|
||||
}
|
||||
@ -320,37 +596,64 @@ PostPreviewComponent extends JComponent {
|
||||
public void
|
||||
setBottom(String text) { bottom.setText(text); }
|
||||
|
||||
public void
|
||||
reset()
|
||||
{
|
||||
setTopLeft(" ");
|
||||
setTopRight(" ");
|
||||
setBottom(" ");
|
||||
}
|
||||
|
||||
public void
|
||||
setSelected(boolean selected)
|
||||
{
|
||||
if (!selected) setBackground(null);
|
||||
else setBackground(new Color(0, 0, 0, 25));
|
||||
}
|
||||
|
||||
// - -%- -
|
||||
|
||||
protected void
|
||||
paintComponent(Graphics g)
|
||||
{
|
||||
g.setColor(getBackground());
|
||||
g.fillRect(0, 0, getWidth(), getHeight());
|
||||
}
|
||||
|
||||
// ---%-@-%---
|
||||
|
||||
public
|
||||
PostPreviewComponent()
|
||||
{
|
||||
Font font;
|
||||
Font f = new JLabel().getFont();
|
||||
Font f1 = f.deriveFont(Font.PLAIN, 12f);
|
||||
Font f2 = f.deriveFont(Font.ITALIC, 12f);
|
||||
Font f3 = f.deriveFont(Font.PLAIN, 14f);
|
||||
|
||||
topLeft = new JLabel();
|
||||
font = topLeft.getFont();
|
||||
topLeft.setFont(font.deriveFont(Font.PLAIN, 12f));
|
||||
topLeft.setFont(f1);
|
||||
setOpaque(false);
|
||||
|
||||
topRight = new JLabel();
|
||||
topRight.setHorizontalAlignment(JLabel.RIGHT);
|
||||
font = topRight.getFont();
|
||||
topRight.setFont(font.deriveFont(Font.ITALIC, 12f));
|
||||
topRight.setFont(f2);
|
||||
setOpaque(false);
|
||||
|
||||
bottom = new JLabel();
|
||||
font = bottom.getFont();
|
||||
bottom.setFont(font.deriveFont(Font.PLAIN, 16f));
|
||||
bottom.setFont(f3);
|
||||
bottom.setOpaque(false);
|
||||
|
||||
Box top = Box.createHorizontalBox();
|
||||
top.setOpaque(false);
|
||||
top.add(topLeft);
|
||||
top.add(Box.createGlue());
|
||||
top.add(topRight);
|
||||
|
||||
setOpaque(false);
|
||||
setSelected(false);
|
||||
setLayout(new BorderLayout());
|
||||
add(top, BorderLayout.NORTH);
|
||||
add(bottom, BorderLayout.SOUTH);
|
||||
add(bottom);
|
||||
}
|
||||
|
||||
}
|
||||
|
265
TwoToggleButton.java
Normal file
@ -0,0 +1,265 @@
|
||||
|
||||
import javax.swing.AbstractButton;
|
||||
import javax.swing.DefaultButtonModel;
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.ImageIcon;
|
||||
import java.awt.Image;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.event.MouseListener;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.KeyListener;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.FocusListener;
|
||||
import java.awt.event.FocusEvent;
|
||||
import java.net.URL;
|
||||
|
||||
import javax.swing.JFrame;
|
||||
|
||||
class
|
||||
TwoToggleButton extends AbstractButton
|
||||
implements KeyListener, MouseListener, FocusListener {
|
||||
|
||||
private String
|
||||
primaryName,
|
||||
secondaryName;
|
||||
|
||||
// - -%- -
|
||||
|
||||
private boolean
|
||||
primaryToggled = false,
|
||||
secondaryToggled = false;
|
||||
|
||||
private Image
|
||||
primaryToggledIcon,
|
||||
secondaryToggledIcon,
|
||||
primaryUntoggledIcon,
|
||||
secondaryUntoggledIcon;
|
||||
|
||||
private int
|
||||
nextEventID = ActionEvent.ACTION_FIRST;
|
||||
|
||||
// - -%- -
|
||||
|
||||
private static Image
|
||||
button,
|
||||
selectedOverlay,
|
||||
disabledOverlay;
|
||||
|
||||
// ---%-@-%---
|
||||
|
||||
public void
|
||||
togglePrimary()
|
||||
{
|
||||
setPrimaryToggled(!primaryToggled);
|
||||
}
|
||||
|
||||
public void
|
||||
toggleSecondary()
|
||||
{
|
||||
setSecondaryToggled(!secondaryToggled);
|
||||
}
|
||||
|
||||
public void
|
||||
setPrimaryToggled(boolean toggled)
|
||||
{
|
||||
primaryToggled = toggled;
|
||||
repaint();
|
||||
announce(primaryName, toggled);
|
||||
}
|
||||
|
||||
public void
|
||||
setSecondaryToggled(boolean toggled)
|
||||
{
|
||||
secondaryToggled = toggled;
|
||||
repaint();
|
||||
announce(secondaryName, toggled);
|
||||
}
|
||||
|
||||
public boolean
|
||||
getPrimaryToggled()
|
||||
{
|
||||
return primaryToggled;
|
||||
}
|
||||
|
||||
public boolean
|
||||
getSecondaryToggled()
|
||||
{
|
||||
return secondaryToggled;
|
||||
}
|
||||
|
||||
// - -%- -
|
||||
|
||||
private void
|
||||
announce(String name, boolean toggled)
|
||||
{
|
||||
ActionEvent eA = new ActionEvent(
|
||||
this, nextEventID++,
|
||||
name + (toggled ? "On" : "Off")
|
||||
);
|
||||
for (ActionListener listener: getActionListeners())
|
||||
listener.actionPerformed(eA);
|
||||
}
|
||||
|
||||
|
||||
protected void
|
||||
paintComponent(Graphics g)
|
||||
{
|
||||
g.drawImage(button, 0, 0, this);
|
||||
if (!isEnabled())
|
||||
g.drawImage(disabledOverlay, 0, 0, this);
|
||||
if (isFocusOwner())
|
||||
g.drawImage(selectedOverlay, 0, 0, this);
|
||||
|
||||
if (primaryToggled)
|
||||
g.drawImage(primaryToggledIcon, 0, 0, this);
|
||||
else
|
||||
g.drawImage(primaryUntoggledIcon, 0, 0, this);
|
||||
if (secondaryToggled)
|
||||
g.drawImage(secondaryToggledIcon, 0, 0, this);
|
||||
else
|
||||
g.drawImage(secondaryUntoggledIcon, 0, 0, this);
|
||||
}
|
||||
|
||||
|
||||
public void
|
||||
mouseClicked(MouseEvent eM)
|
||||
{
|
||||
switch (eM.getButton()) {
|
||||
case MouseEvent.BUTTON1: togglePrimary(); break;
|
||||
case MouseEvent.BUTTON3: toggleSecondary(); break;
|
||||
}
|
||||
}
|
||||
|
||||
public void
|
||||
keyPressed(KeyEvent eK)
|
||||
{
|
||||
switch (eK.getKeyCode()) {
|
||||
case KeyEvent.VK_SPACE: togglePrimary(); break;
|
||||
case KeyEvent.VK_ENTER: toggleSecondary(); break;
|
||||
}
|
||||
}
|
||||
|
||||
public void
|
||||
focusGained(FocusEvent eF) { repaint(); }
|
||||
|
||||
public void
|
||||
focusLost(FocusEvent eF) { repaint(); }
|
||||
|
||||
|
||||
public void
|
||||
mousePressed(MouseEvent eM) { }
|
||||
|
||||
public void
|
||||
mouseReleased(MouseEvent eM) { }
|
||||
|
||||
public void
|
||||
mouseEntered(MouseEvent eM) { }
|
||||
|
||||
public void
|
||||
mouseExited(MouseEvent eM) { }
|
||||
|
||||
public void
|
||||
keyReleased(KeyEvent eK) { }
|
||||
|
||||
public void
|
||||
keyTyped(KeyEvent eK) { }
|
||||
|
||||
// (悪) If we don't have the 'armed' behaviour of JButton,
|
||||
// we really need to rectify that, it's important for this
|
||||
// use case.
|
||||
|
||||
// ---%-@-%---
|
||||
|
||||
TwoToggleButton(String primaryName, String secondaryName)
|
||||
{
|
||||
if (button == null) loadCommonImages();
|
||||
this.primaryName = primaryName;
|
||||
this.secondaryName = secondaryName;
|
||||
|
||||
setModel(new DefaultButtonModel());
|
||||
setFocusable(true);
|
||||
setOpaque(false);
|
||||
|
||||
int w = button.getWidth(null);
|
||||
int h = button.getHeight(null);
|
||||
setPreferredSize(new Dimension(w, h));
|
||||
loadSpecificImages();
|
||||
|
||||
this.addKeyListener(this);
|
||||
this.addMouseListener(this);
|
||||
this.addFocusListener(this);
|
||||
}
|
||||
|
||||
private void
|
||||
loadSpecificImages()
|
||||
{
|
||||
String p1 = "graphics/" + primaryName + "Toggled.png";
|
||||
String p2 = "graphics/" + secondaryName + "Toggled.png";
|
||||
String p3 = "graphics/" + primaryName + "Untoggled.png";
|
||||
String p4 = "graphics/" + secondaryName + "Untoggled.png";
|
||||
URL u1 = getClass().getResource(p1);
|
||||
URL u2 = getClass().getResource(p2);
|
||||
URL u3 = getClass().getResource(p3);
|
||||
URL u4 = getClass().getResource(p4);
|
||||
if (u1 == null) primaryToggledIcon = null;
|
||||
else primaryToggledIcon = new ImageIcon(u1).getImage();
|
||||
if (u2 == null) secondaryToggledIcon = null;
|
||||
else secondaryToggledIcon = new ImageIcon(u2).getImage();
|
||||
if (u3 == null) primaryUntoggledIcon = null;
|
||||
else primaryUntoggledIcon = new ImageIcon(u3).getImage();
|
||||
if (u4 == null) secondaryUntoggledIcon = null;
|
||||
else secondaryUntoggledIcon = new ImageIcon(u4).getImage();
|
||||
}
|
||||
|
||||
// - -%- -
|
||||
|
||||
private static void
|
||||
loadCommonImages()
|
||||
{
|
||||
Class c = TwoToggleButton.class;
|
||||
URL u1 = c.getResource("graphics/button.png");
|
||||
URL u2 = c.getResource("graphics/disabledOverlay.png");
|
||||
URL u3 = c.getResource("graphics/selectedOverlay.png");
|
||||
assert u1 != null && u2 != null && u3 != null;
|
||||
button = new ImageIcon(u1).getImage();
|
||||
disabledOverlay = new ImageIcon(u2).getImage();
|
||||
selectedOverlay = new ImageIcon(u3).getImage();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
class
|
||||
TwoToggleButtonTest {
|
||||
|
||||
public static void
|
||||
main(String... args)
|
||||
{
|
||||
if (args.length != 2)
|
||||
{
|
||||
String err = "Please give two toggle names.";
|
||||
System.err.println(err);
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
String p = args[0], s = args[1];
|
||||
TwoToggleButton t1 = new TwoToggleButton(p, s);
|
||||
TwoToggleButton t2 = new TwoToggleButton(p, s);
|
||||
TwoToggleButton t3 = new TwoToggleButton(p, s);
|
||||
|
||||
JFrame mainframe = new JFrame("Two-toggle button test");
|
||||
mainframe.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
mainframe.setLocationByPlatform(true);
|
||||
mainframe.add(t1, java.awt.BorderLayout.WEST);
|
||||
mainframe.add(t2);
|
||||
mainframe.add(t3, java.awt.BorderLayout.EAST);
|
||||
mainframe.pack();
|
||||
t2.setEnabled(false);
|
||||
t3.requestFocusInWindow();
|
||||
mainframe.setVisible(true);
|
||||
}
|
||||
|
||||
}
|
BIN
graphics/Flags.xcf
Normal file
BIN
graphics/Hourglass.xcf
Normal file
BIN
graphics/boostToggled.png
Executable file
After Width: | Height: | Size: 3.9 KiB |
BIN
graphics/boostUntoggled.png
Executable file
After Width: | Height: | Size: 3.7 KiB |
BIN
graphics/button.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
graphics/disabledOverlay.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
graphics/favouriteToggled.png
Executable file
After Width: | Height: | Size: 353 B |
BIN
graphics/favouriteUntoggled.png
Executable file
After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
BIN
graphics/selectedOverlay.png
Normal file
After Width: | Height: | Size: 313 B |
BIN
graphics/test1.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
graphics/test2.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
graphics/test3.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
graphics/test4.png
Normal file
After Width: | Height: | Size: 2.3 KiB |