Moved image methods to ImageApi.

Added testing implementation of updater.
This commit is contained in:
Snowyfox 2022-04-14 00:38:49 -04:00
parent 28c7da2034
commit 511ca1aeef
22 changed files with 664 additions and 160 deletions

65
ComposeWindow.java Executable file → Normal file
View File

@ -9,6 +9,7 @@ import javax.swing.JButton;
import javax.swing.Box;
import javax.swing.BorderFactory;
import javax.swing.JOptionPane;
import java.awt.GridLayout;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.event.ActionListener;
@ -52,6 +53,7 @@ ComposeWindow extends JFrame {
composition.text = "";
composition.visibility = PostVisibility.MENTIONED;
composition.replyToPostId = null;
composition.contentWarning = null;
syncDisplayToComposition();
}
@ -59,10 +61,16 @@ ComposeWindow extends JFrame {
submit()
{
syncCompositionToDisplay();
if (composition.replyToPostId != null)
assert !composition.replyToPostId.trim().isEmpty();
if (composition.contentWarning != null)
assert !composition.contentWarning.trim().isEmpty();
display.setSubmitting(true);
api.submit(
composition.text, composition.visibility,
composition.replyToPostId,
composition.replyToPostId, composition.contentWarning,
new RequestListener() {
public void
@ -105,6 +113,7 @@ ComposeWindow extends JFrame {
display.setText(composition.text);
display.setReplyToPostId(composition.replyToPostId);
display.setVisibility(stringFor(composition.visibility));
display.setContentWarning(composition.contentWarning);
}
private void
@ -113,7 +122,19 @@ ComposeWindow extends JFrame {
composition.text = display.getText();
composition.visibility =
visibilityFrom(display.getVisibility());
composition.replyToPostId = display.getReplyToPostId();
composition.replyToPostId =
nonEmpty(display.getReplyToPostId());
composition.contentWarning =
nonEmpty(display.getContentWarning());
}
// - -%- -
private static String
nonEmpty(String s)
{
if (s.trim().isEmpty()) return null;
return s;
}
// ---%-@-%---
@ -182,7 +203,7 @@ implements ActionListener {
text;
private JTextField
reply;
reply, contentWarning;
private JComboBox<String>
visibility;
@ -217,6 +238,12 @@ implements ActionListener {
this.visibility.setSelectedIndex(3);
}
public void
setContentWarning(String contentWarning)
{
this.contentWarning.setText(contentWarning);
}
public String
getText()
{
@ -229,6 +256,12 @@ implements ActionListener {
return reply.getText();
}
public String
getContentWarning()
{
return contentWarning.getText();
}
public String
getVisibility()
{
@ -265,11 +298,21 @@ implements ActionListener {
{
this.primaire = primaire;
text = new JTextArea();
text.setLineWrap(true);
text.setWrapStyleWord(true);
reply = new JTextField();
JLabel replyLabel = new JLabel("In reply to: ");
replyLabel.setLabelFor(reply);
contentWarning = new JTextField();
JLabel cwLabel = new JLabel("Content warning: ");
cwLabel.setLabelFor(contentWarning);
JPanel top = new JPanel();
top.setOpaque(false);
top.setLayout(new GridLayout(2, 2, 8, 0));
top.add(replyLabel);
top.add(reply);
top.add(cwLabel);
top.add(contentWarning);
visibility = new JComboBox<>(new String[] {
"Public",
@ -289,11 +332,9 @@ 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);
text = new JTextArea();
text.setLineWrap(true);
text.setWrapStyleWord(true);
setLayout(new BorderLayout(0, 8));
add(top, BorderLayout.NORTH);

33
ImageApi.java Normal file
View File

@ -0,0 +1,33 @@
import javax.swing.ImageIcon;
import java.awt.Image;
import java.awt.Toolkit;
import java.net.URL;
import java.net.MalformedURLException;
interface
ImageApi {
public static Image
local(String name)
{
String path = "/graphics/" + name + ".png";
URL url = ImageApi.class.getResource(name);
if (url == null) return null;
return new ImageIcon(url).getImage();
}
public static Image
remote(String urlr)
{
try {
URL url = new URL(urlr);
Toolkit TK = Toolkit.getDefaultToolkit();
return TK.createImage(url);
}
catch (MalformedURLException eMu) {
return null;
}
}
}

View File

@ -99,7 +99,7 @@ ImageWindow extends JFrame {
ImageWindow()
{
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
setSize(400, 400);
setSize(600, 600);
display = new ImageComponent(this);
showAttachments(new Attachment[0]);
@ -156,6 +156,7 @@ implements
setPrev(Image image)
{
prev.setEnabled(image != null);
prev.setText(image == null ? "<" : "");
prev.setIcon(toIcon(image));
}
@ -163,6 +164,7 @@ implements
setNext(Image image)
{
next.setEnabled(image != null);
next.setText(image == null ? ">" : "");
next.setIcon(toIcon(image));
}
@ -250,7 +252,8 @@ implements
{
if (image == null)
{
String str = "(There are no images being displayed.)";
String str =
"(There are no images being displayed.)";
FontMetrics fm = g.getFontMetrics();
int x = (getWidth() - fm.stringWidth(str)) / 2;
int y = (getHeight() + fm.getHeight()) / 2;
@ -285,15 +288,15 @@ implements
{
this.primaire = primaire;
Dimension BUTTON_SIZE = new Dimension(80, 60);
Dimension BUTTON_SIZE = new Dimension(48, 48);
setOpaque(false);
scaleImage = true;
zoomLevel = 100;
prev = new JButton("<");
prev = new JButton();
toggle = new JButton("Show unscaled");
next = new JButton(">");
next = new JButton();
prev.setPreferredSize(BUTTON_SIZE);
next.setPreferredSize(BUTTON_SIZE);
prev.addActionListener(this);
@ -306,6 +309,9 @@ implements
buttonArea.add(toggle);
buttonArea.add(next);
setPrev(null);
setNext(null);
setLayout(new BorderLayout());
add(buttonArea, BorderLayout.SOUTH);
add(new Painter(), BorderLayout.CENTER);

22
JKomasto.java Executable file → Normal file
View File

@ -28,6 +28,9 @@ JKomasto {
private ImageWindow
mediaWindow;
private TimelineWindowUpdater
timelineWindowUpdater;
private MastodonApi
api;
@ -52,6 +55,8 @@ JKomasto {
loginWindow.dispose();
autoViewWindow.setCursor(null);
timelineWindow.setCursor(null);
timelineWindowUpdater.addWindow(timelineWindow);
}
public PostWindow
@ -87,6 +92,8 @@ JKomasto {
mediaWindow.dispose();
loginWindow.setLocationByPlatform(true);
loginWindow.setVisible(true);
timelineWindowUpdater = new TimelineWindowUpdater(this);
}
}
@ -123,6 +130,9 @@ TimelinePage {
public TimelineType
type;
public String
accountNumId;
public List<Post>
posts;
@ -143,6 +153,12 @@ Post {
public Image
authorAvatar;
public String
authorNumId;
public String
boosterName;
public ZonedDateTime
date;
@ -170,6 +186,9 @@ Attachment {
public String
url;
public String
description;
public Image
image;
@ -181,7 +200,8 @@ class
Composition {
public String
text;
text,
contentWarning;
public PostVisibility
visibility;

10
LoginWindow.java Executable file → Normal file
View File

@ -117,6 +117,7 @@ LoginWindow extends JFrame {
requestSucceeded(Tree<String> json)
{
api.setAccountDetails(json);
serverContacted = true;
haveAccountDetails = true;
updateStatusDisplay();
}
@ -146,7 +147,14 @@ LoginWindow extends JFrame {
public void
useInstanceUrl()
{
if (display.isAutoLoginToggled()) { useCache(); return; }
String url = display.getInstanceUrl();
if (url.trim().isEmpty()) {
// Should we show an error dialog..?
display.setInstanceUrl("");
return;
}
if (!hasProtocol(url)) {
url = "https://" + url;
display.setInstanceUrl(url);
@ -157,8 +165,6 @@ LoginWindow extends JFrame {
haveAccessToken = false;
haveAccountDetails = false;
if (display.isAutoLoginToggled()) { useCache(); return; }
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
api.testUrlConnection(url, new RequestListener() {

View File

@ -11,7 +11,9 @@ import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
class
MastodonApi {
@ -77,7 +79,8 @@ MastodonApi {
getAppCredentials(RequestListener handler)
{
assert instanceUrl != null;
try {
try
{
URL endpoint = new URL(instanceUrl + "/api/v1/apps");
HttpURLConnection conn;
conn = (HttpURLConnection)endpoint.openConnection();
@ -164,7 +167,8 @@ MastodonApi {
URL endpoint = new URL(instanceUrl + s);
HttpURLConnection conn;
conn = (HttpURLConnection)endpoint.openConnection();
conn.setRequestProperty("Authorization", "Bearer " + token);
String s2 = "Bearer " + token;
conn.setRequestProperty("Authorization", s2);
conn.connect();
doStandardJsonReturn(conn, handler);
@ -174,13 +178,18 @@ MastodonApi {
public void
getTimelinePage(
TimelineType type, int count, String maxId, String minId,
TimelineType type, String accountId,
int count, String maxId, String minId,
RequestListener handler)
{
String token = accessToken.get("access_token").value;
String url = instanceUrl + "/api/v1";
switch (type)
if (accountId != null)
{
url += "/accounts/" + accountId + "/statuses";
}
else switch (type)
{
case FEDERATED:
case LOCAL: url += "/timelines/public"; break;
@ -266,13 +275,15 @@ MastodonApi {
public void
submit(
String text, PostVisibility visibility, String replyTo,
String text, PostVisibility visibility,
String replyTo, String contentWarning,
RequestListener handler)
{
String token = accessToken.get("access_token").value;
String visibilityParam = "direct";
switch (visibility) {
switch (visibility)
{
case PUBLIC: visibilityParam = "public"; break;
case UNLISTED: visibilityParam = "unlisted"; break;
case FOLLOWERS: visibilityParam = "private"; break;
@ -283,7 +294,8 @@ MastodonApi {
String url = instanceUrl + "/api/v1/statuses";
try
{
text = URLEncoder.encode(text, "UTF-8");
text = encode(text);
contentWarning = encode(contentWarning);
URL endpoint = new URL(url);
HttpURLConnection conn;
@ -303,6 +315,9 @@ MastodonApi {
if (replyTo != null) {
output.write("&in_reply_to_id=" + replyTo);
}
if (contentWarning != null) {
output.write("&spoiler_text=" + contentWarning);
}
output.close();
doStandardJsonReturn(conn, handler);
@ -310,10 +325,57 @@ MastodonApi {
catch (IOException eIo) { handler.connectionFailed(eIo); }
}
public void
monitorTimeline(
TimelineType type, ServerSideEventsListener handler)
{
String token = accessToken.get("access_token").value;
String url = instanceUrl + "/api/v1/streaming";
switch (type)
{
case FEDERATED: url += "/public"; break;
case LOCAL: url += "/public/local"; break;
case HOME:
case NOTIFICATIONS: url += "/user"; break;
default: assert false;
}
try
{
URL endpoint = new URL(url);
HttpURLConnection conn;
conn = (HttpURLConnection)endpoint.openConnection();
String s = "Bearer " + token;
conn.setRequestProperty("Authorization", s);
conn.connect();
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());
BufferedReader br = new BufferedReader(input);
while (true) {
String line = br.readLine();
if (line != null) handler.lineReceived(line);
}
}
catch (IOException eIo) { handler.connectionFailed(eIo); }
}
// - -%- -
private void
doStandardJsonReturn(HttpURLConnection conn, RequestListener handler)
doStandardJsonReturn(
HttpURLConnection conn, RequestListener handler)
throws IOException
{
InputStreamReader input;
@ -334,7 +396,8 @@ MastodonApi {
}
private void
returnResponseInTree(HttpURLConnection conn, RequestListener handler)
returnResponseInTree(
HttpURLConnection conn, RequestListener handler)
throws IOException
{
InputStreamReader input;
@ -371,6 +434,19 @@ MastodonApi {
return doc;
}
private static String
encode(String s)
{
try {
if (s == null) return null;
return URLEncoder.encode(s, "UTF-8");
}
catch (UnsupportedEncodingException eUe) {
assert false;
return null;
}
}
// ---%-@-%---
public void

37
PostWindow.java Executable file → Normal file
View File

@ -21,6 +21,7 @@ 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;
@ -103,6 +104,7 @@ implements ActionListener {
? null
: post.attachments[0].image
);
repliesDisplay.setReplies(replies);
postDisplay.resetFocus();
repaint();
@ -111,7 +113,11 @@ implements ActionListener {
public void
openAuthorProfile()
{
TimelineWindow w = new TimelineWindow(primaire);
w.showAuthorPosts(post.authorNumId);
w.showLatestPage();
w.setLocationRelativeTo(this);
w.setVisible(true);
}
public void
@ -151,8 +157,9 @@ implements ActionListener {
};
api.setPostFavourited(post.postId, favourited, handler);
postDisplay.setFavouriteBoostEnabled(true);
postDisplay.setCursor(null);
postDisplay.setFavouriteBoostEnabled(true);
postDisplay.repaint();
}
public void
@ -192,8 +199,9 @@ implements ActionListener {
};
api.setPostBoosted(post.postId, boosted, handler);
postDisplay.setFavouriteBoostEnabled(true);
postDisplay.setCursor(null);
postDisplay.setFavouriteBoostEnabled(true);
postDisplay.repaint();
}
public void
@ -217,7 +225,7 @@ implements ActionListener {
int l = Math.min(40, post.text.length());
w.setTitle(post.text.substring(0, l));
if (!w.isVisible()) {
w.setLocation(getX(), getY() + 100);
w.setLocationRelativeTo(null);
w.setVisible(true);
}
}
@ -227,7 +235,7 @@ implements ActionListener {
public void
actionPerformed(ActionEvent eA)
{
Object src = eA.getSource();
Component src = (Component)eA.getSource();
if (!(src instanceof JMenuItem)) return;
String text = ((JMenuItem)src).getText();
@ -358,7 +366,7 @@ implements ActionListener {
public void
actionPerformed(ActionEvent eA)
{
Object src = eA.getSource();
Component src = (Component)eA.getSource();
String command = eA.getActionCommand();
if (src == profile)
@ -412,8 +420,8 @@ implements ActionListener {
g.clearRect(0, 0, getWidth(), getHeight());
((java.awt.Graphics2D)g).setRenderingHint(
java.awt.RenderingHints.KEY_TEXT_ANTIALIASING,
java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_ON
java.awt.RenderingHints.KEY_ANTIALIASING,
java.awt.RenderingHints.VALUE_ANTIALIAS_ON
);
Font f1 = new Font("IPAGothic", Font.PLAIN, 16);
@ -454,13 +462,6 @@ implements ActionListener {
// - -%- -
private static ImageIcon
toIcon(Image image)
{
if (image == null) return null;
return new ImageIcon(image);
}
private static List<String>
split(String string, int lineLength)
{
@ -472,7 +473,7 @@ implements ActionListener {
if (word.length() >= lineLength) {
word = word.substring(0, lineLength - 4) + "...";
}
if (word.equals("\n")) {
if (word.matches("\n")) {
returnee.add(empty(line));
continue;
}
@ -504,9 +505,9 @@ implements ActionListener {
authorName = authorId = time = text = "";
Dimension buttonSize = new Dimension(20, 40);
Border b = BorderFactory.createEmptyBorder(10, 10, 10, 10);
profile = new RoundButton();
//profile.setPreferredSize(buttonSize);
profile.addActionListener(this);
favouriteBoost = new TwoToggleButton("favourite", "boost");
@ -537,7 +538,7 @@ implements ActionListener {
Box buttons = Box.createVerticalBox();
buttons.setOpaque(false);
buttons.add(ibuttons);
buttons.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
buttons.setBorder(b);
setLayout(new BorderLayout());
add(buttons, BorderLayout.WEST);

View File

@ -15,3 +15,17 @@ RequestListener {
requestSucceeded(Tree<String> json);
}
interface
ServerSideEventsListener {
void
connectionFailed(IOException eIo);
void
requestFailed(int httpCode, Tree<String> json);
void
lineReceived(String line);
}

284
TimelineWindow.java Executable file → Normal file
View File

@ -9,7 +9,6 @@ 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;
@ -30,6 +29,10 @@ import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseEvent;
import java.awt.event.FocusListener;
import java.awt.event.FocusEvent;
import java.awt.event.KeyListener;
import java.awt.event.KeyEvent;
import cafe.biskuteri.hinoki.Tree;
import java.io.IOException;
@ -59,8 +62,6 @@ implements ActionListener {
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,
@ -82,21 +83,29 @@ implements ActionListener {
setTimelineType(TimelineType type)
{
page.type = type;
page.accountNumId = null;
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);
display.setBackgroundImage(ImageApi.local(s2));
}
public TimelineType
getTimelineType() { return page.type; }
public void
showAuthorPosts(String authorNumId)
{
assert authorNumId != null;
page.type = TimelineType.FEDERATED;
page.accountNumId = authorNumId;
setTitle(authorNumId + " - JKomasto");
display.setBackgroundImage(ImageApi.local("profile"));
}
public void
@ -104,12 +113,14 @@ implements ActionListener {
{
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
api.getTimelinePage(
page.type, PREVIEW_COUNT, null, null,
page.type, page.accountNumId,
PREVIEW_COUNT, null, null,
new RequestListener() {
public void
connectionFailed(IOException eIo)
{
eIo.printStackTrace();
String s = eIo.getClass().getName();
setTitle(s + " - JKomasto");
}
@ -117,6 +128,7 @@ implements ActionListener {
public void
requestFailed(int httpCode, Tree<String> json)
{
System.err.println(json.get("error").value);
setTitle(httpCode + " - JKomasto");
// lol...
}
@ -124,9 +136,13 @@ implements ActionListener {
public void
requestSucceeded(Tree<String> json)
{
page.posts = toPosts(json);
List<Post> posts = toPosts(json);
page.posts = posts;
display.setPosts(page.posts);
display.setNextPageAvailable(true);
boolean full = posts.size() >= PREVIEW_COUNT;
display.setNextPageAvailable(full);
display.setPreviousPageAvailable(true);
display.resetFocus();
}
}
@ -143,7 +159,8 @@ implements ActionListener {
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
api.getTimelinePage(
page.type, PREVIEW_COUNT, last.postId, null,
page.type, page.accountNumId,
PREVIEW_COUNT, last.postId, null,
new RequestListener() {
public void
@ -162,9 +179,19 @@ implements ActionListener {
public void
requestSucceeded(Tree<String> json)
{
page.posts = toPosts(json);
List<Post> posts = toPosts(json);
if (posts.size() == 0) {
// We should probably say something
// to the user here? For now, we
// quietly cancel.
return;
}
page.posts = posts;
display.setPosts(page.posts);
boolean full = posts.size() >= PREVIEW_COUNT;
display.setNextPageAvailable(full);
display.setPreviousPageAvailable(true);
display.resetFocus();
}
}
@ -181,7 +208,8 @@ implements ActionListener {
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
api.getTimelinePage(
page.type, PREVIEW_COUNT, null, first.postId,
page.type, page.accountNumId,
PREVIEW_COUNT, null, first.postId,
new RequestListener() {
public void
@ -200,17 +228,21 @@ implements ActionListener {
public void
requestSucceeded(Tree<String> json)
{
page.posts = toPosts(json);
List<Post> posts = toPosts(json);
if (posts.size() < PREVIEW_COUNT) {
showLatestPage();
return;
}
page.posts = posts;
display.setPosts(page.posts);
display.setNextPageAvailable(true);
display.setPreviousPageAvailable(true);
display.resetFocus();
}
}
);
display.setCursor(null);
if (page.posts.size() < PREVIEW_COUNT) showLatestPage();
}
// - -%- -
@ -221,6 +253,15 @@ implements ActionListener {
primaire.getAutoViewWindow().showPost(post);
}
public void
postOpened(Post post)
{
PostWindow w = new PostWindow(primaire);
w.showPost(post);
w.setLocationRelativeTo(this);
w.setVisible(true);
}
public void
actionPerformed(ActionEvent eA)
{
@ -278,6 +319,13 @@ implements ActionListener {
{
Post addee = new Post();
if (post.get("reblog").size() != 0) {
Tree<String> a = post.get("account");
String s = a.get("display_name").value;
addee.boosterName = s;
post = post.get("reblog");
}
addee.postId = post.get("id").value;
try {
@ -304,7 +352,7 @@ implements ActionListener {
b.append(" \n ");
}
if (node.get(0).key.equals("/p")) {
b.append(" \n\n ");
b.append(" \n \n ");
}
}
if (node.key.equals("text")) {
@ -323,21 +371,16 @@ implements ActionListener {
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;
addee.authorNumId = account.get("id").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 s3 = account.get("avatar").value;
addee.authorAvatar = ImageApi.remote(s3);
if (addee.authorAvatar == null) {
s3 = "defaultAvatar";
addee.authorAvatar = ImageApi.local(s3);
}
String f = post.get("favourited").value;
@ -345,29 +388,23 @@ implements ActionListener {
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)
Tree<String> as1 = post.get("media_attachments");
Attachment[] as2 = new Attachment[as1.size()];
for (int o = 0; o < as2.length; ++o)
{
a2[o] = new Attachment();
a2[o].type = a1.get(o).get("type").value;
Tree<String> a1 = as1.get(o);
Attachment a2 = as2[o] = new Attachment();
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();
a2.type = a1.get("type").value;
String u1 = a1.get("remote_url").value;
String u2 = a1.get("text_url").value;
a2.url = u1 == null ? u2 : u1;
a2.description = a1.get("description").value;
a2.image = null;
if (a2.type.equals("image"))
a2.image = ImageApi.remote(a2.url);
}
catch (MalformedURLException eMu) { }
}
addee.attachments = a2;
addee.attachments = as2;
posts.add(addee);
}
@ -418,6 +455,7 @@ implements ActionListener {
flipToNewestPost.addActionListener(this);
JMenu programMenu = new JMenu("Program");
programMenu.setMnemonic(KeyEvent.VK_P);
programMenu.add(openHome);
programMenu.add(openFederated);
programMenu.add(new JSeparator());
@ -426,6 +464,7 @@ implements ActionListener {
programMenu.add(new JSeparator());
programMenu.add(quit);
JMenu timelineMenu = new JMenu("Timeline");
timelineMenu.setMnemonic(KeyEvent.VK_T);
timelineMenu.add(flipToNewestPost);
JMenuBar menuBar = new JMenuBar();
menuBar.add(programMenu);
@ -449,7 +488,9 @@ implements ActionListener {
class
TimelineComponent extends JPanel
implements ActionListener, MouseListener {
implements
ActionListener, KeyListener,
MouseListener, FocusListener {
private TimelineWindow
primaire;
@ -494,14 +535,23 @@ implements ActionListener, MouseListener {
Post p = posts.get(o);
c.setTopLeft(p.authorName);
{
String f = "";
if (p.boosterName != null)
f += "b";
if (p.attachments.length > 0)
f += "a";
String t;
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 (s < 30) t = "now";
else if (s < 60) t = d + "s";
else if (s < 3600) t = (d / 60) + "m";
else if (s < 86400) t = (d / 3600) + "h";
else t = (d / 86400) + "d";
c.setTopRight(f + " " + t);
}
if (p.contentWarning != null)
c.setBottom("(" + p.contentWarning + ")");
@ -533,11 +583,19 @@ implements ActionListener, MouseListener {
public void
setBackgroundImage(Image n) { backgroundImage = n; }
public void
resetFocus() { postPreviews.get(0).requestFocusInWindow(); }
// - -%- -
protected void
paintComponent(Graphics g)
{
((java.awt.Graphics2D)g).setRenderingHint(
java.awt.RenderingHints.KEY_ANTIALIASING,
java.awt.RenderingHints.VALUE_ANTIALIAS_ON
);
int w = getWidth(), h = getHeight();
g.clearRect(0, 0, w, h);
int h2 = h * 5 / 10, w2 = h2;
@ -545,6 +603,60 @@ implements ActionListener, MouseListener {
g.drawImage(backgroundImage, x, y, w2, h2, this);
}
private void
select(Object c)
{
assert c instanceof PostPreviewComponent;
PostPreviewComponent p = (PostPreviewComponent)c;
int offset = postPreviews.indexOf(p);
assert offset != -1;
if (offset < posts.size()) {
primaire.postSelected(posts.get(offset));
p.setSelected(true);
}
else {
p.setSelected(false);
}
p.repaint();
}
private void
deselect(Object c)
{
assert c instanceof PostPreviewComponent;
PostPreviewComponent p = (PostPreviewComponent)c;
p.setSelected(false);
p.repaint();
}
private void
open(Object c)
{
assert c instanceof PostPreviewComponent;
PostPreviewComponent p = (PostPreviewComponent)c;
int offset = postPreviews.indexOf(p);
assert offset != -1;
if (offset < posts.size()) {
primaire.postOpened(posts.get(offset));
}
}
public void
focusGained(FocusEvent eF) { select(eF.getSource()); }
public void
focusLost(FocusEvent eF) { deselect(eF.getSource()); }
public void
mouseClicked(MouseEvent eM)
{
if (eM.getClickCount() == 2) open(eM.getSource());
else select(eM.getSource());
}
public void
mouseEntered(MouseEvent eM)
{
@ -552,33 +664,27 @@ implements ActionListener, MouseListener {
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();
deselect(eM.getSource());
}
public void
mousePressed(MouseEvent eM) { }
// () First time I'm using these two..!
public void
mouseReleased(MouseEvent eM) { }
keyPressed(KeyEvent eK)
{
if (eK.getKeyCode() != KeyEvent.VK_ENTER) return;
PostPreviewComponent selected = null;
for (PostPreviewComponent c: postPreviews)
if (c.getSelected()) selected = c;
if (selected == null) return;
open(selected);
}
public void
actionPerformed(ActionEvent eA)
@ -595,6 +701,19 @@ implements ActionListener, MouseListener {
*/
}
public void
mousePressed(MouseEvent eM) { }
public void
mouseReleased(MouseEvent eM) { }
public void
keyTyped(KeyEvent eK) { }
public void
keyReleased(KeyEvent eK) { }
// ---%-@-%---
TimelineComponent(TimelineWindow primaire)
@ -608,6 +727,8 @@ implements ActionListener, MouseListener {
next = new JButton(">");
prev.setEnabled(false);
next.setEnabled(false);
prev.setMnemonic(KeyEvent.VK_PAGE_UP);
next.setMnemonic(KeyEvent.VK_PAGE_DOWN);
prev.addActionListener(this);
next.addActionListener(this);
@ -632,6 +753,8 @@ implements ActionListener, MouseListener {
PostPreviewComponent c = new PostPreviewComponent();
c.reset();
c.addMouseListener(this);
c.addFocusListener(this);
c.addKeyListener(this);
centre.add(c, constraints);
postPreviews.add(c);
}
@ -683,6 +806,9 @@ PostPreviewComponent extends JComponent {
else setBackground(new Color(0, 0, 0, 25));
}
public boolean
getSelected() { return selected; }
// - -%- -
protected void
@ -699,8 +825,6 @@ PostPreviewComponent extends JComponent {
public
PostPreviewComponent()
{
selected = false;
Font f = new JLabel().getFont();
Font f1 = f.deriveFont(Font.PLAIN, 12f);
Font f2 = f.deriveFont(Font.ITALIC, 12f);
@ -725,8 +849,8 @@ PostPreviewComponent extends JComponent {
bottom.setFont(f3);
bottom.setOpaque(false);
setFocusable(true);
setOpaque(false);
setSelected(false);
setLayout(new BorderLayout());
add(top, BorderLayout.NORTH);
add(bottom);

177
TimelineWindowUpdater.java Normal file
View File

@ -0,0 +1,177 @@
import java.util.List;
import java.util.ArrayList;
import java.io.IOException;
import cafe.biskuteri.hinoki.Tree;
class
TimelineWindowUpdater {
private JKomasto
primaire;
private MastodonApi
api;
// - -%- -
private List<TimelineWindow>
updatees;
private StringBuilder
event, data;
// - -%- -
private Thread
federated,
local,
home,
notifications;
// ---%-@-%---
public void
addWindow(TimelineWindow updatee)
{
updatees.add(updatee);
Connection c = new Connection();
c.type = updatee.getTimelineType();
Thread t = new Thread(c);
switch (c.type) {
case FEDERATED:
if (federated != null) return;
federated = t; break;
case LOCAL:
if (local != null) return;
local = t; break;
case HOME:
if (home != null) return;
home = t; break;
case NOTIFICATIONS:
if (notifications != null) return;
notifications = t; break;
default: return;
}
t.start();
System.err.println(t);
}
public void
removeWindow(TimelineWindow updatee)
{
updatees.remove(updatee);
}
// - -%- -
private void
handle(TimelineType type, String event, String data)
{
System.err.println("Handling " + event + ".");
assert !data.isEmpty();
if (event.isEmpty()) return;
boolean newPost = event.equals("update");
boolean newNotif = event.equals("notification");
if (!(newPost || newNotif)) return;
for (TimelineWindow updatee: filter(type)) {
System.err.println("Refreshing " + updatee);
updatee.showLatestPage();
}
}
private List<TimelineWindow>
filter(TimelineType type)
{
List<TimelineWindow> returnee = new ArrayList<>();
for (TimelineWindow updatee: updatees)
if (updatee.getTimelineType() == type)
returnee.add(updatee);
return returnee;
}
// ---%-@-%---
private class
Connection
implements Runnable, ServerSideEventsListener {
private TimelineType
type;
// -=-
private StringBuilder
event, data;
// -=%=-
public void
run()
{
event = new StringBuilder();
data = new StringBuilder();
api.monitorTimeline(type, this);
// monitorTimeline should not return
// until the connection is closed.
System.err.println("Finit.");
}
public void
lineReceived(String line)
{
System.err.println("Line: " + line);
if (line.startsWith(":")) return;
if (line.isEmpty()) {
handle(type, event.toString(), data.toString());
event.delete(0, event.length());
data.delete(0, event.length());
}
if (line.startsWith("data: "))
data.append(line.substring("data: ".length()));
if (line.startsWith("event: "))
event.append(line.substring("event: ".length()));
/*
* Note that I utterly ignore https://html.spec.whatwg.org
* /multipage/server-sent-events.html#dispatchMessage.
* That is because I am not a browser.
*/
}
public void
connectionFailed(IOException eIo)
{
// sais pas dois-je faire..
eIo.printStackTrace();
}
public void
requestFailed(int httpCode, Tree<String> json)
{
// mo shiranu
System.err.println(httpCode + ", " + json.get("error").value);
}
}
// ---%-@-%---
TimelineWindowUpdater(JKomasto primaire)
{
this.primaire = primaire;
this.api = primaire.getMastodonApi();
this.updatees = new ArrayList<>();
}
}

26
TwoToggleButton.java Executable file → Normal file
View File

@ -133,6 +133,7 @@ implements KeyListener, MouseListener, FocusListener {
case MouseEvent.BUTTON1: togglePrimary(); break;
case MouseEvent.BUTTON3: toggleSecondary(); break;
}
requestFocusInWindow();
}
public void
@ -142,6 +143,7 @@ implements KeyListener, MouseListener, FocusListener {
case KeyEvent.VK_SPACE: togglePrimary(); break;
case KeyEvent.VK_ENTER: toggleSecondary(); break;
}
requestFocusInWindow();
}
public void
@ -241,6 +243,8 @@ implements KeyListener, MouseListener, FocusListener {
private Image
image;
// - -%- -
private int
nextEventID = ActionEvent.ACTION_FIRST;
@ -261,11 +265,6 @@ implements KeyListener, MouseListener, FocusListener {
protected void
paintComponent(Graphics g)
{
((java.awt.Graphics2D)g).setRenderingHint(
java.awt.RenderingHints.KEY_TEXT_ANTIALIASING,
java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_ON
);
g.drawImage(button, 0, 0, this);
if (!isEnabled())
g.drawImage(disabledOverlay, 0, 0, this);
@ -273,12 +272,13 @@ implements KeyListener, MouseListener, FocusListener {
g.drawImage(selectedOverlay, 0, 0, this);
if (image == null) return;
int w1 = button.getWidth(this);
int h1 = button.getHeight(this);
Shape defaultClip = g.getClip();
Shape roundClip = new Ellipse2D.Float(6, 6, w1 - 12, h1 - 12);
int w2 = image.getWidth(this);
int h2 = image.getHeight(this);
if (w2 == -1) w2 = w1;
if (h2 == -1) h2 = h1;
if (h2 > w2) {
h2 = h2 * w1 / w2;
w2 = w1;
@ -287,8 +287,14 @@ implements KeyListener, MouseListener, FocusListener {
w2 = w2 * h1 / h2;
h2 = h1;
}
Shape defaultClip, roundClip;
defaultClip = g.getClip();
roundClip = new Ellipse2D.Float(6, 6, w1 - 12, h1 - 12);
g.setClip(roundClip);
g.drawImage(image, 0, 0, w2, h2, this);
g.drawImage(image, 0, 0, w2, h2, getParent());
// I don't know why, but when we repaint ourselves, our
// parent doesn't repaint, so nothing seems to happen.
g.setClip(defaultClip);
}
@ -348,8 +354,8 @@ implements KeyListener, MouseListener, FocusListener {
setFocusable(true);
setOpaque(false);
int w = button.getWidth(null);
int h = button.getHeight(null);
int w = button.getWidth(this);
int h = button.getHeight(this);
setPreferredSize(new Dimension(w, h));
this.addKeyListener(this);

0
graphics/Flags.xcf Executable file → Normal file
View File

0
graphics/Hourglass.xcf Executable file → Normal file
View File

0
graphics/button.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

0
graphics/disabledOverlay.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

0
graphics/favouriteToggled.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 353 B

After

Width:  |  Height:  |  Size: 353 B

0
graphics/ref1.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

0
graphics/selectedOverlay.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 313 B

After

Width:  |  Height:  |  Size: 313 B

0
graphics/test1.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

0
graphics/test2.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

0
graphics/test3.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

0
graphics/test4.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB