diff --git a/src/main/java/com/light/GitManagerApp.java b/src/main/java/com/light/GitManagerApp.java index 76f09a73224083db66d9cc39d3ffd4558cc7c1e4..2179a7e1fea53e934c1a2a4e6c9e57c54d08f4f2 100644 --- a/src/main/java/com/light/GitManagerApp.java +++ b/src/main/java/com/light/GitManagerApp.java @@ -1,9 +1,10 @@ package com.light; -import atlantafx.base.theme.Dracula; -import atlantafx.base.theme.PrimerLight; +import atlantafx.base.controls.ModalPane; +import atlantafx.base.theme.*; import com.light.layout.ContentPane; import com.light.layout.MenuPane; +import com.light.util.FxDataUtil; import com.light.util.FxUtil; import com.light.util.NodeUtils; import javafx.application.Application; @@ -17,21 +18,43 @@ import javafx.stage.Stage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static com.light.util.FxDataUtil.THEME_LIST; +import static com.light.util.NodeUtils.MAIN_MODAL_ID; + public class GitManagerApp extends Application { public static final Logger LOGGER = LoggerFactory.getLogger(GitManagerApp.class); private static final String STYLE_SHEET = FxUtil.getResource("/css/root.css"); + @Override + public void init() throws Exception { + super.init(); + // 初始化主题 + THEME_LIST.add(new PrimerLight()); + THEME_LIST.add(new PrimerDark()); + THEME_LIST.add(new NordLight()); + THEME_LIST.add(new NordDark()); + THEME_LIST.add(new CupertinoLight()); + THEME_LIST.add(new CupertinoDark()); + THEME_LIST.add(new Dracula()); + } + @Override public void start(Stage stage) throws Exception { - // 主题 - Application.setUserAgentStylesheet(new PrimerLight().getUserAgentStylesheet()); -// Application.setUserAgentStylesheet(new Dracula().getUserAgentStylesheet()); + long startTime = System.currentTimeMillis(); + + // 设置主题 + Application.setUserAgentStylesheet(THEME_LIST.get(0).getUserAgentStylesheet()); + FxDataUtil.CURRENT_THEME_NAME.set(THEME_LIST.get(0).getName()); // 根节点 StackPane root = new StackPane(); + // 遮罩层 + var modalPane = new ModalPane(); + modalPane.setId(MAIN_MODAL_ID); + // 左右布局 HBox container = new HBox(); // 内容 @@ -41,7 +64,7 @@ public class GitManagerApp extends Application { container.getChildren().addAll(menuPane, contentPane); HBox.setHgrow(contentPane, Priority.ALWAYS); - root.getChildren().add(container); + root.getChildren().addAll(modalPane, container); NodeUtils.setAnchors(root, Insets.EMPTY); // 场景 @@ -54,7 +77,7 @@ public class GitManagerApp extends Application { stage.show(); scene.getStylesheets().add(STYLE_SHEET); - LOGGER.info("项目启动完成。。。"); + LOGGER.info("项目启动完成。。。{}", System.currentTimeMillis() - startTime); } public static void main(String[] args) { diff --git a/src/main/java/com/light/layout/MenuPane.java b/src/main/java/com/light/layout/MenuPane.java index 9ad67ffa436698009debbabae35978b276bb35a9..68e0cada86749ad2e11a8cd09a0ebed0cdd28e4f 100644 --- a/src/main/java/com/light/layout/MenuPane.java +++ b/src/main/java/com/light/layout/MenuPane.java @@ -1,8 +1,10 @@ package com.light.layout; import atlantafx.base.theme.Styles; +import com.light.theme.ThemeDialog; import com.light.util.FxDataUtil; import com.light.util.FxUtil; +import com.light.util.Lazy; import com.light.view.HomeView; import com.light.view.ManagerView; import javafx.geometry.Orientation; @@ -27,7 +29,7 @@ public class MenuPane extends StackPane { private static final String STYLE_SHEET = FxUtil.getResource("/css/menu.css"); // 顶部导航栏 - private final NavItem setting = new NavItem(new FontIcon(AntDesignIconsFilled.SETTING), "设置", null); + private final NavItem setting = new NavItem(new FontIcon(AntDesignIconsFilled.SETTING), "设置", new Lazy<>(ThemeDialog::new)); private final NavItem notification = new NavItem(new FontIcon(AntDesignIconsFilled.NOTIFICATION), "通知", null); private final VBox topMenu = new VBox(setting, notification); @@ -51,10 +53,14 @@ public class MenuPane extends StackPane { this.contentPane = contentPane; initialize(); + // 子菜单 + Lazy managerViewLazy = new Lazy<>(ManagerView::new); + Lazy homeViewLazy = new Lazy<>(HomeView::new); + // 菜单栏标题 dynamicMenu.getItems().addAll( - new NavItem(new FontIcon(AntDesignIconsOutlined.DOWNLOAD), "下载", new HomeView()), - new NavItem(new FontIcon(AntDesignIconsOutlined.PARTITION), "管理", new ManagerView()), + new NavItem(new FontIcon(AntDesignIconsOutlined.DOWNLOAD), "下载", homeViewLazy), + new NavItem(new FontIcon(AntDesignIconsOutlined.PARTITION), "管理", managerViewLazy), new NavItem(new FontIcon(AntDesignIconsOutlined.EDIT), "笔记", null) ); dynamicMenu.getSelectionModel().selectFirst(); @@ -91,7 +97,8 @@ public class MenuPane extends StackPane { // 加上监听 dynamicMenu.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { Optional.ofNullable(newValue).ifPresent(menuItem -> { - contentPane.setContent(menuItem.getContent());//设置内容 + // 设置内容 + Optional.ofNullable(menuItem.getContent()).ifPresentOrElse(content -> contentPane.setContent(content.get()), () -> contentPane.setContent(null)); }); }); @@ -101,5 +108,11 @@ public class MenuPane extends StackPane { FxDataUtil.UPDATE_PROPERTY.addListener((observable, oldValue, newValue) -> { updateLabelText.setText(newValue.toString()); }); + + // 设置 + setting.setOnMouseClicked(event -> { + ThemeDialog themeDialog = (ThemeDialog) setting.getContent().get(); + themeDialog.show(getScene()); + }); } } diff --git a/src/main/java/com/light/layout/ModalDialog.java b/src/main/java/com/light/layout/ModalDialog.java new file mode 100644 index 0000000000000000000000000000000000000000..c3d2994eb1ea17740f70e5fc6adbbe20147b871c --- /dev/null +++ b/src/main/java/com/light/layout/ModalDialog.java @@ -0,0 +1,70 @@ +/* SPDX-License-Identifier: MIT */ + +package com.light.layout; + +import atlantafx.base.controls.Card; +import atlantafx.base.controls.ModalPane; +import atlantafx.base.controls.Spacer; +import atlantafx.base.controls.Tile; +import atlantafx.base.layout.ModalBox; +import atlantafx.base.theme.Tweaks; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; + +import static com.light.util.NodeUtils.MAIN_MODAL_ID; + +/** + * 遮罩层对话框 + */ +public abstract class ModalDialog extends ModalBox { + protected final Card content = new Card(); + protected final Tile header = new Tile(); + + public ModalDialog() { + super("#" + MAIN_MODAL_ID); + createView(); + } + + public void show(Scene scene) { + var modalPane = (ModalPane) scene.lookup("#" + MAIN_MODAL_ID); + modalPane.show(this); + } + + protected void createView() { + content.setHeader(header); + content.getStyleClass().add(Tweaks.EDGE_TO_EDGE); + + // IMPORTANT: this guarantees client will use correct width and height + setMinWidth(USE_PREF_SIZE); + setMaxWidth(USE_PREF_SIZE); + setMinHeight(USE_PREF_SIZE); + setMaxHeight(USE_PREF_SIZE); + + AnchorPane.setTopAnchor(content, 0d); + AnchorPane.setRightAnchor(content, 0d); + AnchorPane.setBottomAnchor(content, 0d); + AnchorPane.setLeftAnchor(content, 0d); + + addContent(content); + getStyleClass().add("modal-dialog"); + } + + protected HBox createDefaultFooter() { + var closeBtn = new Button("Close"); + closeBtn.getStyleClass().add("form-action"); + closeBtn.setCancelButton(true); + closeBtn.setOnAction(e -> close()); + + var footer = new HBox(10, new Spacer(), closeBtn); + footer.getStyleClass().add("footer"); + footer.setAlignment(Pos.CENTER_RIGHT); + VBox.setVgrow(footer, Priority.NEVER); + + return footer; + } +} diff --git a/src/main/java/com/light/layout/NavItem.java b/src/main/java/com/light/layout/NavItem.java index 76a946da92e3386ff24035d38cd63b7c42c4c4ae..edd90a3956265c08e10e503a2db2f2377ebacf86 100644 --- a/src/main/java/com/light/layout/NavItem.java +++ b/src/main/java/com/light/layout/NavItem.java @@ -1,5 +1,6 @@ package com.light.layout; +import com.light.util.Lazy; import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.layout.HBox; @@ -24,9 +25,9 @@ public class NavItem extends HBox { /** * 内容界面 */ - private Node content; + private Lazy content; - public NavItem(FontIcon fontIcon, String title, Node content) { + public NavItem(FontIcon fontIcon, String title, Lazy content) { this.iconLabel.setGraphic(fontIcon); this.titleLabel.setText(title); this.content = content; @@ -45,7 +46,7 @@ public class NavItem extends HBox { prefWidthProperty().bind(this.widthProperty().subtract(1)); } - public Node getContent() { + public Lazy getContent() { return content; } } diff --git a/src/main/java/com/light/theme/ThemeDialog.java b/src/main/java/com/light/theme/ThemeDialog.java new file mode 100644 index 0000000000000000000000000000000000000000..88e1b02cc3b0c5815ccf7dfc45477b03c0b1f03e --- /dev/null +++ b/src/main/java/com/light/theme/ThemeDialog.java @@ -0,0 +1,65 @@ +/* SPDX-License-Identifier: MIT */ + +package com.light.theme; + +import atlantafx.base.theme.Theme; +import com.light.layout.ModalDialog; +import com.light.util.FxDataUtil; +import javafx.application.Application; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.ToggleGroup; +import javafx.scene.layout.TilePane; +import javafx.scene.layout.VBox; + +import java.util.Objects; + +import static com.light.util.FxDataUtil.THEME_LIST; + +/** + * 主题对话框 + */ +public final class ThemeDialog extends ModalDialog { + + private final TilePane thumbnailsPane = new TilePane(20, 20); + private final ToggleGroup thumbnailsGroup = new ToggleGroup(); + + public ThemeDialog() { + super(); + + header.setTitle("选择主题"); + content.setBody(createContent()); + content.setFooter(null); + + updateThumbnails(); + + thumbnailsGroup.selectedToggleProperty().addListener((obs, old, val) -> { + if (val != null && val.getUserData() instanceof Theme theme) { + Application.setUserAgentStylesheet(theme.getUserAgentStylesheet()); + } + }); + } + + private VBox createContent() { + thumbnailsPane.setAlignment(Pos.TOP_CENTER); + thumbnailsPane.setPrefColumns(3); + thumbnailsPane.setStyle("-color-thumbnail-border:-color-border-subtle;"); + var root = new VBox(thumbnailsPane); + root.setPadding(new Insets(20)); + return root; + } + + private void updateThumbnails() { + thumbnailsPane.getChildren().clear(); + THEME_LIST.forEach(theme -> { + var thumbnail = new ThemeThumbnail(theme); + thumbnail.setToggleGroup(thumbnailsGroup); + thumbnail.setUserData(theme); + thumbnail.setSelected(Objects.equals( + FxDataUtil.CURRENT_THEME_NAME.get(), + theme.getName() + )); + thumbnailsPane.getChildren().add(thumbnail); + }); + } +} diff --git a/src/main/java/com/light/theme/ThemeThumbnail.java b/src/main/java/com/light/theme/ThemeThumbnail.java new file mode 100644 index 0000000000000000000000000000000000000000..3f5cd7126680b8942965c492dc1d2385c8273055 --- /dev/null +++ b/src/main/java/com/light/theme/ThemeThumbnail.java @@ -0,0 +1,194 @@ +/* SPDX-License-Identifier: MIT */ + +package com.light.theme; + +import atlantafx.base.theme.Styles; +import atlantafx.base.theme.Theme; +import com.light.util.FileResource; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ObjectProperty; +import javafx.css.PseudoClass; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.control.RadioButton; +import javafx.scene.control.Toggle; +import javafx.scene.control.ToggleGroup; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import javafx.scene.shape.Circle; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.attribute.FileTime; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.nio.file.LinkOption.NOFOLLOW_LINKS; + +/** + * 主题缩略图 + */ +public final class ThemeThumbnail extends VBox implements Toggle { + + private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected"); + private final RadioButton toggle; // internal helper node to manage selected state + + private final Theme theme; + + private Map colors; + + public ThemeThumbnail(Theme theme) { + super(); + + toggle = new RadioButton(); + this.theme = theme; + + try { + Map colors = this.parseColors(); + + var circles = new HBox( + createCircle(colors.get("-color-fg-default"), colors.get("-color-fg-default"), false), + createCircle(colors.get("-color-fg-default"), colors.get("-color-accent-emphasis"), true), + createCircle(colors.get("-color-fg-default"), colors.get("-color-success-emphasis"), true), + createCircle(colors.get("-color-fg-default"), colors.get("-color-danger-emphasis"), true), + createCircle(colors.get("-color-fg-default"), colors.get("-color-warning-emphasis"), true) + ); + circles.setAlignment(Pos.CENTER); + + var nameLbl = new Label(theme.getName()); + nameLbl.getStyleClass().add(Styles.TEXT_CAPTION); + Styles.appendStyle(nameLbl, "-fx-text-fill", colors.get("-color-fg-muted")); + + setStyle(""" + -fx-background-radius: 10px, 8px; + -fx-background-insets: 0, 3px + """ + ); + Styles.appendStyle( + this, + "-fx-background-color", + "-color-thumbnail-border," + colors.get("-color-bg-default") + ); + setOnMouseClicked(e -> setSelected(true)); + getStyleClass().add("theme-thumbnail"); + getChildren().setAll(nameLbl, circles); + + } catch (IOException e) { + throw new RuntimeException(e.getMessage(), e); + } + + selectedProperty().addListener( + (obs, old, val) -> pseudoClassStateChanged(SELECTED, val) + ); + } + + private Circle createCircle(String borderColor, String bgColor, boolean overlap) { + var circle = new Circle(10); + Styles.appendStyle(circle, "-fx-stroke", borderColor); + Styles.appendStyle(circle, "-fx-fill", bgColor); + if (overlap) { + HBox.setMargin(circle, new Insets(0, 0, 0, -5)); + } + return circle; + } + + @Override + public ToggleGroup getToggleGroup() { + return toggle.getToggleGroup(); + } + + @Override + public void setToggleGroup(ToggleGroup toggleGroup) { + toggle.setToggleGroup(toggleGroup); + } + + @Override + public ObjectProperty toggleGroupProperty() { + return toggle.toggleGroupProperty(); + } + + @Override + public boolean isSelected() { + return toggle.isSelected(); + } + + @Override + public void setSelected(boolean selected) { + toggle.setSelected(selected); + } + + @Override + public BooleanProperty selectedProperty() { + return toggle.selectedProperty(); + } + + @Override + public void setUserData(Object value) { + toggle.setUserData(value); + } + + public Map parseColors() throws IOException { + FileResource file = FileResource.createInternal(theme.getUserAgentStylesheet(), Theme.class); + return file.internal() ? parseColorsForClasspath(file) : parseColorsForFilesystem(file); + } + + private static final Pattern COLOR_PATTERN = + Pattern.compile("\s*?(-color-(fg|bg|accent|success|danger|warning)-.+?):\s*?(.+?);"); + + private static final int PARSE_LIMIT = 250; + + private Map parseColors(BufferedReader br) throws IOException { + Map colors = new HashMap<>(); + + String line; + int lineCount = 0; + + while ((line = br.readLine()) != null) { + Matcher matcher = COLOR_PATTERN.matcher(line); + if (matcher.matches()) { + colors.put(matcher.group(1), matcher.group(3)); + } + + lineCount++; + if (lineCount > PARSE_LIMIT) { + break; + } + } + + return colors; + } + + private Map parseColorsForClasspath(FileResource file) throws IOException { + try (var br = new BufferedReader(new InputStreamReader(file.getInputStream(), UTF_8))) { + colors = parseColors(br); + } + return colors; + } + + private FileTime lastModified; + + private Map parseColorsForFilesystem(FileResource file) throws IOException { + // return cached colors if file wasn't changed since the last read + FileTime fileTime = Files.getLastModifiedTime(file.toPath(), NOFOLLOW_LINKS); + if (Objects.equals(fileTime, lastModified)) { + return colors; + } + + try (var br = new BufferedReader(new InputStreamReader(file.getInputStream(), UTF_8))) { + colors = parseColors(br); + } + + // don't save time before parsing is finished to avoid + // remembering operation that might end up with an error + lastModified = fileTime; + + return colors; + } +} \ No newline at end of file diff --git a/src/main/java/com/light/util/FileResource.java b/src/main/java/com/light/util/FileResource.java new file mode 100644 index 0000000000000000000000000000000000000000..b04de5d763d453cfc5a65329ac528069ca6020ba --- /dev/null +++ b/src/main/java/com/light/util/FileResource.java @@ -0,0 +1,75 @@ +/* SPDX-License-Identifier: MIT */ + +package com.light.util; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Objects; + +public final class FileResource { + + private final String location; + private final boolean internal; + private final Class anchor; + + private FileResource(String location, boolean internal, Class anchor) { + this.location = location; + this.internal = internal; + this.anchor = anchor; + } + + public String location() { + return location; + } + + public boolean internal() { + return internal; + } + + public boolean exists() { + return internal ? anchor.getResource(location) != null : Files.exists(toPath()); + } + + public Path toPath() { + return Paths.get(location); + } + + public URI toURI() { + // the latter adds "file://" scheme to the URI + return internal ? URI.create(location) : Paths.get(location).toUri(); + } + + public String getFilename() { + return Paths.get(location).getFileName().toString(); + } + + public InputStream getInputStream() throws IOException { + if (internal) { + var is = anchor.getResourceAsStream(location); + if (is == null) { + throw new IOException("Resource not found: " + location); + } + return is; + } + return new FileInputStream(toPath().toFile()); + } + + /////////////////////////////////////////////////////////////////////////// + + public static FileResource createInternal(String location) { + return createInternal(location, FileResource.class); + } + + public static FileResource createInternal(String location, Class anchor) { + return new FileResource(location, true, Objects.requireNonNull(anchor)); + } + + public static FileResource createExternal(String location) { + return new FileResource(location, false, null); + } +} \ No newline at end of file diff --git a/src/main/java/com/light/util/FxDataUtil.java b/src/main/java/com/light/util/FxDataUtil.java index 1cbf64f460603919766b9b27e85df124b51cc279..80917778db59809b890f295617f5d0bc70c815bf 100644 --- a/src/main/java/com/light/util/FxDataUtil.java +++ b/src/main/java/com/light/util/FxDataUtil.java @@ -1,10 +1,14 @@ package com.light.util; +import atlantafx.base.theme.Theme; import com.light.model.GitProject; import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleStringProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.atomic.AtomicInteger; /** @@ -25,4 +29,13 @@ public final class FxDataUtil { public static final ObservableList GIT_PROJECT_OBSERVABLE_LIST = FXCollections.observableArrayList(); + /** + * 当前主题民初 + */ + public static final SimpleStringProperty CURRENT_THEME_NAME = new SimpleStringProperty(); + + /** + * 所有主题 + */ + public static final List THEME_LIST = new ArrayList<>(7); } diff --git a/src/main/java/com/light/util/Lazy.java b/src/main/java/com/light/util/Lazy.java new file mode 100644 index 0000000000000000000000000000000000000000..393094b99f332754c6f700fb217420decc9ea84b --- /dev/null +++ b/src/main/java/com/light/util/Lazy.java @@ -0,0 +1,40 @@ +/* SPDX-License-Identifier: MIT */ + +package com.light.util; + +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; +import java.util.function.Supplier; + +/** + * Auxiliary object wrapper to support lazy initialization. + * DO NOT override {@code hashCode()} / {@code equals()}, because each instance + * of this object must remain unique. + */ +public class Lazy implements Supplier { + + protected final Supplier supplier; + protected @Nullable T value = null; + + public Lazy(Supplier supplier) { + this.supplier = Objects.requireNonNull(supplier, "supplier"); + } + + @Override + public T get() { + if (value == null) { + value = supplier.get(); + } + return value; + } + + public boolean initialized() { + return value != null; + } + + @Override + public String toString() { + return String.valueOf(value); + } +} \ No newline at end of file diff --git a/src/main/java/com/light/util/NodeUtils.java b/src/main/java/com/light/util/NodeUtils.java index eeebc219c3912d50da8b0a2ea4655a0bf3b8e073..e30d73692b80d28dd84c5cb00dd82c4792afd0b7 100644 --- a/src/main/java/com/light/util/NodeUtils.java +++ b/src/main/java/com/light/util/NodeUtils.java @@ -5,6 +5,7 @@ import javafx.scene.Node; import javafx.scene.layout.AnchorPane; public final class NodeUtils { + public static final String MAIN_MODAL_ID = "modal-pane"; /** * 设置子节点距AnchorPane锚点布局四边的距离 diff --git a/src/main/resources/css/root.css b/src/main/resources/css/root.css index 6d0966286ea99c91ca3d06cf128e85e8ef940036..26a77258e82096501f642387e3d91f6d9cc4deaa 100644 --- a/src/main/resources/css/root.css +++ b/src/main/resources/css/root.css @@ -1,22 +1,54 @@ @import "colors.css"; -.cf-message{ +.cf-message { -fx-alignment: center-left; -fx-min-height: 40px; -fx-graphic-text-gap: 8px; -fx-padding: 0 10px; - -fx-background-color: rgb(255,255,255); - -fx-background-radius:3px; + -fx-background-color: rgb(255, 255, 255); + -fx-background-radius: 3px; -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.2), 10.0, 0, 0, 0); -fx-text-fill: -cf-text-color; -fx-font-size: 14px; -fx-wrap-text: true; } -.cf-message > .ikonli-font-icon{ + +.cf-message > .ikonli-font-icon { -fx-icon-color: -cf-primary-color; -fx-icon-size: 18px; } -.cf-message.success > .ikonli-font-icon{-fx-icon-color: -cf-success-color;} -.cf-message.info > .ikonli-font-icon{-fx-icon-color: -cf-info-color;} -.cf-message.warn > .ikonli-font-icon{-fx-icon-color: -cf-warn-color;} -.cf-message.danger > .ikonli-font-icon{-fx-icon-color: -cf-danger-color;} \ No newline at end of file + +.cf-message.success > .ikonli-font-icon { + -fx-icon-color: -cf-success-color; +} + +.cf-message.info > .ikonli-font-icon { + -fx-icon-color: -cf-info-color; +} + +.cf-message.warn > .ikonli-font-icon { + -fx-icon-color: -cf-warn-color; +} + +.cf-message.danger > .ikonli-font-icon { + -fx-icon-color: -cf-danger-color; +} + +/*主题缩略图*/ +.theme-thumbnail { + -fx-spacing: 20px; + -fx-padding: 20px; + -fx-alignment: CENTER; +} + +.theme-thumbnail:hover { + -color-thumbnail-border: -color-accent-muted; +} + +.theme-thumbnail:selected { + -color-thumbnail-border: -color-accent-emphasis; +} + +.theme-thumbnail > .label { + -fx-underline: true; +} \ No newline at end of file