Added partially-finished profile window.

Implemented hierarchical HTML parser.
This commit is contained in:
Snowyfox 2022-05-13 10:32:11 -04:00
parent 775faa0bcc
commit 71b9c496c4
41 changed files with 975 additions and 109 deletions

328
BasicHTMLParser.java Normal file
View File

@ -0,0 +1,328 @@
import java.util.List;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Locale;
import java.text.BreakIterator;
import cafe.biskuteri.hinoki.Tree;
interface
BasicHTMLParser {
public static Tree<String>
parse(String html)
{
List<String> segments;
segments = distinguishTagsFromPcdata(html);
segments = evaluateHtmlEscapes(segments);
Tree<String> document;
document = toNodes(segments);
document = distinguishEmojisFromText(document);
document = hierarchise(document);
return document;
}
// - -%- -
private static List<String>
distinguishTagsFromPcdata(String html)
{
List<String> returnee = new ArrayList<>();
StringBuilder segment = new StringBuilder();
boolean inTag = false;
for (char c: html.toCharArray())
{
if (c == '<')
{
String addee = empty(segment);
if (!addee.isEmpty()) returnee.add(addee);
inTag = true;
segment.append(c);
}
else if (c == '>')
{
assert inTag;
assert segment.length() > 0;
segment.append(c);
returnee.add(empty(segment));
inTag = false;
}
else
{
segment.append(c);
}
}
String addee = empty(segment);
if (!addee.isEmpty()) returnee.add(addee);
return returnee;
}
private static List<String>
evaluateHtmlEscapes(List<String> strings)
{
List<String> returnee = new ArrayList<>();
for (String string: strings)
{
StringBuilder whole = new StringBuilder();
StringBuilder part = new StringBuilder();
boolean inEscape = false;
for (char c: string.toCharArray())
{
if (inEscape && c == ';')
{
part.append(c);
inEscape = false;
String v = empty(part);
if (v.equals("&lt;")) part.append('<');
if (v.equals("&gt;")) part.append('>');
if (v.equals("&amp;")) part.append('&');
if (v.equals("&quot;")) part.append('"');
if (v.equals("&apos;")) part.append('\'');
if (v.equals("&#39;")) part.append('\'');
}
else if (!inEscape && c == '&')
{
String v = empty(part);
if (!v.isEmpty()) whole.append(v);
part.append(c);
inEscape = true;
}
else
{
part.append(c);
}
}
String v = empty(part);
if (!v.isEmpty()) whole.append(v);
returnee.add(empty(whole));
}
return returnee;
}
private static Tree<String>
toNodes(List<String> segments)
{
Tree<String> returnee = new Tree<String>();
for (String segment: segments)
{
boolean isTag = segment.startsWith("<");
Tree<String> node = new Tree<String>();
if (!isTag)
{
node.key = "text";
node.value = segment;
returnee.add(node);
continue;
}
node.key = "tag";
String key = null, value = null;
StringBuilder b = new StringBuilder();
boolean inQuotes = false, inValue = false;
char[] chars = segment.toCharArray();
for (int o = 1; o < chars.length - 1; ++o)
{
char c = chars[o];
if (c == '"')
{
inQuotes = !inQuotes;
}
else if (inQuotes)
{
b.append(c);
}
else if (c == '=')
{
assert b.length() > 0;
key = empty(b);
inValue = true;
}
else if (Character.isWhitespace(c))
{
if (b.length() > 0)
{
if (inValue) value = empty(b);
else key = empty(b);
Tree<String> attr = new Tree<String>();
attr.key = key;
attr.value = value;
node.add(attr);
}
inValue = false;
}
else
{
b.append(c);
}
}
if (b.length() > 0)
{
if (inValue) value = empty(b);
else key = empty(b);
Tree<String> attr = new Tree<String>();
attr.key = key;
attr.value = value;
node.add(attr);
}
returnee.add(node);
}
return returnee;
}
private static Tree<String>
distinguishEmojisFromText(Tree<String> nodes)
{
Tree<String> returnee = new Tree<String>();
for (Tree<String> node: nodes)
{
if (!node.key.equals("text"))
{
returnee.add(node);
continue;
}
List<String> segments;
segments = distinguishWhitespaceFromText(node.value);
StringBuilder b = new StringBuilder();
for (String segment: segments)
{
boolean starts = segment.startsWith(":");
boolean ends = segment.endsWith(":");
if (starts && ends)
{
Tree<String> text = new Tree<String>();
text.key = "text";
text.value = empty(b);
returnee.add(text);
Tree<String> emoji = new Tree<String>();
emoji.key = "emoji";
emoji.value = segment;
returnee.add(emoji);
}
else
{
b.append(segment);
}
}
if (b.length() > 0)
{
Tree<String> text = new Tree<String>();
text.key = "text";
text.value = empty(b);
returnee.add(text);
}
}
return returnee;
}
private static Tree<String>
hierarchise(Tree<String> nodes)
{
Tree<String> root = new Tree<String>();
root.add(new Tree<>("attributes", null));
root.get(0).add(new Tree<>("html", null));
root.add(new Tree<>("children", null));
Deque<Tree<String>> parents = new LinkedList<>();
parents.push(root);
for (Tree<String> node: nodes)
{
if (node.key.equals("tag"))
{
assert node.size() > 0;
String tagName = node.get(0).key;
boolean isClosing, selfClosing;
isClosing = tagName.startsWith("/");
selfClosing = node.get("/") != null;
selfClosing |= tagName.equals("br");
if (isClosing)
{
assert parents.size() > 1;
Tree<String> parent, grandparent;
parent = parents.pop();
grandparent = parents.peek();
assert tagName.equals(
"/"
+ parent.get("attributes").get(0).key
);
grandparent.get("children").add(parent);
}
else if (selfClosing)
{
Tree<String> elem = new Tree<String>();
node.key = "attributes";
elem.add(node);
elem.add(new Tree<>("children", null));
parents.peek().get("children").add(elem);
}
else
{
Tree<String> elem = new Tree<String>();
node.key = "attributes";
elem.add(node);
elem.add(new Tree<>("children", null));
parents.push(elem);
}
}
else
{
parents.peek().get("children").add(node);
}
}
assert parents.size() == 1;
return parents.pop();
}
private static String
empty(StringBuilder b)
{
String s = b.toString();
b.delete(0, b.length());
return s;
}
private static List<String>
distinguishWhitespaceFromText(String text)
{
List<String> returnee = new ArrayList<>();
StringBuilder segment = new StringBuilder();
boolean inWhitespace = false;
for (char c: text.toCharArray())
{
boolean w = Character.isWhitespace(c);
boolean change = w ^ inWhitespace;
if (change)
{
returnee.add(empty(segment));
inWhitespace = !inWhitespace;
}
segment.append(c);
}
returnee.add(empty(segment));
return returnee;
}
}

