Move Envoy Client to client/ subdirectory
This commit is contained in:
		
							
								
								
									
										30
									
								
								client/src/main/java/envoy/client/Main.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								client/src/main/java/envoy/client/Main.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
package envoy.client;
 | 
			
		||||
 | 
			
		||||
import javafx.application.Application;
 | 
			
		||||
 | 
			
		||||
import envoy.client.ui.Startup;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Triggers application startup.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * To allow Maven shading, the main method has to be separated from the
 | 
			
		||||
 * {@link Startup} class which extends {@link Application}.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>Main.java</strong><br>
 | 
			
		||||
 * Created: <strong>05.07.2020</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
public class Main {
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Starts the application.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param args the command line arguments are processed by the
 | 
			
		||||
	 *             client configuration
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public static void main(String[] args) { Application.launch(Startup.class, args); }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										65
									
								
								client/src/main/java/envoy/client/data/Cache.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								client/src/main/java/envoy/client/data/Cache.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,65 @@
 | 
			
		||||
package envoy.client.data;
 | 
			
		||||
 | 
			
		||||
import java.io.Serializable;
 | 
			
		||||
import java.util.LinkedList;
 | 
			
		||||
import java.util.Queue;
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
import java.util.logging.Level;
 | 
			
		||||
import java.util.logging.Logger;
 | 
			
		||||
 | 
			
		||||
import envoy.util.EnvoyLog;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Stores elements in a queue to process them later.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>Cache.java</strong><br>
 | 
			
		||||
 * Created: <strong>6 Feb 2020</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @param <T> the type of cached elements
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.3-alpha
 | 
			
		||||
 */
 | 
			
		||||
public final class Cache<T> implements Consumer<T>, Serializable {
 | 
			
		||||
 | 
			
		||||
	private final Queue<T>			elements	= new LinkedList<>();
 | 
			
		||||
	private transient Consumer<T>	processor;
 | 
			
		||||
 | 
			
		||||
	private static final Logger	logger				= EnvoyLog.getLogger(Cache.class);
 | 
			
		||||
	private static final long	serialVersionUID	= 0L;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Adds an element to the cache.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param element the element to add
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	@Override
 | 
			
