Jump to content

RuneLite - News Feed Panel


Recommended Posts

Most of the code is from RuneLite - you can find the repo @ https://github.com/runelite/runelite/

 

Create the MySQL table - 

CREATE TABLE `runelite_feed` (
  `type` text NOT NULL,
  `avatar` text NOT NULL,
  `title` text NOT NULL,
  `content` text NOT NULL,
  `url` text NOT NULL,
  `timestamp` bigint(20) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;


INSERT INTO `runelite_feed` (`type`, `avatar`, `title`, `content`, `url`, `timestamp`) VALUES
('UPDATE', 'http://127.0.0.1/runelite/runelite.png', 'Feed working', 'RuneLite Feed Working', 'https://google.com', 1599613204241);

php file web sided - this is where the client will grab news

<?php

 $con=mysqli_connect("localhost","my_user","my_password","my_db");

  if (mysqli_connect_errno())
  {
   echo "Failed to connect to MySQL: " . mysqli_connect_error();
  }

  $query = "SELECT * FROM `runelite_feed`";

  $result = mysqli_query($con,$query);

  $rows = array();
  while($r = mysqli_fetch_array($result, MYSQLI_ASSOC)) {
    $rows[] = $r;
  }
  echo json_encode($rows);

  mysqli_close($con);
?>

 

Now onto the client..

FeedPanel.java

import lombok.extern.slf4j.Slf4j;
import net.runelite.client.ui.ColorScheme;
import net.runelite.client.ui.FontManager;
import net.runelite.client.ui.PluginPanel;
import net.runelite.client.util.LinkBrowser;
import net.runelite.http.api.RuneLiteAPI;
import net.runelite.http.api.feed.FeedItem;
import net.runelite.http.api.feed.FeedItemType;
import okhttp3.*;

import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
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.time.Duration;
import java.time.Instant;
import java.util.Comparator;
import java.util.List;
import java.util.function.Supplier;

@Slf4j
class FeedPanel extends PluginPanel
{
   private static final ImageIcon RUNELITE_ICON;
   private static final ImageIcon 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;

   /**
    * Holds all feed items.
    */
   private final JPanel feedContainer = new JPanel();

   private static final Comparator<FeedItem> FEED_ITEM_COMPARATOR = (o1, o2) ->
   {
      if (o1.getType() != o2.getType())
      {
         if (o1.getType() == FeedItemType.ANNOUNCEMENT)
         {
            return -1;
         }
         else if (o2.getType() == FeedItemType.ANNOUNCEMENT)
         {
            return 1;
         }
      }

      return -Long.compare(o1.getTimestamp(), o2.getTimestamp());
   };

   static
   {
      try
      {
         synchronized (ImageIO.class)
         {
            RUNELITE_ICON = new ImageIcon(ImageIO.read(FeedPanel.class.getResourceAsStream("runelite.png")));
            OSRS_ICON = new ImageIcon(ImageIO.read(FeedPanel.class.getResourceAsStream("osrs.png")));
         }
      }
      catch (IOException e)
      {
         throw new RuntimeException(e);
      }
   }

   private final FeedConfig config;
   private final Supplier<List<FeedItem>> feedSupplier;

   FeedPanel(FeedConfig config, Supplier<List<FeedItem>> feedSupplier)
   {
      this.config = config;
      this.feedSupplier = feedSupplier;

      setBorder(new EmptyBorder(10, 10, 10, 10));
      setBackground(ColorScheme.DARK_GRAY_COLOR);
      setLayout(new BorderLayout());

      feedContainer.setLayout(new GridLayout(0, 1, 0, 4));
      feedContainer.setBackground(ColorScheme.DARK_GRAY_COLOR);

      JLabel title = new JLabel("News feed");
      title.setBorder(new EmptyBorder(0, 0, 9, 0));
      title.setForeground(Color.WHITE);

      add(title, BorderLayout.NORTH);
      add(feedContainer, BorderLayout.CENTER);
   }

   void rebuildFeed()
   {
      List<FeedItem> feed = feedSupplier.get();

      if (feed == null)
      {
         return;
      }

      SwingUtilities.invokeLater(() ->
      {
         feedContainer.removeAll();

         feed
            .stream()
            .filter(f -> f.getType() != FeedItemType.ANNOUNCEMENT || config.includeAnnouncement())
            .filter(f -> f.getType() != FeedItemType.UPDATE || config.includeUpdate())
            .filter(f -> f.getType() != FeedItemType.MEDIA || config.includeMedia())
            .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));


            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;
                        }

                        BufferedImage icon;
                        synchronized (ImageIO.class)
                        {
                           icon = ImageIO.read(responseBody.byteStream());
                        }
                        avatar.setIcon(new ImageIcon(icon));
                     }
                  }
               });
            }
            catch (IllegalArgumentException | NullPointerException e)
            {
               log.warn(null, e);
            }
            avatarAndRight.setBackground(BLOG_POST_BACKGROUND);


      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);

      log.info("Adding feed item: " + item.getTitle() + " - " + item.getAvatar());

      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);
            avatarAndRight.setCursor(new Cursor(Cursor.HAND_CURSOR));
         }

         @Override
         public void mouseExited(MouseEvent e)
         {
            avatarAndRight.setBackground(backgroundColor);
            avatarAndRight.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
         }

         @Override
         public void mousePressed(MouseEvent e)
         {
            avatarAndRight.setBackground(pressedColor);
         }

         @Override
         public void mouseReleased(MouseEvent e)
         {
            avatarAndRight.setBackground(hoverColor);
            LinkBrowser.browse(item.getUrl());
         }
      });

      feedContainer.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();
   }
}

