diff --git a/BasicHTMLParser.java b/BasicHTMLParser.java new file mode 100644 index 0000000..d55c2eb --- /dev/null +++ b/BasicHTMLParser.java @@ -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 + parse(String html) + { + List segments; + segments = distinguishTagsFromPcdata(html); + segments = evaluateHtmlEscapes(segments); + + Tree document; + document = toNodes(segments); + document = distinguishEmojisFromText(document); + document = hierarchise(document); + + return document; + } + +// - -%- - + + private static List + distinguishTagsFromPcdata(String html) + { + List 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 + evaluateHtmlEscapes(List strings) + { + List 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("<")) part.append('<'); + if (v.equals(">")) part.append('>'); + if (v.equals("&")) part.append('&'); + if (v.equals(""")) part.append('"'); + if (v.equals("'")) part.append('\''); + if (v.equals("'")) 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 + toNodes(List segments) + { + Tree returnee = new Tree(); + + for (String segment: segments) + { + boolean isTag = segment.startsWith("<"); + Tree node = new Tree(); + + 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 attr = new Tree(); + 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 attr = new Tree(); + attr.key = key; + attr.value = value; + node.add(attr); + } + + returnee.add(node); + } + + return returnee; + } + + private static Tree + distinguishEmojisFromText(Tree nodes) + { + Tree returnee = new Tree(); + + for (Tree node: nodes) + { + if (!node.key.equals("text")) + { + returnee.add(node); + continue; + } + + List 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 text = new Tree(); + text.key = "text"; + text.value = empty(b); + returnee.add(text); + Tree emoji = new Tree(); + emoji.key = "emoji"; + emoji.value = segment; + returnee.add(emoji); + } + else + { + b.append(segment); + } + } + if (b.length() > 0) + { + Tree text = new Tree(); + text.key = "text"; + text.value = empty(b); + returnee.add(text); + } + } + + return returnee; + } + + private static Tree + hierarchise(Tree nodes) + { + Tree root = new Tree(); + root.add(new Tree<>("attributes", null)); + root.get(0).add(new Tree<>("html", null)); + root.add(new Tree<>("children", null)); + + Deque> parents = new LinkedList<>(); + parents.push(root); + for (Tree 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 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 elem = new Tree(); + node.key = "attributes"; + elem.add(node); + elem.add(new Tree<>("children", null)); + + parents.peek().get("children").add(elem); + } + else + { + Tree elem = new Tree(); + 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 + distinguishWhitespaceFromText(String text) + { + List 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; + } + +} \ No newline at end of file diff --git a/ClipboardApi.java b/ClipboardApi.java old mode 100644 new mode 100755 diff --git a/ComposeWindow.java b/ComposeWindow.java old mode 100644 new mode 100755 diff --git a/ImageApi.java b/ImageApi.java old mode 100644 new mode 100755 diff --git a/ImageWindow.java b/ImageWindow.java old mode 100644 new mode 100755 diff --git a/JKomasto.java b/JKomasto.java old mode 100644 new mode 100755 index 803c69f..a6f9cac --- a/JKomasto.java +++ b/JKomasto.java @@ -436,7 +436,7 @@ Post { { Tree 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 fs = entity.get("fields"); + fields = new String[fs.size()][]; + for (int o = 0; o < fields.length; ++o) + { + Tree 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 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; } diff --git a/KDE_Dialog_Appear.wav b/KDE_Dialog_Appear.wav old mode 100644 new mode 100755 diff --git a/LoginWindow.java b/LoginWindow.java old mode 100644 new mode 100755 diff --git a/MastodonApi.java b/MastodonApi.java old mode 100644 new mode 100755 index c3e397a..137af65 --- a/MastodonApi.java +++ b/MastodonApi.java @@ -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(); diff --git a/NotificationsWindow.java b/NotificationsWindow.java old mode 100644 new mode 100755 diff --git a/PostWindow.java b/PostWindow.java old mode 100644 new mode 100755 index 2f7aa6b..19e08fb --- a/PostWindow.java +++ b/PostWindow.java @@ -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 nodes = RudimentaryHTMLParser.depthlessRead(n); for (Tree node: nodes) diff --git a/ProfileWindow.java b/ProfileWindow.java new file mode 100755 index 0000000..4f2a97f --- /dev/null +++ b/ProfileWindow.java @@ -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)); + } + +} diff --git a/RepliesWindow.java b/RepliesWindow.java old mode 100644 new mode 100755 index b248b0f..aa632e6 --- a/RepliesWindow.java +++ b/RepliesWindow.java @@ -54,6 +54,7 @@ RepliesWindow extends JFrame { postSelected(Tree post) { postWindow.readEntity(post); + postWindow.setVisible(true); } private Tree diff --git a/RequestListener.java b/RequestListener.java old mode 100644 new mode 100755 diff --git a/RichTextPane.java b/RichTextPane.java old mode 100644 new mode 100755 diff --git a/RudimentaryHTMLParser.java b/RudimentaryHTMLParser.java old mode 100644 new mode 100755 index 665f4a6..8b77e14 --- a/RudimentaryHTMLParser.java +++ b/RudimentaryHTMLParser.java @@ -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 pass3(Tree docu) { - ListIterator> it = docu.children.listIterator(); - while (it.hasNext()) - { - Tree node = it.next(); - if (!node.key.equals("text")) continue; + Tree returnee = new Tree(); - it.remove(); - StringBuilder t = new StringBuilder(); - StringBuilder e = new StringBuilder(); - boolean emoji = false; - char pc = ' '; - for (char c: node.value.toCharArray()) + for (Tree 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 text = new Tree(); + text.key = "text"; + text.value = empty(value); + returnee.add(text); + + Tree emoji = new Tree(); + emoji.key = "emoji"; + emoji.value = segment; + returnee.add(emoji); + } + else { - emoji = true; - if (t.length() > 0) { - Tree text = new Tree(); - text.key = "text"; - text.value = empty(t); - it.add(text); - } - pc = c; - continue; - } - if (emoji && c == ':') - { - emoji = false; - if (e.length() > 0) - { - Tree shortcode = new Tree(); - 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 text = new Tree(); 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 + whitespaceSplit(String text) + { + List 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 diff --git a/TimelineWindow.java b/TimelineWindow.java old mode 100644 new mode 100755 index 07e3ebb..7f01aaf --- a/TimelineWindow.java +++ b/TimelineWindow.java @@ -396,14 +396,10 @@ implements ActionListener { { display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); - Tree 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 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 openee = null; if (query.startsWith("@")) query = query.substring(1); List 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); } diff --git a/TwoToggleButton.java b/TwoToggleButton.java old mode 100644 new mode 100755 index 406f7a8..4f8ce01 --- a/TwoToggleButton.java +++ b/TwoToggleButton.java @@ -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) { } diff --git a/WindowUpdater.java b/WindowUpdater.java old mode 100644 new mode 100755 index f521878..3fbb4f4 --- a/WindowUpdater.java +++ b/WindowUpdater.java @@ -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(); + } } } diff --git a/graphics/Federated.xcf b/graphics/Federated.xcf old mode 100644 new mode 100755 diff --git a/graphics/Flags.xcf b/graphics/Flags.xcf old mode 100644 new mode 100755 diff --git a/graphics/Hourglass.xcf b/graphics/Hourglass.xcf old mode 100644 new mode 100755 diff --git a/graphics/boostToggled.png b/graphics/boostToggled.png old mode 100644 new mode 100755 diff --git a/graphics/boostUntoggled.png b/graphics/boostUntoggled.png old mode 100644 new mode 100755 diff --git a/graphics/button.png b/graphics/button.png old mode 100644 new mode 100755 diff --git a/graphics/disabledOverlay.png b/graphics/disabledOverlay.png old mode 100644 new mode 100755 diff --git a/graphics/favouriteToggled.png b/graphics/favouriteToggled.png old mode 100644 new mode 100755 diff --git a/graphics/favouriteUntoggled.png b/graphics/favouriteUntoggled.png old mode 100644 new mode 100755 diff --git a/graphics/federated.png b/graphics/federated.png old mode 100644 new mode 100755 diff --git a/graphics/miscToggled.png b/graphics/miscToggled.png old mode 100644 new mode 100755 diff --git a/graphics/miscUntoggled.png b/graphics/miscUntoggled.png old mode 100644 new mode 100755 diff --git a/graphics/ref1.png b/graphics/ref1.png old mode 100644 new mode 100755 diff --git a/graphics/replyToggled.png b/graphics/replyToggled.png old mode 100644 new mode 100755 diff --git a/graphics/replyUntoggled.png b/graphics/replyUntoggled.png old mode 100644 new mode 100755 diff --git a/graphics/selectedOverlay.png b/graphics/selectedOverlay.png old mode 100644 new mode 100755 diff --git a/graphics/test1.png b/graphics/test1.png old mode 100644 new mode 100755 diff --git a/graphics/test2.png b/graphics/test2.png old mode 100644 new mode 100755 diff --git a/graphics/test3.png b/graphics/test3.png old mode 100644 new mode 100755 diff --git a/graphics/test4.png b/graphics/test4.png old mode 100644 new mode 100755 diff --git a/notifOptions.txt b/notifOptions.txt old mode 100644 new mode 100755 diff --git a/notifOptions.txt~ b/notifOptions.txt~ old mode 100644 new mode 100755