Add family page & diff label editor (#438)

* add 'parent' url to TerseScratchSerializer

* add family page

* link to family in AboutScratch

* bump react-laag

* ui to edit diff label

* use User-Agent Client Hints API if supported

* fix pwa icons

* use carets instead of slashes between breadcrumbs

* use breadcrumbs on project function page

* fix save problem

* allow diff_label on compile

* a

* change placeholder

* new diff flags fix

* diff flags stuff

Co-authored-by: Ethan Roseman <ethteck@gmail.com>
This commit is contained in:
alex
2022-04-13 15:21:40 +01:00
committed by GitHub
parent 2ae2827e12
commit 9b31c9ebb8
28 changed files with 474 additions and 90 deletions

View File

@@ -0,0 +1,40 @@
# Generated by Django 4.0.4 on 2022-04-13 13:48
import django.db.migrations.operations.special
from django.db import migrations, models
def diff_flags_array(apps, schema_editor):
"""
Diff flags is a json array, but it used to be a string - let's convert empty strings to empty arrays.
"""
Scratch = apps.get_model("coreapp", "Scratch")
for row in Scratch.objects.all():
if row.diff_flags == "":
row.diff_flags = []
row.save(update_fields=["diff_flags"])
class Migration(migrations.Migration):
dependencies = [
("coreapp", "0020_diff_flags"),
]
operations = [
migrations.AlterField(
model_name="compilerconfig",
name="diff_flags",
field=models.JSONField(default=list),
),
migrations.AlterField(
model_name="scratch",
name="diff_flags",
field=models.JSONField(default=list),
),
migrations.RunPython(
code=diff_flags_array,
reverse_code=django.db.migrations.operations.special.RunPython.noop,
),
]

View File

@@ -38,7 +38,7 @@ class CompilerConfig(models.Model):
compiler = models.CharField(max_length=100)
platform = models.CharField(max_length=100)
compiler_flags = models.TextField(max_length=1000, default="", blank=True)
diff_flags = models.JSONField(default=str, blank=True)
diff_flags = models.JSONField(default=list)
class Scratch(models.Model):
@@ -54,9 +54,7 @@ class Scratch(models.Model):
compiler_flags = models.TextField(
max_length=1000, default="", blank=True
) # TODO: reference a CompilerConfig
diff_flags = models.JSONField(
default=str, blank=True
) # TODO: reference a CompilerConfig
diff_flags = models.JSONField(default=list) # TODO: reference a CompilerConfig
preset = models.CharField(max_length=100, blank=True, null=True)
target_assembly = models.ForeignKey(Assembly, on_delete=models.CASCADE)
source_code = models.TextField(blank=True)

View File

@@ -179,6 +179,7 @@ class TerseScratchSerializer(ScratchSerializer):
"max_score",
"project",
"project_function",
"parent",
]

View File

