biskuteri-cafe-JKomasto2/PostWindow.java

746 lines
18 KiB
Java
Raw Normal View History

2021-07-16 00:37:03 +02:00
import javax.swing.JFrame;
import javax.swing.JPanel;
2021-07-16 00:37:03 +02:00
import javax.swing.JMenuBar;
import javax.swing.JMenu;
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 javax.swing.ImageIcon;
import java.awt.Graphics;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Shape;
2021-07-16 00:37:03 +02:00
import java.awt.Dimension;
import java.awt.BorderLayout;
import java.awt.GridLayout;
import java.awt.Cursor;
import java.awt.Image;
import java.awt.Component;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.util.List;
import java.util.ArrayList;
import java.net.URL;
import java.net.MalformedURLException;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.io.IOException;
import cafe.biskuteri.hinoki.Tree;
2021-07-16 00:37:03 +02:00
class
PostWindow extends JFrame
implements ActionListener {
2021-07-16 00:37:03 +02:00
private JKomasto
primaire;
private MastodonApi
api;
2021-07-16 11:46:17 +02:00
private Post
post;
// - -%- -
2021-07-16 00:37:03 +02:00
private PostComponent
2021-07-16 11:46:17 +02:00
postDisplay;
2021-07-16 00:37:03 +02:00
private RepliesComponent
2021-07-16 11:46:17 +02:00
repliesDisplay;
2021-07-16 00:37:03 +02:00
// - -%- -
private static final DateTimeFormatter
DATE_FORMAT = DateTimeFormatter.ofPattern("d LLLL ''uu"),
TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm");
// ---%-@-%---
2021-07-17 13:17:14 +02:00
public void
showPost(Post post)
2021-07-17 13:17:14 +02:00
{
assert post != null;
this.post = post;
2021-07-29 11:26:16 +02:00
List<RepliesComponent.Reply> replies = null;
{
List<Post> posts = null;
// We should make a request to JKomasto here.
}
if (replies == null)
{
RepliesComponent.Reply reply1, reply2, reply3;
reply1 = new RepliesComponent.Reply();
reply1.author = "Black tea";
reply1.text = "Rich..";
reply2 = new RepliesComponent.Reply();
reply2.author = "Green tea";
reply2.text = "Clean!";
reply3 = new RepliesComponent.Reply();
reply3.author = "Coffee";
reply3.text = "sleepy..";
replies = new ArrayList<>();
replies.add(reply1);
replies.add(reply2);
replies.add(reply3);
}
postDisplay.setAuthorName(post.authorName);
postDisplay.setAuthorId(post.authorId);
postDisplay.setAuthorAvatar(post.authorAvatar);
postDisplay.setDate(DATE_FORMAT.format(post.date));
postDisplay.setTime(TIME_FORMAT.format(post.date));
postDisplay.setEmojiUrls(post.emojiUrls);
postDisplay.setText(post.text);
postDisplay.setHtml(post.html);
postDisplay.setFavourited(post.favourited);
postDisplay.setBoosted(post.boosted);
postDisplay.setMediaPreview(
post.attachments.length == 0
? null
: post.attachments[0].image
);
2021-07-29 11:26:16 +02:00
repliesDisplay.setReplies(replies);
postDisplay.resetFocus();
repaint();
2021-07-17 13:17:14 +02:00
}
public void
openAuthorProfile()
{
TimelineWindow w = new TimelineWindow(primaire);
w.showAuthorPosts(post.authorNumId);
w.showLatestPage();
w.setLocationRelativeTo(this);
w.setVisible(true);
}
public void
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.setCursor(null);
postDisplay.setFavouriteBoostEnabled(true);
postDisplay.repaint();
}
public void
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.setCursor(null);
postDisplay.setFavouriteBoostEnabled(true);
postDisplay.repaint();
}
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
openMedia()
{
ImageWindow w = primaire.getMediaWindow();
w.showAttachments(post.attachments);
int l = Math.min(40, post.text.length());
w.setTitle(post.text.substring(0, l));
if (!w.isVisible()) {
w.setLocationRelativeTo(null);
w.setVisible(true);
}
}
// - -%- -
2021-07-17 11:46:27 +02:00
public void
actionPerformed(ActionEvent eA)
2021-07-17 11:46:27 +02:00
{
Component src = (Component)eA.getSource();
if (!(src instanceof JMenuItem)) return;
String text = ((JMenuItem)src).getText();
if (text.equals("Post"))
{
setContentPane(postDisplay);
revalidate();
/*
* () Setting a content pane in itself doesn't
* do anything to the content pane. Validation
* of the validate root does. Which happens on
* window realisation, or by manual call.
*/
}
else if (text.equals("Replies"))
{
setContentPane(repliesDisplay);
revalidate();
}
2021-07-17 11:46:27 +02:00
}
2021-07-16 00:37:03 +02:00
// ---%-@-%---
PostWindow(JKomasto primaire)
2021-07-16 00:37:03 +02:00
{
this.primaire = primaire;
this.api = primaire.getMastodonApi();
2021-07-16 11:46:17 +02:00
getContentPane().setPreferredSize(new Dimension(360, 270));
pack();
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
2021-07-16 00:37:03 +02:00
setLocationByPlatform(true);
postDisplay = new PostComponent(this);
2021-07-16 11:46:17 +02:00
repliesDisplay = new RepliesComponent();
Post samplePost = new Post();
samplePost.text = "This is a sample post.";
samplePost.html = "";
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;
samplePost.attachments = new Attachment[0];
samplePost.emojiUrls = new String[0][];
showPost(samplePost);
2021-07-16 00:37:03 +02:00
2021-07-17 11:46:27 +02:00
setContentPane(postDisplay);
}
2021-07-16 00:37:03 +02:00
}
class
PostComponent extends JPanel
implements ActionListener {
private PostWindow
primaire;
2021-07-31 13:28:46 +02:00
// - -%- -
private List<RichTextPane.Segment>
authorNameOr, bodyOr;
private RichTextPane
authorName, body;
private JLabel
authorId, time, date;
private String[][]
emojiUrls;
private TwoToggleButton
2021-07-31 13:28:46 +02:00
favouriteBoost,
replyMisc,
nextPrev;
private RoundButton
profile,
2021-07-31 13:28:46 +02:00
media;
// ---%-@-%---
public void
setAuthorName(String n)
{
authorNameOr = new RichTextPane.Builder().text(n).finish();
}
public void
setAuthorId(String n) { authorId.setText(n); }
public void
setAuthorAvatar(Image n) { profile.setImage(n); }
public void
setDate(String n) { date.setText(n); }
public void
setTime(String n) { time.setText(n); }
public void
setText(String n) { }
public void
setEmojiUrls(String[][] n) { emojiUrls = n; }
public void
setHtml(String n)
{
RichTextPane.Builder b = new RichTextPane.Builder();
Tree<String> nodes = RudimentaryHTMLParser.depthlessRead(n);
for (Tree<String> node: nodes)
{
if (node.key.equals("tag"))
{
String tagName = node.get(0).key;
if (tagName.equals("br"))
b = b.spacer("\n");
if (tagName.equals("/p"))
b = b.spacer("\n").spacer("\n");
if (tagName.equals("a"))
b = b.link(node.get("href").value, null).spacer(" ");
}
if (node.key.equals("text"))
{
for (String word: node.value.split(" "))
b = b.text(word).spacer(" ");
}
if (node.key.equals("emoji"))
{
String shortcode = node.value;
String url = null;
for (String[] entry: emojiUrls)
if (entry[0].equals(shortcode)) url = entry[1];
try {
ImageIcon image = new ImageIcon(new URL(url));
b = b.image(image, node.value);
}
catch (MalformedURLException eMu) {
b = b.text(":×:");
}
}
}
bodyOr = b.finish();
}
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
setMediaPreview(Image n) { media.setImage(n); }
public void
resetFocus() { media.requestFocusInWindow(); }
// - -%- -
public void
actionPerformed(ActionEvent eA)
{
Component src = (Component)eA.getSource();
String command = eA.getActionCommand();
if (src == profile)
{
primaire.openAuthorProfile();
return;
}
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;
}
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)
{
g.clearRect(0, 0, getWidth(), getHeight());
((java.awt.Graphics2D)g).setRenderingHint(
java.awt.RenderingHints.KEY_ANTIALIASING,
java.awt.RenderingHints.VALUE_ANTIALIAS_ON
);
int w1 = authorName.getWidth();
int w2 = body.getWidth();
FontMetrics fm1 = getFontMetrics(authorName.getFont());
FontMetrics fm2 = getFontMetrics(body.getFont());
authorName.setText(RichTextPane.layout(authorNameOr, fm1, w1));
body.setText(RichTextPane.layout(bodyOr, fm2, w2));
}
// ---%-@-%---
PostComponent(PostWindow primaire)
{
this.primaire = primaire;
emojiUrls = new String[0][];
Border b = BorderFactory.createEmptyBorder(10, 10, 10, 10);
Font f1 = new Font("IPAGothic", Font.PLAIN, 16);
Font f2 = new Font("IPAGothic", Font.PLAIN, 13);
2021-07-16 11:46:17 +02:00
profile = new RoundButton();
favouriteBoost = new TwoToggleButton("favourite", "boost");
replyMisc = new TwoToggleButton("reply", "misc");
nextPrev = new TwoToggleButton("next", "prev");
media = new RoundButton();
profile.addActionListener(this);
favouriteBoost.addActionListener(this);
replyMisc.addActionListener(this);
nextPrev.addActionListener(this);
media.addActionListener(this);
2021-07-31 13:28:46 +02:00
Box buttons = Box.createVerticalBox();
buttons.setOpaque(false);
buttons.add(profile);
buttons.add(Box.createVerticalStrut(8));
buttons.add(favouriteBoost);
buttons.add(Box.createVerticalStrut(8));
buttons.add(replyMisc);
buttons.add(Box.createVerticalStrut(8));
buttons.add(nextPrev);
buttons.add(Box.createVerticalStrut(8));
buttons.add(media);
buttons.setMaximumSize(buttons.getPreferredSize());
Box left = Box.createVerticalBox();
left.setOpaque(false);
left.add(buttons);
authorId = new JLabel();
authorName = new RichTextPane();
time = new JLabel();
date = new JLabel();
authorId.setFont(f2);
date.setFont(f2);
authorName.setFont(f1);
time.setFont(f1);
JPanel top1 = new JPanel();
top1.setLayout(new BorderLayout(8, 0));
top1.add(authorId);
top1.add(date, BorderLayout.EAST);
JPanel top2 = new JPanel();
top2.setLayout(new BorderLayout(8, 0));
top2.add(authorName);
top2.add(time, BorderLayout.EAST);
Box top = Box.createVerticalBox();
top.add(top1);
top.add(Box.createVerticalStrut(2));
top.add(top2);
body = new RichTextPane();
body.setFont(getFont().deriveFont(14f));
JPanel centre = new JPanel();
centre.setOpaque(false);
centre.setLayout(new BorderLayout(0, 8));
centre.add(top, BorderLayout.NORTH);
centre.add(body);
setLayout(new BorderLayout(8, 0));
add(left, BorderLayout.WEST);
add(centre);
setBorder(b);
}
}
class
RepliesComponent extends JPanel {
2021-07-29 11:26:16 +02:00
private List<RepliesComponent.Reply>
replies;
// - -%- -
private JButton
prevPage, nextPage;
private JLabel
pageLabel;
private ReplyPreviewComponent[]
previews;
// ---%-@-%---
public void
2021-07-29 11:26:16 +02:00
setReplies(List<RepliesComponent.Reply> replies)
{
2021-07-29 11:26:16 +02:00
assert replies != null;
this.replies = replies;
displayPage(1);
}
// - -%- -
private void
displayPage(int pageNumber)
{
2021-07-29 11:26:16 +02:00
assert pageNumber > 0;
assert this.replies != null;
List<RepliesComponent.Reply> page;
{
2021-07-29 11:26:16 +02:00
int oS = (pageNumber - 1) * 8;
int oE = Math.min(oS + 8, replies.size());
if (oS > oE) page = new ArrayList<>();
else page = this.replies.subList(oS, oE);
}
for (int o = 0; o < page.size(); ++o)
{
assert o < previews.length;
ReplyPreviewComponent preview = previews[o];
2021-07-29 11:26:16 +02:00
Reply reply = replies.get(o);
preview.setAuthorName(reply.author);
2021-07-29 11:26:16 +02:00
preview.setText(reply.text);
preview.setVisible(true);
}
2021-07-29 11:26:16 +02:00
for (int o = page.size(); o < previews.length; ++o)
{
ReplyPreviewComponent preview = previews[o];
preview.setVisible(false);
}
2021-07-29 11:26:16 +02:00
int pages = 1 + ((replies.size() - 1) / 8);
pageLabel.setText(pageNumber + "/" + pages);
prevPage.setEnabled(pageNumber > 1);
nextPage.setEnabled(pageNumber < pages);
2021-07-29 11:26:16 +02:00
}
// ---%-@-%---
public static class
Reply {
public String
author;
public String
text;
}
// ---%-@-%---
RepliesComponent()
{
prevPage = new JButton("<");
nextPage = new JButton(">");
prevPage.setEnabled(false);
nextPage.setEnabled(false);
2021-07-29 11:26:16 +02:00
pageLabel = new JLabel();
Box bottom = Box.createHorizontalBox();
bottom.add(Box.createGlue());
bottom.add(prevPage);
2021-07-17 11:37:00 +02:00
bottom.add(Box.createHorizontalStrut(8));
bottom.add(pageLabel);
2021-07-17 11:37:00 +02:00
bottom.add(Box.createHorizontalStrut(8));
bottom.add(nextPage);
JPanel centre = new JPanel();
2021-07-17 11:37:00 +02:00
centre.setOpaque(false);
centre.setLayout(new GridLayout(0, 1, 0, 2));
previews = new ReplyPreviewComponent[8];
for (int o = 0; o < previews.length; ++o)
{
previews[o] = new ReplyPreviewComponent();
previews[o].setVisible(false);
centre.add(previews[o]);
}
2021-07-17 11:37:00 +02:00
setLayout(new BorderLayout(0, 8));
add(centre, BorderLayout.CENTER);
add(bottom, BorderLayout.SOUTH);
2021-07-17 11:37:00 +02:00
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
2021-07-29 11:26:16 +02:00
setReplies(new ArrayList<>());
}
}
2021-07-17 11:37:00 +02:00
class
ReplyPreviewComponent extends JButton {
private String
author;
private String
text;
// ---%-@-%---
@Override
public void
setText(String text)
{
assert text != null;
this.text = text;
setText();
}
public void
setAuthorName(String author)
2021-07-17 11:37:00 +02:00
{
assert author != null;
this.author = author;
setText();
}
// - -%- -
private void
setText()
{
StringBuilder text = new StringBuilder();
text.append(this.author);
text.append(" @ ");
text.append(this.text);
super.setText(text.toString());
}
protected void
paintComponent(Graphics g)
{
g.drawString(getText(), 8, 2 * getHeight() / 3);
}
}