runelite-client: add news feed plugin
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 |
Reference in New Issue
Block a user