biskuteri-cafe-JKomasto2/TimelineWindow.java

703 lines
18 KiB
Java
Executable File

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JComponent;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JMenuBar;
import javax.swing.JSeparator;
import javax.swing.Box;
import javax.swing.ImageIcon;
import javax.swing.BorderFactory;
import java.awt.BorderLayout;
import java.awt.GridBagLayout;
import java.awt.GridBagConstraints;
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.net.URL;
import java.net.MalformedURLException;
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 JKomasto
primaire;
private MastodonApi
api;
private TimelinePage
page;
// - -%- -
private TimelineComponent
display;
private JMenuItem
openHome,
// umm, what about the timeline that's like, notes that your
// post was favourited or replied to? those aren't messages..
openMessages,
openLocal,
openFederated,
createPost,
openAutoPostView,
quit;
private JMenuItem
flipToNewestPost;
// - -%- -
private static final int
PREVIEW_COUNT = TimelineComponent.PREVIEW_COUNT;
// ---%-@-%---
public void
setTimelineType(TimelineType type)
{
page.type = type;
String s = type.toString();
s = s.charAt(0) + s.substring(1).toLowerCase();
setTitle(s + " - JKomasto");
}
public void
showLatestPage()
{
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
api.getTimelinePage(
page.type, PREVIEW_COUNT, null, 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");
// lol...
}
public void
requestSucceeded(Tree<String> json)
{
page.posts = toPosts(json);
display.setPosts(page.posts);
display.setNextPageAvailable(true);
}
}
);
display.setCursor(null);
}
public void
nextPage()
{
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()
{
assert page.posts != null;
assert page.posts.size() != 0;
Post first = page.posts.get(0);
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
api.getTimelinePage(
page.type, PREVIEW_COUNT, null, first.postId,
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.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)
{
/*
* Umm.. should we even have a quit option?
* Wouldn't closing every window work? By
* disposing of everyone. We won't be having any
* 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)
{
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;
Tree<String> account = post.get("account");
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;
try {
String av = account.get("avatar").value;
ImageIcon icon = new ImageIcon(new URL(av));
addee.authorAvatar = icon.getImage();
}
catch (MalformedURLException eMu) {
// Weird bug on their part.. We should
// probably react by using a default avatar.
}
String f = post.get("favourited").value;
String b = post.get("reblogged").value;
addee.favourited = f.equals("true");
addee.boosted = b.equals("true");
Tree<String> a1 = post.get("media_attachments");
Attachment[] a2 = new Attachment[a1.size()];
for (int o = 0; o < a2.length; ++o)
{
a2[o] = new Attachment();
a2[o].type = a1.get(o).get("type").value;
a2[o].url = a1.get(o).get("remote_url").value;
if (a2[o].url == null)
a2[o].url = a1.get(o).get("text_url").value;
a2[o].image = null;
if (a2[o].type.equals("image")) try
{
URL url = new URL(a2[o].url);
ImageIcon icon = new ImageIcon(url);
String desc = a1.get(o).get("description").value;
icon.setDescription(desc);
a2[o].image = icon.getImage();
}
catch (MalformedURLException eMu) { }
}
addee.attachments = a2;
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("&lt;", "<");
s = s.replaceAll("&gt;", ">");
s = s.replaceAll("&nbsp;", "");
return s;
}
// ---%-@-%---
TimelineWindow(JKomasto primaire)
{
this.primaire = primaire;
this.api = primaire.getMastodonApi();
getContentPane().setPreferredSize(new Dimension(320, 460));
pack();
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
openHome = new JMenuItem("Open home timeline");
openFederated = new JMenuItem("Open federated timeline");
createPost = new JMenuItem("Create a post");
openAutoPostView = new JMenuItem("Open auto post view");
quit = new JMenuItem("Quit");
openHome.addActionListener(this);
openFederated.addActionListener(this);
createPost.addActionListener(this);
openAutoPostView.addActionListener(this);
quit.addActionListener(this);
flipToNewestPost = new JMenuItem("Flip to newest post");
flipToNewestPost.addActionListener(this);
JMenu programMenu = new JMenu("Program");
programMenu.add(openHome);
programMenu.add(openFederated);
programMenu.add(new JSeparator());
programMenu.add(createPost);
programMenu.add(openAutoPostView);
programMenu.add(new JSeparator());
programMenu.add(quit);
JMenu timelineMenu = new JMenu("Timeline");
timelineMenu.add(flipToNewestPost);
JMenuBar menuBar = new JMenuBar();
menuBar.add(programMenu);
menuBar.add(timelineMenu);
setJMenuBar(menuBar);
page = new TimelinePage();
page.posts = new ArrayList<>();
setTimelineType(TimelineType.HOME);
display = new TimelineComponent(this);
display.setNextPageAvailable(false);
display.setPreviousPageAvailable(false);
setContentPane(display);
}
}
class
TimelineComponent extends JPanel
implements ActionListener, MouseListener {
private TimelineWindow
primaire;
private final List<Post>
posts = new ArrayList<>();
// - -%- -
private JButton
next, prev;
private JLabel
pageLabel;
private final List<PostPreviewComponent>
postPreviews;
private boolean
hoverSelect;
// - -%- -
static final int
PREVIEW_COUNT = 10;
// ---%-@-%---
public void
setPosts(List<Post> posts)
{
this.posts.clear();
this.posts.addAll(posts);
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
setPageLabel(String label)
{
assert label != null;
pageLabel.setText("" + label);
}
public void
setNextPageAvailable(boolean available)
{
next.setEnabled(available);
}
public void
setPreviousPageAvailable(boolean available)
{
prev.setEnabled(available);
}
public void
setHoverSelect(boolean a) { this.hoverSelect = a; }
public void
mouseEntered(MouseEvent eM)
{
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)
{
Object src = eA.getSource();
if (src == next) primaire.nextPage();
else if (src == prev) primaire.previousPage();
/*
* I think the page previews will just forward to us.
* But I think they'll have to tell us where they are
* in the list somehow, because we need to show only
* one post as selected.
*/
}
// ---%-@-%---
TimelineComponent(TimelineWindow primaire)
{
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");
Box bottom = Box.createHorizontalBox();
bottom.add(Box.createGlue());
bottom.add(prev);
bottom.add(Box.createHorizontalStrut(8));
bottom.add(next);
JPanel centre = new JPanel();
centre.setOpaque(false);
centre.setLayout(new GridBagLayout());
GridBagConstraints constraints = new GridBagConstraints();
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.gridx = 0;
constraints.weightx = 1;
constraints.insets = new Insets(4, 0, 4, 0);
for (int n = PREVIEW_COUNT; n > 0; --n)
{
PostPreviewComponent c = new PostPreviewComponent();
c.reset();
c.addMouseListener(this);
centre.add(c, constraints);
postPreviews.add(c);
}
setLayout(new BorderLayout());
add(centre, BorderLayout.CENTER);
add(bottom, BorderLayout.SOUTH);
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
}
}
class
PostPreviewComponent extends JComponent {
private JLabel
topLeft, topRight, bottom;
// ---%-@-%---
public void
setTopLeft(String text) { topLeft.setText(text); }
public void
setTopRight(String text) { topRight.setText(text); }
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 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();
topLeft.setFont(f1);
setOpaque(false);
topRight = new JLabel();
topRight.setHorizontalAlignment(JLabel.RIGHT);
topRight.setFont(f2);
setOpaque(false);
bottom = new JLabel();
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);
}
}