Added buggy rich text pane.
Added rendering of emojis. Added run script.
0
ComposeWindow.java
Normal file → Executable file
0
ImageApi.java
Normal file → Executable file
6
JKomasto.java
Normal file → Executable file
@ -145,7 +145,8 @@ Post {
|
|||||||
|
|
||||||
public String
|
public String
|
||||||
text,
|
text,
|
||||||
contentWarning;
|
contentWarning,
|
||||||
|
html;
|
||||||
|
|
||||||
public String
|
public String
|
||||||
authorId, authorName;
|
authorId, authorName;
|
||||||
@ -174,6 +175,9 @@ Post {
|
|||||||
public Attachment[]
|
public Attachment[]
|
||||||
attachments;
|
attachments;
|
||||||
|
|
||||||
|
public String[][]
|
||||||
|
emojiUrls;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
0
LoginWindow.java
Normal file → Executable file
236
PostWindow.java
Normal file → Executable file
@ -26,6 +26,8 @@ import java.awt.event.ActionListener;
|
|||||||
import java.awt.event.ActionEvent;
|
import java.awt.event.ActionEvent;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -96,7 +98,9 @@ implements ActionListener {
|
|||||||
postDisplay.setAuthorAvatar(post.authorAvatar);
|
postDisplay.setAuthorAvatar(post.authorAvatar);
|
||||||
postDisplay.setDate(DATE_FORMAT.format(post.date));
|
postDisplay.setDate(DATE_FORMAT.format(post.date));
|
||||||
postDisplay.setTime(TIME_FORMAT.format(post.date));
|
postDisplay.setTime(TIME_FORMAT.format(post.date));
|
||||||
|
postDisplay.setEmojiUrls(post.emojiUrls);
|
||||||
postDisplay.setText(post.text);
|
postDisplay.setText(post.text);
|
||||||
|
postDisplay.setHtml(post.html);
|
||||||
postDisplay.setFavourited(post.favourited);
|
postDisplay.setFavourited(post.favourited);
|
||||||
postDisplay.setBoosted(post.boosted);
|
postDisplay.setBoosted(post.boosted);
|
||||||
postDisplay.setMediaPreview(
|
postDisplay.setMediaPreview(
|
||||||
@ -275,6 +279,7 @@ implements ActionListener {
|
|||||||
|
|
||||||
Post samplePost = new Post();
|
Post samplePost = new Post();
|
||||||
samplePost.text = "This is a sample post.";
|
samplePost.text = "This is a sample post.";
|
||||||
|
samplePost.html = "";
|
||||||
samplePost.authorId = "snowyfox@biskuteri.cafe";
|
samplePost.authorId = "snowyfox@biskuteri.cafe";
|
||||||
samplePost.authorName = "snowyfox";
|
samplePost.authorName = "snowyfox";
|
||||||
samplePost.date = ZonedDateTime.now();
|
samplePost.date = ZonedDateTime.now();
|
||||||
@ -283,6 +288,7 @@ implements ActionListener {
|
|||||||
samplePost.boosted = false;
|
samplePost.boosted = false;
|
||||||
samplePost.favourited = true;
|
samplePost.favourited = true;
|
||||||
samplePost.attachments = new Attachment[0];
|
samplePost.attachments = new Attachment[0];
|
||||||
|
samplePost.emojiUrls = new String[0][];
|
||||||
showPost(samplePost);
|
showPost(samplePost);
|
||||||
|
|
||||||
setContentPane(postDisplay);
|
setContentPane(postDisplay);
|
||||||
@ -299,11 +305,20 @@ implements ActionListener {
|
|||||||
private PostWindow
|
private PostWindow
|
||||||
primaire;
|
primaire;
|
||||||
|
|
||||||
private String
|
|
||||||
authorName, authorId, date, time, text;
|
|
||||||
|
|
||||||
// - -%- -
|
// - -%- -
|
||||||
|
|
||||||
|
private List<RichTextPane.Segment>
|
||||||
|
authorNameOr, bodyOr;
|
||||||
|
|
||||||
|
private RichTextPane
|
||||||
|
authorName, body;
|
||||||
|
|
||||||
|
private JLabel
|
||||||
|
authorId, time, date;
|
||||||
|
|
||||||
|
private String[][]
|
||||||
|
emojiUrls;
|
||||||
|
|
||||||
private TwoToggleButton
|
private TwoToggleButton
|
||||||
favouriteBoost,
|
favouriteBoost,
|
||||||
replyMisc,
|
replyMisc,
|
||||||
@ -316,22 +331,68 @@ implements ActionListener {
|
|||||||
// ---%-@-%---
|
// ---%-@-%---
|
||||||
|
|
||||||
public void
|
public void
|
||||||
setAuthorName(String n) { authorName = n; }
|
setAuthorName(String n)
|
||||||
|
{
|
||||||
|
authorNameOr = new RichTextPane.Builder().text(n).finish();
|
||||||
|
}
|
||||||
|
|
||||||
public void
|
public void
|
||||||
setAuthorId(String n) { authorId = n; }
|
setAuthorId(String n) { authorId.setText(n); }
|
||||||
|
|
||||||
public void
|
public void
|
||||||
setAuthorAvatar(Image n) { profile.setImage(n); }
|
setAuthorAvatar(Image n) { profile.setImage(n); }
|
||||||
|
|
||||||
public void
|
public void
|
||||||
setDate(String n) { date = n; }
|
setDate(String n) { date.setText(n); }
|
||||||
|
|
||||||
public void
|
public void
|
||||||
setTime(String n) { time = n; }
|
setTime(String n) { time.setText(n); }
|
||||||
|
|
||||||
public void
|
public void
|
||||||
setText(String n) { text = n; }
|
setText(String n) { }
|
||||||
|
|
||||||
|
public void
|
||||||
|
setEmojiUrls(String[][] n) { emojiUrls = n; }
|
||||||
|
|
||||||
|
public void
|
||||||
|
setHtml(String n)
|
||||||
|
{
|
||||||
|
RichTextPane.Builder b = new RichTextPane.Builder();
|
||||||
|
Tree<String> nodes = RudimentaryHTMLParser.depthlessRead(n);
|
||||||
|
for (Tree<String> node: nodes)
|
||||||
|
{
|
||||||
|
if (node.key.equals("tag"))
|
||||||
|
{
|
||||||
|
String tagName = node.get(0).key;
|
||||||
|
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(node.get("href").value, null).spacer(" ");
|
||||||
|
}
|
||||||
|
if (node.key.equals("text"))
|
||||||
|
{
|
||||||
|
for (String word: node.value.split(" "))
|
||||||
|
b = b.text(word).spacer(" ");
|
||||||
|
}
|
||||||
|
if (node.key.equals("emoji"))
|
||||||
|
{
|
||||||
|
String shortcode = node.value;
|
||||||
|
String url = null;
|
||||||
|
for (String[] entry: emojiUrls)
|
||||||
|
if (entry[0].equals(shortcode)) url = entry[1];
|
||||||
|
try {
|
||||||
|
ImageIcon image = new ImageIcon(new URL(url));
|
||||||
|
b = b.image(image, node.value);
|
||||||
|
}
|
||||||
|
catch (MalformedURLException eMu) {
|
||||||
|
b = b.text(":×:");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bodyOr = b.finish();
|
||||||
|
}
|
||||||
|
|
||||||
public void
|
public void
|
||||||
setFavourited(boolean a)
|
setFavourited(boolean a)
|
||||||
@ -424,76 +485,12 @@ implements ActionListener {
|
|||||||
java.awt.RenderingHints.VALUE_ANTIALIAS_ON
|
java.awt.RenderingHints.VALUE_ANTIALIAS_ON
|
||||||
);
|
);
|
||||||
|
|
||||||
Font f1 = new Font("IPAGothic", Font.PLAIN, 16);
|
int w1 = authorName.getWidth();
|
||||||
Font f2 = new Font("IPAGothic", Font.PLAIN, 14);
|
int w2 = body.getWidth();
|
||||||
FontMetrics fm1 = g.getFontMetrics(f1);
|
FontMetrics fm1 = getFontMetrics(authorName.getFont());
|
||||||
FontMetrics fm2 = g.getFontMetrics(f2);
|
FontMetrics fm2 = getFontMetrics(body.getFont());
|
||||||
|
authorName.setText(RichTextPane.layout(authorNameOr, fm1, w1));
|
||||||
int x1 = 60;
|
body.setText(RichTextPane.layout(bodyOr, fm2, w2));
|
||||||
int x4 = getWidth() - 10;
|
|
||||||
int x2 = x4 - fm2.stringWidth(date);
|
|
||||||
int x3 = x4 - fm1.stringWidth(time);
|
|
||||||
int y1 = 10;
|
|
||||||
int y2 = y1 + fm2.getHeight();
|
|
||||||
int y3 = y2 + fm1.getHeight();
|
|
||||||
int y4 = y3 + 8;
|
|
||||||
|
|
||||||
Shape defaultClip = g.getClip();
|
|
||||||
g.setClip(x1, y1, Math.min(x2, x3) - 8 - x1, y4 - y1);
|
|
||||||
// First time I've used this method..
|
|
||||||
// Cause, clearRect is not working.
|
|
||||||
g.setFont(f2);
|
|
||||||
g.drawString(authorId, x1, y2);
|
|
||||||
g.setFont(f1);
|
|
||||||
g.drawString(authorName, x1, y3);
|
|
||||||
g.setClip(defaultClip);
|
|
||||||
|
|
||||||
g.setFont(f2);
|
|
||||||
g.drawString(date, x2, y2);
|
|
||||||
g.setFont(f1);
|
|
||||||
g.drawString(time, x3, y3);
|
|
||||||
|
|
||||||
int y = y4;
|
|
||||||
for (String line: split(text, 40)) {
|
|
||||||
y += fm1.getHeight();
|
|
||||||
g.drawString(line, x1, y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// - -%- -
|
|
||||||
|
|
||||||
private static List<String>
|
|
||||||
split(String string, int lineLength)
|
|
||||||
{
|
|
||||||
List<String> returnee = new ArrayList<>();
|
|
||||||
|
|
||||||
StringBuilder line = new StringBuilder();
|
|
||||||
for (String word: string.split(" "))
|
|
||||||
{
|
|
||||||
if (word.length() >= lineLength) {
|
|
||||||
word = word.substring(0, lineLength - 4) + "...";
|
|
||||||
}
|
|
||||||
if (word.matches("\n")) {
|
|
||||||
returnee.add(empty(line));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (line.length() + word.length() > lineLength) {
|
|
||||||
returnee.add(empty(line));
|
|
||||||
}
|
|
||||||
line.append(word);
|
|
||||||
line.append(" ");
|
|
||||||
}
|
|
||||||
returnee.add(empty(line));
|
|
||||||
|
|
||||||
return returnee;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String
|
|
||||||
empty(StringBuilder b)
|
|
||||||
{
|
|
||||||
String s = b.toString();
|
|
||||||
b.delete(0, b.length());
|
|
||||||
return s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---%-@-%---
|
// ---%-@-%---
|
||||||
@ -502,48 +499,75 @@ implements ActionListener {
|
|||||||
{
|
{
|
||||||
this.primaire = primaire;
|
this.primaire = primaire;
|
||||||
|
|
||||||
authorName = authorId = time = text = "";
|
emojiUrls = new String[0][];
|
||||||
|
|
||||||
Dimension buttonSize = new Dimension(20, 40);
|
|
||||||
Border b = BorderFactory.createEmptyBorder(10, 10, 10, 10);
|
Border b = BorderFactory.createEmptyBorder(10, 10, 10, 10);
|
||||||
|
Font f1 = new Font("IPAGothic", Font.PLAIN, 16);
|
||||||
|
Font f2 = new Font("IPAGothic", Font.PLAIN, 13);
|
||||||
|
|
||||||
profile = new RoundButton();
|
profile = new RoundButton();
|
||||||
|
favouriteBoost = new TwoToggleButton("favourite", "boost");
|
||||||
|
replyMisc = new TwoToggleButton("reply", "misc");
|
||||||
|
nextPrev = new TwoToggleButton("next", "prev");
|
||||||
|
media = new RoundButton();
|
||||||
profile.addActionListener(this);
|
profile.addActionListener(this);
|
||||||
|
|
||||||
favouriteBoost = new TwoToggleButton("favourite", "boost");
|
|
||||||
favouriteBoost.addActionListener(this);
|
favouriteBoost.addActionListener(this);
|
||||||
|
|
||||||
replyMisc = new TwoToggleButton("reply", "misc");
|
|
||||||
replyMisc.addActionListener(this);
|
replyMisc.addActionListener(this);
|
||||||
|
|
||||||
nextPrev = new TwoToggleButton("next", "prev");
|
|
||||||
nextPrev.addActionListener(this);
|
nextPrev.addActionListener(this);
|
||||||
|
|
||||||
media = new RoundButton();
|
|
||||||
//media.setPreferredSize(buttonSize);
|
|
||||||
media.addActionListener(this);
|
media.addActionListener(this);
|
||||||
|
|
||||||
Box ibuttons = Box.createVerticalBox();
|
|
||||||
ibuttons.setOpaque(false);
|
|
||||||
ibuttons.add(profile);
|
|
||||||
ibuttons.add(Box.createVerticalStrut(8));
|
|
||||||
ibuttons.add(favouriteBoost);
|
|
||||||
ibuttons.add(Box.createVerticalStrut(8));
|
|
||||||
ibuttons.add(replyMisc);
|
|
||||||
ibuttons.add(Box.createVerticalStrut(8));
|
|
||||||
ibuttons.add(nextPrev);
|
|
||||||
ibuttons.add(Box.createVerticalStrut(8));
|
|
||||||
ibuttons.add(media);
|
|
||||||
ibuttons.setMaximumSize(ibuttons.getPreferredSize());
|
|
||||||
Box buttons = Box.createVerticalBox();
|
Box buttons = Box.createVerticalBox();
|
||||||
buttons.setOpaque(false);
|
buttons.setOpaque(false);
|
||||||
buttons.add(ibuttons);
|
buttons.add(profile);
|
||||||
buttons.setBorder(b);
|
buttons.add(Box.createVerticalStrut(8));
|
||||||
|
buttons.add(favouriteBoost);
|
||||||
|
buttons.add(Box.createVerticalStrut(8));
|
||||||
|
buttons.add(replyMisc);
|
||||||
|
buttons.add(Box.createVerticalStrut(8));
|
||||||
|
buttons.add(nextPrev);
|
||||||
|
buttons.add(Box.createVerticalStrut(8));
|
||||||
|
buttons.add(media);
|
||||||
|
buttons.setMaximumSize(buttons.getPreferredSize());
|
||||||
|
Box left = Box.createVerticalBox();
|
||||||
|
left.setOpaque(false);
|
||||||
|
left.add(buttons);
|
||||||
|
|
||||||
setLayout(new BorderLayout());
|
authorId = new JLabel();
|
||||||
add(buttons, BorderLayout.WEST);
|
authorName = new RichTextPane();
|
||||||
|
time = new JLabel();
|
||||||
|
date = new JLabel();
|
||||||
|
authorId.setFont(f2);
|
||||||
|
date.setFont(f2);
|
||||||
|
authorName.setFont(f1);
|
||||||
|
time.setFont(f1);
|
||||||
|
|
||||||
setFont(getFont().deriveFont(14f));
|
JPanel top1 = new JPanel();
|
||||||
|
top1.setLayout(new BorderLayout(8, 0));
|
||||||
|
top1.add(authorId);
|
||||||
|
top1.add(date, BorderLayout.EAST);
|
||||||
|
JPanel top2 = new JPanel();
|
||||||
|
top2.setLayout(new BorderLayout(8, 0));
|
||||||
|
top2.add(authorName);
|
||||||
|
top2.add(time, BorderLayout.EAST);
|
||||||
|
Box top = Box.createVerticalBox();
|
||||||
|
top.add(top1);
|
||||||
|
top.add(Box.createVerticalStrut(2));
|
||||||
|
top.add(top2);
|
||||||
|
|
||||||
|
body = new RichTextPane();
|
||||||
|
body.setFont(getFont().deriveFont(14f));
|
||||||
|
|
||||||
|
JPanel centre = new JPanel();
|
||||||
|
centre.setOpaque(false);
|
||||||
|
centre.setLayout(new BorderLayout(0, 8));
|
||||||
|
centre.add(top, BorderLayout.NORTH);
|
||||||
|
centre.add(body);
|
||||||
|
|
||||||
|
setLayout(new BorderLayout(8, 0));
|
||||||
|
add(left, BorderLayout.WEST);
|
||||||
|
add(centre);
|
||||||
|
|
||||||
|
setBorder(b);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
251
RichTextPane.java
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
|
||||||
|
import javax.swing.JComponent;
|
||||||
|
import javax.swing.ImageIcon;
|
||||||
|
import java.awt.Graphics;
|
||||||
|
import java.awt.FontMetrics;
|
||||||
|
import java.awt.Image;
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.ListIterator;
|
||||||
|
|
||||||
|
class
|
||||||
|
RichTextPane extends JComponent {
|
||||||
|
|
||||||
|
private List<Segment>
|
||||||
|
text;
|
||||||
|
|
||||||
|
// ---%-@-%---
|
||||||
|
|
||||||
|
public void
|
||||||
|
setText(List<Segment> text) { this.text = text; }
|
||||||
|
|
||||||
|
// - -%- -
|
||||||
|
|
||||||
|
protected void
|
||||||
|
paintComponent(Graphics g)
|
||||||
|
{
|
||||||
|
g.setFont(getFont());
|
||||||
|
FontMetrics fm = g.getFontMetrics(getFont());
|
||||||
|
g.clearRect(0, 0, getWidth(), getHeight());
|
||||||
|
|
||||||
|
for (Segment segment: text)
|
||||||
|
{
|
||||||
|
if (segment.image != null) {
|
||||||
|
int ow = segment.image.getIconWidth();
|
||||||
|
int oh = segment.image.getIconHeight();
|
||||||
|
int h = fm.getHeight();
|
||||||
|
int w = h * ow / oh;
|
||||||
|
int x = segment.x, y = segment.y;
|
||||||
|
Image img = segment.image.getImage();
|
||||||
|
g.drawImage(img, x, y - h, w, h, this);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segment.link != null) g.setColor(Color.BLUE);
|
||||||
|
g.drawString(segment.text, segment.x, segment.y);
|
||||||
|
g.setColor(getForeground());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// - -%- -
|
||||||
|
|
||||||
|
public static List<Segment>
|
||||||
|
layout(List<Segment> text, FontMetrics fm, int width)
|
||||||
|
{
|
||||||
|
if (width < fm.getMaxAdvance()) return new LinkedList<>();
|
||||||
|
|
||||||
|
List<Segment> copy = new LinkedList<>();
|
||||||
|
for (Segment segment: text) copy.add(segment.clone());
|
||||||
|
text = copy;
|
||||||
|
ListIterator<Segment> cursor = text.listIterator();
|
||||||
|
int dy = fm.getHeight(), x = 0, y = dy;
|
||||||
|
while (cursor.hasNext())
|
||||||
|
{
|
||||||
|
Segment curr = cursor.next();
|
||||||
|
|
||||||
|
int dx;
|
||||||
|
if (curr.image != null) {
|
||||||
|
int ow = curr.image.getIconWidth();
|
||||||
|
int oh = curr.image.getIconHeight();
|
||||||
|
dx = dy * ow / oh;
|
||||||
|
}
|
||||||
|
else if (curr.text != null) {
|
||||||
|
dx = fm.stringWidth(curr.text);
|
||||||
|
}
|
||||||
|
else if (curr.link != null) {
|
||||||
|
curr.text = curr.link;
|
||||||
|
dx = fm.stringWidth(curr.link);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
assert false;
|
||||||
|
dx = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If can readily fit, just do so.
|
||||||
|
if (x + dx < width || curr.spacer) {
|
||||||
|
curr.x = x;
|
||||||
|
curr.y = y;
|
||||||
|
x += dx;
|
||||||
|
if (curr.spacer && curr.text.equals("\n")) {
|
||||||
|
y += dy;
|
||||||
|
x = 0;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If image, or text that isn't long, just break.
|
||||||
|
if (curr.image != null || dx < width / 3) {
|
||||||
|
curr.x = 0;
|
||||||
|
curr.y = y += dy;
|
||||||
|
x = dx;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Greedily split string to fit into line.
|
||||||
|
int offset = splitForFit(curr.text, fm, width - x);
|
||||||
|
if (offset == 0) {
|
||||||
|
cursor.add(curr); cursor.previous();
|
||||||
|
y += dy;
|
||||||
|
x = dx;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Segment next = new Segment();
|
||||||
|
next.text = curr.text.substring(offset);
|
||||||
|
next.link = curr.link;
|
||||||
|
cursor.add(next); cursor.previous();
|
||||||
|
curr.text = curr.text.substring(0, offset);
|
||||||
|
curr.x = x;
|
||||||
|
curr.y = y;
|
||||||
|
y += dy;
|
||||||
|
x = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// - -%- -
|
||||||
|
|
||||||
|
private static int
|
||||||
|
splitForFit(String s, FontMetrics fm, int width)
|
||||||
|
{
|
||||||
|
int max = 0;
|
||||||
|
for (int o = 1; o < s.length(); max = o++)
|
||||||
|
if (fm.stringWidth(s.substring(0, o)) > width) break;
|
||||||
|
return max;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---%-@-%---
|
||||||
|
|
||||||
|
public static class
|
||||||
|
Segment {
|
||||||
|
|
||||||
|
public ImageIcon
|
||||||
|
image;
|
||||||
|
|
||||||
|
public String
|
||||||
|
link;
|
||||||
|
|
||||||
|
public String
|
||||||
|
text;
|
||||||
|
|
||||||
|
public boolean
|
||||||
|
spacer;
|
||||||
|
|
||||||
|
public int
|
||||||
|
x, y;
|
||||||
|
|
||||||
|
// -=%=-
|
||||||
|
|
||||||
|
public String
|
||||||
|
toString()
|
||||||
|
{
|
||||||
|
StringBuilder b = new StringBuilder();
|
||||||
|
b.append(getClass().getName() + "[");
|
||||||
|
b.append("image=" + image);
|
||||||
|
b.append(",link=" + link);
|
||||||
|
b.append(",text=" + text);
|
||||||
|
b.append(",x=" + x);
|
||||||
|
b.append(",y=" + y);
|
||||||
|
b.append("]");
|
||||||
|
return b.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Segment
|
||||||
|
clone()
|
||||||
|
{
|
||||||
|
Segment segment = new Segment();
|
||||||
|
segment.image = this.image;
|
||||||
|
segment.link = this.link;
|
||||||
|
segment.text = this.text;
|
||||||
|
segment.spacer = this.spacer;
|
||||||
|
segment.x = this.x;
|
||||||
|
segment.y = this.y;
|
||||||
|
return segment;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class
|
||||||
|
Builder {
|
||||||
|
|
||||||
|
private List<Segment>
|
||||||
|
returnee;
|
||||||
|
|
||||||
|
// -=%=-
|
||||||
|
|
||||||
|
public
|
||||||
|
Builder() { returnee = new LinkedList<>(); }
|
||||||
|
|
||||||
|
public Builder
|
||||||
|
image(ImageIcon image, String text)
|
||||||
|
{
|
||||||
|
Segment segment = new Segment();
|
||||||
|
segment.image = image;
|
||||||
|
segment.text = text;
|
||||||
|
returnee.add(segment);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder
|
||||||
|
link(String link, String text)
|
||||||
|
{
|
||||||
|
Segment segment = new Segment();
|
||||||
|
segment.link = link;
|
||||||
|
segment.text = text;
|
||||||
|
returnee.add(segment);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder
|
||||||
|
text(String text)
|
||||||
|
{
|
||||||
|
Segment segment = new Segment();
|
||||||
|
segment.text = text;
|
||||||
|
returnee.add(segment);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder
|
||||||
|
spacer(String text)
|
||||||
|
{
|
||||||
|
Segment segment = new Segment();
|
||||||
|
segment.text = text;
|
||||||
|
segment.spacer = true;
|
||||||
|
returnee.add(segment);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Segment>
|
||||||
|
finish() { return returnee; }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---%-@-%---
|
||||||
|
|
||||||
|
RichTextPane()
|
||||||
|
{
|
||||||
|
text = new LinkedList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,5 +1,7 @@
|
|||||||
|
|
||||||
import cafe.biskuteri.hinoki.Tree;
|
import cafe.biskuteri.hinoki.Tree;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.ListIterator;
|
||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
import java.io.Reader;
|
import java.io.Reader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -9,9 +11,19 @@ RudimentaryHTMLParser {
|
|||||||
|
|
||||||
public static Tree<String>
|
public static Tree<String>
|
||||||
depthlessRead(String html)
|
depthlessRead(String html)
|
||||||
throws IOException
|
|
||||||
{
|
{
|
||||||
return pass2(pass1(html));
|
try {
|
||||||
|
return pass3(pass2(pass1(html)));
|
||||||
|
}
|
||||||
|
catch (IOException eIo) {
|
||||||
|
assert false;
|
||||||
|
/*
|
||||||
|
* We use only StringReaders, which only throw an
|
||||||
|
* IOException when they are read after being closed.
|
||||||
|
* And we don't close them.
|
||||||
|
*/
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// - -%- -
|
// - -%- -
|
||||||
@ -20,11 +32,12 @@ RudimentaryHTMLParser {
|
|||||||
pass1(String html)
|
pass1(String html)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
Reader r = new StringReader(html);
|
Reader r = new StringReader(html);
|
||||||
Tree<String> docu = new Tree<String>();
|
Tree<String> docu = new Tree<String>();
|
||||||
StringBuilder text = new StringBuilder();
|
StringBuilder text = new StringBuilder();
|
||||||
|
StringBuilder emoji = new StringBuilder();
|
||||||
StringBuilder htmlEscape = new StringBuilder();
|
StringBuilder htmlEscape = new StringBuilder();
|
||||||
boolean quoted = false;
|
boolean quoted = false, inEmoji = false;
|
||||||
int c; while ((c = r.read()) != -1)
|
int c; while ((c = r.read()) != -1)
|
||||||
{
|
{
|
||||||
if (c == '&' || htmlEscape.length() > 0)
|
if (c == '&' || htmlEscape.length() > 0)
|
||||||
@ -77,7 +90,7 @@ RudimentaryHTMLParser {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
text.append((char)c);
|
text.append((char)c);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (text.length() > 0)
|
if (text.length() > 0)
|
||||||
@ -96,8 +109,7 @@ RudimentaryHTMLParser {
|
|||||||
{
|
{
|
||||||
for (Tree<String> node: docu.children)
|
for (Tree<String> node: docu.children)
|
||||||
{
|
{
|
||||||
if (node.key.equals("text")) continue;
|
if (!node.key.equals("tag")) continue;
|
||||||
assert node.key.equals("tag");
|
|
||||||
|
|
||||||
Reader r = new StringReader(node.value);
|
Reader r = new StringReader(node.value);
|
||||||
Tree<String> part = new Tree<String>();
|
Tree<String> part = new Tree<String>();
|
||||||
@ -149,6 +161,61 @@ RudimentaryHTMLParser {
|
|||||||
return docu;
|
return docu;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
it.remove();
|
||||||
|
StringBuilder t = new StringBuilder();
|
||||||
|
StringBuilder e = new StringBuilder();
|
||||||
|
boolean emoji = false;
|
||||||
|
char pc = ' ';
|
||||||
|
for (char c: node.value.toCharArray())
|
||||||
|
{
|
||||||
|
if (!emoji && c == ':')
|
||||||
|
{
|
||||||
|
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) e.append((char)c);
|
||||||
|
else t.append((char)c);
|
||||||
|
pc = c;
|
||||||
|
}
|
||||||
|
if (t.length() > 0) {
|
||||||
|
Tree<String> text = new Tree<String>();
|
||||||
|
text.key = "text";
|
||||||
|
text.value = empty(t);
|
||||||
|
it.add(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return docu;
|
||||||
|
}
|
||||||
|
|
||||||
private static String
|
private static String
|
||||||
empty(StringBuilder b)
|
empty(StringBuilder b)
|
||||||
{
|
{
|
||||||
|
28
TestWindow.java
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
class
|
||||||
|
TestWindow extends JFrame {
|
||||||
|
|
||||||
|
TestWindow()
|
||||||
|
{
|
||||||
|
RichTextPane display = new RichTextPane();
|
||||||
|
setContentPane(display);
|
||||||
|
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
|
||||||
|
setLocationByPlatform(true);
|
||||||
|
setSize(320, 240);
|
||||||
|
setVisible(true);
|
||||||
|
setVisible(false);
|
||||||
|
|
||||||
|
String s = "This is a standard English sentence.";
|
||||||
|
RichTextPane.Builder b = new RichTextPane.Builder();
|
||||||
|
for (String word: s.split(" ")) b = b.text(word).spacer(" ");
|
||||||
|
List<RichTextPane.Segment> text = b.finish();
|
||||||
|
FontMetrics fm = display.getFontMetrics(display.getFont());
|
||||||
|
RichTextPane.layout(text, fm, display.getWidth());
|
||||||
|
display.setText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
76
TimelineWindow.java
Normal file → Executable file
@ -339,54 +339,43 @@ implements ActionListener {
|
|||||||
addee.date = ZonedDateTime.now();
|
addee.date = ZonedDateTime.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
String s2 = addee.html = post.get("content").value;
|
||||||
StringBuilder b = new StringBuilder();
|
StringBuilder b = new StringBuilder();
|
||||||
Tree<String> nodes =
|
Tree<String> nodes = RudimentaryHTMLParser.depthlessRead(s2);
|
||||||
RudimentaryHTMLParser
|
for (Tree<String> node: nodes)
|
||||||
.depthlessRead(post.get("content").value);
|
{
|
||||||
for (Tree<String> node: nodes.children)
|
if (node.key.equals("tag"))
|
||||||
{
|
{
|
||||||
if (node.key.equals("tag"))
|
String tagName = node.get(0).key;
|
||||||
{
|
if (tagName.equals("br")) b.append(" \n ");
|
||||||
if (node.get(0).key.equals("br")) {
|
if (tagName.equals("/p")) b.append(" \n \n ");
|
||||||
b.append(" \n ");
|
}
|
||||||
}
|
if (node.key.equals("text")) b.append(node.value);
|
||||||
if (node.get(0).key.equals("/p")) {
|
if (node.key.equals("emoji")) b.append(node.value);
|
||||||
b.append(" \n \n ");
|
}
|
||||||
}
|
addee.text = b.toString();
|
||||||
}
|
|
||||||
if (node.key.equals("text")) {
|
|
||||||
b.append(node.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addee.text = b.toString();
|
|
||||||
}
|
|
||||||
catch (IOException eIo) {
|
|
||||||
eIo.printStackTrace();
|
|
||||||
assert false;
|
|
||||||
}
|
|
||||||
|
|
||||||
String s = post.get("spoiler_text").value;
|
String s3 = post.get("spoiler_text").value;
|
||||||
if (!s.isEmpty()) addee.contentWarning = s;
|
if (!s3.isEmpty()) addee.contentWarning = s3;
|
||||||
else addee.contentWarning = null;
|
else addee.contentWarning = null;
|
||||||
|
|
||||||
Tree<String> account = post.get("account");
|
Tree<String> account = post.get("account");
|
||||||
addee.authorId = account.get("acct").value;
|
addee.authorId = account.get("acct").value;
|
||||||
addee.authorName = account.get("username").value;
|
addee.authorName = account.get("username").value;
|
||||||
addee.authorNumId = account.get("id").value;
|
addee.authorNumId = account.get("id").value;
|
||||||
String s2 = account.get("display_name").value;
|
String s4 = account.get("display_name").value;
|
||||||
if (!s2.isEmpty()) addee.authorName = s2;
|
if (!s4.isEmpty()) addee.authorName = s4;
|
||||||
String s3 = account.get("avatar").value;
|
String s5 = account.get("avatar").value;
|
||||||
addee.authorAvatar = ImageApi.remote(s3);
|
addee.authorAvatar = ImageApi.remote(s5);
|
||||||
if (addee.authorAvatar == null) {
|
if (addee.authorAvatar == null) {
|
||||||
s3 = "defaultAvatar";
|
s5 = "defaultAvatar";
|
||||||
addee.authorAvatar = ImageApi.local(s3);
|
addee.authorAvatar = ImageApi.local(s5);
|
||||||
}
|
}
|
||||||
|
|
||||||
String f = post.get("favourited").value;
|
String s6 = post.get("favourited").value;
|
||||||
String b = post.get("reblogged").value;
|
String s7 = post.get("reblogged").value;
|
||||||
addee.favourited = f.equals("true");
|
addee.favourited = s6.equals("true");
|
||||||
addee.boosted = b.equals("true");
|
addee.boosted = s7.equals("true");
|
||||||
|
|
||||||
Tree<String> as1 = post.get("media_attachments");
|
Tree<String> as1 = post.get("media_attachments");
|
||||||
Attachment[] as2 = new Attachment[as1.size()];
|
Attachment[] as2 = new Attachment[as1.size()];
|
||||||
@ -406,6 +395,17 @@ implements ActionListener {
|
|||||||
}
|
}
|
||||||
addee.attachments = as2;
|
addee.attachments = as2;
|
||||||
|
|
||||||
|
Tree<String> es1 = post.get("emojis");
|
||||||
|
String[][] es2 = new String[es1.size()][];
|
||||||
|
for (int o = 0; o < es2.length; ++o)
|
||||||
|
{
|
||||||
|
Tree<String> e1 = es1.get(o);
|
||||||
|
String[] e2 = es2[o] = new String[2];
|
||||||
|
e2[0] = e1.get("shortcode").value;
|
||||||
|
e2[1] = e1.get("url").value;
|
||||||
|
}
|
||||||
|
addee.emojiUrls = es2;
|
||||||
|
|
||||||
posts.add(addee);
|
posts.add(addee);
|
||||||
}
|
}
|
||||||
return posts;
|
return posts;
|
||||||
|
0
TimelineWindowUpdater.java
Normal file → Executable file
0
TwoToggleButton.java
Normal file → Executable file
0
graphics/Flags.xcf
Normal file → Executable file
0
graphics/Hourglass.xcf
Normal file → Executable file
0
graphics/button.png
Normal file → Executable file
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
0
graphics/disabledOverlay.png
Normal file → Executable file
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
0
graphics/favouriteToggled.png
Normal file → Executable file
Before Width: | Height: | Size: 353 B After Width: | Height: | Size: 353 B |
0
graphics/ref1.png
Normal file → Executable file
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
0
graphics/selectedOverlay.png
Normal file → Executable file
Before Width: | Height: | Size: 313 B After Width: | Height: | Size: 313 B |
0
graphics/test1.png
Normal file → Executable file
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
0
graphics/test2.png
Normal file → Executable file
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
0
graphics/test3.png
Normal file → Executable file
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
0
graphics/test4.png
Normal file → Executable file
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |