重构 FutureCallback (#5268)
This commit is contained in:
@@ -325,13 +325,13 @@ public final class LauncherHelper {
|
|||||||
message = i18n("launch.failed.command_too_long");
|
message = i18n("launch.failed.command_too_long");
|
||||||
} else if (ex instanceof ExecutionPolicyLimitException) {
|
} else if (ex instanceof ExecutionPolicyLimitException) {
|
||||||
Controllers.prompt(new PromptDialogPane.Builder(i18n("launch.failed.execution_policy"),
|
Controllers.prompt(new PromptDialogPane.Builder(i18n("launch.failed.execution_policy"),
|
||||||
(result, resolve, reject) -> {
|
(result, handler) -> {
|
||||||
if (CommandBuilder.setExecutionPolicy()) {
|
if (CommandBuilder.setExecutionPolicy()) {
|
||||||
LOG.info("Set the ExecutionPolicy for the scope 'CurrentUser' to 'RemoteSigned'");
|
LOG.info("Set the ExecutionPolicy for the scope 'CurrentUser' to 'RemoteSigned'");
|
||||||
resolve.run();
|
handler.resolve();
|
||||||
} else {
|
} else {
|
||||||
LOG.warning("Failed to set ExecutionPolicy");
|
LOG.warning("Failed to set ExecutionPolicy");
|
||||||
reject.accept(i18n("launch.failed.execution_policy.failed_to_set"));
|
handler.reject(i18n("launch.failed.execution_policy.failed_to_set"));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.addQuestion(new PromptDialogPane.Builder.HintQuestion(i18n("launch.failed.execution_policy.hint")))
|
.addQuestion(new PromptDialogPane.Builder.HintQuestion(i18n("launch.failed.execution_policy.hint")))
|
||||||
|
|||||||
@@ -72,13 +72,19 @@ public class InputDialogPane extends JFXDialogLayout implements DialogAware {
|
|||||||
acceptButton.setOnAction(e -> {
|
acceptButton.setOnAction(e -> {
|
||||||
acceptPane.showSpinner();
|
acceptPane.showSpinner();
|
||||||
|
|
||||||
onResult.call(textField.getText(), () -> {
|
onResult.call(textField.getText(), new FutureCallback.ResultHandler() {
|
||||||
|
@Override
|
||||||
|
public void resolve() {
|
||||||
acceptPane.hideSpinner();
|
acceptPane.hideSpinner();
|
||||||
future.complete(textField.getText());
|
future.complete(textField.getText());
|
||||||
fireEvent(new DialogCloseEvent());
|
fireEvent(new DialogCloseEvent());
|
||||||
}, msg -> {
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reject(String reason) {
|
||||||
acceptPane.hideSpinner();
|
acceptPane.hideSpinner();
|
||||||
lblCreationWarning.setText(msg);
|
lblCreationWarning.setText(reason);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
textField.setOnAction(event -> acceptButton.fire());
|
textField.setOnAction(event -> acceptButton.fire());
|
||||||
|
|||||||
@@ -117,11 +117,17 @@ public class PromptDialogPane extends DialogPane {
|
|||||||
protected void onAccept() {
|
protected void onAccept() {
|
||||||
setLoading();
|
setLoading();
|
||||||
|
|
||||||
builder.callback.call(builder.questions, () -> {
|
builder.callback.call(builder.questions, new FutureCallback.ResultHandler() {
|
||||||
|
@Override
|
||||||
|
public void resolve() {
|
||||||
future.complete(builder.questions);
|
future.complete(builder.questions);
|
||||||
runInFX(this::onSuccess);
|
runInFX(() -> onSuccess());
|
||||||
}, msg -> {
|
}
|
||||||
runInFX(() -> onFailure(msg));
|
|
||||||
|
@Override
|
||||||
|
public void reject(String reason) {
|
||||||
|
runInFX(() -> onFailure(reason));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
|||||||
|
|
||||||
Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version) : profile.getRepository().getBaseDirectory();
|
Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version) : profile.getRepository().getBaseDirectory();
|
||||||
|
|
||||||
Controllers.prompt(i18n("archive.file.name"), (result, resolve, reject) -> {
|
Controllers.prompt(i18n("archive.file.name"), (result, handler) -> {
|
||||||
Path dest = runDirectory.resolve(subdirectoryName).resolve(result);
|
Path dest = runDirectory.resolve(subdirectoryName).resolve(result);
|
||||||
|
|
||||||
Controllers.taskDialog(Task.composeAsync(() -> {
|
Controllers.taskDialog(Task.composeAsync(() -> {
|
||||||
@@ -152,7 +152,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
|||||||
Controllers.showToast(i18n("install.success"));
|
Controllers.showToast(i18n("install.success"));
|
||||||
}
|
}
|
||||||
}), i18n("message.downloading"), TaskCancellationAction.NORMAL);
|
}), i18n("message.downloading"), TaskCancellationAction.NORMAL);
|
||||||
resolve.run();
|
handler.resolve();
|
||||||
}, file.getFile().getFilename(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid));
|
}, file.getFile().getFilename(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,37 +124,37 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void onChooseRemoteFile() {
|
private void onChooseRemoteFile() {
|
||||||
Controllers.prompt(i18n("modpack.choose.remote.tooltip"), (url, resolve, reject) -> {
|
Controllers.prompt(i18n("modpack.choose.remote.tooltip"), (url, handler) -> {
|
||||||
try {
|
try {
|
||||||
if (url.endsWith("server-manifest.json")) {
|
if (url.endsWith("server-manifest.json")) {
|
||||||
// if urlString ends with .json, we assume that the url is server-manifest.json
|
// if urlString ends with .json, we assume that the url is server-manifest.json
|
||||||
Controllers.taskDialog(new GetTask(url).whenComplete(Schedulers.javafx(), (result, e) -> {
|
Controllers.taskDialog(new GetTask(url).whenComplete(Schedulers.javafx(), (result, e) -> {
|
||||||
ServerModpackManifest manifest = JsonUtils.fromMaybeMalformedJson(result, ServerModpackManifest.class);
|
ServerModpackManifest manifest = JsonUtils.fromMaybeMalformedJson(result, ServerModpackManifest.class);
|
||||||
if (manifest == null) {
|
if (manifest == null) {
|
||||||
reject.accept(i18n("modpack.type.server.malformed"));
|
handler.reject(i18n("modpack.type.server.malformed"));
|
||||||
} else if (e == null) {
|
} else if (e == null) {
|
||||||
resolve.run();
|
handler.resolve();
|
||||||
controller.getSettings().put(MODPACK_SERVER_MANIFEST, manifest);
|
controller.getSettings().put(MODPACK_SERVER_MANIFEST, manifest);
|
||||||
controller.onNext();
|
controller.onNext();
|
||||||
} else {
|
} else {
|
||||||
reject.accept(e.getMessage());
|
handler.reject(e.getMessage());
|
||||||
}
|
}
|
||||||
}).executor(true), i18n("message.downloading"), TaskCancellationAction.NORMAL);
|
}).executor(true), i18n("message.downloading"), TaskCancellationAction.NORMAL);
|
||||||
} else {
|
} else {
|
||||||
// otherwise we still consider the file as modpack zip file
|
// otherwise we still consider the file as modpack zip file
|
||||||
// since casually the url may not ends with ".zip"
|
// since casually the url may not ends with ".zip"
|
||||||
Path modpack = Files.createTempFile("modpack", ".zip");
|
Path modpack = Files.createTempFile("modpack", ".zip");
|
||||||
resolve.run();
|
handler.resolve();
|
||||||
|
|
||||||
Controllers.taskDialog(
|
Controllers.taskDialog(
|
||||||
new FileDownloadTask(url, modpack)
|
new FileDownloadTask(url, modpack)
|
||||||
.whenComplete(Schedulers.javafx(), e -> {
|
.whenComplete(Schedulers.javafx(), e -> {
|
||||||
if (e == null) {
|
if (e == null) {
|
||||||
resolve.run();
|
handler.resolve();
|
||||||
controller.getSettings().put(MODPACK_FILE, modpack);
|
controller.getSettings().put(MODPACK_FILE, modpack);
|
||||||
controller.onNext();
|
controller.onNext();
|
||||||
} else {
|
} else {
|
||||||
reject.accept(e.getMessage());
|
handler.reject(e.getMessage());
|
||||||
}
|
}
|
||||||
}).executor(true),
|
}).executor(true),
|
||||||
i18n("message.downloading"),
|
i18n("message.downloading"),
|
||||||
@@ -162,7 +162,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
reject.accept(e.getMessage());
|
handler.reject(e.getMessage());
|
||||||
}
|
}
|
||||||
}, "", new URLValidator());
|
}, "", new URLValidator());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -245,19 +245,19 @@ public class TerracottaControllerPage extends StackPane {
|
|||||||
guest.setSubtitle(i18n("terracotta.status.waiting.guest.desc"));
|
guest.setSubtitle(i18n("terracotta.status.waiting.guest.desc"));
|
||||||
guest.setRightIcon(SVG.ARROW_FORWARD);
|
guest.setRightIcon(SVG.ARROW_FORWARD);
|
||||||
FXUtils.onClicked(guest, () -> {
|
FXUtils.onClicked(guest, () -> {
|
||||||
Controllers.prompt(i18n("terracotta.status.waiting.guest.prompt.title"), (code, resolve, reject) -> {
|
Controllers.prompt(i18n("terracotta.status.waiting.guest.prompt.title"), (code, handler) -> {
|
||||||
Task<TerracottaState.GuestConnecting> task = TerracottaManager.setGuesting(code);
|
Task<TerracottaState.GuestConnecting> task = TerracottaManager.setGuesting(code);
|
||||||
if (task != null) {
|
if (task != null) {
|
||||||
task.whenComplete(Schedulers.javafx(), (s, e) -> {
|
task.whenComplete(Schedulers.javafx(), (s, e) -> {
|
||||||
if (e != null) {
|
if (e != null) {
|
||||||
reject.accept(i18n("terracotta.status.waiting.guest.prompt.invalid"));
|
handler.reject(i18n("terracotta.status.waiting.guest.prompt.invalid"));
|
||||||
} else {
|
} else {
|
||||||
resolve.run();
|
handler.resolve();
|
||||||
UI_STATE.set(s);
|
UI_STATE.set(s);
|
||||||
}
|
}
|
||||||
}).setSignificance(Task.TaskSignificance.MINOR).start();
|
}).setSignificance(Task.TaskSignificance.MINOR).start();
|
||||||
} else {
|
} else {
|
||||||
resolve.run();
|
handler.resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -163,30 +163,30 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
|
|||||||
Controllers.dialog(new InputDialogPane(
|
Controllers.dialog(new InputDialogPane(
|
||||||
i18n("schematics.create_directory.prompt"),
|
i18n("schematics.create_directory.prompt"),
|
||||||
"",
|
"",
|
||||||
(result, resolve, reject) -> {
|
(result, handler) -> {
|
||||||
if (StringUtils.isBlank(result)) {
|
if (StringUtils.isBlank(result)) {
|
||||||
reject.accept(i18n("schematics.create_directory.failed.empty_name"));
|
handler.reject(i18n("schematics.create_directory.failed.empty_name"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.contains("/") || result.contains("\\") || !FileUtils.isNameValid(result)) {
|
if (result.contains("/") || result.contains("\\") || !FileUtils.isNameValid(result)) {
|
||||||
reject.accept(i18n("schematics.create_directory.failed.invalid_name"));
|
handler.reject(i18n("schematics.create_directory.failed.invalid_name"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Path targetDir = parent.resolve(result);
|
Path targetDir = parent.resolve(result);
|
||||||
if (Files.exists(targetDir)) {
|
if (Files.exists(targetDir)) {
|
||||||
reject.accept(i18n("schematics.create_directory.failed.already_exists"));
|
handler.reject(i18n("schematics.create_directory.failed.already_exists"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Files.createDirectories(targetDir);
|
Files.createDirectories(targetDir);
|
||||||
resolve.run();
|
handler.resolve();
|
||||||
refresh();
|
refresh();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOG.warning("Failed to create directory: " + targetDir, e);
|
LOG.warning("Failed to create directory: " + targetDir, e);
|
||||||
reject.accept(i18n("schematics.create_directory.failed", targetDir));
|
handler.reject(i18n("schematics.create_directory.failed", targetDir));
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,13 +129,13 @@ public final class Versions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static CompletableFuture<String> renameVersion(Profile profile, String version) {
|
public static CompletableFuture<String> renameVersion(Profile profile, String version) {
|
||||||
return Controllers.prompt(i18n("version.manage.rename.message"), (newName, resolve, reject) -> {
|
return Controllers.prompt(i18n("version.manage.rename.message"), (newName, handler) -> {
|
||||||
if (newName.equals(version)) {
|
if (newName.equals(version)) {
|
||||||
resolve.run();
|
handler.resolve();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (profile.getRepository().renameVersion(version, newName)) {
|
if (profile.getRepository().renameVersion(version, newName)) {
|
||||||
resolve.run();
|
handler.resolve();
|
||||||
profile.getRepository().refreshVersionsAsync()
|
profile.getRepository().refreshVersionsAsync()
|
||||||
.thenRunAsync(Schedulers.javafx(), () -> {
|
.thenRunAsync(Schedulers.javafx(), () -> {
|
||||||
if (profile.getRepository().hasVersion(newName)) {
|
if (profile.getRepository().hasVersion(newName)) {
|
||||||
@@ -143,7 +143,7 @@ public final class Versions {
|
|||||||
}
|
}
|
||||||
}).start();
|
}).start();
|
||||||
} else {
|
} else {
|
||||||
reject.accept(i18n("version.manage.rename.fail"));
|
handler.reject(i18n("version.manage.rename.fail"));
|
||||||
}
|
}
|
||||||
}, version, new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId),
|
}, version, new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId),
|
||||||
new Validator(i18n("install.new_game.already_exists"), newVersionName -> !profile.getRepository().versionIdConflicts(newVersionName) || newVersionName.equals(version)));
|
new Validator(i18n("install.new_game.already_exists"), newVersionName -> !profile.getRepository().versionIdConflicts(newVersionName) || newVersionName.equals(version)));
|
||||||
@@ -159,16 +159,16 @@ public final class Versions {
|
|||||||
|
|
||||||
public static void duplicateVersion(Profile profile, String version) {
|
public static void duplicateVersion(Profile profile, String version) {
|
||||||
Controllers.prompt(
|
Controllers.prompt(
|
||||||
new PromptDialogPane.Builder(i18n("version.manage.duplicate.prompt"), (res, resolve, reject) -> {
|
new PromptDialogPane.Builder(i18n("version.manage.duplicate.prompt"), (res, handler) -> {
|
||||||
String newVersionName = ((PromptDialogPane.Builder.StringQuestion) res.get(1)).getValue();
|
String newVersionName = ((PromptDialogPane.Builder.StringQuestion) res.get(1)).getValue();
|
||||||
boolean copySaves = ((PromptDialogPane.Builder.BooleanQuestion) res.get(2)).getValue();
|
boolean copySaves = ((PromptDialogPane.Builder.BooleanQuestion) res.get(2)).getValue();
|
||||||
Task.runAsync(() -> profile.getRepository().duplicateVersion(version, newVersionName, copySaves))
|
Task.runAsync(() -> profile.getRepository().duplicateVersion(version, newVersionName, copySaves))
|
||||||
.thenComposeAsync(profile.getRepository().refreshVersionsAsync())
|
.thenComposeAsync(profile.getRepository().refreshVersionsAsync())
|
||||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||||
if (exception == null) {
|
if (exception == null) {
|
||||||
resolve.run();
|
handler.resolve();
|
||||||
} else {
|
} else {
|
||||||
reject.accept(StringUtils.getStackTrace(exception));
|
handler.reject(StringUtils.getStackTrace(exception));
|
||||||
if (!profile.getRepository().versionIdConflicts(newVersionName)) {
|
if (!profile.getRepository().versionIdConflicts(newVersionName)) {
|
||||||
profile.getRepository().removeVersionFromDisk(newVersionName);
|
profile.getRepository().removeVersionFromDisk(newVersionName);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,18 +156,18 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
|||||||
// Or too many input dialogs are popped.
|
// Or too many input dialogs are popped.
|
||||||
Task.supplyAsync(() -> new World(zipFile))
|
Task.supplyAsync(() -> new World(zipFile))
|
||||||
.whenComplete(Schedulers.javafx(), world -> {
|
.whenComplete(Schedulers.javafx(), world -> {
|
||||||
Controllers.prompt(i18n("world.name.enter"), (name, resolve, reject) -> {
|
Controllers.prompt(i18n("world.name.enter"), (name, handler) -> {
|
||||||
Task.runAsync(() -> world.install(savesDir, name))
|
Task.runAsync(() -> world.install(savesDir, name))
|
||||||
.whenComplete(Schedulers.javafx(), () -> {
|
.whenComplete(Schedulers.javafx(), () -> {
|
||||||
resolve.run();
|
handler.resolve();
|
||||||
refresh();
|
refresh();
|
||||||
}, e -> {
|
}, e -> {
|
||||||
if (e instanceof FileAlreadyExistsException)
|
if (e instanceof FileAlreadyExistsException)
|
||||||
reject.accept(i18n("world.import.failed", i18n("world.import.already_exists")));
|
handler.reject(i18n("world.import.failed", i18n("world.import.already_exists")));
|
||||||
else if (e instanceof IOException && e.getCause() instanceof InvalidPathException)
|
else if (e instanceof IOException && e.getCause() instanceof InvalidPathException)
|
||||||
reject.accept(i18n("world.import.failed", i18n("install.new_game.malformed")));
|
handler.reject(i18n("world.import.failed", i18n("install.new_game.malformed")));
|
||||||
else
|
else
|
||||||
reject.accept(i18n("world.import.failed", e.getClass().getName() + ": " + e.getLocalizedMessage()));
|
handler.reject(i18n("world.import.failed", e.getClass().getName() + ": " + e.getLocalizedMessage()));
|
||||||
}).start();
|
}).start();
|
||||||
}, world.getWorldName(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid));
|
}, world.getWorldName(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid));
|
||||||
}, e -> {
|
}, e -> {
|
||||||
|
|||||||
@@ -92,20 +92,20 @@ public final class WorldManageUIUtils {
|
|||||||
Controllers.dialog(new InputDialogPane(
|
Controllers.dialog(new InputDialogPane(
|
||||||
i18n("world.duplicate.prompt"),
|
i18n("world.duplicate.prompt"),
|
||||||
"",
|
"",
|
||||||
(result, resolve, reject) -> {
|
(result, handler) -> {
|
||||||
if (StringUtils.isBlank(result)) {
|
if (StringUtils.isBlank(result)) {
|
||||||
reject.accept(i18n("world.duplicate.failed.empty_name"));
|
handler.reject(i18n("world.duplicate.failed.empty_name"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.contains("/") || result.contains("\\") || !FileUtils.isNameValid(result)) {
|
if (result.contains("/") || result.contains("\\") || !FileUtils.isNameValid(result)) {
|
||||||
reject.accept(i18n("world.duplicate.failed.invalid_name"));
|
handler.reject(i18n("world.duplicate.failed.invalid_name"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Path targetDir = worldPath.resolveSibling(result);
|
Path targetDir = worldPath.resolveSibling(result);
|
||||||
if (Files.exists(targetDir)) {
|
if (Files.exists(targetDir)) {
|
||||||
reject.accept(i18n("world.duplicate.failed.already_exists"));
|
handler.reject(i18n("world.duplicate.failed.already_exists"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,9 +118,9 @@ public final class WorldManageUIUtils {
|
|||||||
}
|
}
|
||||||
).whenComplete(Schedulers.javafx(), (throwable) -> {
|
).whenComplete(Schedulers.javafx(), (throwable) -> {
|
||||||
if (throwable == null) {
|
if (throwable == null) {
|
||||||
resolve.run();
|
handler.resolve();
|
||||||
} else {
|
} else {
|
||||||
reject.accept(i18n("world.duplicate.failed"));
|
handler.reject(i18n("world.duplicate.failed"));
|
||||||
LOG.warning("Failed to duplicate world " + world.getFile(), throwable);
|
LOG.warning("Failed to duplicate world " + world.getFile(), throwable);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -17,18 +17,23 @@
|
|||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.util;
|
package org.jackhuang.hmcl.util;
|
||||||
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface FutureCallback<T> {
|
public interface FutureCallback<T> {
|
||||||
|
|
||||||
/**
|
/// Callback of future, called after future finishes.
|
||||||
* Callback of future, called after future finishes.
|
/// This callback gives the feedback whether the result of future is acceptable or not,
|
||||||
* This callback gives the feedback whether the result of future is acceptable or not,
|
/// if not, giving the reason, and future will be relaunched when necessary.
|
||||||
* if not, giving the reason, and future will be relaunched when necessary.
|
///
|
||||||
* @param result result of the future
|
/// @param result result of the future
|
||||||
* @param resolve accept the result
|
/// @param handler handler to accept or reject the result
|
||||||
* @param reject reject the result with failure reason
|
void call(T result, ResultHandler handler);
|
||||||
*/
|
|
||||||
void call(T result, Runnable resolve, Consumer<String> reject);
|
interface ResultHandler {
|
||||||
|
|
||||||
|
/// Accept the result.
|
||||||
|
void resolve();
|
||||||
|
|
||||||
|
/// Reject the result with given reason.
|
||||||
|
void reject(String reason);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user