mirror of
https://gitlab.com/biskuteri-cafe/JKomasto2.git
synced 2024-11-20 06:24:50 +01:00
710 lines
14 KiB
Java
710 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 javax.swing.plaf.metal.MetalLookAndFeel;
|
|
import javax.swing.plaf.metal.DefaultMetalTheme;
|
|
import javax.swing.plaf.metal.OceanTheme;
|
|
import javax.swing.plaf.ColorUIResource;
|
|
import javax.swing.UIDefaults;
|
|
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; }
|
|
|
|
// ---%-@-%---
|
|
|
|
private static class
|
|
MetalTheme extends OceanTheme {
|
|
|
|
private ColorUIResource
|
|
lightPink = new ColorUIResource(246, 240, 240),
|
|
mildPink = new ColorUIResource(238, 233, 233),
|
|
white = new ColorUIResource(250, 250, 250),
|
|
darkPink = new ColorUIResource(242, 230, 230),
|
|
veryDarkPink = new ColorUIResource(164, 160, 160);
|
|
|
|
// -=%=-
|
|
|
|
public ColorUIResource
|
|
getPrimary2() { return darkPink; }
|
|
|
|
public ColorUIResource
|
|
getSecondary2() { return white; }
|
|
|
|
public ColorUIResource
|
|
getSecondary3() { return mildPink; }
|
|
|
|
public ColorUIResource
|
|
getSecondary1() { return veryDarkPink; }
|
|
|
|
public ColorUIResource
|
|
getPrimary1() { return veryDarkPink; }
|
|
|
|
public void
|
|
addCustomEntriesToTable(UIDefaults table)
|
|
{
|
|
super.addCustomEntriesToTable(table);
|
|
table.put(
|
|
"TabbedPane.tabAreaBackground",
|
|
getPrimary1()
|
|
);
|
|
table.put(
|
|
"TabbedPane.contentAreaColor",
|
|
getSecondary3()
|
|
);
|
|
table.put(
|
|
"TabbedPane.selected",
|
|
getSecondary3()
|
|
);
|
|
table.put(
|
|
"MenuBar.gradient",
|
|
java.util.Arrays.asList(new Object[] {
|
|
1f, 0f,
|
|
getWhite(),
|
|
getSecondary3(),
|
|
getSecondary1()
|
|
})
|
|
);
|
|
}
|
|
|
|
}
|
|
|
|
// ---%-@-%---
|
|
|
|
public static void
|
|
main(String... args)
|
|
{
|
|
//System.setProperty("swing.boldMetal", "false");
|
|
MetalLookAndFeel.setCurrentTheme(new MetalTheme());
|
|
|
|
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 String
|
|
contentWarning;
|
|
|
|
public ZonedDateTime
|
|
dateTime;
|
|
|
|
public String
|
|
date, time, relativeTime;
|
|
|
|
public boolean
|
|
boosted,
|
|
favourited;
|
|
|
|
public Post
|
|
boostedPost;
|
|
|
|
public Attachment[]
|
|
attachments;
|
|
|
|
public String[][]
|
|
emojiUrls;
|
|
|
|
public String[]
|
|
mentions;
|
|
|
|
// - -%- -
|
|
|
|
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
|
|
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);
|
|
|
|
Tree<String> mentions = entity.get("mentions");
|
|
this.mentions = new String[mentions.size()];
|
|
for (int o = 0; o < mentions.size(); ++o)
|
|
{
|
|
String acct = mentions.get(o).get("acct").value;
|
|
this.mentions[o] = acct;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
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 ownId)
|
|
{
|
|
if (post.boostedPost != null) post = post.boostedPost;
|
|
|
|
Composition c = new Composition();
|
|
c.replyToPostId = post.id;
|
|
c.visibility = post.visibility;
|
|
c.contentWarning = post.contentWarning;
|
|
|
|
StringBuilder text = new StringBuilder();
|
|
for (String id: post.mentions)
|
|
{
|
|
if (id.equals(ownId)) continue;
|
|
text.append("@" + id);
|
|
}
|
|
if (!post.author.id.equals(ownId))
|
|
{
|
|
text.append("@" + post.author.id);
|
|
}
|
|
|
|
c.text = text.toString();
|
|
|
|
return c;
|
|
}
|
|
|
|
}
|