|
| 1 | +package openshock.integrations.minecraft |
| 2 | + |
| 3 | +import net.minecraft.client.gui.components.Button |
| 4 | +import net.minecraft.client.gui.components.EditBox |
| 5 | +import net.minecraft.client.gui.screens.Screen |
| 6 | +import net.minecraft.network.chat.Component |
| 7 | +import openshock.integrations.minecraft.config.AccountConfig |
| 8 | +import openshock.integrations.minecraft.platform.McCompat |
| 9 | + |
| 10 | +/** |
| 11 | + * Where the API token is typed, on a screen of its own. |
| 12 | + * |
| 13 | + * The token is the one secret in the config - anyone holding it can shock whoever it belongs to - |
| 14 | + * and the settings screen is the thing most likely to be on camera while somebody is being talked |
| 15 | + * through the setup. So it is not on that screen at all: Setup says whether a token is set and |
| 16 | + * nothing more, and this is the only place one is ever entered. |
| 17 | + * |
| 18 | + * Which also means there is nothing to mask. The box starts **empty** rather than pre-filled with |
| 19 | + * what is already stored, so the stored token is never rendered anywhere, by anything. That is the |
| 20 | + * whole design, and it is why this replaced an attempt at drawing asterisks over YACL's own text |
| 21 | + * field: that meant fighting the widget's caret and scroll arithmetic on three different YACL |
| 22 | + * versions, for a weaker result than simply not drawing the thing. |
| 23 | + * |
| 24 | + * An empty box cannot be saved - Save stays greyed out until something is typed - so opening this |
| 25 | + * and pressing the obvious button cannot wipe a working token. Clearing is its own button. |
| 26 | + * |
| 27 | + * ### Versions |
| 28 | + * |
| 29 | + * Built out of [Button] and [EditBox] and nothing else, for the same reason [RemoteScreen] is |
| 30 | + * built out of buttons: both are spelled identically from 1.20.4 to 26.2, and a screen that only |
| 31 | + * adds widgets and never draws anything itself needs no per-version handling at all. Anything that |
| 32 | + * paints - a title, a label, a background - would have to follow Minecraft's rendering rewrites. |
| 33 | + * Hence the captions below being buttons nobody can press. |
| 34 | + */ |
| 35 | +class ApiTokenScreen(private val parent: Screen?) : Screen(Component.literal("API Token")) { |
| 36 | + |
| 37 | + /** Longer than any token OpenShock issues, and far longer than [EditBox]'s default of 32. */ |
| 38 | + private val maxLength = 256 |
| 39 | + |
| 40 | + override fun init() { |
| 41 | + val account = AccountConfig.HANDLER.instance() |
| 42 | + |
| 43 | + val width = 260 |
| 44 | + val height = 20 |
| 45 | + val left = (this.width - width) / 2 |
| 46 | + var top = this.height / 4 |
| 47 | + |
| 48 | + fun next(): Int { |
| 49 | + val y = top |
| 50 | + top += height + 6 |
| 51 | + return y |
| 52 | + } |
| 53 | + |
| 54 | + // What is already stored, said without saying it. |
| 55 | + caption( |
| 56 | + if (account.apiToken.isBlank()) "No API token set" else "An API token is already set", |
| 57 | + left, next(), width, height, |
| 58 | + ) |
| 59 | + |
| 60 | + caption("Paste a new one below to replace it", left, next(), width, height) |
| 61 | + |
| 62 | + val box = EditBox(font, left, next(), width, height, Component.literal("API Token")) |
| 63 | + box.setMaxLength(maxLength) |
| 64 | + box.setHint(Component.literal("Paste your API token")) |
| 65 | + |
| 66 | + val save = Button.builder(Component.literal("Save")) { |
| 67 | + // Trimmed because a token that arrives with a stray space or newline from a copy fails |
| 68 | + // authentication in a way that looks exactly like a wrong token. |
| 69 | + account.apiToken = box.value.trim() |
| 70 | + AccountConfig.HANDLER.save() |
| 71 | + |
| 72 | + // Back to Setup, which rebuilds - and so notices the new token and goes and fetches the |
| 73 | + // shockers it can reach, which is the next thing anybody wants after setting one. |
| 74 | + back() |
| 75 | + } |
| 76 | + .bounds(left, next(), width, height) |
| 77 | + .build() |
| 78 | + |
| 79 | + // Nothing typed is not a request to clear the token, it is somebody who opened this and |
| 80 | + // changed their mind. Clearing has its own button precisely so it cannot happen by accident. |
| 81 | + save.active = false |
| 82 | + box.setResponder { typed -> save.active = typed.isNotBlank() } |
| 83 | + |
| 84 | + addRenderableWidget(box) |
| 85 | + addRenderableWidget(save) |
| 86 | + |
| 87 | + addRenderableWidget( |
| 88 | + Button.builder(Component.literal("Clear token")) { |
| 89 | + account.apiToken = "" |
| 90 | + AccountConfig.HANDLER.save() |
| 91 | + back() |
| 92 | + } |
| 93 | + .bounds(left, next(), width, height) |
| 94 | + .build() |
| 95 | + .also { it.active = account.apiToken.isNotBlank() } |
| 96 | + ) |
| 97 | + |
| 98 | + addRenderableWidget( |
| 99 | + Button.builder(Component.literal("Cancel")) { back() } |
| 100 | + .bounds(left, next(), width, height) |
| 101 | + .build() |
| 102 | + ) |
| 103 | + |
| 104 | + // So the box takes typing straight away: this screen exists to be pasted into. |
| 105 | + setInitialFocus(box) |
| 106 | + } |
| 107 | + |
| 108 | + /** Escape leaves the token alone, the same as Cancel. */ |
| 109 | + override fun onClose() = back() |
| 110 | + |
| 111 | + private fun back() = McCompat.setScreen(ConfigScreen.create(parent)) |
| 112 | + |
| 113 | + /** A line of text, as a button nobody can press - see the note on this class about drawing. */ |
| 114 | + private fun caption(text: String, x: Int, y: Int, width: Int, height: Int) { |
| 115 | + addRenderableWidget( |
| 116 | + Button.builder(Component.literal(text)) { } |
| 117 | + .bounds(x, y, width, height) |
| 118 | + .build() |
| 119 | + .also { it.active = false } |
| 120 | + ) |
| 121 | + } |
| 122 | +} |
0 commit comments