mirror of
https://gitlab.com/biskuteri-cafe/JKomasto2.git
synced 2024-11-20 05:14:50 +01:00
28c7da2034
Bug fix on ImageWindow.
736 lines
19 KiB
Java
Executable File
736 lines
19 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.awt.Image;
|
|
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 s1 = type.toString();
|
|
s1 = s1.charAt(0) + s1.substring(1).toLowerCase();
|
|
setTitle(s1 + " - JKomasto");
|
|
|
|
String s2 = type.toString().toLowerCase();
|
|
s2 = "/graphics/" + s2 + ".png";
|
|
URL url = getClass().getResource(s2);
|
|
if (url != null) {
|
|
ImageIcon icon = new ImageIcon(url);
|
|
display.setBackgroundImage(icon.getImage());
|
|
}
|
|
else {
|
|
display.setBackgroundImage(null);
|
|
}
|
|
}
|
|
|
|
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");
|
|
if (post.get("reblog").size() != 0) {
|
|
account = post.get("reblog").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("<", "<");
|
|
s = s.replaceAll(">", ">");
|
|
s = s.replaceAll(" ", "");
|
|
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<>();
|
|
|
|
display = new TimelineComponent(this);
|
|
display.setNextPageAvailable(false);
|
|
display.setPreviousPageAvailable(false);
|
|
setContentPane(display);
|
|
|
|
setTimelineType(TimelineType.HOME);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
private Image
|
|
backgroundImage;
|
|
|
|
// - -%- -
|
|
|
|
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 n) { next.setEnabled(n); }
|
|
|
|
public void
|
|
setPreviousPageAvailable(boolean n) { prev.setEnabled(n); }
|
|
|
|
public void
|
|
setHoverSelect(boolean n) { hoverSelect = n; }
|
|
|
|
public void
|
|
setBackgroundImage(Image n) { backgroundImage = n; }
|
|
|
|
// - -%- -
|
|
|
|
protected void
|
|
paintComponent(Graphics g)
|
|
{
|
|
int w = getWidth(), h = getHeight();
|
|
g.clearRect(0, 0, w, h);
|
|
int h2 = h * 5 / 10, w2 = h2;
|
|
int x = w - w2, y = h - h2;
|
|
g.drawImage(backgroundImage, x, y, w2, h2, this);
|
|
}
|
|
|
|
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;
|
|
|
|
private boolean
|
|
selected;
|
|
|
|
// ---%-@-%---
|
|
|
|
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)
|
|
{
|
|
this.selected = selected;
|
|
if (!selected) setBackground(null);
|
|
else setBackground(new Color(0, 0, 0, 25));
|
|
}
|
|
|
|
// - -%- -
|
|
|
|
protected void
|
|
paintComponent(Graphics g)
|
|
{
|
|
if (selected) {
|
|
g.setColor(getBackground());
|
|
g.fillRect(0, 0, getWidth(), getHeight());
|
|
}
|
|
}
|
|
|
|
// ---%-@-%---
|
|
|
|
public
|
|
PostPreviewComponent()
|
|
{
|
|
selected = false;
|
|
|
|
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);
|
|
topLeft.setOpaque(false);
|
|
|
|
topRight = new JLabel();
|
|
topRight.setHorizontalAlignment(JLabel.RIGHT);
|
|
topRight.setFont(f2);
|
|
topRight.setOpaque(false);
|
|
|
|
Box top = Box.createHorizontalBox();
|
|
top.setOpaque(false);
|
|
top.add(topLeft);
|
|
top.add(Box.createGlue());
|
|
top.add(topRight);
|
|
|
|
bottom = new JLabel();
|
|
bottom.setFont(f3);
|
|
bottom.setOpaque(false);
|
|
|
|
setOpaque(false);
|
|
setSelected(false);
|
|
setLayout(new BorderLayout());
|
|
add(top, BorderLayout.NORTH);
|
|
add(bottom);
|
|
}
|
|
|
|
}
|