mirror of
https://gitlab.com/biskuteri-cafe/JKomasto2.git
synced 2024-11-20 05:44:51 +01:00
695d1057a2
Switched to BreakIterator.
759 lines
18 KiB
Java
Executable File
759 lines
18 KiB
Java
Executable File
|
||
import javax.swing.JFrame;
|
||
import javax.swing.JPanel;
|
||
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;
|
||
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;
|
||
import java.text.BreakIterator;
|
||
import java.util.Locale;
|
||
|
||
|
||
class
|
||
PostWindow extends JFrame
|
||
implements ActionListener {
|
||
|
||
private JKomasto
|
||
primaire;
|
||
|
||
private MastodonApi
|
||
api;
|
||
|
||
private Post
|
||
post;
|
||
|
||
// - -%- -
|
||
|
||
private PostComponent
|
||
postDisplay;
|
||
|
||
private RepliesComponent
|
||
repliesDisplay;
|
||
|
||
// - -%- -
|
||
|
||
private static final DateTimeFormatter
|
||
DATE_FORMAT = DateTimeFormatter.ofPattern("d LLLL ''uu"),
|
||
TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm");
|
||
|
||
// ---%-@-%---
|
||
|
||
public void
|
||
showPost(Post post)
|
||
{
|
||
assert post != null;
|
||
this.post = post;
|
||
|
||
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
|
||
);
|
||
|
||
repliesDisplay.setReplies(replies);
|
||
postDisplay.resetFocus();
|
||
repaint();
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
// - -%- -
|
||
|
||
public void
|
||
actionPerformed(ActionEvent eA)
|
||
{
|
||
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();
|
||
}
|
||
}
|
||
|
||
// ---%-@-%---
|
||
|
||
PostWindow(JKomasto primaire)
|
||
{
|
||
this.primaire = primaire;
|
||
this.api = primaire.getMastodonApi();
|
||
|
||
getContentPane().setPreferredSize(new Dimension(360, 270));
|
||
pack();
|
||
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
|
||
setLocationByPlatform(true);
|
||
|
||
postDisplay = new PostComponent(this);
|
||
|
||
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);
|
||
|
||
setContentPane(postDisplay);
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
class
|
||
PostComponent extends JPanel
|
||
implements ActionListener {
|
||
|
||
private PostWindow
|
||
primaire;
|
||
|
||
// - -%- -
|
||
|
||
private List<RichTextPane.Segment>
|
||
authorNameOr, bodyOr;
|
||
|
||
private RichTextPane
|
||
authorName, body;
|
||
|
||
private JLabel
|
||
authorId, time, date;
|
||
|
||
private String[][]
|
||
emojiUrls;
|
||
|
||
private TwoToggleButton
|
||
favouriteBoost,
|
||
replyMisc,
|
||
nextPrev;
|
||
|
||
private RoundButton
|
||
profile,
|
||
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"))
|
||
{
|
||
BreakIterator it = BreakIterator.getWordInstance(Locale.ROOT);
|
||
String text = node.value;
|
||
it.setText(text);
|
||
int start = it.first(), end = it.next();
|
||
while (end != BreakIterator.DONE)
|
||
{
|
||
String word = text.substring(start, end);
|
||
char c = word.isEmpty() ? ' ' : word.charAt(0);
|
||
boolean w = Character.isWhitespace(c);
|
||
b = w ? b.spacer(word) : b.text(word);
|
||
start = end;
|
||
end = it.next();
|
||
}
|
||
}
|
||
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);
|
||
|
||
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);
|
||
|
||
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 {
|
||
|
||
private List<RepliesComponent.Reply>
|
||
replies;
|
||
|
||
// - -%- -
|
||
|
||
private JButton
|
||
prevPage, nextPage;
|
||
|
||
private JLabel
|
||
pageLabel;
|
||
|
||
private ReplyPreviewComponent[]
|
||
previews;
|
||
|
||
// ---%-@-%---
|
||
|
||
public void
|
||
setReplies(List<RepliesComponent.Reply> replies)
|
||
{
|
||
assert replies != null;
|
||
this.replies = replies;
|
||
displayPage(1);
|
||
}
|
||
|
||
// - -%- -
|
||
|
||
private void
|
||
displayPage(int pageNumber)
|
||
{
|
||
assert pageNumber > 0;
|
||
assert this.replies != null;
|
||
|
||
List<RepliesComponent.Reply> page;
|
||
{
|
||
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];
|
||
Reply reply = replies.get(o);
|
||
preview.setAuthorName(reply.author);
|
||
preview.setText(reply.text);
|
||
preview.setVisible(true);
|
||
}
|
||
|
||
for (int o = page.size(); o < previews.length; ++o)
|
||
{
|
||
ReplyPreviewComponent preview = previews[o];
|
||
preview.setVisible(false);
|
||
}
|
||
|
||
int pages = 1 + ((replies.size() - 1) / 8);
|
||
pageLabel.setText(pageNumber + "/" + pages);
|
||
prevPage.setEnabled(pageNumber > 1);
|
||
nextPage.setEnabled(pageNumber < pages);
|
||
}
|
||
|
||
// ---%-@-%---
|
||
|
||
public static class
|
||
Reply {
|
||
|
||
public String
|
||
author;
|
||
|
||
public String
|
||
text;
|
||
|
||
}
|
||
|
||
// ---%-@-%---
|
||
|
||
RepliesComponent()
|
||
{
|
||
prevPage = new JButton("<");
|
||
nextPage = new JButton(">");
|
||
prevPage.setEnabled(false);
|
||
nextPage.setEnabled(false);
|
||
|
||
pageLabel = new JLabel();
|
||
|
||
Box bottom = Box.createHorizontalBox();
|
||
bottom.add(Box.createGlue());
|
||
bottom.add(prevPage);
|
||
bottom.add(Box.createHorizontalStrut(8));
|
||
bottom.add(pageLabel);
|
||
bottom.add(Box.createHorizontalStrut(8));
|
||
bottom.add(nextPage);
|
||
|
||
JPanel centre = new JPanel();
|
||
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]);
|
||
}
|
||
|
||
setLayout(new BorderLayout(0, 8));
|
||
add(centre, BorderLayout.CENTER);
|
||
add(bottom, BorderLayout.SOUTH);
|
||
|
||
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
|
||
|
||
setReplies(new ArrayList<>());
|
||
}
|
||
|
||
}
|
||
|
||
|
||
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)
|
||
{
|
||
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);
|
||
}
|
||
|
||
}
|