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.event.ActionListener;
import java.awt.event.ActionEvent;
import java.awt.event.KeyListener;
import java.awt.event.KeyEvent;
import java.awt.Cursor;
import java.awt.Color;
import javax.swing.event.CaretListener;
import javax.swing.event.CaretEvent;
import cafe.biskuteri.hinoki.Tree;
import java.io.IOException;
@ -92,7 +96,7 @@ ComposeWindow extends JFrame {
ComposeWindow.this,
"Tried to submit post, failed..."
+ "\n" + json.get("error").value
+ "(HTTP error code: " + httpCode + ")"
+ "\n(HTTP error code: " + httpCode + ")"
);
}
@ -194,7 +198,7 @@ ComposeWindow extends JFrame {
class
ComposeComponent extends JPanel
implements ActionListener {
implements ActionListener, CaretListener, KeyListener {
private ComposeWindow
primaire;
@ -207,6 +211,9 @@ implements ActionListener {
private JTextField
reply, contentWarning;
private JLabel
textLength;
private JComboBox<String>
visibility;
@ -294,6 +301,42 @@ implements ActionListener {
public void
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)
@ -321,6 +364,9 @@ implements ActionListener {
top.add(cwLabel);
top.add(contentWarning);
textLength = new JLabel("0");
textLength.setFont(textLength.getFont().deriveFont(14f));
visibility = new JComboBox<>(new String[] {
"Public",
"Unlisted",
@ -335,8 +381,10 @@ implements ActionListener {
Box bottom = Box.createHorizontalBox();
bottom.add(Box.createGlue());
bottom.add(textLength);
bottom.add(Box.createHorizontalStrut(12));
bottom.add(visibility);
bottom.add(Box.createHorizontalStrut(8));
bottom.add(Box.createHorizontalStrut(12));
bottom.add(submit);
text = new JTextArea();
@ -344,6 +392,8 @@ implements ActionListener {
text.setWrapStyleWord(true);
text.setFont(text.getFont().deriveFont(16f));
text.setBorder(bc);
text.addCaretListener(this);
text.addKeyListener(this);
setLayout(new BorderLayout(0, 8));
add(top, BorderLayout.NORTH);

24
ImageApi.java Executable file → Normal file
View File

@ -11,8 +11,8 @@ ImageApi {
public static Image
local(String name)
{
String path = "/graphics/" + name + ".png";
URL url = ImageApi.class.getResource(name);
String path = "graphics/" + name + ".png";
URL url = ImageApi.class.getResource(path);
if (url == null) return null;
return new ImageIcon(url).getImage();
}
@ -20,12 +20,28 @@ ImageApi {
public static Image
remote(String urlr)
{
try {
try
{
URL url = new URL(urlr);
Toolkit TK = Toolkit.getDefaultToolkit();
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;
}
}

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.JPanel;
import javax.swing.JComponent;
import javax.swing.ImageIcon;
import java.awt.Dimension;
import java.awt.BorderLayout;
import java.awt.Cursor;
import java.awt.Image;
import java.awt.FontMetrics;
import java.util.List;
import java.util.Locale;
import java.text.BreakIterator;
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;
@ -126,7 +135,8 @@ TimelineType {
FEDERATED,
LOCAL,
HOME,
LIST
LIST,
PROFILE
}
@ -154,9 +164,22 @@ TimelinePage {
public String
accountNumId, listId;
public Tree<String>
public Post[]
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
Attachment {
public String
id;
public String
type;
@ -190,6 +517,34 @@ Attachment {
public 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
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)
{
Tree<String> post = t.get("status");
String pid, phtml, ptext;
pid = post.get("id").value;
phtml = post.get("content").value;
ptext = TimelineWindow.textApproximation(phtml);
n.postId = pid;
n.postText = ptext;
Post post = new Post(t.get("status"));
post.resolveApproximateText();
n.postId = post.id;
n.postText = post.approximateText;
}
notifications.add(n);

274
PostWindow.java Executable file → Normal file
View File

@ -49,8 +49,9 @@ PostWindow extends JFrame {
private MastodonApi
api;
private Tree<String>
post;
private Post
post,
wrapperPost;
// - -%- -
@ -65,78 +66,67 @@ PostWindow extends JFrame {
// ---%-@-%---
public void
readEntity(Tree<String> post)
{
this.post = post;
public void
use(Post post)
{
assert post != null;
Tree<String> boosted = post.get("reblog");
if (boosted.size() > 0) post = boosted;
if (post.boostedPost != null)
{
wrapperPost = post;
this.post = post.boostedPost;
post = post.boostedPost;
}
else
{
wrapperPost = null;
this.post = post;
}
Tree<String> author = post.get("account");
Tree<String> emojis = post.get("emojis");
Tree<String> media = post.get("media_attachments");
display.setAuthorName(post.author.name);
display.setAuthorId(post.author.id);
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 aid = post.author.numId;
display.setDeleteEnabled(aid.equals(oid));
String avurl = author.get("avatar").value;
display.setAuthorAvatar(ImageApi.remote(avurl));
post.author.resolveAvatar();
display.setAuthorAvatar(post.author.avatar);
String sdate = post.get("created_at").value;
ZonedDateTime date = ZonedDateTime.parse(sdate);
date = date.withZoneSameInstant(ZoneId.systemDefault());
display.setDate(DATE_FORMAT.format(date));
display.setTime(TIME_FORMAT.format(date));
display.setDate(post.date);
display.setTime(post.time);
String[][] emojiUrls = new String[emojis.size()][];
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.setEmojiUrls(post.emojiUrls);
display.setHtml(post.get("content").value);
boolean f = post.get("favourited").value.equals("true");
boolean b = post.get("reblogged").value.equals("true");
display.setFavourited(f);
display.setBoosted(b);
display.setHtml(post.text);
display.setFavourited(post.favourited);
display.setBoosted(post.boosted);
if (media.size() > 0)
{
Tree<String> first = media.get(0);
String u1 = first.get("remote_url").value;
String u2 = first.get("text_url").value;
String u3 = first.get("url").value;
String purl = u1 != null ? u1 : u2 != null ? u2 : u3;
display.setMediaPreview(ImageApi.remote(purl));
}
else display.setMediaPreview(null);
if (post.attachments.length > 0)
{
post.attachments[0].resolveImage();
display.setMediaPreview(post.attachments[0].image);
}
else display.setMediaPreview(null);
String html = post.get("content").value;
setTitle(TimelineWindow.textApproximation(html));
post.resolveApproximateText();
this.setTitle(post.approximateText);
display.resetFocus();
repaint();
}
public void
readEntity(Tree<String> post)
{
use(new Post(post));
}
public void
openAuthorProfile()
{
Tree<String> post = this.post;
Tree<String> boosted = post.get("reblog");
if (boosted.size() > 0) post = boosted;
TimelineWindow w = new TimelineWindow(primaire);
w.showAuthorPosts(post.get("account").get("id").value);
w.showAuthorPosts(post.author.numId);
w.showLatestPage();
w.setLocationRelativeTo(this);
w.setVisible(true);
@ -145,11 +135,7 @@ PostWindow extends JFrame {
public void
favourite(boolean favourited)
{
Tree<String> post = this.post;
Tree<String> boosted = post.get("reblog");
if (boosted.size() > 0) post = boosted;
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
display.setFavouriteBoostEnabled(false);
display.paintImmediately(display.getBounds());
RequestListener handler = new RequestListener() {
@ -178,13 +164,11 @@ PostWindow extends JFrame {
public void
requestSucceeded(Tree<String> json)
{
String n = Boolean.toString(favourited);
PostWindow.this.post.get("favourited").value = n;
PostWindow.this.post.favourited = favourited;
}
};
String postId = post.get("id").value;
api.setPostFavourited(postId, favourited, handler);
api.setPostFavourited(post.id, favourited, handler);
display.setCursor(null);
display.setFavouriteBoostEnabled(true);
display.repaint();
@ -193,10 +177,6 @@ PostWindow extends JFrame {
public void
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.setFavouriteBoostEnabled(false);
display.paintImmediately(display.getBounds());
@ -226,13 +206,11 @@ PostWindow extends JFrame {
public void
requestSucceeded(Tree<String> json)
{
String n = Boolean.toString(boosted);
PostWindow.this.post.get("reblogged").value = n;
PostWindow.this.post.boosted = boosted;
}
};
String postId = post.get("id").value;
api.setPostBoosted(postId, boosted, handler);
api.setPostBoosted(post.id, boosted, handler);
display.setCursor(null);
display.setFavouriteBoostEnabled(true);
display.repaint();
@ -241,66 +219,28 @@ PostWindow extends JFrame {
public void
reply()
{
Tree<String> post = this.post;
Tree<String> boosted = post.get("reblog");
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;
String ownId = api.getAccountDetails().get("id").value;
Composition c = Composition.reply(this.post, ownId);
ComposeWindow w = primaire.getComposeWindow();
w.setComposition(c);
w.setLocation(getX(), getY() + 100);
w.setVisible(true);
w.setComposition(c);
}
public void
openMedia()
{
Tree<String> post = this.post;
Tree<String> boosted = post.get("reblog");
if (boosted.size() > 0) post = boosted;
Tree<String> media = post.get("media_attachments");
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
Attachment[] as = new Attachment[media.size()];
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;
for (Attachment a: post.attachments) a.resolveImage();
Attachment a = as[o] = new Attachment();
a.url = u1 != null ? u1 : u2 != null ? u2 : u3;
a.type = medium.get("type").value;
a.description = medium.get("description").value;
a.image = ImageApi.remote(a.url);
}
ImageWindow w = new ImageWindow();
w.setTitle("Media - " + this.getTitle());
w.showAttachments(post.attachments);
w.setLocationRelativeTo(null);
w.setVisible(true);
ImageWindow w = primaire.getMediaWindow();
w.setTitle(post.get("id").value);
w.showAttachments(as);
if (!w.isVisible()) {
w.setLocationRelativeTo(null);
w.setVisible(true);
}
display.setCursor(null);
}
public void
@ -310,69 +250,7 @@ PostWindow extends JFrame {
display.setDeleteEnabled(false);
display.paintImmediately(display.getBounds());
if (redraft)
{
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() {
api.deletePost(post.id, new RequestListener() {
public void
connectionFailed(IOException eIo)
@ -399,6 +277,15 @@ PostWindow extends JFrame {
requestSucceeded(Tree<String> json)
{
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
copyPostId()
{
Tree<String> post = this.post;
Tree<String> reblogged = post.get("reblog");
if (reblogged.size() > 0) post = reblogged;
ClipboardApi.serve(post.get("id").value);
ClipboardApi.serve(post.id);
}
public void
copyPostLink()
{
Tree<String> post = this.post;
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);
ClipboardApi.serve(post.uri);
}
public void
openReplies()
{
Tree<String> post = this.post;
Tree<String> boosted = post.get("reblog");
if (boosted.size() > 0) post = boosted;
RepliesWindow w = new RepliesWindow(primaire, this);
w.showFor(post.get("id").value);
w.showFor(post.id);
w.setLocation(getX(), getY() + 100);
w.setVisible(true);
}

5
RepliesWindow.java Executable file → Normal file
View File

@ -267,8 +267,9 @@ implements TreeSelectionListener {
public String
toString()
{
String html = post.get("content").value;
return TimelineWindow.textApproximation(html);
Post post = new Post(this.post);
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
readEntity(Tree<String> postEntityArray)
{
page.posts = postEntityArray;
public void
use(TimelinePage page)
{
assert page != null;
this.page = page;
List<PostPreviewComponent> previews;
List<PostPreviewComponent> previews;
previews = display.getPostPreviews();
int available = postEntityArray.size();
int available = page.posts.length;
int max = previews.size();
assert available <= max;
for (int o = 0; o < available; ++o)
{
PostPreviewComponent c = previews.get(o);
Tree<String> p = postEntityArray.get(o);
Tree<String> a = p.get("account");
PostPreviewComponent preview = previews.get(o);
Post post = page.posts[o];
String an = a.get("display_name").value;
if (an.isEmpty()) an = a.get("username").value;
c.setTopLeft(an);
Tree<String> boosted = p.get("reblog");
if (boosted.size() > 0) {
c.setTopLeft("boosted by " + an);
p = boosted;
a = p.get("account");
preview.setTopLeft(post.author.name);
if (post.boostedPost != null)
{
String s = "boosted by " + post.author.name;
preview.setTopLeft(s);
post = post.boostedPost;
}
String f = "";
if (p.get("media_attachments").size() > 0) f += "a";
String t = "";
try {
String jv = p.get("created_at").value;
ZonedDateTime pv = ZonedDateTime.parse(jv);
ZoneId z = ZoneId.systemDefault();
pv = pv.withZoneSameInstant(z);
String flags = "";
if (post.attachments.length > 0) flags += "a";
post.resolveRelativeTime();
preview.setTopRight(flags + " " + post.relativeTime);
ZonedDateTime now = ZonedDateTime.now();
long d = ChronoUnit.SECONDS.between(pv, now);
long s = Math.abs(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";
}
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));
post.resolveApproximateText();
if (post.contentWarning != null)
preview.setBottom("(" + post.contentWarning + ")");
else
preview.setBottom(post.approximateText);
}
for (int o = available; o < max; ++o)
{
@ -159,6 +137,16 @@ implements ActionListener {
display.setNextPageAvailable(full);
display.setPreviousPageAvailable(true);
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
@ -168,9 +156,8 @@ implements ActionListener {
if (!showingLatest)
{
assert page.posts != null;
assert page.posts.size() != 0;
Tree<String> first = page.posts.get(0);
firstId = first.get("id").value;
assert page.posts.length != 0;
firstId = page.posts[0].id;
}
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
@ -204,7 +191,8 @@ implements ActionListener {
public void
requestSucceeded(Tree<String> json)
{
if (json.size() < PREVIEW_COUNT) {
if (json.size() < PREVIEW_COUNT)
{
showLatestPage();
return;
}
@ -264,9 +252,8 @@ implements ActionListener {
showNextPage()
{
assert page.posts != null;
assert page.posts.size() != 0;
Tree<String> last = page.posts.get(page.posts.size() - 1);
String lastId = last.get("id").value;
assert page.posts.length != 0;
String lastId = page.posts[page.posts.length - 1].id;
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
api.getTimelinePage(
@ -299,7 +286,8 @@ implements ActionListener {
public void
requestSucceeded(Tree<String> json)
{
if (json.size() == 0) {
if (json.size() == 0)
{
// We should probably say something
// to the user here? For now, we
// quietly cancel.
@ -319,9 +307,8 @@ implements ActionListener {
showPreviousPage()
{
assert page.posts != null;
assert page.posts.size() != 0;
Tree<String> first = page.posts.get(0);
String firstId = first.get("id").value;
assert page.posts.length != 0;
String firstId = page.posts[0].id;
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
api.getTimelinePage(
@ -354,7 +341,8 @@ implements ActionListener {
public void
requestSucceeded(Tree<String> json)
{
if (json.size() < PREVIEW_COUNT) {
if (json.size() < PREVIEW_COUNT)
{
showLatestPage();
return;
}
@ -368,19 +356,19 @@ implements ActionListener {
display.setCursor(null);
}
public void
setTimelineType(TimelineType type)
{
assert type != TimelineType.LIST;
assert type != TimelineType.PROFILE;
page.type = type;
page.accountNumId = null;
setTitle(toString(type) + " timeline - JKomasto");
String s1 = type.toString();
s1 = s1.charAt(0) + s1.substring(1).toLowerCase();
setTitle(s1 + " - JKomasto");
String s2 = type.toString().toLowerCase();
display.setBackgroundImage(ImageApi.local(s2));
String f = type.toString().toLowerCase();
display.setBackgroundImage(ImageApi.local(f));
display.repaint();
}
public TimelineType
@ -391,19 +379,20 @@ implements ActionListener {
{
assert authorNumId != null;
page.type = TimelineType.FEDERATED;
page.type = TimelineType.PROFILE;
page.accountNumId = authorNumId;
setTitle(authorNumId + " - JKomasto");
display.setBackgroundImage(ImageApi.local("profile"));
display.repaint();
}
public void
openOwnProfile()
{
Tree<String> accountDetails = api.getAccountDetails();
assert accountDetails != null;
String id = accountDetails.get("id").value;
assert id != null;
TimelineWindow w = new TimelineWindow(primaire);
w.showAuthorPosts(id);
@ -463,7 +452,8 @@ implements ActionListener {
api.getAccounts(query, handler);
if (handler.json == null) return;
if (handler.json.size() == 0) {
if (handler.json.size() == 0)
{
JOptionPane.showMessageDialog(
this,
"There were no results from the query.. ☹️"
@ -508,6 +498,12 @@ implements ActionListener {
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);
@ -522,18 +518,19 @@ implements ActionListener {
public void
previewSelected(int index)
{
if (index > page.posts.size()) return;
Tree<String> post = page.posts.get(index - 1);
primaire.getAutoViewWindow().readEntity(post);
if (index > page.posts.length) return;
Post post = page.posts[index - 1];
primaire.getAutoViewWindow().use(post);
}
public void
previewOpened(int index)
{
if (index > page.posts.size()) return;
Tree<String> post = page.posts.get(index - 1);
if (index > page.posts.length) return;
Post post = page.posts[index - 1];
PostWindow w = new PostWindow(primaire);
w.readEntity(post);
w.use(post);
w.setLocationRelativeTo(this);
w.setVisible(true);
}
@ -578,8 +575,10 @@ implements ActionListener {
}
if (src == openNotifications)
{
NotificationsWindow w = primaire.getNotificationsWindow();
if (!w.isVisible()) {
NotificationsWindow w =
primaire.getNotificationsWindow();
if (!w.isVisible())
{
w.setLocationByPlatform(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
plainify(String html)
toString(TimelineType type)
{
// 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);
switch (type)
{
case FEDERATED: return "Federated";
case LOCAL: return "Local";
case HOME: return "Home";
default: return "";
}
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);
page = new TimelinePage();
page.posts = new Tree<String>();
page.posts = new Post[0];
display = new TimelineComponent(this);
display.setNextPageAvailable(false);
@ -925,6 +886,8 @@ implements
pageLabel = new JLabel("0");
Box bottom = Box.createHorizontalBox();
bottom.setBackground(null);
bottom.setOpaque(false);
bottom.add(Box.createGlue());
bottom.add(prev);
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