FeedConfig.java

import net.runelite.client.config.Config;
import net.runelite.client.config.ConfigGroup;
import net.runelite.client.config.ConfigItem;

@ConfigGroup("feed")
public interface FeedConfig extends Config
{
   @ConfigItem(
      keyName = "includeAnnouncement",
      name = "Include Announcements",
      description = "Configures whether Announcements are displayed",
      position = 0
   )
   default boolean includeAnnouncement()
   {
      return true;
   }

   @ConfigItem(
      keyName = "includeUpdate",
      name = "Include Updates",
      description = "Configures whether Updates are displayed",
      position = 1
   )
   default boolean includeUpdate()
   {
      return true;
   }

   @ConfigItem(
      keyName = "includeMedia",
      name = "Include Media Posts",
      description = "Configures whether Media posts are displayed",
      position = 2
   )
   default boolean includeMedia()
   {
      return true;
   }
}

FeedPlugin.java

@PluginDescriptor(
   name = "News Feed",
   description = "Show the latest RuneLite blog posts, OSRS news, and JMod Twitter posts",
   tags = {"external", "integration", "panel", "twitter"},
   loadWhenOutdated = true
)
@Slf4j
public class FeedPlugin extends Plugin
{
   @Inject
   private PluginToolbar pluginToolbar;

   @Inject
   private FeedConfig config;

   @Inject
   private ScheduledExecutorService executorService;

   private FeedPanel feedPanel;
   private NavigationButton navButton;

   private FeedClient feedClient = new FeedClient();
   private Supplier<List<FeedItem>> feedSupplier = Suppliers.memoizeWithExpiration(() ->
   {
      try
      {
         return feedClient.lookupFeed();
      }
      catch (IOException e)
      {
         log.warn(null, e);
      }
      return null;
   }, 10, TimeUnit.SECONDS);

   @Override
   protected void startUp() throws Exception
   {
      feedPanel = new FeedPanel(config, feedSupplier);

      BufferedImage icon;
      synchronized (ImageIO.class)
      {
         icon = ImageIO.read(getClass().getResourceAsStream("icon.png"));
      }

      navButton = NavigationButton.builder()
         .tooltip("News Feed")
         .icon(icon)
         .priority(8)
         .panel(feedPanel)
         .build();

      pluginToolbar.addNavigation(navButton);
      executorService.submit(this::updateFeed);
   }

   @Override
   protected void shutDown() throws Exception
   {
      pluginToolbar.removeNavigation(navButton);
   }

   private void updateFeed()
   {
      feedPanel.rebuildFeed();
   }

   @Subscribe
   public void onConfigChanged(ConfigChanged event)
   {
      if (event.getGroup().equals("feed"))
      {
         executorService.submit(this::updateFeed);
      }
   }

   @Schedule(
      period = 23,
      unit = TimeUnit.SECONDS,
      asynchronous = true
   )
   public void updateFeedTask()
   {
      updateFeed();
   }

   @Provides
   FeedConfig provideConfig(ConfigManager configManager)
   {
      return configManager.getConfig(FeedConfig.class);
   }
}

FeedClient.java - you will need to change the URL to direct too the php file you created earlier..

import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import net.runelite.http.api.RuneLiteAPI;
import okhttp3.Request;
import okhttp3.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.List;

public class FeedClient
{
   private static final Logger logger = LoggerFactory.getLogger(FeedClient.class);

   public List<FeedItem> lookupFeed() throws IOException
   {


      logger.debug("Built URI: {}", "http://127.0.0.1/runelite/feed/feed.php");

      Request request = new Request.Builder()
         .url("http://127.0.0.1/runelite/feed/feed.php")
         .build();

      try (Response response = RuneLiteAPI.CLIENT.newCall(request).execute())
      {
         if (!response.isSuccessful())
         {
            logger.debug("Error looking up feed: {}", response.message());
            return null;
         }

         InputStream in = response.body().byteStream();
         return RuneLiteAPI.GSON.fromJson(new InputStreamReader(in), new TypeToken<List<FeedItem>>(){}.getType());
      }
      catch (JsonParseException ex)
      {
         throw new IOException(ex);
      }
   }
}

 

Need help setting this up? Hope your happy to pay :) NULL#1111

Link to post
Share on other sites
  • 5 months later...
  • 2 months later...

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...