diff --git a/client/src/main/java/net/xircon/xenon/client/Xenon.java b/client/src/main/java/net/xircon/xenon/client/Xenon.java index cefe0cd..dd4c678 100644 --- a/client/src/main/java/net/xircon/xenon/client/Xenon.java +++ b/client/src/main/java/net/xircon/xenon/client/Xenon.java @@ -2,21 +2,25 @@ package net.xircon.xenon.client; import com.formdev.flatlaf.FlatDarkLaf; +import net.xircon.xenon.client.io.CustomOutputStream; import net.xircon.xenon.client.io.Log; import net.xircon.xenon.client.io.Util; import net.xircon.xenon.client.networking.Networking; +import net.xircon.xenon.client.networking.textclient.TextClient; +import org.jspecify.annotations.NonNull; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; +import java.io.PrintStream; import java.net.Socket; import java.util.Objects; public class Xenon implements Runnable { - private Thread mainThread; public static void main(String[] args) {new Xenon().start();} private static Socket mainSocket; + public Color buttonColour = new Color(55, 90, 129); + public Image xenonIcon; static final String propertiesFile = "client.properties"; // Properties @@ -28,12 +32,39 @@ public class Xenon implements Runnable { private static String nickname = "Test User"; public void start() { - mainThread = new Thread(this, "mainThread"); + Thread mainThread = new Thread(this, "mainThread"); mainThread.start(); } @Override public void run() { + // Load Icon + xenonIcon = Util.loadIcon("xenon.png"); + + // Set Swing LaF before all Swing Stuff gets init + try { + UIManager.setLookAndFeel( new FlatDarkLaf()); + } catch (UnsupportedLookAndFeelException e) { + Log.err("Java Swing GUI Look and Feel Did Not Correctly Apply! Falling Back to Default!"); + } + // Start Swing Logger ASAP + // Disable this If the entire thing dies :) + JFrame logFrame = new JFrame("Xenon Log"); + logFrame.setIconImage(xenonIcon); + // logFrame.setLayout(new BorderLayout()); + logFrame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE); + logFrame.setSize(640,480); + JTextArea logText = new JTextArea(); + logText.setAutoscrolls(true); + logText.setLineWrap(true); + logText.setEditable(false); + JScrollPane logScrollPane = new JScrollPane(logText); + logFrame.add(logScrollPane); + PrintStream systemOutput = new PrintStream(new CustomOutputStream(logText)); + System.setOut(systemOutput); + System.setErr(systemOutput); + + Log.info("Starting Xenon Client!"); // Get Property Values PortVoIP = Objects.requireNonNull(Util.GetProperties(propertiesFile, "PortVoIP", Util.PROPERTY_TYPE_INT)).intValue; @@ -43,21 +74,17 @@ public class Xenon implements Runnable { // Start Java Swing GUI JFrame mainFrame = new JFrame("Xenon Client"); + mainFrame.setIconImage(xenonIcon); mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); mainFrame.setSize(Width, Height); - try { - UIManager.setLookAndFeel( new FlatDarkLaf()); - } catch (UnsupportedLookAndFeelException e) { - Log.err("Java Swing GUI Look and Feel Did Not Correctly Apply! Falling Back to Default!"); - } // Creating the MenuBar and adding components - JMenuBar mb = new JMenuBar(); + JMenuBar menubar = new JMenuBar(); // Menu 1 JMenu m1 = new JMenu("File"); JMenuItem m11 = new JMenuItem("Open"); JMenuItem m12 = new JMenuItem("Save as"); - mb.add(m1); + menubar.add(m1); m1.add(m11); m1.add(m12); // Menu 2 @@ -66,8 +93,7 @@ public class Xenon implements Runnable { @Override public void actionPerformed(ActionEvent e) { String IPAddress = JOptionPane.showInputDialog(null, "IP Address Of Server", "127.0.0.1"); - if (IPAddress == null) {return;} - else { + if (IPAddress != null) { mainSocket = Networking.connectToServer(IPAddress, PortText); } } @@ -82,27 +108,53 @@ public class Xenon implements Runnable { @Override public void actionPerformed(ActionEvent e) { String Nick = JOptionPane.showInputDialog(null, "New Nickname", nickname); - if (Nick == null) {return;} - else { + if (Nick != null) { nickname = Nick; } } }); - mb.add(m2); + menubar.add(m2); m2.add(m21); m2.add(m22); m2.add(m23); + // Menu 3 - JMenu m3 = new JMenu("Help"); - JMenuItem m31 = new JMenuItem(new AbstractAction("About") { + JMenu m3 = new JMenu("Edit"); + // Preferences Menu + JFrame preferences = preferencesJFrame(); + JMenuItem m31 = new JMenuItem(new AbstractAction("Preferences") { + @Override + public void actionPerformed(ActionEvent e) { + preferences.setVisible(true); + } + }); + + + menubar.add(m3); + m3.add(m31); + + //Menu 4 + JMenu m4 = new JMenu("View"); + JMenuItem m41 = new JMenuItem(new AbstractAction("Log") { + @Override + public void actionPerformed(ActionEvent e) { + logFrame.setVisible(true); + } + }); + menubar.add(m4); + m4.add(m41); + + // Menu 5 + JMenu m5 = new JMenu("Help"); + JMenuItem m51 = new JMenuItem(new AbstractAction("About") { @Override public void actionPerformed(ActionEvent actionEvent) { JOptionPane.showMessageDialog(null, "Xenon Created By Xircon\nLicenced under the GNU GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.en.html)", "About Xenon", JOptionPane.INFORMATION_MESSAGE); } }); - mb.add(m3); - m3.add(m31); + menubar.add(m5); + m5.add(m51); // Creating the panel at bottom and adding components @@ -112,7 +164,9 @@ public class Xenon implements Runnable { sendField.setToolTipText("Enter Message"); sendField.setPreferredSize(new Dimension(200 ,20 )); JButton send = new JButton("Send"); + send.setBackground(buttonColour); JButton upload = new JButton("Upload"); + upload.setBackground(buttonColour); panel.add(sendField, BorderLayout.CENTER); JPanel buttonPanel = new JPanel(); buttonPanel.setLayout(new BorderLayout()); @@ -125,19 +179,98 @@ public class Xenon implements Runnable { if (mainSocket == null) {JOptionPane.showMessageDialog(null, "Ensure you are Connected to a Server!", "Error", JOptionPane.ERROR_MESSAGE);} else { Log.info("Sending " + sendField.getText()); - Networking.sendMessage(sendField.getText(), nickname, mainSocket); + Thread textClient = new Thread(new TextClient(mainSocket, nickname, sendField.getText())); + textClient.start(); SwingUtilities.invokeLater(() -> sendField.setText("")); } }); - // Text Area at the Center - JTextArea ta = new JTextArea(); + // Central Panel With Text Stuffs + JPanel centrePanel = new JPanel(); + JTextArea textarea = new JTextArea(); + textarea.setAutoscrolls(true); + textarea.setLineWrap(true); + textarea.setEditable(false); + textarea.setBackground(new Color(70,73,75)); + centrePanel.setLayout(new BorderLayout()); + centrePanel.add(panel, BorderLayout.SOUTH); + centrePanel.add(textarea, BorderLayout.CENTER); + + // Side Panel + JPanel sidePanel = sideJPanel(); //Adding Components to the Frame. - mainFrame.getContentPane().add(BorderLayout.SOUTH, panel); - mainFrame.getContentPane().add(BorderLayout.NORTH, mb); - mainFrame.getContentPane().add(BorderLayout.CENTER, ta); + mainFrame.getContentPane().add(BorderLayout.NORTH, menubar); + mainFrame.getContentPane().add(BorderLayout.CENTER, centrePanel); + mainFrame.getContentPane().add(BorderLayout.WEST, sidePanel); // Set GUI To Visible mainFrame.setVisible(true); } + + private @NonNull JPanel sideJPanel() { + JPanel sidePanel = new JPanel(new BorderLayout()); + sidePanel.setBackground(new Color(60,63,65)); + JPanel bottomSidePanel = new JPanel(new BorderLayout()); + // Mute Button + JToggleButton muteButton = muteJToggleButton(); + + // Deaf Button + JToggleButton deafButton = deafJToggleButton(); + + bottomSidePanel.add(muteButton, BorderLayout.WEST); + bottomSidePanel.add(deafButton, BorderLayout.CENTER); + sidePanel.add(bottomSidePanel, BorderLayout.SOUTH); + return sidePanel; + } + + private @NonNull JToggleButton deafJToggleButton() { + JToggleButton deafButton = new JToggleButton("Deafen"); + deafButton.setBackground(buttonColour); + deafButton.setForeground(Color.WHITE); + deafButton.setFocusPainted(false); + deafButton.setBorderPainted(false); + deafButton.addActionListener(e -> { + if (deafButton.isSelected()) { + deafButton.setBackground(Color.RED); // Change to RED + } else { + deafButton.setBackground(buttonColour); + } + }); + return deafButton; + } + + private @NonNull JToggleButton muteJToggleButton() { + JToggleButton muteButton = new JToggleButton("Mute"); + muteButton.setBackground(buttonColour); + muteButton.setForeground(Color.WHITE); + muteButton.setFocusPainted(false); + muteButton.setBorderPainted(false); + muteButton.addActionListener(e -> { + if (muteButton.isSelected()) { + muteButton.setBackground(Color.RED); // Change to RED + } else { + muteButton.setBackground(buttonColour); + } + }); + return muteButton; + } + + private @NonNull JFrame preferencesJFrame() { + JFrame preferences = new JFrame("Preferences"); + preferences.setSize(800,600); + preferences.setIconImage(xenonIcon); + preferences.setLayout(new BorderLayout()); + preferences.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE); + JPanel preferencesSidePanel = new JPanel(new BorderLayout()); + JButton preferencesGeneralButton = new JButton(new AbstractAction("General") { + @Override + public void actionPerformed(ActionEvent e) { + + } + }); + preferencesSidePanel.add(preferencesGeneralButton); + preferences.add(preferencesSidePanel, BorderLayout.WEST); + + return preferences; + } } \ No newline at end of file diff --git a/client/src/main/java/net/xircon/xenon/client/io/CustomOutputStream.java b/client/src/main/java/net/xircon/xenon/client/io/CustomOutputStream.java new file mode 100644 index 0000000..73c6147 --- /dev/null +++ b/client/src/main/java/net/xircon/xenon/client/io/CustomOutputStream.java @@ -0,0 +1,23 @@ +package net.xircon.xenon.client.io; + +import javax.swing.*; +import java.io.IOException; +import java.io.OutputStream; + +public class CustomOutputStream extends OutputStream { + private JTextArea textArea; + + public CustomOutputStream(JTextArea textArea) { + this.textArea = textArea; + } + + @Override + public void write(int b) throws IOException { + // redirects data to the text area + textArea.append(String.valueOf((char)b)); + // scrolls the text area to the end of data + textArea.setCaretPosition(textArea.getDocument().getLength()); + // keeps the textArea up to date + textArea.update(textArea.getGraphics()); + } +} \ No newline at end of file diff --git a/client/src/main/java/net/xircon/xenon/client/io/Log.java b/client/src/main/java/net/xircon/xenon/client/io/Log.java index d64d4e7..51684d0 100644 --- a/client/src/main/java/net/xircon/xenon/client/io/Log.java +++ b/client/src/main/java/net/xircon/xenon/client/io/Log.java @@ -2,6 +2,7 @@ package net.xircon.xenon.client.io; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.Appender; import javax.swing.*; @@ -23,3 +24,4 @@ public class Log { logger.info(message); } } + diff --git a/client/src/main/java/net/xircon/xenon/client/io/Util.java b/client/src/main/java/net/xircon/xenon/client/io/Util.java index 61603a5..2beb2e8 100644 --- a/client/src/main/java/net/xircon/xenon/client/io/Util.java +++ b/client/src/main/java/net/xircon/xenon/client/io/Util.java @@ -1,11 +1,11 @@ package net.xircon.xenon.client.io; import javax.annotation.Nullable; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.*; import java.net.URL; +import java.util.Objects; import java.util.Properties; public class Util { @@ -47,6 +47,7 @@ public class Util { Log.warn("[Property Loader] Unable to Find Property/Invalid Type!"); return null; } + public static String GetExternalIP() throws IOException { URL whatismyip = new URL("http://checkip.amazonaws.com"); BufferedReader in = new BufferedReader(new InputStreamReader( @@ -55,4 +56,13 @@ public class Util { // Return IP As String return in.readLine(); } + + @Nullable + public static BufferedImage loadIcon(String path) { + try { + return ImageIO.read(Objects.requireNonNull(Util.class.getClassLoader().getResource(path))); + } catch (IOException e) { + throw new RuntimeException(e); + } + } } diff --git a/client/src/main/java/net/xircon/xenon/client/networking/Networking.java b/client/src/main/java/net/xircon/xenon/client/networking/Networking.java index 324448b..eb5d468 100644 --- a/client/src/main/java/net/xircon/xenon/client/networking/Networking.java +++ b/client/src/main/java/net/xircon/xenon/client/networking/Networking.java @@ -13,7 +13,7 @@ import java.net.SocketAddress; import java.net.SocketTimeoutException; public class Networking { - private static final String nameTerminator = "\uffff"; + @Nullable public static Socket connectToServer(String Address, int Port) { @@ -44,18 +44,4 @@ public class Networking { Log.warn("Ignoring Disconnect Request, Null Socket!"); } } - - public static String sendMessage(String message, String nickname, Socket socket) { - try { - BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); - PrintWriter output = new PrintWriter(socket.getOutputStream(), true); - output.println(nickname + nameTerminator + message); - String messageFromServer = input.readLine(); - Log.info("Server Message: " + messageFromServer); - return messageFromServer; - } catch (IOException e) { - Log.warn("Could not Read/Send Message!"); - return null; - } - } } diff --git a/client/src/main/java/net/xircon/xenon/client/networking/textclient/TextClient.java b/client/src/main/java/net/xircon/xenon/client/networking/textclient/TextClient.java index 37de8c0..736d8f1 100644 --- a/client/src/main/java/net/xircon/xenon/client/networking/textclient/TextClient.java +++ b/client/src/main/java/net/xircon/xenon/client/networking/textclient/TextClient.java @@ -1,15 +1,33 @@ package net.xircon.xenon.client.networking.textclient; +import net.xircon.xenon.client.io.Log; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; import java.net.Socket; public class TextClient implements Runnable { + private final String nameTerminator = "\uffff"; + private String nickname; + private String message; + private Socket socket; - public TextClient(Socket serverSocket) { - + public TextClient(Socket serverSocket, String nickname, String message) { + this.socket = serverSocket; this.message = message; this.nickname = nickname; } @Override public void run() { - + try { + BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); + PrintWriter output = new PrintWriter(socket.getOutputStream(), true); + output.println(nickname + nameTerminator + message); + String messageFromServer = input.readLine(); + Log.info("Server Message: " + messageFromServer); + } catch (IOException e) { + Log.warn("Could not Read/Send Message!"); + } } } diff --git a/client/src/main/resources/xenon.jpg b/client/src/main/resources/xenon.jpg new file mode 100644 index 0000000..92d9f0d Binary files /dev/null and b/client/src/main/resources/xenon.jpg differ diff --git a/client/src/main/resources/xenon.png b/client/src/main/resources/xenon.png new file mode 100644 index 0000000..31f2922 Binary files /dev/null and b/client/src/main/resources/xenon.png differ