diff --git a/frontend/src/components/Scratch/LibraryPanel.module.css b/frontend/src/components/Scratch/LibraryPanel.module.css
new file mode 100644
index 00000000..a2629b87
--- /dev/null
+++ b/frontend/src/components/Scratch/LibraryPanel.module.css
@@ -0,0 +1,42 @@
+.section {
+ padding: 1.5em;
+ border-radius: 0;
+ background: transparent;
+}
+
+.section > h3 {
+ font-size: 1.1em;
+ font-weight: 500;
+
+ padding: 0.5em;
+ padding-top: 0;
+
+ color: var(--g1200);
+}
+
+.section:not(:last-child) {
+ border-bottom: 1px solid var(--a100);
+}
+
+.library {
+ display: inline-flex;
+ flex-direction: row;
+
+ width: 100%;
+ border-radius: 0.5em;
+}
+
+.libraryName {
+ cursor: default;
+ font-size: 0.8rem;
+
+ font-size: 1.0em;
+ font-weight: 500;
+
+ padding: .5em;
+
+}
+
+.librarySelect {
+ flex-grow: 1;
+}
diff --git a/frontend/src/components/Scratch/LibraryPanel.tsx b/frontend/src/components/Scratch/LibraryPanel.tsx
new file mode 100644
index 00000000..f6937e37
--- /dev/null
+++ b/frontend/src/components/Scratch/LibraryPanel.tsx
@@ -0,0 +1,85 @@
+import { useLibraries } from "@/lib/api"
+import { Library, TerseScratch } from "@/lib/api/types"
+
+import Select from "../Select2"
+
+import styles from "./LibraryPanel.module.css"
+
+type LibrariesT = {
+ libraries: Library[]
+}
+
+type Props = {
+ scratch: TerseScratch
+ onChange: (value: LibrariesT) => void
+}
+
+export default function LibraryPanel({ scratch, onChange }: Props) {
+ const libraries = useLibraries()
+
+ const hasLibrary = libName => scratch.libraries.some(lib => lib.name == libName)
+ const libraryVersion = lib => {
+ const scratchlib = scratch.libraries.find(scratchlib => scratchlib.name == lib.name)
+ if (scratchlib != null) {
+ return scratchlib.version
+ } else {
+ return "___NULL_VERSION___"
+ }
+ }
+
+ const setLibraryVersion = (libName, ver) => {
+ if (ver == "___NULL_VERSION___") {
+ return unsetLibrary(libName)
+ }
+
+ // clone the libraries
+ const libs = JSON.parse(JSON.stringify(scratch.libraries))
+ // Check if the library is already enabled, if so return it
+ const scratchlib = scratch.libraries.find(scratchlib => scratchlib.name == libName)
+ if (scratchlib != null) {
+ // If it is, set the version
+ scratchlib.version = ver
+ } else {
+ // If it isn't, add the library to the list
+ libs.push({ name: libName, version: ver })
+ }
+ onChange({
+ libraries: libs,
+ })
+ }
+ const unsetLibrary = libName => {
+ // clone the libraries
+ let libs = JSON.parse(JSON.stringify(scratch.libraries))
+ // Only keep the libs whose name are not libName
+ libs = libs.filter(lib => lib.name != libName)
+ onChange({
+ libraries: libs,
+ })
+ }
+ const toggleLibrary = lib => {
+ if (hasLibrary(lib.name)) {
+ unsetLibrary(lib.name)
+ } else {
+ setLibraryVersion(lib.name, lib.supported_versions[0])
+ }
+ }
+
+ const selectOptions = lib => Object.fromEntries([["___NULL_VERSION___", "Disabled"], ...lib.supported_versions.map(ver => [ver, ver])])
+
+ const librariesElements = libraries.map(lib =>
+ toggleLibrary(lib)} />
+
+
)
+
+ return
+
+ Libraries
+ {librariesElements}
+
+
+}
diff --git a/frontend/src/components/Scratch/Scratch.tsx b/frontend/src/components/Scratch/Scratch.tsx
index 63b8cc0d..b512f12c 100644
--- a/frontend/src/components/Scratch/Scratch.tsx
+++ b/frontend/src/components/Scratch/Scratch.tsx
@@ -21,6 +21,7 @@ import AboutScratch from "./AboutScratch"
import DecompilationPanel from "./DecompilePanel"
import FamilyPanel from "./FamilyPanel"
import useLanguageServer from "./hooks/useLanguageServer"
+import LibraryPanel from "./LibraryPanel"
import styles from "./Scratch.module.scss"
import ScratchMatchBanner from "./ScratchMatchBanner"
import ScratchToolbar from "./ScratchToolbar"
@@ -33,6 +34,7 @@ enum TabId {
DIFF = "scratch_diff",
DECOMPILATION = "scratch_decompilation",
FAMILY = "scratch_family",
+ LIBRARIES = "libraries",
}
const DEFAULT_LAYOUTS: Record<"desktop_2col" | "mobile_2row", Layout> = {
@@ -52,6 +54,7 @@ const DEFAULT_LAYOUTS: Record<"desktop_2col" | "mobile_2row", Layout> = {
TabId.SOURCE_CODE,
TabId.CONTEXT,
TabId.OPTIONS,
+ TabId.LIBRARIES,
],
},
{
@@ -92,6 +95,7 @@ const DEFAULT_LAYOUTS: Record<"desktop_2col" | "mobile_2row", Layout> = {
TabId.SOURCE_CODE,
TabId.CONTEXT,
TabId.OPTIONS,
+ TabId.LIBRARIES,
],
},
],
@@ -274,6 +278,10 @@ export default function Scratch({
return
{() => }
+ case TabId.LIBRARIES:
+ return
+
+
default:
return
}
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
index 6c782ab8..dac9b952 100644
--- a/frontend/src/lib/api.ts
+++ b/frontend/src/lib/api.ts
@@ -6,7 +6,7 @@ import useSWR, { Revalidator, RevalidatorOptions, mutate } from "swr"
import { useDebouncedCallback } from "use-debounce"
import { ResponseError, get, post, patch, delete_ } from "./api/request"
-import { AnonymousUser, User, Scratch, TerseScratch, Compilation, Page, Compiler, Platform, Project, ProjectMember } from "./api/types"
+import { AnonymousUser, User, Scratch, TerseScratch, Compilation, Page, Compiler, LibraryVersions, Platform, Project, ProjectMember } from "./api/types"
import { ignoreNextWarnBeforeUnload } from "./hooks"
function onErrorRetry(error: ResponseError, key: string, config: C, revalidate: Revalidator, { retryCount }: RevalidatorOptions) {
@@ -82,6 +82,7 @@ export function useSaveScratch(localScratch: Scratch): () => Promise {
name: undefinedIfUnchanged(savedScratch, localScratch, "name"),
description: undefinedIfUnchanged(savedScratch, localScratch, "description"),
match_override: undefinedIfUnchanged(savedScratch, localScratch, "match_override"),
+ libraries: undefinedIfUnchanged(savedScratch, localScratch, "libraries"),
})
await mutate(localScratch.url, updatedScratch, false)
@@ -166,6 +167,7 @@ export function useCompilation(scratch: Scratch | null, autoRecompile = true, au
compiler_flags: scratch.compiler_flags,
diff_flags: scratch.diff_flags,
diff_label: scratch.diff_label,
+ libraries: scratch.libraries,
source_code: scratch.source_code,
context: savedScratch ? undefinedIfUnchanged(savedScratch, scratch, "context") : scratch.context,
}).then((compilation: Compilation) => {
@@ -251,6 +253,16 @@ export function useCompilers(): Record {
return data.compilers
}
+export function useLibraries(): LibraryVersions[] {
+ const { data } = useSWR("/libraries", get, {
+ refreshInterval: 0,
+ suspense: true, // TODO: remove
+ onErrorRetry,
+ })
+
+ return data.libraries
+}
+
export function usePaginated(url: string, firstPage?: Page): {
results: T[]
hasNext: boolean
diff --git a/frontend/src/lib/api/types.ts b/frontend/src/lib/api/types.ts
index 2230a230..a61525de 100644
--- a/frontend/src/lib/api/types.ts
+++ b/frontend/src/lib/api/types.ts
@@ -48,6 +48,7 @@ export interface TerseScratch {
match_override: boolean
project: string
project_function: string
+ libraries: Library[]
}
export interface Scratch extends TerseScratch {
@@ -165,6 +166,16 @@ export type Compiler = {
diff_flags: Flag[]
}
+export type Library = {
+ name: string
+ version: string
+}
+
+export type LibraryVersions = {
+ name: string
+ supported_versions: string[]
+}
+
export type Platform = {
name: string
description: string