Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<String?>()

data class FileExtensionFilter(val description: String, val extensions: List<String>)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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++
Expand Down Expand Up @@ -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)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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? {
Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,33 +19,28 @@ along with GanttProject. If not, see <http://www.gnu.org/licenses/>.
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

Expand All @@ -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
}
Expand All @@ -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

Expand All @@ -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 {
Expand All @@ -99,71 +97,112 @@ 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<String, String> =
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() {}

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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ open class GanttProjectImpl(
}

protected open fun fireProjectOpened() {
val barrier = TwoPhaseBarrierImpl<IGanttProject>("Project opened", this)
val barrier = TwoPhaseBarrierImpl<IGanttProject>("Project opened")
for (l in listeners) {
l.projectOpened(barrier, barrier)
}
Expand Down Expand Up @@ -319,4 +319,4 @@ internal fun createProjectModificationListener(project: IGanttProject, uiFacade:
timerBarrier.isPaused = false
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading