|
17 | 17 | */ |
18 | 18 | package org.jackhuang.hmcl.ui.main; |
19 | 19 |
|
| 20 | +import com.jfoenix.controls.JFXButton; |
| 21 | +import com.jfoenix.controls.JFXComboBox; |
| 22 | +import com.jfoenix.controls.JFXRadioButton; |
20 | 23 | import javafx.application.Platform; |
21 | 24 | import javafx.beans.InvalidationListener; |
22 | 25 | import javafx.beans.WeakInvalidationListener; |
23 | 26 | import javafx.beans.binding.Bindings; |
24 | 27 | import javafx.beans.property.ObjectProperty; |
| 28 | +import javafx.geometry.Insets; |
| 29 | +import javafx.geometry.Pos; |
| 30 | +import javafx.geometry.VPos; |
| 31 | +import javafx.scene.Cursor; |
| 32 | +import javafx.scene.control.Label; |
| 33 | +import javafx.scene.control.ScrollPane; |
25 | 34 | import javafx.scene.control.ToggleGroup; |
| 35 | +import javafx.scene.layout.*; |
| 36 | +import javafx.scene.text.TextAlignment; |
26 | 37 | import org.jackhuang.hmcl.Metadata; |
| 38 | +import org.jackhuang.hmcl.setting.EnumCommonDirectory; |
27 | 39 | import org.jackhuang.hmcl.setting.Settings; |
28 | 40 | import org.jackhuang.hmcl.ui.Controllers; |
29 | 41 | import org.jackhuang.hmcl.ui.FXUtils; |
| 42 | +import org.jackhuang.hmcl.ui.SVG; |
| 43 | +import org.jackhuang.hmcl.ui.construct.ComponentList; |
| 44 | +import org.jackhuang.hmcl.ui.construct.ComponentSublist; |
30 | 45 | import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType; |
| 46 | +import org.jackhuang.hmcl.ui.construct.MultiFileItem; |
| 47 | +import org.jackhuang.hmcl.ui.construct.OptionToggleButton; |
31 | 48 | import org.jackhuang.hmcl.upgrade.RemoteVersion; |
32 | 49 | import org.jackhuang.hmcl.upgrade.UpdateChannel; |
33 | 50 | import org.jackhuang.hmcl.upgrade.UpdateChecker; |
34 | 51 | import org.jackhuang.hmcl.upgrade.UpdateHandler; |
35 | 52 | import org.jackhuang.hmcl.util.StringUtils; |
| 53 | +import org.jackhuang.hmcl.util.i18n.I18n; |
36 | 54 | import org.jackhuang.hmcl.util.i18n.SupportedLocale; |
37 | 55 | import org.jackhuang.hmcl.util.io.FileUtils; |
38 | 56 | import org.jackhuang.hmcl.util.io.IOUtils; |
|
45 | 63 | import java.nio.file.Path; |
46 | 64 | import java.time.LocalDateTime; |
47 | 65 | import java.time.format.DateTimeFormatter; |
| 66 | +import java.util.Arrays; |
48 | 67 | import java.util.List; |
49 | 68 | import java.util.Optional; |
50 | 69 | import java.util.zip.GZIPInputStream; |
51 | 70 | import java.util.zip.ZipEntry; |
52 | 71 | import java.util.zip.ZipOutputStream; |
53 | 72 |
|
54 | 73 | import static org.jackhuang.hmcl.setting.ConfigHolder.config; |
| 74 | +import static org.jackhuang.hmcl.ui.FXUtils.stringConverter; |
55 | 75 | import static org.jackhuang.hmcl.util.Lang.thread; |
56 | 76 | import static org.jackhuang.hmcl.util.i18n.I18n.i18n; |
57 | 77 | import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor; |
58 | 78 | import static org.jackhuang.hmcl.util.logging.Logger.LOG; |
59 | 79 |
|
60 | | -public final class SettingsPage extends SettingsView { |
| 80 | +public final class SettingsPage extends ScrollPane { |
61 | 81 |
|
62 | | - private InvalidationListener updateListener; |
| 82 | + @SuppressWarnings("FieldCanBeLocal") |
| 83 | + private final ToggleGroup updateChannelGroup; |
| 84 | + @SuppressWarnings("FieldCanBeLocal") |
| 85 | + private final InvalidationListener updateListener; |
63 | 86 |
|
64 | 87 | public SettingsPage() { |
65 | | - FXUtils.smoothScrolling(scroll); |
66 | | - |
67 | | - // ==== Languages ==== |
68 | | - cboLanguage.getItems().setAll(SupportedLocale.getSupportedLocales()); |
69 | | - selectedItemPropertyFor(cboLanguage).bindBidirectional(config().localizationProperty()); |
70 | | - |
71 | | - disableAutoGameOptionsPane.selectedProperty().bindBidirectional(config().disableAutoGameOptionsProperty()); |
72 | | - // ==== |
73 | | - |
74 | | - fileCommonLocation.selectedDataProperty().bindBidirectional(config().commonDirTypeProperty()); |
75 | | - fileCommonLocationSublist.subtitleProperty().bind( |
76 | | - Bindings.createObjectBinding(() -> Optional.ofNullable(Settings.instance().getCommonDirectory()) |
77 | | - .orElse(i18n("launcher.cache_directory.disabled")), |
78 | | - config().commonDirectoryProperty(), config().commonDirTypeProperty())); |
79 | | - |
80 | | - // ==== Update ==== |
81 | | - FXUtils.installFastTooltip(btnUpdate, i18n("update.tooltip")); |
82 | | - updateListener = any -> { |
83 | | - btnUpdate.setVisible(UpdateChecker.isOutdated()); |
84 | | - |
85 | | - if (UpdateChecker.isOutdated()) { |
86 | | - lblUpdateSub.setText(i18n("update.newest_version", UpdateChecker.getLatestVersion().getVersion())); |
87 | | - lblUpdateSub.getStyleClass().setAll("update-label"); |
88 | | - |
89 | | - lblUpdate.setText(i18n("update.found")); |
90 | | - lblUpdate.getStyleClass().setAll("update-label"); |
91 | | - } else if (UpdateChecker.isCheckingUpdate()) { |
92 | | - lblUpdateSub.setText(i18n("update.checking")); |
93 | | - lblUpdateSub.getStyleClass().setAll("subtitle-label"); |
94 | | - |
95 | | - lblUpdate.setText(i18n("update")); |
96 | | - lblUpdate.getStyleClass().setAll("title-label"); |
97 | | - } else { |
98 | | - lblUpdateSub.setText(i18n("update.latest")); |
99 | | - lblUpdateSub.getStyleClass().setAll("subtitle-label"); |
100 | | - |
101 | | - lblUpdate.setText(i18n("update")); |
102 | | - lblUpdate.getStyleClass().setAll("title-label"); |
| 88 | + this.setFitToWidth(true); |
| 89 | + |
| 90 | + VBox rootPane = new VBox(); |
| 91 | + rootPane.setPadding(new Insets(32, 10, 32, 10)); |
| 92 | + this.setContent(rootPane); |
| 93 | + FXUtils.smoothScrolling(this); |
| 94 | + |
| 95 | + ComponentList settingsPane = new ComponentList(); |
| 96 | + { |
| 97 | + { |
| 98 | + StackPane sponsorPane = new StackPane(); |
| 99 | + sponsorPane.setCursor(Cursor.HAND); |
| 100 | + FXUtils.onClicked(sponsorPane, this::onSponsor); |
| 101 | + sponsorPane.setPadding(new Insets(8, 0, 8, 0)); |
| 102 | + |
| 103 | + GridPane gridPane = new GridPane(); |
| 104 | + |
| 105 | + ColumnConstraints col = new ColumnConstraints(); |
| 106 | + col.setHgrow(Priority.SOMETIMES); |
| 107 | + col.setMaxWidth(Double.POSITIVE_INFINITY); |
| 108 | + |
| 109 | + gridPane.getColumnConstraints().setAll(col); |
| 110 | + |
| 111 | + RowConstraints row = new RowConstraints(); |
| 112 | + row.setMinHeight(Double.NEGATIVE_INFINITY); |
| 113 | + row.setValignment(VPos.TOP); |
| 114 | + row.setVgrow(Priority.SOMETIMES); |
| 115 | + gridPane.getRowConstraints().setAll(row); |
| 116 | + |
| 117 | + { |
| 118 | + Label label = new Label(i18n("sponsor.hmcl")); |
| 119 | + label.setWrapText(true); |
| 120 | + label.setTextAlignment(TextAlignment.JUSTIFY); |
| 121 | + GridPane.setRowIndex(label, 0); |
| 122 | + GridPane.setColumnIndex(label, 0); |
| 123 | + gridPane.getChildren().add(label); |
| 124 | + } |
| 125 | + |
| 126 | + sponsorPane.getChildren().setAll(gridPane); |
| 127 | + settingsPane.getContent().add(sponsorPane); |
| 128 | + } |
| 129 | + |
| 130 | + { |
| 131 | + ComponentSublist updatePane = new ComponentSublist(); |
| 132 | + updatePane.setTitle(i18n("update")); |
| 133 | + updatePane.setHasSubtitle(true); |
| 134 | + |
| 135 | + final Label lblUpdate; |
| 136 | + final Label lblUpdateSub; |
| 137 | + { |
| 138 | + VBox headerLeft = new VBox(); |
| 139 | + |
| 140 | + lblUpdate = new Label(i18n("update")); |
| 141 | + lblUpdate.getStyleClass().add("title-label"); |
| 142 | + lblUpdateSub = new Label(); |
| 143 | + lblUpdateSub.getStyleClass().add("subtitle-label"); |
| 144 | + |
| 145 | + headerLeft.getChildren().setAll(lblUpdate, lblUpdateSub); |
| 146 | + updatePane.setHeaderLeft(headerLeft); |
| 147 | + } |
| 148 | + |
| 149 | + { |
| 150 | + JFXButton btnUpdate = new JFXButton(); |
| 151 | + btnUpdate.setOnAction(e -> onUpdate()); |
| 152 | + btnUpdate.getStyleClass().add("toggle-icon4"); |
| 153 | + btnUpdate.setGraphic(SVG.UPDATE.createIcon(20)); |
| 154 | + FXUtils.installFastTooltip(btnUpdate, i18n("update.tooltip")); |
| 155 | + |
| 156 | + updateListener = any -> { |
| 157 | + btnUpdate.setVisible(UpdateChecker.isOutdated()); |
| 158 | + |
| 159 | + if (UpdateChecker.isOutdated()) { |
| 160 | + lblUpdateSub.setText(i18n("update.newest_version", UpdateChecker.getLatestVersion().getVersion())); |
| 161 | + lblUpdateSub.getStyleClass().setAll("update-label"); |
| 162 | + |
| 163 | + lblUpdate.setText(i18n("update.found")); |
| 164 | + lblUpdate.getStyleClass().setAll("update-label"); |
| 165 | + } else if (UpdateChecker.isCheckingUpdate()) { |
| 166 | + lblUpdateSub.setText(i18n("update.checking")); |
| 167 | + lblUpdateSub.getStyleClass().setAll("subtitle-label"); |
| 168 | + |
| 169 | + lblUpdate.setText(i18n("update")); |
| 170 | + lblUpdate.getStyleClass().setAll("title-label"); |
| 171 | + } else { |
| 172 | + lblUpdateSub.setText(i18n("update.latest")); |
| 173 | + lblUpdateSub.getStyleClass().setAll("subtitle-label"); |
| 174 | + |
| 175 | + lblUpdate.setText(i18n("update")); |
| 176 | + lblUpdate.getStyleClass().setAll("title-label"); |
| 177 | + } |
| 178 | + }; |
| 179 | + UpdateChecker.latestVersionProperty().addListener(new WeakInvalidationListener(updateListener)); |
| 180 | + UpdateChecker.outdatedProperty().addListener(new WeakInvalidationListener(updateListener)); |
| 181 | + UpdateChecker.checkingUpdateProperty().addListener(new WeakInvalidationListener(updateListener)); |
| 182 | + updateListener.invalidated(null); |
| 183 | + |
| 184 | + updatePane.setHeaderRight(btnUpdate); |
| 185 | + } |
| 186 | + |
| 187 | + { |
| 188 | + VBox content = new VBox(); |
| 189 | + content.setSpacing(8); |
| 190 | + |
| 191 | + JFXRadioButton chkUpdateStable = new JFXRadioButton(i18n("update.channel.stable")); |
| 192 | + JFXRadioButton chkUpdateDev = new JFXRadioButton(i18n("update.channel.dev")); |
| 193 | + |
| 194 | + updateChannelGroup = new ToggleGroup(); |
| 195 | + chkUpdateDev.setToggleGroup(updateChannelGroup); |
| 196 | + chkUpdateDev.setUserData(UpdateChannel.DEVELOPMENT); |
| 197 | + chkUpdateStable.setToggleGroup(updateChannelGroup); |
| 198 | + chkUpdateStable.setUserData(UpdateChannel.STABLE); |
| 199 | + |
| 200 | + Label noteWrapper = new Label(i18n("update.note")); |
| 201 | + VBox.setMargin(noteWrapper, new Insets(10, 0, 0, 0)); |
| 202 | + |
| 203 | + content.getChildren().setAll(chkUpdateStable, chkUpdateDev, noteWrapper); |
| 204 | + |
| 205 | + updatePane.getContent().add(content); |
| 206 | + } |
| 207 | + settingsPane.getContent().add(updatePane); |
103 | 208 | } |
104 | | - }; |
105 | | - UpdateChecker.latestVersionProperty().addListener(new WeakInvalidationListener(updateListener)); |
106 | | - UpdateChecker.outdatedProperty().addListener(new WeakInvalidationListener(updateListener)); |
107 | | - UpdateChecker.checkingUpdateProperty().addListener(new WeakInvalidationListener(updateListener)); |
108 | | - updateListener.invalidated(null); |
109 | | - |
110 | | - ToggleGroup updateChannelGroup = new ToggleGroup(); |
111 | | - chkUpdateDev.setToggleGroup(updateChannelGroup); |
112 | | - chkUpdateDev.setUserData(UpdateChannel.DEVELOPMENT); |
113 | | - chkUpdateStable.setToggleGroup(updateChannelGroup); |
114 | | - chkUpdateStable.setUserData(UpdateChannel.STABLE); |
115 | | - ObjectProperty<UpdateChannel> updateChannel = selectedItemPropertyFor(updateChannelGroup, UpdateChannel.class); |
116 | | - updateChannel.set(UpdateChannel.getChannel()); |
117 | | - |
118 | | - InvalidationListener checkUpdateListener = e -> { |
119 | | - UpdateChecker.requestCheckUpdate(updateChannel.get(), previewPane.isSelected()); |
120 | | - }; |
121 | | - updateChannel.addListener(checkUpdateListener); |
122 | | - previewPane.selectedProperty().addListener(checkUpdateListener); |
123 | | - // ==== |
| 209 | + |
| 210 | + { |
| 211 | + OptionToggleButton previewPane = new OptionToggleButton(); |
| 212 | + previewPane.setTitle(i18n("update.preview")); |
| 213 | + previewPane.selectedProperty().bindBidirectional(config().acceptPreviewUpdateProperty()); |
| 214 | + FXUtils.installFastTooltip(previewPane, i18n("update.preview.tooltip")); |
| 215 | + |
| 216 | + ObjectProperty<UpdateChannel> updateChannel = selectedItemPropertyFor(updateChannelGroup, UpdateChannel.class); |
| 217 | + updateChannel.set(UpdateChannel.getChannel()); |
| 218 | + InvalidationListener checkUpdateListener = e -> { |
| 219 | + UpdateChecker.requestCheckUpdate(updateChannel.get(), previewPane.isSelected()); |
| 220 | + }; |
| 221 | + updateChannel.addListener(checkUpdateListener); |
| 222 | + previewPane.selectedProperty().addListener(checkUpdateListener); |
| 223 | + |
| 224 | + settingsPane.getContent().add(previewPane); |
| 225 | + } |
| 226 | + |
| 227 | + { |
| 228 | + MultiFileItem<EnumCommonDirectory> fileCommonLocation = new MultiFileItem<>(); |
| 229 | + fileCommonLocation.selectedDataProperty().bindBidirectional(config().commonDirTypeProperty()); |
| 230 | + fileCommonLocation.loadChildren(Arrays.asList( |
| 231 | + new MultiFileItem.Option<>(i18n("launcher.cache_directory.default"), EnumCommonDirectory.DEFAULT), |
| 232 | + new MultiFileItem.FileOption<>(i18n("settings.custom"), EnumCommonDirectory.CUSTOM) |
| 233 | + .setChooserTitle(i18n("launcher.cache_directory.choose")) |
| 234 | + .setDirectory(true) |
| 235 | + .bindBidirectional(config().commonDirectoryProperty()) |
| 236 | + )); |
| 237 | + |
| 238 | + ComponentSublist fileCommonLocationSublist = new ComponentSublist(); |
| 239 | + fileCommonLocationSublist.getContent().add(fileCommonLocation); |
| 240 | + fileCommonLocationSublist.setTitle(i18n("launcher.cache_directory")); |
| 241 | + fileCommonLocationSublist.setHasSubtitle(true); |
| 242 | + fileCommonLocationSublist.subtitleProperty().bind( |
| 243 | + Bindings.createObjectBinding(() -> Optional.ofNullable(Settings.instance().getCommonDirectory()) |
| 244 | + .orElse(i18n("launcher.cache_directory.disabled")), |
| 245 | + config().commonDirectoryProperty(), config().commonDirTypeProperty())); |
| 246 | + |
| 247 | + JFXButton cleanButton = FXUtils.newBorderButton(i18n("launcher.cache_directory.clean")); |
| 248 | + cleanButton.setOnAction(e -> clearCacheDirectory()); |
| 249 | + fileCommonLocationSublist.setHeaderRight(cleanButton); |
| 250 | + |
| 251 | + settingsPane.getContent().add(fileCommonLocationSublist); |
| 252 | + } |
| 253 | + |
| 254 | + { |
| 255 | + BorderPane languagePane = new BorderPane(); |
| 256 | + |
| 257 | + Label left = new Label(i18n("settings.launcher.language")); |
| 258 | + BorderPane.setAlignment(left, Pos.CENTER_LEFT); |
| 259 | + languagePane.setLeft(left); |
| 260 | + |
| 261 | + SupportedLocale currentLocale = I18n.getLocale(); |
| 262 | + JFXComboBox<SupportedLocale> cboLanguage = new JFXComboBox<>(); |
| 263 | + cboLanguage.setConverter(stringConverter(locale -> { |
| 264 | + if (locale.isDefault()) |
| 265 | + return locale.getDisplayName(currentLocale); |
| 266 | + else if (locale.isSameLanguage(currentLocale)) |
| 267 | + return locale.getDisplayName(locale); |
| 268 | + else |
| 269 | + return locale.getDisplayName(currentLocale) + " - " + locale.getDisplayName(locale); |
| 270 | + })); |
| 271 | + cboLanguage.getItems().setAll(SupportedLocale.getSupportedLocales()); |
| 272 | + selectedItemPropertyFor(cboLanguage).bindBidirectional(config().localizationProperty()); |
| 273 | + |
| 274 | + FXUtils.setLimitWidth(cboLanguage, 300); |
| 275 | + languagePane.setRight(cboLanguage); |
| 276 | + |
| 277 | + settingsPane.getContent().add(languagePane); |
| 278 | + } |
| 279 | + |
| 280 | + { |
| 281 | + OptionToggleButton disableAutoGameOptionsPane = new OptionToggleButton(); |
| 282 | + disableAutoGameOptionsPane.setTitle(i18n("settings.launcher.disable_auto_game_options")); |
| 283 | + disableAutoGameOptionsPane.selectedProperty().bindBidirectional(config().disableAutoGameOptionsProperty()); |
| 284 | + |
| 285 | + settingsPane.getContent().add(disableAutoGameOptionsPane); |
| 286 | + } |
| 287 | + |
| 288 | + { |
| 289 | + BorderPane debugPane = new BorderPane(); |
| 290 | + |
| 291 | + Label left = new Label(i18n("settings.launcher.debug")); |
| 292 | + BorderPane.setAlignment(left, Pos.CENTER_LEFT); |
| 293 | + debugPane.setLeft(left); |
| 294 | + |
| 295 | + JFXButton openLogFolderButton = new JFXButton(i18n("settings.launcher.launcher_log.reveal")); |
| 296 | + openLogFolderButton.setOnAction(e -> openLogFolder()); |
| 297 | + openLogFolderButton.getStyleClass().add("jfx-button-border"); |
| 298 | + if (LOG.getLogFile() == null) |
| 299 | + openLogFolderButton.setDisable(true); |
| 300 | + |
| 301 | + JFXButton logButton = FXUtils.newBorderButton(i18n("settings.launcher.launcher_log.export")); |
| 302 | + logButton.setOnAction(e -> onExportLogs()); |
| 303 | + |
| 304 | + HBox buttonBox = new HBox(); |
| 305 | + buttonBox.setSpacing(10); |
| 306 | + buttonBox.getChildren().addAll(openLogFolderButton, logButton); |
| 307 | + BorderPane.setAlignment(buttonBox, Pos.CENTER_RIGHT); |
| 308 | + debugPane.setRight(buttonBox); |
| 309 | + |
| 310 | + settingsPane.getContent().add(debugPane); |
| 311 | + } |
| 312 | + |
| 313 | + rootPane.getChildren().add(settingsPane); |
| 314 | + } |
| 315 | + } |
| 316 | + |
| 317 | + private void openLogFolder() { |
| 318 | + FXUtils.openFolder(LOG.getLogFile().getParent()); |
124 | 319 | } |
125 | 320 |
|
126 | | - @Override |
127 | | - protected void onUpdate() { |
| 321 | + private void onUpdate() { |
128 | 322 | RemoteVersion target = UpdateChecker.getLatestVersion(); |
129 | 323 | if (target == null) { |
130 | 324 | return; |
@@ -168,8 +362,7 @@ private static boolean exportLogFile(ZipOutputStream output, |
168 | 362 | } |
169 | 363 | } |
170 | 364 |
|
171 | | - @Override |
172 | | - protected void onExportLogs() { |
| 365 | + private void onExportLogs() { |
173 | 366 | thread(() -> { |
174 | 367 | String nameBase = "hmcl-exported-logs-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")); |
175 | 368 | List<Path> recentLogFiles = LOG.findRecentLogFiles(5); |
@@ -249,13 +442,11 @@ protected void onExportLogs() { |
249 | 442 | }); |
250 | 443 | } |
251 | 444 |
|
252 | | - @Override |
253 | | - protected void onSponsor() { |
| 445 | + private void onSponsor() { |
254 | 446 | FXUtils.openLink("https://github.com/HMCL-dev/HMCL"); |
255 | 447 | } |
256 | 448 |
|
257 | | - @Override |
258 | | - protected void clearCacheDirectory() { |
| 449 | + private void clearCacheDirectory() { |
259 | 450 | String commonDirectory = Settings.instance().getCommonDirectory(); |
260 | 451 | if (commonDirectory != null) { |
261 | 452 | FileUtils.cleanDirectoryQuietly(Path.of(commonDirectory, "cache")); |
|
0 commit comments