Initial commit of new UI frontend component
This commit is contained in:
23
Il2CppInspector.Redux.GUI.UI/src/routes/+layout.svelte
Normal file
23
Il2CppInspector.Redux.GUI.UI/src/routes/+layout.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import Footer from "$lib/components/Footer.svelte";
|
||||
import Header from "$lib/components/Header.svelte";
|
||||
import Loading from "$lib/components/loading.svelte";
|
||||
import { Toaster } from "$lib/components/ui/sonner";
|
||||
import "../app.css";
|
||||
import { ModeWatcher } from "mode-watcher";
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<ModeWatcher />
|
||||
<Toaster richColors />
|
||||
|
||||
<Header />
|
||||
<Footer />
|
||||
|
||||
<div
|
||||
class="absolute inset-x-0 top-[--header-height] mb-[--footer-height] flex h-[--main-height]"
|
||||
>
|
||||
<Loading>
|
||||
{@render children()}
|
||||
</Loading>
|
||||
</div>
|
||||
5
Il2CppInspector.Redux.GUI.UI/src/routes/+layout.ts
Normal file
5
Il2CppInspector.Redux.GUI.UI/src/routes/+layout.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// Tauri doesn't have a Node.js server to do proper SSR
|
||||
// so we will use adapter-static to prerender the app (SSG)
|
||||
// See: https://v2.tauri.app/start/frontend/sveltekit/ for more info
|
||||
export const prerender = true;
|
||||
export const ssr = false;
|
||||
134
Il2CppInspector.Redux.GUI.UI/src/routes/+page.svelte
Normal file
134
Il2CppInspector.Redux.GUI.UI/src/routes/+page.svelte
Normal file
@@ -0,0 +1,134 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { signalRState } from "../lib/signalr/api.svelte";
|
||||
import { toast } from "svelte-sonner";
|
||||
import {
|
||||
getCurrentWebview,
|
||||
type DragDropEvent,
|
||||
} from "@tauri-apps/api/webview";
|
||||
import type { UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { goto } from "$app/navigation";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
|
||||
async function chooseFile(event: Event) {
|
||||
event.preventDefault();
|
||||
|
||||
const selection = await open({
|
||||
title: "Select your input files",
|
||||
multiple: true,
|
||||
directory: false,
|
||||
filters: [
|
||||
{
|
||||
name: "IL2CPP metadata",
|
||||
extensions: ["dat", "dat.dec"],
|
||||
},
|
||||
{
|
||||
name: "Android app packages",
|
||||
extensions: ["apk", "xapk", "apkm", "aab"],
|
||||
},
|
||||
{
|
||||
name: "iOS app packages",
|
||||
extensions: ["ipa"],
|
||||
},
|
||||
{
|
||||
name: "Archives",
|
||||
extensions: ["zip"],
|
||||
},
|
||||
{
|
||||
name: "Native libraries",
|
||||
extensions: ["dll", "so", "dylib"],
|
||||
},
|
||||
{
|
||||
name: "All files",
|
||||
extensions: ["*"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (selection === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await signalRState.api?.server.submitInputFiles(
|
||||
Array.isArray(selection) ? selection : [selection],
|
||||
);
|
||||
}
|
||||
|
||||
async function handleDragDropEvent(event: DragDropEvent) {
|
||||
if (event.type === "drop") {
|
||||
await signalRState.api?.server.submitInputFiles(event.paths);
|
||||
}
|
||||
}
|
||||
|
||||
let unlisten: UnlistenFn | undefined;
|
||||
|
||||
onMount(async () => {
|
||||
if (!signalRState.apiAvailable) {
|
||||
try {
|
||||
await signalRState.start();
|
||||
await signalRState.api?.server.sendUiLaunched();
|
||||
} catch (e) {
|
||||
if (e instanceof Error) toast.error(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
unlisten = await getCurrentWebview().onDragDropEvent((event) => {
|
||||
handleDragDropEvent(event.payload);
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
unlisten?.();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex w-screen flex-col">
|
||||
<button
|
||||
class="m-auto h-[calc(var(--main-height)-10vh)] w-[75vh] content-center rounded-md border-4 border-dashed text-center hover:cursor-pointer hover:border-dotted"
|
||||
onclick={chooseFile}
|
||||
>
|
||||
<div class="mt-[7.5%]">
|
||||
<h3 class="scroll-m-20 text-2xl font-semibold tracking-tight">
|
||||
Drag and drop, or select
|
||||
</h3>
|
||||
|
||||
<p class="text-m text-muted-foreground">your input files.</p>
|
||||
|
||||
<br />
|
||||
|
||||
<div>
|
||||
<div class="mt-10 text-lg font-semibold">
|
||||
Supported file types:
|
||||
</div>
|
||||
|
||||
<ul class="my-3 list-outside list-none [&>li]:mt-2">
|
||||
<li>
|
||||
<p class="text-m text-muted-foreground">
|
||||
IL2CPP metadata files (global-metadata.dat)
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p class="text-m text-muted-foreground">
|
||||
Android app packages (.apk, .xapk, .aab)
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p class="text-m text-muted-foreground">
|
||||
iOS app packages (.ipa, decrypted only)
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p class="text-m text-muted-foreground">
|
||||
Archives (.zip)
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div class="mx-5 mb-3 flex flex-row-reverse justify-between">
|
||||
<Button href="/options" variant="secondary">Options</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import Button from "$lib/components/ui/button/button.svelte";
|
||||
</script>
|
||||
|
||||
<div class="flex w-screen flex-col">
|
||||
<div class="mb-10 flex h-full flex-col">
|
||||
<h1
|
||||
class="ml-10 scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl"
|
||||
>
|
||||
Advanced information
|
||||
</h1>
|
||||
</div>
|
||||
<div class="mx-5 mb-3 flex flex-row-reverse justify-between">
|
||||
<Button onclick={() => history.back()} variant="outline">Go back</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
51
Il2CppInspector.Redux.GUI.UI/src/routes/export/+page.svelte
Normal file
51
Il2CppInspector.Redux.GUI.UI/src/routes/export/+page.svelte
Normal file
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import Button, {
|
||||
buttonVariants,
|
||||
} from "$lib/components/ui/button/button.svelte";
|
||||
import * as Tooltip from "$lib/components/ui/tooltip";
|
||||
import { cn } from "$lib/utils";
|
||||
import type { PageProps } from "./$types";
|
||||
|
||||
let { data }: PageProps = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex w-screen flex-col">
|
||||
<div class="mb-10 flex h-full flex-col">
|
||||
<h1
|
||||
class="ml-10 scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl"
|
||||
>
|
||||
Select your export type:
|
||||
</h1>
|
||||
<div class="mx-5 mt-10 grid h-full grid-cols-2 gap-4 sm:gap-6">
|
||||
{#each data.outputFormats as format}
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger
|
||||
class={cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"sm:p-10",
|
||||
)}
|
||||
onclick={() => goto(`/export/${format.id}`)}
|
||||
>
|
||||
<h3
|
||||
class="scroll-m-20 text-2xl font-semibold tracking-tight"
|
||||
>
|
||||
{format.name}
|
||||
</h3>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<small class="text-sm font-medium leading-none"
|
||||
>{format.description ?? format.name}</small
|
||||
>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx-5 mb-3 flex flex-row-reverse justify-between">
|
||||
<Button href="/" variant="outline">Cancel</Button>
|
||||
<Button href="/advanced" variant="ghost">Advanced</Button>
|
||||
</div>
|
||||
</div>
|
||||
38
Il2CppInspector.Redux.GUI.UI/src/routes/export/+page.ts
Normal file
38
Il2CppInspector.Redux.GUI.UI/src/routes/export/+page.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
type FormatData = {
|
||||
outputFormats: {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export const load: PageLoad<FormatData> = async () => {
|
||||
return {
|
||||
outputFormats: [
|
||||
{
|
||||
id: "cs",
|
||||
name: "C# prototypes",
|
||||
description: "hehe",
|
||||
},
|
||||
{
|
||||
id: "vssolution",
|
||||
name: "Visual Studio solution",
|
||||
description: "hihi",
|
||||
},
|
||||
{
|
||||
id: "dummydlls",
|
||||
name: ".NET dummy assemblies",
|
||||
},
|
||||
{
|
||||
id: "disassemblermetadata",
|
||||
name: "Disassembler metadata",
|
||||
},
|
||||
{
|
||||
id: "cppscaffolding",
|
||||
name: "C++ scaffolding project",
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,194 @@
|
||||
<script lang="ts">
|
||||
import { page } from "$app/state";
|
||||
import Button from "$lib/components/ui/button/button.svelte";
|
||||
|
||||
import type { PageProps } from "./$types";
|
||||
import Combobox from "$lib/components/settings/combobox.svelte";
|
||||
import Option from "$lib/components/settings/option.svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import PathSelector from "$lib/components/settings/path-selector.svelte";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
import { signalRState } from "$lib/signalr/api.svelte";
|
||||
import { toast } from "svelte-sonner";
|
||||
|
||||
let formatId = page.params.formatId;
|
||||
let { data }: PageProps = $props();
|
||||
|
||||
type ValueType =
|
||||
| {
|
||||
setting: ComboboxSetting;
|
||||
selected: string;
|
||||
}
|
||||
| {
|
||||
setting: OptionSetting;
|
||||
selected: boolean;
|
||||
}
|
||||
| {
|
||||
setting: FilepathSetting;
|
||||
selected: string;
|
||||
};
|
||||
|
||||
let values = $state<ValueType[]>(
|
||||
data.settings.map((value) => {
|
||||
switch (value.type) {
|
||||
case "combobox":
|
||||
return {
|
||||
setting: value,
|
||||
selected: value.default ?? "",
|
||||
};
|
||||
case "option":
|
||||
return {
|
||||
setting: value,
|
||||
selected: value.default ?? false,
|
||||
};
|
||||
case "filepath":
|
||||
return {
|
||||
setting: value,
|
||||
selected: "",
|
||||
};
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
function getValueEntry(id: string) {
|
||||
return values.find((x) => x.setting.name.id === id);
|
||||
}
|
||||
|
||||
function getValue(setting: ComboboxSetting): string;
|
||||
function getValue(setting: OptionSetting): boolean;
|
||||
function getValue(setting: FilepathSetting): string;
|
||||
|
||||
function getValue(setting: SettingTypes): string | boolean {
|
||||
return getValueEntry(setting.name.id)!.selected;
|
||||
}
|
||||
|
||||
function setValue(
|
||||
setting: ComboboxSetting | FilepathSetting,
|
||||
value: string,
|
||||
): void;
|
||||
function setValue(setting: OptionSetting, value: boolean): void;
|
||||
|
||||
function setValue(setting: SettingTypes, value: string | boolean): void {
|
||||
getValueEntry(setting.name.id)!.selected = value;
|
||||
}
|
||||
|
||||
function isDisabled(setting: Setting) {
|
||||
if (setting.condition !== undefined) {
|
||||
const conditionalSetting = getValueEntry(setting.condition.id);
|
||||
if (conditionalSetting === undefined) return true;
|
||||
|
||||
switch (conditionalSetting.setting.type) {
|
||||
case "combobox":
|
||||
if (Array.isArray(setting.condition.value))
|
||||
return !setting.condition.value.includes(
|
||||
conditionalSetting.selected as string,
|
||||
);
|
||||
|
||||
return (
|
||||
conditionalSetting.selected === setting.condition.value
|
||||
);
|
||||
case "option":
|
||||
return (
|
||||
conditionalSetting.selected === setting.condition.value
|
||||
);
|
||||
case "filepath":
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function queueExport(e: Event, shouldStartExport: boolean) {
|
||||
e.preventDefault();
|
||||
|
||||
const exportDirectory = await open({
|
||||
title: "Select the output folder",
|
||||
directory: true,
|
||||
multiple: false,
|
||||
recursive: false,
|
||||
});
|
||||
|
||||
if (exportDirectory === null) return;
|
||||
|
||||
const settings = new Map<string, string>(
|
||||
values.map((x) => [x.setting.name.id, x.selected.toString()]),
|
||||
);
|
||||
|
||||
await signalRState.api?.server.queueExport(
|
||||
formatId,
|
||||
exportDirectory,
|
||||
settings,
|
||||
);
|
||||
|
||||
if (shouldStartExport) {
|
||||
await signalRState.api?.server.startExport();
|
||||
} else {
|
||||
toast.info("Successfully queued export.");
|
||||
}
|
||||
|
||||
await goto("/export");
|
||||
}
|
||||
|
||||
let isExportAvailable = $derived.by(() => {
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
if (values[i].selected === "") return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex w-screen flex-col">
|
||||
<div class="mb-10 flex h-full flex-col">
|
||||
<h1
|
||||
class="ml-10 scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl"
|
||||
>
|
||||
Adjust your export settings:
|
||||
</h1>
|
||||
|
||||
<div class="mx-5 mt-10 h-full *:mt-5">
|
||||
{#each data.settings as setting}
|
||||
{#if setting.type == "combobox"}
|
||||
<Combobox
|
||||
bind:selected={
|
||||
() => getValue(setting), (v) => setValue(setting, v)
|
||||
}
|
||||
disabled={isDisabled(setting)}
|
||||
{setting}
|
||||
/>
|
||||
{:else if setting.type == "option"}
|
||||
<Option
|
||||
bind:selected={
|
||||
() => getValue(setting), (v) => setValue(setting, v)
|
||||
}
|
||||
disabled={isDisabled(setting)}
|
||||
{setting}
|
||||
/>
|
||||
{:else if setting.type == "filepath"}
|
||||
<PathSelector
|
||||
bind:selected={
|
||||
() => getValue(setting), (v) => setValue(setting, v)
|
||||
}
|
||||
disabled={isDisabled(setting)}
|
||||
{setting}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx-5 mb-3 flex flex-row-reverse">
|
||||
<Button href="/export" variant="outline">Go back</Button>
|
||||
<Button
|
||||
onclick={(e) => queueExport(e, true)}
|
||||
class="mr-5"
|
||||
disabled={!isExportAvailable}>Export</Button
|
||||
>
|
||||
<Button
|
||||
onclick={(e) => queueExport(e, false)}
|
||||
class="mr-5"
|
||||
variant="secondary"
|
||||
disabled={!isExportAvailable}>Queue</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,260 @@
|
||||
import { signalRState } from "$lib/signalr/api.svelte";
|
||||
import type { EntryGenerator, PageLoad } from "./$types";
|
||||
|
||||
interface FormatConfiguration {
|
||||
settings: SettingTypes[];
|
||||
}
|
||||
|
||||
let mockFormatSettings: {
|
||||
[key: string]: FormatConfiguration;
|
||||
} = {
|
||||
cs: {
|
||||
settings: [
|
||||
{
|
||||
type: "combobox",
|
||||
name: {
|
||||
id: "layout",
|
||||
label: "Layout",
|
||||
},
|
||||
default: "singlefile",
|
||||
values: [
|
||||
{
|
||||
id: "singlefile",
|
||||
label: "Single file",
|
||||
},
|
||||
{
|
||||
id: "namespace",
|
||||
label: "File per namespace",
|
||||
},
|
||||
{
|
||||
id: "assembly",
|
||||
label: "File per assembly",
|
||||
},
|
||||
{
|
||||
id: "class",
|
||||
label: "File per class",
|
||||
},
|
||||
{
|
||||
id: "tree",
|
||||
label: "Tree layout",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "option",
|
||||
name: {
|
||||
id: "flatten",
|
||||
label: "Don't nest folders (flatten hierarchy)",
|
||||
},
|
||||
default: false,
|
||||
condition: {
|
||||
id: "layout",
|
||||
value: ["namespace", "class", "tree"],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "combobox",
|
||||
name: {
|
||||
id: "sorting",
|
||||
label: "Type sorting",
|
||||
},
|
||||
default: "alphabetical",
|
||||
values: [
|
||||
{
|
||||
id: "alphabetical",
|
||||
label: "Alphabetical",
|
||||
},
|
||||
{
|
||||
id: "typedefinitionindex",
|
||||
label: "Type definition index",
|
||||
},
|
||||
],
|
||||
condition: {
|
||||
id: "layout",
|
||||
value: ["singlefile", "namespace", "assembly"],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "option",
|
||||
name: {
|
||||
id: "suppressmetadata",
|
||||
label: "Suppress pointer, offset and index metadata comments",
|
||||
},
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
type: "option",
|
||||
name: {
|
||||
id: "compilable",
|
||||
label: "Attempt to generate output that compiles",
|
||||
},
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
type: "option",
|
||||
name: {
|
||||
id: "seperateassemblyattributes",
|
||||
label: "Place assembly-level attributes in seperate files",
|
||||
},
|
||||
default: true,
|
||||
condition: {
|
||||
id: "layout",
|
||||
value: ["assembly", "tree"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
vssolution: {
|
||||
settings: [
|
||||
{
|
||||
type: "filepath",
|
||||
name: {
|
||||
id: "editorpath",
|
||||
label: "Unity editor path",
|
||||
},
|
||||
directoryPath: true,
|
||||
},
|
||||
{
|
||||
type: "filepath",
|
||||
name: {
|
||||
id: "assembliespath",
|
||||
label: "Unity script assemblies path",
|
||||
},
|
||||
directoryPath: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
dummydlls: {
|
||||
settings: [
|
||||
{
|
||||
type: "option",
|
||||
name: {
|
||||
id: "suppressmetadata",
|
||||
label: "Suppress output of all metadata attributes",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
disassemblermetadata: {
|
||||
settings: [
|
||||
{
|
||||
type: "combobox",
|
||||
name: {
|
||||
id: "unityversion",
|
||||
label: "Unity version (if known)",
|
||||
},
|
||||
values: [
|
||||
{
|
||||
id: "nya",
|
||||
label: "nya",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "combobox",
|
||||
name: {
|
||||
id: "disassembler",
|
||||
label: "Target disassembler",
|
||||
},
|
||||
values: [
|
||||
{
|
||||
id: "idapro",
|
||||
label: "IDA Pro v7.7+",
|
||||
},
|
||||
{
|
||||
id: "ghidra",
|
||||
label: "Ghidra v11.3+",
|
||||
},
|
||||
{
|
||||
id: "binaryninja",
|
||||
label: "Binary Ninja",
|
||||
},
|
||||
{
|
||||
id: "none",
|
||||
label: "None",
|
||||
},
|
||||
],
|
||||
default: "idapro",
|
||||
},
|
||||
],
|
||||
},
|
||||
cppscaffolding: {
|
||||
settings: [
|
||||
{
|
||||
type: "combobox",
|
||||
name: {
|
||||
id: "unityversion",
|
||||
label: "Unity version (if known)",
|
||||
},
|
||||
values: [
|
||||
{
|
||||
id: "nya",
|
||||
label: "nya",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "combobox",
|
||||
name: {
|
||||
id: "compiler",
|
||||
label: "Target C++ compiler for output",
|
||||
},
|
||||
values: [
|
||||
{
|
||||
id: "msvc",
|
||||
label: "MSVC",
|
||||
},
|
||||
{
|
||||
id: "gcc",
|
||||
label: "GCC",
|
||||
},
|
||||
],
|
||||
default: "msvc",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const load: PageLoad<FormatConfiguration> = async ({ params }) => {
|
||||
const unityVersions =
|
||||
(await signalRState.api?.server.getPotentialUnityVersions()) ?? [];
|
||||
|
||||
const unityVersionEntries = unityVersions.map<StringValue>((version) => ({
|
||||
id: version,
|
||||
label: version,
|
||||
}));
|
||||
|
||||
let settings = mockFormatSettings[params.formatId];
|
||||
|
||||
settings.settings.forEach((setting) => {
|
||||
if (setting.name.id === "unityversion" && setting.type === "combobox") {
|
||||
setting.values = unityVersionEntries;
|
||||
setting.default =
|
||||
unityVersionEntries.length > 0
|
||||
? unityVersionEntries[0].id
|
||||
: undefined;
|
||||
}
|
||||
});
|
||||
|
||||
return settings;
|
||||
};
|
||||
|
||||
export const entries: EntryGenerator = () => {
|
||||
return [
|
||||
{
|
||||
formatId: "cs",
|
||||
},
|
||||
{
|
||||
formatId: "vssolution",
|
||||
},
|
||||
{
|
||||
formatId: "dummydlls",
|
||||
},
|
||||
{
|
||||
formatId: "disassemblermetadata",
|
||||
},
|
||||
{
|
||||
formatId: "cppscaffolding",
|
||||
},
|
||||
];
|
||||
};
|
||||
16
Il2CppInspector.Redux.GUI.UI/src/routes/options/+page.svelte
Normal file
16
Il2CppInspector.Redux.GUI.UI/src/routes/options/+page.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
</script>
|
||||
|
||||
<div class="flex w-screen flex-col">
|
||||
<div class="mb-10 flex h-full flex-col">
|
||||
<h1
|
||||
class="ml-10 scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl"
|
||||
>
|
||||
Options
|
||||
</h1>
|
||||
</div>
|
||||
<div class="mx-5 mb-3 flex flex-row-reverse justify-between">
|
||||
<Button href="/" variant="outline">Go back</Button>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user