runelite-client: add news feed plugin

This commit is contained in:
Lotto
2018-03-02 23:28:27 +01:00
committed by Adam
parent b1990c72c5
commit 92e224fb94
6 changed files with 535 additions and 0 deletions

View File

@@ -0,0 +1,46 @@
package net.runelite.client.plugins.feed;
import net.runelite.client.config.Config;
import net.runelite.client.config.ConfigGroup;
import net.runelite.client.config.ConfigItem;
@ConfigGroup(
keyName = "feed",
name = "News Feed",
description = "Displays client and game-related news"
)
public interface FeedConfig extends Config
{
@ConfigItem(
keyName = "includeBlogPosts",
name = "Include Blog Posts",
description = "Configures whether blog posts are displayed",
position = 0
)
default boolean includeBlogPosts()
{
return true;
}
@ConfigItem(
keyName = "includeTweets",
name = "Include Tweets",
description = "Configures whether tweets are displayed",
position = 1
)
default boolean includeTweets()
{
return true;
}
@ConfigItem(
keyName = "includeOsrsNews",
name = "Include OSRS News",
description = "Configures whether OSRS news are displayed",
position = 2
)
default boolean includeOsrsNews()
{
return true;
}
}

View File

@@ -0,0 +1,358 @@
/*
* Copyright (c) 2018, Lotto <https://github.com/devLotto>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.plugins.feed;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.font.FontRenderContext;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Duration;
import java.time.Instant;
import java.util.Comparator;
import java.util.function.Supplier;
import javax.imageio.ImageIO;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.border.EmptyBorder;
import lombok.extern.slf4j.Slf4j;
import net.runelite.client.ui.FontManager;
import net.runelite.client.ui.PluginPanel;
import net.runelite.http.api.RuneLiteAPI;
import net.runelite.http.api.feed.FeedItem;
import net.runelite.http.api.feed.FeedItemType;
import net.runelite.http.api.feed.FeedResult;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
@Slf4j
class FeedPanel extends PluginPanel
{
private static BufferedImage RUNELITE_ICON;
private static BufferedImage OSRS_ICON;
private static final Color TWEET_BACKGROUND = new Color(15, 15, 15);
private static final Color OSRS_NEWS_BACKGROUND = new Color(36, 30, 19);
private static final Color BLOG_POST_BACKGROUND = new Color(11, 30, 41);
private static final int MAX_CONTENT_LINES = 3;
private static final int CONTENT_WIDTH = 148;
private static final int TIME_WIDTH = 20;
private static final Comparator<FeedItem> FEED_ITEM_COMPARATOR = (o1, o2) ->
{
if (o1.getType() != o2.getType())
{
if (o1.getType() == FeedItemType.BLOG_POST)
{
return -1;
}
else if (o2.getType() == FeedItemType.BLOG_POST)
{
return 1;
}
}
return -Long.compare(o1.getTimestamp(), o2.getTimestamp());
};
static
{
try
{
RUNELITE_ICON = ImageIO.read(FeedPanel.class.getResourceAsStream("runelite.png"));
}
catch (IOException e)
{
log.warn("Client icon failed to load", e);
}
try
{
OSRS_ICON = ImageIO.read(FeedPanel.class.getResourceAsStream("osrs.png"));
}
catch (IOException e)
{
log.warn("OSRS icon failed to load", e);
}
}
private final FeedConfig config;
private final Supplier<FeedResult> feedSupplier;
FeedPanel(FeedConfig config, Supplier<FeedResult> feedSupplier)
{
this.config = config;
this.feedSupplier = feedSupplier;
}
void rebuildFeed()
{
FeedResult feed = feedSupplier.get();
if (feed == null)
{
return;
}
SwingUtilities.invokeLater(() ->
{
removeAll();
feed.getItems()
.stream()
.filter(f -> f.getType() != FeedItemType.BLOG_POST || config.includeBlogPosts())
.filter(f -> f.getType() != FeedItemType.TWEET || config.includeTweets())
.filter(f -> f.getType() != FeedItemType.OSRS_NEWS || config.includeOsrsNews())
.sorted(FEED_ITEM_COMPARATOR)
.forEach(this::addItemToPanel);
});
}
private void addItemToPanel(FeedItem item)
{
JPanel avatarAndRight = new JPanel(new BorderLayout());
avatarAndRight.setPreferredSize(new Dimension(0, 56));
JLabel avatar = new JLabel();
// width = 48+4 to compensate for the border
avatar.setPreferredSize(new Dimension(52, 48));
avatar.setBorder(new EmptyBorder(0, 4, 0, 0));
switch (item.getType())
{
case TWEET:
try
{
Request request = new Request.Builder()
.url(item.getAvatar())
.build();
RuneLiteAPI.CLIENT.newCall(request).enqueue(new Callback()
{
@Override
public void onFailure(Call call, IOException e)
{
log.warn(null, e);
}
@Override
public void onResponse(Call call, Response response) throws IOException
{
try (ResponseBody responseBody = response.body())
{
if (!response.isSuccessful())
{
log.warn("Failed to download image " + item.getAvatar());
return;
}
avatar.setIcon(new ImageIcon(ImageIO.read(responseBody.byteStream())));
}
}
});
}
catch (IllegalArgumentException | NullPointerException e)
{
log.warn(null, e);
}
avatarAndRight.setBackground(TWEET_BACKGROUND);
break;
case OSRS_NEWS:
if (OSRS_ICON != null)
{
avatar.setIcon(new ImageIcon(OSRS_ICON));
}
avatarAndRight.setBackground(OSRS_NEWS_BACKGROUND);
break;
default:
if (RUNELITE_ICON != null)
{
avatar.setIcon(new ImageIcon(RUNELITE_ICON));
}
avatarAndRight.setBackground(BLOG_POST_BACKGROUND);
break;
}
JPanel upAndContent = new JPanel();
upAndContent.setLayout(new BoxLayout(upAndContent, BoxLayout.Y_AXIS));
upAndContent.setBorder(new EmptyBorder(4, 8, 4, 4));
upAndContent.setBackground(null);
JPanel titleAndTime = new JPanel();
titleAndTime.setLayout(new BorderLayout());
titleAndTime.setBackground(null);
Color darkerForeground = UIManager.getColor("Label.foreground").darker();
JLabel titleLabel = new JLabel(item.getTitle());
titleLabel.setFont(FontManager.getRunescapeSmallFont());
titleLabel.setBackground(null);
titleLabel.setForeground(darkerForeground);
titleLabel.setPreferredSize(new Dimension(CONTENT_WIDTH - TIME_WIDTH, 0));
Duration duration = Duration.between(Instant.ofEpochMilli(item.getTimestamp()), Instant.now());
JLabel timeLabel = new JLabel(durationToString(duration));
timeLabel.setFont(FontManager.getRunescapeSmallFont());
timeLabel.setForeground(darkerForeground);
titleAndTime.add(titleLabel, BorderLayout.WEST);
titleAndTime.add(timeLabel, BorderLayout.EAST);
JPanel content = new JPanel(new BorderLayout());
content.setBackground(null);
JLabel contentLabel = new JLabel(lineBreakText(item.getContent(), FontManager.getRunescapeSmallFont()));
contentLabel.setBorder(new EmptyBorder(2, 0, 0, 0));
contentLabel.setFont(FontManager.getRunescapeSmallFont());
contentLabel.setForeground(darkerForeground);
content.add(contentLabel, BorderLayout.CENTER);
upAndContent.add(titleAndTime);
upAndContent.add(content);
upAndContent.add(new Box.Filler(new Dimension(0, 0),
new Dimension(0, Short.MAX_VALUE),
new Dimension(0, Short.MAX_VALUE)));
avatarAndRight.add(avatar, BorderLayout.WEST);
avatarAndRight.add(upAndContent, BorderLayout.CENTER);
Color backgroundColor = avatarAndRight.getBackground();
Color hoverColor = backgroundColor.brighter().brighter();
Color pressedColor = hoverColor.brighter();
avatarAndRight.addMouseListener(new MouseAdapter()
{
@Override
public void mouseEntered(MouseEvent e)
{
avatarAndRight.setBackground(hoverColor);
}
@Override
public void mouseExited(MouseEvent e)
{
avatarAndRight.setBackground(backgroundColor);
}
@Override
public void mousePressed(MouseEvent e)
{
avatarAndRight.setBackground(pressedColor);
}
@Override
public void mouseReleased(MouseEvent e)
{
avatarAndRight.setBackground(hoverColor);
Desktop desktop = Desktop.getDesktop();
if (!desktop.isSupported(Desktop.Action.BROWSE))
{
log.info("Desktop browser is not supported");
return;
}
try
{
desktop.browse(new URI(item.getUrl()));
}
catch (IOException | URISyntaxException ex)
{
log.warn("Unable to open URL " + item.getUrl(), ex);
}
}
});
add(avatarAndRight);
}
private String durationToString(Duration duration)
{
if (duration.getSeconds() >= 60 * 60 * 24)
{
return (int) (duration.getSeconds() / (60 * 60 * 24)) + "d";
}
else if (duration.getSeconds() >= 60 * 60)
{
return (int) (duration.getSeconds() / (60 * 60)) + "h";
}
return (int) (duration.getSeconds() / 60) + "m";
}
private String lineBreakText(String text, Font font)
{
StringBuilder newText = new StringBuilder("<html>");
FontRenderContext fontRenderContext = new FontRenderContext(font.getTransform(),
true, true);
int lines = 0;
int pos = 0;
String[] words = text.split(" ");
String line = "";
while (lines < MAX_CONTENT_LINES && pos < words.length)
{
String newLine = pos > 0 ? line + " " + words[pos] : words[pos];
double width = font.getStringBounds(newLine, fontRenderContext).getWidth();
if (width >= CONTENT_WIDTH)
{
newText.append(line);
newText.append("<br>");
line = "";
lines++;
}
else
{
line = newLine;
pos++;
}
}
newText.append(line);
newText.append("</html>");
return newText.toString();
}
}

View File

@@ -0,0 +1,131 @@
/*
* Copyright (c) 2018, Lotto <https://github.com/devLotto>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.plugins.feed;
import com.google.common.base.Suppliers;
import com.google.common.eventbus.Subscribe;
import com.google.inject.Provides;
import java.io.IOException;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import javax.imageio.ImageIO;
import javax.inject.Inject;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.events.ConfigChanged;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
import net.runelite.client.task.Schedule;
import net.runelite.client.ui.ClientUI;
import net.runelite.client.ui.NavigationButton;
import net.runelite.http.api.feed.FeedClient;
import net.runelite.http.api.feed.FeedResult;
@PluginDescriptor(
name = "News Feed",
loadWhenOutdated = true
)
@Slf4j
public class FeedPlugin extends Plugin
{
@Inject
private ClientUI ui;
@Inject
private FeedConfig config;
@Inject
private ScheduledExecutorService executorService;
private FeedPanel feedPanel;
private NavigationButton navButton;
private FeedClient feedClient = new FeedClient();
private Supplier<FeedResult> feedSupplier = Suppliers.memoizeWithExpiration(() ->
{
try
{
return feedClient.lookupFeed();
}
catch (IOException e)
{
log.warn(null, e);
}
return null;
}, 10, TimeUnit.MINUTES);
@Override
protected void startUp() throws Exception
{
feedPanel = new FeedPanel(config, feedSupplier);
navButton = new NavigationButton(
"News Feed",
ImageIO.read(getClass().getResourceAsStream("icon.png")),
() -> feedPanel);
ui.getPluginToolbar().addNavigation(navButton);
executorService.submit(this::updateFeed);
}
@Override
protected void shutDown() throws Exception
{
ui.getPluginToolbar().removeNavigation(navButton);
}
private void updateFeed()
{
feedPanel.rebuildFeed();
}
@Subscribe
public void onConfigChanged(ConfigChanged event)
{
if (event.getGroup().equals("feed"))
{
executorService.submit(this::updateFeed);
}
}
@Schedule(
period = 10,
unit = ChronoUnit.MINUTES,
asynchronous = true
)
private void updateFeedTask()
{
updateFeed();
}
@Provides
FeedConfig provideConfig(ConfigManager configManager)
{
return configManager.getConfig(FeedConfig.class);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB