diff --git a/biz.ganttproject.app.localization b/biz.ganttproject.app.localization index b54143b8bb..25b96efc7f 160000 --- a/biz.ganttproject.app.localization +++ b/biz.ganttproject.app.localization @@ -1 +1 @@ -Subproject commit b54143b8bbba27e00911bd7f57a25d52ed357d6f +Subproject commit 25b96efc7f926c964156e139a8fb42af363411de diff --git a/biz.ganttproject.core/src/main/java/biz/ganttproject/core/option/PropertyPaneBuilder.kt b/biz.ganttproject.core/src/main/java/biz/ganttproject/core/option/PropertyPaneBuilder.kt index 09104cd46c..b152f023ce 100644 --- a/biz.ganttproject.core/src/main/java/biz/ganttproject/core/option/PropertyPaneBuilder.kt +++ b/biz.ganttproject.core/src/main/java/biz/ganttproject/core/option/PropertyPaneBuilder.kt @@ -101,6 +101,7 @@ data class TextDisplayOptions( var isScreened: Boolean = false, var columnCount: Int = 40, var rightNode: Node? = null, + val isValid: ObservableBoolean = ObservableBoolean("",true), ) : PropertyDisplayOptions() data class FileExtensionFilter(val description: String, val extensions: List) diff --git a/ganttproject/src/main/java/biz/ganttproject/app/PropertySheet.kt b/ganttproject/src/main/java/biz/ganttproject/app/PropertySheet.kt index 8c3260e9ef..3d72357dc3 100644 --- a/ganttproject/src/main/java/biz/ganttproject/app/PropertySheet.kt +++ b/ganttproject/src/main/java/biz/ganttproject/app/PropertySheet.kt @@ -279,9 +279,11 @@ class PropertyPaneBuilderImpl(private val localizer: Localizer, private val grid validatedText.validationMessage.addWatcher { if (it.newValue == null) { + displayOptions?.isValid?.value = true textField.markValid() validationErrors.remove(property) } else { + displayOptions?.isValid?.value = false textField.markInvalid() validationErrors[property] = it.newValue } @@ -489,7 +491,7 @@ class PropertyPaneBuilderImpl(private val localizer: Localizer, private val grid GridPane.setHalignment(label, options?.labelHAlignment ?: HPos.RIGHT) } LabelPosition.ABOVE -> { - grid.add(label, 0, idx, 2, 1) + grid.add(label, 0, idx, grid.columnCount, 1) GridPane.setHgrow(label, Priority.NEVER) GridPane.setHalignment(label, options?.labelHAlignment ?: HPos.LEFT) resultRow++ @@ -521,7 +523,7 @@ class PropertyPaneBuilderImpl(private val localizer: Localizer, private val grid GridPane.setHgrow(hbox, Priority.SOMETIMES) } LabelPosition.ABOVE -> { - grid.add(hbox, 0, resultRow, 2, 1) + grid.add(hbox, 0, resultRow, grid.columnCount, 1) GridPane.setHgrow(hbox, Priority.ALWAYS) } } diff --git a/ganttproject/src/main/java/biz/ganttproject/storage/cloud/GPCloudHttpImpl.kt b/ganttproject/src/main/java/biz/ganttproject/storage/cloud/GPCloudHttpImpl.kt index 4bd99b090d..dd5460db56 100644 --- a/ganttproject/src/main/java/biz/ganttproject/storage/cloud/GPCloudHttpImpl.kt +++ b/ganttproject/src/main/java/biz/ganttproject/storage/cloud/GPCloudHttpImpl.kt @@ -446,6 +446,7 @@ typealias AuthStartCallback = ()->Unit class HttpServerImpl : NanoHTTPD("localhost", 0) { var onTokenReceived: AuthTokenCallback? = null + var onAuthReceived: (() -> Unit)? = null var onStart: AuthStartCallback? = null private fun getParam(session: IHTTPSession, key: String): String? { @@ -465,6 +466,7 @@ class HttpServerImpl : NanoHTTPD("localhost", 0) { LOG.debug("Received Auth Token:{} validity:{}", token, validity) onTokenReceived?.invoke(token, validity, userId, websocketToken) + onAuthReceived?.invoke() newFixedLengthResponse("").apply { addHeader("Access-Control-Allow-Origin", GPCLOUD_ORIGIN) } diff --git a/ganttproject/src/main/java/biz/ganttproject/storage/cloud/SigninPane.kt b/ganttproject/src/main/java/biz/ganttproject/storage/cloud/SigninPane.kt index a5017c5386..c6e3a6c334 100644 --- a/ganttproject/src/main/java/biz/ganttproject/storage/cloud/SigninPane.kt +++ b/ganttproject/src/main/java/biz/ganttproject/storage/cloud/SigninPane.kt @@ -19,33 +19,28 @@ along with GanttProject. If not, see . package biz.ganttproject.storage.cloud import biz.ganttproject.FXUtil -import biz.ganttproject.app.DIALOG_STYLESHEET -import biz.ganttproject.app.RootLocalizer -import biz.ganttproject.app.Spinner +import biz.ganttproject.app.* +import biz.ganttproject.core.option.LabelPosition +import biz.ganttproject.core.option.ObservableString +import biz.ganttproject.core.option.ValidationException import biz.ganttproject.lib.fx.VBoxBuilder import biz.ganttproject.lib.fx.isBrowseSupported import biz.ganttproject.lib.fx.openInBrowser -import biz.ganttproject.lib.fx.vbox import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView import javafx.beans.property.SimpleStringProperty import javafx.event.ActionEvent -import javafx.event.EventHandler +import javafx.geometry.HPos import javafx.geometry.Pos -import javafx.scene.Node import javafx.scene.control.Button import javafx.scene.control.ContentDisplay import javafx.scene.control.Label -import javafx.scene.control.TextField import javafx.scene.input.Clipboard import javafx.scene.input.ClipboardContent import javafx.scene.layout.BorderPane import javafx.scene.layout.Pane import javafx.scene.layout.Priority -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.javafx.JavaFx -import kotlinx.coroutines.launch +import java.net.URLDecoder import java.util.* import kotlin.concurrent.schedule @@ -58,7 +53,8 @@ import kotlin.concurrent.schedule * * @author dbarashev@bardsoftware.com */ -class SigninPane() : FlowPage() { +class SigninPane : FlowPage() { + enum class Status { INIT, WAITING_FOR_BROWSER, WAITING_FOR_AUTH, AUTH_COMPLETED } @@ -71,9 +67,11 @@ class SigninPane() : FlowPage() { private val indicatorPane = BorderPane().apply { styleClass.add("indicator-pane") } + private val urlOption = ObservableString("cloud.signin.url", "") + private val tokenOption = ObservableString("cloud.signin.token", "", validator = this::validateTokenString) private fun onStartCallback() { - GlobalScope.launch(Dispatchers.JavaFx) { + FXThread.runLater { status = Status.WAITING_FOR_AUTH spinner.state = Spinner.State.ATTENTION @@ -82,7 +80,7 @@ class SigninPane() : FlowPage() { } fun createSigninPane(): Pane { - val uri = "$GPCLOUD_SIGNIN_URL?callback=${controller.httpd.listeningPort}" + val uri = "$GPCLOUD_SIGNIN_URL?callback=${controller.httpd.listeningPort}&timeout=$SIGNIN_TIMEOUT_SECONDS" val vboxBuilder = VBoxBuilder("signin-pane", "pane-service-contents") vboxBuilder.addTitle(ourLocalizer.formatText("title")).also { @@ -99,61 +97,96 @@ class SigninPane() : FlowPage() { spinner.state = Spinner.State.WAITING statusText.value = ourLocalizer.formatText("text.browser_opening") + val copyButton = Button(ourLocalizer.formatText("button.copyLink"), FontAwesomeIconView(FontAwesomeIcon.COPY)).apply { + contentDisplay = ContentDisplay.RIGHT + addEventHandler(ActionEvent.ACTION) { + Clipboard.getSystemClipboard().setContent(ClipboardContent().apply { + putString(uri) + }) + } + } + + urlOption.value = uri + val submitButton = Button(ourLocalizer.formatText("button.submitToken")).apply { + addEventHandler(ActionEvent.ACTION) { submitToken(tokenOption.value ?: "") } + } + + val tokenPane = properties(ourLocalizer) { + text(urlOption) { + labelText = ourLocalizer.formatText("text.browser_failed") + labelPosition = LabelPosition.ABOVE + labelHAlignment = HPos.LEFT + rightNode = copyButton + } + text(tokenOption) { + labelText = ourLocalizer.formatText("label.pasteToken") + labelHAlignment = HPos.LEFT + labelPosition = LabelPosition.ABOVE + rightNode = submitButton + isValid.addWatcher { + submitButton.isDisable = !it.newValue + } + submitButton.isDisable = true + } + } + vboxBuilder.add(tokenPane, Pos.CENTER, Priority.NEVER) vboxBuilder.vbox.let { it.stylesheets.addAll( DIALOG_STYLESHEET, "/biz/ganttproject/app/Util.css", "biz/ganttproject/storage/StorageDialog.css", - "/biz/ganttproject/storage/cloud/GPCloudSignupPane.css" + "biz/ganttproject/storage/cloud/GPCloudSignupPane.css" ) } - GlobalScope.launch(Dispatchers.IO) { + FXThread.runLater { if (isBrowseSupported()) { status = Status.WAITING_FOR_BROWSER openInBrowser(uri.trim()) startBrowserTimeout(uri) - } else { - GlobalScope.launch(Dispatchers.JavaFx) { - FXUtil.transitionCenterPane(indicatorPane, createUrlPane(uri)) {} - } } } return vboxBuilder.vbox } private fun startBrowserTimeout(uri: String) { - Timer().schedule(60000) { + Timer().schedule(SIGNIN_TIMEOUT_SECONDS * 1000L) { if (status == Status.WAITING_FOR_BROWSER) { - GlobalScope.launch(Dispatchers.JavaFx) { - FXUtil.transitionCenterPane(indicatorPane, createUrlPane(uri), {}) + FXThread.runLater { statusText.value = ourLocalizer.formatText("text.browser_failed") } } } } - private fun createUrlPane(uri: String): Node { - return vbox { - i18n = ourLocalizer - vbox.spacing = 5.0 - add(TextField().apply { - text = uri - isEditable = false - onMouseClicked = EventHandler { this.selectAll() } - }, Pos.CENTER, Priority.NEVER) - add(Button(i18n.formatText("button.copyLink"), FontAwesomeIconView(FontAwesomeIcon.COPY)).apply { - contentDisplay = ContentDisplay.RIGHT - styleClass.addAll("btn-attention") - addEventHandler(ActionEvent.ACTION) { - Clipboard.getSystemClipboard().setContent(ClipboardContent().apply { - putString(uri) - }) - } - }, Pos.CENTER, Priority.NEVER) - } + private fun submitToken(rawInput: String) { + val params = parseTokenString(rawInput) + controller.httpd.onTokenReceived?.invoke( + params["token"], + params["validity"], + params["userId"], + params["websocketToken"] + ) + controller.httpd.onAuthReceived?.invoke() } + private fun parseTokenString(rawInput: String): Map = + if (rawInput.isBlank()) emptyMap() + else rawInput.split("&").associate { + val parts = it.split("=", limit = 2) + val key = URLDecoder.decode(parts[0], Charsets.UTF_8.name()) + key to (parts.getOrNull(1)?.let { v -> URLDecoder.decode(v, Charsets.UTF_8.name()) } ?: "") + } + + private fun validateTokenString(value: String) = + parseTokenString(value).let { + if (it["token"].isNullOrEmpty() || it["validity"].isNullOrEmpty() || it["userId"].isNullOrEmpty() || it["websocketToken"].isNullOrEmpty()) { + throw ValidationException("Invalid token string") + } + value + } + + override fun createUi(): Pane = createSigninPane() override fun resetUi() {} @@ -161,9 +194,15 @@ class SigninPane() : FlowPage() { override fun setController(controller: GPCloudUiFlow) { this.controller = controller controller.httpd.onStart = ::onStartCallback + controller.httpd.onAuthReceived = { + FXThread.runLater { + tokenOption.isWritable.value = false + } + } } } +private const val SIGNIN_TIMEOUT_SECONDS = 60 private val ourLocalizer = RootLocalizer.createWithRootKey( rootKey = "cloud.signin", diff --git a/ganttproject/src/main/java/net/sourceforge/ganttproject/GanttProjectImpl.kt b/ganttproject/src/main/java/net/sourceforge/ganttproject/GanttProjectImpl.kt index c8794ca33c..f4558b0437 100644 --- a/ganttproject/src/main/java/net/sourceforge/ganttproject/GanttProjectImpl.kt +++ b/ganttproject/src/main/java/net/sourceforge/ganttproject/GanttProjectImpl.kt @@ -145,7 +145,7 @@ open class GanttProjectImpl( } protected open fun fireProjectOpened() { - val barrier = TwoPhaseBarrierImpl("Project opened", this) + val barrier = TwoPhaseBarrierImpl("Project opened") for (l in listeners) { l.projectOpened(barrier, barrier) } @@ -319,4 +319,4 @@ internal fun createProjectModificationListener(project: IGanttProject, uiFacade: timerBarrier.isPaused = false } } -} \ No newline at end of file +} diff --git a/ganttproject/src/main/java/biz/ganttproject/storage/cloud/GPCloudSignupPane.css b/ganttproject/src/main/sass/biz/ganttproject/storage/cloud/GPCloudSignupPane.scss similarity index 84% rename from ganttproject/src/main/java/biz/ganttproject/storage/cloud/GPCloudSignupPane.css rename to ganttproject/src/main/sass/biz/ganttproject/storage/cloud/GPCloudSignupPane.scss index e82c23de8c..ce53b17562 100644 --- a/ganttproject/src/main/java/biz/ganttproject/storage/cloud/GPCloudSignupPane.css +++ b/ganttproject/src/main/sass/biz/ganttproject/storage/cloud/GPCloudSignupPane.scss @@ -29,8 +29,20 @@ /* -fx-fill: white;*/ /* -fx-text-fill: white;*/ /*}*/ +@import "../../app/textfields"; +@import "../../app/theme"; - +.signin-pane { + .property-pane { + HBox { + -fx-padding: 0 0 15 0; + } + -fx-padding: 30 10 0 10; + .custom-text-field { + @include textfield-with-button($gp-light-gray, $gp-dark-gray); + } + } +} .signup-pane { -fx-padding: 20px; -fx-background-color: transparent;