0
ClipboardApi.java Normal file → Executable file
View File

0
ComposeWindow.java Normal file → Executable file
View File

0
ImageApi.java Normal file → Executable file
View File

0
ImageWindow.java Normal file → Executable file
View File

55
JKomasto.java Normal file → Executable file
View File

@ -436,7 +436,7 @@ Post {
{
Tree<String> emoji = emojis.get(o);
String[] mapping = emojiUrls[o] = new String[2];
mapping[0] = emoji.get("shortcode").value;
mapping[0] = ":" + emoji.get("shortcode").value + ":";
mapping[1] = emoji.get("url").value;
}
@ -467,6 +467,22 @@ Account {
public Image
avatar;
public ZonedDateTime
creationDate;
public int
followedCount,
followerCount;
public int
postCount;
public String[][]
fields;
public String
description;
// ---%-@-%---
public void
@ -501,6 +517,36 @@ Account {
name = displayName.isEmpty() ? username : displayName;
avatarUrl = entity.get("avatar").value;
creationDate =
ZonedDateTime.parse(entity.get("created_at").value)
.withZoneSameInstant(ZoneId.systemDefault());
String c1 = entity.get("following_count").value;
String c2 = entity.get("followers_count").value;
String c3 = entity.get("statuses_count").value;
try {
followedCount = (int)Double.parseDouble(c1);
followerCount = (int)Double.parseDouble(c2);
postCount = (int)Double.parseDouble(c3);
}
catch (NumberFormatException eNf) {
assert false;
}
Tree<String> fs = entity.get("fields");
fields = new String[fs.size()][];
for (int o = 0; o < fields.length; ++o)
{
Tree<String> f = fs.get(o);
String[] field = fields[o] = new String[3];
field[0] = f.get("name").value;
field[1] = f.get("value").value;
boolean v = f.get("verified_at").value != null;
field[2] = Boolean.toString(v);
}
description = entity.get("note").value;
}
}
@ -543,11 +589,10 @@ 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 = entity.get("remote_url").value;
if (url == null) url = entity.get("url").value;
url = u1 != null ? u1 : u2 != null ? u2 : u3;
id = entity.get("id").value;
type = entity.get("type").value;
description = entity.get("description").value;
}

