Initial commit of new UI frontend component

This commit is contained in:
LukeFZ
2025-01-25 15:38:45 +01:00
parent cc822b418b
commit 44af299ec3
105 changed files with 10298 additions and 0 deletions

View 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>

View 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;

View 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>

View File

@@ -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>

View 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>

View 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",
},
],
};
};

View File

@@ -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>

View File

@@ -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",
},
];
};

View 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>