		||||
	public void accept(T element) {
 | 
			
		||||
		logger.log(Level.FINE, String.format("Adding element %s to cache", element));
 | 
			
		||||
		elements.offer(element);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public String toString() { return String.format("Cache[elements=" + elements + "]"); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Sets the processor to which cached elements are relayed.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param processor the processor to set
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public void setProcessor(Consumer<T> processor) { this.processor = processor; }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Relays all cached elements to the processor.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @throws IllegalStateException if the processor is not initialized
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public void relay() {
 | 
			
		||||
		if (processor == null) throw new IllegalStateException("Processor is not defined");
 | 
			
		||||
		elements.forEach(processor::accept);
 | 
			
		||||
		elements.clear();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										66
									
								
								client/src/main/java/envoy/client/data/CacheMap.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								client/src/main/java/envoy/client/data/CacheMap.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,66 @@
 | 
			
		||||
package envoy.client.data;
 | 
			
		||||
 | 
			
		||||
import java.io.Serializable;
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Stores a heterogeneous map of {@link Cache} objects with different type
 | 
			
		||||
 * parameters.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>CacheMap.java</strong><br>
 | 
			
		||||
 * Created: <strong>09.07.2020</strong><br>
 | 
			
		||||
 * 
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
public final class CacheMap implements Serializable {
 | 
			
		||||
 | 
			
		||||
	private final Map<Class<?>, Cache<?>> map = new HashMap<>();
 | 
			
		||||
 | 
			
		||||
	private static final long serialVersionUID = 1L;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Adds a cache to the map.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @param <T>   the type accepted by the cache
 | 
			
		||||
	 * @param key   the class that maps to the cache
 | 
			
		||||
	 * @param cache the cache to store
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public <T> void put(Class<T> key, Cache<T> cache) { map.put(key, cache); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Returns a cache mapped by a class.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @param <T> the type accepted by the cache
 | 
			
		||||
	 * @param key the class that maps to the cache
 | 
			
		||||
	 * @return the cache
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public <T> Cache<T> get(Class<T> key) { return (Cache<T>) map.get(key); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Returns a cache mapped by a class or any of its subclasses.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @param <T> the type accepted by the cache
 | 
			
		||||
	 * @param key the class that maps to the cache
 | 
			
		||||
	 * @return the cache
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public <T> Cache<? super T> getApplicable(Class<T> key) {
 | 
			
		||||
		Cache<? super T> cache = get(key);
 | 
			
		||||
		if (cache == null)
 | 
			
		||||
			for (var e : map.entrySet())
 | 
			
		||||
				if (e.getKey().isAssignableFrom(key))
 | 
			
		||||
					cache = (Cache<? super T>) e.getValue();
 | 
			
		||||
		return cache;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return the map in which the caches are stored
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public Map<Class<?>, Cache<?>> getMap() { return map; }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										153
									
								
								client/src/main/java/envoy/client/data/Chat.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								client/src/main/java/envoy/client/data/Chat.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,153 @@
 | 
			
		||||
package envoy.client.data;
 | 
			
		||||
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.io.Serializable;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
 | 
			
		||||
import envoy.client.net.WriteProxy;
 | 
			
		||||
import envoy.data.*;
 | 
			
		||||
import envoy.data.Message.MessageStatus;
 | 
			
		||||
import envoy.event.MessageStatusChange;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Represents a chat between two {@link User}s
 | 
			
		||||
 * as a list of {@link Message} objects.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>Chat.java</strong><br>
 | 
			
		||||
 * Created: <strong>19 Oct 2019</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Maximilian Käfer
 | 
			
		||||
 * @author Leon Hofmeister
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.1-alpha
 | 
			
		||||
 */
 | 
			
		||||
public class Chat implements Serializable {
 | 
			
		||||
 | 
			
		||||
	protected final Contact			recipient;
 | 
			
		||||
	protected final List<Message>	messages	= new ArrayList<>();
 | 
			
		||||
 | 
			
		||||
	protected int unreadAmount;
 | 
			
		||||
 | 
			
		||||
	private static final long serialVersionUID = 1L;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Provides the list of messages that the recipient receives.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * Saves the Messages in the corresponding chat at that Point.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param recipient the user who receives the messages
 | 
			
		||||
	 * @since Envoy Client v0.1-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public Chat(Contact recipient) {
 | 
			
		||||
		this.recipient	= recipient;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public String toString() { return String.format("Chat[recipient=%s,messages=%d]", recipient, messages.size()); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Generates a hash code based on the recipient.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	@Override
 | 
			
		||||
	public int hashCode() { return Objects.hash(recipient); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Tests equality to another object based on the recipient.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	@Override
 | 
			
		||||
	public boolean equals(Object obj) {
 | 
			
		||||
		if (this == obj) return true;
 | 
			
		||||
		if (!(obj instanceof Chat)) return false;
 | 
			
		||||
		Chat other = (Chat) obj;
 | 
			
		||||
		return Objects.equals(recipient, other.recipient);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Sets the status of all chat messages received from the recipient to
 | 
			
		||||
	 * {@code READ} starting from the bottom and stopping once a read message is
 | 
			
		||||
	 * found.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param writeProxy the write proxy instance used to notify the server about
 | 
			
		||||
	 *                   the message status changes
 | 
			
		||||
	 * @throws IOException if a {@link MessageStatusChange} could not be
 | 
			
		||||
	 *                     delivered to the server
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public void read(WriteProxy writeProxy) throws IOException {
 | 
			
		||||
		for (int i = messages.size() - 1; i >= 0; --i) {
 | 
			
		||||
			final Message m = messages.get(i);
 | 
			
		||||
			if (m.getSenderID() == recipient.getID()) if (m.getStatus() == MessageStatus.READ) break;
 | 
			
		||||
			else {
 | 
			
		||||
				m.setStatus(MessageStatus.READ);
 | 
			
		||||
				writeProxy.writeMessageStatusChange(new MessageStatusChange(m));
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		unreadAmount = 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return {@code true} if the newest message received in the chat doesn't have
 | 
			
		||||
	 *         the status {@code READ}
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public boolean isUnread() { return !messages.isEmpty() && messages.get(messages.size() - 1).getStatus() != MessageStatus.READ; }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Inserts a message at the correct place according to its creation date.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @param message the message to insert
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public void insert(Message message) {
 | 
			
		||||
		for (int i = messages.size() - 1; i >= 0; --i)
 | 
			
		||||
			if (message.getCreationDate().isAfter(messages.get(i).getCreationDate())) {
 | 
			
		||||
				messages.add(i + 1, message);
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
		messages.add(0, message);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Increments the amount of unread messages.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public void incrementUnreadAmount() { unreadAmount++; }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return the amount of unread mesages in this chat
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public int getUnreadAmount() { return unreadAmount; }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return all messages in the current chat
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public List<Message> getMessages() { return messages; }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return the recipient of a message
 | 
			
		||||
	 * @since Envoy Client v0.1-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public Contact getRecipient() { return recipient; }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return whether this {@link Chat} points at a {@link User}
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public boolean isUserChat() { return recipient instanceof User; }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return whether this {@link Chat} points at a {@link Group}
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public boolean isGroupChat() { return recipient instanceof Group; }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										115
									
								
								client/src/main/java/envoy/client/data/ClientConfig.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								client/src/main/java/envoy/client/data/ClientConfig.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,115 @@
 | 
			
		||||
package envoy.client.data;
 | 
			
		||||
 | 
			
		||||
import static java.util.function.Function.identity;
 | 
			
		||||
 | 
			
		||||
import java.io.File;
 | 
			
		||||
import java.util.logging.Level;
 | 
			
		||||
 | 
			
		||||
import envoy.client.ui.Startup;
 | 
			
		||||
import envoy.data.Config;
 | 
			
		||||
import envoy.data.ConfigItem;
 | 
			
		||||
import envoy.data.LoginCredentials;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Implements a configuration specific to the Envoy Client with default values
 | 
			
		||||
 * and convenience methods.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>ClientConfig.java</strong><br>
 | 
			
		||||
 * Created: <strong>01.03.2020</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
public class ClientConfig extends Config {
 | 
			
		||||
 | 
			
		||||
	private static ClientConfig config;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return the singleton instance of the client config
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public static ClientConfig getInstance() {
 | 
			
		||||
		if (config == null) config = new ClientConfig();
 | 
			
		||||
		return config;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private ClientConfig() {
 | 
			
		||||
		items.put("server", new ConfigItem<>("server", "s", identity(), null, true));
 | 
			
		||||
		items.put("port", new ConfigItem<>("port", "p", Integer::parseInt, null, true));
 | 
			
		||||
		items.put("localDB", new ConfigItem<>("localDB", "db", File::new, new File("localDB"), true));
 | 
			
		||||
		items.put("ignoreLocalDB", new ConfigItem<>("ignoreLocalDB", "nodb", Boolean::parseBoolean, false, false));
 | 
			
		||||
		items.put("homeDirectory", new ConfigItem<>("homeDirectory", "h", File::new, new File(System.getProperty("user.home"), ".envoy"), true));
 | 
			
		||||
		items.put("fileLevelBarrier", new ConfigItem<>("fileLevelBarrier", "fb", Level::parse, Level.CONFIG, true));
 | 
			
		||||
		items.put("consoleLevelBarrier", new ConfigItem<>("consoleLevelBarrier", "cb", Level::parse, Level.FINEST, true));
 | 
			
		||||
		items.put("user", new ConfigItem<>("user", "u", identity()));
 | 
			
		||||
		items.put("password", new ConfigItem<>("password", "pw", identity()));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return the host name of the Envoy server
 | 
			
		||||
	 * @since Envoy Client v0.1-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public String getServer() { return (String) items.get("server").get(); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return the port at which the Envoy server is located on the host
 | 
			
		||||
	 * @since Envoy Client v0.1-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public Integer getPort() { return (Integer) items.get("port").get(); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return the local database specific to the client user
 | 
			
		||||
	 * @since Envoy Client v0.1-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public File getLocalDB() { return (File) items.get("localDB").get(); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return {@code true} if the local database is to be ignored
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public Boolean isIgnoreLocalDB() { return (Boolean) items.get("ignoreLocalDB").get(); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return the directory in which all local files are saves
 | 
			
		||||
	 * @since Envoy Client v0.2-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public File getHomeDirectory() { return (File) items.get("homeDirectory").get(); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return the minimal {@link Level} to log inside the log file
 | 
			
		||||
	 * @since Envoy Client v0.2-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public Level getFileLevelBarrier() { return (Level) items.get("fileLevelBarrier").get(); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return the minimal {@link Level} to log inside the console
 | 
			
		||||
	 * @since Envoy Client v0.2-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public Level getConsoleLevelBarrier() { return (Level) items.get("consoleLevelBarrier").get(); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return the user name
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public String getUser() { return (String) items.get("user").get(); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return the password
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public String getPassword() { return (String) items.get("password").get(); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return {@code true} if user name and password are set
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public boolean hasLoginCredentials() { return getUser() != null && getPassword() != null; }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return login credentials for the specified user name and password, without
 | 
			
		||||
	 *         the registration option
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public LoginCredentials getLoginCredentials() { return new LoginCredentials(getUser(), getPassword(), false, Startup.VERSION); }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										55
									
								
								client/src/main/java/envoy/client/data/GroupChat.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								client/src/main/java/envoy/client/data/GroupChat.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,55 @@
 | 
			
		||||
package envoy.client.data;
 | 
			
		||||
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.time.LocalDateTime;
 | 
			
		||||
 | 
			
		||||
import envoy.client.net.WriteProxy;
 | 
			
		||||
import envoy.data.Contact;
 | 
			
		||||
import envoy.data.GroupMessage;
 | 
			
		||||
import envoy.data.Message.MessageStatus;
 | 
			
		||||
import envoy.data.User;
 | 
			
		||||
import envoy.event.GroupMessageStatusChange;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Represents a chat between a user and a group
 | 
			
		||||
 * as a list of messages.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>GroupChat.java</strong><br>
 | 
			
		||||
 * Created: <strong>05.07.2020</strong><br>
 | 
			
		||||
 * 
 | 
			
		||||
 * @author Maximilian Käfer
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
public class GroupChat extends Chat {
 | 
			
		||||
 | 
			
		||||
	private final User sender;
 | 
			
		||||
 | 
			
		||||
	private static final long serialVersionUID = 1L;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @param sender    the user sending the messages
 | 
			
		||||
	 * @param recipient the group whose members receive the messages
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public GroupChat(User sender, Contact recipient) {
 | 
			
		||||
		super(recipient);
 | 
			
		||||
		this.sender = sender;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public void read(WriteProxy writeProxy) throws IOException {
 | 
			
		||||
		for (int i = messages.size() - 1; i >= 0; --i) {
 | 
			
		||||
			final GroupMessage gmsg = (GroupMessage) messages.get(i);
 | 
			
		||||
			if (gmsg.getSenderID() != sender.getID()) {
 | 
			
		||||
				if (gmsg.getMemberStatuses().get(sender.getID()) == MessageStatus.READ) break;
 | 
			
		||||
				else {
 | 
			
		||||
					gmsg.getMemberStatuses().replace(sender.getID(), MessageStatus.READ);
 | 
			
		||||
					writeProxy
 | 
			
		||||
						.writeMessageStatusChange(new GroupMessageStatusChange(gmsg.getID(), MessageStatus.READ, LocalDateTime.now(), sender.getID()));
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		unreadAmount = 0;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										205
									
								
								client/src/main/java/envoy/client/data/LocalDB.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										205
									
								
								client/src/main/java/envoy/client/data/LocalDB.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,205 @@
 | 
			
		||||
package envoy.client.data;
 | 
			
		||||
 | 
			
		||||
import java.util.*;
 | 
			
		||||
 | 
			
		||||
import envoy.data.*;
 | 
			
		||||
import envoy.event.GroupResize;
 | 
			
		||||
import envoy.event.MessageStatusChange;
 | 
			
		||||
import envoy.event.NameChange;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Stores information about the current {@link User} and their {@link Chat}s.
 | 
			
		||||
 * For message ID generation a {@link IDGenerator} is stored as well.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>LocalDB.java</strong><br>
 | 
			
		||||
 * Created: <strong>3 Feb 2020</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.3-alpha
 | 
			
		||||
 */
 | 
			
		||||
public abstract class LocalDB {
 | 
			
		||||
 | 
			
		||||
	protected User					user;
 | 
			
		||||
	protected Map<String, Contact>	users		= new HashMap<>();
 | 
			
		||||
	protected List<Chat>			chats		= new ArrayList<>();
 | 
			
		||||
	protected IDGenerator			idGenerator;
 | 
			
		||||
	protected CacheMap				cacheMap	= new CacheMap();
 | 
			
		||||
 | 
			
		||||
	{
 | 
			
		||||
		cacheMap.put(Message.class, new Cache<>());
 | 
			
		||||
		cacheMap.put(MessageStatusChange.class, new Cache<>());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Initializes a storage space for a user-specific list of chats.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public void initializeUserStorage() {}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Stores all users. If the client user is specified, their chats will be stored
 | 
			
		||||
	 * as well. The message id generator will also be saved if present.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @throws Exception if the saving process failed
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public void save() throws Exception {}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Loads all user data.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @throws Exception if the loading process failed
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public void loadUsers() throws Exception {}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Loads all data of the client user.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @throws Exception if the loading process failed
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public void loadUserData() throws Exception {}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Loads the ID generator. Any exception thrown during this process is ignored.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public void loadIDGenerator() {}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Synchronizes the contact list of the client user with the chat and user
 | 
			
		||||
	 * storage.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public void synchronize() {
 | 
			
		||||
		user.getContacts().stream().filter(u -> u instanceof User && !users.containsKey(u.getName())).forEach(u -> users.put(u.getName(), u));
 | 
			
		||||
		users.put(user.getName(), user);
 | 
			
		||||
 | 
			
		||||
		// Synchronize user status data
 | 
			
		||||
		for (Contact contact : users.values())
 | 
			
		||||
			if (contact instanceof User)
 | 
			
		||||
				getChat(contact.getID()).ifPresent(chat -> { ((User) chat.getRecipient()).setStatus(((User) contact).getStatus()); });
 | 
			
		||||
 | 
			
		||||
		// Create missing chats
 | 
			
		||||
		user.getContacts()
 | 
			
		||||
			.stream()
 | 
			
		||||
			.filter(c -> !c.equals(user) && getChat(c.getID()).isEmpty())
 | 
			
		||||
			.map(c -> c instanceof User ? new Chat(c) : new GroupChat(user, c))
 | 
			
		||||
			.forEach(chats::add);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return a {@code Map<String, User>} of all users stored locally with their
 | 
			
		||||
	 *         user names as keys
 | 
			
		||||
	 * @since Envoy Client v0.2-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public Map<String, Contact> getUsers() { return users; }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return all saved {@link Chat} objects that list the client user as the
 | 
			
		||||
	 *         sender
 | 
			
		||||
	 * @since Envoy Client v0.1-alpha
 | 
			
		||||
	 **/
 | 
			
		||||
	public List<Chat> getChats() { return chats; }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @param chats the chats to set
 | 
			
		||||
	 */
 | 
			
		||||
	public void setChats(List<Chat> chats) { this.chats = chats; }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return the {@link User} who initialized the local database
 | 
			
		||||
	 * @since Envoy Client v0.2-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public User getUser() { return user; }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @param user the user to set
 | 
			
		||||
	 * @since Envoy Client v0.2-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public void setUser(User user) { this.user = user; }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return the message ID generator
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public IDGenerator getIDGenerator() { return idGenerator; }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @param idGenerator the message ID generator to set
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public void setIDGenerator(IDGenerator idGenerator) { this.idGenerator = idGenerator; }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return {@code true} if an {@link IDGenerator} is present
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public boolean hasIDGenerator() { return idGenerator != null; }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return the cache map for messages and message status changes
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public CacheMap getCacheMap() { return cacheMap; }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Searches for a message by ID.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param id the ID of the message to search for
 | 
			
		||||
	 * @return an optional containing the message
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public Optional<Message> getMessage(long id) {
 | 
			
		||||
		return chats.stream().map(Chat::getMessages).flatMap(List::stream).filter(m -> m.getID() == id).findAny();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Searches for a chat by recipient ID.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @param recipientID the ID of the chat's recipient
 | 
			
		||||
	 * @return an optional containing the chat
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public Optional<Chat> getChat(long recipientID) { return chats.stream().filter(c -> c.getRecipient().getID() == recipientID).findAny(); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Performs a contact name change if the corresponding contact is present.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param event the {@link NameChange} to process
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public void replaceContactName(NameChange event) {
 | 
			
		||||
		chats.stream().map(Chat::getRecipient).filter(c -> c.getID() == event.getID()).findAny().ifPresent(c -> c.setName(event.get()));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Performs a group resize operation if the corresponding group is present.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param event the {@link GroupResize} to process
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public void updateGroup(GroupResize event) {
 | 
			
		||||
		chats.stream()
 | 
			
		||||
			.map(Chat::getRecipient)
 | 
			
		||||
			.filter(Group.class::isInstance)
 | 
			
		||||
			.filter(g -> g.getID() == event.getGroupID() && g.getID() != user.getID())
 | 
			
		||||
			.map(Group.class::cast)
 | 
			
		||||
			.findAny()
 | 
			
		||||
			.ifPresent(group -> {
 | 
			
		||||
				switch (event.getOperation()) {
 | 
			
		||||
					case ADD:
 | 
			
		||||
						group.getContacts().add(event.get());
 | 
			
		||||
						break;
 | 
			
		||||
					case REMOVE:
 | 
			
		||||
						group.getContacts().remove(event.get());
 | 
			
		||||
						break;
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,88 @@
 | 
			
		||||
package envoy.client.data;
 | 
			
		||||
 | 
			
		||||
import java.io.*;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
 | 
			
		||||
import envoy.data.IDGenerator;
 | 
			
		||||
import envoy.util.SerializationUtils;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Implements a {@link LocalDB} in a way that stores all information inside a
 | 
			
		||||
 * folder on the local file system.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>PersistentLocalDB.java</strong><br>
 | 
			
		||||
 * Created: <strong>27.10.2019</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @author Maximilian Käfer
 | 
			
		||||
 * @since Envoy Client v0.1-alpha
 | 
			
		||||
 */
 | 
			
		||||
public final class PersistentLocalDB extends LocalDB {
 | 
			
		||||
 | 
			
		||||
	private File dbDir, userFile, idGeneratorFile, usersFile;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Constructs an empty local database. To serialize any user-specific data to
 | 
			
		||||
	 * the file system, call {@link PersistentLocalDB#initializeUserStorage()} first
 | 
			
		||||
	 * and then {@link PersistentLocalDB#save()}.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param dbDir the directory in which to persist data
 | 
			
		||||
	 * @throws IOException if {@code dbDir} is a file (and not a directory)
 | 
			
		||||
	 * @since Envoy Client v0.1-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public PersistentLocalDB(File dbDir) throws IOException {
 | 
			
		||||
		this.dbDir = dbDir;
 | 
			
		||||
 | 
			
		||||
		// Test if the database directory is actually a directory
 | 
			
		||||
		if (dbDir.exists() && !dbDir.isDirectory())
 | 
			
		||||
			throw new IOException(String.format("LocalDBDir '%s' is not a directory!", dbDir.getAbsolutePath()));
 | 
			
		||||
 | 
			
		||||
		// Initialize global files
 | 
			
		||||
		idGeneratorFile	= new File(dbDir, "id_gen.db");
 | 
			
		||||
		usersFile		= new File(dbDir, "users.db");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Creates a database file for a user-specific list of chats.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @throws IllegalStateException if the client user is not specified
 | 
			
		||||
	 * @since Envoy Client v0.1-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	@Override
 | 
			
		||||
	public void initializeUserStorage() {
 | 
			
		||||
		if (user == null) throw new IllegalStateException("Client user is null, cannot initialize user storage");
 | 
			
		||||
		userFile = new File(dbDir, user.getID() + ".db");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public void save() throws IOException {
 | 
			
		||||
		// Save users
 | 
			
		||||
		SerializationUtils.write(usersFile, users);
 | 
			
		||||
 | 
			
		||||
		// Save user data
 | 
			
		||||
		if (user != null) SerializationUtils.write(userFile, chats, cacheMap);
 | 
			
		||||
 | 
			
		||||
		// Save id generator
 | 
			
		||||
		if (hasIDGenerator()) SerializationUtils.write(idGeneratorFile, idGenerator);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public void loadUsers() throws ClassNotFoundException, IOException { users = SerializationUtils.read(usersFile, HashMap.class); }
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public void loadUserData() throws ClassNotFoundException, IOException {
 | 
			
		||||
		try (var in = new ObjectInputStream(new FileInputStream(userFile))) {
 | 
			
		||||
			chats		= (ArrayList<Chat>) in.readObject();
 | 
			
		||||
			cacheMap	= (CacheMap) in.readObject();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public void loadIDGenerator() {
 | 
			
		||||
		try {
 | 
			
		||||
			idGenerator = SerializationUtils.read(idGeneratorFile, IDGenerator.class);
 | 
			
		||||
		} catch (ClassNotFoundException | IOException e) {}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										146
									
								
								client/src/main/java/envoy/client/data/Settings.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								client/src/main/java/envoy/client/data/Settings.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,146 @@
 | 
			
		||||
package envoy.client.data;
 | 
			
		||||
 | 
			
		||||
import java.io.File;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.prefs.Preferences;
 | 
			
		||||
 | 
			
		||||
import envoy.util.SerializationUtils;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Manages all application settings, which are different objects that can be
 | 
			
		||||
 * changed during runtime and serialized them by using either the file system or
 | 
			
		||||
 * the {@link Preferences} API.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>Settings.java</strong><br>
 | 
			
		||||
 * Created: <strong>11 Nov 2019</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Leon Hofmeister
 | 
			
		||||
 * @author Maximilian Käfer
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.2-alpha
 | 
			
		||||
 */
 | 
			
		||||
public class Settings {
 | 
			
		||||
 | 
			
		||||
	// Actual settings accessible by the rest of the application
 | 
			
		||||
	private Map<String, SettingsItem<?>> items;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Settings are stored in this file.
 | 
			
		||||
	 */
 | 
			
		||||
	private static final File settingsFile = new File(ClientConfig.getInstance().getHomeDirectory(), "settings.ser");
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Singleton instance of this class.
 | 
			
		||||
	 */
 | 
			
		||||
	private static Settings settings = new Settings();
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * The way to instantiate the settings. Is set to private to deny other
 | 
			
		||||
	 * instances of that object.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.2-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	private Settings() {
 | 
			
		||||
		// Load settings from settings file
 | 
			
		||||
		try {
 | 
			
		||||
			items = SerializationUtils.read(settingsFile, HashMap.class);
 | 
			
		||||
		} catch (ClassNotFoundException | IOException e) {
 | 
			
		||||
			items = new HashMap<>();
 | 
			
		||||
		}
 | 
			
		||||
		supplementDefaults();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * This method is used to ensure that there is only one instance of Settings.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @return the instance of Settings
 | 
			
		||||
	 * @since Envoy Client v0.2-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public static Settings getInstance() { return settings; }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Updates the preferences when the save button is clicked.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @throws IOException if an error occurs while saving the themes
 | 
			
		||||
	 * @since Envoy Client v0.2-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public void save() throws IOException {
 | 
			
		||||
 | 
			
		||||
		// Save settings to settings file
 | 
			
		||||
		SerializationUtils.write(settingsFile, items);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void supplementDefaults() {
 | 
			
		||||
		items.putIfAbsent("enterToSend", new SettingsItem<>(true, "Enter to send", "Sends a message by pressing the enter key."));
 | 
			
		||||
		items.putIfAbsent("onCloseMode", new SettingsItem<>(true, "Hide on close", "Hides the chat window when it is closed."));
 | 
			
		||||
		items.putIfAbsent("currentTheme", new SettingsItem<>("dark", "Current Theme Name", "The name of the currently selected theme."));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return the name of the currently active theme
 | 
			
		||||
	 * @since Envoy Client v0.2-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public String getCurrentTheme() { return (String) items.get("currentTheme").get(); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Sets the name of the current theme.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param themeName the name to set
 | 
			
		||||
	 * @since Envoy Client v0.2-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public void setCurrentTheme(String themeName) { ((SettingsItem<String>) items.get("currentTheme")).set(themeName); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return true if the currently used theme is one of the default themes
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public boolean isUsingDefaultTheme() {
 | 
			
		||||
		final var theme = getCurrentTheme();
 | 
			
		||||
		return theme.equals("dark") || theme.equals("light");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return {@code true}, if pressing the {@code Enter} key suffices to send a
 | 
			
		||||
	 *         message. Otherwise it has to be pressed in conjunction with the
 | 
			
		||||
	 *         {@code Control} key.
 | 
			
		||||
	 * @since Envoy Client v0.2-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public Boolean isEnterToSend() { return (Boolean) items.get("enterToSend").get(); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Changes the keystrokes performed by the user to send a message.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param enterToSend If set to {@code true} a message can be sent by pressing
 | 
			
		||||
	 *                    the {@code Enter} key. Otherwise it has to be pressed in
 | 
			
		||||
	 *                    conjunction with the {@code Control} key.
 | 
			
		||||
	 * @since Envoy Client v0.2-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public void setEnterToSend(boolean enterToSend) { ((SettingsItem<Boolean>) items.get("enterToSend")).set(enterToSend); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return the current on close mode.
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public Boolean getCurrentOnCloseMode() { return (Boolean) items.get("onCloseMode").get(); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Sets the current on close mode.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param currentOnCloseMode the on close mode that should be set.
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public void setCurrentOnCloseMode(boolean currentOnCloseMode) { ((SettingsItem<Boolean>) items.get("onCloseMode")).set(currentOnCloseMode); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return the items
 | 
			
		||||
	 */
 | 
			
		||||
	public Map<String, SettingsItem<?>> getItems() { return items; }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @param items the items to set
 | 
			
		||||
	 */
 | 
			
		||||
	public void setItems(Map<String, SettingsItem<?>> items) { this.items = items; }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										99
									
								
								client/src/main/java/envoy/client/data/SettingsItem.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								client/src/main/java/envoy/client/data/SettingsItem.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,99 @@
 | 
			
		||||
package envoy.client.data;
 | 
			
		||||
 | 
			
		||||
import java.io.Serializable;
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
 | 
			
		||||
import javax.swing.JComponent;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Encapsulates a persistent value that is directly or indirectly mutable by the
 | 
			
		||||
 * user.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>SettingsItem.java</strong><br>
 | 
			
		||||
 * Created: <strong>23.12.2019</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @param <T> the type of this {@link SettingsItem}'s value
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.3-alpha
 | 
			
		||||
 */
 | 
			
		||||
public class SettingsItem<T> implements Serializable {
 | 
			
		||||
 | 
			
		||||
	private T		value;
 | 
			
		||||
	private String	userFriendlyName, description;
 | 
			
		||||
 | 
			
		||||
	private transient Consumer<T> changeHandler;
 | 
			
		||||
 | 
			
		||||
	private static final long serialVersionUID = 1L;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Initializes a {@link SettingsItem}. The default value's class will be mapped
 | 
			
		||||
	 * to a {@link JComponent} that can be used to display this {@link SettingsItem}
 | 
			
		||||
	 * to the user.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param value            the default value
 | 
			
		||||
	 * @param userFriendlyName the user friendly name (short)
 | 
			
		||||
	 * @param description      the description (long)
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public SettingsItem(T value, String userFriendlyName, String description) {
 | 
			
		||||
		this.value				= value;
 | 
			
		||||
		this.userFriendlyName	= userFriendlyName;
 | 
			
		||||
		this.description		= description;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return the value
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public T get() { return value; }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Changes the value of this {@link SettingsItem}. If a {@code ChangeHandler} if
 | 
			
		||||
	 * defined, it will be invoked with this value.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param value the value to set
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public void set(T value) {
 | 
			
		||||
		if (changeHandler != null && value != this.value) changeHandler.accept(value);
 | 
			
		||||
		this.value = value;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return the userFriendlyName
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public String getUserFriendlyName() { return userFriendlyName; }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @param userFriendlyName the userFriendlyName to set
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public void setUserFriendlyName(String userFriendlyName) { this.userFriendlyName = userFriendlyName; }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return the description
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public String getDescription() { return description; }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @param description the description to set
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public void setDescription(String description) { this.description = description; }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Sets a {@code ChangeHandler} for this {@link SettingsItem}. It will be
 | 
			
		||||
	 * invoked with the current value once during the registration and every time
 | 
			
		||||
	 * when the value changes.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param changeHandler the changeHandler to set
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public void setChangeHandler(Consumer<T> changeHandler) {
 | 
			
		||||
		this.changeHandler = changeHandler;
 | 
			
		||||
		changeHandler.accept(value);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								client/src/main/java/envoy/client/data/TransientLocalDB.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								client/src/main/java/envoy/client/data/TransientLocalDB.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
package envoy.client.data;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Implements a {@link LocalDB} in a way that does not persist any information
 | 
			
		||||
 * after application shutdown.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>TransientLocalDB.java</strong><br>
 | 
			
		||||
 * Created: <strong>3 Feb 2020</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.3-alpha
 | 
			
		||||
 */
 | 
			
		||||
public final class TransientLocalDB extends LocalDB {
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,64 @@
 | 
			
		||||
package envoy.client.data.audio;
 | 
			
		||||
 | 
			
		||||
import javax.sound.sampled.*;
 | 
			
		||||
 | 
			
		||||
import envoy.exception.EnvoyException;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Plays back audio from a byte array.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>AudioPlayer.java</strong><br>
 | 
			
		||||
 * Created: <strong>05.07.2020</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
public final class AudioPlayer {
 | 
			
		||||
 | 
			
		||||
	private final AudioFormat	format;
 | 
			
		||||
	private final DataLine.Info	info;
 | 
			
		||||
 | 
			
		||||
	private Clip clip;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Initializes the player with the default audio format.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public AudioPlayer() { this(AudioRecorder.DEFAULT_AUDIO_FORMAT); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Initializes the player with a given audio format.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param format the audio format to use
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public AudioPlayer(AudioFormat format) {
 | 
			
		||||
		this.format	= format;
 | 
			
		||||
		info		= new DataLine.Info(Clip.class, format);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return {@code true} if audio play back is supported
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public boolean isSupported() { return AudioSystem.isLineSupported(info); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Plays back an audio clip.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param data the data of the clip
 | 
			
		||||
	 * @throws EnvoyException if the play back failed
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public void play(byte[] data) throws EnvoyException {
 | 
			
		||||
		try {
 | 
			
		||||
			clip = (Clip) AudioSystem.getLine(info);
 | 
			
		||||
			clip.open(format, data, 0, data.length);
 | 
			
		||||
			clip.start();
 | 
			
		||||
		} catch (final LineUnavailableException e) {
 | 
			
		||||
			throw new EnvoyException("Cannot play back audio", e);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										122
									
								
								client/src/main/java/envoy/client/data/audio/AudioRecorder.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								client/src/main/java/envoy/client/data/audio/AudioRecorder.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,122 @@
 | 
			
		||||
package envoy.client.data.audio;
 | 
			
		||||
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.nio.file.Files;
 | 
			
		||||
import java.nio.file.Path;
 | 
			
		||||
 | 
			
		||||
import javax.sound.sampled.*;
 | 
			
		||||
 | 
			
		||||
import envoy.exception.EnvoyException;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Records audio and exports it as a byte array.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>AudioRecorder.java</strong><br>
 | 
			
		||||
 * Created: <strong>02.07.2020</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
public final class AudioRecorder {
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * The default audio format used for recording and play back.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public static final AudioFormat DEFAULT_AUDIO_FORMAT = new AudioFormat(16000, 16, 1, true, false);
 | 
			
		||||
 | 
			
		||||
	private final AudioFormat	format;
 | 
			
		||||
	private final DataLine.Info	info;
 | 
			
		||||
 | 
			
		||||
	private TargetDataLine	line;
 | 
			
		||||
	private Path			tempFile;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Initializes the recorder with the default audio format.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public AudioRecorder() { this(DEFAULT_AUDIO_FORMAT); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Initializes the recorder with a given audio format.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param format the audio format to use
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public AudioRecorder(AudioFormat format) {
 | 
			
		||||
		this.format	= format;
 | 
			
		||||
		info		= new DataLine.Info(TargetDataLine.class, format);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return {@code true} if audio recording is supported
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public boolean isSupported() { return AudioSystem.isLineSupported(info); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return {@code true} if the recorder is active
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public boolean isRecording() { return line != null && line.isActive(); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Starts the audio recording.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @throws EnvoyException if starting the recording failed
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public void start() throws EnvoyException {
 | 
			
		||||
		try {
 | 
			
		||||
 | 
			
		||||
			// Open the line
 | 
			
		||||
			line = (TargetDataLine) AudioSystem.getLine(info);
 | 
			
		||||
			line.open(format);
 | 
			
		||||
			line.start();
 | 
			
		||||
 | 
			
		||||
			// Prepare temp file
 | 
			
		||||
			tempFile = Files.createTempFile("recording", "wav");
 | 
			
		||||
 | 
			
		||||
			// Start the recording
 | 
			
		||||
			final var ais = new AudioInputStream(line);
 | 
			
		||||
			AudioSystem.write(ais, AudioFileFormat.Type.WAVE, tempFile.toFile());
 | 
			
		||||
		} catch (IOException | LineUnavailableException e) {
 | 
			
		||||
			throw new EnvoyException("Cannot record voice", e);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Stops the recording.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @return the finished recording
 | 
			
		||||
	 * @throws EnvoyException if finishing the recording failed
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public byte[] finish() throws EnvoyException {
 | 
			
		||||
		try {
 | 
			
		||||
			line.stop();
 | 
			
		||||
			line.close();
 | 
			
		||||
			final byte[] data = Files.readAllBytes(tempFile);
 | 
			
		||||
			Files.delete(tempFile);
 | 
			
		||||
			return data;
 | 
			
		||||
		} catch (final IOException e) {
 | 
			
		||||
			throw new EnvoyException("Cannot save voice recording", e);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Cancels the active recording.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public void cancel() {
 | 
			
		||||
		line.stop();
 | 
			
		||||
		line.close();
 | 
			
		||||
		try {
 | 
			
		||||
			Files.deleteIfExists(tempFile);
 | 
			
		||||
		} catch (IOException e) {}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,11 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Contains classes related to recording and playing back audio clips.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>package-info.java</strong><br>
 | 
			
		||||
 * Created: <strong>05.07.2020</strong><br>
 | 
			
		||||
 * 
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
package envoy.client.data.audio;
 | 
			
		||||
							
								
								
									
										9
									
								
								client/src/main/java/envoy/client/data/package-info.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								client/src/main/java/envoy/client/data/package-info.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
/**
 | 
			
		||||
 * This package contains all data classes and classes related to persistence.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @author Leon Hofmeister
 | 
			
		||||
 * @author Maximilian Käfer
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
package envoy.client.data;
 | 
			
		||||
@@ -0,0 +1,22 @@
 | 
			
		||||
package envoy.client.event;
 | 
			
		||||
 | 
			
		||||
import envoy.data.Message;
 | 
			
		||||
import envoy.event.Event;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>MessageCreationEvent.java</strong><br>
 | 
			
		||||
 * Created: <strong>4 Dec 2019</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.2-alpha
 | 
			
		||||
 */
 | 
			
		||||
public class MessageCreationEvent extends Event<Message> {
 | 
			
		||||
 | 
			
		||||
	private static final long serialVersionUID = 0L;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @param message the {@link Message} that has been created
 | 
			
		||||
	 */
 | 
			
		||||
	public MessageCreationEvent(Message message) { super(message); }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,22 @@
 | 
			
		||||
package envoy.client.event;
 | 
			
		||||
 | 
			
		||||
import envoy.data.Message;
 | 
			
		||||
import envoy.event.Event;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>MessageModificationEvent.java</strong><br>
 | 
			
		||||
 * Created: <strong>4 Dec 2019</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.2-alpha
 | 
			
		||||
 */
 | 
			
		||||
public class MessageModificationEvent extends Event<Message> {
 | 
			
		||||
 | 
			
		||||
	private static final long serialVersionUID = 0L;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @param message the {@link Message} that has been modified
 | 
			
		||||
	 */
 | 
			
		||||
	public MessageModificationEvent(Message message) { super(message); }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								client/src/main/java/envoy/client/event/SendEvent.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								client/src/main/java/envoy/client/event/SendEvent.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
package envoy.client.event;
 | 
			
		||||
 | 
			
		||||
import envoy.event.Event;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>SendEvent.java</strong><br>
 | 
			
		||||
 * Created: <strong>11.02.2020</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author: Maximilian Käfer
 | 
			
		||||
 * @since Envoy Client v0.3-alpha
 | 
			
		||||
 */
 | 
			
		||||
public class SendEvent extends Event<Event<?>> {
 | 
			
		||||
 | 
			
		||||
	private static final long serialVersionUID = 0L;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @param value the event to send to the server
 | 
			
		||||
	 */
 | 
			
		||||
	public SendEvent(Event<?> value) { super(value); }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,25 @@
 | 
			
		||||
package envoy.client.event;
 | 
			
		||||
 | 
			
		||||
import envoy.event.Event;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>ThemeChangeEvent.java</strong><br>
 | 
			
		||||
 * Created: <strong>15 Dec 2019</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.2-alpha
 | 
			
		||||
 */
 | 
			
		||||
public class ThemeChangeEvent extends Event<String> {
 | 
			
		||||
 | 
			
		||||
	private static final long serialVersionUID = 0L;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Initializes a {@link ThemeChangeEvent} conveying information about the change
 | 
			
		||||
	 * of the theme currently in use.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param theme the name of the new theme
 | 
			
		||||
	 * @since Envoy Client v0.2-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public ThemeChangeEvent(String theme) { super(theme); }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,9 @@
 | 
			
		||||
/**
 | 
			
		||||
 * This package contains all client-sided events.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @author Leon Hofmeister
 | 
			
		||||
 * @author Maximilian Käfer
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
package envoy.client.event;
 | 
			
		||||
							
								
								
									
										241
									
								
								client/src/main/java/envoy/client/net/Client.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										241
									
								
								client/src/main/java/envoy/client/net/Client.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,241 @@
 | 
			
		||||
package envoy.client.net;
 | 
			
		||||
 | 
			
		||||
import java.io.Closeable;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.net.Socket;
 | 
			
		||||
import java.util.concurrent.TimeoutException;
 | 
			
		||||
import java.util.logging.Level;
 | 
			
		||||
import java.util.logging.Logger;
 | 
			
		||||
 | 
			
		||||
import envoy.client.data.*;
 | 
			
		||||
import envoy.client.event.SendEvent;
 | 
			
		||||
import envoy.data.*;
 | 
			
		||||
import envoy.event.*;
 | 
			
		||||
import envoy.event.contact.ContactOperation;
 | 
			
		||||
import envoy.event.contact.ContactSearchResult;
 | 
			
		||||
import envoy.util.EnvoyLog;
 | 
			
		||||
import envoy.util.SerializationUtils;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Establishes a connection to the server, performs a handshake and delivers
 | 
			
		||||
 * certain objects to the server.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>Client.java</strong><br>
 | 
			
		||||
 * Created: <strong>28 Sep 2019</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @author Maximilian Käfer
 | 
			
		||||
 * @author Leon Hofmeister
 | 
			
		||||
 * @since Envoy Client v0.1-alpha
 | 
			
		||||
 */
 | 
			
		||||
public class Client implements Closeable {
 | 
			
		||||
 | 
			
		||||
	// Connection handling
 | 
			
		||||
	private Socket		socket;
 | 
			
		||||
	private Receiver	receiver;
 | 
			
		||||
	private boolean		online;
 | 
			
		||||
 | 
			
		||||
	// Asynchronously initialized during handshake
 | 
			
		||||
	private volatile User		sender;
 | 
			
		||||
	private volatile boolean	rejected;
 | 
			
		||||
 | 
			
		||||
	// Configuration, logging and event management
 | 
			
		||||
	private static final ClientConfig	config		= ClientConfig.getInstance();
 | 
			
		||||
	private static final Logger			logger		= EnvoyLog.getLogger(Client.class);
 | 
			
		||||
	private static final EventBus		eventBus	= EventBus.getInstance();
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Enters the online mode by acquiring a user ID from the server. As a
 | 
			
		||||
	 * connection has to be established and a handshake has to be made, this method
 | 
			
		||||
	 * will block for up to 5 seconds. If the handshake does exceed this time limit,
 | 
			
		||||
	 * an exception is thrown.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param credentials the login credentials of the user
 | 
			
		||||
	 * @param cacheMap    the map of all caches needed
 | 
			
		||||
	 * @throws TimeoutException     if the server could not be reached
 | 
			
		||||
	 * @throws IOException          if the login credentials could not be written
 | 
			
		||||
	 * @throws InterruptedException if the current thread is interrupted while
 | 
			
		||||
	 *                              waiting for the handshake response
 | 
			
		||||
	 */
 | 
			
		||||
	public void performHandshake(LoginCredentials credentials, CacheMap cacheMap) throws TimeoutException, IOException, InterruptedException {
 | 
			
		||||
		if (online) throw new IllegalStateException("Handshake has already been performed successfully");
 | 
			
		||||
 | 
			
		||||
		// Establish TCP connection
 | 
			
		||||
		logger.log(Level.FINER, String.format("Attempting connection to server %s:%d...", config.getServer(), config.getPort()));
 | 
			
		||||
		socket = new Socket(config.getServer(), config.getPort());
 | 
			
		||||
		logger.log(Level.FINE, "Successfully established TCP connection to server");
 | 
			
		||||
 | 
			
		||||
		// Create object receiver
 | 
			
		||||
		receiver = new Receiver(socket.getInputStream());
 | 
			
		||||
 | 
			
		||||
		// Register user creation processor, contact list processor and message cache
 | 
			
		||||
		receiver.registerProcessor(User.class, sender -> this.sender = sender);
 | 
			
		||||
		receiver.registerProcessors(cacheMap.getMap());
 | 
			
		||||
		receiver.registerProcessor(HandshakeRejection.class, evt -> { rejected = true; eventBus.dispatch(evt); });
 | 
			
		||||
 | 
			
		||||
		rejected = false;
 | 
			
		||||
 | 
			
		||||
		// Start receiver
 | 
			
		||||
		receiver.start();
 | 
			
		||||
 | 
			
		||||
		// Write login credentials
 | 
			
		||||
		SerializationUtils.writeBytesWithLength(credentials, socket.getOutputStream());
 | 
			
		||||
 | 
			
		||||
		// Wait for a maximum of five seconds to acquire the sender object
 | 
			
		||||
		final long start = System.currentTimeMillis();
 | 
			
		||||
		while (sender == null) {
 | 
			
		||||
 | 
			
		||||
			// Quit immediately after handshake rejection
 | 
			
		||||
			// This method can then be called again
 | 
			
		||||
			if (rejected) {
 | 
			
		||||
				socket.close();
 | 
			
		||||
				receiver.removeAllProcessors();
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (System.currentTimeMillis() - start > 5000) throw new TimeoutException("Did not log in after 5 seconds");
 | 
			
		||||
			Thread.sleep(500);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		online = true;
 | 
			
		||||
 | 
			
		||||
		logger.log(Level.INFO, "Handshake completed.");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Initializes the {@link Receiver} used to process data sent from the server to
 | 
			
		||||
	 * this client.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param localDB  the local database used to persist the current
 | 
			
		||||
	 *                 {@link IDGenerator}
 | 
			
		||||
	 * @param cacheMap the map of all caches needed
 | 
			
		||||
	 * @throws IOException if no {@link IDGenerator} is present and none could be
 | 
			
		||||
	 *                     requested from the server
 | 
			
		||||
	 * @since Envoy Client v0.2-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public void initReceiver(LocalDB localDB, CacheMap cacheMap) throws IOException {
 | 
			
		||||
		checkOnline();
 | 
			
		||||
 | 
			
		||||
		// Remove all processors as they are only used during the handshake
 | 
			
		||||
		receiver.removeAllProcessors();
 | 
			
		||||
 | 
			
		||||
		// Process incoming messages
 | 
			
		||||
		final var	receivedMessageProcessor			= new ReceivedMessageProcessor();
 | 
			
		||||
		final var	receivedGroupMessageProcessor		= new ReceivedGroupMessageProcessor();
 | 
			
		||||
		final var	messageStatusChangeProcessor		= new MessageStatusChangeProcessor();
 | 
			
		||||
		final var	groupMessageStatusChangeProcessor	= new GroupMessageStatusChangeProcessor();
 | 
			
		||||
 | 
			
		||||
		receiver.registerProcessor(GroupMessage.class, receivedGroupMessageProcessor);
 | 
			
		||||
		receiver.registerProcessor(Message.class, receivedMessageProcessor);
 | 
			
		||||
		receiver.registerProcessor(MessageStatusChange.class, messageStatusChangeProcessor);
 | 
			
		||||
		receiver.registerProcessor(GroupMessageStatusChange.class, groupMessageStatusChangeProcessor);
 | 
			
		||||
 | 
			
		||||
		// Relay cached messages and message status changes
 | 
			
		||||
		cacheMap.get(Message.class).setProcessor(receivedMessageProcessor);
 | 
			
		||||
		cacheMap.get(GroupMessage.class).setProcessor(receivedGroupMessageProcessor);
 | 
			
		||||
		cacheMap.get(MessageStatusChange.class).setProcessor(messageStatusChangeProcessor);
 | 
			
		||||
		cacheMap.get(GroupMessageStatusChange.class).setProcessor(groupMessageStatusChangeProcessor);
 | 
			
		||||
 | 
			
		||||
		// Process user status changes
 | 
			
		||||
		receiver.registerProcessor(UserStatusChange.class, eventBus::dispatch);
 | 
			
		||||
 | 
			
		||||
		// Process message ID generation
 | 
			
		||||
		receiver.registerProcessor(IDGenerator.class, localDB::setIDGenerator);
 | 
			
		||||
 | 
			
		||||
		// Process name changes
 | 
			
		||||
		receiver.registerProcessor(NameChange.class, evt -> { localDB.replaceContactName(evt); eventBus.dispatch(evt); });
 | 
			
		||||
 | 
			
		||||
		// Process contact searches
 | 
			
		||||
		receiver.registerProcessor(ContactSearchResult.class, eventBus::dispatch);
 | 
			
		||||
 | 
			
		||||
		// Process contact operations
 | 
			
		||||
		receiver.registerProcessor(ContactOperation.class, eventBus::dispatch);
 | 
			
		||||
 | 
			
		||||
		// Process group size changes
 | 
			
		||||
		receiver.registerProcessor(GroupResize.class, evt -> { localDB.updateGroup(evt); eventBus.dispatch(evt); });
 | 
			
		||||
 | 
			
		||||
		// Send event
 | 
			
		||||
		eventBus.register(SendEvent.class, evt -> {
 | 
			
		||||
			try {
 | 
			
		||||
				sendEvent(evt.get());
 | 
			
		||||
			} catch (final IOException e) {
 | 
			
		||||
				logger.log(Level.WARNING, "An error occurred when trying to send " + evt, e);
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		// Request a generator if none is present or the existing one is consumed
 | 
			
		||||
		if (!localDB.hasIDGenerator() || !localDB.getIDGenerator().hasNext()) requestIdGenerator();
 | 
			
		||||
 | 
			
		||||
		// Relay caches
 | 
			
		||||
		cacheMap.getMap().values().forEach(Cache::relay);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Sends a message to the server. The message's status will be incremented once
 | 
			
		||||
	 * it was delivered successfully.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param message the message to send
 | 
			
		||||
	 * @throws IOException if the message does not reach the server
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public void sendMessage(Message message) throws IOException {
 | 
			
		||||
		writeObject(message);
 | 
			
		||||
		message.nextStatus();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Sends an event to the server.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param evt the event to send
 | 
			
		||||
	 * @throws IOException if the event did not reach the server
 | 
			
		||||
	 */
 | 
			
		||||
	public void sendEvent(Event<?> evt) throws IOException { writeObject(evt); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Requests a new {@link IDGenerator} from the server.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @throws IOException if the request does not reach the server
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public void requestIdGenerator() throws IOException {
 | 
			
		||||
		logger.log(Level.INFO, "Requesting new id generator...");
 | 
			
		||||
		writeObject(new IDGeneratorRequest());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public void close() throws IOException { if (online) socket.close(); }
 | 
			
		||||
 | 
			
		||||
	private void writeObject(Object obj) throws IOException {
 | 
			
		||||
		checkOnline();
 | 
			
		||||
		logger.log(Level.FINE, "Sending " + obj);
 | 
			
		||||
		SerializationUtils.writeBytesWithLength(obj, socket.getOutputStream());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void checkOnline() { if (!online) throw new IllegalStateException("Client is not online"); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return the {@link User} as which this client is logged in
 | 
			
		||||
	 * @since Envoy Client v0.1-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public User getSender() { return sender; }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Sets the client user which is used to send messages.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param clientUser the client user to set
 | 
			
		||||
	 * @since Envoy Client v0.2-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public void setSender(User clientUser) { sender = clientUser; }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return the {@link Receiver} used by this {@link Client}
 | 
			
		||||
	 */
 | 
			
		||||
	public Receiver getReceiver() { return receiver; }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return {@code true} if a connection to the server could be established
 | 
			
		||||
	 * @since Envoy Client v0.2-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public boolean isOnline() { return online; }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,29 @@
 | 
			
		||||
package envoy.client.net;
 | 
			
		||||
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
import java.util.logging.Logger;
 | 
			
		||||
 | 
			
		||||
import envoy.data.Message.MessageStatus;
 | 
			
		||||
import envoy.event.EventBus;
 | 
			
		||||
import envoy.event.GroupMessageStatusChange;
 | 
			
		||||
import envoy.util.EnvoyLog;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>GroupMessageStatusChangePocessor.java</strong><br>
 | 
			
		||||
 * Created: <strong>03.07.2020</strong><br>
 | 
			
		||||
 * 
 | 
			
		||||
 * @author Maximilian Käfer
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
public class GroupMessageStatusChangeProcessor implements Consumer<GroupMessageStatusChange> {
 | 
			
		||||
 | 
			
		||||
	private static final Logger logger = EnvoyLog.getLogger(GroupMessageStatusChangeProcessor.class);
 | 
			
		||||
	
 | 
			
		||||
	@Override
 | 
			
		||||
	public void accept(GroupMessageStatusChange evt) {
 | 
			
		||||
		if (evt.get().ordinal() < MessageStatus.RECEIVED.ordinal()) logger.warning("Received invalid group message status change " + evt);
 | 
			
		||||
		else EventBus.getInstance().dispatch(evt);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,35 @@
 | 
			
		||||
package envoy.client.net;
 | 
			
		||||
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
import java.util.logging.Logger;
 | 
			
		||||
 | 
			
		||||
import envoy.data.Message.MessageStatus;
 | 
			
		||||
import envoy.event.EventBus;
 | 
			
		||||
import envoy.event.MessageStatusChange;
 | 
			
		||||
import envoy.util.EnvoyLog;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>MessageStatusChangeProcessor.java</strong><br>
 | 
			
		||||
 * Created: <strong>4 Feb 2020</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.3-alpha
 | 
			
		||||
 */
 | 
			
		||||
public class MessageStatusChangeProcessor implements Consumer<MessageStatusChange> {
 | 
			
		||||
 | 
			
		||||
	private static final Logger logger = EnvoyLog.getLogger(MessageStatusChangeProcessor.class);
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Dispatches a {@link MessageStatusChange} if the status is
 | 
			
		||||
	 * {@code RECEIVED} or {@code READ}.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param evt the status change event
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	@Override
 | 
			
		||||
	public void accept(MessageStatusChange evt) {
 | 
			
		||||
		if (evt.get().ordinal() < MessageStatus.RECEIVED.ordinal()) logger.warning("Received invalid message status change " + evt);
 | 
			
		||||
		else EventBus.getInstance().dispatch(evt);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,33 @@
 | 
			
		||||
package envoy.client.net;
 | 
			
		||||
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
import java.util.logging.Logger;
 | 
			
		||||
 | 
			
		||||
import envoy.client.event.MessageCreationEvent;
 | 
			
		||||
import envoy.data.GroupMessage;
 | 
			
		||||
import envoy.data.Message.MessageStatus;
 | 
			
		||||
import envoy.event.EventBus;
 | 
			
		||||
import envoy.util.EnvoyLog;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>ReceivedGroupMessageProcessor.java</strong><br>
 | 
			
		||||
 * Created: <strong>13.06.2020</strong><br>
 | 
			
		||||
 * 
 | 
			
		||||
 * @author Maximilian Käfer
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
public class ReceivedGroupMessageProcessor implements Consumer<GroupMessage> {
 | 
			
		||||
 | 
			
		||||
	private static final Logger logger = EnvoyLog.getLogger(ReceivedGroupMessageProcessor.class);
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public void accept(GroupMessage groupMessage) {
 | 
			
		||||
		if (groupMessage.getStatus() == MessageStatus.WAITING || groupMessage.getStatus() == MessageStatus.READ)
 | 
			
		||||
			logger.warning("The groupMessage has the unexpected status " + groupMessage.getStatus());
 | 
			
		||||
 | 
			
		||||
		// Dispatch event
 | 
			
		||||
		EventBus.getInstance().dispatch(new MessageCreationEvent(groupMessage));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,36 @@
 | 
			
		||||
package envoy.client.net;
 | 
			
		||||
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
import java.util.logging.Level;
 | 
			
		||||
import java.util.logging.Logger;
 | 
			
		||||
 | 
			
		||||
import envoy.client.event.MessageCreationEvent;
 | 
			
		||||
import envoy.data.Message;
 | 
			
		||||
import envoy.data.Message.MessageStatus;
 | 
			
		||||
import envoy.event.EventBus;
 | 
			
		||||
import envoy.util.EnvoyLog;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>ReceivedMessageProcessor.java</strong><br>
 | 
			
		||||
 * Created: <strong>31.12.2019</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.3-alpha
 | 
			
		||||
 */
 | 
			
		||||
public class ReceivedMessageProcessor implements Consumer<Message> {
 | 
			
		||||
 | 
			
		||||
	private static final Logger logger = EnvoyLog.getLogger(ReceivedMessageProcessor.class);
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public void accept(Message message) {
 | 
			
		||||
		if (message.getStatus() != MessageStatus.SENT) logger.log(Level.WARNING, "The message has the unexpected status " + message.getStatus());
 | 
			
		||||
		else {
 | 
			
		||||
			// Update status to RECEIVED
 | 
			
		||||
			message.nextStatus();
 | 
			
		||||
 | 
			
		||||
			// Dispatch event
 | 
			
		||||
			EventBus.getInstance().dispatch(new MessageCreationEvent(message));
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										118
									
								
								client/src/main/java/envoy/client/net/Receiver.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								client/src/main/java/envoy/client/net/Receiver.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,118 @@
 | 
			
		||||
package envoy.client.net;
 | 
			
		||||
 | 
			
		||||
import java.io.ByteArrayInputStream;
 | 
			
		||||
import java.io.InputStream;
 | 
			
		||||
import java.io.ObjectInputStream;
 | 
			
		||||
import java.net.SocketException;
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
import java.util.logging.Level;
 | 
			
		||||
import java.util.logging.Logger;
 | 
			
		||||
 | 
			
		||||
import envoy.util.EnvoyLog;
 | 
			
		||||
import envoy.util.SerializationUtils;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Receives objects from the server and passes them to processor objects based
 | 
			
		||||
 * on their class.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>Receiver.java</strong><br>
 | 
			
		||||
 * Created: <strong>30.12.2019</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.3-alpha
 | 
			
		||||
 */
 | 
			
		||||
public class Receiver extends Thread {
 | 
			
		||||
 | 
			
		||||
	private final InputStream					in;
 | 
			
		||||
	private final Map<Class<?>, Consumer<?>>	processors	= new HashMap<>();
 | 
			
		||||
 | 
			
		||||
	private static final Logger logger = EnvoyLog.getLogger(Receiver.class);
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Creates an instance of {@link Receiver}.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param in the {@link InputStream} to parse objects from
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public Receiver(InputStream in) {
 | 
			
		||||
		super("Receiver");
 | 
			
		||||
		this.in = in;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Starts the receiver loop. When an object is read, it is passed to the
 | 
			
		||||
	 * appropriate processor.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	@Override
 | 
			
		||||
	public void run() {
 | 
			
		||||
 | 
			
		||||
		while (true) {
 | 
			
		||||
			try {
 | 
			
		||||
				// Read object length
 | 
			
		||||
				final byte[] lenBytes = new byte[4];
 | 
			
		||||
				in.read(lenBytes);
 | 
			
		||||
				final int len = SerializationUtils.bytesToInt(lenBytes, 0);
 | 
			
		||||
				logger.log(Level.FINEST, "Expecting object of length " + len + ".");
 | 
			
		||||
 | 
			
		||||
				// Read object into byte array
 | 
			
		||||
				final byte[]	objBytes	= new byte[len];
 | 
			
		||||
				final int		bytesRead	= in.read(objBytes);
 | 
			
		||||
				logger.log(Level.FINEST, "Read " + bytesRead + " bytes.");
 | 
			
		||||
 | 
			
		||||
				// Catch LV encoding errors
 | 
			
		||||
				if (len != bytesRead) {
 | 
			
		||||
					logger.log(Level.WARNING,
 | 
			
		||||
							String.format("LV encoding violated: expected %d bytes, received %d bytes. Discarding object...", len, bytesRead));
 | 
			
		||||
					continue;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				try (ObjectInputStream oin = new ObjectInputStream(new ByteArrayInputStream(objBytes))) {
 | 
			
		||||
					final Object obj = oin.readObject();
 | 
			
		||||
					logger.log(Level.FINE, "Received " + obj);
 | 
			
		||||
 | 
			
		||||
					// Get appropriate processor
 | 
			
		||||
					@SuppressWarnings("rawtypes")
 | 
			
		||||
					final Consumer processor = processors.get(obj.getClass());
 | 
			
		||||
					if (processor == null)
 | 
			
		||||
						logger.log(Level.WARNING, String.format("The received object has the %s for which no processor is defined.", obj.getClass()));
 | 
			
		||||
					else processor.accept(obj);
 | 
			
		||||
				}
 | 
			
		||||
			} catch (final SocketException e) {
 | 
			
		||||
				// Connection probably closed by client.
 | 
			
		||||
				return;
 | 
			
		||||
			} catch (final Exception e) {
 | 
			
		||||
				logger.log(Level.SEVERE, "Error on receiver thread", e);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Adds an object processor to this {@link Receiver}. It will be called once an
 | 
			
		||||
	 * object of the accepted class has been received.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param processorClass the object class accepted by the processor
 | 
			
		||||
	 * @param processor      the object processor
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public <T> void registerProcessor(Class<T> processorClass, Consumer<T> processor) { processors.put(processorClass, processor); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Adds a map of object processors to this {@link Receiver}.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @param processors the processors to add the processors to add
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public void registerProcessors(Map<Class<?>, ? extends Consumer<?>> processors) { this.processors.putAll(processors); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Removes all object processors registered at this {@link Receiver}.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public void removeAllProcessors() { processors.clear(); }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										100
									
								
								client/src/main/java/envoy/client/net/WriteProxy.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								client/src/main/java/envoy/client/net/WriteProxy.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,100 @@
 | 
			
		||||
package envoy.client.net;
 | 
			
		||||
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.util.logging.Level;
 | 
			
		||||
import java.util.logging.Logger;
 | 
			
		||||
 | 
			
		||||
import envoy.client.data.Cache;
 | 
			
		||||
import envoy.client.data.LocalDB;
 | 
			
		||||
import envoy.data.Message;
 | 
			
		||||
import envoy.event.MessageStatusChange;
 | 
			
		||||
import envoy.util.EnvoyLog;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Implements methods to send {@link Message}s and
 | 
			
		||||
 * {@link MessageStatusChange}s to the server or cache them inside a
 | 
			
		||||
 * {@link LocalDB} depending on the online status.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>WriteProxy.java</strong><br>
 | 
			
		||||
 * Created: <strong>6 Feb 2020</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.3-alpha
 | 
			
		||||
 */
 | 
			
		||||
public class WriteProxy {
 | 
			
		||||
 | 
			
		||||
	private final Client	client;
 | 
			
		||||
	private final LocalDB	localDB;
 | 
			
		||||
 | 
			
		||||
	private static final Logger logger = EnvoyLog.getLogger(WriteProxy.class);
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Initializes a write proxy using a client and a local database. The
 | 
			
		||||
	 * corresponding cache processors are injected into the caches.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param client  the client used to send messages and message status change
 | 
			
		||||
	 *                events
 | 
			
		||||
	 * @param localDB the local database used to cache messages and message status
 | 
			
		||||
	 *                change events
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public WriteProxy(Client client, LocalDB localDB) {
 | 
			
		||||
		this.client		= client;
 | 
			
		||||
		this.localDB	= localDB;
 | 
			
		||||
 | 
			
		||||
		// Initialize cache processors for messages and message status change events
 | 
			
		||||
		localDB.getCacheMap().get(Message.class).setProcessor(msg -> {
 | 
			
		||||
			try {
 | 
			
		||||
				logger.log(Level.FINER, "Sending cached " + msg);
 | 
			
		||||
				client.sendMessage(msg);
 | 
			
		||||
			} catch (final IOException e) {
 | 
			
		||||
				logger.log(Level.SEVERE, "Could not send cached message: ", e);
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
		localDB.getCacheMap().get(MessageStatusChange.class).setProcessor(evt -> {
 | 
			
		||||
			logger.log(Level.FINER, "Sending cached " + evt);
 | 
			
		||||
			try {
 | 
			
		||||
				client.sendEvent(evt);
 | 
			
		||||
			} catch (final IOException e) {
 | 
			
		||||
				logger.log(Level.SEVERE, "Could not send cached message status change event: ", e);
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Sends cached {@link Message}s and {@link MessageStatusChange}s to the
 | 
			
		||||
	 * server.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public void flushCache() {
 | 
			
		||||
		localDB.getCacheMap().getMap().values().forEach(Cache::relay);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Delivers a message to the server if online. Otherwise the message is cached
 | 
			
		||||
	 * inside the local database.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param message the message to send
 | 
			
		||||
	 * @throws IOException if the message could not be sent
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public void writeMessage(Message message) throws IOException {
 | 
			
		||||
		if (client.isOnline()) client.sendMessage(message);
 | 
			
		||||
		else localDB.getCacheMap().getApplicable(Message.class).accept(message);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Delivers a message status change event to the server if online. Otherwise the
 | 
			
		||||
	 * event is cached inside the local database.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param evt the event to send
 | 
			
		||||
	 * @throws IOException if the event could not be sent
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public void writeMessageStatusChange(MessageStatusChange evt) throws IOException {
 | 
			
		||||
		if (client.isOnline()) client.sendEvent(evt);
 | 
			
		||||
		else localDB.getCacheMap().getApplicable(MessageStatusChange.class).accept(evt);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								client/src/main/java/envoy/client/net/package-info.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								client/src/main/java/envoy/client/net/package-info.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
/**
 | 
			
		||||
 * This package contains all classes related to client-server communication.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @author Leon Hofmeister
 | 
			
		||||
 * @author Maximilian Käfer
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
package envoy.client.net;
 | 
			
		||||
							
								
								
									
										49
									
								
								client/src/main/java/envoy/client/ui/AudioControl.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								client/src/main/java/envoy/client/ui/AudioControl.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,49 @@
 | 
			
		||||
package envoy.client.ui;
 | 
			
		||||
 | 
			
		||||
import java.util.logging.Level;
 | 
			
		||||
import java.util.logging.Logger;
 | 
			
		||||
 | 
			
		||||
import javafx.scene.control.Alert;
 | 
			
		||||
import javafx.scene.control.Alert.AlertType;
 | 
			
		||||
import javafx.scene.control.Button;
 | 
			
		||||
import javafx.scene.layout.HBox;
 | 
			
		||||
 | 
			
		||||
import envoy.client.data.audio.AudioPlayer;
 | 
			
		||||
import envoy.exception.EnvoyException;
 | 
			
		||||
import envoy.util.EnvoyLog;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Enables the play back of audio clips through a button.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>AudioControl.java</strong><br>
 | 
			
		||||
 * Created: <strong>05.07.2020</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
public final class AudioControl extends HBox {
 | 
			
		||||
 | 
			
		||||
	private AudioPlayer player = new AudioPlayer();
 | 
			
		||||
 | 
			
		||||
	private static final Logger logger = EnvoyLog.getLogger(AudioControl.class);
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Initializes the audio control.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param audioData the audio data to play.
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public AudioControl(byte[] audioData) {
 | 
			
		||||
		var button = new Button("Play");
 | 
			
		||||
		button.setOnAction(e -> {
 | 
			
		||||
			try {
 | 
			
		||||
				player.play(audioData);
 | 
			
		||||
			} catch (EnvoyException ex) {
 | 
			
		||||
				logger.log(Level.SEVERE, "Could not play back audio: ", ex);
 | 
			
		||||
				new Alert(AlertType.ERROR, "Could not play back audio").showAndWait();
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
		getChildren().add(button);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										169
									
								
								client/src/main/java/envoy/client/ui/ClearableTextField.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								client/src/main/java/envoy/client/ui/ClearableTextField.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,169 @@
 | 
			
		||||
package envoy.client.ui;
 | 
			
		||||
 | 
			
		||||
import javafx.beans.property.BooleanProperty;
 | 
			
		||||
import javafx.beans.property.ObjectProperty;
 | 
			
		||||
import javafx.beans.property.StringProperty;
 | 
			
		||||
import javafx.event.ActionEvent;
 | 
			
		||||
import javafx.event.EventHandler;
 | 
			
		||||
import javafx.scene.control.*;
 | 
			
		||||
import javafx.scene.image.ImageView;
 | 
			
		||||
import javafx.scene.layout.Background;
 | 
			
		||||
import javafx.scene.layout.ColumnConstraints;
 | 
			
		||||
import javafx.scene.layout.GridPane;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This class offers a text field that is automatically equipped with a clear
 | 
			
		||||
 * button.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>ClearableTextField.java</strong><br>
 | 
			
		||||
 * Created: <strong>25.06.2020</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Leon Hofmeister
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
public class ClearableTextField extends GridPane {
 | 
			
		||||
 | 
			
		||||
	private final TextField textField;
 | 
			
		||||
 | 
			
		||||
	private final Button clearButton;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Constructs a new {@code ClearableTextField} with no initial text and icon
 | 
			
		||||
	 * size 16.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public ClearableTextField() { this("", 16); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Constructs a new {@code ClearableTextField} with initial text and a
 | 
			
		||||
	 * predetermined icon size.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param text the text that should be displayed by default
 | 
			
		||||
	 * @param size the size of the icon
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public ClearableTextField(String text, int size) {
 | 
			
		||||
		// initializing the textField and the button
 | 
			
		||||
		textField	= new TextField(text);
 | 
			
		||||
		clearButton	= new Button("", new ImageView(IconUtil.loadIconThemeSensitive("clear_button", size)));
 | 
			
		||||
		clearButton.setOnAction(e -> textField.clear());
 | 
			
		||||
		clearButton.setFocusTraversable(false);
 | 
			
		||||
		clearButton.getStyleClass().clear();
 | 
			
		||||
		clearButton.setBackground(Background.EMPTY);
 | 
			
		||||
		// Adding the two elements to the GridPane
 | 
			
		||||
		add(textField, 0, 0, 2, 1);
 | 
			
		||||
		add(clearButton, 1, 0, 1, 1);
 | 
			
		||||
		// Setting the percent - widths of the two columns.
 | 
			
		||||
		// Used to locate the button on the right.
 | 
			
		||||
		final var columnConstraints = new ColumnConstraints();
 | 
			
		||||
		columnConstraints.setPercentWidth(90);
 | 
			
		||||
		getColumnConstraints().add(columnConstraints);
 | 
			
		||||
		final var columnConstraints2 = new ColumnConstraints();
 | 
			
		||||
		columnConstraints2.setPercentWidth(10);
 | 
			
		||||
		getColumnConstraints().add(columnConstraints2);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return the underlying {@code textField}
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public TextField getTextField() { return textField; }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * This method offers the freedom to perform custom actions when the
 | 
			
		||||
	 * {@code clearButton} has been pressed.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * The default is
 | 
			
		||||
	 * <b><code>  e -> {clearableTextField.getTextField().clear();}</code></b>
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param onClearButtonAction the action that should be performed
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public void setClearButtonListener(EventHandler<ActionEvent> onClearButtonAction) { clearButton.setOnAction(onClearButtonAction); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return the current property of the prompt text
 | 
			
		||||
	 * @see javafx.scene.control.TextInputControl#promptTextProperty()
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public final StringProperty promptTextProperty() { return textField.promptTextProperty(); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return the current prompt text
 | 
			
		||||
	 * @see javafx.scene.control.TextInputControl#getPromptText()
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public final String getPromptText() { return textField.getPromptText(); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @param value the prompt text to display
 | 
			
		||||
	 * @see javafx.scene.control.TextInputControl#setPromptText(java.lang.String)
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public final void setPromptText(String value) { textField.setPromptText(value); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return the current property of the tooltip
 | 
			
		||||
	 * @see javafx.scene.control.Control#tooltipProperty()
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public final ObjectProperty<Tooltip> tooltipProperty() { return textField.tooltipProperty(); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @param value the new tooltip
 | 
			
		||||
	 * @see javafx.scene.control.Control#setTooltip(javafx.scene.control.Tooltip)
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public final void setTooltip(Tooltip value) { textField.setTooltip(value); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return the current tooltip
 | 
			
		||||
	 * @see javafx.scene.control.Control#getTooltip()
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public final Tooltip getTooltip() { return textField.getTooltip(); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return the current property of the context menu
 | 
			
		||||
	 * @see javafx.scene.control.Control#contextMenuProperty()
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public final ObjectProperty<ContextMenu> contextMenuProperty() { return textField.contextMenuProperty(); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @param value the new context menu
 | 
			
		||||
	 * @see javafx.scene.control.Control#setContextMenu(javafx.scene.control.ContextMenu)
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public final void setContextMenu(ContextMenu value) { textField.setContextMenu(value); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return the current context menu
 | 
			
		||||
	 * @see javafx.scene.control.Control#getContextMenu()
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public final ContextMenu getContextMenu() { return textField.getContextMenu(); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @param value whether this ClearableTextField should be editable
 | 
			
		||||
	 * @see javafx.scene.control.TextInputControl#setEditable(boolean)
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public final void setEditable(boolean value) { textField.setEditable(value); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return the current property whether this ClearableTextField is editable
 | 
			
		||||
	 * @see javafx.scene.control.TextInputControl#editableProperty()
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public final BooleanProperty editableProperty() { return textField.editableProperty(); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return whether this {@code ClearableTextField} is editable
 | 
			
		||||
	 * @see javafx.scene.control.TextInputControl#isEditable()
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public final boolean isEditable() { return textField.isEditable(); }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										160
									
								
								client/src/main/java/envoy/client/ui/IconUtil.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								client/src/main/java/envoy/client/ui/IconUtil.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,160 @@
 | 
			
		||||
package envoy.client.ui;
 | 
			
		||||
 | 
			
		||||
import java.util.EnumMap;
 | 
			
		||||
import java.util.EnumSet;
 | 
			
		||||
import java.util.logging.Level;
 | 
			
		||||
 | 
			
		||||
import javafx.scene.image.Image;
 | 
			
		||||
 | 
			
		||||
import envoy.client.data.Settings;
 | 
			
		||||
import envoy.util.EnvoyLog;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Provides static utility methods for loading icons from the resource
 | 
			
		||||
 * folder.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>IconUtil.java</strong><br>
 | 
			
		||||
 * Created: <strong>16.03.2020</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
public class IconUtil {
 | 
			
		||||
 | 
			
		||||
	private IconUtil() {}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Loads an image from the resource folder.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param path the path to the icon inside the resource folder
 | 
			
		||||
	 * @return the loaded image
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public static Image load(String path) {
 | 
			
		||||
		Image image = null;
 | 
			
		||||
		try {
 | 
			
		||||
			image = new Image(IconUtil.class.getResource(path).toExternalForm());
 | 
			
		||||
		} catch (final NullPointerException e) {
 | 
			
		||||
			EnvoyLog.getLogger(IconUtil.class).log(Level.WARNING, String.format("Could not load image at path %s: ", path), e);
 | 
			
		||||
		}
 | 
			
		||||
		return image;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Loads an image from the resource folder and scales it to the given size.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param path the path to the icon inside the resource folder
 | 
			
		||||
	 * @param size the size to scale the icon to
 | 
			
		||||
	 * @return the scaled image
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public static Image load(String path, int size) {
 | 
			
		||||
		Image image = null;
 | 
			
		||||
		try {
 | 
			
		||||
			image = new Image(IconUtil.class.getResource(path).toExternalForm(), size, size, true, true);
 | 
			
		||||
		} catch (final NullPointerException e) {
 | 
			
		||||
			EnvoyLog.getLogger(IconUtil.class).log(Level.WARNING, String.format("Could not load image at path %s: ", path), e);
 | 
			
		||||
		}
 | 
			
		||||
		return image;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Loads a {@code .png} image from the sub-folder {@code /icons/} of the
 | 
			
		||||
	 * resource folder.<br>
 | 
			
		||||
	 * The suffix {@code .png} is automatically appended.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param name the image name without the .png suffix
 | 
			
		||||
	 * @return the loaded image
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 * @apiNote let's load a sample image {@code /icons/abc.png}.<br>
 | 
			
		||||
	 *          To do that, we only have to call {@code IconUtil.loadIcon("abc")}
 | 
			
		||||
	 */
 | 
			
		||||
	public static Image loadIcon(String name) { return load("/icons/" + name + ".png"); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Loads a {@code .png} image from the sub-folder {@code /icons/} of the
 | 
			
		||||
	 * resource folder and scales it to the given size.<br>
 | 
			
		||||
	 * The suffix {@code .png} is automatically appended.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param name the image name without the .png suffix
 | 
			
		||||
	 * @param size the size of the image to scale to
 | 
			
		||||
	 * @return the loaded image
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 * @apiNote let's load a sample image {@code /icons/abc.png} in size 16.<br>
 | 
			
		||||
	 *          To do that, we only have to call
 | 
			
		||||
	 *          {@code IconUtil.loadIcon("abc", 16)}
 | 
			
		||||
	 */
 | 
			
		||||
	public static Image loadIcon(String name, int size) { return load("/icons/" + name + ".png", size); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Loads a {@code .png} image whose design depends on the currently active theme
 | 
			
		||||
	 * from the sub-folder {@code /icons/dark/} or {@code /icons/light/} of the
 | 
			
		||||
	 * resource folder.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * The suffix {@code .png} is automatically appended.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param name the image name without the "black" or "white" suffix and without
 | 
			
		||||
	 *             the .png suffix
 | 
			
		||||
	 * @return the loaded image
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 * @apiNote let's take two sample images {@code /icons/dark/abc.png} and
 | 
			
		||||
	 *          {@code /icons/light/abc.png}, and load one of them.<br>
 | 
			
		||||
	 *          To do that theme sensitive, we only have to call
 | 
			
		||||
	 *          {@code IconUtil.loadIconThemeSensitive("abc")}
 | 
			
		||||
	 */
 | 
			
		||||
	public static Image loadIconThemeSensitive(String name) { return loadIcon(themeSpecificSubFolder() + name); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Loads a {@code .png} image whose design depends on the currently active theme
 | 
			
		||||
	 * from the sub-folder {@code /icons/dark/} or {@code /icons/light/} of the
 | 
			
		||||
	 * resource folder and scales it to the given size.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * The suffix {@code .png} is automatically appended.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param name the image name without the .png suffix
 | 
			
		||||
	 * @param size the size of the image to scale to
 | 
			
		||||
	 * @return the loaded image
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 * @apiNote let's take two sample images {@code /icons/dark/abc.png} and
 | 
			
		||||
	 *          {@code /icons/light/abc.png}, and load one of them in size 16.<br>
 | 
			
		||||
	 *          To do that theme sensitive, we only have to call
 | 
			
		||||
	 *          {@code IconUtil.loadIconThemeSensitive("abc", 16)}
 | 
			
		||||
	 */
 | 
			
		||||
	public static Image loadIconThemeSensitive(String name, int size) { return loadIcon(themeSpecificSubFolder() + name, size); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 *
 | 
			
		||||
	 * Loads images specified by an enum. The images have to be named like the
 | 
			
		||||
	 * lowercase enum constants with {@code .png} extension and be located inside a
 | 
			
		||||
	 * folder with the lowercase name of the enum, which must be contained inside
 | 
			
		||||
	 * the {@code /icons/} folder.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param <T>       the enum that specifies the images to load
 | 
			
		||||
	 * @param enumClass the class of the enum
 | 
			
		||||
	 * @param size      the size to scale the images to
 | 
			
		||||
	 * @return a map containing the loaded images with the corresponding enum
 | 
			
		||||
	 *         constants as keys
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public static <T extends Enum<T>> EnumMap<T, Image> loadByEnum(Class<T> enumClass, int size) {
 | 
			
		||||
		final var	icons	= new EnumMap<T, Image>(enumClass);
 | 
			
		||||
		final var	path	= "/icons/" + enumClass.getSimpleName().toLowerCase() + "/";
 | 
			
		||||
		for (final var e : EnumSet.allOf(enumClass))
 | 
			
		||||
			icons.put(e, load(path + e.toString().toLowerCase() + ".png", size));
 | 
			
		||||
		return icons;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * This method should be called if the display of an image depends upon the
 | 
			
		||||
	 * currently active theme.<br>
 | 
			
		||||
	 * In case of a default theme, the string returned will be
 | 
			
		||||
	 * ({@code dark/} or {@code light/}), otherwise it will be empty.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @return the theme specific folder
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public static String themeSpecificSubFolder() {
 | 
			
		||||
		return Settings.getInstance().isUsingDefaultTheme() ? Settings.getInstance().getCurrentTheme() + "/" : "";
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										25
									
								
								client/src/main/java/envoy/client/ui/Restorable.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								client/src/main/java/envoy/client/ui/Restorable.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
package envoy.client.ui;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This interface defines an action that should be performed when a scene gets
 | 
			
		||||
 * restored from the scene stack in {@link SceneContext}.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>Restorable.java</strong><br>
 | 
			
		||||
 * Created: <strong>03.07.2020</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Leon Hofmeister
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
@FunctionalInterface
 | 
			
		||||
public interface Restorable {
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * This method is getting called when a scene gets restored.<br>
 | 
			
		||||
	 * Hence, it can contain anything that should be done when the underlying scene
 | 
			
		||||
	 * gets restored.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	void onRestore();
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										179
									
								
								client/src/main/java/envoy/client/ui/SceneContext.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								client/src/main/java/envoy/client/ui/SceneContext.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,179 @@
 | 
			
		||||
package envoy.client.ui;
 | 
			
		||||
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.util.Stack;
 | 
			
		||||
import java.util.logging.Level;
 | 
			
		||||
 | 
			
		||||
import javafx.fxml.FXMLLoader;
 | 
			
		||||
import javafx.scene.Parent;
 | 
			
		||||
import javafx.scene.Scene;
 | 
			
		||||
import javafx.stage.Stage;
 | 
			
		||||
 | 
			
		||||
import envoy.client.data.Settings;
 | 
			
		||||
import envoy.client.event.ThemeChangeEvent;
 | 
			
		||||
import envoy.event.EventBus;
 | 
			
		||||
import envoy.util.EnvoyLog;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Manages a stack of scenes. The most recently added scene is displayed inside
 | 
			
		||||
 * a stage. When a scene is removed from the stack, its predecessor is
 | 
			
		||||
 * displayed.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * When a scene is loaded, the style sheet for the current theme is applied to
 | 
			
		||||
 * it.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>SceneContext.java</strong><br>
 | 
			
		||||
 * Created: <strong>06.06.2020</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
public final class SceneContext {
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Contains information about different scenes and their FXML resource files.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @author Kai S. K. Engelbart
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public enum SceneInfo {
 | 
			
		||||
 | 
			
		||||
		/**
 | 
			
		||||
		 * The main scene in which the chat screen is displayed.
 | 
			
		||||
		 *
 | 
			
		||||
		 * @since Envoy Client v0.1-beta
 | 
			
		||||
		 */
 | 
			
		||||
		CHAT_SCENE("/fxml/ChatScene.fxml"),
 | 
			
		||||
 | 
			
		||||
		/**
 | 
			
		||||
		 * The scene in which the settings screen is displayed.
 | 
			
		||||
		 *
 | 
			
		||||
		 * @since Envoy Client v0.1-beta
 | 
			
		||||
		 */
 | 
			
		||||
		SETTINGS_SCENE("/fxml/SettingsScene.fxml"),
 | 
			
		||||
 | 
			
		||||
		/**
 | 
			
		||||
		 * The scene in which the contact search screen is displayed.
 | 
			
		||||
		 *
 | 
			
		||||
		 * @since Envoy Client v0.1-beta
 | 
			
		||||
		 */
 | 
			
		||||
		CONTACT_SEARCH_SCENE("/fxml/ContactSearchScene.fxml"),
 | 
			
		||||
 | 
			
		||||
		/**
 | 
			
		||||
		 * The scene in which the group creation screen is displayed.
 | 
			
		||||
		 *
 | 
			
		||||
		 * @since Envoy Client v0.1-beta
 | 
			
		||||
		 */
 | 
			
		||||
		GROUP_CREATION_SCENE("/fxml/GroupCreationScene.fxml"),
 | 
			
		||||
 | 
			
		||||
		/**
 | 
			
		||||
		 * The scene in which the login screen is displayed.
 | 
			
		||||
		 *
 | 
			
		||||
		 * @since Envoy Client v0.1-beta
 | 
			
		||||
		 */
 | 
			
		||||
		LOGIN_SCENE("/fxml/LoginScene.fxml"),
 | 
			
		||||
 | 
			
		||||
		/**
 | 
			
		||||
		 * The scene in which the info screen is displayed.
 | 
			
		||||
		 *
 | 
			
		||||
		 * @since Envoy Client v0.1-beta
 | 
			
		||||
		 */
 | 
			
		||||
		MESSAGE_INFO_SCENE("/fxml/MessageInfoScene.fxml");
 | 
			
		||||
 | 
			
		||||
		/**
 | 
			
		||||
		 * The path to the FXML resource.
 | 
			
		||||
		 */
 | 
			
		||||
		public final String path;
 | 
			
		||||
 | 
			
		||||
		SceneInfo(String path) { this.path = path; }
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private final Stage			stage;
 | 
			
		||||
	private final FXMLLoader	loader			= new FXMLLoader();
 | 
			
		||||
	private final Stack<Scene>	sceneStack		= new Stack<>();
 | 
			
		||||
	private final Stack<Object>	controllerStack	= new Stack<>();
 | 
			
		||||
 | 
			
		||||
	private static final Settings settings = Settings.getInstance();
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Initializes the scene context.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param stage the stage in which scenes will be displayed
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public SceneContext(Stage stage) {
 | 
			
		||||
		this.stage = stage;
 | 
			
		||||
		EventBus.getInstance().register(ThemeChangeEvent.class, theme -> applyCSS());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Loads a new scene specified by a scene info.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param sceneInfo specifies the scene to load
 | 
			
		||||
	 * @throws RuntimeException if the loading process fails
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public void load(SceneInfo sceneInfo) {
 | 
			
		||||
		loader.setRoot(null);
 | 
			
		||||
		loader.setController(null);
 | 
			
		||||
 | 
			
		||||
		try {
 | 
			
		||||
			final var	rootNode	= (Parent) loader.load(getClass().getResourceAsStream(sceneInfo.path));
 | 
			
		||||
			final var	scene		= new Scene(rootNode);
 | 
			
		||||
			controllerStack.push(loader.getController());
 | 
			
		||||
 | 
			
		||||
			sceneStack.push(scene);
 | 
			
		||||
			stage.setScene(scene);
 | 
			
		||||
			applyCSS();
 | 
			
		||||
			stage.sizeToScene();
 | 
			
		||||
			stage.show();
 | 
			
		||||
		} catch (final IOException e) {
 | 
			
		||||
			EnvoyLog.getLogger(SceneContext.class).log(Level.SEVERE, String.format("Could not load scene for %s: ", sceneInfo), e);
 | 
			
		||||
			throw new RuntimeException(e);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Removes the current scene and displays the previous one.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public void pop() {
 | 
			
		||||
		sceneStack.pop();
 | 
			
		||||
		controllerStack.pop();
 | 
			
		||||
		if (!sceneStack.isEmpty()) {
 | 
			
		||||
			final var newScene = sceneStack.peek();
 | 
			
		||||
			stage.setScene(newScene);
 | 
			
		||||
			applyCSS();
 | 
			
		||||
			stage.sizeToScene();
 | 
			
		||||
			// If the controller implements the Restorable interface,
 | 
			
		||||
			// the actions to perform on restoration will be executed here
 | 
			
		||||
			final var controller = controllerStack.peek();
 | 
			
		||||
			if (controller instanceof Restorable) ((Restorable) controller).onRestore();
 | 
			
		||||
		}
 | 
			
		||||
		stage.show();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void applyCSS() {
 | 
			
		||||
		if (!sceneStack.isEmpty()) {
 | 
			
		||||
			final var	styleSheets	= stage.getScene().getStylesheets();
 | 
			
		||||
			final var	themeCSS	= "/css/" + settings.getCurrentTheme() + ".css";
 | 
			
		||||
			styleSheets.clear();
 | 
			
		||||
			styleSheets.addAll(getClass().getResource("/css/base.css").toExternalForm(), getClass().getResource(themeCSS).toExternalForm());
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @param <T> the type of the controller
 | 
			
		||||
	 * @return the controller used by the current scene
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public <T> T getController() { return (T) controllerStack.peek(); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return the stage in which the scenes are displayed
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public Stage getStage() { return stage; }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										135
									
								
								client/src/main/java/envoy/client/ui/Startup.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								client/src/main/java/envoy/client/ui/Startup.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,135 @@
 | 
			
		||||
package envoy.client.ui;
 | 
			
		||||
 | 
			
		||||
import java.io.File;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.util.Properties;
 | 
			
		||||
import java.util.logging.Level;
 | 
			
		||||
import java.util.logging.Logger;
 | 
			
		||||
 | 
			
		||||
import javafx.application.Application;
 | 
			
		||||
import javafx.scene.control.Alert;
 | 
			
		||||
import javafx.scene.control.Alert.AlertType;
 | 
			
		||||
import javafx.stage.Stage;
 | 
			
		||||
 | 
			
		||||
import envoy.client.data.*;
 | 
			
		||||
import envoy.client.net.Client;
 | 
			
		||||
import envoy.client.ui.SceneContext.SceneInfo;
 | 
			
		||||
import envoy.client.ui.controller.LoginScene;
 | 
			
		||||
import envoy.data.GroupMessage;
 | 
			
		||||
import envoy.data.Message;
 | 
			
		||||
import envoy.event.GroupMessageStatusChange;
 | 
			
		||||
import envoy.event.MessageStatusChange;
 | 
			
		||||
import envoy.exception.EnvoyException;
 | 
			
		||||
import envoy.util.EnvoyLog;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Handles application startup and shutdown.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>Startup.java</strong><br>
 | 
			
		||||
 * Created: <strong>26.03.2020</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @author Maximilian Käfer
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
public final class Startup extends Application {
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * The version of this client. Used to verify compatibility with the server.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public static final String VERSION = "0.1-beta";
 | 
			
		||||
 | 
			
		||||
	private LocalDB	localDB;
 | 
			
		||||
	private Client	client;
 | 
			
		||||
 | 
			
		||||
	private static final ClientConfig	config	= ClientConfig.getInstance();
 | 
			
		||||
	private static final Logger			logger	= EnvoyLog.getLogger(Startup.class);
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Loads the configuration, initializes the client and the local database and
 | 
			
		||||
	 * delegates the rest of the startup process to {@link LoginScene}.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	@Override
 | 
			
		||||
	public void start(Stage stage) throws Exception {
 | 
			
		||||
		try {
 | 
			
		||||
			// Load the configuration from client.properties first
 | 
			
		||||
			final Properties properties = new Properties();
 | 
			
		||||
			properties.load(Startup.class.getClassLoader().getResourceAsStream("client.properties"));
 | 
			
		||||
			config.load(properties);
 | 
			
		||||
 | 
			
		||||
			// Override configuration values with command line arguments
 | 
			
		||||
			final String[] args = getParameters().getRaw().toArray(new String[0]);
 | 
			
		||||
			if (args.length > 0) config.load(args);
 | 
			
		||||
 | 
			
		||||
			// Check if all mandatory configuration values have been initialized
 | 
			
		||||
			if (!config.isInitialized()) throw new EnvoyException("Configuration is not fully initialized");
 | 
			
		||||
		} catch (final Exception e) {
 | 
			
		||||
			new Alert(AlertType.ERROR, "Error loading configuration values:\n" + e);
 | 
			
		||||
			logger.log(Level.SEVERE, "Error loading configuration values: ", e);
 | 
			
		||||
			e.printStackTrace();
 | 
			
		||||
			System.exit(1);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Setup logger for the envoy package
 | 
			
		||||
		EnvoyLog.initialize(config);
 | 
			
		||||
		EnvoyLog.attach("envoy");
 | 
			
		||||
		EnvoyLog.setFileLevelBarrier(config.getFileLevelBarrier());
 | 
			
		||||
		EnvoyLog.setConsoleLevelBarrier(config.getConsoleLevelBarrier());
 | 
			
		||||
 | 
			
		||||
		logger.log(Level.INFO, "Envoy starting...");
 | 
			
		||||
 | 
			
		||||
		// Initialize the local database
 | 
			
		||||
		if (config.isIgnoreLocalDB()) {
 | 
			
		||||
			localDB = new TransientLocalDB();
 | 
			
		||||
			new Alert(AlertType.WARNING, "Ignoring local database.\nMessages will not be saved!").showAndWait();
 | 
			
		||||
		} else try {
 | 
			
		||||
			localDB = new PersistentLocalDB(new File(config.getHomeDirectory(), config.getLocalDB().getPath()));
 | 
			
		||||
		} catch (final IOException e3) {
 | 
			
		||||
			logger.log(Level.SEVERE, "Could not initialize local database: ", e3);
 | 
			
		||||
			new Alert(AlertType.ERROR, "Could not initialize local database!\n" + e3).showAndWait();
 | 
			
		||||
			System.exit(1);
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Initialize client and unread message cache
 | 
			
		||||
		client = new Client();
 | 
			
		||||
 | 
			
		||||
		final var cacheMap = new CacheMap();
 | 
			
		||||
		cacheMap.put(Message.class, new Cache<Message>());
 | 
			
		||||
		cacheMap.put(GroupMessage.class, new Cache<GroupMessage>());
 | 
			
		||||
		cacheMap.put(MessageStatusChange.class, new Cache<MessageStatusChange>());
 | 
			
		||||
		cacheMap.put(GroupMessageStatusChange.class, new Cache<GroupMessageStatusChange>());
 | 
			
		||||
 | 
			
		||||
		stage.setTitle("Envoy");
 | 
			
		||||
		stage.getIcons().add(IconUtil.loadIcon("envoy_logo"));
 | 
			
		||||
 | 
			
		||||
		final var sceneContext = new SceneContext(stage);
 | 
			
		||||
		sceneContext.load(SceneInfo.LOGIN_SCENE);
 | 
			
		||||
		sceneContext.<LoginScene>getController().initializeData(client, localDB, cacheMap, sceneContext);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Closes the client connection and saves the local database and settings.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	@Override
 | 
			
		||||
	public void stop() {
 | 
			
		||||
		try {
 | 
			
		||||
			logger.log(Level.INFO, "Closing connection...");
 | 
			
		||||
			client.close();
 | 
			
		||||
 | 
			
		||||
			logger.log(Level.INFO, "Saving local database and settings...");
 | 
			
		||||
			localDB.save();
 | 
			
		||||
			Settings.getInstance().save();
 | 
			
		||||
			logger.log(Level.INFO, "Envoy was terminated by its user");
 | 
			
		||||
		} catch (final Exception e) {
 | 
			
		||||
			logger.log(Level.SEVERE, "Unable to save local files: ", e);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										100
									
								
								client/src/main/java/envoy/client/ui/StatusTrayIcon.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								client/src/main/java/envoy/client/ui/StatusTrayIcon.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,100 @@
 | 
			
		||||
package envoy.client.ui;
 | 
			
		||||
 | 
			
		||||
import java.awt.*;
 | 
			
		||||
import java.awt.TrayIcon.MessageType;
 | 
			
		||||
import java.awt.event.WindowAdapter;
 | 
			
		||||
import java.awt.event.WindowEvent;
 | 
			
		||||
import java.util.logging.Level;
 | 
			
		||||
 | 
			
		||||
import envoy.client.event.MessageCreationEvent;
 | 
			
		||||
import envoy.data.Message;
 | 
			
		||||
import envoy.event.EventBus;
 | 
			
		||||
import envoy.exception.EnvoyException;
 | 
			
		||||
import envoy.util.EnvoyLog;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>StatusTrayIcon.java</strong><br>
 | 
			
		||||
 * Created: <strong>3 Dec 2019</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.2-alpha
 | 
			
		||||
 */
 | 
			
		||||
public class StatusTrayIcon {
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * The {@link TrayIcon} provided by the System Tray API for controlling the
 | 
			
		||||
	 * system tray. This includes displaying the icon, but also creating
 | 
			
		||||
	 * notifications when new messages are received.
 | 
			
		||||
	 */
 | 
			
		||||
	private final TrayIcon trayIcon;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * A received {@link Message} is only displayed as a system tray notification if
 | 
			
		||||
	 * this variable is set to {@code true}.
 | 
			
		||||
	 */
 | 
			
		||||
	private boolean displayMessages = false;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Creates a {@link StatusTrayIcon} with the Envoy logo, a tool tip and a pop-up
 | 
			
		||||
	 * menu.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param focusTarget the {@link Window} which focus determines if message
 | 
			
		||||
	 *                    notifications are displayed
 | 
			
		||||
	 * @throws EnvoyException if the currently used OS does not support the System
 | 
			
		||||
	 *                        Tray API
 | 
			
		||||
	 * @since Envoy Client v0.2-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public StatusTrayIcon(Window focusTarget) throws EnvoyException {
 | 
			
		||||
		if (!SystemTray.isSupported()) throw new EnvoyException("The Envoy tray icon is not supported.");
 | 
			
		||||
 | 
			
		||||
		final ClassLoader	loader	= Thread.currentThread().getContextClassLoader();
 | 
			
		||||
		final Image			img		= Toolkit.getDefaultToolkit().createImage(loader.getResource("envoy_logo.png"));
 | 
			
		||||
		trayIcon = new TrayIcon(img, "Envoy Client");
 | 
			
		||||
		trayIcon.setImageAutoSize(true);
 | 
			
		||||
		trayIcon.setToolTip("You are notified if you have unread messages.");
 | 
			
		||||
 | 
			
		||||
		final PopupMenu popup = new PopupMenu();
 | 
			
		||||
 | 
			
		||||
		final MenuItem exitMenuItem = new MenuItem("Exit");
 | 
			
		||||
		exitMenuItem.addActionListener(evt -> System.exit(0));
 | 
			
		||||
		popup.add(exitMenuItem);
 | 
			
		||||
 | 
			
		||||
		trayIcon.setPopupMenu(popup);
 | 
			
		||||
 | 
			
		||||
		// Only display messages if the chat window is not focused
 | 
			
		||||
		focusTarget.addWindowFocusListener(new WindowAdapter() {
 | 
			
		||||
 | 
			
		||||
			@Override
 | 
			
		||||
			public void windowGainedFocus(WindowEvent e) { displayMessages = false; }
 | 
			
		||||
 | 
			
		||||
			@Override
 | 
			
		||||
			public void windowLostFocus(WindowEvent e) { displayMessages = true; }
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		// Show the window if the user clicks on the icon
 | 
			
		||||
		trayIcon.addActionListener(evt -> { focusTarget.setVisible(true); focusTarget.requestFocus(); });
 | 
			
		||||
 | 
			
		||||
		// Start processing message events
 | 
			
		||||
		// TODO: Handle other message types
 | 
			
		||||
		EventBus.getInstance()
 | 
			
		||||
			.register(MessageCreationEvent.class,
 | 
			
		||||
					evt -> { if (displayMessages) trayIcon.displayMessage("New message received", evt.get().getText(), MessageType.INFO); });
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Makes this {@link StatusTrayIcon} appear in the system tray.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @throws EnvoyException if the status icon could not be attaches to the system
 | 
			
		||||
	 *                        tray for system-internal reasons
 | 
			
		||||
	 * @since Envoy Client v0.2-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public void show() throws EnvoyException {
 | 
			
		||||
		try {
 | 
			
		||||
			SystemTray.getSystemTray().add(trayIcon);
 | 
			
		||||
		} catch (final AWTException e) {
 | 
			
		||||
			EnvoyLog.getLogger(StatusTrayIcon.class).log(Level.INFO, "Could not display StatusTrayIcon: ", e);
 | 
			
		||||
			throw new EnvoyException("Could not attach Envoy tray icon to system tray.", e);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										570
									
								
								client/src/main/java/envoy/client/ui/controller/ChatScene.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										570
									
								
								client/src/main/java/envoy/client/ui/controller/ChatScene.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,570 @@
 | 
			
		||||
package envoy.client.ui.controller;
 | 
			
		||||
 | 
			
		||||
import java.awt.Toolkit;
 | 
			
		||||
import java.awt.datatransfer.StringSelection;
 | 
			
		||||
import java.io.ByteArrayInputStream;
 | 
			
		||||
import java.io.File;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.nio.file.Files;
 | 
			
		||||
import java.util.Random;
 | 
			
		||||
import java.util.logging.Level;
 | 
			
		||||
import java.util.logging.Logger;
 | 
			
		||||
 | 
			
		||||
import javafx.animation.RotateTransition;
 | 
			
		||||
import javafx.application.Platform;
 | 
			
		||||
import javafx.collections.FXCollections;
 | 
			
		||||
import javafx.fxml.FXML;
 | 
			
		||||
import javafx.scene.Node;
 | 
			
		||||
import javafx.scene.control.*;
 | 
			
		||||
import javafx.scene.control.Alert.AlertType;
 | 
			
		||||
import javafx.scene.image.Image;
 | 
			
		||||
import javafx.scene.image.ImageView;
 | 
			
		||||
import javafx.scene.input.KeyCode;
 | 
			
		||||
import javafx.scene.input.KeyEvent;
 | 
			
		||||
import javafx.scene.layout.GridPane;
 | 
			
		||||
import javafx.scene.paint.Color;
 | 
			
		||||
import javafx.stage.FileChooser;
 | 
			
		||||
import javafx.util.Duration;
 | 
			
		||||
 | 
			
		||||
import envoy.client.data.*;
 | 
			
		||||
import envoy.client.data.audio.AudioRecorder;
 | 
			
		||||
import envoy.client.event.MessageCreationEvent;
 | 
			
		||||
import envoy.client.net.Client;
 | 
			
		||||
import envoy.client.net.WriteProxy;
 | 
			
		||||
import envoy.client.ui.IconUtil;
 | 
			
		||||
import envoy.client.ui.Restorable;
 | 
			
		||||
import envoy.client.ui.SceneContext;
 | 
			
		||||
import envoy.client.ui.listcell.ContactListCellFactory;
 | 
			
		||||
import envoy.client.ui.listcell.MessageControl;
 | 
			
		||||
import envoy.client.ui.listcell.MessageListCellFactory;
 | 
			
		||||
import envoy.data.*;
 | 
			
		||||
import envoy.data.Attachment.AttachmentType;
 | 
			
		||||
import envoy.event.*;
 | 
			
		||||
import envoy.event.contact.ContactOperation;
 | 
			
		||||
import envoy.exception.EnvoyException;
 | 
			
		||||
import envoy.util.EnvoyLog;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>ChatSceneController.java</strong><br>
 | 
			
		||||
 * Created: <strong>26.03.2020</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
public final class ChatScene implements Restorable {
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private GridPane scene;
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private Label contactLabel;
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private ListView<Message> messageList;
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private ListView<Chat> chatList;
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private Button postButton;
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private Button voiceButton;
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private Button attachmentButton;
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private Button settingsButton;
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private Button rotateButton;
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private TextArea messageTextArea;
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private Label remainingChars;
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private Label infoLabel;
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private MenuItem deleteContactMenuItem;
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private ImageView attachmentView;
 | 
			
		||||
 | 
			
		||||
	private LocalDB			localDB;
 | 
			
		||||
	private Client			client;
 | 
			
		||||
	private WriteProxy		writeProxy;
 | 
			
		||||
	private SceneContext	sceneContext;
 | 
			
		||||
 | 
			
		||||
	private Chat			currentChat;
 | 
			
		||||
	private AudioRecorder	recorder;
 | 
			
		||||
	private boolean			recording;
 | 
			
		||||
	private Attachment		pendingAttachment;
 | 
			
		||||
	private boolean			postingPermanentlyDisabled;
 | 
			
		||||
 | 
			
		||||
	private static final Settings	settings	= Settings.getInstance();
 | 
			
		||||
	private static final EventBus	eventBus	= EventBus.getInstance();
 | 
			
		||||
	private static final Logger		logger		= EnvoyLog.getLogger(ChatScene.class);
 | 
			
		||||
 | 
			
		||||
	private static final Image	DEFAULT_ATTACHMENT_VIEW_IMAGE	= IconUtil.loadIconThemeSensitive("attachment_present", 20);
 | 
			
		||||
	private static final int	MAX_MESSAGE_LENGTH				= 255;
 | 
			
		||||
	private static final int	DEFAULT_ICON_SIZE				= 16;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Initializes the appearance of certain visual components.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	@FXML
 | 
			
		||||
	private void initialize() {
 | 
			
		||||
 | 
			
		||||
		// Initialize message and user rendering
 | 
			
		||||
		messageList.setCellFactory(MessageListCellFactory::new);
 | 
			
		||||
		chatList.setCellFactory(ContactListCellFactory::new);
 | 
			
		||||
 | 
			
		||||
		settingsButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("settings", DEFAULT_ICON_SIZE)));
 | 
			
		||||
		voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE)));
 | 
			
		||||
		attachmentButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("attachment", DEFAULT_ICON_SIZE)));
 | 
			
		||||
		attachmentView.setImage(DEFAULT_ATTACHMENT_VIEW_IMAGE);
 | 
			
		||||
		rotateButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("rotate", (int) (DEFAULT_ICON_SIZE * 1.5))));
 | 
			
		||||
 | 
			
		||||
		// Listen to received messages
 | 
			
		||||
		eventBus.register(MessageCreationEvent.class, e -> {
 | 
			
		||||
			final var message = e.get();
 | 
			
		||||
			localDB.getChat(message instanceof GroupMessage ? message.getRecipientID() : message.getSenderID()).ifPresent(chat -> {
 | 
			
		||||
				chat.insert(message);
 | 
			
		||||
				if (chat.equals(currentChat)) {
 | 
			
		||||
					try {
 | 
			
		||||
						currentChat.read(writeProxy);
 | 
			
		||||
					} catch (final IOException e1) {
 | 
			
		||||
						logger.log(Level.WARNING, "Could not read current chat: ", e1);
 | 
			
		||||
					}
 | 
			
		||||
					Platform.runLater(() -> { messageList.refresh(); scrollToMessageListEnd(); });
 | 
			
		||||
				} else chat.incrementUnreadAmount();
 | 
			
		||||
				// Moving chat with most recent unreadMessages to the top
 | 
			
		||||
				Platform.runLater(() -> {
 | 
			
		||||
					chatList.getItems().remove(chat);
 | 
			
		||||
					chatList.getItems().add(0, chat);
 | 
			
		||||
					if (chat.equals(currentChat)) chatList.getSelectionModel().select(0);
 | 
			
		||||
					localDB.getChats().remove(chat);
 | 
			
		||||
					localDB.getChats().add(0, chat);
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		// Listen to message status changes
 | 
			
		||||
		eventBus.register(MessageStatusChange.class, e -> localDB.getMessage(e.getID()).ifPresent(message -> {
 | 
			
		||||
			message.setStatus(e.get());
 | 
			
		||||
			// Update UI if in current chat
 | 
			
		||||
			if (currentChat != null && message.getSenderID() == currentChat.getRecipient().getID()) Platform.runLater(messageList::refresh);
 | 
			
		||||
		}));
 | 
			
		||||
 | 
			
		||||
		eventBus.register(GroupMessageStatusChange.class, e -> localDB.getMessage(e.getID()).ifPresent(groupMessage -> {
 | 
			
		||||
			((GroupMessage) groupMessage).getMemberStatuses().replace(e.getMemberID(), e.get());
 | 
			
		||||
 | 
			
		||||
			// Update UI if in current chat
 | 
			
		||||
			if (currentChat != null && groupMessage.getRecipientID() == currentChat.getRecipient().getID()) Platform.runLater(messageList::refresh);
 | 
			
		||||
		}));
 | 
			
		||||
 | 
			
		||||
		// Listen to user status changes
 | 
			
		||||
		eventBus.register(UserStatusChange.class,
 | 
			
		||||
				e -> chatList.getItems()
 | 
			
		||||
					.stream()
 | 
			
		||||
					.filter(c -> c.getRecipient().getID() == e.getID())
 | 
			
		||||
					.findAny()
 | 
			
		||||
					.map(Chat::getRecipient)
 | 
			
		||||
					.ifPresent(u -> { ((User) u).setStatus(e.get()); Platform.runLater(chatList::refresh); }));
 | 
			
		||||
 | 
			
		||||
		// Listen to contacts changes
 | 
			
		||||
		eventBus.register(ContactOperation.class, e -> {
 | 
			
		||||
			final var contact = e.get();
 | 
			
		||||
			switch (e.getOperationType()) {
 | 
			
		||||
				case ADD:
 | 
			
		||||
					localDB.getUsers().put(contact.getName(), contact);
 | 
			
		||||
					Chat chat = contact instanceof User ? new Chat(contact) : new GroupChat(client.getSender(), contact);
 | 
			
		||||
					localDB.getChats().add(chat);
 | 
			
		||||
					Platform.runLater(() -> chatList.getItems().add(chat));
 | 
			
		||||
					break;
 | 
			
		||||
				case REMOVE:
 | 
			
		||||
					localDB.getUsers().remove(contact.getName());
 | 
			
		||||
					localDB.getChats().removeIf(c -> c.getRecipient().getID() == contact.getID());
 | 
			
		||||
					Platform.runLater(() -> chatList.getItems().removeIf(c -> c.getRecipient().getID() == contact.getID()));
 | 
			
		||||
					break;
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Initializes all necessary data via dependency injection-
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param sceneContext the scene context used to load other scenes
 | 
			
		||||
	 * @param localDB      the local database form which chats and users are loaded
 | 
			
		||||
	 * @param client       the client used to request ID generators
 | 
			
		||||
	 * @param writeProxy   the write proxy used to send messages and other data to
 | 
			
		||||
	 *                     the server
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public void initializeData(SceneContext sceneContext, LocalDB localDB, Client client, WriteProxy writeProxy) {
 | 
			
		||||
		this.sceneContext	= sceneContext;
 | 
			
		||||
		this.localDB		= localDB;
 | 
			
		||||
		this.client			= client;
 | 
			
		||||
		this.writeProxy		= writeProxy;
 | 
			
		||||
 | 
			
		||||
		chatList.setItems(FXCollections.observableList(localDB.getChats()));
 | 
			
		||||
		contactLabel.setText(localDB.getUser().getName());
 | 
			
		||||
		MessageControl.setUser(localDB.getUser());
 | 
			
		||||
		if (!client.isOnline()) updateInfoLabel("You are offline", "infoLabel-info");
 | 
			
		||||
 | 
			
		||||
		recorder = new AudioRecorder();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public void onRestore() { updateRemainingCharsLabel(); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Actions to perform when the list of contacts has been clicked.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	@FXML
 | 
			
		||||
	private void chatListClicked() {
 | 
			
		||||
		final Contact user = chatList.getSelectionModel().getSelectedItem().getRecipient();
 | 
			
		||||
		if (user != null && (currentChat == null || !user.equals(currentChat.getRecipient()))) {
 | 
			
		||||
 | 
			
		||||
			// LEON: JFC <===> JAVA FRIED CHICKEN <=/=> Java Foundation Classes
 | 
			
		||||
 | 
			
		||||
			// Load the chat
 | 
			
		||||
			currentChat = localDB.getChat(user.getID()).get();
 | 
			
		||||
 | 
			
		||||
			messageList.setItems(FXCollections.observableList(currentChat.getMessages()));
 | 
			
		||||
			final var scrollIndex = messageList.getItems().size() - currentChat.getUnreadAmount() - 1;
 | 
			
		||||
			messageList.scrollTo(scrollIndex);
 | 
			
		||||
			logger.log(Level.FINEST, "Loading chat with " + user + " at index " + scrollIndex);
 | 
			
		||||
			deleteContactMenuItem.setText("Delete " + user.getName());
 | 
			
		||||
 | 
			
		||||
			// Read the current chat
 | 
			
		||||
			try {
 | 
			
		||||
				currentChat.read(writeProxy);
 | 
			
		||||
			} catch (final IOException e) {
 | 
			
		||||
				logger.log(Level.WARNING, "Could not read current chat.", e);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Discard the pending attachment
 | 
			
		||||
			if (recorder.isRecording()) {
 | 
			
		||||
				recorder.cancel();
 | 
			
		||||
				recording = false;
 | 
			
		||||
			}
 | 
			
		||||
			pendingAttachment = null;
 | 
			
		||||
			updateAttachmentView(false);
 | 
			
		||||
 | 
			
		||||
			remainingChars.setVisible(true);
 | 
			
		||||
			remainingChars
 | 
			
		||||
				.setText(String.format("remaining chars: %d/%d", MAX_MESSAGE_LENGTH - messageTextArea.getText().length(), MAX_MESSAGE_LENGTH));
 | 
			
		||||
		}
 | 
			
		||||
		messageTextArea.setDisable(currentChat == null || postingPermanentlyDisabled);
 | 
			
		||||
		voiceButton.setDisable(!recorder.isSupported());
 | 
			
		||||
		attachmentButton.setDisable(false);
 | 
			
		||||
		chatList.refresh();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Actions to perform when the Settings Button has been clicked.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	@FXML
 | 
			
		||||
	private void settingsButtonClicked() {
 | 
			
		||||
		sceneContext.load(SceneContext.SceneInfo.SETTINGS_SCENE);
 | 
			
		||||
		sceneContext.<SettingsScene>getController().initializeData(sceneContext);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Actions to perform when the "Add Contact" - Button has been clicked.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	@FXML
 | 
			
		||||
	private void addContactButtonClicked() {
 | 
			
		||||
		sceneContext.load(SceneContext.SceneInfo.CONTACT_SEARCH_SCENE);
 | 
			
		||||
		sceneContext.<ContactSearchScene>getController().initializeData(sceneContext, localDB);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private void voiceButtonClicked() {
 | 
			
		||||
		new Thread(() -> {
 | 
			
		||||
			try {
 | 
			
		||||
				if (!recording) {
 | 
			
		||||
					recording = true;
 | 
			
		||||
					Platform.runLater(() -> {
 | 
			
		||||
						voiceButton.setText("Recording");
 | 
			
		||||
						voiceButton.setGraphic(new ImageView(IconUtil.loadIcon("microphone_recording", DEFAULT_ICON_SIZE)));
 | 
			
		||||
					});
 | 
			
		||||
					recorder.start();
 | 
			
		||||
				} else {
 | 
			
		||||
					pendingAttachment	= new Attachment(recorder.finish(), AttachmentType.VOICE);
 | 
			
		||||
					recording			= false;
 | 
			
		||||
					Platform.runLater(() -> {
 | 
			
		||||
						voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE)));
 | 
			
		||||
						voiceButton.setText(null);
 | 
			
		||||
						checkPostConditions(false);
 | 
			
		||||
						updateAttachmentView(true);
 | 
			
		||||
					});
 | 
			
		||||
				}
 | 
			
		||||
			} catch (final EnvoyException e) {
 | 
			
		||||
				logger.log(Level.SEVERE, "Could not record audio: ", e);
 | 
			
		||||
				Platform.runLater(new Alert(AlertType.ERROR, "Could not record audio")::showAndWait);
 | 
			
		||||
			}
 | 
			
		||||
		}).start();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private void attachmentButtonClicked() {
 | 
			
		||||
 | 
			
		||||
		// Display file chooser
 | 
			
		||||
		final var fileChooser = new FileChooser();
 | 
			
		||||
		fileChooser.setTitle("Add Attachment");
 | 
			
		||||
		fileChooser.setInitialDirectory(new File(System.getProperty("user.home")));
 | 
			
		||||
		fileChooser.getExtensionFilters()
 | 
			
		||||
			.addAll(new FileChooser.ExtensionFilter("Pictures", "*.png", "*.jpg", "*.bmp", "*.gif"),
 | 
			
		||||
					new FileChooser.ExtensionFilter("Videos", "*.mp4"),
 | 
			
		||||
					new FileChooser.ExtensionFilter("All Files", "*.*"));
 | 
			
		||||
		final var file = fileChooser.showOpenDialog(sceneContext.getStage());
 | 
			
		||||
 | 
			
		||||
		if (file != null) {
 | 
			
		||||
 | 
			
		||||
			// Check max file size
 | 
			
		||||
			if (file.length() > 16E6) {
 | 
			
		||||
				new Alert(AlertType.WARNING, "The selected file exceeds the size limit of 16MB!").showAndWait();
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Get attachment type (default is document)
 | 
			
		||||
			AttachmentType type = AttachmentType.DOCUMENT;
 | 
			
		||||
			switch (fileChooser.getSelectedExtensionFilter().getDescription()) {
 | 
			
		||||
				case "Pictures":
 | 
			
		||||
					type = AttachmentType.PICTURE;
 | 
			
		||||
					break;
 | 
			
		||||
				case "Videos":
 | 
			
		||||
					type = AttachmentType.VIDEO;
 | 
			
		||||
					break;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Create the pending attachment
 | 
			
		||||
			try {
 | 
			
		||||
				final var fileBytes = Files.readAllBytes(file.toPath());
 | 
			
		||||
				pendingAttachment = new Attachment(fileBytes, type);
 | 
			
		||||
				// Setting the preview image as image of the attachmentView
 | 
			
		||||
				if (type == AttachmentType.PICTURE)
 | 
			
		||||
					attachmentView.setImage(new Image(new ByteArrayInputStream(fileBytes), DEFAULT_ICON_SIZE, DEFAULT_ICON_SIZE, true, true));
 | 
			
		||||
				attachmentView.setVisible(true);
 | 
			
		||||
			} catch (final IOException e) {
 | 
			
		||||
				new Alert(AlertType.ERROR, "The selected file could not be loaded!").showAndWait();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Rotates every element in our application by 360° in at most 2.75s.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	@FXML
 | 
			
		||||
	private void doABarrelRoll() {
 | 
			
		||||
		// contains all Node objects in ChatScene in alphabetical order
 | 
			
		||||
		final var	rotatableNodes	= new Node[] { attachmentButton, attachmentView, contactLabel, infoLabel, messageList, messageTextArea,
 | 
			
		||||
				postButton, remainingChars, rotateButton, scene, settingsButton, userList, voiceButton };
 | 
			
		||||
		final var	random			= new Random();
 | 
			
		||||
		for (final var node : rotatableNodes) {
 | 
			
		||||
			// Defines at most four whole rotation in at most 4s
 | 
			
		||||
			final var rotateTransition = new RotateTransition(Duration.seconds(random.nextDouble() * 3 + 1), node);
 | 
			
		||||
			rotateTransition.setByAngle((random.nextInt(3) + 1) * 360);
 | 
			
		||||
			rotateTransition.play();
 | 
			
		||||
			// This is needed as for some strange reason objects could stop before being
 | 
			
		||||
			// rotated back to 0°
 | 
			
		||||
			rotateTransition.setOnFinished(e -> node.setRotate(0));
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Checks the text length of the {@code messageTextArea}, adjusts the
 | 
			
		||||
	 * {@code remainingChars} label and checks whether to send the message
 | 
			
		||||
	 * automatically.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param e the key event that will be analyzed for a post request
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	@FXML
 | 
			
		||||
	private void checkKeyCombination(KeyEvent e) {
 | 
			
		||||
		// Checks whether the text is too long
 | 
			
		||||
		messageTextUpdated();
 | 
			
		||||
		// Automatic sending of messages via (ctrl +) enter
 | 
			
		||||
		checkPostConditions(e);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @param e the keys that have been pressed
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	@FXML
 | 
			
		||||
	private void checkPostConditions(KeyEvent e) {
 | 
			
		||||
		checkPostConditions(settings.isEnterToSend() && e.getCode() == KeyCode.ENTER
 | 
			
		||||
				|| !settings.isEnterToSend() && e.getCode() == KeyCode.ENTER && e.isControlDown());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void checkPostConditions(boolean sendKeyPressed) {
 | 
			
		||||
		if (!postingPermanentlyDisabled) {
 | 
			
		||||
			if (!postButton.isDisabled() && sendKeyPressed) postMessage();
 | 
			
		||||
			postButton.setDisable(messageTextArea.getText().isBlank() && pendingAttachment == null || currentChat == null);
 | 
			
		||||
		} else {
 | 
			
		||||
			final var noMoreMessaging = "Go online to send messages";
 | 
			
		||||
			if (!infoLabel.getText().equals(noMoreMessaging))
 | 
			
		||||
				// Informing the user that he is a f*cking moron and should use Envoy online
 | 
			
		||||
				// because he ran out of messageIDs to use
 | 
			
		||||
				updateInfoLabel(noMoreMessaging, "infoLabel-error");
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Actions to perform when the text was updated in the messageTextArea.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	@FXML
 | 
			
		||||
	private void messageTextUpdated() {
 | 
			
		||||
		// Truncating messages that are too long and staying at the same position
 | 
			
		||||
		if (messageTextArea.getText().length() >= MAX_MESSAGE_LENGTH) {
 | 
			
		||||
			messageTextArea.setText(messageTextArea.getText().substring(0, MAX_MESSAGE_LENGTH));
 | 
			
		||||
			messageTextArea.positionCaret(MAX_MESSAGE_LENGTH);
 | 
			
		||||
			messageTextArea.setScrollTop(Double.MAX_VALUE);
 | 
			
		||||
		}
 | 
			
		||||
		updateRemainingCharsLabel();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Sets the text and text color of the {@code remainingChars} label.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	private void updateRemainingCharsLabel() {
 | 
			
		||||
		final int	currentLength	= messageTextArea.getText().length();
 | 
			
		||||
		final int	remainingLength	= MAX_MESSAGE_LENGTH - currentLength;
 | 
			
		||||
		remainingChars.setText(String.format("remaining chars: %d/%d", remainingLength, MAX_MESSAGE_LENGTH));
 | 
			
		||||
		remainingChars.setTextFill(Color.rgb(currentLength, remainingLength, 0, 1));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Sends a new {@link Message} or {@link GroupMessage} to the server based on
 | 
			
		||||
	 * the text entered in the {@code messageTextArea} and the given attachment.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	@FXML
 | 
			
		||||
	private void postMessage() {
 | 
			
		||||
		postingPermanentlyDisabled = !(client.isOnline() || localDB.getIDGenerator().hasNext());
 | 
			
		||||
		if (postingPermanentlyDisabled) {
 | 
			
		||||
			postButton.setDisable(true);
 | 
			
		||||
			messageTextArea.setDisable(true);
 | 
			
		||||
			messageTextArea.clear();
 | 
			
		||||
			updateInfoLabel("You need to go online to send more messages", "infoLabel-error");
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
		final var text = messageTextArea.getText().strip();
 | 
			
		||||
		try {
 | 
			
		||||
			// Creating the message and its metadata
 | 
			
		||||
			final var builder = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator())
 | 
			
		||||
				.setText(text);
 | 
			
		||||
			// Setting an attachment, if present
 | 
			
		||||
			if (pendingAttachment != null) {
 | 
			
		||||
				builder.setAttachment(pendingAttachment);
 | 
			
		||||
				pendingAttachment = null;
 | 
			
		||||
				updateAttachmentView(false);
 | 
			
		||||
			}
 | 
			
		||||
			// Building the final message
 | 
			
		||||
			final var message = currentChat.getRecipient() instanceof Group ? builder.buildGroupMessage((Group) currentChat.getRecipient())
 | 
			
		||||
					: builder.build();
 | 
			
		||||
 | 
			
		||||
			// Send message
 | 
			
		||||
			writeProxy.writeMessage(message);
 | 
			
		||||
 | 
			
		||||
			// Add message to LocalDB and update UI
 | 
			
		||||
			currentChat.insert(message);
 | 
			
		||||
			// Moving currentChat to the top
 | 
			
		||||
			Platform.runLater(() -> {
 | 
			
		||||
				chatList.getItems().remove(currentChat);
 | 
			
		||||
				chatList.getItems().add(0, currentChat);
 | 
			
		||||
				chatList.getSelectionModel().select(0);
 | 
			
		||||
				localDB.getChats().remove(currentChat);
 | 
			
		||||
				localDB.getChats().add(0, currentChat);
 | 
			
		||||
			});
 | 
			
		||||
			messageList.refresh();
 | 
			
		||||
			scrollToMessageListEnd();
 | 
			
		||||
 | 
			
		||||
			// Request a new ID generator if all IDs were used
 | 
			
		||||
			if (!localDB.getIDGenerator().hasNext() && client.isOnline()) client.requestIdGenerator();
 | 
			
		||||
 | 
			
		||||
		} catch (final IOException e) {
 | 
			
		||||
			logger.log(Level.SEVERE, "Error while sending message: ", e);
 | 
			
		||||
			new Alert(AlertType.ERROR, "An error occured while sending the message!").showAndWait();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Clear text field and disable post button
 | 
			
		||||
		messageTextArea.setText("");
 | 
			
		||||
		postButton.setDisable(true);
 | 
			
		||||
		updateRemainingCharsLabel();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Scrolls to the bottom of the {@code messageList}.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	private void scrollToMessageListEnd() { messageList.scrollTo(messageList.getItems().size() - 1); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Updates the {@code infoLabel}.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param text        the text to use
 | 
			
		||||
	 * @param infoLabelID the id the the {@code infoLabel} should have so that it
 | 
			
		||||
	 *                    can be styled accordingly in CSS
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	private void updateInfoLabel(String text, String infoLabelID) {
 | 
			
		||||
		infoLabel.setText(text);
 | 
			
		||||
		infoLabel.setId(infoLabelID);
 | 
			
		||||
		infoLabel.setVisible(true);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Updates the {@code attachmentView} in terms of visibility.<br>
 | 
			
		||||
	 * Additionally resets the shown image to
 | 
			
		||||
	 * {@code DEFAULT_ATTACHMENT_VIEW_IMAGE} if another image is currently
 | 
			
		||||
	 * present.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param visible whether the {@code attachmentView} should be displayed
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	private void updateAttachmentView(boolean visible) {
 | 
			
		||||
		if (!attachmentView.getImage().equals(DEFAULT_ATTACHMENT_VIEW_IMAGE)) attachmentView.setImage(DEFAULT_ATTACHMENT_VIEW_IMAGE);
 | 
			
		||||
		attachmentView.setVisible(visible);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Context menu actions
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private void deleteContact() { try {} catch (final NullPointerException e) {} }
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private void copyAndPostMessage() {
 | 
			
		||||
		final var messageText = messageTextArea.getText();
 | 
			
		||||
		Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(messageText), null);
 | 
			
		||||
		postMessage();
 | 
			
		||||
		messageTextArea.setText(messageText);
 | 
			
		||||
		updateRemainingCharsLabel();
 | 
			
		||||
		postButton.setDisable(messageText.isBlank());
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,130 @@
 | 
			
		||||
package envoy.client.ui.controller;
 | 
			
		||||
 | 
			
		||||
import java.util.logging.Level;
 | 
			
		||||
import java.util.logging.Logger;
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
 | 
			
		||||
import javafx.application.Platform;
 | 
			
		||||
import javafx.fxml.FXML;
 | 
			
		||||
import javafx.scene.control.Alert;
 | 
			
		||||
import javafx.scene.control.Alert.AlertType;
 | 
			
		||||
import javafx.scene.control.ButtonType;
 | 
			
		||||
import javafx.scene.control.ListView;
 | 
			
		||||
 | 
			
		||||
import envoy.client.data.Chat;
 | 
			
		||||
import envoy.client.data.LocalDB;
 | 
			
		||||
import envoy.client.event.SendEvent;
 | 
			
		||||
import envoy.client.ui.ClearableTextField;
 | 
			
		||||
import envoy.client.ui.SceneContext;
 | 
			
		||||
import envoy.client.ui.listcell.ContactListCellFactory;
 | 
			
		||||
import envoy.event.ElementOperation;
 | 
			
		||||
import envoy.event.EventBus;
 | 
			
		||||
import envoy.event.contact.ContactOperation;
 | 
			
		||||
import envoy.event.contact.ContactSearchRequest;
 | 
			
		||||
import envoy.event.contact.ContactSearchResult;
 | 
			
		||||
import envoy.util.EnvoyLog;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>ContactSearchSceneController.java</strong><br>
 | 
			
		||||
 * Created: <strong>07.06.2020</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Leon Hofmeister
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
public class ContactSearchScene {
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private ClearableTextField searchBar;
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private ListView<Chat> chatList;
 | 
			
		||||
 | 
			
		||||
	private SceneContext sceneContext;
 | 
			
		||||
 | 
			
		||||
	private LocalDB localDB;
 | 
			
		||||
 | 
			
		||||
	private static EventBus		eventBus	= EventBus.getInstance();
 | 
			
		||||
	private static final Logger	logger		= EnvoyLog.getLogger(ChatScene.class);
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @param sceneContext enables the user to return to the chat scene
 | 
			
		||||
	 * @param localDB      the local database to which new contacts are added
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public void initializeData(SceneContext sceneContext, LocalDB localDB) {
 | 
			
		||||
		this.sceneContext	= sceneContext;
 | 
			
		||||
		this.localDB		= localDB;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private void initialize() {
 | 
			
		||||
		chatList.setCellFactory(ContactListCellFactory::new);
 | 
			
		||||
		searchBar.setClearButtonListener(e -> { searchBar.getTextField().clear(); chatList.getItems().clear(); });
 | 
			
		||||
		eventBus.register(ContactSearchResult.class,
 | 
			
		||||
				response -> Platform.runLater(() -> {
 | 
			
		||||
					chatList.getItems().clear();
 | 
			
		||||
					chatList.getItems().addAll(response.get().stream().map(Chat::new).collect(Collectors.toList()));
 | 
			
		||||
				}));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Disables the clear and search button if no text is present in the search bar.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	@FXML
 | 
			
		||||
	private void sendRequest() {
 | 
			
		||||
		final var text = searchBar.getTextField().getText().strip();
 | 
			
		||||
		if (!text.isBlank()) eventBus.dispatch(new SendEvent(new ContactSearchRequest(text)));
 | 
			
		||||
		else chatList.getItems().clear();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Clears the text in the search bar and the items shown in the list.
 | 
			
		||||
	 * Additionally disables both clear and search button.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	@FXML
 | 
			
		||||
	private void clear() {
 | 
			
		||||
		searchBar.getTextField().setText(null);
 | 
			
		||||
		chatList.getItems().clear();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Sends an {@link ContactOperation} for every selected contact to the
 | 
			
		||||
	 * server.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	@FXML
 | 
			
		||||
	private void chatListClicked() {
 | 
			
		||||
		final var chat = chatList.getSelectionModel().getSelectedItem();
 | 
			
		||||
		if (chat != null) {
 | 
			
		||||
			final var alert = new Alert(AlertType.CONFIRMATION);
 | 
			
		||||
			alert.setTitle("Add Contact to Contact List");
 | 
			
		||||
			alert.setHeaderText("Add the user " + chat.getRecipient().getName() + " to your contact list?");
 | 
			
		||||
			// Normally, this would be total BS (we are already on the FX Thread), however
 | 
			
		||||
			// it could be proven that the creation of this dialog wrapped in
 | 
			
		||||
			// Platform.runLater is less error-prone than without it
 | 
			
		||||
			Platform.runLater(() -> alert.showAndWait().filter(btn -> btn == ButtonType.OK).ifPresent(btn -> {
 | 
			
		||||
				final var event = new ContactOperation(chat.getRecipient(), ElementOperation.ADD);
 | 
			
		||||
				// Sends the event to the server
 | 
			
		||||
				eventBus.dispatch(new SendEvent(event));
 | 
			
		||||
				// Updates the UI
 | 
			
		||||
				eventBus.dispatch(event);
 | 
			
		||||
				logger.log(Level.INFO, "Added contact " + chat.getRecipient());
 | 
			
		||||
			}));
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private void newGroupButtonClicked() {
 | 
			
		||||
		sceneContext.load(SceneContext.SceneInfo.GROUP_CREATION_SCENE);
 | 
			
		||||
		sceneContext.<GroupCreationScene>getController().initializeData(sceneContext, localDB);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private void backButtonClicked() { sceneContext.pop(); }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,109 @@
 | 
			
		||||
package envoy.client.ui.controller;
 | 
			
		||||
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
 | 
			
		||||
import javafx.application.Platform;
 | 
			
		||||
import javafx.fxml.FXML;
 | 
			
		||||
import javafx.scene.control.*;
 | 
			
		||||
import javafx.scene.control.Alert.AlertType;
 | 
			
		||||
 | 
			
		||||
import envoy.client.data.Chat;
 | 
			
		||||
import envoy.client.data.LocalDB;
 | 
			
		||||
import envoy.client.event.SendEvent;
 | 
			
		||||
import envoy.client.ui.ClearableTextField;
 | 
			
		||||
import envoy.client.ui.SceneContext;
 | 
			
		||||
import envoy.client.ui.listcell.ContactListCellFactory;
 | 
			
		||||
import envoy.data.Group;
 | 
			
		||||
import envoy.event.EventBus;
 | 
			
		||||
import envoy.event.GroupCreation;
 | 
			
		||||
import envoy.util.Bounds;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>ContactSearchSceneController.java</strong><br>
 | 
			
		||||
 * Created: <strong>07.06.2020</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Maximilian Käfer
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
public class GroupCreationScene {
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private Button createButton;
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private ClearableTextField groupNameField;
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private ListView<Chat> chatList;
 | 
			
		||||
 | 
			
		||||
	private SceneContext sceneContext;
 | 
			
		||||
 | 
			
		||||
	private static final EventBus eventBus = EventBus.getInstance();
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private void initialize() {
 | 
			
		||||
		chatList.setCellFactory(ContactListCellFactory::new);
 | 
			
		||||
		chatList.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
 | 
			
		||||
		groupNameField.setClearButtonListener(e -> { groupNameField.getTextField().clear(); createButton.setDisable(true); });
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @param sceneContext enables the user to return to the chat scene
 | 
			
		||||
	 * @param localDB      the local database from which potential group members can
 | 
			
		||||
	 *                     be selected
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public void initializeData(SceneContext sceneContext, LocalDB localDB) {
 | 
			
		||||
		this.sceneContext = sceneContext;
 | 
			
		||||
		Platform.runLater(() -> chatList.getItems()
 | 
			
		||||
			.addAll(localDB.getChats()
 | 
			
		||||
				.stream()
 | 
			
		||||
				.filter(c -> !(c.getRecipient() instanceof Group))
 | 
			
		||||
				.filter(c -> c.getRecipient().getID() != localDB.getUser().getID())
 | 
			
		||||
				.collect(Collectors.toList())));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Enables the {@code createButton} if at least one contact is selected.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	@FXML
 | 
			
		||||
	private void chatListClicked() {
 | 
			
		||||
		createButton.setDisable(chatList.getSelectionModel().isEmpty() || groupNameField.getTextField().getText().isBlank());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Checks, whether the {@code createButton} can be enabled because text is
 | 
			
		||||
	 * present in the textfield.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	@FXML
 | 
			
		||||
	private void textUpdated() { createButton.setDisable(groupNameField.getTextField().getText().isBlank()); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Sends a {@link GroupCreation} to the server and closes this scene.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * If the given group name is not valid, an error is displayed instead.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	@FXML
 | 
			
		||||
	private void createButtonClicked() {
 | 
			
		||||
		final var name = groupNameField.getTextField().getText();
 | 
			
		||||
		if (!Bounds.isValidContactName(name)) {
 | 
			
		||||
			new Alert(AlertType.ERROR, "The entered group name is not valid (" + Bounds.CONTACT_NAME_PATTERN + ")").showAndWait();
 | 
			
		||||
			groupNameField.getTextField().clear();
 | 
			
		||||
		} else {
 | 
			
		||||
			eventBus.dispatch(new SendEvent(new GroupCreation(name,
 | 
			
		||||
					chatList.getSelectionModel().getSelectedItems().stream().map(c -> c.getRecipient().getID()).collect(Collectors.toSet()))));
 | 
			
		||||
			new Alert(AlertType.INFORMATION, String.format("Group '%s' successfully created.", name)).showAndWait();
 | 
			
		||||
			sceneContext.pop();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private void backButtonClicked() { sceneContext.pop(); }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										199
									
								
								client/src/main/java/envoy/client/ui/controller/LoginScene.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										199
									
								
								client/src/main/java/envoy/client/ui/controller/LoginScene.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,199 @@
 | 
			
		||||
package envoy.client.ui.controller;
 | 
			
		||||
 | 
			
		||||
import java.io.FileNotFoundException;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.util.concurrent.TimeoutException;
 | 
			
		||||
import java.util.logging.Level;
 | 
			
		||||
import java.util.logging.Logger;
 | 
			
		||||
 | 
			
		||||
import javafx.application.Platform;
 | 
			
		||||
import javafx.fxml.FXML;
 | 
			
		||||
import javafx.scene.control.*;
 | 
			
		||||
import javafx.scene.control.Alert.AlertType;
 | 
			
		||||
 | 
			
		||||
import envoy.client.data.*;
 | 
			
		||||
import envoy.client.net.Client;
 | 
			
		||||
import envoy.client.net.WriteProxy;
 | 
			
		||||
import envoy.client.ui.ClearableTextField;
 | 
			
		||||
import envoy.client.ui.SceneContext;
 | 
			
		||||
import envoy.client.ui.Startup;
 | 
			
		||||
import envoy.data.LoginCredentials;
 | 
			
		||||
import envoy.data.User;
 | 
			
		||||
import envoy.data.User.UserStatus;
 | 
			
		||||
import envoy.event.EventBus;
 | 
			
		||||
import envoy.event.HandshakeRejection;
 | 
			
		||||
import envoy.exception.EnvoyException;
 | 
			
		||||
import envoy.util.Bounds;
 | 
			
		||||
import envoy.util.EnvoyLog;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>LoginDialog.java</strong><br>
 | 
			
		||||
 * Created: <strong>03.04.2020</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @author Maximilian Käfer
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
public final class LoginScene {
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private ClearableTextField userTextField;
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private PasswordField passwordField;
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private PasswordField repeatPasswordField;
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private Label repeatPasswordLabel;
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private CheckBox registerCheckBox;
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private Label connectionLabel;
 | 
			
		||||
 | 
			
		||||
	private Client			client;
 | 
			
		||||
	private LocalDB			localDB;
 | 
			
		||||
	private CacheMap		cacheMap;
 | 
			
		||||
	private SceneContext	sceneContext;
 | 
			
		||||
 | 
			
		||||
	private static final Logger			logger		= EnvoyLog.getLogger(LoginScene.class);
 | 
			
		||||
	private static final EventBus		eventBus	= EventBus.getInstance();
 | 
			
		||||
	private static final ClientConfig	config		= ClientConfig.getInstance();
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private void initialize() {
 | 
			
		||||
		connectionLabel.setText("Server: " + config.getServer() + ":" + config.getPort());
 | 
			
		||||
 | 
			
		||||
		// Show an alert after an unsuccessful handshake
 | 
			
		||||
		eventBus.register(HandshakeRejection.class, e -> Platform.runLater(() -> { new Alert(AlertType.ERROR, e.get()).showAndWait(); }));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Loads the login dialog using the FXML file {@code LoginDialog.fxml}.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param client       the client used to perform the handshake
 | 
			
		||||
	 * @param localDB      the local database used for offline login
 | 
			
		||||
	 * @param cacheMap     the map of all caches needed
 | 
			
		||||
	 * @param sceneContext the scene context used to initialize the chat scene
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public void initializeData(Client client, LocalDB localDB, CacheMap cacheMap, SceneContext sceneContext) {
 | 
			
		||||
		this.client			= client;
 | 
			
		||||
		this.localDB		= localDB;
 | 
			
		||||
		this.cacheMap		= cacheMap;
 | 
			
		||||
		this.sceneContext	= sceneContext;
 | 
			
		||||
 | 
			
		||||
		// Prepare handshake
 | 
			
		||||
		localDB.loadIDGenerator();
 | 
			
		||||
 | 
			
		||||
		// Set initial cursor
 | 
			
		||||
		userTextField.requestFocus();
 | 
			
		||||
 | 
			
		||||
		// Perform automatic login if configured
 | 
			
		||||
		if (config.hasLoginCredentials()) performHandshake(config.getLoginCredentials());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private void loginButtonPressed() {
 | 
			
		||||
 | 
			
		||||
		// Prevent registration with unequal passwords
 | 
			
		||||
		if (registerCheckBox.isSelected() && !passwordField.getText().equals(repeatPasswordField.getText())) {
 | 
			
		||||
			new Alert(AlertType.ERROR, "The entered password is unequal to the repeated one").showAndWait();
 | 
			
		||||
			repeatPasswordField.clear();
 | 
			
		||||
		} else if (!Bounds.isValidContactName(userTextField.getTextField().getText())) {
 | 
			
		||||
			new Alert(AlertType.ERROR, "The entered user name is not valid (" + Bounds.CONTACT_NAME_PATTERN + ")").showAndWait();
 | 
			
		||||
			userTextField.getTextField().clear();
 | 
			
		||||
		} else performHandshake(new LoginCredentials(userTextField.getTextField().getText(), passwordField.getText(), registerCheckBox.isSelected(),
 | 
			
		||||
				Startup.VERSION));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private void offlineModeButtonPressed() {
 | 
			
		||||
		attemptOfflineMode(new LoginCredentials(userTextField.getTextField().getText(), passwordField.getText(), false, Startup.VERSION));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private void registerCheckboxChanged() {
 | 
			
		||||
 | 
			
		||||
		// Make repeat password field and label visible / invisible
 | 
			
		||||
		repeatPasswordField.setVisible(registerCheckBox.isSelected());
 | 
			
		||||
		repeatPasswordLabel.setVisible(registerCheckBox.isSelected());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private void abortLogin() {
 | 
			
		||||
		logger.log(Level.INFO, "The login process has been cancelled. Exiting...");
 | 
			
		||||
		System.exit(0);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void performHandshake(LoginCredentials credentials) {
 | 
			
		||||
		try {
 | 
			
		||||
			client.performHandshake(credentials, cacheMap);
 | 
			
		||||
			if (client.isOnline()) {
 | 
			
		||||
				loadChatScene();
 | 
			
		||||
				client.initReceiver(localDB, cacheMap);
 | 
			
		||||
			}
 | 
			
		||||
		} catch (IOException | InterruptedException | TimeoutException e) {
 | 
			
		||||
			logger.log(Level.INFO, "Could not connect to server. Entering offline mode...");
 | 
			
		||||
			attemptOfflineMode(credentials);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void attemptOfflineMode(LoginCredentials credentials) {
 | 
			
		||||
		try {
 | 
			
		||||
			// Try entering offline mode
 | 
			
		||||
			localDB.loadUsers();
 | 
			
		||||
			final User clientUser = (User) localDB.getUsers().get(credentials.getIdentifier());
 | 
			
		||||
			if (clientUser == null) throw new EnvoyException("Could not enter offline mode: user name unknown");
 | 
			
		||||
			client.setSender(clientUser);
 | 
			
		||||
			loadChatScene();
 | 
			
		||||
		} catch (final Exception e) {
 | 
			
		||||
			new Alert(AlertType.ERROR, "Client error: " + e).showAndWait();
 | 
			
		||||
			logger.log(Level.SEVERE, "Offline mode could not be loaded: ", e);
 | 
			
		||||
			System.exit(1);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void loadChatScene() {
 | 
			
		||||
 | 
			
		||||
		// Set client user in local database
 | 
			
		||||
		localDB.setUser(client.getSender());
 | 
			
		||||
 | 
			
		||||
		// Initialize chats in local database
 | 
			
		||||
		try {
 | 
			
		||||
			localDB.initializeUserStorage();
 | 
			
		||||
			localDB.loadUserData();
 | 
			
		||||
		} catch (final FileNotFoundException e) {
 | 
			
		||||
			// The local database file has not yet been created, probably first login
 | 
			
		||||
		} catch (final Exception e) {
 | 
			
		||||
			new Alert(AlertType.ERROR, "Error while loading local database: " + e + "\nChats will not be stored locally.").showAndWait();
 | 
			
		||||
			logger.log(Level.WARNING, "Could not load local database: ", e);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Initialize write proxy
 | 
			
		||||
		final var writeProxy = new WriteProxy(client, localDB);
 | 
			
		||||
 | 
			
		||||
		localDB.synchronize();
 | 
			
		||||
 | 
			
		||||
		if (client.isOnline()) writeProxy.flushCache();
 | 
			
		||||
		else
 | 
			
		||||
			// Set all contacts to offline mode
 | 
			
		||||
			localDB.getChats()
 | 
			
		||||
				.stream()
 | 
			
		||||
				.map(Chat::getRecipient)
 | 
			
		||||
				.filter(User.class::isInstance)
 | 
			
		||||
				.map(User.class::cast)
 | 
			
		||||
				.forEach(u -> u.setStatus(UserStatus.OFFLINE));
 | 
			
		||||
 | 
			
		||||
		// Load ChatScene
 | 
			
		||||
		sceneContext.pop();
 | 
			
		||||
		sceneContext.getStage().setMinHeight(400);
 | 
			
		||||
		sceneContext.getStage().setMinWidth(350);
 | 
			
		||||
		sceneContext.load(SceneContext.SceneInfo.CHAT_SCENE);
 | 
			
		||||
		sceneContext.<ChatScene>getController().initializeData(sceneContext, localDB, client, writeProxy);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,59 @@
 | 
			
		||||
package envoy.client.ui.controller;
 | 
			
		||||
 | 
			
		||||
import javafx.fxml.FXML;
 | 
			
		||||
import javafx.scene.control.*;
 | 
			
		||||
 | 
			
		||||
import envoy.client.ui.SceneContext;
 | 
			
		||||
import envoy.client.ui.settings.GeneralSettingsPane;
 | 
			
		||||
import envoy.client.ui.settings.SettingsPane;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>SettingsSceneController.java</strong><br>
 | 
			
		||||
 * Created: <strong>10.04.2020</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
public class SettingsScene {
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private ListView<SettingsPane> settingsList;
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private TitledPane titledPane;
 | 
			
		||||
 | 
			
		||||
	private SceneContext sceneContext;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @param sceneContext enables the user to return to the chat scene
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public void initializeData(SceneContext sceneContext) { this.sceneContext = sceneContext; }
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private void initialize() {
 | 
			
		||||
		settingsList.setCellFactory(listView -> new ListCell<>() {
 | 
			
		||||
 | 
			
		||||
			@Override
 | 
			
		||||
			protected void updateItem(SettingsPane item, boolean empty) {
 | 
			
		||||
				super.updateItem(item, empty);
 | 
			
		||||
				if (!empty && item != null) setGraphic(new Label(item.getTitle()));
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		settingsList.getItems().add(new GeneralSettingsPane());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private void settingsListClicked() {
 | 
			
		||||
		final var pane = settingsList.getSelectionModel().getSelectedItem();
 | 
			
		||||
		if (pane != null) {
 | 
			
		||||
			titledPane.setText(pane.getTitle());
 | 
			
		||||
			titledPane.setContent(pane);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@FXML
 | 
			
		||||
	private void backButtonClicked() { sceneContext.pop(); }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,11 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Contains JavaFX scene controllers.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>package-info.java</strong><br>
 | 
			
		||||
 * Created: <strong>08.06.2020</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
package envoy.client.ui.controller;
 | 
			
		||||
@@ -0,0 +1,58 @@
 | 
			
		||||
package envoy.client.ui.listcell;
 | 
			
		||||
 | 
			
		||||
import javafx.geometry.Pos;
 | 
			
		||||
import javafx.scene.control.Label;
 | 
			
		||||
import javafx.scene.layout.*;
 | 
			
		||||
 | 
			
		||||
import envoy.client.data.Chat;
 | 
			
		||||
import envoy.data.Contact;
 | 
			
		||||
import envoy.data.Group;
 | 
			
		||||
import envoy.data.User;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This class formats a single {@link Contact} into a UI component.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>ContactControl.java</strong><br>
 | 
			
		||||
 * Created: <strong>01.07.2020</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Leon Hofmeister
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
public class ChatControl extends HBox {
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @param chat the chat to display
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public ChatControl(Chat chat) {
 | 
			
		||||
		// Container with contact name
 | 
			
		||||
		final var	vBox		= new VBox();
 | 
			
		||||
		final var	nameLabel	= new Label(chat.getRecipient().getName());
 | 
			
		||||
		nameLabel.setWrapText(true);
 | 
			
		||||
		vBox.getChildren().add(nameLabel);
 | 
			
		||||
		if (chat.getRecipient() instanceof User) {
 | 
			
		||||
			// Online status
 | 
			
		||||
			final var	user		= (User) chat.getRecipient();
 | 
			
		||||
			final var	statusLabel	= new Label(user.getStatus().toString());
 | 
			
		||||
			statusLabel.getStyleClass().add(user.getStatus().toString().toLowerCase());
 | 
			
		||||
			vBox.getChildren().add(statusLabel);
 | 
			
		||||
		} else // Member count
 | 
			
		||||
			vBox.getChildren().add(new Label(((Group) chat.getRecipient()).getContacts().size() + " members"));
 | 
			
		||||
				
 | 
			
		||||
		getChildren().add(vBox);
 | 
			
		||||
		if (chat.getUnreadAmount() != 0) {
 | 
			
		||||
			Region spacing = new Region();
 | 
			
		||||
			setHgrow(spacing, Priority.ALWAYS);
 | 
			
		||||
			getChildren().add(spacing);
 | 
			
		||||
			final var unreadMessagesLabel = new Label(Integer.toString(chat.getUnreadAmount()));
 | 
			
		||||
			unreadMessagesLabel.setMinSize(15, 15);
 | 
			
		||||
			var vBox2 = new VBox();
 | 
			
		||||
			vBox2.setAlignment(Pos.CENTER_RIGHT);
 | 
			
		||||
			unreadMessagesLabel.setAlignment(Pos.CENTER);
 | 
			
		||||
			unreadMessagesLabel.getStyleClass().add("unreadMessagesAmount");
 | 
			
		||||
			vBox2.getChildren().add(unreadMessagesLabel);
 | 
			
		||||
			getChildren().add(vBox2);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,44 @@
 | 
			
		||||
package envoy.client.ui.listcell;
 | 
			
		||||
 | 
			
		||||
import javafx.scene.control.ListCell;
 | 
			
		||||
import javafx.scene.control.ListView;
 | 
			
		||||
 | 
			
		||||
import envoy.client.data.Chat;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>UserListCell.java</strong><br>
 | 
			
		||||
 * Created: <strong>28.03.2020</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
public class ContactListCellFactory extends ListCell<Chat> {
 | 
			
		||||
 | 
			
		||||
	private final ListView<Chat> listView;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @param listView the list view inside which this cell is contained
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public ContactListCellFactory(ListView<Chat> listView) { this.listView = listView; }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Displays the name of a contact. If the contact is a user, their online status
 | 
			
		||||
	 * is displayed as well.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	@Override
 | 
			
		||||
	protected void updateItem(Chat chat, boolean empty) {
 | 
			
		||||
		super.updateItem(chat, empty);
 | 
			
		||||
		if (empty || chat.getRecipient() == null) {
 | 
			
		||||
			setText(null);
 | 
			
		||||
			setGraphic(null);
 | 
			
		||||
		} else {
 | 
			
		||||
			final var control = new ChatControl(chat);
 | 
			
		||||
			prefWidthProperty().bind(listView.widthProperty().subtract(40));
 | 
			
		||||
			setGraphic(control);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,123 @@
 | 
			
		||||
package envoy.client.ui.listcell;
 | 
			
		||||
 | 
			
		||||
import java.awt.Toolkit;
 | 
			
		||||
import java.awt.datatransfer.StringSelection;
 | 
			
		||||
import java.io.ByteArrayInputStream;
 | 
			
		||||
import java.time.format.DateTimeFormatter;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.logging.Level;
 | 
			
		||||
import java.util.logging.Logger;
 | 
			
		||||
 | 
			
		||||
import javafx.geometry.Insets;
 | 
			
		||||
import javafx.scene.control.ContextMenu;
 | 
			
		||||
import javafx.scene.control.Label;
 | 
			
		||||
import javafx.scene.control.MenuItem;
 | 
			
		||||
import javafx.scene.image.Image;
 | 
			
		||||
import javafx.scene.image.ImageView;
 | 
			
		||||
import javafx.scene.layout.VBox;
 | 
			
		||||
 | 
			
		||||
import envoy.client.ui.AudioControl;
 | 
			
		||||
import envoy.client.ui.IconUtil;
 | 
			
		||||
import envoy.data.Message;
 | 
			
		||||
import envoy.data.Message.MessageStatus;
 | 
			
		||||
import envoy.data.User;
 | 
			
		||||
import envoy.util.EnvoyLog;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This class formats a single {@link Message} into a UI component.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>MessageControl.java</strong><br>
 | 
			
		||||
 * Created: <strong>01.07.2020</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Leon Hofmeister
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
public class MessageControl extends Label {
 | 
			
		||||
 | 
			
		||||
	private static User								client;
 | 
			
		||||
	private static final DateTimeFormatter			dateFormat		= DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
 | 
			
		||||
	private static final Map<MessageStatus, Image>	statusImages	= IconUtil.loadByEnum(MessageStatus.class, 16);
 | 
			
		||||
 | 
			
		||||
	private static final Logger logger = EnvoyLog.getLogger(MessageControl.class);
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param message the message that should be formatted
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public MessageControl(Message message) {
 | 
			
		||||
		// Creating the underlying VBox and the dateLabel
 | 
			
		||||
		final var vbox = new VBox(new Label(dateFormat.format(message.getCreationDate())));
 | 
			
		||||
 | 
			
		||||
		// Creating the actions for the MenuItems
 | 
			
		||||
		final ContextMenu	contextMenu		= new ContextMenu();
 | 
			
		||||
		final MenuItem		copyMenuItem	= new MenuItem("Copy");
 | 
			
		||||
		final MenuItem		deleteMenuItem	= new MenuItem("Delete");
 | 
			
		||||
		final MenuItem		forwardMenuItem	= new MenuItem("Forward");
 | 
			
		||||
		final MenuItem		quoteMenuItem	= new MenuItem("Quote");
 | 
			
		||||
		final MenuItem		infoMenuItem	= new MenuItem("Info");
 | 
			
		||||
		copyMenuItem.setOnAction(e -> copyMessage(message));
 | 
			
		||||
		deleteMenuItem.setOnAction(e -> deleteMessage(message));
 | 
			
		||||
		forwardMenuItem.setOnAction(e -> forwardMessage(message));
 | 
			
		||||
		quoteMenuItem.setOnAction(e -> quoteMessage(message));
 | 
			
		||||
		infoMenuItem.setOnAction(e -> loadMessageInfoScene(message));
 | 
			
		||||
		contextMenu.getItems().addAll(copyMenuItem, deleteMenuItem, forwardMenuItem, quoteMenuItem, infoMenuItem);
 | 
			
		||||
 | 
			
		||||
		// Handling message attachment display
 | 
			
		||||
		if (message.hasAttachment()) {
 | 
			
		||||
			switch (message.getAttachment().getType()) {
 | 
			
		||||
				case PICTURE:
 | 
			
		||||
					vbox.getChildren().add(new ImageView(new Image(new ByteArrayInputStream(message.getAttachment().getData()), 256, 256, true, true)));
 | 
			
		||||
					break;
 | 
			
		||||
				case VIDEO:
 | 
			
		||||
					break;
 | 
			
		||||
				case VOICE:
 | 
			
		||||
					vbox.getChildren().add(new AudioControl(message.getAttachment().getData()));
 | 
			
		||||
					break;
 | 
			
		||||
				case DOCUMENT:
 | 
			
		||||
					break;
 | 
			
		||||
			}
 | 
			
		||||
			final var saveAttachment = new MenuItem("Save attachment");
 | 
			
		||||
			saveAttachment.setOnAction(e -> saveAttachment(message));
 | 
			
		||||
			contextMenu.getItems().add(saveAttachment);
 | 
			
		||||
		}
 | 
			
		||||
		// Creating the textLabel
 | 
			
		||||
		final var textLabel = new Label(message.getText());
 | 
			
		||||
		textLabel.setWrapText(true);
 | 
			
		||||
		vbox.getChildren().add(textLabel);
 | 
			
		||||
		// Setting the message status icon and background color
 | 
			
		||||
		if (message.getSenderID() == client.getID()) {
 | 
			
		||||
			final var statusIcon = new ImageView(statusImages.get(message.getStatus()));
 | 
			
		||||
			statusIcon.setPreserveRatio(true);
 | 
			
		||||
			vbox.getChildren().add(statusIcon);
 | 
			
		||||
			getStyleClass().add("own-message");
 | 
			
		||||
		} else getStyleClass().add("received-message");
 | 
			
		||||
		// Adjusting height and weight of the cell to the corresponding ListView
 | 
			
		||||
		paddingProperty().setValue(new Insets(5, 20, 5, 20));
 | 
			
		||||
		setContextMenu(contextMenu);
 | 
			
		||||
		setGraphic(vbox);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Context Menu actions
 | 
			
		||||
 | 
			
		||||
	private void copyMessage(Message message) {
 | 
			
		||||
		Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(message.getText()), null);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void deleteMessage(Message message) { logger.log(Level.FINEST, "message deletion was requested for " + message); }
 | 
			
		||||
 | 
			
		||||
	private void forwardMessage(Message message) { logger.log(Level.FINEST, "message forwarding was requested for " + message); }
 | 
			
		||||
 | 
			
		||||
	private void quoteMessage(Message message) { logger.log(Level.FINEST, "message quotation was requested for " + message); }
 | 
			
		||||
 | 
			
		||||
	private void loadMessageInfoScene(Message message) { logger.log(Level.FINEST, "message info scene was requested for " + message); }
 | 
			
		||||
 | 
			
		||||
	private void saveAttachment(Message message) { logger.log(Level.FINEST, "attachment saving was requested for " + message); }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @param client the user who has logged in
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public static void setUser(User client) { MessageControl.client = client; }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,52 @@
 | 
			
		||||
package envoy.client.ui.listcell;
 | 
			
		||||
 | 
			
		||||
import javafx.scene.control.ListCell;
 | 
			
		||||
import javafx.scene.control.ListView;
 | 
			
		||||
import javafx.scene.control.Tooltip;
 | 
			
		||||
import javafx.stage.PopupWindow.AnchorLocation;
 | 
			
		||||
 | 
			
		||||
import envoy.data.Message;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Displays a single message inside the message list.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>MessageListCellFactory.java</strong><br>
 | 
			
		||||
 * Created: <strong>28.03.2020</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
public class MessageListCellFactory extends ListCell<Message> {
 | 
			
		||||
 | 
			
		||||
	private final ListView<Message> listView;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @param listView the list view inside which this cell is contained
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public MessageListCellFactory(ListView<Message> listView) { this.listView = listView; }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Displays the text, the data of creation and the status of a message.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	@Override
 | 
			
		||||
	protected void updateItem(Message message, boolean empty) {
 | 
			
		||||
		super.updateItem(message, empty);
 | 
			
		||||
		if (empty || message == null) {
 | 
			
		||||
			setText(null);
 | 
			
		||||
			setGraphic(null);
 | 
			
		||||
		} else {
 | 
			
		||||
			final var control = new MessageControl(message);
 | 
			
		||||
			control.prefWidthProperty().bind(listView.widthProperty().subtract(40));
 | 
			
		||||
			// Creating the Tooltip to deselect a message
 | 
			
		||||
			final var tooltip = new Tooltip("You can select a message by clicking on it \nand deselect it by pressing \"ctrl\" and clicking on it");
 | 
			
		||||
			tooltip.setWrapText(true);
 | 
			
		||||
			tooltip.setAnchorLocation(AnchorLocation.WINDOW_TOP_LEFT);
 | 
			
		||||
			setTooltip(tooltip);
 | 
			
		||||
			setGraphic(control);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,12 @@
 | 
			
		||||
/**
 | 
			
		||||
 * This package contains custom list cells that are used to display certain
 | 
			
		||||
 * things.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>package-info.java</strong><br>
 | 
			
		||||
 * Created: <strong>30.06.2020</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Leon Hofmeister
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
package envoy.client.ui.listcell;
 | 
			
		||||
							
								
								
									
										9
									
								
								client/src/main/java/envoy/client/ui/package-info.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								client/src/main/java/envoy/client/ui/package-info.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
/**
 | 
			
		||||
 * This package contains classes defining the user interface.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Leon Hofmeister
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @author Maximilian Käfer
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
package envoy.client.ui;
 | 
			
		||||
@@ -0,0 +1,57 @@
 | 
			
		||||
package envoy.client.ui.settings;
 | 
			
		||||
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
import javafx.scene.control.ComboBox;
 | 
			
		||||
import javafx.scene.layout.VBox;
 | 
			
		||||
 | 
			
		||||
import envoy.client.data.Settings;
 | 
			
		||||
import envoy.client.data.SettingsItem;
 | 
			
		||||
import envoy.client.event.ThemeChangeEvent;
 | 
			
		||||
import envoy.data.User.UserStatus;
 | 
			
		||||
import envoy.event.EventBus;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>GeneralSettingsPane.java</strong><br>
 | 
			
		||||
 * Created: <strong>18.04.2020</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
public class GeneralSettingsPane extends SettingsPane {
 | 
			
		||||
 | 
			
		||||
	private static final Settings settings = Settings.getInstance();
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public GeneralSettingsPane() {
 | 
			
		||||
		super("General");
 | 
			
		||||
		final var vbox = new VBox();
 | 
			
		||||
 | 
			
		||||
		// TODO: Support other value types
 | 
			
		||||
		List.of("onCloseMode", "enterToSend")
 | 
			
		||||
			.stream()
 | 
			
		||||
			.map(settings.getItems()::get)
 | 
			
		||||
			.map(i -> new SettingsCheckbox((SettingsItem<Boolean>) i))
 | 
			
		||||
			.forEach(vbox.getChildren()::add);
 | 
			
		||||
 | 
			
		||||
		final var combobox = new ComboBox<String>();
 | 
			
		||||
		combobox.getItems().add("dark");
 | 
			
		||||
		combobox.getItems().add("light");
 | 
			
		||||
		combobox.setValue(settings.getCurrentTheme());
 | 
			
		||||
		combobox.setOnAction(
 | 
			
		||||
				e -> { settings.setCurrentTheme(combobox.getValue()); EventBus.getInstance().dispatch(new ThemeChangeEvent(combobox.getValue())); });
 | 
			
		||||
		vbox.getChildren().add(combobox);
 | 
			
		||||
 | 
			
		||||
		final var statusComboBox = new ComboBox<UserStatus>();
 | 
			
		||||
		statusComboBox.getItems().setAll(UserStatus.values());
 | 
			
		||||
		statusComboBox.setValue(UserStatus.ONLINE);
 | 
			
		||||
		// TODO add action when value is changed
 | 
			
		||||
		statusComboBox.setOnAction(e -> {});
 | 
			
		||||
		vbox.getChildren().add(statusComboBox);
 | 
			
		||||
 | 
			
		||||
		getChildren().add(vbox);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,32 @@
 | 
			
		||||
package envoy.client.ui.settings;
 | 
			
		||||
 | 
			
		||||
import javafx.event.ActionEvent;
 | 
			
		||||
import javafx.scene.control.CheckBox;
 | 
			
		||||
 | 
			
		||||
import envoy.client.data.SettingsItem;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>SettingsToggleButton.java</strong><br>
 | 
			
		||||
 * Created: <strong>18.04.2020</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
public final class SettingsCheckbox extends CheckBox {
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Creates an instance of {@link SettingsCheckbox}.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param settingsItem the {@link SettingsItem} whose values could be adapted
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public SettingsCheckbox(SettingsItem<Boolean> settingsItem) {
 | 
			
		||||
		super(settingsItem.getUserFriendlyName());
 | 
			
		||||
		setSelected(settingsItem.get());
 | 
			
		||||
		
 | 
			
		||||
		// "Schau, es hat sich behindert" - Kai, 2020
 | 
			
		||||
		
 | 
			
		||||
		addEventHandler(ActionEvent.ACTION, e -> settingsItem.set(isSelected()));
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,24 @@
 | 
			
		||||
package envoy.client.ui.settings;
 | 
			
		||||
 | 
			
		||||
import javafx.scene.layout.Pane;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>SettingsPane.java</strong><br>
 | 
			
		||||
 * Created: <strong>18.04.2020</strong><br>
 | 
			
		||||
 * 
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
public abstract class SettingsPane extends Pane {
 | 
			
		||||
 | 
			
		||||
	protected String title;
 | 
			
		||||
 | 
			
		||||
	protected SettingsPane(String title) { this.title = title; }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return the title of this settings pane
 | 
			
		||||
	 * @since Envoy Client v0.1-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public String getTitle() { return title; }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,14 @@
 | 
			
		||||
/**
 | 
			
		||||
 * This package contains classes used for representing the settings
 | 
			
		||||
 * visually.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Project: <strong>envoy-client</strong><br>
 | 
			
		||||
 * File: <strong>package-info.java</strong><br>
 | 
			
		||||
 * Created: <strong>19 Apr 2020</strong><br>
 | 
			
		||||
 *
 | 
			
		||||
 * @author Leon Hofmeister
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @author Maximilian Käfer
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
package envoy.client.ui.settings;
 | 
			
		||||
							
								
								
									
										23
									
								
								client/src/main/java/module-info.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								client/src/main/java/module-info.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
/**
 | 
			
		||||
 * This module contains all classes defining the client application of the Envoy
 | 
			
		||||
 * project.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @author Leon Hofmeister
 | 
			
		||||
 * @author Maximilian Käfer
 | 
			
		||||
 * @since Envoy Client v0.1-beta
 | 
			
		||||
 */
 | 
			
		||||
module envoy {
 | 
			
		||||
 | 
			
		||||
	requires transitive envoy.common;
 | 
			
		||||
	requires transitive java.desktop;
 | 
			
		||||
	requires transitive java.logging;
 | 
			
		||||
	requires transitive java.prefs;
 | 
			
		||||
	requires javafx.controls;
 | 
			
		||||
	requires javafx.fxml;
 | 
			
		||||
	requires javafx.base;
 | 
			
		||||
	requires javafx.graphics;
 | 
			
		||||
 | 
			
		||||
	opens envoy.client.ui to javafx.graphics, javafx.fxml;
 | 
			
		||||
	opens envoy.client.ui.controller to javafx.graphics, javafx.fxml;
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user