@@ -161,6 +161,7 @@ def update_needs_recompile(partial: Dict[str, Any]) -> bool:
"compiler",
"compiler_flags",
"diff_flags",
"diff_label",
"source_code",
"context",
]
@@ -349,6 +350,8 @@ class ScratchViewSet(
scratch.compiler_flags = request.data["compiler_flags"]
if "diff_flags" in request.data:
scratch.diff_flags = request.data["diff_flags"]
if "diff_label" in request.data:
scratch.diff_label = request.data["diff_label"]
if "source_code" in request.data:
scratch.source_code = request.data["source_code"]
if "context" in request.data:

25
backend/poetry.lock generated
View File

@@ -88,7 +88,7 @@ unicode_backport = ["unicodedata2"]
[[package]]
name = "click"
version = "8.1.0"
version = "8.1.2"
description = "Composable command line interface toolkit"
category = "dev"
optional = false
@@ -176,7 +176,7 @@ dev = ["tox", "bump2version (<1)", "sphinx (<2)", "importlib-metadata (<3)", "im
[[package]]
name = "django"
version = "4.0.3"
version = "4.0.4"
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
category = "main"
optional = false
@@ -575,7 +575,7 @@ python-versions = ">=3.7"
[[package]]
name = "tqdm"
version = "4.63.1"
version = "4.64.0"
description = "Fast, Extensible Progress Meter"
category = "main"
optional = false
@@ -587,6 +587,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
[package.extras]
dev = ["py-make (>=0.1.0)", "twine", "wheel"]
notebook = ["ipywidgets (>=6)"]
slack = ["slack-sdk"]
telegram = ["requests"]
[[package]]
@@ -618,7 +619,7 @@ python-versions = "*"
[[package]]
name = "types-requests"
version = "2.27.15"
version = "2.27.16"
description = "Typing stubs for requests"
category = "dev"
optional = false
@@ -824,8 +825,8 @@ charset-normalizer = [
{file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"},
]
click = [
{file = "click-8.1.0-py3-none-any.whl", hash = "sha256:19a4baa64da924c5e0cd889aba8e947f280309f1a2ce0947a3e3a7bcb7cc72d6"},
{file = "click-8.1.0.tar.gz", hash = "sha256:977c213473c7665d3aa092b41ff12063227751c41d7b17165013e10069cc5cd2"},
{file = "click-8.1.2-py3-none-any.whl", hash = "sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e"},
{file = "click-8.1.2.tar.gz", hash = "sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72"},
]
colorama = [
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
@@ -870,8 +871,8 @@ deprecated = [
{file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"},
]
django = [
{file = "Django-4.0.3-py3-none-any.whl", hash = "sha256:1239218849e922033a35d2a2f777cb8bee18bd725416744074f455f34ff50d0c"},
{file = "Django-4.0.3.tar.gz", hash = "sha256:77ff2e7050e3324c9b67e29b6707754566f58514112a9ac73310f60cd5261930"},
{file = "Django-4.0.4-py3-none-any.whl", hash = "sha256:07c8638e7a7f548dc0acaaa7825d84b7bd42b10e8d22268b3d572946f1e9b687"},
{file = "Django-4.0.4.tar.gz", hash = "sha256:4e8177858524417563cc0430f29ea249946d831eacb0068a1455686587df40b5"},
]
django-cors-headers = [
{file = "django-cors-headers-3.11.0.tar.gz", hash = "sha256:eb98389bf7a2afc5d374806af4a9149697e3a6955b5a2dc2bf049f7d33647456"},
@@ -1184,8 +1185,8 @@ tomli = [
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
tqdm = [
{file = "tqdm-4.63.1-py2.py3-none-any.whl", hash = "sha256:6461b009d6792008d0000e1b0c7ca50195ec78c0e808a3a6b668a56a3236c3a5"},
{file = "tqdm-4.63.1.tar.gz", hash = "sha256:4230a49119a416c88cc47d0d2d32d5d90f1a282d5e497d49801950704e49863d"},
{file = "tqdm-4.64.0-py2.py3-none-any.whl", hash = "sha256:74a2cdefe14d11442cedf3ba4e21a3b84ff9a2dbdc6cfae2c34addb2a14a5ea6"},
{file = "tqdm-4.64.0.tar.gz", hash = "sha256:40be55d30e200777a307a7585aee69e4eabb46b4ec6a4b4a5f2d9f11e7d5408d"},
]
trailrunner = [
{file = "trailrunner-1.1.3-py3-none-any.whl", hash = "sha256:7eea60167384329012a5b5464b4e97da68e8984fa52286094c08dbe9fce8901d"},
@@ -1200,8 +1201,8 @@ types-pyyaml = [
{file = "types_PyYAML-6.0.5-py3-none-any.whl", hash = "sha256:2fd21310870addfd51db621ad9f3b373f33ee3cbb81681d70ef578760bd22d35"},
]
types-requests = [
{file = "types-requests-2.27.15.tar.gz", hash = "sha256:2d371183c535208d2cc8fe7473d9b49c344c7077eb70302eb708638fb86086a8"},
{file = "types_requests-2.27.15-py3-none-any.whl", hash = "sha256:77d09182a68e447e9e8b0ffc21abf54618b96f07689dffbb6a41cf0356542969"},
{file = "types-requests-2.27.16.tar.gz", hash = "sha256:c8010c18b291a7efb60b1452dbe12530bc25693dd657e70c62803fcdc4bffe9b"},
{file = "types_requests-2.27.16-py3-none-any.whl", hash = "sha256:2437a5f4d16c0c8bd7539a8126d492b7aeb41e6cda670d76b286c7f83a658d42"},
]
types-urllib3 = [
{file = "types-urllib3-1.26.11.tar.gz", hash = "sha256:24d64e441168851eb05f1d022de18ae31558f5649c8f1117e384c2e85e31315b"},

View File

@@ -28,7 +28,7 @@
"react": "^18.0.0",
"react-contenteditable": "^3.3.6",
"react-dom": "^18.0.0",
"react-laag": "^2.0.3",
"react-laag": "^2.0.4",
"react-modal": "^3.14.4",
"react-simple-resizer": "^2.1.0",
"react-timeago": "^6.2.1",

View File

@@ -1,7 +1,7 @@
{
"name": "decomp.me",
"short_name": "decomp.me",
"description": "Decompile code in the browser",
"description": "Decompile code",
"theme_color": "#951fd9",
"background_color": "#292f33",
"display": "minimal-ui",
@@ -10,13 +10,15 @@
"start_url": "/",
"icons": [
{
"src": "purplefrog.svg",
"sizes": "48x48 320x320",
"purpose": "monochrome"
"src": "purplefrog-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any monochrome"
},
{
"src": "purplefrog-bg.svg",
"sizes": "48x48 320x320",
"src": "purplefrog-bg-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -8,17 +8,78 @@
width="42"
height="42"
inkscape:export-filename="/Users/alex/bitmap.png"
inkscape:export-xdpi="685.71002"
inkscape:export-ydpi="685.71002"
inkscape:export-xdpi="1170.2858"
inkscape:export-ydpi="1170.2858"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs1029" />
id="defs1029">
<linearGradient
inkscape:collect="always"
id="linearGradient1111">
<stop
style="stop-color:#333b40;stop-opacity:1"
offset="0"
id="stop1107" />
<stop
style="stop-color:#282e31;stop-opacity:1"
offset="1"
id="stop1109" />
</linearGradient>
<filter
style="color-interpolation-filters:sRGB;"
inkscape:label="Drop Shadow"
id="filter1377"
x="-0.13333333"
y="-0.15"
width="1.2666667"
height="1.3625">
<feFlood
flood-opacity="0.2"
flood-color="rgb(0,0,0)"
result="flood"
id="feFlood1367" />
<feComposite
in="flood"
in2="SourceGraphic"
operator="in"
result="composite1"
id="feComposite1369" />
<feGaussianBlur
in="composite1"
stdDeviation="2"
result="blur"
id="feGaussianBlur1371" />
<feOffset
dx="0"
dy="2"
result="offset"
id="feOffset1373" />
<feComposite
in="SourceGraphic"
in2="offset"
operator="over"
result="composite2"
id="feComposite1375" />
</filter>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient1111"
id="radialGradient1547"
cx="22.276571"
cy="41.439331"
fx="22.276571"
fy="41.439331"
r="20.999056"
gradientTransform="matrix(1.7367955,-0.04400681,0.04964455,1.9592976,-18.470515,-38.771409)"
gradientUnits="userSpaceOnUse" />
</defs>
<sodipodi:namedview
id="namedview1027"
pagecolor="#505050"
@@ -29,8 +90,8 @@
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="7.044923"
inkscape:cx="20.014413"
inkscape:cy="31.795947"
inkscape:cx="22.853337"
inkscape:cy="20.724144"
inkscape:window-width="1312"
inkscape:window-height="918"
inkscape:window-x="0"
@@ -41,15 +102,16 @@
<title
id="title1007">decomp.me</title>
<rect
style="fill:#292f33;fill-opacity:1;stroke-width:1.2717"
style="fill:url(#radialGradient1547);fill-opacity:1;stroke-width:1.2717"
id="rect1392"
width="42"
width="41.998112"
height="42"
x="0"
y="0" />
<g
id="g1513"
transform="matrix(0.9053692,0,0,0.92459108,4.7033544,4.3573606)">
transform="matrix(0.69396031,0,0,0.70869376,8.5087144,8.2435123)"
style="filter:url(#filter1377)">
<path
fill="var(--frog-secondary)"
d="M 36,22 C 36,29.456 27.941,34 18,34 8.059,34 0,29.456 0,22 0,14.544 8.059,7 18,7 c 9.941,0 18,7.544 18,15 z"

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -0,0 +1,55 @@
.breadcrumbs {
padding: 1em;
background: var(--g300);
> ol {
margin: 0;
padding-left: 0;
list-style: none;
display: flex;
flex-wrap: wrap;
align-items: stretch;
// Page links
> li {
flex-shrink: 0;
display: inline-flex;
align-items: center;
> a {
color: var(--g1500);
text-decoration: none;
&:hover {
color: var(--link);
text-decoration: underline;
}
&[aria-current="page"] {
color: var(--g1900);
font-weight: 600;
text-decoration: none; // override :hover
}
}
}
// '>' shape between pages
> li + li::before {
content: "";
display: inline-block;
margin: 0 1em;
width: 0.5em;
height: 0.5em;
border-top: 0.1em solid;
border-right: 0.1em solid;
border-color: var(--g1000);
transform: rotate(45deg);
}
}
}

View File

@@ -0,0 +1,34 @@
import { ReactNode } from "react"
import Link from "next/link"
import classNames from "classnames"
import styles from "./Breadcrumbs.module.scss"
export interface Props {
pages: {
label: ReactNode
href?: string
}[]
className?: string
}
export default function Breadcrumbs({ pages, className }: Props) {
// https://www.w3.org/TR/wai-aria-practices/examples/breadcrumb/index.html
return <nav aria-label="Breadcrumb" className={classNames(styles.breadcrumbs, className)}>
<ol>
{pages.map((page, index) => {
const isLast = index == pages.length - 1
const a = <a aria-current={isLast ? "page" : undefined}>
{page.label}
</a>
return <li key={page.href || index}>
{page.href ? <Link href={page.href}>{a}</Link> : a}
</li>
})}
</ol>
</nav>
}

View File

@@ -38,6 +38,10 @@
width: 100px;
flex-shrink: 0;
}
a:hover {
color: var(--link);
}
}
.scratchLinkContainer {

View File

@@ -40,6 +40,20 @@ function ScratchLink({ url }: { url: string }) {
</span>
}
function FamilyField({ scratch }: { scratch: api.Scratch }) {
const { data } = useSWR<api.TerseScratch[]>(`/scratch/${scratch.slug}/family`, api.get)
return <div className={styles.horizontalField}>
<p className={styles.label}>Family</p>
{(data?.length ?? 1) == 1
? "No family"
: <Link href={`/scratch/${scratch.slug}/family`}>
<a>{`${data.length} scratches`}</a>
</Link>
}
</div>
}
export type Props = {
scratch: api.Scratch
setScratch?: (scratch: Partial<api.Scratch>) => void
@@ -70,6 +84,7 @@ export default function AboutScratch({ scratch, setScratch }: Props) {
<p className={styles.label}>Fork of</p>
<ScratchLink url={scratch.parent} />
</div>}
<FamilyField scratch={scratch} />
<div className={styles.horizontalField}>
<p className={styles.label}>Platform</p>
<PlatformIcon platform={scratch.platform} className={styles.platformIcon} />

View File

@@ -111,6 +111,9 @@ export function useLeftTabs({ scratch, setScratch, setSelectedSourceLine }: {
platform={scratch.platform}
value={scratch}
onChange={setScratch}
diffLabel={scratch.diff_label}
onDiffLabelChange={d => setScratch({ diff_label: d })}
/>
</div>
</Tab>

View File

@@ -56,6 +56,15 @@ export default function ScratchList({ url, className, item, emptyButtonLabel }:
</ul>
}
export function LoadedScratchList({ className, item, scratches }: Pick<Props, "className" | "item"> & { scratches: api.TerseScratch[] }) {
const Item = item || ScratchItem
return <ul className={classNames(styles.list, className)}>
{scratches.map(scratch => <Item key={scratch.url} scratch={scratch} />)}
</ul>
}
export function ScratchItem({ scratch }: { scratch: api.TerseScratch }) {
const compilersTranslation = useTranslation("compilers")
const compilerName = compilersTranslation.t(scratch.compiler)

View File

@@ -1,6 +1,21 @@
import { useEffect, useState } from "react"
const isMacOS = typeof window !== "undefined" && window.navigator.userAgent.includes("Mac OS X")
function isMacOS(): boolean {
if (typeof window === "undefined") {
// SSR
return false
}
// Use User-Agent Client Hints API if supported
// @ts-ignore
if (navigator.userAgentData) {
// @ts-ignore
return navigator.userAgentData.platform == "macOS"
}
// Fall back to user-agent sniffing
return navigator.userAgent.includes("Mac OS X")
}
export type Key = string | SpecialKey

View File

@@ -63,13 +63,9 @@
background: var(--g200);
border: 1px solid var(--g400);
border-left: 0;
color: var(--frog-secondary);
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
border-radius: 4px;
font-family: var(--monospace);
font-size: 0.8rem;
@@ -81,6 +77,12 @@
.textbox::-webkit-input-placeholder {
color: var(--g500);
font-family: var(--font-ui);
}
.compilerSelect + .textbox {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.flags {
@@ -137,10 +139,16 @@
border-radius: 0.5em;
}
.flagSetName {
.flagSetName,
.diffLabel label {
cursor: default;
font-size: 0.8rem;
padding: 0.6em 1em;
padding-bottom: 0.2em;
color: var(--g800);
}
.diffLabel input {
display: block;
width: 100%;
}

View File

@@ -4,7 +4,7 @@ import useTranslation from "next-translate/useTranslation"
import * as api from "../../lib/api"
import PlatformIcon from "../PlatformSelect/PlatformIcon"
import Select from "../Select"
import Select from "../Select" // TODO: use Select2
import styles from "./CompilerOpts.module.css"
import { useCompilersForPlatform } from "./compilers"
@@ -92,9 +92,12 @@ export type Props = {
platform?: string
value: CompilerOptsT
onChange: (value: CompilerOptsT) => void
diffLabel: string
onDiffLabelChange: (diffLabel: string) => void
}
export default function CompilerOpts({ platform, value, onChange }: Props) {
export default function CompilerOpts({ platform, value, onChange, diffLabel, onDiffLabelChange }: Props) {
const compiler = value.compiler
let opts = value.compiler_flags
const diff_opts = value.diff_flags || []
@@ -180,6 +183,16 @@ export default function CompilerOpts({ platform, value, onChange }: Props) {
{display_diff_opts &&
<section className={styles.section}>
<h3 className={styles.heading}>Diff options</h3>
<div className={styles.diffLabel}>
<label>Diff label</label>
<input
type="text"
className={styles.textbox}
value={diffLabel}
placeholder="Top of file"
onChange={e => onDiffLabelChange(e.target.value)}
/>
</div>
<DiffOptsEditor platform={platform} compiler={compiler} />
</section>}
</OptsContext.Provider>
@@ -223,7 +236,7 @@ export function OptsEditor({ platform, compiler: compilerId, setCompiler, opts,
type="text"
className={styles.textbox}
value={opts}
placeholder="no arguments"
placeholder="No arguments"
onChange={e => setOpts((e.target as HTMLInputElement).value)}
/>
</div>

View File

@@ -6,6 +6,8 @@
display: inline-block;
vertical-align: middle;
img {
border-radius: 999px;
overflow: hidden;

View File

@@ -173,6 +173,7 @@ export interface TerseScratch {
url: string
html_url: string
owner: AnonymousUser | User | null // null = unclaimed
parent: string | null
name: string
creation_time: string
last_updated: string
@@ -193,7 +194,6 @@ export interface Scratch extends TerseScratch {
source_code: string
context: string
diff_label: string
parent: string | null
}
export interface Project {
@@ -357,6 +357,7 @@ export function useSaveScratch(localScratch: Scratch): () => Promise<Scratch> {
compiler: undefinedIfUnchanged(savedScratch, localScratch, "compiler"),
compiler_flags: undefinedIfUnchanged(savedScratch, localScratch, "compiler_flags"),
diff_flags: undefinedIfUnchanged(savedScratch, localScratch, "diff_flags"),
diff_label: undefinedIfUnchanged(savedScratch, localScratch, "diff_label"),
preset: undefinedIfUnchanged(savedScratch, localScratch, "preset"),
name: undefinedIfUnchanged(savedScratch, localScratch, "name"),
description: undefinedIfUnchanged(savedScratch, localScratch, "description"),
@@ -413,7 +414,8 @@ export function useIsScratchSaved(scratch: Scratch): boolean {
scratch.description === saved.description &&
scratch.compiler === saved.compiler &&
scratch.compiler_flags === saved.compiler_flags &&
scratch.diff_flags === saved.diff_flags &&
scratch.diff_flags.join(",") === saved.diff_flags.join(",") &&
scratch.diff_label === saved.diff_label &&
scratch.source_code === saved.source_code &&
scratch.context === saved.context
)
@@ -447,6 +449,7 @@ export function useCompilation(scratch: Scratch | null, autoRecompile = true, au
compiler: scratch.compiler,
compiler_flags: scratch.compiler_flags,
diff_flags: scratch.diff_flags,
diff_label: scratch.diff_label,
source_code: scratch.source_code,
context: savedScratch ? undefinedIfUnchanged(savedScratch, scratch, "context") : scratch.context,
}).then((compilation: Compilation) => {
@@ -496,7 +499,7 @@ export function useCompilation(scratch: Scratch | null, autoRecompile = true, au
// fields passed to compilations
scratch.compiler,
scratch.compiler_flags, scratch.diff_flags,
scratch.compiler_flags, scratch.diff_flags, scratch.diff_label,
scratch.source_code, scratch.context,
])

View File

@@ -5,37 +5,13 @@
.headerInner {
max-width: 50em;
padding: 1em;
margin: 0 auto;
}
// breadcrumbs
h1 {
color: var(--g500); // "/" separator color
font-size: 1.25em;
font-weight: 300;
.projectLink {
display: flex;
align-items: center;
gap: 0.5em;
// breadcrumbs
a {
display: flex;
align-items: center;
gap: 0.5em;
font-weight: 500;
color: var(--g1200);
&:last-child {
color: var(--g1900);
}
&:any-link:hover {
color: var(--link);
}
}
}
}

View File

@@ -7,6 +7,7 @@ import { useRouter } from "next/router"
import { ArrowRightIcon } from "@primer/octicons-react"
import AsyncButton from "../../components/AsyncButton"
import Breadcrumbs from "../../components/Breadcrumbs"
import Button from "../../components/Button"
import ErrorBoundary from "../../components/ErrorBoundary"
import Footer from "../../components/Footer"
@@ -83,18 +84,18 @@ export default function ProjectFunctionPage({ project, func, attempts }: { proje
<Nav />
<header className={styles.header}>
<div className={styles.headerInner}>
<h1>
<Link href={project.html_url}>
<a>
<Image src={project.icon_url} alt="" width={32} height={32} />
<Breadcrumbs pages={[
{
label: <div className={styles.projectLink}>
<Image src={project.icon_url} alt="" width={24} height={24} />
{project.slug}
</a>
</Link>
{" / "}
<a>
{func.display_name}
</a>
</h1>
</div>,
href: project.html_url,
},
{
label: func.display_name,
},
]} />
</div>
</header>
<main className={styles.container}>

View File

@@ -3,6 +3,7 @@
:root {
--link: #3db8e9;
--monospace: "Menlo", "Monaco", monospace;
--font-ui: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji;
}
.themePlum {
@@ -120,7 +121,7 @@
html {
font-size: 16px;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji;
font-family: var(--font-ui);
line-height: 1.5;
text-rendering: optimizeQuality;
overflow: hidden;

View File

@@ -27,7 +27,7 @@ function getLabels(asm: string): string[] {
const lines = asm.split("\n")
let labels = []
const jtbl_label_regex = /L[0-9a-fA-F]{8}/
const jtbl_label_regex = /(^L[0-9a-fA-F]{8}$)|(^jtbl_)/
for (const line of lines) {
let match = line.match(/^\s*glabel\s+([A-z0-9_]+)\s*$/)
@@ -84,7 +84,7 @@ export default function NewScratch({ serverCompilers }: {
const defaultLabel = useMemo(() => {
const labels = getLabels(asm)
return labels.length > 0 ? labels[labels.length - 1] : null
return labels.length > 0 ? labels[0] : null
}, [asm])
const [label, setLabel] = useState<string>("")
@@ -104,7 +104,7 @@ export default function NewScratch({ serverCompilers }: {
setPlatform(localStorage["new_scratch_platform"] ?? "")
setCompiler(localStorage["new_scratch_compiler"] ?? undefined)
setCompilerFlags(localStorage["new_scratch_compilerFlags"] ?? "")
setDiffFlags(localStorage["new_scratch_diffFlags"] ?? "")
setDiffFlags(JSON.parse(localStorage["new_scratch_diffFlags"]) ?? [])
setPresetName(localStorage["new_scratch_presetName"] ?? "")
} catch (error) {
console.warn("bad localStorage", error)
@@ -119,7 +119,7 @@ export default function NewScratch({ serverCompilers }: {
localStorage["new_scratch_platform"] = platform
localStorage["new_scratch_compiler"] = compilerId
localStorage["new_scratch_compilerFlags"] = compilerFlags
localStorage["new_scratch_diffFlags"] = diffFlags
localStorage["new_scratch_diffFlags"] = JSON.stringify(diffFlags)
localStorage["new_scratch_presetName"] = presetName
}, [label, asm, context, platform, compilerId, compilerFlags, diffFlags, presetName])
@@ -245,7 +245,7 @@ export default function NewScratch({ serverCompilers }: {
<div>
<label className={styles.label} htmlFor="label">
Function name <small>(asm label from which the diff will begin)</small>
Diff label <small>(asm label from which the diff will begin)</small>
</label>
<input
name="label"

View File

@@ -0,0 +1,26 @@
.container {
width: 100%;
max-width: 50em;
margin: 0 auto;
padding: 0 0.5em;
}
.header {
width: 100%;
background: var(--g300);
}
.actions {
padding: 0.5em 0;
display: flex;
justify-content: flex-end;
> label {
font-size: 0.9em;
display: inline-flex;
align-items: center;
gap: 0.5em;
}
}

View File

@@ -0,0 +1,112 @@
import { useState } from "react"
import { GetServerSideProps } from "next"
import Breadcrumbs from "../../../components/Breadcrumbs"
import Footer from "../../../components/Footer"
import Nav from "../../../components/Nav"
import PageTitle from "../../../components/PageTitle"
import { LoadedScratchList } from "../../../components/ScratchList"
import Select from "../../../components/Select2"
import UserAvatar from "../../../components/user/UserAvatar"
import * as api from "../../../lib/api"
import styles from "./family.module.scss"
enum SortMode {
NEWEST_FIRST = "newest_first",
OLDEST_FIRST = "oldest_first",
LAST_UPDATED = "last_updated",
SCORE = "score",
}
function produceSortFunction(sortMode: SortMode): (a: api.TerseScratch, b: api.TerseScratch) => number {
switch (sortMode) {
case SortMode.NEWEST_FIRST:
return (a, b) => new Date(b.creation_time).getTime() - new Date(a.creation_time).getTime()
case SortMode.OLDEST_FIRST:
return (a, b) => new Date(a.creation_time).getTime() - new Date(b.creation_time).getTime()
case SortMode.LAST_UPDATED:
return (a, b) => new Date(a.last_updated).getTime() - new Date(b.last_updated).getTime()
case SortMode.SCORE:
return (a, b) => {
const aScore = a.score == 0 ? Infinity : a.score
const bScore = b.score == 0 ? Infinity : b.score
return aScore - bScore
}
}
}
export const getServerSideProps: GetServerSideProps = async context => {
const { slug } = context.params
try {
const scratch: api.Scratch = await api.get(`/scratch/${slug}`)
const family: api.TerseScratch[] = await api.get(`/scratch/${slug}/family`)
return {
props: {
scratch,
family,
},
}
} catch (error) {
console.log(error)
return {
notFound: true,
}
}
}
export default function ScratchPage({ scratch, family }: { scratch: api.Scratch, family: api.TerseScratch[] }) {
const [sortMode, setSortMode] = useState(SortMode.NEWEST_FIRST)
// Sort family in-place
family.sort(produceSortFunction(sortMode))
return <>
<PageTitle
title={`Family of '${scratch.name}'`}
description={`${family.length} family member${family.length == 1 ? "" : "s"} (forks, parents, siblings)`}
/>
<Nav />
<header className={styles.header}>
<div className={styles.container}>
<Breadcrumbs pages={[
(!scratch.owner || api.isAnonUser(scratch.owner))
? { label: "anon" }
: {
label: <>
<UserAvatar user={scratch.owner} />
<span style={{ marginLeft: "6px" }} />
{scratch.owner.username}
</>,
href: `/u/${scratch.owner.username}`,
},
{ label: scratch.name, href: scratch.url },
{ label: "Family" },
]} />
</div>
</header>
<main className={styles.container}>
<div className={styles.actions}>
<label>
Sort by
<Select
value={sortMode}
onChange={m => setSortMode(m as SortMode)}
options={{
[SortMode.NEWEST_FIRST]: "Newest first",
[SortMode.OLDEST_FIRST]: "Oldest first",
[SortMode.LAST_UPDATED]: "Last modified",
[SortMode.SCORE]: "Match completion",
}}
/>
</label>
</div>
<LoadedScratchList scratches={family} />
</main>
<Footer />
</>
}

View File

@@ -6495,10 +6495,10 @@ react-is@^18.0.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.0.0.tgz#026f6c4a27dbe33bf4a35655b9e1327c4e55e3f5"
integrity sha512-yUcBYdBBbo3QiPsgYDcfQcIkGZHfxOaoE6HLSnr1sPzMhdyxusbfKOSUbSd/ocGi32dxcj366PsTj+5oggeKKw==
react-laag@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/react-laag/-/react-laag-2.0.3.tgz#2be19aed3091ebe648f55325fbad9891708b7c33"
integrity sha512-f3LYHu6kOU+Ii63/ZkzLDobGLf2Q4EYjIA/TUHmaQmBv5use09ClPy5tEgXazkTnp/PC1Sivb+wr0+q8mv9ITQ==
react-laag@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/react-laag/-/react-laag-2.0.4.tgz#9a2787ca9d83bf4d8b6e304f29d22cff0365348c"
integrity sha512-9CGIwYJbysmpQC4KeeTx3fNzchvZT3AIYapi2/z7kOJrYopP2uCoPK39qHKuiyawE57EVRI8F1OtbJeyJ7NTrg==
dependencies:
tiny-warning "^1.0.3"