0
KDE_Dialog_Appear.wav Normal file → Executable file
View File

0
LoginWindow.java Normal file → Executable file
View File

50
MastodonApi.java Normal file → Executable file
View File

@ -15,8 +15,10 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.FileInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
@ -427,6 +429,52 @@ MastodonApi {
catch (IOException eIo) { handler.connectionFailed(eIo); }
}
public void
uploadFile(File file, RequestListener handler)
{
assert file != null;
assert file.canRead();
String token = accessToken.get("access_token").value;
String url = instanceUrl + "/api/v1/media/";
try
{
URL endpoint = new URL(url);
HttpURLConnection conn = cast(endpoint.openConnection());
String s1 = "Bearer " + token;
conn.setRequestProperty("Authorization", s1);
conn.setDoOutput(true);
conn.setRequestMethod("POST");
String s2 = "multipart/form-data; ";
String s3 = "boundary=\"MastodonMediaUpload\"";
conn.setRequestProperty("Content-Type", s2 + s3);
conn.connect();
OutputStream ostream = conn.getOutputStream();
Writer owriter = owriter(ostream);
InputStream istream = new FileInputStream(file);
// Let's see if this works!
String s4, s5, s6;
s4 = "--MastodonMediaUpload";
s5 = "Content-Disposition: form-data; name=file";
s6 = "Content-Type: application/octet-stream";
owriter.write(s4 + "\r\n");
owriter.write(s5 + "\r\n");
owriter.write(s6 + "\r\n\r\n");
int c; while ((c = istream.read()) != -1)
ostream.write(c);
owriter.write("\r\n" + s4 + "--\r\n");
istream.close();
ostream.close();
wrapResponseInTree(conn, handler);
}
catch (IOException eIo) { handler.connectionFailed(eIo); }
}
public void
monitorTimeline(
TimelineType type, ServerSideEventsListener handler)
@ -448,7 +496,6 @@ MastodonApi {
HttpURLConnection conn = cast(endpoint.openConnection());
String s = "Bearer " + token;
conn.setRequestProperty("Authorization", s);
conn.setReadTimeout(500);
conn.connect();
int code = conn.getResponseCode();
@ -461,6 +508,7 @@ MastodonApi {
return;
}
conn.setReadTimeout(500);
Reader input = ireader(conn.getInputStream());
BufferedReader br = new BufferedReader(input);
Thread thread = Thread.currentThread();

0
NotificationsWindow.java Normal file → Executable file
View File

31
PostWindow.java Normal file → Executable file
View File

@ -125,11 +125,10 @@ PostWindow extends JFrame {
public synchronized void
openAuthorProfile()
{
TimelineWindow w = new TimelineWindow(primaire);
w.showAuthorPosts(post.author.numId);
w.showLatestPage();
w.setLocationRelativeTo(this);
w.setVisible(true);
ProfileWindow w = new ProfileWindow(primaire);
w.use(post.author);
w.setLocationRelativeTo(this);
w.setVisible(true);
}
public synchronized void
@ -251,6 +250,26 @@ PostWindow extends JFrame {
display.setDeleteEnabled(false);
display.paintImmediately(display.getBounds());
final String S1 =
"Are you sure you'd like to delete this post?\n";
final String S2 =
"Are you sure you'd like to delete this post?\n"
+ "You are redrafting, so a composition window\n"
+ "should open with its contents filled.";
JOptionPane dialog = new JOptionPane();
dialog.setMessageType(JOptionPane.QUESTION_MESSAGE);
dialog.setMessage(redraft ? S2 : S1);
dialog.setOptions(new String[] { "No", "Yes" });
String title = "Confirm delete";
dialog.createDialog(this, title).setVisible(true);
if (!dialog.getValue().equals("Yes"))
{
display.setCursor(null);
display.setDeleteEnabled(true);
display.paintImmediately(display.getBounds());
return;
}
api.deletePost(post.id, new RequestListener() {
public void
@ -409,6 +428,8 @@ implements ActionListener {
public void
setHtml(String n)
{
BasicHTMLParser.parse(n);
RichTextPane.Builder b = new RichTextPane.Builder();
Tree<String> nodes = RudimentaryHTMLParser.depthlessRead(n);
for (Tree<String> node: nodes)

427
ProfileWindow.java Executable file
View File

@ -0,0 +1,427 @@
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JLabel;
import javax.swing.JButton;
import javax.swing.JTextArea;
import javax.swing.JScrollPane;
import java.awt.Graphics;
import java.awt.Cursor;
import java.awt.Image;
import java.awt.Dimension;
import java.awt.Color;
import java.awt.Shape;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.geom.Ellipse2D;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
class
ProfileWindow extends JFrame {
private JKomasto
primaire;
private MastodonApi
api;
private Account
account;
// - -%- -
private ProfileComponent
display;
// ---%-@-%---
public void
use(Account account)
{
this.account = account;
account.resolveAvatar();
display.setAvatar(account.avatar);
display.setAccountID(account.id);
display.setDisplayName(account.name);
int n1 = account.followedCount;
int n2 = account.followerCount;
display.setFollowedAndFollowers(n1 + " & " + n2);
int n3 = account.postCount;
String hs;
if (n3 >= 1000) hs = "~" + (n3 / 1000) + "K";
else if (n3 >= 300) hs = "~" + (n3 / 100) + "00";
else hs = Integer.toString(n3);
hs += " posts since ";
switch (account.creationDate.getMonth())
{
case JANUARY: hs += "Jan"; break;
case FEBRUARY: hs += "Feb"; break;
case MARCH: hs += "Mar"; break;
case APRIL: hs += "Apr"; break;
case MAY: hs += "May"; break;
case JUNE: hs += "Jun"; break;
case JULY: hs += "Jul"; break;
case AUGUST: hs += "Aug"; break;
case SEPTEMBER: hs += "Sept"; break;
case OCTOBER: hs += "Oct"; break;
case NOVEMBER: hs += "Nov"; break;
case DECEMBER: hs += "Dec"; break;
/*
* () We're hardcoding for English right now,
* but later we need to localise properly using
* Month#getDisplayName. Right now I'm just
* finishing this component ASAP for English.
*/
}
hs += " " + account.creationDate.getYear();
display.setHistory(hs);
for (int i = 1; i <= 4; ++i)
{
if (i > account.fields.length)
{
display.setField(i, "", "");
continue;
}
String[] field = account.fields[i - 1];
display.setField(i, field[0], field[1]);
}
display.setDescription(account.description);
setTitle(account.name + " - JKomasto");
}
// - -%- -
public void
seePosts()
{
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
TimelineWindow w = new TimelineWindow(primaire);
w.showAuthorPosts(account.numId);
w.showLatestPage();
w.setLocationRelativeTo(this);
w.setVisible(true);
display.setCursor(null);
}
// ---%-@-%---
ProfileWindow(JKomasto primaire)
{
super("Profile window - JKomasto");
this.primaire = primaire;
this.api = primaire.getMastodonApi();
this.display = new ProfileComponent(this);
add(display);
pack();
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
}
}
class
ProfileComponent extends JPanel
implements ActionListener {
private ProfileWindow
primaire;
// - -5- -
private Image
avatar;
private JLabel
accountIdLabel,
accountId,
displayNameLabel,
displayName,
followedLabel,
followed,
historyLabel,
history,
field1Label,
field1,
field2Label,
field2,
field3Label,
field3,
field4Label,
field4;
private JTextArea
description;
private JScrollPane
scroll;
private JButton
seePosts;
private int
dx1, dx2, dx3, dx4, dy1, dy2, dy3, dy4;
// ---%-@-%---
public void
setAvatar(Image avatar)
{
this.avatar = avatar;
}
public void
setAccountID(String id)
{
accountId.setText(id);
}
public void
setDisplayName(String name)
{
displayName.setText(name);
}
public void
setFollowedAndFollowers(String text)
{
followed.setText(text);
}
public void
setHistory(String text)
{
history.setText(text);
}
public void
setField(int index, String name, String value)
{
assert index >= 1 && index <= 4;
JLabel label = null, field = null;
switch (index)
{
case 1: label = field1Label; field = field1; break;
case 2: label = field2Label; field = field2; break;
case 3: label = field3Label; field = field3; break;
case 4: label = field4Label; field = field4; break;
}
label.setText(name);
field.setText(value);
}
public void
setDescription(String html)
{
description.setText(html);
}
// - -%- -
public void
actionPerformed(ActionEvent eA)
{
assert eA.getSource() == seePosts;
primaire.seePosts();
}
protected void
paintComponent(Graphics g)
{
g.clearRect(0, 0, getWidth(), getHeight());
int w = getWidth(), h = getHeight();
int aw = 256;
int ah = 256;
int ax = (w - aw) / 2;
int ay = 10;
int acx = ax + (aw / 2);
int acy = ay + (ah / 2);
Shape defaultClip = g.getClip();
g.setClip(new Ellipse2D.Float(ax, ay, aw, ah));
g.drawImage(avatar, ax, ay, aw, ah, this);
g.setClip(defaultClip);
g.setColor(new Color(0, 0, 0, 50));
g.fillRect(0, acy - dy1, acx - dx1, 2);
g.fillRect(0, acy - dy2, acx - dx2, 2);
g.fillRect(0, acy + dy3, acx - dx3, 2);
g.fillRect(0, acy + dy4, acx - dx4, 2);
g.fillRect(acx + dx1, acy - dy1, w - (acx + dx1), 2);
g.fillRect(acx + dx2, acy - dy2, w - (acx + dx2), 2);
g.fillRect(acx + dx3, acy + dy3, w - (acx + dx3), 2);
g.fillRect(acx + dx4, acy + dy4, w - (acx + dx4), 2);
((java.awt.Graphics2D)g).setRenderingHint(
java.awt.RenderingHints.KEY_ANTIALIASING,
java.awt.RenderingHints.VALUE_ANTIALIAS_ON
);
}
public void
doLayout()
{
final double TAU = 2 * Math.PI;
int w = getWidth(), h = getHeight();
int aw = 256;
int ah = 256;
int ax = (w - aw) / 2;
int ay = 10;
int acx = ax + (aw / 2);
int acy = ay + (ah / 2);
dx1 = (int)((aw * 11/20) * Math.cos(TAU * 45 / 360));
dx2 = (int)((aw * 11/20) * Math.cos(TAU * 15 / 360));
dx3 = dx2;
dx4 = dx1;
dy1 = (int)((ah / 2) * Math.sin(TAU * 45 / 360));
dy2 = (int)((ah / 2) * Math.sin(TAU * 15 / 360));
dy3 = dy2;
dy4 = dy1;
FontMetrics fm = getFontMetrics(field1.getFont());
int lh = fm.getAscent() * 9 / 8;
accountIdLabel.setLocation(10, acy - dy1 - lh - 1);
accountId.setLocation(10, acy - dy1 + 1);
accountIdLabel.setSize(acx - dx1 - 16, lh);
accountId.setSize(acx - dx1 - 24, lh);
displayNameLabel.setLocation(10, acy - dy2 - lh - 1);
displayName.setLocation(10, acy - dy2 + 1);
displayNameLabel.setSize(acx - dx2 - 16, lh);
displayName.setSize(acx - dx2 - 24, lh);
followedLabel.setLocation(10, acy + dy3 - lh - 1);
followed.setLocation(10, acy + dy3 + 1);
followedLabel.setSize(acx - dx3 - 24, lh);
followed.setSize(acx - dx3 - 16, lh);
historyLabel.setLocation(10, acy + dy4 - lh - 1);
history.setLocation(10, acy + dy4 + 1);
historyLabel.setSize(acx - dx4 - 24, lh);
history.setSize(acx - dx4 - 16, lh);
field1Label.setLocation(acx + dx1 + 16, acy - dy1 - lh - 1);
field1.setLocation(acx + dx1 + 24, acy - dy1 + 1);
field1Label.setSize(w - 10 - (acy + dx1 + 16), lh);
field1.setSize(w - 10 - (acy + dx1 + 24), lh);
field2Label.setLocation(acx + dx2 + 16, acy - dy2 - lh - 1);
field2.setLocation(acx + dx2 + 24, acy - dy2 + 1);
field2Label.setSize((w - 10) - (acy + dx2 + 16), lh);
field2.setSize((w - 10) - (acy + dx2 + 24), lh);
field3Label.setLocation(acx + dx3 + 24, acy + dy3 - lh - 1);
field3.setLocation(acx + dx3 + 16, acy + dy3 + 1);
field3Label.setSize((w - 10) - (acy + dx3 + 24), lh);
field3.setSize((w - 10) - (acy + dx3 + 16), lh);
field4Label.setLocation(acx + dx4 + 24, acy + dy4 - lh - 1);
field4.setLocation(acx + dx4 + 16, acy + dy4 + 1);
field4Label.setSize((w - 10) - (acy + dx4 + 24), lh);
field4.setSize((w - 10) - (acy + dx4 + 16), lh);
seePosts.setLocation(10, h - 10 - 24);
seePosts.setSize((w - 40) / 4, 24);
scroll.setLocation(10, (ay + ah) + 10);
scroll.setSize(w - 20, seePosts.getY() - 10 - scroll.getY());
}
// ---%-@-%---
ProfileComponent(ProfileWindow primaire)
{
this.primaire = primaire;
Font f1 = new Font("VL Gothic", Font.PLAIN, 16);
Font f2 = new Font("VL Gothic", Font.PLAIN, 14);
int a = JLabel.RIGHT;
accountIdLabel = new JLabel("Account ID", a);
accountId = new JLabel("", a);
displayNameLabel = new JLabel("Display name", a);
displayName = new JLabel("", a);
followedLabel = new JLabel("Followed & followers", a);
followed = new JLabel("", a);
historyLabel = new JLabel("History", a);
history = new JLabel("", a);
field1Label = new JLabel("");
field1 = new JLabel("");
field2Label = new JLabel("");
field2 = new JLabel("");
field3Label = new JLabel("");
field3 = new JLabel("");
field4Label = new JLabel("");
field4 = new JLabel("");
accountIdLabel.setFont(f1);
accountId.setFont(f1);
displayNameLabel.setFont(f1);
displayName.setFont(f1);
followedLabel.setFont(f1);
followed.setFont(f1);
historyLabel.setFont(f1);
history.setFont(f2);
field1Label.setFont(f1);
field1.setFont(f2);
field2Label.setFont(f1);
field2.setFont(f2);
field3Label.setFont(f1);
field3.setFont(f2);
field4Label.setFont(f1);
field4.setFont(f2);
description = new JTextArea();
description.setEditable(false);
description.setLineWrap(true);
description.setBackground(null);
description.setFont(f1);
scroll = new JScrollPane(
description,
JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER
);
scroll.setBorder(null);
seePosts = new JButton("See posts");
seePosts.addActionListener(this);
setLayout(null);
add(accountIdLabel);
add(accountId);
add(displayNameLabel);
add(displayName);
add(followedLabel);
add(followed);
add(historyLabel);
add(history);
add(field1Label);
add(field1);
add(field2Label);
add(field2);
add(field3Label);
add(field3);
add(field4Label);
add(field4);
add(scroll);
add(seePosts);
setPreferredSize(new Dimension(640, 480));
}
}

1
RepliesWindow.java Normal file → Executable file
View File

@ -54,6 +54,7 @@ RepliesWindow extends JFrame {
postSelected(Tree<String> post)
{
postWindow.readEntity(post);
postWindow.setVisible(true);
}
private Tree<String>

0
RequestListener.java Normal file → Executable file
View File

0
RichTextPane.java Normal file → Executable file
View File

112
RudimentaryHTMLParser.java Normal file → Executable file
View File

@ -1,7 +1,7 @@
import cafe.biskuteri.hinoki.Tree;
import java.util.List;
import java.util.ListIterator;
import java.util.ArrayList;
import java.io.StringReader;
import java.io.Reader;
import java.io.IOException;
@ -165,72 +165,48 @@ RudimentaryHTMLParser {
private static Tree<String>
pass3(Tree<String> docu)
{
ListIterator<Tree<String>> it = docu.children.listIterator();
while (it.hasNext())
{
Tree<String> node = it.next();
if (!node.key.equals("text")) continue;
Tree<String> returnee = new Tree<String>();
it.remove();
StringBuilder t = new StringBuilder();
StringBuilder e = new StringBuilder();
boolean emoji = false;
char pc = ' ';
for (char c: node.value.toCharArray())
for (Tree<String> node: docu)
{
if (!node.key.equals("text"))
{
returnee.add(node);
continue;
}
StringBuilder value = new StringBuilder();
for (String segment: whitespaceSplit(node.value))
{
if (!emoji && c == ':')
boolean st = segment.startsWith(":");
boolean ed = segment.endsWith(":");
if (st && ed)
{
Tree<String> text = new Tree<String>();
text.key = "text";
text.value = empty(value);
returnee.add(text);
Tree<String> emoji = new Tree<String>();
emoji.key = "emoji";
emoji.value = segment;
returnee.add(emoji);
}
else
{
emoji = true;
if (t.length() > 0) {
Tree<String> text = new Tree<String>();
text.key = "text";
text.value = empty(t);
it.add(text);
}
pc = c;
continue;
}
if (emoji && c == ':')
{
emoji = false;
if (e.length() > 0)
{
Tree<String> shortcode = new Tree<String>();
shortcode.key = "emoji";
shortcode.value = empty(e);
it.add(shortcode);
}
pc = c;
continue;
}
if (emoji && Character.isWhitespace(c))
{
emoji = false;
if (e.length() > 0) {
t.append(':');
t.append(empty(e));
}
}
if (emoji) e.append((char)c);
else t.append((char)c);
pc = c;
}
if (emoji)
{
emoji = false;
if (e.length() > 0) {
t.append(':');
t.append(empty(e));
value.append(segment);
}
}
if (t.length() > 0) {
if (value.length() > 0)
{
Tree<String> text = new Tree<String>();
text.key = "text";
text.value = empty(t);
it.add(text);
text.value = empty(value);
returnee.add(text);
}
}
return docu;
return returnee;
}
private static String
@ -241,6 +217,26 @@ RudimentaryHTMLParser {
return s;
}
private static List<String>
whitespaceSplit(String text)
{
List<String> returnee = new ArrayList<>();
StringBuilder segment = new StringBuilder();
boolean isWhitespace = false;
for (char c: text.toCharArray())
{
boolean diff = isWhitespace ^ Character.isWhitespace(c);
if (diff) {
returnee.add(empty(segment));
isWhitespace = !isWhitespace;
}
segment.append(c);
}
returnee.add(empty(segment));
return returnee;
}
// ---%-@-%---
public static void

29
TimelineWindow.java Normal file → Executable file
View File

@ -396,14 +396,10 @@ implements ActionListener {
{
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
Tree<String> accountDetails = api.getAccountDetails();
assert accountDetails != null;
String id = accountDetails.get("id").value;
TimelineWindow w = new TimelineWindow(primaire);
w.showAuthorPosts(id);
w.showLatestPage();
w.setLocationRelativeTo(this);
Tree<String> accountDetails = api.getAccountDetails();
ProfileWindow w = new ProfileWindow(primaire);
w.use(new Account(accountDetails));
w.setLocationByPlatform(true);
w.setVisible(true);
display.setCursor(null);
@ -469,7 +465,7 @@ implements ActionListener {
return;
}
String id = null;
Tree<String> openee = null;
if (query.startsWith("@")) query = query.substring(1);
List<Object> message = new ArrayList<>();
@ -480,7 +476,7 @@ implements ActionListener {
String dname = account.get("display_name").value;
String acct = account.get("acct").value;
if (query.equals(acct)) {
id = account.get("id").value;
openee = account;
break;
}
JRadioButton b = new JRadioButton();
@ -488,7 +484,7 @@ implements ActionListener {
selGroup.add(b);
message.add(b);
}
if (id == null)
if (openee == null)
{
int response = JOptionPane.showConfirmDialog(
this,
@ -502,11 +498,11 @@ implements ActionListener {
JRadioButton b = (JRadioButton)message.get(o);
if (selGroup.isSelected(b.getModel()))
{
id = handler.json.get(o - 1).get("id").value;
openee = handler.json.get(o - 1);
break;
}
}
if (id == null) return;
if (openee == null) return;
/*
* It seems like this can happen if someone
* presses escape out of the confirm dialog.
@ -514,10 +510,9 @@ implements ActionListener {
*/
}
TimelineWindow w = new TimelineWindow(primaire);
w.showAuthorPosts(id);
w.showLatestPage();
w.setLocationRelativeTo(this);
ProfileWindow w = new ProfileWindow(primaire);
w.use(new Account(openee));
w.setLocationByPlatform(true);
w.setVisible(true);
}

4
TwoToggleButton.java Normal file → Executable file
View File

@ -127,7 +127,7 @@ implements KeyListener, MouseListener, FocusListener {
public void
mouseClicked(MouseEvent eM)
mousePressed(MouseEvent eM)
{
switch (eM.getButton()) {
case MouseEvent.BUTTON1: togglePrimary(); break;
@ -154,7 +154,7 @@ implements KeyListener, MouseListener, FocusListener {
public void
mousePressed(MouseEvent eM) { }
mouseClicked(MouseEvent eM) { }
public void
mouseReleased(MouseEvent eM) { }

47
WindowUpdater.java Normal file → Executable file
View File

@ -36,7 +36,7 @@ WindowUpdater {
// ---%-@-%---
public void
public synchronized void
add(TimelineWindow updatee)
{
if (!timelineWindows.contains(updatee))
@ -46,7 +46,7 @@ WindowUpdater {
userConn.reevaluate();
}
public void
public synchronized void
add(NotificationsWindow updatee)
{
if (!notificationWindows.contains(updatee))
@ -55,7 +55,7 @@ WindowUpdater {
userConn.reevaluate();
}
public void
public synchronized void
remove(TimelineWindow updatee)
{
timelineWindows.remove(updatee);
@ -63,7 +63,7 @@ WindowUpdater {
userConn.reevaluate();
}
public void
public synchronized void
remove(NotificationsWindow updatee)
{
notificationWindows.remove(updatee);
@ -136,11 +136,11 @@ WindowUpdater {
{
boolean hasUpdatee = false;
for (NotificationsWindow updatee: notificationWindows)
if (responsibleFor(updatee)) hasUpdatee = true;
for (NotificationsWindow w: notificationWindows)
if (responsibleFor(w)) hasUpdatee = true;
for (TimelineWindow updatee: timelineWindows)
if (responsibleFor(updatee)) hasUpdatee = true;
for (TimelineWindow w: timelineWindows)
if (responsibleFor(w)) hasUpdatee = true;
if (!hasUpdatee && thread != null) stop();
if (hasUpdatee && thread == null) start();
@ -161,6 +161,12 @@ WindowUpdater {
// monitorTimeline should not return until
// the connection is closed, or this thread
// is interrupted.
if (thread == Thread.currentThread()) thread = null;
/*
* This isn't thread safe. But I'd like the
* restart after sleep mode, so.
*/
}
public void
@ -204,23 +210,22 @@ WindowUpdater {
notificationSound.start();
}
for (TimelineWindow updatee: timelineWindows)
synchronized (WindowUpdater.this)
{
if (!responsibleFor(updatee)) continue;
updatee.refresh();
/*
* () Note that we're in a separate thread,
* and our windows aren't thread-safe. We could
* probably make them a bit bananas asking
* for a refresh while they're in the middle
* of one. Could we add mutexes?
*/
for (TimelineWindow w: timelineWindows)
{
if (!responsibleFor(w)) continue;
w.refresh();
}
}
for (NotificationsWindow updatee: notificationWindows)
synchronized (WindowUpdater.this)
{
if (!responsibleFor(updatee)) continue;
updatee.refresh();
for (NotificationsWindow w: notificationWindows)
{
if (!responsibleFor(w)) continue;
w.refresh();
}
}
}

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

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

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

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

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

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

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

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

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

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

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

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

Before

Width:  |  Height:  |  Size: 353 B

After

Width:  |  Height:  |  Size: 353 B

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

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

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

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

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

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

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

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

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

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

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

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

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

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

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

Before

Width:  |  Height:  |  Size: 313 B

After

Width:  |  Height:  |  Size: 313 B

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

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

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

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

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

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

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

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

0
notifOptions.txt Normal file → Executable file
View File

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