Refactored back to objects. Moved entity unpacking to object classes.
Added composition length counter. Fixed timeline background image bug.
0
ClipboardApi.java
Executable file → Normal file
56
ComposeWindow.java
Executable file → Normal file
@ -15,8 +15,12 @@ import java.awt.BorderLayout;
|
|||||||
import java.awt.Dimension;
|
import java.awt.Dimension;
|
||||||
import java.awt.event.ActionListener;
|
import java.awt.event.ActionListener;
|
||||||
import java.awt.event.ActionEvent;
|
import java.awt.event.ActionEvent;
|
||||||
|
import java.awt.event.KeyListener;
|
||||||
|
import java.awt.event.KeyEvent;
|
||||||
import java.awt.Cursor;
|
import java.awt.Cursor;
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
|
import javax.swing.event.CaretListener;
|
||||||
|
import javax.swing.event.CaretEvent;
|
||||||
|
|
||||||
import cafe.biskuteri.hinoki.Tree;
|
import cafe.biskuteri.hinoki.Tree;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -92,7 +96,7 @@ ComposeWindow extends JFrame {
|
|||||||
ComposeWindow.this,
|
ComposeWindow.this,
|
||||||
"Tried to submit post, failed..."
|
"Tried to submit post, failed..."
|
||||||
+ "\n" + json.get("error").value
|
+ "\n" + json.get("error").value
|
||||||
+ "(HTTP error code: " + httpCode + ")"
|
+ "\n(HTTP error code: " + httpCode + ")"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,7 +198,7 @@ ComposeWindow extends JFrame {
|
|||||||
|
|
||||||
class
|
class
|
||||||
ComposeComponent extends JPanel
|
ComposeComponent extends JPanel
|
||||||
implements ActionListener {
|
implements ActionListener, CaretListener, KeyListener {
|
||||||
|
|
||||||
private ComposeWindow
|
private ComposeWindow
|
||||||
primaire;
|
primaire;
|
||||||
@ -207,6 +211,9 @@ implements ActionListener {
|
|||||||
private JTextField
|
private JTextField
|
||||||
reply, contentWarning;
|
reply, contentWarning;
|
||||||
|
|
||||||
|
private JLabel
|
||||||
|
textLength;
|
||||||
|
|
||||||
private JComboBox<String>
|
private JComboBox<String>
|
||||||
visibility;
|
visibility;
|
||||||
|
|
||||||
@ -294,6 +301,42 @@ implements ActionListener {
|
|||||||
public void
|
public void
|
||||||
actionPerformed(ActionEvent eA) { primaire.submit(); }
|
actionPerformed(ActionEvent eA) { primaire.submit(); }
|
||||||
|
|
||||||
|
public void
|
||||||
|
caretUpdate(CaretEvent eCa) { updateTextLength(); }
|
||||||
|
|
||||||
|
public void
|
||||||
|
keyPressed(KeyEvent eK) { updateTextLength(); }
|
||||||
|
|
||||||
|
public void
|
||||||
|
keyReleased(KeyEvent eK) { }
|
||||||
|
|
||||||
|
public void
|
||||||
|
keyTyped(KeyEvent eK) { }
|
||||||
|
|
||||||
|
private void
|
||||||
|
updateTextLength()
|
||||||
|
{
|
||||||
|
int length = text.getText().length();
|
||||||
|
/*
|
||||||
|
* The web interface doesn't do this expensive thing.
|
||||||
|
* It has an upwards counter, incremented by I'm not
|
||||||
|
* sure what. Presumably they have some control over
|
||||||
|
* the text input. I'd rather not, cause I use a
|
||||||
|
* Japanese IME, I'm going to see how laggy this is.
|
||||||
|
* It raises our app's system requirements, but, I was
|
||||||
|
* going to transition it to multithreading anyways,
|
||||||
|
* I don't think we're going to be very cheap.. Which
|
||||||
|
* sucks, but the Mastodon API is not helping us here.
|
||||||
|
*/
|
||||||
|
textLength.setText(Integer.toString(length));
|
||||||
|
/*
|
||||||
|
* Another thing I could do is temporarily move the
|
||||||
|
* caret to the end and then find its position, then
|
||||||
|
* seek back. Not sure how much that would help, but
|
||||||
|
* if this is too laggy, that's what I'd try next.
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
// ---%-@-%---
|
// ---%-@-%---
|
||||||
|
|
||||||
ComposeComponent(ComposeWindow primaire)
|
ComposeComponent(ComposeWindow primaire)
|
||||||
@ -321,6 +364,9 @@ implements ActionListener {
|
|||||||
top.add(cwLabel);
|
top.add(cwLabel);
|
||||||
top.add(contentWarning);
|
top.add(contentWarning);
|
||||||
|
|
||||||
|
textLength = new JLabel("0");
|
||||||
|
textLength.setFont(textLength.getFont().deriveFont(14f));
|
||||||
|
|
||||||
visibility = new JComboBox<>(new String[] {
|
visibility = new JComboBox<>(new String[] {
|
||||||
"Public",
|
"Public",
|
||||||
"Unlisted",
|
"Unlisted",
|
||||||
@ -335,8 +381,10 @@ implements ActionListener {
|
|||||||
|
|
||||||
Box bottom = Box.createHorizontalBox();
|
Box bottom = Box.createHorizontalBox();
|
||||||
bottom.add(Box.createGlue());
|
bottom.add(Box.createGlue());
|
||||||
|
bottom.add(textLength);
|
||||||
|
bottom.add(Box.createHorizontalStrut(12));
|
||||||
bottom.add(visibility);
|
bottom.add(visibility);
|
||||||
bottom.add(Box.createHorizontalStrut(8));
|
bottom.add(Box.createHorizontalStrut(12));
|
||||||
bottom.add(submit);
|
bottom.add(submit);
|
||||||
|
|
||||||
text = new JTextArea();
|
text = new JTextArea();
|
||||||
@ -344,6 +392,8 @@ implements ActionListener {
|
|||||||
text.setWrapStyleWord(true);
|
text.setWrapStyleWord(true);
|
||||||
text.setFont(text.getFont().deriveFont(16f));
|
text.setFont(text.getFont().deriveFont(16f));
|
||||||
text.setBorder(bc);
|
text.setBorder(bc);
|
||||||
|
text.addCaretListener(this);
|
||||||
|
text.addKeyListener(this);
|
||||||
|
|
||||||
setLayout(new BorderLayout(0, 8));
|
setLayout(new BorderLayout(0, 8));
|
||||||
add(top, BorderLayout.NORTH);
|
add(top, BorderLayout.NORTH);
|
||||||
|
24
ImageApi.java
Executable file → Normal file
@ -11,8 +11,8 @@ ImageApi {
|
|||||||
public static Image
|
public static Image
|
||||||
local(String name)
|
local(String name)
|
||||||
{
|
{
|
||||||
String path = "/graphics/" + name + ".png";
|
String path = "graphics/" + name + ".png";
|
||||||
URL url = ImageApi.class.getResource(name);
|
URL url = ImageApi.class.getResource(path);
|
||||||
if (url == null) return null;
|
if (url == null) return null;
|
||||||
return new ImageIcon(url).getImage();
|
return new ImageIcon(url).getImage();
|
||||||
}
|
}
|
||||||
@ -20,12 +20,28 @@ ImageApi {
|
|||||||
public static Image
|
public static Image
|
||||||
remote(String urlr)
|
remote(String urlr)
|
||||||
{
|
{
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
URL url = new URL(urlr);
|
URL url = new URL(urlr);
|
||||||
Toolkit TK = Toolkit.getDefaultToolkit();
|
Toolkit TK = Toolkit.getDefaultToolkit();
|
||||||
return TK.createImage(url);
|
return TK.createImage(url);
|
||||||
}
|
}
|
||||||
catch (MalformedURLException eMu) {
|
catch (MalformedURLException eMu)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ImageIcon
|
||||||
|
iconRemote(String urlr)
|
||||||
|
{
|
||||||
|
if (urlr == null) return null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return new ImageIcon(new URL(urlr));
|
||||||
|
}
|
||||||
|
catch (MalformedURLException eMu)
|
||||||
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
0
ImageWindow.java
Executable file → Normal file
438
JKomasto.java
Executable file → Normal file
@ -2,12 +2,21 @@
|
|||||||
import javax.swing.JFrame;
|
import javax.swing.JFrame;
|
||||||
import javax.swing.JPanel;
|
import javax.swing.JPanel;
|
||||||
import javax.swing.JComponent;
|
import javax.swing.JComponent;
|
||||||
|
import javax.swing.ImageIcon;
|
||||||
import java.awt.Dimension;
|
import java.awt.Dimension;
|
||||||
import java.awt.BorderLayout;
|
import java.awt.BorderLayout;
|
||||||
import java.awt.Cursor;
|
import java.awt.Cursor;
|
||||||
import java.awt.Image;
|
import java.awt.Image;
|
||||||
|
import java.awt.FontMetrics;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.text.BreakIterator;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.Period;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.format.DateTimeParseException;
|
||||||
import cafe.biskuteri.hinoki.Tree;
|
import cafe.biskuteri.hinoki.Tree;
|
||||||
|
|
||||||
|
|
||||||
@ -126,7 +135,8 @@ TimelineType {
|
|||||||
FEDERATED,
|
FEDERATED,
|
||||||
LOCAL,
|
LOCAL,
|
||||||
HOME,
|
HOME,
|
||||||
LIST
|
LIST,
|
||||||
|
PROFILE
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,9 +164,22 @@ TimelinePage {
|
|||||||
public String
|
public String
|
||||||
accountNumId, listId;
|
accountNumId, listId;
|
||||||
|
|
||||||
public Tree<String>
|
public Post[]
|
||||||
posts;
|
posts;
|
||||||
|
|
||||||
|
// ---%-@-%---
|
||||||
|
|
||||||
|
public
|
||||||
|
TimelinePage() { }
|
||||||
|
|
||||||
|
public
|
||||||
|
TimelinePage(Tree<String> entity)
|
||||||
|
{
|
||||||
|
posts = new Post[entity.size()];
|
||||||
|
for (int o = 0; o < posts.length; ++o)
|
||||||
|
posts[o] = new Post(entity.get(o));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -175,9 +198,313 @@ Notification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class
|
||||||
|
Post {
|
||||||
|
|
||||||
|
public String
|
||||||
|
id,
|
||||||
|
uri;
|
||||||
|
|
||||||
|
public Account
|
||||||
|
author;
|
||||||
|
|
||||||
|
public PostVisibility
|
||||||
|
visibility;
|
||||||
|
|
||||||
|
public String
|
||||||
|
text,
|
||||||
|
approximateText;
|
||||||
|
|
||||||
|
public List<RichTextPane.Segment>
|
||||||
|
formattedText,
|
||||||
|
laidoutText;
|
||||||
|
|
||||||
|
public String
|
||||||
|
contentWarning;
|
||||||
|
|
||||||
|
public ZonedDateTime
|
||||||
|
dateTime;
|
||||||
|
|
||||||
|
public String
|
||||||
|
date, time, relativeTime;
|
||||||
|
|
||||||
|
public boolean
|
||||||
|
boosted,
|
||||||
|
favourited;
|
||||||
|
|
||||||
|
public Post
|
||||||
|
boostedPost;
|
||||||
|
|
||||||
|
public Attachment[]
|
||||||
|
attachments;
|
||||||
|
|
||||||
|
public String[][]
|
||||||
|
emojiUrls;
|
||||||
|
|
||||||
|
// - -%- -
|
||||||
|
|
||||||
|
private static final DateTimeFormatter
|
||||||
|
DATE_FORMAT = DateTimeFormatter.ofPattern("d LLLL ''uu"),
|
||||||
|
TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm");
|
||||||
|
|
||||||
|
// ---%-@-%---
|
||||||
|
|
||||||
|
public void
|
||||||
|
resolveApproximateText()
|
||||||
|
{
|
||||||
|
assert text != null;
|
||||||
|
if (approximateText != null) return;
|
||||||
|
|
||||||
|
Tree<String> nodes;
|
||||||
|
nodes = RudimentaryHTMLParser.depthlessRead(text);
|
||||||
|
if (nodes.size() == 0)
|
||||||
|
{
|
||||||
|
approximateText = "-";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder b = new StringBuilder();
|
||||||
|
Tree<String> first = nodes.get(0);
|
||||||
|
for (Tree<String> node: nodes)
|
||||||
|
{
|
||||||
|
if (node.key.equals("tag"))
|
||||||
|
{
|
||||||
|
if (node.get(0).key.equals("br"))
|
||||||
|
b.append("; ");
|
||||||
|
if (node.get(0).key.equals("p") && node != first)
|
||||||
|
b.append("; ");
|
||||||
|
}
|
||||||
|
if (node.key.equals("emoji"))
|
||||||
|
{
|
||||||
|
b.append(":" + node.value + ":");
|
||||||
|
}
|
||||||
|
if (node.key.equals("text"))
|
||||||
|
{
|
||||||
|
b.append(node.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
approximateText = b.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void
|
||||||
|
resolveFormattedText()
|
||||||
|
{
|
||||||
|
assert text != null;
|
||||||
|
assert emojiUrls != null;
|
||||||
|
if (formattedText != null) return;
|
||||||
|
|
||||||
|
RichTextPane.Builder b = new RichTextPane.Builder();
|
||||||
|
Tree<String> nodes;
|
||||||
|
nodes = RudimentaryHTMLParser.depthlessRead(text);
|
||||||
|
for (Tree<String> node: nodes)
|
||||||
|
{
|
||||||
|
if (node.key.equals("tag"))
|
||||||
|
{
|
||||||
|
String tagName = node.get(0).key;
|
||||||
|
Tree<String> href = node.get("href");
|
||||||
|
|
||||||
|
if (tagName.equals("br"))
|
||||||
|
b = b.spacer("\n");
|
||||||
|
if (tagName.equals("/p"))
|
||||||
|
b = b.spacer("\n").spacer("\n");
|
||||||
|
if (tagName.equals("a"))
|
||||||
|
b = b.link(href.value, null).spacer(" ");
|
||||||
|
}
|
||||||
|
if (node.key.equals("text"))
|
||||||
|
{
|
||||||
|
BreakIterator it;
|
||||||
|
it = BreakIterator.getWordInstance(Locale.ROOT);
|
||||||
|
it.setText(node.value);
|
||||||
|
int start = it.first(), end = it.next();
|
||||||
|
while (end != BreakIterator.DONE)
|
||||||
|
{
|
||||||
|
String word = text.substring(start, end);
|
||||||
|
char c = word.isEmpty() ? ' ' : word.charAt(0);
|
||||||
|
boolean w = Character.isWhitespace(c);
|
||||||
|
b = w ? b.spacer(word) : b.text(word);
|
||||||
|
start = end;
|
||||||
|
end = it.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (node.key.equals("emoji"))
|
||||||
|
{
|
||||||
|
String shortcode = node.value;
|
||||||
|
|
||||||
|
String url = null;
|
||||||
|
for (String[] mapping: emojiUrls)
|
||||||
|
{
|
||||||
|
String ms = mapping[0];
|
||||||
|
String mu = mapping[1];
|
||||||
|
if (ms.equals(shortcode)) url = mu;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageIcon icon = ImageApi.iconRemote(url);
|
||||||
|
if (icon != null) b = b.image(icon, node.value);
|
||||||
|
else b = b.text(":" + node.value + ":");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
formattedText = b.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int
|
||||||
|
resolveLaidoutText(int width, FontMetrics fm)
|
||||||
|
{
|
||||||
|
assert formattedText != null;
|
||||||
|
|
||||||
|
laidoutText = RichTextPane.layout(formattedText, fm, width);
|
||||||
|
|
||||||
|
int maxY = 0;
|
||||||
|
for (RichTextPane.Segment segment: laidoutText)
|
||||||
|
if (segment.y > maxY) maxY = segment.y;
|
||||||
|
|
||||||
|
return maxY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void
|
||||||
|
resolveRelativeTime()
|
||||||
|
{
|
||||||
|
assert date != null;
|
||||||
|
|
||||||
|
ZonedDateTime now = ZonedDateTime.now();
|
||||||
|
long d = ChronoUnit.SECONDS.between(dateTime, now);
|
||||||
|
long s = Math.abs(d);
|
||||||
|
if (s < 30) relativeTime = "now";
|
||||||
|
else if (s < 60) relativeTime = d + "s";
|
||||||
|
else if (s < 3600) relativeTime = (d / 60) + "m";
|
||||||
|
else if (s < 86400) relativeTime = (d / 3600) + "h";
|
||||||
|
else relativeTime = (d / 86400) + "d";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---%-@-%---
|
||||||
|
|
||||||
|
public
|
||||||
|
Post() { }
|
||||||
|
|
||||||
|
public
|
||||||
|
Post(Tree<String> entity)
|
||||||
|
{
|
||||||
|
id = entity.get("id").value;
|
||||||
|
|
||||||
|
uri = entity.get("url").value;
|
||||||
|
if (uri == null) uri = entity.get("uri").value;
|
||||||
|
|
||||||
|
author = new Account(entity.get("account"));
|
||||||
|
|
||||||
|
String v = entity.get("visibility").value;
|
||||||
|
boolean p = v.equals("public");
|
||||||
|
boolean u = v.equals("unlisted");
|
||||||
|
boolean f = v.equals("private");
|
||||||
|
boolean m = v.equals("direct");
|
||||||
|
if (p) visibility = PostVisibility.PUBLIC;
|
||||||
|
if (u) visibility = PostVisibility.UNLISTED;
|
||||||
|
if (f) visibility = PostVisibility.FOLLOWERS;
|
||||||
|
if (m) visibility = PostVisibility.MENTIONED;
|
||||||
|
|
||||||
|
dateTime =
|
||||||
|
ZonedDateTime.parse(entity.get("created_at").value)
|
||||||
|
.withZoneSameInstant(ZoneId.systemDefault());
|
||||||
|
date = DATE_FORMAT.format(dateTime);
|
||||||
|
time = TIME_FORMAT.format(dateTime);
|
||||||
|
|
||||||
|
text = entity.get("content").value;
|
||||||
|
String st = entity.get("spoiler_text").value;
|
||||||
|
contentWarning = st.trim().isEmpty() ? null : st;
|
||||||
|
|
||||||
|
String favourited = entity.get("favourited").value;
|
||||||
|
String boosted = entity.get("reblogged").value;
|
||||||
|
this.favourited = favourited.equals("true");
|
||||||
|
this.boosted = boosted.equals("true");
|
||||||
|
|
||||||
|
Tree<String> media = entity.get("media_attachments");
|
||||||
|
attachments = new Attachment[media.size()];
|
||||||
|
for (int o = 0; o < attachments.length; ++o)
|
||||||
|
{
|
||||||
|
attachments[o] = new Attachment(media.get(o));
|
||||||
|
}
|
||||||
|
|
||||||
|
Tree<String> emojis = entity.get("emojis");
|
||||||
|
emojiUrls = new String[emojis.size()][];
|
||||||
|
for (int o = 0; o < emojiUrls.length; ++o)
|
||||||
|
{
|
||||||
|
Tree<String> emoji = emojis.get(o);
|
||||||
|
String[] mapping = emojiUrls[o] = new String[2];
|
||||||
|
mapping[0] = emoji.get("shortcode").value;
|
||||||
|
mapping[1] = emoji.get("url").value;
|
||||||
|
}
|
||||||
|
|
||||||
|
Tree<String> boostedPost = entity.get("reblog");
|
||||||
|
if (boostedPost.size() > 0)
|
||||||
|
this.boostedPost = new Post(boostedPost);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class
|
||||||
|
Account {
|
||||||
|
|
||||||
|
public String
|
||||||
|
numId;
|
||||||
|
|
||||||
|
public String
|
||||||
|
id,
|
||||||
|
name;
|
||||||
|
|
||||||
|
public List<RichTextPane.Segment>
|
||||||
|
formattedName;
|
||||||
|
|
||||||
|
public String
|
||||||
|
avatarUrl;
|
||||||
|
|
||||||
|
public Image
|
||||||
|
avatar;
|
||||||
|
|
||||||
|
// ---%-@-%---
|
||||||
|
|
||||||
|
public void
|
||||||
|
resolveFormattedName()
|
||||||
|
{
|
||||||
|
assert name != null;
|
||||||
|
formattedName =
|
||||||
|
new RichTextPane.Builder().text(name).finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void
|
||||||
|
resolveAvatar()
|
||||||
|
{
|
||||||
|
assert avatarUrl != null;
|
||||||
|
if (avatar != null) return;
|
||||||
|
avatar = ImageApi.remote(avatarUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---%-@-%---
|
||||||
|
|
||||||
|
public
|
||||||
|
Account() { }
|
||||||
|
|
||||||
|
public
|
||||||
|
Account(Tree<String> entity)
|
||||||
|
{
|
||||||
|
numId = entity.get("id").value;
|
||||||
|
id = entity.get("acct").value;
|
||||||
|
|
||||||
|
String displayName = entity.get("display_name").value;
|
||||||
|
String username = entity.get("username").value;
|
||||||
|
name = displayName.isEmpty() ? username : displayName;
|
||||||
|
|
||||||
|
avatarUrl = entity.get("avatar").value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class
|
class
|
||||||
Attachment {
|
Attachment {
|
||||||
|
|
||||||
|
public String
|
||||||
|
id;
|
||||||
|
|
||||||
public String
|
public String
|
||||||
type;
|
type;
|
||||||
|
|
||||||
@ -190,6 +517,34 @@ Attachment {
|
|||||||
public Image
|
public Image
|
||||||
image;
|
image;
|
||||||
|
|
||||||
|
// ---%-@-%---
|
||||||
|
|
||||||
|
public void
|
||||||
|
resolveImage()
|
||||||
|
{
|
||||||
|
assert url != null;
|
||||||
|
if (image != null) return;
|
||||||
|
if (!type.equals("image")) return;
|
||||||
|
image = ImageApi.remote(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---%-@-%---
|
||||||
|
|
||||||
|
public
|
||||||
|
Attachment() { }
|
||||||
|
|
||||||
|
public
|
||||||
|
Attachment(Tree<String> entity)
|
||||||
|
{
|
||||||
|
String u1 = entity.get("remote_url").value;
|
||||||
|
String u2 = entity.get("text_url").value;
|
||||||
|
String u3 = entity.get("url").value;
|
||||||
|
|
||||||
|
url = u1 != null ? u1 : u2 != null ? u2 : u3;
|
||||||
|
type = entity.get("type").value;
|
||||||
|
description = entity.get("description").value;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -207,4 +562,83 @@ Composition {
|
|||||||
public String
|
public String
|
||||||
replyToPostId;
|
replyToPostId;
|
||||||
|
|
||||||
|
// ---%-@-%---
|
||||||
|
|
||||||
|
public
|
||||||
|
Composition() { }
|
||||||
|
|
||||||
|
public static Composition
|
||||||
|
reply(Tree<String> entity, String ownNumId)
|
||||||
|
{
|
||||||
|
Composition c = new Composition();
|
||||||
|
|
||||||
|
Tree<String> boosted = entity.get("reblog");
|
||||||
|
if (boosted.size() > 0) entity = boosted;
|
||||||
|
|
||||||
|
String st = entity.get("spoiler_text").value;
|
||||||
|
String ri = entity.get("id").value;
|
||||||
|
c.contentWarning = st.trim().isEmpty() ? null : st;
|
||||||
|
c.replyToPostId = ri.trim().isEmpty() ? null : ri;
|
||||||
|
|
||||||
|
Tree<String> author = entity.get("account");
|
||||||
|
String authorId = author.get("acct").value;
|
||||||
|
String authorNumId = author.get("id").value;
|
||||||
|
c.text = "";
|
||||||
|
if (!authorNumId.equals(ownNumId))
|
||||||
|
c.text = "@" + authorId + " ";
|
||||||
|
|
||||||
|
String visibility = entity.get("visibility").value;
|
||||||
|
boolean p = visibility.equals("public");
|
||||||
|
boolean u = visibility.equals("unlisted");
|
||||||
|
boolean f = visibility.equals("private");
|
||||||
|
boolean m = visibility.equals("direct");
|
||||||
|
assert p || u || f || m;
|
||||||
|
if (p) c.visibility = PostVisibility.PUBLIC;
|
||||||
|
if (u) c.visibility = PostVisibility.UNLISTED;
|
||||||
|
if (f) c.visibility = PostVisibility.FOLLOWERS;
|
||||||
|
if (m) c.visibility = PostVisibility.MENTIONED;
|
||||||
|
// Less eye strain arranged this way.
|
||||||
|
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Composition
|
||||||
|
recover(Tree<String> entity)
|
||||||
|
{
|
||||||
|
assert entity.get("text") != null;
|
||||||
|
|
||||||
|
Composition c = new Composition();
|
||||||
|
c.text = entity.get("text").value;
|
||||||
|
c.contentWarning = entity.get("spoiler_text").value;
|
||||||
|
c.replyToPostId = entity.get("in_reply_to_id").value;
|
||||||
|
|
||||||
|
String visibility = entity.get("visibility").value;
|
||||||
|
boolean p = visibility.equals("public");
|
||||||
|
boolean u = visibility.equals("unlisted");
|
||||||
|
boolean f = visibility.equals("private");
|
||||||
|
boolean m = visibility.equals("direct");
|
||||||
|
assert p || u || f || m;
|
||||||
|
if (p) c.visibility = PostVisibility.PUBLIC;
|
||||||
|
if (u) c.visibility = PostVisibility.UNLISTED;
|
||||||
|
if (f) c.visibility = PostVisibility.FOLLOWERS;
|
||||||
|
if (m) c.visibility = PostVisibility.MENTIONED;
|
||||||
|
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Composition
|
||||||
|
reply(Post post, String ownNumId)
|
||||||
|
{
|
||||||
|
if (post.boostedPost != null) post = post.boostedPost;
|
||||||
|
|
||||||
|
Composition c = new Composition();
|
||||||
|
c.replyToPostId = post.id;
|
||||||
|
c.visibility = post.visibility;
|
||||||
|
c.contentWarning = post.contentWarning;
|
||||||
|
c.text = "";
|
||||||
|
if (!post.author.numId.equals(ownNumId))
|
||||||
|
c.text = "@" + post.author.id + " ";
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
0
KDE_Dialog_Appear.wav
Executable file → Normal file
0
LoginWindow.java
Executable file → Normal file
0
MastodonApi.java
Executable file → Normal file
11
NotificationsWindow.java
Executable file → Normal file
@ -87,13 +87,10 @@ NotificationsWindow extends JFrame {
|
|||||||
|
|
||||||
if (n.type != NotificationType.FOLLOW)
|
if (n.type != NotificationType.FOLLOW)
|
||||||
{
|
{
|
||||||
Tree<String> post = t.get("status");
|
Post post = new Post(t.get("status"));
|
||||||
String pid, phtml, ptext;
|
post.resolveApproximateText();
|
||||||
pid = post.get("id").value;
|
n.postId = post.id;
|
||||||
phtml = post.get("content").value;
|
n.postText = post.approximateText;
|
||||||
ptext = TimelineWindow.textApproximation(phtml);
|
|
||||||
n.postId = pid;
|
|
||||||
n.postText = ptext;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
notifications.add(n);
|
notifications.add(n);
|
||||||
|
274
PostWindow.java
Executable file → Normal file
@ -49,8 +49,9 @@ PostWindow extends JFrame {
|
|||||||
private MastodonApi
|
private MastodonApi
|
||||||
api;
|
api;
|
||||||
|
|
||||||
private Tree<String>
|
private Post
|
||||||
post;
|
post,
|
||||||
|
wrapperPost;
|
||||||
|
|
||||||
// - -%- -
|
// - -%- -
|
||||||
|
|
||||||
@ -65,78 +66,67 @@ PostWindow extends JFrame {
|
|||||||
|
|
||||||
// ---%-@-%---
|
// ---%-@-%---
|
||||||
|
|
||||||
public void
|
public void
|
||||||
readEntity(Tree<String> post)
|
use(Post post)
|
||||||
{
|
{
|
||||||
this.post = post;
|
assert post != null;
|
||||||
|
|
||||||
Tree<String> boosted = post.get("reblog");
|
if (post.boostedPost != null)
|
||||||
if (boosted.size() > 0) post = boosted;
|
{
|
||||||
|
wrapperPost = post;
|
||||||
|
this.post = post.boostedPost;
|
||||||
|
post = post.boostedPost;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
wrapperPost = null;
|
||||||
|
this.post = post;
|
||||||
|
}
|
||||||
|
|
||||||
Tree<String> author = post.get("account");
|
display.setAuthorName(post.author.name);
|
||||||
Tree<String> emojis = post.get("emojis");
|
display.setAuthorId(post.author.id);
|
||||||
Tree<String> media = post.get("media_attachments");
|
|
||||||
|
|
||||||
String an = author.get("display_name").value;
|
|
||||||
if (an.isEmpty()) an = author.get("username").value;
|
|
||||||
display.setAuthorName(an);
|
|
||||||
display.setAuthorId(author.get("acct").value);
|
|
||||||
|
|
||||||
String aid = author.get("id").value;
|
|
||||||
String oid = api.getAccountDetails().get("id").value;
|
String oid = api.getAccountDetails().get("id").value;
|
||||||
|
String aid = post.author.numId;
|
||||||
display.setDeleteEnabled(aid.equals(oid));
|
display.setDeleteEnabled(aid.equals(oid));
|
||||||
|
|
||||||
String avurl = author.get("avatar").value;
|
post.author.resolveAvatar();
|
||||||
display.setAuthorAvatar(ImageApi.remote(avurl));
|
display.setAuthorAvatar(post.author.avatar);
|
||||||
|
|
||||||
String sdate = post.get("created_at").value;
|
display.setDate(post.date);
|
||||||
ZonedDateTime date = ZonedDateTime.parse(sdate);
|
display.setTime(post.time);
|
||||||
date = date.withZoneSameInstant(ZoneId.systemDefault());
|
|
||||||
display.setDate(DATE_FORMAT.format(date));
|
|
||||||
display.setTime(TIME_FORMAT.format(date));
|
|
||||||
|
|
||||||
String[][] emojiUrls = new String[emojis.size()][];
|
display.setEmojiUrls(post.emojiUrls);
|
||||||
for (int o = 0; o < emojiUrls.length; ++o) {
|
|
||||||
Tree<String> emoji = emojis.get(o);
|
|
||||||
emojiUrls[o] = new String[2];
|
|
||||||
emojiUrls[o][0] = emoji.get("shortcode").value;
|
|
||||||
emojiUrls[o][1] = emoji.get("url").value;
|
|
||||||
}
|
|
||||||
display.setEmojiUrls(emojiUrls);
|
|
||||||
|
|
||||||
display.setHtml(post.get("content").value);
|
display.setHtml(post.text);
|
||||||
boolean f = post.get("favourited").value.equals("true");
|
display.setFavourited(post.favourited);
|
||||||
boolean b = post.get("reblogged").value.equals("true");
|
display.setBoosted(post.boosted);
|
||||||
display.setFavourited(f);
|
|
||||||
display.setBoosted(b);
|
|
||||||
|
|
||||||
if (media.size() > 0)
|
if (post.attachments.length > 0)
|
||||||
{
|
{
|
||||||
Tree<String> first = media.get(0);
|
post.attachments[0].resolveImage();
|
||||||
String u1 = first.get("remote_url").value;
|
display.setMediaPreview(post.attachments[0].image);
|
||||||
String u2 = first.get("text_url").value;
|
}
|
||||||
String u3 = first.get("url").value;
|
else display.setMediaPreview(null);
|
||||||
String purl = u1 != null ? u1 : u2 != null ? u2 : u3;
|
|
||||||
display.setMediaPreview(ImageApi.remote(purl));
|
|
||||||
}
|
|
||||||
else display.setMediaPreview(null);
|
|
||||||
|
|
||||||
String html = post.get("content").value;
|
post.resolveApproximateText();
|
||||||
setTitle(TimelineWindow.textApproximation(html));
|
this.setTitle(post.approximateText);
|
||||||
|
|
||||||
display.resetFocus();
|
display.resetFocus();
|
||||||
repaint();
|
repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void
|
||||||
|
readEntity(Tree<String> post)
|
||||||
|
{
|
||||||
|
use(new Post(post));
|
||||||
|
}
|
||||||
|
|
||||||
public void
|
public void
|
||||||
openAuthorProfile()
|
openAuthorProfile()
|
||||||
{
|
{
|
||||||
Tree<String> post = this.post;
|
|
||||||
Tree<String> boosted = post.get("reblog");
|
|
||||||
if (boosted.size() > 0) post = boosted;
|
|
||||||
|
|
||||||
TimelineWindow w = new TimelineWindow(primaire);
|
TimelineWindow w = new TimelineWindow(primaire);
|
||||||
w.showAuthorPosts(post.get("account").get("id").value);
|
w.showAuthorPosts(post.author.numId);
|
||||||
w.showLatestPage();
|
w.showLatestPage();
|
||||||
w.setLocationRelativeTo(this);
|
w.setLocationRelativeTo(this);
|
||||||
w.setVisible(true);
|
w.setVisible(true);
|
||||||
@ -145,11 +135,7 @@ PostWindow extends JFrame {
|
|||||||
public void
|
public void
|
||||||
favourite(boolean favourited)
|
favourite(boolean favourited)
|
||||||
{
|
{
|
||||||
Tree<String> post = this.post;
|
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
|
||||||
Tree<String> boosted = post.get("reblog");
|
|
||||||
if (boosted.size() > 0) post = boosted;
|
|
||||||
|
|
||||||
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
|
|
||||||
display.setFavouriteBoostEnabled(false);
|
display.setFavouriteBoostEnabled(false);
|
||||||
display.paintImmediately(display.getBounds());
|
display.paintImmediately(display.getBounds());
|
||||||
RequestListener handler = new RequestListener() {
|
RequestListener handler = new RequestListener() {
|
||||||
@ -178,13 +164,11 @@ PostWindow extends JFrame {
|
|||||||
public void
|
public void
|
||||||
requestSucceeded(Tree<String> json)
|
requestSucceeded(Tree<String> json)
|
||||||
{
|
{
|
||||||
String n = Boolean.toString(favourited);
|
PostWindow.this.post.favourited = favourited;
|
||||||
PostWindow.this.post.get("favourited").value = n;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
String postId = post.get("id").value;
|
api.setPostFavourited(post.id, favourited, handler);
|
||||||
api.setPostFavourited(postId, favourited, handler);
|
|
||||||
display.setCursor(null);
|
display.setCursor(null);
|
||||||
display.setFavouriteBoostEnabled(true);
|
display.setFavouriteBoostEnabled(true);
|
||||||
display.repaint();
|
display.repaint();
|
||||||
@ -193,10 +177,6 @@ PostWindow extends JFrame {
|
|||||||
public void
|
public void
|
||||||
boost(boolean boosted)
|
boost(boolean boosted)
|
||||||
{
|
{
|
||||||
Tree<String> post = this.post;
|
|
||||||
Tree<String> boosted2 = post.get("reblog");
|
|
||||||
if (boosted2.size() > 0) post = boosted2;
|
|
||||||
|
|
||||||
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
|
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
|
||||||
display.setFavouriteBoostEnabled(false);
|
display.setFavouriteBoostEnabled(false);
|
||||||
display.paintImmediately(display.getBounds());
|
display.paintImmediately(display.getBounds());
|
||||||
@ -226,13 +206,11 @@ PostWindow extends JFrame {
|
|||||||
public void
|
public void
|
||||||
requestSucceeded(Tree<String> json)
|
requestSucceeded(Tree<String> json)
|
||||||
{
|
{
|
||||||
String n = Boolean.toString(boosted);
|
PostWindow.this.post.boosted = boosted;
|
||||||
PostWindow.this.post.get("reblogged").value = n;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
String postId = post.get("id").value;
|
api.setPostBoosted(post.id, boosted, handler);
|
||||||
api.setPostBoosted(postId, boosted, handler);
|
|
||||||
display.setCursor(null);
|
display.setCursor(null);
|
||||||
display.setFavouriteBoostEnabled(true);
|
display.setFavouriteBoostEnabled(true);
|
||||||
display.repaint();
|
display.repaint();
|
||||||
@ -241,66 +219,28 @@ PostWindow extends JFrame {
|
|||||||
public void
|
public void
|
||||||
reply()
|
reply()
|
||||||
{
|
{
|
||||||
Tree<String> post = this.post;
|
String ownId = api.getAccountDetails().get("id").value;
|
||||||
Tree<String> boosted = post.get("reblog");
|
Composition c = Composition.reply(this.post, ownId);
|
||||||
if (boosted.size() > 0) post = boosted;
|
|
||||||
|
|
||||||
String authorId = post.get("account").get("acct").value;
|
|
||||||
String postId = post.get("id").value;
|
|
||||||
String cw = post.get("spoiler_text").value;
|
|
||||||
|
|
||||||
String id1 = post.get("account").get("id").value;
|
|
||||||
String id2 = api.getAccountDetails().get("id").value;
|
|
||||||
|
|
||||||
String vs = post.get("visibility").value;
|
|
||||||
PostVisibility v = null;
|
|
||||||
if (vs.equals("public")) v = PostVisibility.PUBLIC;
|
|
||||||
if (vs.equals("unlisted")) v = PostVisibility.UNLISTED;
|
|
||||||
if (vs.equals("private")) v = PostVisibility.FOLLOWERS;
|
|
||||||
if (vs.equals("direct")) v = PostVisibility.MENTIONED;
|
|
||||||
|
|
||||||
Composition c = new Composition();
|
|
||||||
c.contentWarning = cw;
|
|
||||||
c.text = id1.equals(id2) ? "" : "@" + authorId + " ";
|
|
||||||
c.visibility = v;
|
|
||||||
c.replyToPostId = postId;
|
|
||||||
|
|
||||||
ComposeWindow w = primaire.getComposeWindow();
|
ComposeWindow w = primaire.getComposeWindow();
|
||||||
|
w.setComposition(c);
|
||||||
w.setLocation(getX(), getY() + 100);
|
w.setLocation(getX(), getY() + 100);
|
||||||
w.setVisible(true);
|
w.setVisible(true);
|
||||||
w.setComposition(c);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void
|
public void
|
||||||
openMedia()
|
openMedia()
|
||||||
{
|
{
|
||||||
Tree<String> post = this.post;
|
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
|
||||||
Tree<String> boosted = post.get("reblog");
|
|
||||||
if (boosted.size() > 0) post = boosted;
|
|
||||||
Tree<String> media = post.get("media_attachments");
|
|
||||||
|
|
||||||
Attachment[] as = new Attachment[media.size()];
|
for (Attachment a: post.attachments) a.resolveImage();
|
||||||
for (int o = 0; o < as.length; ++o)
|
|
||||||
{
|
|
||||||
Tree<String> medium = media.get(o);
|
|
||||||
String u1 = medium.get("remote_url").value;
|
|
||||||
String u2 = medium.get("text_url").value;
|
|
||||||
String u3 = medium.get("url").value;
|
|
||||||
|
|
||||||
Attachment a = as[o] = new Attachment();
|
ImageWindow w = new ImageWindow();
|
||||||
a.url = u1 != null ? u1 : u2 != null ? u2 : u3;
|
w.setTitle("Media - " + this.getTitle());
|
||||||
a.type = medium.get("type").value;
|
w.showAttachments(post.attachments);
|
||||||
a.description = medium.get("description").value;
|
w.setLocationRelativeTo(null);
|
||||||
a.image = ImageApi.remote(a.url);
|
w.setVisible(true);
|
||||||
}
|
|
||||||
|
|
||||||
ImageWindow w = primaire.getMediaWindow();
|
display.setCursor(null);
|
||||||
w.setTitle(post.get("id").value);
|
|
||||||
w.showAttachments(as);
|
|
||||||
if (!w.isVisible()) {
|
|
||||||
w.setLocationRelativeTo(null);
|
|
||||||
w.setVisible(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void
|
public void
|
||||||
@ -310,69 +250,7 @@ PostWindow extends JFrame {
|
|||||||
display.setDeleteEnabled(false);
|
display.setDeleteEnabled(false);
|
||||||
display.paintImmediately(display.getBounds());
|
display.paintImmediately(display.getBounds());
|
||||||
|
|
||||||
if (redraft)
|
api.deletePost(post.id, new RequestListener() {
|
||||||
{
|
|
||||||
String html = post.get("content").value;
|
|
||||||
StringBuilder b = new StringBuilder();
|
|
||||||
Tree<String> nodes;
|
|
||||||
nodes = RudimentaryHTMLParser.depthlessRead(html);
|
|
||||||
// We have to salvage whatever we can from the HTML.
|
|
||||||
for (Tree<String> node: nodes)
|
|
||||||
{
|
|
||||||
if (node.key.equals("text"))
|
|
||||||
{
|
|
||||||
b.append(node.value);
|
|
||||||
}
|
|
||||||
if (node.key.equals("emoji"))
|
|
||||||
{
|
|
||||||
b.append(":" + node.value + ":");
|
|
||||||
}
|
|
||||||
if (node.key.equals("tag"))
|
|
||||||
{
|
|
||||||
if (node.get(0).key.equals("/p"))
|
|
||||||
b.append("\n\n");
|
|
||||||
if (node.get(0).key.equals("br"))
|
|
||||||
b.append("\n");
|
|
||||||
if (node.get(0).key.equals("a")) {
|
|
||||||
b.append(node.get("href").value);
|
|
||||||
b.append(" ");
|
|
||||||
continue;
|
|
||||||
/*
|
|
||||||
* We don't omit the contents of the <a>
|
|
||||||
* which is an automatic label, but we'll
|
|
||||||
* need a non-depthless parser to omit that.
|
|
||||||
* For now prioritise not losing anything
|
|
||||||
* from our composition.
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
// I think that's all. I hope.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String cw = post.get("spoiler_text").value;
|
|
||||||
|
|
||||||
String vs = post.get("visibility").value;
|
|
||||||
PostVisibility v = null;
|
|
||||||
if (vs.equals("public")) v = PostVisibility.PUBLIC;
|
|
||||||
if (vs.equals("unlisted")) v = PostVisibility.UNLISTED;
|
|
||||||
if (vs.equals("private")) v = PostVisibility.FOLLOWERS;
|
|
||||||
if (vs.equals("direct")) v = PostVisibility.MENTIONED;
|
|
||||||
|
|
||||||
String replyTo = post.get("in_reply_to_id").value;
|
|
||||||
|
|
||||||
Composition c = new Composition();
|
|
||||||
c.contentWarning = cw;
|
|
||||||
c.text = b.toString();
|
|
||||||
c.visibility = v;
|
|
||||||
c.replyToPostId = replyTo;
|
|
||||||
|
|
||||||
ComposeWindow w = primaire.getComposeWindow();
|
|
||||||
w.setLocation(getX(), getY() + 100);
|
|
||||||
w.setVisible(true);
|
|
||||||
w.setComposition(c);
|
|
||||||
}
|
|
||||||
|
|
||||||
api.deletePost(post.get("id").value, new RequestListener() {
|
|
||||||
|
|
||||||
public void
|
public void
|
||||||
connectionFailed(IOException eIo)
|
connectionFailed(IOException eIo)
|
||||||
@ -399,6 +277,15 @@ PostWindow extends JFrame {
|
|||||||
requestSucceeded(Tree<String> json)
|
requestSucceeded(Tree<String> json)
|
||||||
{
|
{
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
|
|
||||||
|
if (redraft)
|
||||||
|
{
|
||||||
|
Composition c = Composition.recover(json);
|
||||||
|
ComposeWindow w = new ComposeWindow(primaire);
|
||||||
|
w.setComposition(c);
|
||||||
|
w.setLocation(getX(), getY() + 100);
|
||||||
|
w.setVisible(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
@ -412,35 +299,20 @@ PostWindow extends JFrame {
|
|||||||
public void
|
public void
|
||||||
copyPostId()
|
copyPostId()
|
||||||
{
|
{
|
||||||
Tree<String> post = this.post;
|
ClipboardApi.serve(post.id);
|
||||||
Tree<String> reblogged = post.get("reblog");
|
|
||||||
if (reblogged.size() > 0) post = reblogged;
|
|
||||||
|
|
||||||
ClipboardApi.serve(post.get("id").value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void
|
public void
|
||||||
copyPostLink()
|
copyPostLink()
|
||||||
{
|
{
|
||||||
Tree<String> post = this.post;
|
ClipboardApi.serve(post.uri);
|
||||||
Tree<String> reblogged = post.get("reblog");
|
|
||||||
if (reblogged.size() > 0) post = reblogged;
|
|
||||||
|
|
||||||
String url = post.get("url").value;
|
|
||||||
if (url == null) url = post.get("uri").value;
|
|
||||||
|
|
||||||
ClipboardApi.serve(url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void
|
public void
|
||||||
openReplies()
|
openReplies()
|
||||||
{
|
{
|
||||||
Tree<String> post = this.post;
|
|
||||||
Tree<String> boosted = post.get("reblog");
|
|
||||||
if (boosted.size() > 0) post = boosted;
|
|
||||||
|
|
||||||
RepliesWindow w = new RepliesWindow(primaire, this);
|
RepliesWindow w = new RepliesWindow(primaire, this);
|
||||||
w.showFor(post.get("id").value);
|
w.showFor(post.id);
|
||||||
w.setLocation(getX(), getY() + 100);
|
w.setLocation(getX(), getY() + 100);
|
||||||
w.setVisible(true);
|
w.setVisible(true);
|
||||||
}
|
}
|
||||||
|
5
RepliesWindow.java
Executable file → Normal file
@ -267,8 +267,9 @@ implements TreeSelectionListener {
|
|||||||
public String
|
public String
|
||||||
toString()
|
toString()
|
||||||
{
|
{
|
||||||
String html = post.get("content").value;
|
Post post = new Post(this.post);
|
||||||
return TimelineWindow.textApproximation(html);
|
post.resolveApproximateText();
|
||||||
|
return post.approximateText;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -=%=-
|
// -=%=-
|
||||||
|
0
RequestListener.java
Executable file → Normal file
0
RichTextPane.java
Executable file → Normal file
0
RudimentaryHTMLParser.java
Executable file → Normal file
209
TimelineWindow.java
Executable file → Normal file
@ -91,64 +91,42 @@ implements ActionListener {
|
|||||||
|
|
||||||
// ---%-@-%---
|
// ---%-@-%---
|
||||||
|
|
||||||
public void
|
public void
|
||||||
readEntity(Tree<String> postEntityArray)
|
use(TimelinePage page)
|
||||||
{
|
{
|
||||||
page.posts = postEntityArray;
|
assert page != null;
|
||||||
|
this.page = page;
|
||||||
|
|
||||||
List<PostPreviewComponent> previews;
|
List<PostPreviewComponent> previews;
|
||||||
previews = display.getPostPreviews();
|
previews = display.getPostPreviews();
|
||||||
|
|
||||||
int available = postEntityArray.size();
|
int available = page.posts.length;
|
||||||
int max = previews.size();
|
int max = previews.size();
|
||||||
assert available <= max;
|
assert available <= max;
|
||||||
|
|
||||||
for (int o = 0; o < available; ++o)
|
for (int o = 0; o < available; ++o)
|
||||||
{
|
{
|
||||||
PostPreviewComponent c = previews.get(o);
|
PostPreviewComponent preview = previews.get(o);
|
||||||
Tree<String> p = postEntityArray.get(o);
|
Post post = page.posts[o];
|
||||||
Tree<String> a = p.get("account");
|
|
||||||
|
|
||||||
String an = a.get("display_name").value;
|
preview.setTopLeft(post.author.name);
|
||||||
if (an.isEmpty()) an = a.get("username").value;
|
if (post.boostedPost != null)
|
||||||
c.setTopLeft(an);
|
{
|
||||||
|
String s = "boosted by " + post.author.name;
|
||||||
Tree<String> boosted = p.get("reblog");
|
preview.setTopLeft(s);
|
||||||
if (boosted.size() > 0) {
|
post = post.boostedPost;
|
||||||
c.setTopLeft("boosted by " + an);
|
|
||||||
p = boosted;
|
|
||||||
a = p.get("account");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String f = "";
|
String flags = "";
|
||||||
if (p.get("media_attachments").size() > 0) f += "a";
|
if (post.attachments.length > 0) flags += "a";
|
||||||
String t = "";
|
post.resolveRelativeTime();
|
||||||
try {
|
preview.setTopRight(flags + " " + post.relativeTime);
|
||||||
String jv = p.get("created_at").value;
|
|
||||||
ZonedDateTime pv = ZonedDateTime.parse(jv);
|
|
||||||
ZoneId z = ZoneId.systemDefault();
|
|
||||||
pv = pv.withZoneSameInstant(z);
|
|
||||||
|
|
||||||
ZonedDateTime now = ZonedDateTime.now();
|
post.resolveApproximateText();
|
||||||
long d = ChronoUnit.SECONDS.between(pv, now);
|
if (post.contentWarning != null)
|
||||||
long s = Math.abs(d);
|
preview.setBottom("(" + post.contentWarning + ")");
|
||||||
if (s < 30) t = "now";
|
else
|
||||||
else if (s < 60) t = d + "s";
|
preview.setBottom(post.approximateText);
|
||||||
else if (s < 3600) t = (d / 60) + "m";
|
|
||||||
else if (s < 86400) t = (d / 3600) + "h";
|
|
||||||
else t = (d / 86400) + "d";
|
|
||||||
}
|
|
||||||
catch (DateTimeParseException eDt) {
|
|
||||||
assert false;
|
|
||||||
}
|
|
||||||
c.setTopRight(f + " " + t);
|
|
||||||
|
|
||||||
String html = p.get("content").value;
|
|
||||||
String cw = p.get("spoiler_text").value;
|
|
||||||
if (!cw.isEmpty())
|
|
||||||
c.setBottom("(" + cw + ")");
|
|
||||||
else
|
|
||||||
c.setBottom(textApproximation(html));
|
|
||||||
}
|
}
|
||||||
for (int o = available; o < max; ++o)
|
for (int o = available; o < max; ++o)
|
||||||
{
|
{
|
||||||
@ -159,6 +137,16 @@ implements ActionListener {
|
|||||||
display.setNextPageAvailable(full);
|
display.setNextPageAvailable(full);
|
||||||
display.setPreviousPageAvailable(true);
|
display.setPreviousPageAvailable(true);
|
||||||
display.resetFocus();
|
display.resetFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void
|
||||||
|
readEntity(Tree<String> postEntityArray)
|
||||||
|
{
|
||||||
|
TimelinePage page = new TimelinePage(postEntityArray);
|
||||||
|
page.type = this.page.type;
|
||||||
|
page.accountNumId = this.page.accountNumId;
|
||||||
|
page.listId = this.page.listId;
|
||||||
|
use(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void
|
public void
|
||||||
@ -168,9 +156,8 @@ implements ActionListener {
|
|||||||
if (!showingLatest)
|
if (!showingLatest)
|
||||||
{
|
{
|
||||||
assert page.posts != null;
|
assert page.posts != null;
|
||||||
assert page.posts.size() != 0;
|
assert page.posts.length != 0;
|
||||||
Tree<String> first = page.posts.get(0);
|
firstId = page.posts[0].id;
|
||||||
firstId = first.get("id").value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
|
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
|
||||||
@ -204,7 +191,8 @@ implements ActionListener {
|
|||||||
public void
|
public void
|
||||||
requestSucceeded(Tree<String> json)
|
requestSucceeded(Tree<String> json)
|
||||||
{
|
{
|
||||||
if (json.size() < PREVIEW_COUNT) {
|
if (json.size() < PREVIEW_COUNT)
|
||||||
|
{
|
||||||
showLatestPage();
|
showLatestPage();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -264,9 +252,8 @@ implements ActionListener {
|
|||||||
showNextPage()
|
showNextPage()
|
||||||
{
|
{
|
||||||
assert page.posts != null;
|
assert page.posts != null;
|
||||||
assert page.posts.size() != 0;
|
assert page.posts.length != 0;
|
||||||
Tree<String> last = page.posts.get(page.posts.size() - 1);
|
String lastId = page.posts[page.posts.length - 1].id;
|
||||||
String lastId = last.get("id").value;
|
|
||||||
|
|
||||||
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
|
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
|
||||||
api.getTimelinePage(
|
api.getTimelinePage(
|
||||||
@ -299,7 +286,8 @@ implements ActionListener {
|
|||||||
public void
|
public void
|
||||||
requestSucceeded(Tree<String> json)
|
requestSucceeded(Tree<String> json)
|
||||||
{
|
{
|
||||||
if (json.size() == 0) {
|
if (json.size() == 0)
|
||||||
|
{
|
||||||
// We should probably say something
|
// We should probably say something
|
||||||
// to the user here? For now, we
|
// to the user here? For now, we
|
||||||
// quietly cancel.
|
// quietly cancel.
|
||||||
@ -319,9 +307,8 @@ implements ActionListener {
|
|||||||
showPreviousPage()
|
showPreviousPage()
|
||||||
{
|
{
|
||||||
assert page.posts != null;
|
assert page.posts != null;
|
||||||
assert page.posts.size() != 0;
|
assert page.posts.length != 0;
|
||||||
Tree<String> first = page.posts.get(0);
|
String firstId = page.posts[0].id;
|
||||||
String firstId = first.get("id").value;
|
|
||||||
|
|
||||||
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
|
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
|
||||||
api.getTimelinePage(
|
api.getTimelinePage(
|
||||||
@ -354,7 +341,8 @@ implements ActionListener {
|
|||||||
public void
|
public void
|
||||||
requestSucceeded(Tree<String> json)
|
requestSucceeded(Tree<String> json)
|
||||||
{
|
{
|
||||||
if (json.size() < PREVIEW_COUNT) {
|
if (json.size() < PREVIEW_COUNT)
|
||||||
|
{
|
||||||
showLatestPage();
|
showLatestPage();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -368,19 +356,19 @@ implements ActionListener {
|
|||||||
display.setCursor(null);
|
display.setCursor(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void
|
public void
|
||||||
setTimelineType(TimelineType type)
|
setTimelineType(TimelineType type)
|
||||||
{
|
{
|
||||||
|
assert type != TimelineType.LIST;
|
||||||
|
assert type != TimelineType.PROFILE;
|
||||||
|
|
||||||
page.type = type;
|
page.type = type;
|
||||||
page.accountNumId = null;
|
page.accountNumId = null;
|
||||||
|
setTitle(toString(type) + " timeline - JKomasto");
|
||||||
|
|
||||||
String s1 = type.toString();
|
String f = type.toString().toLowerCase();
|
||||||
s1 = s1.charAt(0) + s1.substring(1).toLowerCase();
|
display.setBackgroundImage(ImageApi.local(f));
|
||||||
setTitle(s1 + " - JKomasto");
|
display.repaint();
|
||||||
|
|
||||||
String s2 = type.toString().toLowerCase();
|
|
||||||
display.setBackgroundImage(ImageApi.local(s2));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public TimelineType
|
public TimelineType
|
||||||
@ -391,19 +379,20 @@ implements ActionListener {
|
|||||||
{
|
{
|
||||||
assert authorNumId != null;
|
assert authorNumId != null;
|
||||||
|
|
||||||
page.type = TimelineType.FEDERATED;
|
page.type = TimelineType.PROFILE;
|
||||||
page.accountNumId = authorNumId;
|
page.accountNumId = authorNumId;
|
||||||
|
|
||||||
setTitle(authorNumId + " - JKomasto");
|
setTitle(authorNumId + " - JKomasto");
|
||||||
|
|
||||||
display.setBackgroundImage(ImageApi.local("profile"));
|
display.setBackgroundImage(ImageApi.local("profile"));
|
||||||
|
display.repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void
|
public void
|
||||||
openOwnProfile()
|
openOwnProfile()
|
||||||
{
|
{
|
||||||
Tree<String> accountDetails = api.getAccountDetails();
|
Tree<String> accountDetails = api.getAccountDetails();
|
||||||
|
assert accountDetails != null;
|
||||||
String id = accountDetails.get("id").value;
|
String id = accountDetails.get("id").value;
|
||||||
assert id != null;
|
|
||||||
|
|
||||||
TimelineWindow w = new TimelineWindow(primaire);
|
TimelineWindow w = new TimelineWindow(primaire);
|
||||||
w.showAuthorPosts(id);
|
w.showAuthorPosts(id);
|
||||||
@ -463,7 +452,8 @@ implements ActionListener {
|
|||||||
api.getAccounts(query, handler);
|
api.getAccounts(query, handler);
|
||||||
if (handler.json == null) return;
|
if (handler.json == null) return;
|
||||||
|
|
||||||
if (handler.json.size() == 0) {
|
if (handler.json.size() == 0)
|
||||||
|
{
|
||||||
JOptionPane.showMessageDialog(
|
JOptionPane.showMessageDialog(
|
||||||
this,
|
this,
|
||||||
"There were no results from the query.. ☹️"
|
"There were no results from the query.. ☹️"
|
||||||
@ -508,6 +498,12 @@ implements ActionListener {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (id == null) return;
|
||||||
|
/*
|
||||||
|
* It seems like this can happen if someone
|
||||||
|
* presses escape out of the confirm dialog.
|
||||||
|
* I don't know why that doesn't map to cancel.
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
TimelineWindow w = new TimelineWindow(primaire);
|
TimelineWindow w = new TimelineWindow(primaire);
|
||||||
@ -522,18 +518,19 @@ implements ActionListener {
|
|||||||
public void
|
public void
|
||||||
previewSelected(int index)
|
previewSelected(int index)
|
||||||
{
|
{
|
||||||
if (index > page.posts.size()) return;
|
if (index > page.posts.length) return;
|
||||||
Tree<String> post = page.posts.get(index - 1);
|
Post post = page.posts[index - 1];
|
||||||
primaire.getAutoViewWindow().readEntity(post);
|
primaire.getAutoViewWindow().use(post);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void
|
public void
|
||||||
previewOpened(int index)
|
previewOpened(int index)
|
||||||
{
|
{
|
||||||
if (index > page.posts.size()) return;
|
if (index > page.posts.length) return;
|
||||||
Tree<String> post = page.posts.get(index - 1);
|
Post post = page.posts[index - 1];
|
||||||
|
|
||||||
PostWindow w = new PostWindow(primaire);
|
PostWindow w = new PostWindow(primaire);
|
||||||
w.readEntity(post);
|
w.use(post);
|
||||||
w.setLocationRelativeTo(this);
|
w.setLocationRelativeTo(this);
|
||||||
w.setVisible(true);
|
w.setVisible(true);
|
||||||
}
|
}
|
||||||
@ -578,8 +575,10 @@ implements ActionListener {
|
|||||||
}
|
}
|
||||||
if (src == openNotifications)
|
if (src == openNotifications)
|
||||||
{
|
{
|
||||||
NotificationsWindow w = primaire.getNotificationsWindow();
|
NotificationsWindow w =
|
||||||
if (!w.isVisible()) {
|
primaire.getNotificationsWindow();
|
||||||
|
if (!w.isVisible())
|
||||||
|
{
|
||||||
w.setLocationByPlatform(true);
|
w.setLocationByPlatform(true);
|
||||||
w.setVisible(true);
|
w.setVisible(true);
|
||||||
}
|
}
|
||||||
@ -601,54 +600,16 @@ implements ActionListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// - -%- -
|
|
||||||
|
|
||||||
public static String
|
|
||||||
textApproximation(String html)
|
|
||||||
{
|
|
||||||
StringBuilder returnee = new StringBuilder();
|
|
||||||
Tree<String> nodes = RudimentaryHTMLParser.depthlessRead(html);
|
|
||||||
if (nodes.size() == 0) return "-";
|
|
||||||
Tree<String> first = nodes.get(0);
|
|
||||||
for (Tree<String> node: nodes)
|
|
||||||
{
|
|
||||||
if (node.key.equals("tag"))
|
|
||||||
{
|
|
||||||
if (node.get(0).key.equals("br"))
|
|
||||||
returnee.append("; ");
|
|
||||||
if (node.get(0).key.equals("p") && node != first)
|
|
||||||
returnee.append("; ");
|
|
||||||
}
|
|
||||||
if (node.key.equals("emoji"))
|
|
||||||
{
|
|
||||||
returnee.append(":" + node.value + ":");
|
|
||||||
}
|
|
||||||
if (node.key.equals("text"))
|
|
||||||
{
|
|
||||||
returnee.append(node.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return returnee.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// - -%- -
|
|
||||||
|
|
||||||
private static String
|
private static String
|
||||||
plainify(String html)
|
toString(TimelineType type)
|
||||||
{
|
{
|
||||||
// Delete all tags.
|
switch (type)
|
||||||
StringBuilder b = new StringBuilder();
|
{
|
||||||
boolean in = false;
|
case FEDERATED: return "Federated";
|
||||||
for (char c: html.toCharArray()) switch (c) {
|
case LOCAL: return "Local";
|
||||||
case '<': in = true; break;
|
case HOME: return "Home";
|
||||||
case '>': in = false; break;
|
default: return "";
|
||||||
default: if (!in) b.append(c);
|
|
||||||
}
|
}
|
||||||
String s = b.toString();
|
|
||||||
s = s.replaceAll("<", "<");
|
|
||||||
s = s.replaceAll(">", ">");
|
|
||||||
s = s.replaceAll(" ", "");
|
|
||||||
return s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---%-@-%---
|
// ---%-@-%---
|
||||||
@ -704,7 +665,7 @@ implements ActionListener {
|
|||||||
setJMenuBar(menuBar);
|
setJMenuBar(menuBar);
|
||||||
|
|
||||||
page = new TimelinePage();
|
page = new TimelinePage();
|
||||||
page.posts = new Tree<String>();
|
page.posts = new Post[0];
|
||||||
|
|
||||||
display = new TimelineComponent(this);
|
display = new TimelineComponent(this);
|
||||||
display.setNextPageAvailable(false);
|
display.setNextPageAvailable(false);
|
||||||
@ -925,6 +886,8 @@ implements
|
|||||||
pageLabel = new JLabel("0");
|
pageLabel = new JLabel("0");
|
||||||
|
|
||||||
Box bottom = Box.createHorizontalBox();
|
Box bottom = Box.createHorizontalBox();
|
||||||
|
bottom.setBackground(null);
|
||||||
|
bottom.setOpaque(false);
|
||||||
bottom.add(Box.createGlue());
|
bottom.add(Box.createGlue());
|
||||||
bottom.add(prev);
|
bottom.add(prev);
|
||||||
bottom.add(Box.createHorizontalStrut(8));
|
bottom.add(Box.createHorizontalStrut(8));
|
||||||
|
0
TwoToggleButton.java
Executable file → Normal file
0
WindowUpdater.java
Executable file → Normal file
0
graphics/Federated.xcf
Executable file → Normal file
0
graphics/Flags.xcf
Executable file → Normal file
0
graphics/Hourglass.xcf
Executable file → Normal file
0
graphics/boostToggled.png
Executable file → Normal file
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
0
graphics/boostUntoggled.png
Executable file → Normal file
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
0
graphics/button.png
Executable file → Normal file
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
0
graphics/disabledOverlay.png
Executable file → Normal file
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
0
graphics/favouriteToggled.png
Executable file → Normal file
Before Width: | Height: | Size: 353 B After Width: | Height: | Size: 353 B |
0
graphics/favouriteUntoggled.png
Executable file → Normal file
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
0
graphics/federated.png
Executable file → Normal file
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
0
graphics/miscToggled.png
Executable file → Normal file
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
0
graphics/miscUntoggled.png
Executable file → Normal file
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
0
graphics/ref1.png
Executable file → Normal file
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
0
graphics/replyToggled.png
Executable file → Normal file
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
0
graphics/replyUntoggled.png
Executable file → Normal file
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
0
graphics/selectedOverlay.png
Executable file → Normal file
Before Width: | Height: | Size: 313 B After Width: | Height: | Size: 313 B |
0
graphics/test1.png
Executable file → Normal file
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
0
graphics/test2.png
Executable file → Normal file
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
0
graphics/test3.png
Executable file → Normal file
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
0
graphics/test4.png
Executable file → Normal file
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |