/* copyright This file is part of JKomasto2. Written in 2022 by Usawashi 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 . 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 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 nodes; nodes = RudimentaryHTMLParser.depthlessRead(text); if (nodes.size() == 0) { approximateText = "-"; return; } StringBuilder b = new StringBuilder(); Tree first = nodes.get(0); for (Tree 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 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 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 emojis = entity.get("emojis"); emojiUrls = new String[emojis.size()][]; for (int o = 0; o < emojiUrls.length; ++o) { Tree emoji = emojis.get(o); String[] mapping = emojiUrls[o] = new String[2]; mapping[0] = ":" + emoji.get("shortcode").value + ":"; mapping[1] = emoji.get("url").value; } Tree boostedPost = entity.get("reblog"); if (boostedPost.size() > 0) this.boostedPost = new Post(boostedPost); Tree 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 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 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 fs = entity.get("fields"); fields = new String[fs.size()][]; for (int o = 0; o < fields.length; ++o) { Tree 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 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 entity, String ownNumId) { Composition c = new Composition(); Tree 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 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 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; } }