Refactored back to objects. Moved entity unpacking to object classes.

Added composition length counter.
Fixed timeline background image bug.
This commit is contained in:
Snowyfox 2022-05-06 03:05:01 -04:00
parent a114b1a386
commit 1bf48d831b
39 changed files with 675 additions and 342 deletions

0
ClipboardApi.java Executable file → Normal file
View File

56
ComposeWindow.java Executable file → Normal file
View File

@ -15,8 +15,12 @@ import java.awt.BorderLayout;
import java.awt.Dimension; import java.awt.Dimension;
import java.awt.event.ActionListener; import java.awt.event.ActionListener;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.awt.event.KeyListener;
import java.awt.event.KeyEvent;
import java.awt.Cursor; import java.awt.Cursor;
import java.awt.Color; import java.awt.Color;
import javax.swing.event.CaretListener;
import javax.swing.event.CaretEvent;
import cafe.biskuteri.hinoki.Tree; import cafe.biskuteri.hinoki.Tree;
import java.io.IOException; import java.io.IOException;
@ -92,7 +96,7 @@ ComposeWindow extends JFrame {
ComposeWindow.this, ComposeWindow.this,
"Tried to submit post, failed..." "Tried to submit post, failed..."
+ "\n" + json.get("error").value + "\n" + json.get("error").value
+ "(HTTP error code: " + httpCode + ")" + "\n(HTTP error code: " + httpCode + ")"
); );
} }
@ -194,7 +198,7 @@ ComposeWindow extends JFrame {
class class
ComposeComponent extends JPanel ComposeComponent extends JPanel
implements ActionListener { implements ActionListener, CaretListener, KeyListener {
private ComposeWindow private ComposeWindow
primaire; primaire;
@ -207,6 +211,9 @@ implements ActionListener {
private JTextField private JTextField
reply, contentWarning; reply, contentWarning;
private JLabel
textLength;
private JComboBox<String> private JComboBox<String>
visibility; visibility;
@ -294,6 +301,42 @@ implements ActionListener {
public void public void
actionPerformed(ActionEvent eA) { primaire.submit(); } actionPerformed(ActionEvent eA) { primaire.submit(); }
public void
caretUpdate(CaretEvent eCa) { updateTextLength(); }
public void
keyPressed(KeyEvent eK) { updateTextLength(); }
public void
keyReleased(KeyEvent eK) { }
public void
keyTyped(KeyEvent eK) { }
private void
updateTextLength()
{
int length = text.getText().length();
/*
* The web interface doesn't do this expensive thing.
* It has an upwards counter, incremented by I'm not
* sure what. Presumably they have some control over
* the text input. I'd rather not, cause I use a
* Japanese IME, I'm going to see how laggy this is.
* It raises our app's system requirements, but, I was
* going to transition it to multithreading anyways,
* I don't think we're going to be very cheap.. Which
* sucks, but the Mastodon API is not helping us here.
*/
textLength.setText(Integer.toString(length));
/*
* Another thing I could do is temporarily move the
* caret to the end and then find its position, then
* seek back. Not sure how much that would help, but
* if this is too laggy, that's what I'd try next.
*/
}
// ---%-@-%--- // ---%-@-%---
ComposeComponent(ComposeWindow primaire) ComposeComponent(ComposeWindow primaire)
@ -321,6 +364,9 @@ implements ActionListener {
top.add(cwLabel); top.add(cwLabel);
top.add(contentWarning); top.add(contentWarning);
textLength = new JLabel("0");
textLength.setFont(textLength.getFont().deriveFont(14f));
visibility = new JComboBox<>(new String[] { visibility = new JComboBox<>(new String[] {
"Public", "Public",
"Unlisted", "Unlisted",
@ -335,8 +381,10 @@ implements ActionListener {
Box bottom = Box.createHorizontalBox(); Box bottom = Box.createHorizontalBox();
bottom.add(Box.createGlue()); bottom.add(Box.createGlue());
bottom.add(textLength);
bottom.add(Box.createHorizontalStrut(12));
bottom.add(visibility); bottom.add(visibility);
bottom.add(Box.createHorizontalStrut(8)); bottom.add(Box.createHorizontalStrut(12));
bottom.add(submit); bottom.add(submit);
text = new JTextArea(); text = new JTextArea();
@ -344,6 +392,8 @@ implements ActionListener {
text.setWrapStyleWord(true); text.setWrapStyleWord(true);
text.setFont(text.getFont().deriveFont(16f)); text.setFont(text.getFont().deriveFont(16f));
text.setBorder(bc); text.setBorder(bc);
text.addCaretListener(this);
text.addKeyListener(this);
setLayout(new BorderLayout(0, 8)); setLayout(new BorderLayout(0, 8));
add(top, BorderLayout.NORTH); add(top, BorderLayout.NORTH);

24
ImageApi.java Executable file → Normal file
View File

@ -11,8 +11,8 @@ ImageApi {
public static Image public static Image
local(String name) local(String name)
{ {
String path = "/graphics/" + name + ".png"; String path = "graphics/" + name + ".png";
URL url = ImageApi.class.getResource(name); URL url = ImageApi.class.getResource(path);
if (url == null) return null; if (url == null) return null;
return new ImageIcon(url).getImage(); return new ImageIcon(url).getImage();
} }
@ -20,12 +20,28 @@ ImageApi {
public static Image public static Image
remote(String urlr) remote(String urlr)
{ {
try { try
{
URL url = new URL(urlr); URL url = new URL(urlr);
Toolkit TK = Toolkit.getDefaultToolkit(); Toolkit TK = Toolkit.getDefaultToolkit();
return TK.createImage(url); return TK.createImage(url);
} }
catch (MalformedURLException eMu) { catch (MalformedURLException eMu)
{
return null;
}
}
public static ImageIcon
iconRemote(String urlr)
{
if (urlr == null) return null;
try
{
return new ImageIcon(new URL(urlr));
}
catch (MalformedURLException eMu)
{
return null; return null;
} }
} }

0
ImageWindow.java Executable file → Normal file
View File

438
JKomasto.java Executable file → Normal file
View File

@ -2,12 +2,21 @@
import javax.swing.JFrame; import javax.swing.JFrame;
import javax.swing.JPanel; import javax.swing.JPanel;
import javax.swing.JComponent; import javax.swing.JComponent;
import javax.swing.ImageIcon;
import java.awt.Dimension; import java.awt.Dimension;
import java.awt.BorderLayout; import java.awt.BorderLayout;
import java.awt.Cursor; import java.awt.Cursor;
import java.awt.Image; import java.awt.Image;
import java.awt.FontMetrics;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.text.BreakIterator;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.time.Period;
import java.time.temporal.ChronoUnit;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import cafe.biskuteri.hinoki.Tree; import cafe.biskuteri.hinoki.Tree;
@ -126,7 +135,8 @@ TimelineType {
FEDERATED, FEDERATED,
LOCAL, LOCAL,
HOME, HOME,
LIST LIST,
PROFILE
} }
@ -154,9 +164,22 @@ TimelinePage {
public String public String
accountNumId, listId; accountNumId, listId;
public Tree<String> public Post[]
posts; posts;
// ---%-@-%---
public
TimelinePage() { }
public
TimelinePage(Tree<String> entity)
{
posts = new Post[entity.size()];
for (int o = 0; o < posts.length; ++o)
posts[o] = new Post(entity.get(o));
}
} }
@ -175,9 +198,313 @@ Notification {
} }
class
Post {
public String
id,
uri;
public Account
author;
public PostVisibility
visibility;
public String
text,
approximateText;
public List<RichTextPane.Segment>
formattedText,
laidoutText;
public String
contentWarning;
public ZonedDateTime
dateTime;
public String
date, time, relativeTime;
public boolean
boosted,
favourited;
public Post
boostedPost;
public Attachment[]
attachments;
public String[][]
emojiUrls;
// - -%- -
private static final DateTimeFormatter
DATE_FORMAT = DateTimeFormatter.ofPattern("d LLLL ''uu"),
TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm");
// ---%-@-%---
public void
resolveApproximateText()
{
assert text != null;
if (approximateText != null) return;
Tree<String> nodes;
nodes = RudimentaryHTMLParser.depthlessRead(text);
if (nodes.size() == 0)
{
approximateText = "-";
return;
}
StringBuilder b = new StringBuilder();
Tree<String> first = nodes.get(0);
for (Tree<String> node: nodes)
{
if (node.key.equals("tag"))
{
if (node.get(0).key.equals("br"))
b.append("; ");
if (node.get(0).key.equals("p") && node != first)
b.append("; ");
}
if (node.key.equals("emoji"))
{
b.append(":" + node.value + ":");
}
if (node.key.equals("text"))
{
b.append(node.value);
}
}
approximateText = b.toString();
}
public void
resolveFormattedText()
{
assert text != null;
assert emojiUrls != null;
if (formattedText != null) return;
RichTextPane.Builder b = new RichTextPane.Builder();
Tree<String> nodes;
nodes = RudimentaryHTMLParser.depthlessRead(text);
for (Tree<String> node: nodes)
{
if (node.key.equals("tag"))
{
String tagName = node.get(0).key;
Tree<String> href = node.get("href");
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(href.value, null).spacer(" ");
}
if (node.key.equals("text"))
{
BreakIterator it;
it = BreakIterator.getWordInstance(Locale.ROOT);
it.setText(node.value);
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[] mapping: emojiUrls)
{
String ms = mapping[0];
String mu = mapping[1];
if (ms.equals(shortcode)) url = mu;
}
ImageIcon icon = ImageApi.iconRemote(url);
if (icon != null) b = b.image(icon, node.value);
else b = b.text(":" + node.value + ":");
}
}
formattedText = b.finish();
}
public int
resolveLaidoutText(int width, FontMetrics fm)
{
assert formattedText != null;
laidoutText = RichTextPane.layout(formattedText, fm, width);
int maxY = 0;
for (RichTextPane.Segment segment: laidoutText)
if (segment.y > maxY) maxY = segment.y;
return maxY;
}
public void
resolveRelativeTime()
{
assert date != null;
ZonedDateTime now = ZonedDateTime.now();
long d = ChronoUnit.SECONDS.between(dateTime, now);
long s = Math.abs(d);
if (s < 30) relativeTime = "now";
else if (s < 60) relativeTime = d + "s";
else if (s < 3600) relativeTime = (d / 60) + "m";
else if (s < 86400) relativeTime = (d / 3600) + "h";
else relativeTime = (d / 86400) + "d";
}
// ---%-@-%---
public
Post() { }
public
Post(Tree<String> entity)
{
id = entity.get("id").value;
uri = entity.get("url").value;
if (uri == null) uri = entity.get("uri").value;
author = new Account(entity.get("account"));
String v = entity.get("visibility").value;
boolean p = v.equals("public");
boolean u = v.equals("unlisted");
boolean f = v.equals("private");
boolean m = v.equals("direct");
if (p) visibility = PostVisibility.PUBLIC;
if (u) visibility = PostVisibility.UNLISTED;
if (f) visibility = PostVisibility.FOLLOWERS;
if (m) visibility = PostVisibility.MENTIONED;
dateTime =
ZonedDateTime.parse(entity.get("created_at").value)
.withZoneSameInstant(ZoneId.systemDefault());
date = DATE_FORMAT.format(dateTime);
time = TIME_FORMAT.format(dateTime);
text = entity.get("content").value;
String st = entity.get("spoiler_text").value;
contentWarning = st.trim().isEmpty() ? null : st;
String favourited = entity.get("favourited").value;
String boosted = entity.get("reblogged").value;
this.favourited = favourited.equals("true");
this.boosted = boosted.equals("true");
Tree<String> media = entity.get("media_attachments");
attachments = new Attachment[media.size()];
for (int o = 0; o < attachments.length; ++o)
{
attachments[o] = new Attachment(media.get(o));
}
Tree<String> emojis = entity.get("emojis");
emojiUrls = new String[emojis.size()][];
for (int o = 0; o < emojiUrls.length; ++o)
{
Tree<String> emoji = emojis.get(o);
String[] mapping = emojiUrls[o] = new String[2];
mapping[0] = emoji.get("shortcode").value;
mapping[1] = emoji.get("url").value;
}
Tree<String> boostedPost = entity.get("reblog");
if (boostedPost.size() > 0)
this.boostedPost = new Post(boostedPost);
}
}
class
Account {
public String
numId;
public String
id,
name;
public List<RichTextPane.Segment>
formattedName;
public String
avatarUrl;
public Image
avatar;
// ---%-@-%---
public void
resolveFormattedName()
{
assert name != null;
formattedName =
new RichTextPane.Builder().text(name).finish();
}
public void
resolveAvatar()
{
assert avatarUrl != null;
if (avatar != null) return;
avatar = ImageApi.remote(avatarUrl);
}
// ---%-@-%---
public
Account() { }
public
Account(Tree<String> entity)
{
numId = entity.get("id").value;
id = entity.get("acct").value;
String displayName = entity.get("display_name").value;
String username = entity.get("username").value;
name = displayName.isEmpty() ? username : displayName;
avatarUrl = entity.get("avatar").value;
}
}
class class
Attachment { Attachment {
public String
id;
public String public String
type; type;
@ -190,6 +517,34 @@ Attachment {
public Image public Image
image; image;
// ---%-@-%---
public void
resolveImage()
{
assert url != null;
if (image != null) return;
if (!type.equals("image")) return;
image = ImageApi.remote(url);
}
// ---%-@-%---
public
Attachment() { }
public
Attachment(Tree<String> entity)
{
String u1 = entity.get("remote_url").value;
String u2 = entity.get("text_url").value;
String u3 = entity.get("url").value;
url = u1 != null ? u1 : u2 != null ? u2 : u3;
type = entity.get("type").value;
description = entity.get("description").value;
}
} }
@ -207,4 +562,83 @@ Composition {
public String public String
replyToPostId; replyToPostId;
// ---%-@-%---
public
Composition() { }
public static Composition
reply(Tree<String> entity, String ownNumId)
{
Composition c = new Composition();
Tree<String> boosted = entity.get("reblog");
if (boosted.size() > 0) entity = boosted;
String st = entity.get("spoiler_text").value;
String ri = entity.get("id").value;
c.contentWarning = st.trim().isEmpty() ? null : st;
c.replyToPostId = ri.trim().isEmpty() ? null : ri;
Tree<String> author = entity.get("account");
String authorId = author.get("acct").value;
String authorNumId = author.get("id").value;
c.text = "";
if (!authorNumId.equals(ownNumId))
c.text = "@" + authorId + " ";
String visibility = entity.get("visibility").value;
boolean p = visibility.equals("public");
boolean u = visibility.equals("unlisted");
boolean f = visibility.equals("private");
boolean m = visibility.equals("direct");
assert p || u || f || m;
if (p) c.visibility = PostVisibility.PUBLIC;
if (u) c.visibility = PostVisibility.UNLISTED;
if (f) c.visibility = PostVisibility.FOLLOWERS;
if (m) c.visibility = PostVisibility.MENTIONED;
// Less eye strain arranged this way.
return c;
}
public static Composition
recover(Tree<String> entity)
{
assert entity.get("text") != null;
Composition c = new Composition();
c.text = entity.get("text").value;
c.contentWarning = entity.get("spoiler_text").value;
c.replyToPostId = entity.get("in_reply_to_id").value;
String visibility = entity.get("visibility").value;
boolean p = visibility.equals("public");
boolean u = visibility.equals("unlisted");
boolean f = visibility.equals("private");
boolean m = visibility.equals("direct");
assert p || u || f || m;
if (p) c.visibility = PostVisibility.PUBLIC;
if (u) c.visibility = PostVisibility.UNLISTED;
if (f) c.visibility = PostVisibility.FOLLOWERS;
if (m) c.visibility = PostVisibility.MENTIONED;
return c;
}
public static Composition
reply(Post post, String ownNumId)
{
if (post.boostedPost != null) post = post.boostedPost;
Composition c = new Composition();
c.replyToPostId = post.id;
c.visibility = post.visibility;
c.contentWarning = post.contentWarning;
c.text = "";
if (!post.author.numId.equals(ownNumId))
c.text = "@" + post.author.id + " ";
return c;
}
} }

0
KDE_Dialog_Appear.wav Executable file → Normal file
View File

0
LoginWindow.java Executable file → Normal file
View File

0
MastodonApi.java Executable file → Normal file
View File

11
NotificationsWindow.java Executable file → Normal file
View File

@ -87,13 +87,10 @@ NotificationsWindow extends JFrame {
if (n.type != NotificationType.FOLLOW) if (n.type != NotificationType.FOLLOW)
{ {
Tree<String> post = t.get("status"); Post post = new Post(t.get("status"));
String pid, phtml, ptext; post.resolveApproximateText();
pid = post.get("id").value; n.postId = post.id;
phtml = post.get("content").value; n.postText = post.approximateText;
ptext = TimelineWindow.textApproximation(phtml);
n.postId = pid;
n.postText = ptext;
} }
notifications.add(n); notifications.add(n);

274
PostWindow.java Executable file → Normal file
View File

@ -49,8 +49,9 @@ PostWindow extends JFrame {
private MastodonApi private MastodonApi
api; api;
private Tree<String> private Post
post; post,
wrapperPost;
// - -%- - // - -%- -
@ -65,78 +66,67 @@ PostWindow extends JFrame {
// ---%-@-%--- // ---%-@-%---
public void public void
readEntity(Tree<String> post) use(Post post)
{ {
this.post = post; assert post != null;
Tree<String> boosted = post.get("reblog"); if (post.boostedPost != null)
if (boosted.size() > 0) post = boosted; {
wrapperPost = post;
this.post = post.boostedPost;
post = post.boostedPost;
}
else
{
wrapperPost = null;
this.post = post;
}
Tree<String> author = post.get("account"); display.setAuthorName(post.author.name);
Tree<String> emojis = post.get("emojis"); display.setAuthorId(post.author.id);
Tree<String> media = post.get("media_attachments");
String an = author.get("display_name").value;
if (an.isEmpty()) an = author.get("username").value;
display.setAuthorName(an);
display.setAuthorId(author.get("acct").value);
String aid = author.get("id").value;
String oid = api.getAccountDetails().get("id").value; String oid = api.getAccountDetails().get("id").value;
String aid = post.author.numId;
display.setDeleteEnabled(aid.equals(oid)); display.setDeleteEnabled(aid.equals(oid));
String avurl = author.get("avatar").value; post.author.resolveAvatar();
display.setAuthorAvatar(ImageApi.remote(avurl)); display.setAuthorAvatar(post.author.avatar);
String sdate = post.get("created_at").value; display.setDate(post.date);
ZonedDateTime date = ZonedDateTime.parse(sdate); display.setTime(post.time);
date = date.withZoneSameInstant(ZoneId.systemDefault());
display.setDate(DATE_FORMAT.format(date));
display.setTime(TIME_FORMAT.format(date));
String[][] emojiUrls = new String[emojis.size()][]; display.setEmojiUrls(post.emojiUrls);
for (int o = 0; o < emojiUrls.length; ++o) {
Tree<String> emoji = emojis.get(o);
emojiUrls[o] = new String[2];
emojiUrls[o][0] = emoji.get("shortcode").value;
emojiUrls[o][1] = emoji.get("url").value;
}
display.setEmojiUrls(emojiUrls);
display.setHtml(post.get("content").value); display.setHtml(post.text);
boolean f = post.get("favourited").value.equals("true"); display.setFavourited(post.favourited);
boolean b = post.get("reblogged").value.equals("true"); display.setBoosted(post.boosted);
display.setFavourited(f);
display.setBoosted(b);
if (media.size() > 0) if (post.attachments.length > 0)
{ {
Tree<String> first = media.get(0); post.attachments[0].resolveImage();
String u1 = first.get("remote_url").value; display.setMediaPreview(post.attachments[0].image);
String u2 = first.get("text_url").value; }
String u3 = first.get("url").value; else display.setMediaPreview(null);
String purl = u1 != null ? u1 : u2 != null ? u2 : u3;
display.setMediaPreview(ImageApi.remote(purl));
}
else display.setMediaPreview(null);
String html = post.get("content").value; post.resolveApproximateText();
setTitle(TimelineWindow.textApproximation(html)); this.setTitle(post.approximateText);
display.resetFocus(); display.resetFocus();
repaint(); repaint();
} }
public void
readEntity(Tree<String> post)
{
use(new Post(post));
}
public void public void
openAuthorProfile() openAuthorProfile()
{ {
Tree<String> post = this.post;
Tree<String> boosted = post.get("reblog");
if (boosted.size() > 0) post = boosted;
TimelineWindow w = new TimelineWindow(primaire); TimelineWindow w = new TimelineWindow(primaire);
w.showAuthorPosts(post.get("account").get("id").value); w.showAuthorPosts(post.author.numId);
w.showLatestPage(); w.showLatestPage();
w.setLocationRelativeTo(this); w.setLocationRelativeTo(this);
w.setVisible(true); w.setVisible(true);
@ -145,11 +135,7 @@ PostWindow extends JFrame {
public void public void
favourite(boolean favourited) favourite(boolean favourited)
{ {
Tree<String> post = this.post; display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
Tree<String> boosted = post.get("reblog");
if (boosted.size() > 0) post = boosted;
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
display.setFavouriteBoostEnabled(false); display.setFavouriteBoostEnabled(false);
display.paintImmediately(display.getBounds()); display.paintImmediately(display.getBounds());
RequestListener handler = new RequestListener() { RequestListener handler = new RequestListener() {
@ -178,13 +164,11 @@ PostWindow extends JFrame {
public void public void
requestSucceeded(Tree<String> json) requestSucceeded(Tree<String> json)
{ {
String n = Boolean.toString(favourited); PostWindow.this.post.favourited = favourited;
PostWindow.this.post.get("favourited").value = n;
} }
}; };
String postId = post.get("id").value; api.setPostFavourited(post.id, favourited, handler);
api.setPostFavourited(postId, favourited, handler);
display.setCursor(null); display.setCursor(null);
display.setFavouriteBoostEnabled(true); display.setFavouriteBoostEnabled(true);
display.repaint(); display.repaint();
@ -193,10 +177,6 @@ PostWindow extends JFrame {
public void public void
boost(boolean boosted) boost(boolean boosted)
{ {
Tree<String> post = this.post;
Tree<String> boosted2 = post.get("reblog");
if (boosted2.size() > 0) post = boosted2;
display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
display.setFavouriteBoostEnabled(false); display.setFavouriteBoostEnabled(false);
display.paintImmediately(display.getBounds()); display.paintImmediately(display.getBounds());
@ -226,13 +206,11 @@ PostWindow extends JFrame {
public void public void
requestSucceeded(Tree<String> json) requestSucceeded(Tree<String> json)
{ {
String n = Boolean.toString(boosted); PostWindow.this.post.boosted = boosted;
PostWindow.this.post.get("reblogged").value = n;
} }
}; };
String postId = post.get("id").value; api.setPostBoosted(post.id, boosted, handler);
api.setPostBoosted(postId, boosted, handler);
display.setCursor(null); display.setCursor(null);
display.setFavouriteBoostEnabled(true); display.setFavouriteBoostEnabled(true);
display.repaint(); display.repaint();
@ -241,66 +219,28 @@ PostWindow extends JFrame {
public void public void
reply() reply()
{ {
Tree<String> post = this.post; String ownId = api.getAccountDetails().get("id").value;
Tree<String> boosted = post.get("reblog"); Composition c = Composition.reply(this.post, ownId);
if (boosted.size() > 0) post = boosted;
String authorId = post.get("account").get("acct").value;
String postId = post.get("id").value;
String cw = post.get("spoiler_text").value;
String id1 = post.get("account").get("id").value;
String id2 = api.getAccountDetails().get("id").value;
String vs = post.get("visibility").value;
PostVisibility v = null;
if (vs.equals("public")) v = PostVisibility.PUBLIC;
if (vs.equals("unlisted")) v = PostVisibility.UNLISTED;
if (vs.equals("private")) v = PostVisibility.FOLLOWERS;
if (vs.equals("direct")) v = PostVisibility.MENTIONED;
Composition c = new Composition();
c.contentWarning = cw;
c.text = id1.equals(id2) ? "" : "@" + authorId + " ";
c.visibility = v;
c.replyToPostId = postId;
ComposeWindow w = primaire.getComposeWindow(); ComposeWindow w = primaire.getComposeWindow();
w.setComposition(c);
w.setLocation(getX(), getY() + 100); w.setLocation(getX(), getY() + 100);
w.setVisible(true); w.setVisible(true);
w.setComposition(c);
} }
public void public void
openMedia() openMedia()
{ {
Tree<String> post = this.post; display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
Tree<String> boosted = post.get("reblog");
if (boosted.size() > 0) post = boosted;
Tree<String> media = post.get("media_attachments");
Attachment[] as = new Attachment[media.size()]; for (Attachment a: post.attachments) a.resolveImage();
for (int o = 0; o < as.length; ++o)
{
Tree<String> medium = media.get(o);
String u1 = medium.get("remote_url").value;
String u2 = medium.get("text_url").value;
String u3 = medium.get("url").value;
Attachment a = as[o] = new Attachment(); ImageWindow w = new ImageWindow();
a.url = u1 != null ? u1 : u2 != null ? u2 : u3; w.setTitle("Media - " + this.getTitle());
a.type = medium.get("type").value; w.showAttachments(post.attachments);
a.description = medium.get("description").value; w.setLocationRelativeTo(null);
a.image = ImageApi.remote(a.url); w.setVisible(true);
}
ImageWindow w = primaire.getMediaWindow(); display.setCursor(null);
w.setTitle(post.get("id").value);
w.showAttachments(as);
if (!w.isVisible()) {
w.setLocationRelativeTo(null);
w.setVisible(true);
}
} }
public void public void
@ -310,69 +250,7 @@ PostWindow extends JFrame {
display.setDeleteEnabled(false); display.setDeleteEnabled(false);
display.paintImmediately(display.getBounds()); display.paintImmediately(display.getBounds());
if (redraft) api.deletePost(post.id, new RequestListener() {
{
String html = post.get("content").value;
StringBuilder b = new StringBuilder();
Tree<String> nodes;
nodes = RudimentaryHTMLParser.depthlessRead(html);
// We have to salvage whatever we can from the HTML.
for (Tree<String> node: nodes)
{
if (node.key.equals("text"))
{
b.append(node.value);
}
if (node.key.equals("emoji"))
{
b.append(":" + node.value + ":");
}
if (node.key.equals("tag"))
{
if (node.get(0).key.equals("/p"))
b.append("\n\n");
if (node.get(0).key.equals("br"))
b.append("\n");
if (node.get(0).key.equals("a")) {
b.append(node.get("href").value);
b.append(" ");
continue;
/*
* We don't omit the contents of the <a>
* which is an automatic label, but we'll
* need a non-depthless parser to omit that.
* For now prioritise not losing anything
* from our composition.
*/
}
// I think that's all. I hope.
}
}
String cw = post.get("spoiler_text").value;
String vs = post.get("visibility").value;
PostVisibility v = null;
if (vs.equals("public")) v = PostVisibility.PUBLIC;
if (vs.equals("unlisted")) v = PostVisibility.UNLISTED;
if (vs.equals("private")) v = PostVisibility.FOLLOWERS;
if (vs.equals("direct")) v = PostVisibility.MENTIONED;
String replyTo = post.get("in_reply_to_id").value;
Composition c = new Composition();
c.contentWarning = cw;
c.text = b.toString();
c.visibility = v;
c.replyToPostId = replyTo;
ComposeWindow w = primaire.getComposeWindow();
w.setLocation(getX(), getY() + 100);
w.setVisible(true);
w.setComposition(c);
}
api.deletePost(post.get("id").value, new RequestListener() {
public void public void
connectionFailed(IOException eIo) connectionFailed(IOException eIo)
@ -399,6 +277,15 @@ PostWindow extends JFrame {
requestSucceeded(Tree<String> json) requestSucceeded(Tree<String> json)
{ {
setVisible(false); setVisible(false);
if (redraft)
{
Composition c = Composition.recover(json);
ComposeWindow w = new ComposeWindow(primaire);
w.setComposition(c);
w.setLocation(getX(), getY() + 100);
w.setVisible(true);
}
} }
}); });
@ -412,35 +299,20 @@ PostWindow extends JFrame {
public void public void
copyPostId() copyPostId()
{ {
Tree<String> post = this.post; ClipboardApi.serve(post.id);
Tree<String> reblogged = post.get("reblog");
if (reblogged.size() > 0) post = reblogged;
ClipboardApi.serve(post.get("id").value);
} }
public void public void
copyPostLink() copyPostLink()
{ {
Tree<String> post = this.post; ClipboardApi.serve(post.uri);
Tree<String> reblogged = post.get("reblog");
if (reblogged.size() > 0) post = reblogged;
String url = post.get("url").value;
if (url == null) url = post.get("uri").value;
ClipboardApi.serve(url);
} }
public void public void
openReplies() openReplies()
{ {
Tree<String> post = this.post;
Tree<String> boosted = post.get("reblog");
if (boosted.size() > 0) post = boosted;
RepliesWindow w = new RepliesWindow(primaire, this); RepliesWindow w = new RepliesWindow(primaire, this);
w.showFor(post.get("id").value); w.showFor(post.id);
w.setLocation(getX(), getY() + 100); w.setLocation(getX(), getY() + 100);
w.setVisible(true); w.setVisible(true);
} }

5
RepliesWindow.java Executable file → Normal file
View File

@ -267,8 +267,9 @@ implements TreeSelectionListener {
public String public String
toString() toString()
{ {
String html = post.get("content").value; Post post = new Post(this.post);
return TimelineWindow.textApproximation(html); post.resolveApproximateText();
return post.approximateText;
} }
// -=%=- // -=%=-

0
RequestListener.java Executable file → Normal file
View File

0
RichTextPane.java Executable file → Normal file
View File

0
RudimentaryHTMLParser.java Executable file → Normal file
View File

209
TimelineWindow.java Executable file → Normal file
View File

@ -91,64 +91,42 @@ implements ActionListener {
// ---%-@-%--- // ---%-@-%---
public void public void
readEntity(Tree<String> postEntityArray) use(TimelinePage page)
{ {
page.posts = postEntityArray; assert page != null;
this.page = page;
List<PostPreviewComponent> previews; List<PostPreviewComponent> previews;
previews = display.getPostPreviews(); previews = display.getPostPreviews();
int available = postEntityArray.size(); int available = page.posts.length;
int max = previews.size(); int max = previews.size();
assert available <= max; assert available <= max;
for (int o = 0; o < available; ++o) for (int o = 0; o < available; ++o)
{ {
PostPreviewComponent c = previews.get(o); PostPreviewComponent preview = previews.get(o);
Tree<String> p = postEntityArray.get(o); Post post = page.posts[o];
Tree<String> a = p.get("account");
String an = a.get("display_name").value; preview.setTopLeft(post.author.name);
if (an.isEmpty()) an = a.get("username").value; if (post.boostedPost != null)
c.setTopLeft(an); {
String s = "boosted by " + post.author.name;
Tree<String> boosted = p.get("reblog"); preview.setTopLeft(s);
if (boosted.size() > 0) { post = post.boostedPost;
c.setTopLeft("boosted by " + an);
p = boosted;
a = p.get("account");
} }
String f = ""; String flags = "";
if (p.get("media_attachments").size() > 0) f += "a"; if (post.attachments.length > 0) flags += "a";
String t = ""; post.resolveRelativeTime();
try { preview.setTopRight(flags + " " + post.relativeTime);
String jv = p.get("created_at").value;
ZonedDateTime pv = ZonedDateTime.parse(jv);
ZoneId z = ZoneId.systemDefault();
pv = pv.withZoneSameInstant(z);
ZonedDateTime now = ZonedDateTime.now(); post.resolveApproximateText();
long d = ChronoUnit.SECONDS.between(pv, now); if (post.contentWarning != null)
long s = Math.abs(d); preview.setBottom("(" + post.contentWarning + ")");
if (s < 30) t = "now"; else
else if (s < 60) t = d + "s"; preview.setBottom(post.approximateText);
else if (s < 3600) t = (d / 60) + "m";
else if (s < 86400) t = (d / 3600) + "h";
else t = (d / 86400) + "d";
}
catch (DateTimeParseException eDt) {
assert false;
}
c.setTopRight(f + " " + t);
String html = p.get("content").value;
String cw = p.get("spoiler_text").value;
if (!cw.isEmpty())
c.setBottom("(" + cw + ")");
else
c.setBottom(textApproximation(html));
} }
for (int o = available; o < max; ++o) for (int o = available; o < max; ++o)
{ {
@ -159,6 +137,16 @@ implements ActionListener {
display.setNextPageAvailable(full); display.setNextPageAvailable(full);
display.setPreviousPageAvailable(true); display.setPreviousPageAvailable(true);
display.resetFocus(); display.resetFocus();
}
public void
readEntity(Tree<String> postEntityArray)
{
TimelinePage page = new TimelinePage(postEntityArray);
page.type = this.page.type;
page.accountNumId = this.page.accountNumId;
page.listId = this.page.listId;
use(page);
} }
public void public void
@ -168,9 +156,8 @@ implements ActionListener {
if (!showingLatest) if (!showingLatest)
{ {
assert page.posts != null; assert page.posts != null;
assert page.posts.size() != 0; assert page.posts.length != 0;
Tree<String> first = page.posts.get(0); firstId = page.posts[0].id;
firstId = first.get("id").value;
} }
display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
@ -204,7 +191,8 @@ implements ActionListener {
public void public void
requestSucceeded(Tree<String> json) requestSucceeded(Tree<String> json)
{ {
if (json.size() < PREVIEW_COUNT) { if (json.size() < PREVIEW_COUNT)
{
showLatestPage(); showLatestPage();
return; return;
} }
@ -264,9 +252,8 @@ implements ActionListener {
showNextPage() showNextPage()
{ {
assert page.posts != null; assert page.posts != null;
assert page.posts.size() != 0; assert page.posts.length != 0;
Tree<String> last = page.posts.get(page.posts.size() - 1); String lastId = page.posts[page.posts.length - 1].id;
String lastId = last.get("id").value;
display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
api.getTimelinePage( api.getTimelinePage(
@ -299,7 +286,8 @@ implements ActionListener {
public void public void
requestSucceeded(Tree<String> json) requestSucceeded(Tree<String> json)
{ {
if (json.size() == 0) { if (json.size() == 0)
{
// We should probably say something // We should probably say something
// to the user here? For now, we // to the user here? For now, we
// quietly cancel. // quietly cancel.
@ -319,9 +307,8 @@ implements ActionListener {
showPreviousPage() showPreviousPage()
{ {
assert page.posts != null; assert page.posts != null;
assert page.posts.size() != 0; assert page.posts.length != 0;
Tree<String> first = page.posts.get(0); String firstId = page.posts[0].id;
String firstId = first.get("id").value;
display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
api.getTimelinePage( api.getTimelinePage(
@ -354,7 +341,8 @@ implements ActionListener {
public void public void
requestSucceeded(Tree<String> json) requestSucceeded(Tree<String> json)
{ {
if (json.size() < PREVIEW_COUNT) { if (json.size() < PREVIEW_COUNT)
{
showLatestPage(); showLatestPage();
return; return;
} }
@ -368,19 +356,19 @@ implements ActionListener {
display.setCursor(null); display.setCursor(null);
} }
public void public void
setTimelineType(TimelineType type) setTimelineType(TimelineType type)
{ {
assert type != TimelineType.LIST;
assert type != TimelineType.PROFILE;
page.type = type; page.type = type;
page.accountNumId = null; page.accountNumId = null;
setTitle(toString(type) + " timeline - JKomasto");
String s1 = type.toString(); String f = type.toString().toLowerCase();
s1 = s1.charAt(0) + s1.substring(1).toLowerCase(); display.setBackgroundImage(ImageApi.local(f));
setTitle(s1 + " - JKomasto"); display.repaint();
String s2 = type.toString().toLowerCase();
display.setBackgroundImage(ImageApi.local(s2));
} }
public TimelineType public TimelineType
@ -391,19 +379,20 @@ implements ActionListener {
{ {
assert authorNumId != null; assert authorNumId != null;
page.type = TimelineType.FEDERATED; page.type = TimelineType.PROFILE;
page.accountNumId = authorNumId; page.accountNumId = authorNumId;
setTitle(authorNumId + " - JKomasto"); setTitle(authorNumId + " - JKomasto");
display.setBackgroundImage(ImageApi.local("profile")); display.setBackgroundImage(ImageApi.local("profile"));
display.repaint();
} }
public void public void
openOwnProfile() openOwnProfile()
{ {
Tree<String> accountDetails = api.getAccountDetails(); Tree<String> accountDetails = api.getAccountDetails();
assert accountDetails != null;
String id = accountDetails.get("id").value; String id = accountDetails.get("id").value;
assert id != null;
TimelineWindow w = new TimelineWindow(primaire); TimelineWindow w = new TimelineWindow(primaire);
w.showAuthorPosts(id); w.showAuthorPosts(id);
@ -463,7 +452,8 @@ implements ActionListener {
api.getAccounts(query, handler); api.getAccounts(query, handler);
if (handler.json == null) return; if (handler.json == null) return;
if (handler.json.size() == 0) { if (handler.json.size() == 0)
{
JOptionPane.showMessageDialog( JOptionPane.showMessageDialog(
this, this,
"There were no results from the query.. ☹️" "There were no results from the query.. ☹️"
@ -508,6 +498,12 @@ implements ActionListener {
break; break;
} }
} }
if (id == null) return;
/*
* It seems like this can happen if someone
* presses escape out of the confirm dialog.
* I don't know why that doesn't map to cancel.
*/
} }
TimelineWindow w = new TimelineWindow(primaire); TimelineWindow w = new TimelineWindow(primaire);
@ -522,18 +518,19 @@ implements ActionListener {
public void public void
previewSelected(int index) previewSelected(int index)
{ {
if (index > page.posts.size()) return; if (index > page.posts.length) return;
Tree<String> post = page.posts.get(index - 1); Post post = page.posts[index - 1];
primaire.getAutoViewWindow().readEntity(post); primaire.getAutoViewWindow().use(post);
} }
public void public void
previewOpened(int index) previewOpened(int index)
{ {
if (index > page.posts.size()) return; if (index > page.posts.length) return;
Tree<String> post = page.posts.get(index - 1); Post post = page.posts[index - 1];
PostWindow w = new PostWindow(primaire); PostWindow w = new PostWindow(primaire);
w.readEntity(post); w.use(post);
w.setLocationRelativeTo(this); w.setLocationRelativeTo(this);
w.setVisible(true); w.setVisible(true);
} }
@ -578,8 +575,10 @@ implements ActionListener {
} }
if (src == openNotifications) if (src == openNotifications)
{ {
NotificationsWindow w = primaire.getNotificationsWindow(); NotificationsWindow w =
if (!w.isVisible()) { primaire.getNotificationsWindow();
if (!w.isVisible())
{
w.setLocationByPlatform(true); w.setLocationByPlatform(true);
w.setVisible(true); w.setVisible(true);
} }
@ -601,54 +600,16 @@ implements ActionListener {
} }
} }
// - -%- -
public static String
textApproximation(String html)
{
StringBuilder returnee = new StringBuilder();
Tree<String> nodes = RudimentaryHTMLParser.depthlessRead(html);
if (nodes.size() == 0) return "-";
Tree<String> first = nodes.get(0);
for (Tree<String> node: nodes)
{
if (node.key.equals("tag"))
{
if (node.get(0).key.equals("br"))
returnee.append("; ");
if (node.get(0).key.equals("p") && node != first)
returnee.append("; ");
}
if (node.key.equals("emoji"))
{
returnee.append(":" + node.value + ":");
}
if (node.key.equals("text"))
{
returnee.append(node.value);
}
}
return returnee.toString();
}
// - -%- -
private static String private static String
plainify(String html) toString(TimelineType type)
{ {
// Delete all tags. switch (type)
StringBuilder b = new StringBuilder(); {
boolean in = false; case FEDERATED: return "Federated";
for (char c: html.toCharArray()) switch (c) { case LOCAL: return "Local";
case '<': in = true; break; case HOME: return "Home";
case '>': in = false; break; default: return "";
default: if (!in) b.append(c);
} }
String s = b.toString();
s = s.replaceAll("&lt;", "<");
s = s.replaceAll("&gt;", ">");
s = s.replaceAll("&nbsp;", "");
return s;
} }
// ---%-@-%--- // ---%-@-%---
@ -704,7 +665,7 @@ implements ActionListener {
setJMenuBar(menuBar); setJMenuBar(menuBar);
page = new TimelinePage(); page = new TimelinePage();
page.posts = new Tree<String>(); page.posts = new Post[0];
display = new TimelineComponent(this); display = new TimelineComponent(this);
display.setNextPageAvailable(false); display.setNextPageAvailable(false);
@ -925,6 +886,8 @@ implements
pageLabel = new JLabel("0"); pageLabel = new JLabel("0");
Box bottom = Box.createHorizontalBox(); Box bottom = Box.createHorizontalBox();
bottom.setBackground(null);
bottom.setOpaque(false);
bottom.add(Box.createGlue()); bottom.add(Box.createGlue());
bottom.add(prev); bottom.add(prev);
bottom.add(Box.createHorizontalStrut(8)); bottom.add(Box.createHorizontalStrut(8));

0
TwoToggleButton.java Executable file → Normal file
View File

0
WindowUpdater.java Executable file → Normal file
View File

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

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

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

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

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

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

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

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/favouriteUntoggled.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

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

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

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

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

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

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

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

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

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

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

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

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 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

0
notifOptions.txt Executable file → Normal file
View File

0
notifOptions.txt~ Executable file → Normal file
View File