biskuteri-cafe-JKomasto2/JKomasto.java
Snowyfox 174df078a5 Added UI prototype for attachments uploading.
Fixed text selection bug.
2022-05-21 21:41:46 -04:00

700 lines
14 KiB
Java

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JComponent;
import javax.swing.ImageIcon;
import java.awt.Dimension;
import java.awt.BorderLayout;
import java.awt.Cursor;
import java.awt.Image;
import java.awt.FontMetrics;
import java.util.List;
import java.util.Locale;
import java.text.BreakIterator;
import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.time.Period;
import java.time.temporal.ChronoUnit;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import cafe.biskuteri.hinoki.Tree;
class
JKomasto {
private TimelineWindow
timelineWindow;
private ComposeWindow
composeWindow;
private PostWindow
autoViewWindow;
private LoginWindow
loginWindow;
private ImageWindow
mediaWindow;
private NotificationsWindow
notificationsWindow;
private WindowUpdater
windowUpdater;
private MastodonApi
api;
private Image
programIcon;
// ---%-@-%---
public MastodonApi
getMastodonApi() { return api; }
public void
finishedLogin()
{
timelineWindow.setCursor(new Cursor(Cursor.WAIT_CURSOR));
timelineWindow.showLatestPage();
notificationsWindow.showLatestPage();
timelineWindow.setVisible(true);
loginWindow.dispose();
timelineWindow.setCursor(null);
}
public PostWindow
getAutoViewWindow() { return autoViewWindow; }
public ComposeWindow
getComposeWindow() { return composeWindow; }
public ImageWindow
getMediaWindow() { return mediaWindow; }
public NotificationsWindow
getNotificationsWindow() { return notificationsWindow; }
public WindowUpdater
getWindowUpdater() { return windowUpdater; }
public Image
getProgramIcon() { return programIcon; }
// ---%-@-%---
public static void
main(String... args)
{
new JKomasto().loginWindow.setVisible(true);
}
// ---%-@-%---
public
JKomasto()
{
api = new MastodonApi();
windowUpdater = new WindowUpdater(this);
programIcon = ImageApi.local("kettle");
timelineWindow = new TimelineWindow(this);
composeWindow = new ComposeWindow(this);
autoViewWindow = new PostWindow(this);
loginWindow = new LoginWindow(this);
mediaWindow = new ImageWindow();
notificationsWindow = new NotificationsWindow(this);
autoViewWindow.setTitle("Auto view - JKomasto");
composeWindow.dispose();
autoViewWindow.dispose();
timelineWindow.dispose();
mediaWindow.dispose();
notificationsWindow.dispose();
timelineWindow.setLocationByPlatform(true);
loginWindow.setLocationByPlatform(true);
}
}
enum
PostVisibility {
PUBLIC,
UNLISTED,
FOLLOWERS,
MENTIONED
}
enum
TimelineType {
FEDERATED,
LOCAL,
HOME,
LIST,
PROFILE
}
enum
NotificationType {
MENTION,
BOOST,
FAVOURITE,
FOLLOW,
FOLLOWREQ,
POLL,
ALERT
}
class
TimelinePage {
public TimelineType
type;
public String
accountNumId, listId;
public Post[]
posts;
// ---%-@-%---
public
TimelinePage() { }
public
TimelinePage(Tree<String> entity)
{
posts = new Post[entity.size()];
for (int o = 0; o < posts.length; ++o)
posts[o] = new Post(entity.get(o));
}
}
class
Notification {
public NotificationType
type;
public String
id;
public String
postId, postText, actorNumId, actorName;
}
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 ZonedDateTime
creationDate;
public int
followedCount,
followerCount;
public int
postCount;
public String[][]
fields;
public String
description;
// ---%-@-%---
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;
creationDate =
ZonedDateTime.parse(entity.get("created_at").value)
.withZoneSameInstant(ZoneId.systemDefault());
String c1 = entity.get("following_count").value;
String c2 = entity.get("followers_count").value;
String c3 = entity.get("statuses_count").value;
try {
followedCount = (int)Double.parseDouble(c1);
followerCount = (int)Double.parseDouble(c2);
postCount = (int)Double.parseDouble(c3);
}
catch (NumberFormatException eNf) {
assert false;
}
Tree<String> fs = entity.get("fields");
fields = new String[fs.size()][];
for (int o = 0; o < fields.length; ++o)
{
Tree<String> f = fs.get(o);
String[] field = fields[o] = new String[3];
field[0] = f.get("name").value;
field[1] = f.get("value").value;
boolean v = f.get("verified_at").value != null;
field[2] = Boolean.toString(v);
}
description = entity.get("note").value;
}
}
class
Attachment {
public String
id;
public String
type;
public String
url;
public String
description;
public Image
image;
// ---%-@-%---
public void
resolveImage()
{
assert url != null;
if (image != null) return;
if (!type.equals("image")) return;
image = ImageApi.remote(url);
}
// ---%-@-%---
public
Attachment() { }
public
Attachment(Tree<String> entity)
{
url = entity.get("remote_url").value;
if (url == null) url = entity.get("url").value;
id = entity.get("id").value;
type = entity.get("type").value;
description = entity.get("description").value;
}
}
class
Composition {
public String
text,
contentWarning;
public PostVisibility
visibility;
public String
replyToPostId;
public Attachment[]
attachments;
// ---%-@-%---
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;
}
}