biskuteri-cafe-JKomasto2/JKomasto.java

736 lines
16 KiB
Java

/* copyright
This file is part of JKomasto2.
Written in 2022 by Usawashi <usawashi16@yahoo.co.jp>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
copyright */
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 java.io.File;
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 File
uploadee;
// ---%-@-%---
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;
private File
uploadee;
// ---%-@-%---
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;
}
}