diff --git a/conference-stepengine/build.gradle b/conference-stepengine/build.gradle index c427f597d..259f1005b 100644 --- a/conference-stepengine/build.gradle +++ b/conference-stepengine/build.gradle @@ -28,6 +28,7 @@ dependencies { implementation 'de.jensd:fontawesomefx-fontawesome:4.7.0-11' implementation 'org.slf4j:slf4j-api' implementation project(':tweetwallfx-controls') + implementation project(':tweetwallfx-emoji') implementation project(':tweetwallfx-stepengine-dataproviders') implementation project(':tweetwallfx-transitions') } diff --git a/conference-stepengine/src/main/java/org/tweetwallfx/conference/stepengine/dataprovider/SessionData.java b/conference-stepengine/src/main/java/org/tweetwallfx/conference/stepengine/dataprovider/SessionData.java index 3be538efe..f319bdbb4 100644 --- a/conference-stepengine/src/main/java/org/tweetwallfx/conference/stepengine/dataprovider/SessionData.java +++ b/conference-stepengine/src/main/java/org/tweetwallfx/conference/stepengine/dataprovider/SessionData.java @@ -59,6 +59,7 @@ public class SessionData { public final List speakerObjects; public final int favouritesCount; public final String trackImageUrl; + public final List tags; private SessionData(final ScheduleSlot slot) { this.room = slot.getRoom(); @@ -76,6 +77,7 @@ private SessionData(final ScheduleSlot slot) { .findFirst() .orElse(0); this.trackImageUrl = talk.getTrack().getAvatarURL(); + this.tags = talk.getTags(); } public static List from(final List slots, final OffsetTime now, final ZoneId zoneId) { 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 c2c707643..18733b706 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 @@ -30,7 +30,6 @@ import java.util.Collection; import java.util.Iterator; import java.util.Optional; -import java.util.stream.Collectors; import javafx.animation.FadeTransition; import javafx.application.Platform; import javafx.geometry.Pos; @@ -39,6 +38,7 @@ import javafx.scene.control.Label; import javafx.scene.image.ImageView; import javafx.scene.layout.BorderPane; +import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.VBox; @@ -50,11 +50,13 @@ import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.tweetwallfx.conference.api.Speaker; import org.tweetwallfx.conference.stepengine.dataprovider.ScheduleDataProvider; import org.tweetwallfx.conference.stepengine.dataprovider.SessionData; import org.tweetwallfx.conference.stepengine.dataprovider.SpeakerImageProvider; import org.tweetwallfx.conference.stepengine.dataprovider.TrackImageDataProvider; 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; @@ -68,8 +70,8 @@ public class ShowSchedule implements Step { private static final Logger LOGGER = LoggerFactory.getLogger(ShowSchedule.class); private static final ZoneId ZONE_ID = Optional.ofNullable(System.getProperty("org.tweetwallfx.scheduledata.zone")) - .map(ZoneId::of) - .orElseGet(ZoneId::systemDefault); + .map(ZoneId::of) + .orElseGet(ZoneId::systemDefault); private static final DateTimeFormatter HOUR_MINUTES = DateTimeFormatter.ofPattern("HH:mm"); private final Config config; @@ -115,9 +117,18 @@ public void doStep(final MachineContext context) { int row = 0; Iterator iterator = dataProvider.getFilteredSessionData().iterator(); + String oldRoom = null; while (iterator.hasNext()) { - Pane sessionPane = createSessionNode(context, iterator.next()); - double sessionWidth = (config.width - config.sessionHGap) / 2.0; + var sessionData = iterator.next(); + if (null == oldRoom) { + oldRoom = sessionData.room.getName(); + } + if (config.autoSeparateRoomTypes && col != 0 && !oldRoom.startsWith(sessionData.room.getName().substring(0, 2))) { + row++; + col = 0; + } + Pane sessionPane = createSessionNode(context, sessionData); + double sessionWidth = (config.width - (config.columns - 1) * config.sessionHGap) / config.columns; sessionPane.setMinWidth(sessionWidth); sessionPane.setMaxWidth(sessionWidth); sessionPane.setPrefWidth(sessionWidth); @@ -127,10 +138,12 @@ public void doStep(final MachineContext context) { sessionPane.setLayoutX(col * (sessionWidth + config.sessionHGap)); sessionPane.setLayoutY(config.titleHeight + config.sessionVGap + (config.sessionHeight + config.sessionVGap) * row); scheduleNode.getChildren().add(sessionPane); - col = (col == 0) ? 1 : 0; - if (col == 0) { + col++; + if (col == config.columns) { row++; + col = 0; } + oldRoom = sessionData.room.getName(); } Platform.runLater(() -> { @@ -151,10 +164,23 @@ public java.time.Duration preferredStepDuration(final MachineContext context) { } private Pane createSessionNode(final MachineContext context, final SessionData sessionData) { - var speakerNames = new Label(sessionData.speakers.stream().collect(Collectors.joining(", "))); - speakerNames.setWrapText(true); - speakerNames.setTextAlignment(TextAlignment.RIGHT); - speakerNames.getStyleClass().add("speakerName"); + var speakerNames = new VBox(); + speakerNames.getStyleClass().add("speakerNames"); + + sessionData.speakerObjects.stream().forEach(speaker -> { + var speakerName = new Label(speaker.getFullName()); + speakerName.setTextAlignment(TextAlignment.RIGHT); + speakerName.getStyleClass().add("speakerName"); + speakerNames.getChildren().add(speakerName); + if (config.showCompanyName) { + speaker.getCompany().ifPresent(company -> { + var companyName = new Label("(" + company + ")"); + companyName.setTextAlignment(TextAlignment.RIGHT); + companyName.getStyleClass().add("companyName"); + speakerNames.getChildren().add(companyName); + }); + } + }); var room = new Label(sessionData.room.getName()); room.getStyleClass().add("room"); @@ -169,31 +195,25 @@ private Pane createSessionNode(final MachineContext context, final SessionData s if (config.showAvatar) { var speakerImageProvider = context.getDataProvider(SpeakerImageProvider.class); - var speakerImages = new HBox(config.avatarSpacing, sessionData.speakerObjects.stream() - .map(speakerImageProvider::getSpeakerImage) - .map(ImageView::new) - .peek(img -> { - // general image sizing - img.getStyleClass().add("speakerImage"); - img.setFitHeight(config.avatarSize); - img.setFitWidth(config.avatarSize); - }) - .peek(img -> { - // avatar image clipping - if (config.circularAvatar) { - Circle circle = new Circle(config.avatarSize/2f, config.avatarSize/2f, config.avatarSize/2f); - img.setClip(circle); - } else { - Rectangle clip = new Rectangle(config.avatarSize, config.avatarSize); - clip.setArcWidth(config.avatarArcSize); - clip.setArcHeight(config.avatarArcSize); - img.setClip(clip); - } - }) - .toArray(Node[]::new) - ); - - topLeft = new HBox(4, topLeftVBox, speakerImages); + if (config.compressedAvatars && sessionData.speakerObjects.size() >= config.compressedAvatarsLimit) { + var speakerImages = new Pane(); + var images = sessionData.speakerObjects.stream() + .map(speaker -> createSpeakerImage(speakerImageProvider, speaker)) + .toList(); + for (int i = 0; i < images.size(); i++) { + var image = images.get(i); + image.setLayoutX(i * (config.avatarSize * 3 / 4d + 2)); + image.setLayoutY(i % 2 * config.avatarSize / 2d + 2); + speakerImages.getChildren().add(image); + } + topLeft = new HBox(4, topLeftVBox, speakerImages); + } else { + var speakerImages = new HBox(config.avatarSpacing, sessionData.speakerObjects.stream() + .map(speaker -> createSpeakerImage(speakerImageProvider, speaker)) + .toArray(Node[]::new) + ); + topLeft = new HBox(4, topLeftVBox, speakerImages); + } } if (config.showFavourite && sessionData.favouritesCount >= 0) { @@ -209,9 +229,10 @@ private Pane createSessionNode(final MachineContext context, final SessionData s topLeftVBox.getChildren().add(favourites); } - var title = new Label(sessionData.title); - title.setWrapText(true); - title.setAlignment(Pos.BOTTOM_LEFT); + var title = new EmojiFlow(); + title.setText(sessionData.title); + title.setEmojiFitWidth(15); + title.setEmojiFitHeight(15); title.getStyleClass().add("title"); title.setMaxHeight(Double.MAX_VALUE); @@ -236,7 +257,28 @@ private Pane createSessionNode(final MachineContext context, final SessionData s var bpSessionBottomPane = new BorderPane(); bpSessionBottomPane.getStyleClass().add("sessionBottomPane"); bpSessionBottomPane.setRight(trackImageView); - bpSessionBottomPane.setCenter(bpTitle); + if (config.showTags) { + var tags = new FlowPane(); + tags.getStyleClass().add("tags"); + sessionData.tags.stream().forEach(tag -> { + var tagLabel = new Label(tag); + tagLabel.getStyleClass().add("tagLabel"); + tags.getChildren().add(tagLabel); + }); + + var bpTags = new BorderPane(); + bpTags.getStyleClass().add("tagPane"); + BorderPane.setAlignment(tags, Pos.BOTTOM_LEFT); + bpTags.setLeft(tags); + + VBox bpCenter = new VBox(bpTitle, bpTags); + bpCenter.getStyleClass().add("centerFlow"); + + bpSessionBottomPane.setCenter(bpCenter); + } else { + bpSessionBottomPane.setRight(trackImageView); + bpSessionBottomPane.setCenter(bpTitle); + } var bpSessionPane = new BorderPane(); bpSessionPane.getStyleClass().add("scheduleSession"); @@ -250,6 +292,30 @@ private Pane createSessionNode(final MachineContext context, final SessionData s return bpSessionPane; } + private Node createSpeakerImage(SpeakerImageProvider speakerImageProvider, Speaker speaker) { + var image = speakerImageProvider.getSpeakerImage(speaker); + var speakerImage = new ImageView(image); + speakerImage.getStyleClass().add("speakerImage"); + speakerImage.setPreserveRatio(true); + if (image.getWidth() > image.getHeight()) { + speakerImage.setFitHeight(config.avatarSize); + } else { + speakerImage.setFitWidth(config.avatarSize); + } + + // avatar image clipping + if (config.circularAvatar) { + Circle circle = new Circle(config.avatarSize / 2f, config.avatarSize / 2f, config.avatarSize / 2f); + speakerImage.setClip(circle); + } else { + Rectangle clip = new Rectangle(config.avatarSize, config.avatarSize); + clip.setArcWidth(config.avatarArcSize); + clip.setArcHeight(config.avatarArcSize); + speakerImage.setClip(clip); + } + return speakerImage; + } + /** * Implementation of {@link Step.Factory} as Service implementation creating * {@link ShowSchedule}. @@ -288,5 +354,11 @@ public static class Config extends AbstractConfig { public double sessionHeight = 200; public boolean showTrackAvatar = true; public boolean circularAvatar = true; + public boolean showTags = false; + public boolean compressedAvatars = true; + public int compressedAvatarsLimit = 4; + public int columns = 2; + public boolean autoSeparateRoomTypes = false; + public boolean showCompanyName = false; } } diff --git a/emoji/src/main/java/org/tweetwallfx/emoji/control/EmojiFlow.java b/emoji/src/main/java/org/tweetwallfx/emoji/control/EmojiFlow.java index ba23f708d..54c8b151f 100644 --- a/emoji/src/main/java/org/tweetwallfx/emoji/control/EmojiFlow.java +++ b/emoji/src/main/java/org/tweetwallfx/emoji/control/EmojiFlow.java @@ -25,6 +25,7 @@ import java.util.List; import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.scene.image.Image; import javafx.scene.image.ImageView; @@ -41,11 +42,13 @@ public class EmojiFlow extends TextFlow { private static final Logger LOG = LoggerFactory.getLogger(EmojiFlow.class); private final ObjectProperty textProperty = new SimpleObjectProperty<>(); - private final int emojiFitWidthProperty = 12; - private final int emojiFitHeightProperty = 12; + private final SimpleDoubleProperty emojiFitWidthProperty = new SimpleDoubleProperty(12); + private final SimpleDoubleProperty emojiFitHeightProperty = new SimpleDoubleProperty(12); public EmojiFlow() { this.textProperty.addListener((o, old, text) -> updateContent(text)); + this.emojiFitWidthProperty.addListener((o, old, newValue) -> updateContent(textProperty.get())); + this.emojiFitHeightProperty.addListener((o, old, newValue) -> updateContent(textProperty.get())); } public final String getText() { @@ -56,7 +59,24 @@ public final void setText(String text) { textProperty.set(text); } + public final double getEmojiFitWidth() { + return emojiFitWidthProperty.get(); + } + + public final void setEmojiFitWidth(double fitWidth) { + emojiFitWidthProperty.set(fitWidth); + } + + public final double getEmojiFitHeight() { + return emojiFitHeightProperty.get(); + } + + public final void setEmojiFitHeight(double fitHeight) { + emojiFitHeightProperty.set(fitHeight); + } + private void updateContent(String message) { + this.getChildren().clear(); List obs = Emojify.tokenizeStringToTextAndEmoji(message); for (Object ob : obs) { if (ob instanceof String s) { @@ -75,15 +95,15 @@ private void updateContent(String message) { private Text createTextNode(String text) { Text textNode = new Text(); textNode.setText(text); - textNode.getStyleClass().add("tweetText"); + textNode.getStyleClass().add("emojiFlow"); textNode.applyCss(); return textNode; } private ImageView createEmojiImageNode(Twemoji emoji) throws NullPointerException { ImageView imageView = new ImageView(); - imageView.setFitWidth(emojiFitWidthProperty); - imageView.setFitHeight(emojiFitHeightProperty); + imageView.setFitWidth(emojiFitWidthProperty.get()); + imageView.setFitHeight(emojiFitHeightProperty.get()); imageView.setImage(new Image(EmojiImageCache.INSTANCE.get(emoji.hex()).getInputStream())); return imageView; }