diff --git a/app/src/main/java/io/xeres/app/net/protocol/PeerAddress.java b/app/src/main/java/io/xeres/app/net/protocol/PeerAddress.java index 383b95828..3f049977d 100644 --- a/app/src/main/java/io/xeres/app/net/protocol/PeerAddress.java +++ b/app/src/main/java/io/xeres/app/net/protocol/PeerAddress.java @@ -462,6 +462,11 @@ public boolean isHostname() private static boolean isInvalidIpAddress(String address) { + if (address == null) + { + return true; + } + var octets = address.split("\\."); if (octets.length != 4) diff --git a/ui/build.gradle b/ui/build.gradle index 13beef9fe..54a6161af 100644 --- a/ui/build.gradle +++ b/ui/build.gradle @@ -62,10 +62,13 @@ dependencies { implementation 'org.apache.commons:commons-lang3' implementation "org.jsoup:jsoup:$jsoupVersion" implementation "com.github.java-json-tools:json-patch:$jsonPatchVersion" - implementation 'de.jensd:fontawesomefx-fontawesome:4.7.0-9.1.2' implementation 'com.github.sarxos:webcam-capture:0.3.12' implementation "com.google.zxing:javase:$zxingVersion" implementation "net.harawata:appdirs:$appDirsVersion" + implementation 'io.github.mkpaz:atlantafx-base:2.0.1' + implementation platform('org.kordamp.ikonli:ikonli-bom:12.3.1') + implementation 'org.kordamp.ikonli:ikonli-javafx' + implementation 'org.kordamp.ikonli:ikonli-fontawesome5-pack' testImplementation('org.springframework.boot:spring-boot-starter-test') { exclude group: "com.vaadin.external.google", module: "android-json" } diff --git a/ui/src/main/java/io/xeres/ui/JavaFxApplication.java b/ui/src/main/java/io/xeres/ui/JavaFxApplication.java index eedf6bcfa..255fc90e9 100644 --- a/ui/src/main/java/io/xeres/ui/JavaFxApplication.java +++ b/ui/src/main/java/io/xeres/ui/JavaFxApplication.java @@ -21,13 +21,16 @@ import io.xeres.common.mui.MinimalUserInterface; import io.xeres.ui.controller.MainWindowController; +import io.xeres.ui.support.theme.AppTheme; import javafx.application.Application; import javafx.application.HostServices; import javafx.stage.Stage; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.context.ConfigurableApplicationContext; +import java.lang.reflect.InvocationTargetException; import java.util.Objects; +import java.util.prefs.Preferences; public class JavaFxApplication extends Application { @@ -75,11 +78,27 @@ public void start(Stage primaryStage) { hostServices = getHostServices(); + var preferences = Preferences.userRoot().node("Application"); + var theme = preferences.get("Theme", AppTheme.PRIMER_LIGHT.getName()); + setTheme(AppTheme.findByName(theme)); + Objects.requireNonNull(springContext); mainWindowController = springContext.getBean(MainWindowController.class); springContext.publishEvent(new StageReadyEvent(primaryStage)); } + private static void setTheme(AppTheme appTheme) + { + try + { + Application.setUserAgentStylesheet(appTheme.getThemeClass().getDeclaredConstructor().newInstance().getUserAgentStylesheet()); + } + catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) + { + throw new RuntimeException(e); + } + } + @Override public void stop() { diff --git a/ui/src/main/java/io/xeres/ui/controller/about/AboutWindowController.java b/ui/src/main/java/io/xeres/ui/controller/about/AboutWindowController.java index d13934b15..5f4b6b3f8 100644 --- a/ui/src/main/java/io/xeres/ui/controller/about/AboutWindowController.java +++ b/ui/src/main/java/io/xeres/ui/controller/about/AboutWindowController.java @@ -24,7 +24,11 @@ import io.xeres.ui.support.util.UiUtils; import javafx.application.Platform; import javafx.fxml.FXML; -import javafx.scene.control.*; +import javafx.scene.control.Button; +import javafx.scene.control.Hyperlink; +import javafx.scene.control.Label; +import javafx.scene.control.TabPane; +import javafx.scene.text.Text; import net.rgielen.fxweaver.core.FxmlView; import org.springframework.boot.info.BuildProperties; import org.springframework.core.env.Environment; @@ -46,7 +50,7 @@ public class AboutWindowController implements WindowController private TabPane infoPane; @FXML - private TextArea licenseTextArea; + private Text license; @FXML private Label version; @@ -83,7 +87,7 @@ public void initialize() throws IOException { profile.setText("Profiles: " + String.join(", ", environment.getActiveProfiles())); } - licenseTextArea.setText(UiUtils.getResourceFileAsString(getClass().getResourceAsStream("/LICENSE"))); + license.setText(UiUtils.getResourceFileAsString(getClass().getResourceAsStream("/LICENSE"))); twemoji.setOnAction(event -> JavaFxApplication.openUrl("https://github.com/twitter/twemoji")); twemojiLicense.setOnAction(event -> JavaFxApplication.openUrl("https://creativecommons.org/licenses/by/4.0/")); diff --git a/ui/src/main/java/io/xeres/ui/controller/chat/ChatRoomCreationWindowController.java b/ui/src/main/java/io/xeres/ui/controller/chat/ChatRoomCreationWindowController.java index d85c84997..6636baa8d 100644 --- a/ui/src/main/java/io/xeres/ui/controller/chat/ChatRoomCreationWindowController.java +++ b/ui/src/main/java/io/xeres/ui/controller/chat/ChatRoomCreationWindowController.java @@ -69,8 +69,8 @@ public ChatRoomCreationWindowController(ChatClient chatClient, ResourceBundle bu @Override public void initialize() { - roomName.textProperty().addListener(observable -> createButton.setDisable(roomName.getText().isBlank())); - topic.textProperty().addListener(observable -> createButton.setDisable(topic.getText().isBlank())); + roomName.textProperty().addListener(observable -> checkCreatable()); + topic.textProperty().addListener(observable -> checkCreatable()); visibility.setItems(FXCollections.observableArrayList(bundle.getString("enum.roomtype.public"), bundle.getString("enum.roomtype.private"))); visibility.getSelectionModel().select(0); @@ -83,4 +83,9 @@ public void initialize() .subscribe()); cancelButton.setOnAction(UiUtils::closeWindow); } + + private void checkCreatable() + { + createButton.setDisable(roomName.getText().isBlank() || topic.getText().isBlank()); + } } diff --git a/ui/src/main/java/io/xeres/ui/controller/chat/ChatUserCell.java b/ui/src/main/java/io/xeres/ui/controller/chat/ChatUserCell.java index 34b7d6540..5b0bc0284 100644 --- a/ui/src/main/java/io/xeres/ui/controller/chat/ChatUserCell.java +++ b/ui/src/main/java/io/xeres/ui/controller/chat/ChatUserCell.java @@ -28,7 +28,8 @@ public class ChatUserCell extends ListCell { - private static final ImageView defaultImage = new ImageView("/image/avatar_16.png"); + private static final int DEFAULT_AVATAR_SIZE = 32; + private static final ImageView defaultImage = new ImageView("/image/avatar_" + DEFAULT_AVATAR_SIZE + ".png"); public ChatUserCell() { @@ -53,8 +54,8 @@ private ImageView getAvatarImage(ChatRoomUser item) if (item.image() != null) { image = new ImageView(item.image().getImage()); - image.setFitWidth(16.0); - image.setFitHeight(16.0); + image.setFitWidth(DEFAULT_AVATAR_SIZE); + image.setFitHeight(DEFAULT_AVATAR_SIZE); } else { diff --git a/ui/src/main/java/io/xeres/ui/controller/chat/ChatViewController.java b/ui/src/main/java/io/xeres/ui/controller/chat/ChatViewController.java index 2f9491f39..f3f414170 100644 --- a/ui/src/main/java/io/xeres/ui/controller/chat/ChatViewController.java +++ b/ui/src/main/java/io/xeres/ui/controller/chat/ChatViewController.java @@ -19,11 +19,8 @@ package io.xeres.ui.controller.chat; -import io.xeres.common.AppName; import io.xeres.common.i18n.I18nUtils; import io.xeres.common.message.chat.*; -import io.xeres.common.rest.location.RSIdResponse; -import io.xeres.common.rsid.Type; import io.xeres.ui.client.ChatClient; import io.xeres.ui.client.LocationClient; import io.xeres.ui.client.ProfileClient; @@ -35,12 +32,12 @@ import io.xeres.ui.support.markdown.MarkdownService; import io.xeres.ui.support.tray.TrayService; import io.xeres.ui.support.util.ImageUtils; +import io.xeres.ui.support.util.TextInputControlUtils; import io.xeres.ui.support.util.UiUtils; import io.xeres.ui.support.window.WindowManager; import javafx.animation.KeyFrame; import javafx.animation.Timeline; import javafx.application.Platform; -import javafx.beans.binding.Bindings; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; @@ -55,8 +52,6 @@ import org.springframework.stereotype.Component; import java.io.IOException; -import java.net.URI; -import java.net.URLEncoder; import java.text.MessageFormat; import java.time.Duration; import java.time.Instant; @@ -66,9 +61,7 @@ import java.util.function.Consumer; import java.util.stream.Collectors; -import static io.xeres.common.dto.location.LocationConstants.OWN_LOCATION_ID; import static io.xeres.common.message.chat.ChatConstants.TYPING_NOTIFICATION_DELAY; -import static java.nio.charset.StandardCharsets.UTF_8; import static org.apache.commons.lang3.ObjectUtils.isEmpty; import static org.apache.commons.lang3.StringUtils.isNotBlank; @@ -240,7 +233,7 @@ public void initialize() throws IOException ae -> typingNotification.setText(""))); send.addEventHandler(KeyEvent.KEY_PRESSED, this::handleInputKeys); - send.setContextMenu(createChatInputContextMenu(send)); + send.setContextMenu(TextInputControlUtils.createInputContextMenu(send, locationClient)); invite.setOnAction(event -> windowManager.openInvite(UiUtils.getWindow(event), selectedRoom.getId())); @@ -640,70 +633,6 @@ private void setPreviewGroupVisibility(boolean visible) previewGroup.setManaged(visible); } - private ContextMenu createChatInputContextMenu(TextInputControl textInputControl) - { - var contextMenu = new ContextMenu(); - - contextMenu.getItems().addAll(createDefaultChatInputMenuItems(textInputControl)); - var pasteId = new MenuItem(bundle.getString("chat.room.input.paste-id")); - pasteId.setOnAction(event -> appendOwnId(textInputControl)); - contextMenu.getItems().addAll(new SeparatorMenuItem(), pasteId); - return contextMenu; - } - - private void appendOwnId(TextInputControl textInputControl) - { - var rsIdResponse = locationClient.getRSId(OWN_LOCATION_ID, Type.CERTIFICATE); - rsIdResponse.subscribe(reply -> Platform.runLater(() -> textInputControl.appendText(buildRetroshareUrl(reply)))); - } - - private String buildRetroshareUrl(RSIdResponse rsIdResponse) - { - var uri = URI.create("retroshare://certificate?" + - "radix=" + URLEncoder.encode(rsIdResponse.rsId().replace("\n", ""), UTF_8) + // Removing the '\n' is in case this is a certificate which is sliced for presentation - "&name=" + URLEncoder.encode(rsIdResponse.name(), UTF_8) + - "&location=" + URLEncoder.encode(rsIdResponse.location(), UTF_8)); - return "" + AppName.NAME + " Certificate (" + rsIdResponse.name() + ", @" + rsIdResponse.location() + ")"; - } - - private List createDefaultChatInputMenuItems(TextInputControl textInputControl) - { - var undo = new MenuItem(bundle.getString("chat.room.input.undo")); - undo.setOnAction(event -> textInputControl.undo()); - - var redo = new MenuItem(bundle.getString("chat.room.input.redo")); - redo.setOnAction(event -> textInputControl.redo()); - - var cut = new MenuItem(bundle.getString("chat.room.input.cut")); - cut.setOnAction(event -> textInputControl.cut()); - - var copy = new MenuItem(bundle.getString("chat.room.input.copy")); - copy.setOnAction(event -> textInputControl.copy()); - - var paste = new MenuItem(bundle.getString("chat.room.input.paste")); - paste.setOnAction(event -> textInputControl.paste()); - - var delete = new MenuItem(bundle.getString("chat.room.input.delete")); - delete.setOnAction(event -> textInputControl.deleteText(textInputControl.getSelection())); - - var selectAll = new MenuItem(bundle.getString("chat.room.input.select-all")); - selectAll.setOnAction(event -> textInputControl.selectAll()); - - var emptySelection = Bindings.createBooleanBinding(() -> textInputControl.getSelection().getLength() == 0, textInputControl.selectionProperty()); - - cut.disableProperty().bind(emptySelection); - copy.disableProperty().bind(emptySelection); - delete.disableProperty().bind(emptySelection); - - var canUndo = Bindings.createBooleanBinding(() -> !textInputControl.isUndoable(), textInputControl.undoableProperty()); - var canRedo = Bindings.createBooleanBinding(() -> !textInputControl.isRedoable(), textInputControl.redoableProperty()); - - undo.disableProperty().bind(canUndo); - redo.disableProperty().bind(canRedo); - - return List.of(undo, redo, cut, copy, paste, delete, new SeparatorMenuItem(), selectAll); - } - public void openInvite(long chatRoomId, ChatRoomInviteEvent event) { Platform.runLater(() -> UiUtils.alertConfirm(MessageFormat.format(bundle.getString("chat.room.invite.request"), event.getLocationId(), event.getRoomName(), event.getRoomTopic()), diff --git a/ui/src/main/java/io/xeres/ui/controller/forum/ForumEditorViewController.java b/ui/src/main/java/io/xeres/ui/controller/forum/ForumEditorViewController.java index a905c0c88..c26c97524 100644 --- a/ui/src/main/java/io/xeres/ui/controller/forum/ForumEditorViewController.java +++ b/ui/src/main/java/io/xeres/ui/controller/forum/ForumEditorViewController.java @@ -22,6 +22,7 @@ import io.xeres.common.message.forum.ForumMessage; import io.xeres.common.rest.forum.PostRequest; import io.xeres.ui.client.ForumClient; +import io.xeres.ui.client.LocationClient; import io.xeres.ui.controller.WindowController; import io.xeres.ui.custom.EditorView; import io.xeres.ui.support.util.UiUtils; @@ -55,10 +56,12 @@ public class ForumEditorViewController implements WindowController private PostRequest postRequest; private final ForumClient forumClient; + private final LocationClient locationClient; - public ForumEditorViewController(ForumClient forumClient) + public ForumEditorViewController(ForumClient forumClient, LocationClient locationClient) { this.forumClient = forumClient; + this.locationClient = locationClient; } @Override @@ -67,6 +70,7 @@ public void initialize() throws IOException Platform.runLater(() -> title.requestFocus()); editorView.lengthProperty.addListener((observable, oldValue, newValue) -> checkSendable((Integer) newValue)); + editorView.setInputContextMenu(locationClient); title.setOnKeyTyped(event -> checkSendable(editorView.lengthProperty.getValue())); send.setOnAction(event -> postMessage()); diff --git a/ui/src/main/java/io/xeres/ui/controller/settings/SettingsGeneralController.java b/ui/src/main/java/io/xeres/ui/controller/settings/SettingsGeneralController.java index d9662af3f..a931990a7 100644 --- a/ui/src/main/java/io/xeres/ui/controller/settings/SettingsGeneralController.java +++ b/ui/src/main/java/io/xeres/ui/controller/settings/SettingsGeneralController.java @@ -22,17 +22,28 @@ import io.xeres.common.rest.config.Capabilities; import io.xeres.ui.client.ConfigClient; import io.xeres.ui.model.settings.Settings; +import io.xeres.ui.support.theme.AppTheme; +import javafx.application.Application; import javafx.application.Platform; +import javafx.beans.value.ObservableValue; import javafx.fxml.FXML; import javafx.scene.control.CheckBox; +import javafx.scene.control.ChoiceBox; import javafx.scene.control.Label; import net.rgielen.fxweaver.core.FxmlView; import org.springframework.stereotype.Component; +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; +import java.util.prefs.Preferences; + @Component @FxmlView(value = "/view/settings/settings_general.fxml") public class SettingsGeneralController implements SettingsController { + @FXML + private ChoiceBox themeSelector; + @FXML private CheckBox autoStartEnabled; @@ -48,9 +59,29 @@ public SettingsGeneralController(ConfigClient configClient) this.configClient = configClient; } + private static void changeTheme(ObservableValue observable, AppTheme oldValue, AppTheme newValue) + { + try + { + Application.setUserAgentStylesheet(newValue.getThemeClass().getDeclaredConstructor().newInstance().getUserAgentStylesheet()); + var preferences = Preferences.userRoot().node("Application"); + preferences.put("Theme", newValue.getName()); + } + catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) + { + throw new RuntimeException(e); + } + } + @Override public void initialize() { + themeSelector.getItems().addAll(Arrays.stream(AppTheme.values()).toList()); + var preferences = Preferences.userRoot().node("Application"); + var theme = preferences.get("Theme", AppTheme.PRIMER_LIGHT.getName()); + themeSelector.getSelectionModel().select(AppTheme.findByName(theme)); + themeSelector.getSelectionModel().selectedItemProperty().addListener(SettingsGeneralController::changeTheme); + configClient.getCapabilities() .doOnSuccess(capabilities -> Platform.runLater(() -> { if (capabilities.contains(Capabilities.AUTOSTART)) diff --git a/ui/src/main/java/io/xeres/ui/custom/ChatListCell.java b/ui/src/main/java/io/xeres/ui/custom/ChatListCell.java index ca89a5001..a47dc4c53 100644 --- a/ui/src/main/java/io/xeres/ui/custom/ChatListCell.java +++ b/ui/src/main/java/io/xeres/ui/custom/ChatListCell.java @@ -20,15 +20,17 @@ package io.xeres.ui.custom; import io.xeres.ui.support.chat.ChatLine; +import io.xeres.ui.support.chat.ColorGenerator; import io.xeres.ui.support.contentline.Content; +import javafx.css.PseudoClass; import javafx.scene.control.Label; -import javafx.scene.text.Text; import javafx.scene.text.TextFlow; import org.fxmisc.flowless.Cell; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.time.format.FormatStyle; +import java.util.List; import java.util.Locale; public class ChatListCell implements Cell @@ -37,6 +39,10 @@ public class ChatListCell implements Cell .withLocale(Locale.ROOT) .withZone(ZoneId.systemDefault()); + private static final PseudoClass passivePseudoClass = PseudoClass.getPseudoClass("passive"); + + private static final List allColors = ColorGenerator.getAllColors(); + private final TextFlow content; private final Label time; private final Label action; @@ -86,15 +92,20 @@ public void updateItem(ChatLine line) time.setText(formatter.format(line.getInstant())); action.setText(line.getAction()); - action.setTextFill(line.getNicknameColor()); + var nicknameColor = line.getNicknameColor(); + action.getStyleClass().removeAll(allColors); + if (nicknameColor != null) + { + action.getStyleClass().add(nicknameColor); + } var nodes = line.getChatContents().stream() .map(Content::getNode) .toList(); - if (nodes.size() == 1 && nodes.get(0) instanceof Text text) // XXX: check if that works for single URLs.. + if (!isComplex) { - text.setFill(line.getContentColor()); + content.pseudoClassStateChanged(passivePseudoClass, !line.isActiveAction()); } content.getChildren().addAll(nodes); diff --git a/ui/src/main/java/io/xeres/ui/custom/EditorView.java b/ui/src/main/java/io/xeres/ui/custom/EditorView.java index 673ef0654..edeaffa95 100644 --- a/ui/src/main/java/io/xeres/ui/custom/EditorView.java +++ b/ui/src/main/java/io/xeres/ui/custom/EditorView.java @@ -20,7 +20,9 @@ package io.xeres.ui.custom; import io.xeres.common.i18n.I18nUtils; +import io.xeres.ui.client.LocationClient; import io.xeres.ui.support.util.ImageUtils; +import io.xeres.ui.support.util.TextInputControlUtils; import io.xeres.ui.support.util.UiUtils; import javafx.beans.property.ReadOnlyIntegerWrapper; import javafx.fxml.FXML; @@ -127,6 +129,11 @@ public void setReply(String reply) editor.requestFocus(); } + public void setInputContextMenu(LocationClient locationClient) + { + editor.setContextMenu(TextInputControlUtils.createInputContextMenu(editor, locationClient)); + } + private void surround(String text) { var selection = editor.getSelection(); diff --git a/ui/src/main/java/io/xeres/ui/custom/ReadOnlyTextField.java b/ui/src/main/java/io/xeres/ui/custom/ReadOnlyTextField.java index d79b0bf3d..b90cf7eeb 100644 --- a/ui/src/main/java/io/xeres/ui/custom/ReadOnlyTextField.java +++ b/ui/src/main/java/io/xeres/ui/custom/ReadOnlyTextField.java @@ -52,8 +52,6 @@ public ReadOnlyTextField(String text) private void init() { - getStyleClass().add("text-field-readonly"); - setOnMouseClicked(event -> selectAll()); setContextMenu(createContextMenu()); diff --git a/ui/src/main/java/io/xeres/ui/support/chat/ChatLine.java b/ui/src/main/java/io/xeres/ui/support/chat/ChatLine.java index 8324549fc..e8b704a88 100644 --- a/ui/src/main/java/io/xeres/ui/support/chat/ChatLine.java +++ b/ui/src/main/java/io/xeres/ui/support/chat/ChatLine.java @@ -22,7 +22,6 @@ import io.xeres.common.id.GxsId; import io.xeres.ui.support.contentline.Content; import io.xeres.ui.support.contentline.ContentText; -import javafx.scene.paint.Color; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -70,23 +69,24 @@ public boolean hasSaid(GxsId gxsId) return action.getType() == ChatAction.Type.SAY && gxsId.toString().equals(action.getGxsId()); } - public Color getNicknameColor() + public String getNicknameColor() { return switch (action.getType()) { - case JOIN, LEAVE, TIMEOUT -> Color.GRAY; - case ACTION, SAY_OWN -> Color.BLACK; case SAY -> ColorGenerator.generateColor(action.getGxsId() != null ? action.getGxsId() : action.getNickname()); + default -> null; }; } - public Color getContentColor() + public boolean isActiveAction() { return switch (action.getType()) - { - case JOIN, LEAVE, TIMEOUT -> Color.GRAY; - case SAY, SAY_OWN, ACTION -> Color.BLACK; - }; + { + case JOIN, LEAVE, TIMEOUT: + yield false; + case SAY, SAY_OWN, ACTION: + yield true; + }; } public List getChatContents() diff --git a/ui/src/main/java/io/xeres/ui/support/chat/ColorGenerator.java b/ui/src/main/java/io/xeres/ui/support/chat/ColorGenerator.java index 6734fce61..fbae855f2 100644 --- a/ui/src/main/java/io/xeres/ui/support/chat/ColorGenerator.java +++ b/ui/src/main/java/io/xeres/ui/support/chat/ColorGenerator.java @@ -19,8 +19,8 @@ package io.xeres.ui.support.chat; -import javafx.scene.paint.Color; - +import java.util.Arrays; +import java.util.List; import java.util.Objects; public final class ColorGenerator @@ -36,39 +36,44 @@ private ColorGenerator() */ private enum ColorSpec { - COLOR_00(Color.rgb(204, 0, 0)), - COLOR_01(Color.rgb(0, 108, 173)), - COLOR_02(Color.rgb(77, 153, 0)), - COLOR_03(Color.rgb(102, 0, 204)), - COLOR_04(Color.rgb(166, 125, 0)), - COLOR_05(Color.rgb(0, 153, 39)), - COLOR_06(Color.rgb(0, 48, 192)), - COLOR_07(Color.rgb(204, 0, 154)), - COLOR_08(Color.rgb(185, 70, 0)), - COLOR_09(Color.rgb(134, 153, 0)), - COLOR_10(Color.rgb(20, 153, 0)), - COLOR_11(Color.rgb(0, 153, 96)), - COLOR_12(Color.rgb(0, 108, 173)), - COLOR_13(Color.rgb(0, 153, 204)), - COLOR_14(Color.rgb(179, 0, 204)), - COLOR_15(Color.rgb(204, 0, 77)); + COLOR_00("color-00"), + COLOR_01("color-01"), + COLOR_02("color-02"), + COLOR_03("color-03"), + COLOR_04("color-04"), + COLOR_05("color-05"), + COLOR_06("color-06"), + COLOR_07("color-07"), + COLOR_08("color-08"), + COLOR_09("color-09"), + COLOR_10("color-10"), + COLOR_11("color-11"), + COLOR_12("color-12"), + COLOR_13("color-13"), + COLOR_14("color-14"), + COLOR_15("color-15"); - private final Color color; + private final String color; - ColorSpec(Color color) + ColorSpec(String color) { this.color = color; } - public Color getColor() + public String getColor() { return color; } } - public static Color generateColor(String s) + public static String generateColor(String s) { Objects.requireNonNull(s); return ColorSpec.values()[Math.floorMod(s.hashCode(), ColorSpec.values().length)].getColor(); } + + public static List getAllColors() + { + return Arrays.stream(ColorSpec.values()).map(ColorSpec::getColor).toList(); + } } diff --git a/ui/src/main/java/io/xeres/ui/support/contentline/ContentUri.java b/ui/src/main/java/io/xeres/ui/support/contentline/ContentUri.java index 54d47a065..f23c4bff0 100644 --- a/ui/src/main/java/io/xeres/ui/support/contentline/ContentUri.java +++ b/ui/src/main/java/io/xeres/ui/support/contentline/ContentUri.java @@ -20,6 +20,7 @@ package io.xeres.ui.support.contentline; import io.xeres.ui.JavaFxApplication; +import io.xeres.ui.support.util.TooltipUtils; import javafx.scene.Node; import javafx.scene.control.Hyperlink; @@ -35,9 +36,17 @@ public ContentUri(String uri) node.setOnAction(event -> JavaFxApplication.openUrl(appendMailToIfNeeded(node.getText()))); } + public ContentUri(String uri, String description) + { + node = new Hyperlink(description); + TooltipUtils.install(node, uri); + node.setOnAction(event -> JavaFxApplication.openUrl(appendMailToIfNeeded(uri))); + } + public ContentUri(String uri, String description, Consumer action) { node = new Hyperlink(description); + TooltipUtils.install(node, uri); node.setOnAction(event -> action.accept(uri)); } diff --git a/ui/src/main/java/io/xeres/ui/support/markdown/LinkDetector.java b/ui/src/main/java/io/xeres/ui/support/markdown/LinkDetector.java index 74b578df7..7f4313722 100644 --- a/ui/src/main/java/io/xeres/ui/support/markdown/LinkDetector.java +++ b/ui/src/main/java/io/xeres/ui/support/markdown/LinkDetector.java @@ -19,24 +19,36 @@ package io.xeres.ui.support.markdown; -import io.xeres.ui.support.contentline.ContentUri; +import io.xeres.ui.support.uri.UriParser; import java.util.regex.Pattern; class LinkDetector implements MarkdownDetector { - private static final Pattern URL_PATTERN = Pattern.compile("\\b(?(?:https?|ftps?)://[-A-Z0-9+&@#/%?=~_|!:,.;]*[-A-Z0-9+&@#/%=~_|])|(?[0-9A-Z._+\\-=]+@[0-9a-z\\-]+\\.[a-z]{2,})", Pattern.CASE_INSENSITIVE); + private static final Pattern LINK_PATTERN = Pattern.compile("\\[.{0,256}]\\(.{0,2048}\\)"); // Large URL @Override public boolean isPossibly(String line) { - return line.contains("http") || line.contains("ftp") || line.contains("@"); + return line.contains("["); } @Override public void process(Context context, String line) { - MarkdownService.processPattern(URL_PATTERN, context, line, - (s, groupName) -> context.addContent(new ContentUri(s))); + MarkdownService.processPattern(LINK_PATTERN, context, line, + (s, groupName) -> context.addContent(UriParser.parse(getUrl(s), getDescription(s)))); + } + + private static String getUrl(String s) + { + var index = s.lastIndexOf("("); + return s.substring(index + 1, s.length() - 1); // skip the ")" at the end + } + + private static String getDescription(String s) + { + var index = s.indexOf("]"); + return s.substring(1, index - 1); } } diff --git a/ui/src/main/java/io/xeres/ui/support/markdown/MarkdownService.java b/ui/src/main/java/io/xeres/ui/support/markdown/MarkdownService.java index 41b410477..e3087bb3e 100644 --- a/ui/src/main/java/io/xeres/ui/support/markdown/MarkdownService.java +++ b/ui/src/main/java/io/xeres/ui/support/markdown/MarkdownService.java @@ -34,8 +34,9 @@ public MarkdownService(EmojiService emojiService) substringDetectors.add(new CodeDetector()); substringDetectors.add(new EmphasisDetector()); - substringDetectors.add(new HrefDetector()); substringDetectors.add(new LinkDetector()); + substringDetectors.add(new HrefDetector()); + substringDetectors.add(new UrlDetector()); substringDetectors.add(new ImageDetector()); if (emojiService.isColoredEmojis()) { diff --git a/ui/src/main/java/io/xeres/ui/support/markdown/UrlDetector.java b/ui/src/main/java/io/xeres/ui/support/markdown/UrlDetector.java new file mode 100644 index 000000000..e4e9fb3e1 --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/support/markdown/UrlDetector.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.support.markdown; + +import io.xeres.ui.support.contentline.ContentUri; + +import java.util.regex.Pattern; + +class UrlDetector implements MarkdownDetector +{ + private static final Pattern URL_PATTERN = Pattern.compile("\\b(?(?:https?|ftps?)://[-A-Z0-9+&@#/%?=~_|!:,.;]*[-A-Z0-9+&@#/%=~_|])|(?[0-9A-Z._+\\-=]+@[0-9a-z\\-]+\\.[a-z]{2,})", Pattern.CASE_INSENSITIVE); + + @Override + public boolean isPossibly(String line) + { + return line.contains("http") || line.contains("ftp") || line.contains("@"); + } + + @Override + public void process(Context context, String line) + { + MarkdownService.processPattern(URL_PATTERN, context, line, + (s, groupName) -> context.addContent(new ContentUri(s))); + } +} diff --git a/ui/src/main/java/io/xeres/ui/support/theme/AppTheme.java b/ui/src/main/java/io/xeres/ui/support/theme/AppTheme.java new file mode 100644 index 000000000..566bde3be --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/support/theme/AppTheme.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2023 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.support.theme; + +import atlantafx.base.theme.*; + +import java.util.Arrays; + +public enum AppTheme +{ + PRIMER_LIGHT("Primer Light", PrimerLight.class), + PRIMER_DARK("Primer Dark", PrimerDark.class), + NORD_LIGHT("Nord Light", NordLight.class), + NORD_DARK("Nord Dark", NordDark.class), + CUPERTINO_LIGHT("Cupertino Light", CupertinoLight.class), + CUPERTINO_DARK("Cupertino Dark", CupertinoDark.class), + DRACULA("Dracula", Dracula.class); + + private final String name; + private final Class themeClass; + + AppTheme(String name, Class themeClass) + { + this.name = name; + this.themeClass = themeClass; + } + + public String getName() + { + return name; + } + + public Class getThemeClass() + { + return themeClass; + } + + public static AppTheme findByName(String name) + { + return Arrays.stream(values()).filter(appTheme -> appTheme.getName().equals(name)).findFirst().orElse(PRIMER_LIGHT); + } + + @Override + public String toString() + { + return name; + } +} diff --git a/ui/src/main/java/io/xeres/ui/support/uri/UriParser.java b/ui/src/main/java/io/xeres/ui/support/uri/UriParser.java index 70dc982e9..12e0e44f8 100644 --- a/ui/src/main/java/io/xeres/ui/support/uri/UriParser.java +++ b/ui/src/main/java/io/xeres/ui/support/uri/UriParser.java @@ -73,21 +73,26 @@ public static Content parse(String href, String text) { var uri = new URI(href); var contentParserMap = contentParsers.get(uri.getScheme()); - if (contentParserMap == null) + if (contentParserMap != null) { - return ContentText.EMPTY; + var contentParser = contentParserMap.get(uri.getAuthority()); + + if (contentParser != null) + { + var uriComponents = UriComponentsBuilder.fromPath(uri.getPath()) + .query(uri.getQuery()) + .build(); + + return contentParser.parse(uriComponents, text); + } } - var contentParser = contentParserMap.get(uri.getAuthority()); - if (contentParser != null) + // If we don't know the URL, delegate to the OS + if (isBlank(text)) { - var uriComponents = UriComponentsBuilder.fromPath(uri.getPath()) - .query(uri.getQuery()) - .build(); - - return contentParser.parse(uriComponents, text); + return new ContentUri(uri.toString()); } - return new ContentUri(uri.toString()); + return new ContentUri(uri.toString(), text); } catch (URISyntaxException e) { diff --git a/ui/src/main/java/io/xeres/ui/support/util/TextInputControlUtils.java b/ui/src/main/java/io/xeres/ui/support/util/TextInputControlUtils.java new file mode 100644 index 000000000..e16962841 --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/support/util/TextInputControlUtils.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2023 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.support.util; + +import io.xeres.common.AppName; +import io.xeres.common.i18n.I18nUtils; +import io.xeres.common.rest.location.RSIdResponse; +import io.xeres.common.rsid.Type; +import io.xeres.ui.client.LocationClient; +import javafx.application.Platform; +import javafx.beans.binding.Bindings; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.MenuItem; +import javafx.scene.control.SeparatorMenuItem; +import javafx.scene.control.TextInputControl; + +import java.net.URI; +import java.net.URLEncoder; +import java.util.List; + +import static io.xeres.common.dto.location.LocationConstants.OWN_LOCATION_ID; +import static java.nio.charset.StandardCharsets.UTF_8; + +public final class TextInputControlUtils +{ + private TextInputControlUtils() + { + throw new UnsupportedOperationException("Utility class"); + } + + public static ContextMenu createInputContextMenu(TextInputControl textInputControl, LocationClient locationClient) + { + var contextMenu = new ContextMenu(); + + contextMenu.getItems().addAll(createDefaultChatInputMenuItems(textInputControl)); + var pasteId = new MenuItem(I18nUtils.getString("chat.room.input.paste-id")); + pasteId.setOnAction(event -> appendOwnId(textInputControl, locationClient)); + contextMenu.getItems().addAll(new SeparatorMenuItem(), pasteId); + return contextMenu; + } + + private static void appendOwnId(TextInputControl textInputControl, LocationClient locationClient) + { + var rsIdResponse = locationClient.getRSId(OWN_LOCATION_ID, Type.CERTIFICATE); + rsIdResponse.subscribe(reply -> Platform.runLater(() -> textInputControl.appendText(buildRetroshareUrl(reply)))); + } + + private static String buildRetroshareUrl(RSIdResponse rsIdResponse) + { + var uri = URI.create("retroshare://certificate?" + + "radix=" + URLEncoder.encode(rsIdResponse.rsId().replace("\n", ""), UTF_8) + // Removing the '\n' is in case this is a certificate which is sliced for presentation + "&name=" + URLEncoder.encode(rsIdResponse.name(), UTF_8) + + "&location=" + URLEncoder.encode(rsIdResponse.location(), UTF_8)); + return "" + AppName.NAME + " Certificate (" + rsIdResponse.name() + ", @" + rsIdResponse.location() + ")"; + } + + private static List createDefaultChatInputMenuItems(TextInputControl textInputControl) + { + var undo = new MenuItem(I18nUtils.getString("chat.room.input.undo")); + undo.setOnAction(event -> textInputControl.undo()); + + var redo = new MenuItem(I18nUtils.getString("chat.room.input.redo")); + redo.setOnAction(event -> textInputControl.redo()); + + var cut = new MenuItem(I18nUtils.getString("chat.room.input.cut")); + cut.setOnAction(event -> textInputControl.cut()); + + var copy = new MenuItem(I18nUtils.getString("chat.room.input.copy")); + copy.setOnAction(event -> textInputControl.copy()); + + var paste = new MenuItem(I18nUtils.getString("chat.room.input.paste")); + paste.setOnAction(event -> textInputControl.paste()); + + var delete = new MenuItem(I18nUtils.getString("chat.room.input.delete")); + delete.setOnAction(event -> textInputControl.deleteText(textInputControl.getSelection())); + + var selectAll = new MenuItem(I18nUtils.getString("chat.room.input.select-all")); + selectAll.setOnAction(event -> textInputControl.selectAll()); + + var emptySelection = Bindings.createBooleanBinding(() -> textInputControl.getSelection().getLength() == 0, textInputControl.selectionProperty()); + + cut.disableProperty().bind(emptySelection); + copy.disableProperty().bind(emptySelection); + delete.disableProperty().bind(emptySelection); + + var canUndo = Bindings.createBooleanBinding(() -> !textInputControl.isUndoable(), textInputControl.undoableProperty()); + var canRedo = Bindings.createBooleanBinding(() -> !textInputControl.isRedoable(), textInputControl.redoableProperty()); + + undo.disableProperty().bind(canUndo); + redo.disableProperty().bind(canRedo); + + return List.of(undo, redo, cut, copy, paste, delete, new SeparatorMenuItem(), selectAll); + } +} diff --git a/ui/src/main/java/io/xeres/ui/support/util/UiUtils.java b/ui/src/main/java/io/xeres/ui/support/util/UiUtils.java index 2f8fb377a..07bdba0a7 100644 --- a/ui/src/main/java/io/xeres/ui/support/util/UiUtils.java +++ b/ui/src/main/java/io/xeres/ui/support/util/UiUtils.java @@ -52,8 +52,7 @@ private UiUtils() throw new UnsupportedOperationException("Utility class"); } - private static final PseudoClass errorPseudoClass = PseudoClass.getPseudoClass("error"); - private static final PseudoClass warningPseudoClass = PseudoClass.getPseudoClass("warning"); + private static final PseudoClass dangerPseudoClass = PseudoClass.getPseudoClass("danger"); private static final String KEY_LISTENER = "listener"; private static final String KEY_POPUP = "popup"; @@ -80,7 +79,7 @@ public static void showAlertError(WebClientResponseException e) public static void showError(TextField field, String error) { - field.pseudoClassStateChanged(errorPseudoClass, true); + field.pseudoClassStateChanged(dangerPseudoClass, true); var label = new Label(); label.setText(error); @@ -111,22 +110,14 @@ public static void clearError(TextField field) { popup.hide(); } - field.pseudoClassStateChanged(errorPseudoClass, false); + field.pseudoClassStateChanged(dangerPseudoClass, false); } public static void showError(Node... nodes) { for (var node : nodes) { - node.pseudoClassStateChanged(errorPseudoClass, true); - } - } - - public static void showWarning(Node... nodes) - { - for (var node : nodes) - { - node.pseudoClassStateChanged(warningPseudoClass, true); + node.pseudoClassStateChanged(dangerPseudoClass, true); } } @@ -134,7 +125,7 @@ public static void clearError(Node... nodes) { for (var node : nodes) { - node.pseudoClassStateChanged(errorPseudoClass, false); + node.pseudoClassStateChanged(dangerPseudoClass, false); } } @@ -172,8 +163,7 @@ public static void setDefaultIcon(Stage stage) public static void setDefaultStyle(Scene scene) { - scene.getStylesheets().add("/view/javafx.css"); - //scene.getStylesheets().add("/view/javafx-dark.css"); + scene.getStylesheets().add("/view/default.css"); } /** @@ -263,6 +253,10 @@ public static Window getWindow(Event event) if (target instanceof MenuItem menuItem) { + if (menuItem.getParentMenu() != null) + { + return menuItem.getParentMenu().getParentPopup().getOwnerWindow(); + } return menuItem.getParentPopup().getOwnerWindow(); } else if (target instanceof Node node) diff --git a/ui/src/main/resources/image/avatar_16.png b/ui/src/main/resources/image/avatar_16.png deleted file mode 100644 index f41169ca7..000000000 Binary files a/ui/src/main/resources/image/avatar_16.png and /dev/null differ diff --git a/ui/src/main/resources/image/avatar_32.png b/ui/src/main/resources/image/avatar_32.png new file mode 100644 index 000000000..23de49164 Binary files /dev/null and b/ui/src/main/resources/image/avatar_32.png differ diff --git a/ui/src/main/resources/view/about/about.fxml b/ui/src/main/resources/view/about/about.fxml index bc53a7432..2ed7d1467 100644 --- a/ui/src/main/resources/view/about/about.fxml +++ b/ui/src/main/resources/view/about/about.fxml @@ -25,11 +25,8 @@ - + - - - @@ -37,11 +34,7 @@ - + - - - - - - - + - - + + - - - - - + @@ -85,28 +68,20 @@ - - + + - - - - - + - - - - - + - - + + @@ -115,8 +90,8 @@ - - + + @@ -128,8 +103,8 @@ - - + + @@ -139,17 +114,19 @@ - - - + + + + + + + + + - - + + diff --git a/ui/src/main/resources/view/account/account_creation.fxml b/ui/src/main/resources/view/account/account_creation.fxml index e2bc11685..f37f895b6 100644 --- a/ui/src/main/resources/view/account/account_creation.fxml +++ b/ui/src/main/resources/view/account/account_creation.fxml @@ -21,21 +21,16 @@ - - - - + + - + + + + + diff --git a/ui/src/main/resources/view/custom/editorview.fxml b/ui/src/main/resources/view/custom/editorview.fxml index 80ceadc5b..580beba8c 100644 --- a/ui/src/main/resources/view/custom/editorview.fxml +++ b/ui/src/main/resources/view/custom/editorview.fxml @@ -19,18 +19,18 @@ ~ along with Xeres. If not, see . --> - - + + - + - + + + - + \ No newline at end of file diff --git a/ui/src/main/resources/view/default.css b/ui/src/main/resources/view/default.css new file mode 100644 index 000000000..531f6307c --- /dev/null +++ b/ui/src/main/resources/view/default.css @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2019-2023 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + + +.base-spacing { + -fx-padding: 12px; +} + +/*noinspection CssInvalidPseudoSelector,CssUnusedSymbol*/ +.text-field:danger { + -fx-text-box-border: red; + -fx-focus-color: red; +} + +/*noinspection CssInvalidPseudoSelector,CssUnusedSymbol*/ +.text-area:danger { + -fx-background-color: red; + -fx-text-fill: red; +} + +/*noinspection CssInvalidPseudoSelector*/ +.label:danger { + -fx-text-fill: red; +} + +.chat-list-pane { + -fx-background-color: -color-border-default; + -fx-padding: 1px; +} + +.chat-list { + -fx-background-color: -color-bg-default; +} + +/*noinspection CssUnusedSymbol*/ +.chat-list .list-cell { + -fx-padding: 2px 8px 2px 8px; +} + +/*noinspection CssInvalidPseudoSelector,CssUnusedSymbol*/ +.chat-list .list-cell:passive > .label { + -fx-text-fill: -color-fg-subtle; +} + +/*noinspection CssInvalidPseudoSelector,CssUnusedSymbol*/ +.chat-list .list-cell:passive > Text { + -fx-fill: -color-fg-subtle; +} + +.chat-list .list-cell .time { + -fx-padding: 0px 0.25em 0px 0px; + -fx-text-fill: -color-fg-muted; +} + +.chat-list .list-cell .action { + -fx-padding: 0px 0.25em 0px 0px; +} + +/* this is needed to display emojis before sending them */ +.chat-send { + -fx-font-family: "Segoe UI Emoji", "Noto Color Emoji", "Noto Emoji", "Apple Color Emoji"; +} + +.chat-user-list { + -fx-border-width: 0 1 1 1; +} + +#messageHeader { + -fx-background-color: -color-bg-subtle; + -fx-border-color: -color-border-default; + -fx-padding: 8px; +} + +#messagePane { + -fx-border-color: -color-border-default; + -fx-padding: 8px; + -fx-border-width: 0 1 1 1; +} + +.menu-bar { + -fx-background-color: transparent; +} + +/* led */ +.led-control { + -color: black; +} + +.led-status-ok { + -color: #adff2f; +} + +.led-status-warning { + -color: #ffa500; +} + +.led-status-error { + -color: #ff3333; +} + +.led-control .frame { + -fx-background-color: linear-gradient(from 14% 14% to 84% 84%, + rgba(20, 20, 20, 0.64706) 0%, + rgba(20, 20, 20, 0.64706) 15%, + rgba(41, 41, 41, 0.64706) 26%, + rgba(200, 200, 200, 0.40631) 85%, + rgba(200, 200, 200, 0.3451) 100%); + -fx-background-radius: 1024px; +} + +/*noinspection CssInvalidFunction*/ +.led-control .main { + -fx-background-color: linear-gradient(from 15% 15% to 83% 83%, + derive(-color, -80%) 0%, + derive(-color, -87%) 49%, + derive(-color, -80%) 100%); + -fx-background-radius: 1024px; +} + +/*noinspection CssInvalidFunction,CssInvalidPseudoSelector*/ +.led-control:on .main { + -fx-background-color: linear-gradient(from 15% 15% to 83% 83%, + derive(-color, -23%) 0%, + derive(-color, -50%) 49%, + -color 100%); +} + +.led-control .highlight { + -fx-background-color: radial-gradient(center 15% 15%, radius 50%, white 0%, transparent 100%); + -fx-background-radius: 1024; +} + +.credit-card { + -fx-background-color: rgba(255, 255, 255, 0); + -fx-background-radius: 19px; + -fx-border-color: black; + -fx-border-radius: 19px; +} + +#addFriendButton .ikonli-font-icon { + -fx-icon-color: blue; + -fx-icon-size: 24px; +} + +#webHelpButton .ikonli-font-icon { + -fx-icon-color: green; + -fx-icon-size: 24px; +} + +.imageview-avatar { + -fx-background-color: -color-bg-subtle; + -fx-border-color: -color-border-default; + -fx-padding: 1px; +} + +.color-00 { + -fx-text-fill: #CC0000; +} + +.color-01 { + -fx-text-fill: #006CAD; +} + +.color-02 { + -fx-text-fill: #4D9900; +} + +.color-03 { + -fx-text-fill: #6600CC; +} + +.color-04 { + -fx-text-fill: #A67D00; +} + +.color-05 { + -fx-text-fill: #009927; +} + +.color-06 { + -fx-text-fill: #0030C0; +} + +.color-07 { + -fx-text-fill: #CC009A; +} + +.color-08 { + -fx-text-fill: #B94600; +} + +.color-09 { + -fx-text-fill: #869900; +} + +.color-10 { + -fx-text-fill: #149900; +} + +.color-11 { + -fx-text-fill: #009960; +} + +.color-12 { + -fx-text-fill: #006CAD; +} + +.color-13 { + -fx-text-fill: #0099CC; +} + +.color-14 { + -fx-text-fill: #B300CC; +} + +.color-15 { + -fx-text-fill: #CC004D; +} + diff --git a/ui/src/main/resources/view/forum/forum_create.fxml b/ui/src/main/resources/view/forum/forum_create.fxml index f45df5424..2497dc647 100644 --- a/ui/src/main/resources/view/forum/forum_create.fxml +++ b/ui/src/main/resources/view/forum/forum_create.fxml @@ -24,14 +24,14 @@ - + - - + +