diff --git a/src/main/java/com/smallhacker/disbrowser/Grid.kt b/src/main/java/com/smallhacker/disbrowser/Grid.kt index ff75a03..9ac6a65 100644 --- a/src/main/java/com/smallhacker/disbrowser/Grid.kt +++ b/src/main/java/com/smallhacker/disbrowser/Grid.kt @@ -47,7 +47,9 @@ class Grid { arrowClasses[x to y] = "arrow arrow-$dir-middle" } arrowClasses[x to y2] = "arrow arrow-$dir-end" - arrowCells[x to yEnd] = div().addClass("arrow-head") + arrowCells[x to yEnd] = htmlFragment { + div.addClass("arrow-head") + } } private fun nextArrowX(y1: Int, y2: Int): Int { @@ -79,10 +81,14 @@ class Grid { = ins.print(gameData) add(y, ins.address, - text(address ?: ""), - text(bytes), + htmlFragment { + text(address ?: "") + }, + htmlFragment { + text(bytes) + }, editableField(game, indicativeAddress, "label", label), - fragment { + htmlFragment { if (secondaryMnemonic == null) { text(primaryMnemonic) } else { @@ -113,7 +119,9 @@ class Grid { }.attr("href", url) } }, - text(state ?: ""), + htmlFragment { + text(state ?: "") + }, editableField(game, indicativeAddress, "comment", comment) ) @@ -125,12 +133,15 @@ class Grid { } private fun editableField(game: Game, address: SnesAddress, type: String, value: String?): HtmlNode { - return input(value = value ?: "") - .addClass("field-$type") - .addClass("field-editable") - .attr("data-field", type) - .attr("data-game", game.id) - .attr("data-address", address.toSimpleString()) + return htmlFragment { + input.attr("value", value ?: "") + .attr("type", "text") + .addClass("field-$type") + .addClass("field-editable") + .attr("data-field", type) + .attr("data-game", game.id) + .attr("data-address", address.toSimpleString()) + } } private fun HtmlArea.editablePopupField(game: Game, address: SnesAddress, type: String, displayValue: String?, editValue: String?) { @@ -148,7 +159,17 @@ class Grid { private fun addDummy() { val y = (height++) - add(y, null, null, null, null, text("..."), null, null) + add(y, + null, + null, + null, + null, + htmlFragment { + text("...") + }, + null, + null + ) } private fun add(y: Int, address: SnesAddress?, @@ -176,30 +197,32 @@ class Grid { .max() ?: -1 - return table { - for (y in 0 until height) { - tr { - for (x in 0..3) { - val cssClass = cellClasses[x to y] - td { - content[x to y]?.appendTo(parent) - }.addClass(cssClass) - } - for (x in 0 until arrowWidth) { - val cssClass = arrowClasses[x to y] - td { - arrowCells[x to y]?.appendTo(parent) - }.addClass(cssClass) - } - for (x in 4..contentMaxX) { - val cssClass = cellClasses[x to y] - td { - content[x to y]?.appendTo(parent) - }.addClass(cssClass) - } - }.addClass(rowClasses[y]) - .attr("id", rowId[y]) - .attr("row-certainty", rowCertainties[y]) + return htmlFragment { + table { + for (y in 0 until height) { + tr { + for (x in 0..3) { + val cssClass = cellClasses[x to y] + td { + content[x to y]?.appendTo(parent) + }.addClass(cssClass) + } + for (x in 0 until arrowWidth) { + val cssClass = arrowClasses[x to y] + td { + arrowCells[x to y]?.appendTo(parent) + }.addClass(cssClass) + } + for (x in 4..contentMaxX) { + val cssClass = cellClasses[x to y] + td { + content[x to y]?.appendTo(parent) + }.addClass(cssClass) + } + }.addClass(rowClasses[y]) + .attr("id", rowId[y]) + .attr("row-certainty", rowCertainties[y]) + } } } } diff --git a/src/main/java/com/smallhacker/disbrowser/Html.kt b/src/main/java/com/smallhacker/disbrowser/Html.kt index cf6fee0..b47998e 100644 --- a/src/main/java/com/smallhacker/disbrowser/Html.kt +++ b/src/main/java/com/smallhacker/disbrowser/Html.kt @@ -1,5 +1,9 @@ package com.smallhacker.disbrowser +import kotlin.reflect.KProperty + +typealias InnerHtml = HtmlArea.() -> Unit + interface HtmlNode { fun print(): String { val out = StringBuilder() @@ -40,13 +44,9 @@ open class HtmlElement(protected val tag: String) : HtmlNode { } } -private class ParentHtmlElement(tag: String, inner: InnerHtml) : HtmlElement(tag) { +private class ParentHtmlElement(tag: String) : HtmlElement(tag) { private val children = ArrayList() - init { - inner(HtmlArea(this)) - } - override fun printTo(out: StringBuilder): StringBuilder { super.printTo(out) children.forEach { it.printTo(out) } @@ -57,22 +57,22 @@ private class ParentHtmlElement(tag: String, inner: InnerHtml) : HtmlElement(tag override fun append(node: HtmlNode) = apply { children.add(node) } } -private fun parent(tag: String, inner: InnerHtml): HtmlNode = ParentHtmlElement(tag, inner) -private fun leaf(tag: String): HtmlNode = HtmlElement(tag) - -typealias InnerHtml = HtmlArea.() -> Unit - class HtmlArea(val parent: HtmlNode) -fun text(text: String): HtmlNode = object : HtmlNode { +private class HtmlTextNode(private val text: String): HtmlNode { override fun printTo(out: StringBuilder) = out.append(text) override fun attr(key: String, value: String?) = this override fun append(node: HtmlNode): HtmlNode = this } -fun HtmlArea.text(text: String) = com.smallhacker.disbrowser.text(text).appendTo(parent) +private object ParentBuilder { + operator fun getValue(a: HtmlArea, b: KProperty<*>) = ParentHtmlElement(b.name).appendTo(a.parent) +} +private object LeafBuilder { + operator fun getValue(a: HtmlArea, b: KProperty<*>) = HtmlElement(b.name).appendTo(a.parent) +} -fun fragment(inner: InnerHtml = {}) = object : HtmlNode { +fun htmlFragment(inner: InnerHtml = {}) = object : HtmlNode { private val children = ArrayList() init { @@ -82,48 +82,45 @@ fun fragment(inner: InnerHtml = {}) = object : HtmlNode { override fun printTo(out: StringBuilder) = out.apply { children.forEach { it.printTo(out) } } override fun append(node: HtmlNode) = apply { children.add(node) } -} -fun HtmlArea.fragment(inner: InnerHtml = {}) = com.smallhacker.disbrowser.fragment(inner).appendTo(parent) -fun html(inner: InnerHtml = {}) = parent("html", inner) -fun HtmlArea.html(inner: InnerHtml = {}) = com.smallhacker.disbrowser.html(inner).appendTo(parent) -fun head(inner: InnerHtml = {}) = parent("head", inner) -fun HtmlArea.head(inner: InnerHtml = {}) = com.smallhacker.disbrowser.head(inner).appendTo(parent) -fun title(inner: InnerHtml = {}) = parent("title", inner) -fun HtmlArea.title(inner: InnerHtml = {}) = com.smallhacker.disbrowser.title(inner).appendTo(parent) -fun link(inner: InnerHtml = {}) = leaf("link") -fun HtmlArea.link(inner: InnerHtml = {}) = com.smallhacker.disbrowser.link(inner).appendTo(parent) -fun meta(inner: InnerHtml = {}) = leaf("meta") -fun HtmlArea.meta(inner: InnerHtml = {}) = com.smallhacker.disbrowser.meta(inner).appendTo(parent) -fun body(inner: InnerHtml = {}) = parent("body", inner) -fun HtmlArea.body(inner: InnerHtml = {}) = com.smallhacker.disbrowser.body(inner).appendTo(parent) -fun div(inner: InnerHtml = {}) = parent("div", inner) -fun HtmlArea.div(inner: InnerHtml = {}) = com.smallhacker.disbrowser.div(inner).appendTo(parent) -fun span(inner: InnerHtml = {}) = parent("span", inner) -fun HtmlArea.span(inner: InnerHtml = {}) = com.smallhacker.disbrowser.span(inner).appendTo(parent) -fun table(inner: InnerHtml = {}) = parent("table", inner) -fun HtmlArea.table(inner: InnerHtml = {}) = com.smallhacker.disbrowser.table(inner).appendTo(parent) -fun tr(inner: InnerHtml = {}) = parent("tr", inner) -fun HtmlArea.tr(inner: InnerHtml = {}) = com.smallhacker.disbrowser.tr(inner).appendTo(parent) -fun td(inner: InnerHtml = {}) = parent("td", inner) -fun HtmlArea.td(inner: InnerHtml = {}) = com.smallhacker.disbrowser.td(inner).appendTo(parent) -fun a(inner: InnerHtml = {}) = parent("a", inner) -fun HtmlArea.a(inner: InnerHtml = {}) = com.smallhacker.disbrowser.a(inner).appendTo(parent) -fun script(inner: InnerHtml = {}) = parent("script", inner) -fun HtmlArea.script(inner: InnerHtml = {}) = com.smallhacker.disbrowser.script(inner).appendTo(parent) -fun input(type: String = "text", value: String = "") = leaf("input").attr("type", type).attr("value", value) -fun HtmlArea.input(type: String = "text", value: String = "") = com.smallhacker.disbrowser.input(type, value).appendTo(parent) + override fun toString(): String = print() +} +fun HtmlArea.text(text: String) = HtmlTextNode(text).appendTo(parent) +val HtmlArea.fragment get() = htmlFragment().appendTo(parent) +val HtmlArea.html by ParentBuilder +val HtmlArea.head by ParentBuilder +val HtmlArea.title by ParentBuilder +val HtmlArea.link by LeafBuilder +val HtmlArea.meta by LeafBuilder +val HtmlArea.body by ParentBuilder +val HtmlArea.main by ParentBuilder +val HtmlArea.aside by ParentBuilder +val HtmlArea.div by ParentBuilder +val HtmlArea.span by ParentBuilder +val HtmlArea.table by ParentBuilder +val HtmlArea.tr by ParentBuilder +val HtmlArea.td by ParentBuilder +val HtmlArea.a by ParentBuilder +val HtmlArea.script by ParentBuilder +val HtmlArea.input by LeafBuilder +val HtmlArea.button by ParentBuilder fun HtmlNode.appendTo(node: HtmlNode) = apply { node.append(this) } fun HtmlNode.addClass(c: String?) = attrAdd("class", c) +val HtmlNode.inner get() = this +operator fun HtmlNode.invoke(inner: InnerHtml): HtmlNode = append(htmlFragment(inner)) -fun HtmlNode.attrSet(name: String): MutableSet = (attr(name) ?: "") +fun HtmlNode.attr(key: String, value: String?, inner: InnerHtml) = attr(key, value).inner(inner) +fun HtmlNode.addClass(c: String?, inner: InnerHtml) = addClass(c).inner(inner) +fun HtmlNode.append(node: HtmlNode, inner: InnerHtml) = append(node).inner(inner) + +private fun HtmlNode.attrSet(name: String): MutableSet = (attr(name) ?: "") .split(" ") .asSequence() .filterNot { it.isEmpty() } .toMutableSet() -fun HtmlNode.attrAdd(name: String, value: String?) = apply { +private fun HtmlNode.attrAdd(name: String, value: String?) = apply { if (value != null) { val set = attrSet(name) set.add(value) diff --git a/src/main/java/com/smallhacker/disbrowser/resource/DisassemblyResource.kt b/src/main/java/com/smallhacker/disbrowser/resource/DisassemblyResource.kt index a0735f3..01bee9d 100644 --- a/src/main/java/com/smallhacker/disbrowser/resource/DisassemblyResource.kt +++ b/src/main/java/com/smallhacker/disbrowser/resource/DisassemblyResource.kt @@ -25,20 +25,22 @@ class DisassemblyResource { @Produces(MediaType.TEXT_HTML) fun getIt(@PathParam("game") gameName: String) = handle { games.getGame(gameName)?.let { game -> - table { - Service.getVectors(game).forEach { - tr { - td { text(it.name) } - td { text("(" + it.vectorLocation.toFormattedString() + ")") } - td { - a { - text(it.label) - }.attr("href", "/${game.id}/${it.codeLocation.toSimpleString()}/MX") + htmlFragment { + table { + Service.getVectors(game).forEach { + tr { + td { text(it.name) } + td { text("(" + it.vectorLocation.toFormattedString() + ")") } + td { + a { + text(it.label) + }.attr("href", "/${game.id}/${it.codeLocation.toSimpleString()}/MX") + } + td { text("(" + it.codeLocation.toFormattedString() + ")") } } - td { text("(" + it.codeLocation.toFormattedString() + ")") } } - } - }.addClass("vector-table") + }.addClass("vector-table") + } } } @@ -73,17 +75,29 @@ class DisassemblyResource { val output = runner() ?: return Response.status(404).build() - val html = html { - head { - title { text("Disassembly Browser") } - link {}.attr("rel", "stylesheet").attr("href", "/resources/style.css") - meta {}.attr("charset", "UTF-8") - } - body { - output.appendTo(parent) - script().attr("src", "/resources/disbrowser.js") - } - } + val html = + htmlFragment { + html { + head { + title { text("Disassembly Browser") } + link.attr("rel", "stylesheet").attr("href", "/resources/style.css") + meta.attr("charset", "UTF-8") + } + body { + main { + output.appendTo(parent) + } + + aside.addClass("sidebar") { + button.attr("id", "btn-dark-mode") { + text("Dark Mode") + } + } + + script.attr("src", "/resources/disbrowser.js") + } + } + } return Response.ok(html.toString().toByteArray(StandardCharsets.UTF_8)) .encoding("UTF-8") diff --git a/src/main/resources/public/style.css b/src/main/resources/public/style.css index 73b72d6..2cb6900 100644 --- a/src/main/resources/public/style.css +++ b/src/main/resources/public/style.css @@ -105,7 +105,7 @@ tr.line-active { width: 500px; } -.opcode-info{ +.opcode-info { text-decoration: green underline dotted; } @@ -125,56 +125,97 @@ tr.line-active { .field-editable-popup-icon:hover { font-weight: bold; } + .field-editable-popup-icon::before { content: "[e]" } -[row-certainty="0"],[row-certainty="1"],[row-certainty="2"],[row-certainty="3"],[row-certainty="4"], -[row-certainty="5"],[row-certainty="6"],[row-certainty="7"],[row-certainty="8"],[row-certainty="9"] { - color: rgb(200,200,200); +[row-certainty="0"], [row-certainty="1"], [row-certainty="2"], [row-certainty="3"], [row-certainty="4"], +[row-certainty="5"], [row-certainty="6"], [row-certainty="7"], [row-certainty="8"], [row-certainty="9"] { + color: rgb(200, 200, 200); } -[row-certainty="10"],[row-certainty="11"],[row-certainty="12"],[row-certainty="13"],[row-certainty="14"], -[row-certainty="15"],[row-certainty="16"],[row-certainty="17"],[row-certainty="18"],[row-certainty="19"] { - color: rgb(180,180,180); +[row-certainty="10"], [row-certainty="11"], [row-certainty="12"], [row-certainty="13"], [row-certainty="14"], +[row-certainty="15"], [row-certainty="16"], [row-certainty="17"], [row-certainty="18"], [row-certainty="19"] { + color: rgb(180, 180, 180); } -[row-certainty="20"],[row-certainty="21"],[row-certainty="22"],[row-certainty="23"],[row-certainty="24"], -[row-certainty="25"],[row-certainty="26"],[row-certainty="27"],[row-certainty="28"],[row-certainty="29"] { - color: rgb(160,160,160); +[row-certainty="20"], [row-certainty="21"], [row-certainty="22"], [row-certainty="23"], [row-certainty="24"], +[row-certainty="25"], [row-certainty="26"], [row-certainty="27"], [row-certainty="28"], [row-certainty="29"] { + color: rgb(160, 160, 160); } -[row-certainty="30"],[row-certainty="31"],[row-certainty="32"],[row-certainty="33"],[row-certainty="34"], -[row-certainty="35"],[row-certainty="36"],[row-certainty="37"],[row-certainty="38"],[row-certainty="39"] { - color: rgb(140,140,140); +[row-certainty="30"], [row-certainty="31"], [row-certainty="32"], [row-certainty="33"], [row-certainty="34"], +[row-certainty="35"], [row-certainty="36"], [row-certainty="37"], [row-certainty="38"], [row-certainty="39"] { + color: rgb(140, 140, 140); } -[row-certainty="40"],[row-certainty="41"],[row-certainty="42"],[row-certainty="43"],[row-certainty="44"], -[row-certainty="45"],[row-certainty="46"],[row-certainty="47"],[row-certainty="48"],[row-certainty="49"] { - color: rgb(120,120,120); +[row-certainty="40"], [row-certainty="41"], [row-certainty="42"], [row-certainty="43"], [row-certainty="44"], +[row-certainty="45"], [row-certainty="46"], [row-certainty="47"], [row-certainty="48"], [row-certainty="49"] { + color: rgb(120, 120, 120); } -[row-certainty="50"],[row-certainty="51"],[row-certainty="52"],[row-certainty="53"],[row-certainty="54"], -[row-certainty="55"],[row-certainty="56"],[row-certainty="57"],[row-certainty="58"],[row-certainty="59"] { - color: rgb(100,100,100); +[row-certainty="50"], [row-certainty="51"], [row-certainty="52"], [row-certainty="53"], [row-certainty="54"], +[row-certainty="55"], [row-certainty="56"], [row-certainty="57"], [row-certainty="58"], [row-certainty="59"] { + color: rgb(100, 100, 100); } -[row-certainty="60"],[row-certainty="61"],[row-certainty="62"],[row-certainty="63"],[row-certainty="64"], -[row-certainty="65"],[row-certainty="66"],[row-certainty="67"],[row-certainty="68"],[row-certainty="69"] { - color: rgb(80,80,80); +[row-certainty="60"], [row-certainty="61"], [row-certainty="62"], [row-certainty="63"], [row-certainty="64"], +[row-certainty="65"], [row-certainty="66"], [row-certainty="67"], [row-certainty="68"], [row-certainty="69"] { + color: rgb(80, 80, 80); } -[row-certainty="70"],[row-certainty="71"],[row-certainty="72"],[row-certainty="73"],[row-certainty="74"], -[row-certainty="75"],[row-certainty="76"],[row-certainty="77"],[row-certainty="78"],[row-certainty="79"] { - color: rgb(60,60,60); +[row-certainty="70"], [row-certainty="71"], [row-certainty="72"], [row-certainty="73"], [row-certainty="74"], +[row-certainty="75"], [row-certainty="76"], [row-certainty="77"], [row-certainty="78"], [row-certainty="79"] { + color: rgb(60, 60, 60); } -[row-certainty="80"],[row-certainty="81"],[row-certainty="82"],[row-certainty="83"],[row-certainty="84"], -[row-certainty="85"],[row-certainty="86"],[row-certainty="87"],[row-certainty="88"],[row-certainty="89"] { - color: rgb(40,40,40); +[row-certainty="80"], [row-certainty="81"], [row-certainty="82"], [row-certainty="83"], [row-certainty="84"], +[row-certainty="85"], [row-certainty="86"], [row-certainty="87"], [row-certainty="88"], [row-certainty="89"] { + color: rgb(40, 40, 40); } -[row-certainty="90"],[row-certainty="91"],[row-certainty="92"],[row-certainty="93"],[row-certainty="94"], -[row-certainty="95"],[row-certainty="96"],[row-certainty="97"],[row-certainty="98"],[row-certainty="99"] { - color: rgb(20,20,20); +[row-certainty="90"], [row-certainty="91"], [row-certainty="92"], [row-certainty="93"], [row-certainty="94"], +[row-certainty="95"], [row-certainty="96"], [row-certainty="97"], [row-certainty="98"], [row-certainty="99"] { + color: rgb(20, 20, 20); +} + +body.dark-mode { + filter: invert(0.9) hue-rotate(180deg); + background-color: black; +} + +body { + margin: 0; + position: relative; + overflow: hidden; +} + +main { + position: absolute; + left: 0; + right: 50px; + top: 0; + bottom: 0; + padding: 8px; + overflow: auto; +} + +.sidebar { + position: absolute; + right: -200px; + top: 0; + bottom: 0; + width: 250px; + z-index: 100; + background-color: lightgray; + border-left: 3px double black; + padding: 10px 10px 10px 50px; + box-sizing: border-box; + transition: right 0.1s; + box-shadow: -1px 0 13px rgba(0,0,0,0.4); +} + +.sidebar:hover { + right: 0; } \ No newline at end of file diff --git a/src/main/ts/main.ts b/src/main/ts/main.ts index 10d376f..d316fb3 100644 --- a/src/main/ts/main.ts +++ b/src/main/ts/main.ts @@ -107,4 +107,57 @@ for (let i = 0; i < popupEditables.length; i++) { return false; }); +} + +class PersistentProperty { + private _value: T; + private readonly key: string; + private readonly onChange: (value: T) => void; + + constructor(key: string, defaultValue: T, onChange: (value: T) => void) { + this.key = key; + this.onChange = onChange; + this.value = this.parse(key, defaultValue); + } + + get value(): T { + return this._value; + } + + set value(value: T) { + this._value = value; + localStorage.setItem(this.key, JSON.stringify(value)); + this.onChange(value); + } + + private parse(key: string, defaultValue: T): T { + let value = localStorage.getItem(key); + if (value === null) { + return defaultValue; + } + let parsedValue = JSON.parse(value); + if (parsedValue === null) { + return defaultValue; + } + return parsedValue; + } +} + +let darkMode = new PersistentProperty( + "ui.presentation.darkMode", + false, + (enable) => { + if (enable) { + document.body.classList.add("dark-mode") + } else { + document.body.classList.remove("dark-mode") + } + } +); + +let btnDarkMode = document.getElementById("btn-dark-mode"); +if (btnDarkMode) { + btnDarkMode.addEventListener("click", () => { + darkMode.value = !darkMode.value; + }) } \ No newline at end of file