From 2538776ce1b60913a47b428f7abc07e5f879ef35 Mon Sep 17 00:00:00 2001 From: Patrick Reinhart Date: Fri, 16 Feb 2024 15:16:17 +0100 Subject: [PATCH] Initial post step version --- .../stepengine/steps/ShowPosts.java | 244 ++++++++++++++++++ .../stepengine/steps/ShowSchedule.java | 6 +- ...rg.tweetwallfx.stepengine.api.Step$Factory | 1 + .../tweet/impl/mastodon4j/MastodonStatus.java | 41 ++- .../impl/mastodon4j/MastodonStatusTest.java | 12 +- 5 files changed, 293 insertions(+), 11 deletions(-) create mode 100644 conference-stepengine/src/main/java/org/tweetwallfx/conference/stepengine/steps/ShowPosts.java diff --git a/conference-stepengine/src/main/java/org/tweetwallfx/conference/stepengine/steps/ShowPosts.java b/conference-stepengine/src/main/java/org/tweetwallfx/conference/stepengine/steps/ShowPosts.java new file mode 100644 index 000000000..9caae056f --- /dev/null +++ b/conference-stepengine/src/main/java/org/tweetwallfx/conference/stepengine/steps/ShowPosts.java @@ -0,0 +1,244 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2024 TweetWallFX + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.tweetwallfx.conference.stepengine.steps; + +import java.util.Arrays; +import java.util.Collection; + +import javafx.application.Platform; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.Pane; +import javafx.scene.shape.Circle; +import javafx.scene.shape.Rectangle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tweetwallfx.controls.WordleSkin; +import org.tweetwallfx.emoji.control.EmojiFlow; +import org.tweetwallfx.stepengine.api.DataProvider; +import org.tweetwallfx.stepengine.api.Step; +import org.tweetwallfx.stepengine.api.StepEngine.MachineContext; +import org.tweetwallfx.stepengine.api.config.AbstractConfig; +import org.tweetwallfx.stepengine.api.config.StepEngineSettings; +import org.tweetwallfx.stepengine.dataproviders.TweetStreamDataProvider; +import org.tweetwallfx.stepengine.dataproviders.TweetUserProfileImageDataProvider; +import org.tweetwallfx.tweet.api.Tweet; + +import javafx.animation.FadeTransition; +import javafx.geometry.Insets; +import javafx.scene.CacheHint; +import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import javafx.util.Duration; + +public class ShowPosts implements Step { + private static final Logger LOGGER = LoggerFactory.getLogger(ShowPosts.class); + private final Config config; + + private ShowPosts(Config config) { + this.config = config; + } + + @Override + public void doStep(MachineContext context) { + final var wordleSkin = (WordleSkin) context.get("WordleSkin"); + final var dataProvider = context.getDataProvider(TweetStreamDataProvider.class); + + var existingPostsNode = (Pane)wordleSkin.getNode().lookup("#postsNode"); + if (null == existingPostsNode) { + LOGGER.debug("Initializing posts node"); + + var postsNode = new Pane(); + postsNode.getStyleClass().add("posts"); + postsNode.setId("postsNode"); + postsNode.setOpacity(0); + + var title = new Label("Latest Posts"); + + title.setPrefWidth(config.width); + title.getStyleClass().add("title"); + title.setPrefHeight(config.titleHeight); + title.setAlignment(Pos.CENTER); + + postsNode.getChildren().add(title); + + final var fadeIn = new FadeTransition(Duration.millis(500), postsNode); + fadeIn.setFromValue(0); + fadeIn.setToValue(1); + fadeIn.setOnFinished(e -> { + LOGGER.info("Calling proceed from ShowPosts"); + context.proceed(); + }); + postsNode.setLayoutX(config.layoutX); + postsNode.setLayoutY(config.layoutY); + postsNode.setMinWidth(config.width); + postsNode.setMaxWidth(config.width); + postsNode.setPrefWidth(config.width); + postsNode.setCacheHint(CacheHint.SPEED); + postsNode.setCache(true); + int col = 0; + int row = 0; + + for (Tweet post : dataProvider.getTweets()) { + Pane postPane = createPostNode(context, post); + double postWidth = (config.width - (config.columns - 1) * config.postHGap) / config.columns; + postPane.setMinWidth(postWidth); + postPane.setMaxWidth(postWidth); + postPane.setPrefWidth(postWidth); + postPane.setMinHeight(config.postHeight); + postPane.setMaxHeight(config.postHeight); + postPane.setPrefHeight(config.postHeight); + postPane.setLayoutX(col * (postWidth + config.postHGap)); + postPane.setLayoutY(config.titleHeight + config.postVGap + (config.postHeight + config.postVGap) * row); + postsNode.getChildren().add(postPane); + col++; + if (col == config.columns) { + row++; + col = 0; + } + } + + Platform.runLater(() -> { + wordleSkin.getPane().getChildren().add(postsNode); + fadeIn.play(); + }); + } + } + + private Pane createPostNode(MachineContext context, Tweet post) { + final var userImageProvider = context.getDataProvider(TweetUserProfileImageDataProvider.class); + final var bpPostPane = new BorderPane(); + bpPostPane.getStyleClass().add("postDisplay"); + bpPostPane.setCenter(createSinglePostDisplay(post, userImageProvider)); + return bpPostPane; + } + + private HBox createSinglePostDisplay(final Tweet post, + final TweetUserProfileImageDataProvider userImageProvider) { + var postFlow = new EmojiFlow(); + postFlow.setText(post.getDisplayEnhancedText()); + postFlow.setEmojiFitWidth(config.postFontSize); + postFlow.setEmojiFitHeight(config.postFontSize); + postFlow.getStyleClass().add("postFlow"); + postFlow.setMaxHeight(Double.MAX_VALUE); + + var postUser = new EmojiFlow(); + postUser.setText(post.getUser().getName()); + postUser.setEmojiFitWidth(config.postUserFontSize); + postUser.setEmojiFitHeight(config.postFontSize); + postUser.getStyleClass().add("postUserName"); + postUser.setMaxHeight(Double.MAX_VALUE); + + VBox imageBox = new VBox(createPostUserImage(userImageProvider, post)); + imageBox.setPadding(new Insets(10, 0, 10, 10)); + + VBox postContentBox = new VBox(postUser, postFlow); + postContentBox.setSpacing(10); + postContentBox.setPadding(new Insets(10, 10, 10, 0)); + + HBox postBox = new HBox(imageBox, postContentBox); + postBox.setCacheHint(CacheHint.QUALITY); + postBox.setSpacing(10); + + return postBox; + } + + private Node createPostUserImage(TweetUserProfileImageDataProvider postImageProvider, Tweet post) { + var image = postImageProvider.getImageBig(post.getUser()); + var postUserImage = new ImageView(image); + postUserImage.getStyleClass().add("postUserImage"); + postUserImage.setPreserveRatio(true); + if (image.getWidth() > image.getHeight()) { + postUserImage.setFitHeight(config.avatarSize); + } else { + postUserImage.setFitWidth(config.avatarSize); + } + + // avatar image clipping + if (config.circularAvatar) { + Circle circle = new Circle(config.avatarSize / 2f, config.avatarSize / 2f, config.avatarSize / 2f); + postUserImage.setClip(circle); + } else { + Rectangle clip = new Rectangle(config.avatarSize, config.avatarSize); + clip.setArcWidth(config.avatarArcSize); + clip.setArcHeight(config.avatarArcSize); + postUserImage.setClip(clip); + } + return postUserImage; + } + + static String fixup(String source) { + return source.replaceAll("[\n\r]+", "|").replaceAll("[\u200D]",""); + } + + @Override + public boolean requiresPlatformThread() { + return false; + } + + @Override + public java.time.Duration preferredStepDuration(final MachineContext context) { + return java.time.Duration.ofMillis(config.getStepDuration()); + } + + /** + * Implementation of {@link Step.Factory} as Service implementation creating + * {@link ShowPosts}. + */ + public static final class FactoryImpl implements Step.Factory { + @Override + public Step create(final StepEngineSettings.StepDefinition stepDefinition) { + return new ShowPosts(stepDefinition.getConfig(ShowPosts.Config.class)); + } + + @Override + public Class getStepClass() { + return ShowPosts.class; + } + + @Override + public Collection> getRequiredDataProviders(final StepEngineSettings.StepDefinition stepSettings) { + return Arrays.asList(TweetStreamDataProvider.class, TweetUserProfileImageDataProvider.class); + } + } + + public static class Config extends AbstractConfig { + public double layoutX = 0; + public double layoutY = 0; + public double width = 800; + public double titleHeight = 60; + public double postVGap = 10; + public double postHGap = 10; + public double postHeight = 150; + public int postFontSize = 13; + public int postUserFontSize = 13; + public int columns = 1; + public int avatarSize = 64; + public int avatarArcSize = 20; + public boolean circularAvatar = true; + } +} diff --git a/conference-stepengine/src/main/java/org/tweetwallfx/conference/stepengine/steps/ShowSchedule.java b/conference-stepengine/src/main/java/org/tweetwallfx/conference/stepengine/steps/ShowSchedule.java index 6a69747c3..f47fb3b95 100644 --- a/conference-stepengine/src/main/java/org/tweetwallfx/conference/stepengine/steps/ShowSchedule.java +++ b/conference-stepengine/src/main/java/org/tweetwallfx/conference/stepengine/steps/ShowSchedule.java @@ -116,10 +116,8 @@ public void doStep(final MachineContext context) { int col = 0; int row = 0; - Iterator iterator = dataProvider.getFilteredSessionData().iterator(); String oldRoom = null; - while (iterator.hasNext()) { - var sessionData = iterator.next(); + for (SessionData sessionData : dataProvider.getFilteredSessionData()) { if (null == oldRoom) { oldRoom = sessionData.room.getName(); } @@ -266,7 +264,7 @@ private Pane createSessionNode(final MachineContext context, final SessionData s if (config.showTags) { var tags = new FlowPane(); tags.getStyleClass().add("tags"); - sessionData.tags.stream().forEach(tag -> { + sessionData.tags.forEach(tag -> { var tagLabel = new Label(tag); tagLabel.getStyleClass().add("tagLabel"); tags.getChildren().add(tagLabel); diff --git a/conference-stepengine/src/main/resources/META-INF/services/org.tweetwallfx.stepengine.api.Step$Factory b/conference-stepengine/src/main/resources/META-INF/services/org.tweetwallfx.stepengine.api.Step$Factory index b2d590e05..5c076b11a 100644 --- a/conference-stepengine/src/main/resources/META-INF/services/org.tweetwallfx.stepengine.api.Step$Factory +++ b/conference-stepengine/src/main/resources/META-INF/services/org.tweetwallfx.stepengine.api.Step$Factory @@ -1,3 +1,4 @@ +org.tweetwallfx.conference.stepengine.steps.ShowPosts$FactoryImpl org.tweetwallfx.conference.stepengine.steps.ShowSchedule$FactoryImpl org.tweetwallfx.conference.stepengine.steps.ShowTopRated$FactoryImpl org.tweetwallfx.conference.stepengine.steps.SpeakerImageMosaicStep$FactoryImpl \ No newline at end of file diff --git a/tweet-impl-mastodon4j/src/main/java/org/tweetwallfx/tweet/impl/mastodon4j/MastodonStatus.java b/tweet-impl-mastodon4j/src/main/java/org/tweetwallfx/tweet/impl/mastodon4j/MastodonStatus.java index 5d97b75da..7655a5cfb 100644 --- a/tweet-impl-mastodon4j/src/main/java/org/tweetwallfx/tweet/impl/mastodon4j/MastodonStatus.java +++ b/tweet-impl-mastodon4j/src/main/java/org/tweetwallfx/tweet/impl/mastodon4j/MastodonStatus.java @@ -1,7 +1,7 @@ /* * The MIT License (MIT) * - * Copyright (c) 2015-2023 TweetWallFX + * Copyright (c) 2015-2024 TweetWallFX * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -24,8 +24,14 @@ package org.tweetwallfx.tweet.impl.mastodon4j; import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.nodes.Node; +import org.jsoup.nodes.TextNode; import org.jsoup.safety.Cleaner; import org.jsoup.safety.Safelist; +import org.jsoup.select.NodeTraversor; +import org.jsoup.select.NodeVisitor; import org.mastodon4j.core.api.entities.Status; import org.tweetwallfx.tweet.api.Tweet; import org.tweetwallfx.tweet.api.User; @@ -36,14 +42,42 @@ import org.tweetwallfx.tweet.api.entry.UserMentionTweetEntry; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; +import java.util.StringJoiner; final class MastodonStatus implements Tweet { - private final Status status; + private final String text; + private final List hashtagTweetEntryList; public MastodonStatus(Status status) { this.status = status; + Cleaner cleaner = new Cleaner(Safelist.none().addTags("img", "movie", "a")); + Document document = cleaner.clean(Jsoup.parse(status.content())); + hashtagTweetEntryList = new ArrayList<>(); + NodeTraversor.traverse(new NodeVisitor() { + @Override + public void head(Node node, int i) { + if (node instanceof TextNode textNode) { + //String wholeText = textNode.getWholeText(); + hashtagTweetEntryList.add(null); + } else if (node instanceof Element element) { + switch (element.nodeName()) { + case "a" -> { + } + case "img" -> { + } + case "movie" -> { + } + default -> { + } + } + } + } + }, document); + this.text = document.text(); } @Override @@ -106,8 +140,7 @@ public Tweet getOriginTweet() { @Override public String getText() { - Cleaner cleaner = new Cleaner(Safelist.none()); - return cleaner.clean(Jsoup.parse(status.content())).text(); + return text; } @Override diff --git a/tweet-impl-mastodon4j/src/test/java/org/tweetwallfx/tweet/impl/mastodon4j/MastodonStatusTest.java b/tweet-impl-mastodon4j/src/test/java/org/tweetwallfx/tweet/impl/mastodon4j/MastodonStatusTest.java index d1783300f..faf22e4b0 100644 --- a/tweet-impl-mastodon4j/src/test/java/org/tweetwallfx/tweet/impl/mastodon4j/MastodonStatusTest.java +++ b/tweet-impl-mastodon4j/src/test/java/org/tweetwallfx/tweet/impl/mastodon4j/MastodonStatusTest.java @@ -1,7 +1,7 @@ /* * The MIT License (MIT) * - * Copyright (c) 2023 TweetWallFX + * Copyright (c) 2023-2024 TweetWallFX * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -32,9 +32,13 @@ import static org.assertj.core.api.Assertions.assertThat; class MastodonStatusTest { + public static final String STATUS_CONTENT = """ +

the status #message html to @TweetwallFX, a new line: + some nice image + and a #special link to some content for @reinhapa

"""; ZonedDateTime createdAt = ZonedDateTime.now(ZoneId.systemDefault()); MastodonStatus status = new MastodonStatus(new Status("42", null, createdAt, null, - "

the status message html

", null, null, null, null, + STATUS_CONTENT, null, null, null, null, null, null, null, null, 33, 22, null, null, null, null, null, null, null, "german", null, null, null, true, null, null, null, null)); @@ -107,7 +111,9 @@ void getOriginTweet() { @Test void getText() { - assertThat(status.getText()).isEqualTo("the status message html"); + assertThat(status.getText()).isEqualTo(""" + the status #message html to @TweetwallFX, a new line: \ + some nice image and a #special link to some content for @reinhapa"""); assertThat(statusWithoutOptionals.getText()).isEmpty(); }