mirror of
https://github.com/decompme/decomp.me.git
synced 2025-12-21 13:00:46 -06:00
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:
40
backend/coreapp/migrations/0021_diff_flags_default.py
Normal file
40
backend/coreapp/migrations/0021_diff_flags_default.py
Normal 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,
|
||||
),
|
||||
]
|
||||
@@ -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)
|
||||
|
||||
@@ -179,6 +179,7 @@ class TerseScratchSerializer(ScratchSerializer):
|
||||
"max_score",
|
||||
"project",
|
||||
"project_function",
|
||||
"parent",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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
25
backend/poetry.lock
generated
@@ -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"},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
|
||||
BIN
frontend/public/purplefrog-512.png
Normal file
BIN
frontend/public/purplefrog-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
BIN
frontend/public/purplefrog-bg-512.png
Normal file
BIN
frontend/public/purplefrog-bg-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
@@ -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 |
55
frontend/src/components/Breadcrumbs.module.scss
Normal file
55
frontend/src/components/Breadcrumbs.module.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
34
frontend/src/components/Breadcrumbs.tsx
Normal file
34
frontend/src/components/Breadcrumbs.tsx
Normal 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>
|
||||
}
|
||||
@@ -38,6 +38,10 @@
|
||||
width: 100px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--link);
|
||||
}
|
||||
}
|
||||
|
||||
.scratchLinkContainer {
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
display: inline-block;
|
||||
|
||||
vertical-align: middle;
|
||||
|
||||
img {
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -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,
|
||||
])
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
26
frontend/src/pages/scratch/[slug]/family.module.scss
Normal file
26
frontend/src/pages/scratch/[slug]/family.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
112
frontend/src/pages/scratch/[slug]/family.tsx
Normal file
112
frontend/src/pages/scratch/[slug]/family.tsx
Normal 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 />
|
||||
</>
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user