Implement new GUI and CLI, fix misc. smaller issues (#22)

* Initial commit of new UI c# component

* Initial commit of new UI frontend component

* target WinExe to hide console window in release mode, move ui exe into resources

* force single file publishing and add initial gh workflow for publishing ui

* fix workflow errors

* update dependencies and remove cxxdemangler, as it was outdated

* fix c# single file output due to invalid output path

* smaller tweaks, hack around loops in cpp type layouting

* process other queued exports even if one fails and show error message

* add basic support for processing LC_DYLD_CHAINED_FIXUPS

* ELF loading should not use the file offset for loading the dynamic section

* fix symbol table loading in some modified elfs

* add "start export" button on format selection screen, clear all toasts after selecting an export format

* embed ui executable directly into c# assembly

* only build tauri component in c# release builds

* add il2cpp file (binary, metadata) export to advanced tab

* fix and enable binary ninja fake string segment support

* add support for metadata

* unify logic for getting element type index

* fix new ui not allowing script exports other than ida

* new ui: clear out loaded binary if no IL2CPP images could be loaded

* fix toAddr calls in ghidra script target

* remove dependency on a section being named .text in loaded pe files

* tweak symbol reading a bit and remove sht relocation reading

* add initial support for required forward references in il2cpp types, also fix issues with type names clashing with il2cpp api types

* reduce clang errors for header file, fix better array size struct, emit required forward definitions in header

* expose forward definitions in AppModel, fix issue with method-only used types not being emitted

* remove debug log line

* fix spelling mistakes in gui outputs

* fix il2cpp_array_size_t not being an actual type for later method definitions

* change the default port for new ui dev to 5000

* show current version and hash in new ui footer

* seperate redux ui impl into FrontendCore project

* make inspector version a server api, split up output subtypes and tweak some option names

* add redux CLI based on redux GUI output formats

* replace all Console.WriteLine calls in core inspector with AnsiConsole calls

* add workflow for new cli and add back old gui workflow

* disable aot publish and enable single file for redux cli
This commit is contained in:
Luke
2025-08-15 21:13:32 +02:00
committed by GitHub
parent e161e0f226
commit 3439ca912b
184 changed files with 13425 additions and 964 deletions

10
Il2CppInspector.Redux.GUI.UI/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

View File

@@ -0,0 +1,4 @@
{
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"tabWidth": 4
}

View File

@@ -0,0 +1,17 @@
{
"$schema": "https://next.shadcn-svelte.com/schema.json",
"style": "new-york",
"tailwind": {
"config": "tailwind.config.ts",
"css": "src\\app.css",
"baseColor": "stone"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils",
"ui": "$lib/components/ui",
"hooks": "$lib/hooks"
},
"typescript": true,
"registry": "https://next.shadcn-svelte.com/registry"
}

View File

@@ -0,0 +1,46 @@
{
"name": "il2cppinspectorredux",
"version": "0.1.0",
"description": "",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"tauri": "tauri"
},
"license": "MIT",
"dependencies": {
"@microsoft/signalr": "^8.0.7",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "~2",
"@tauri-apps/plugin-opener": "^2"
},
"devDependencies": {
"@sveltejs/adapter-static": "^3.0.6",
"@sveltejs/kit": "^2.9.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/typography": "^0.5.16",
"@tauri-apps/cli": "^2",
"autoprefixer": "^10.4.20",
"bits-ui": "1.0.0-next.78",
"clsx": "^2.1.1",
"lucide-svelte": "^0.473.0",
"mode-watcher": "^0.5.0",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.10",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"svelte-sonner": "^0.3.28",
"tailwind-merge": "^2.6.0",
"tailwind-variants": "^0.3.1",
"tailwindcss": "^3.4.17",
"tailwindcss-animate": "^1.0.7",
"typescript": "~5.6.2",
"vite": "^6.0.3"
},
"packageManager": "pnpm@10.0.0+sha512.b8fef5494bd3fe4cbd4edabd0745df2ee5be3e4b0b8b08fa643aa3e4c6702ccc0f00d68fa8a8c9858a735a0032485a44990ed2810526c875e416f001b17df12b"
}

2349
Il2CppInspector.Redux.GUI.UI/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};

View File

@@ -0,0 +1,7 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,26 @@
[package]
name = "il2cppinspectorredux"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "il2cppinspectorredux_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tauri-plugin-dialog = "2"

View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": [
"main"
],
"permissions": [
"core:default",
"opener:default",
"dialog:default"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,20 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command]
fn get_signalr_url() -> String {
let args: Vec<String> = std::env::args().collect();
if args.len() < 2 {
return String::from("");
}
return args[1].clone();
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![get_signalr_url])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
il2cppinspectorredux_lib::run()
}

View File

@@ -0,0 +1,35 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Il2CppInspectorRedux",
"version": "0.1.0",
"identifier": "xyz.lukefz.il2cppinspectorredux",
"build": {
"beforeDevCommand": "pnpm dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "pnpm build",
"frontendDist": "../build"
},
"app": {
"windows": [
{
"title": "Il2CppInspectorRedux",
"width": 800,
"height": 600
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}

View File

@@ -0,0 +1,66 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 72.22% 50.59%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 5.9% 10%;
--radius: 0.5rem;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
@layer base {
:root {
--header-height: 60px;
--footer-height: 30px;
--main-height: calc(100vh - var(--header-height) - var(--footer-height));
}
}

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Tauri + SvelteKit + Typescript App</title>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@@ -0,0 +1,33 @@
<script lang="ts">
import { signalRState } from "$lib/signalr/api.svelte";
let inspectorVersion = $state<string>();
$effect(() => {
if (signalRState.api === undefined) return;
if (inspectorVersion === undefined) {
signalRState.api.server.getInspectorVersion().then((version) => {
inspectorVersion = version;
});
}
});
</script>
<div
class="absolute inset-x-0 bottom-0 flex h-[--footer-height] flex-row justify-between border-t-2 text-center"
>
<div class="ml-4 mt-1">
<p class="text-sm text-muted-foreground">
Il2CppInspectorRedux - created by djkaty, maintained by LukeFZ
</p>
</div>
{#if inspectorVersion !== undefined}
<div class="mr-4 mt-1">
<p class="text-sm text-muted-foreground">
{inspectorVersion}
</p>
</div>
{/if}
</div>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import Button from "./ui/button/button.svelte";
import { Moon, Sun } from "lucide-svelte";
import { toggleMode } from "mode-watcher";
</script>
<div class="absolute inset-x-0 top-0 flex h-[--header-height] flex-row-reverse">
<div class="mr-3 mt-3">
<Button onclick={toggleMode} variant="outline" size="icon">
<Sun
class="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"
/>
<Moon
class="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100"
/>
<span class="sr-only">Toggle theme</span>
</Button>
</div>
</div>

View File

@@ -0,0 +1,64 @@
<script>
import { signalRState } from "$lib/signalr/api.svelte";
import { LoaderCircle } from "lucide-svelte";
let isLoading = $state(false);
let statusMessage = $state("");
let { children } = $props();
/*
let currentIndex = 0;
let mockMessages = [
"Building type model",
"Generating Assembly-CSharp.dll",
"Writing Assembly-CSharp.dll",
"(this is just a test)",
"aaaaaa",
"almost there!",
];
setInterval(() => {
statusMessage = mockMessages[currentIndex++ % mockMessages.length];
}, 100);
*/
$effect(() => {
if (signalRState.api === undefined) return;
const unregisterLogMessage =
signalRState.api.client.onLogMessageReceived(async (message) => {
statusMessage = message;
});
const unregisterBeginLoading = signalRState.api.client.onLoadingStarted(
async () => {
isLoading = true;
},
);
const unregisterFinishLoading =
signalRState.api.client.onLoadingFinished(async () => {
isLoading = false;
statusMessage = "";
});
return () => {
unregisterFinishLoading();
unregisterBeginLoading();
unregisterLogMessage();
};
});
</script>
{#if isLoading}
<div class="flex h-full w-screen flex-col">
<div class="m-auto flex h-full w-screen flex-col items-center">
<LoaderCircle class="mt-[25%] h-16 w-16 animate-spin" />
<p class="leading-7 [&:not(:first-child)]:mt-6">
{statusMessage}
</p>
</div>
</div>
{:else}
{@render children()}
{/if}

View File

@@ -0,0 +1,33 @@
<script lang="ts">
import * as Select from "../ui/select";
let {
selected = $bindable(""),
disabled = $bindable(false),
setting,
}: {
selected: string;
disabled: boolean;
setting: ComboboxSetting;
} = $props();
const selectedLabel = $derived(
setting.values.find((o) => o.id === selected)?.label,
);
</script>
<Select.Root
type="single"
bind:value={selected}
{disabled}
name={setting.name.id}
>
<Select.Trigger class="w-[250px]"
>{setting.name.label}: {selectedLabel}</Select.Trigger
>
<Select.Content>
{#each setting.values as value}
<Select.Item value={value.id}>{value.label}</Select.Item>
{/each}
</Select.Content>
</Select.Root>

View File

@@ -0,0 +1,26 @@
<script lang="ts">
import { Checkbox } from "../ui/checkbox";
import { Label } from "../ui/label";
let {
selected = $bindable(false),
disabled = $bindable(false),
setting,
}: {
selected: boolean;
disabled: boolean;
setting: OptionSetting;
} = $props();
</script>
<div class="items-top mt-5 flex space-x-2">
<Checkbox id={setting.name.id} bind:checked={selected} {disabled} />
<div class="grid gap-1.5 leading-none">
<Label
for={setting.name.id}
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{setting.name.label}
</Label>
</div>
</div>

View File

@@ -0,0 +1,37 @@
<script lang="ts">
import { open } from "@tauri-apps/plugin-dialog";
import Button from "../ui/button/button.svelte";
let {
selected = $bindable(""),
disabled = $bindable(false),
setting,
}: {
selected: string;
disabled: boolean;
setting: FilepathSetting;
} = $props();
async function openFileDialog(e: Event) {
e.preventDefault();
const selection = await open({
directory: setting.directoryPath,
title: `Select ${setting.name.label}`,
multiple: false,
});
if (selection === null) return;
selected = selection;
}
</script>
<div class="mx-[3.3%] flex flex-row-reverse justify-between">
<Button variant="outline" {disabled} onclick={openFileDialog}>Browse</Button
>
<p class="w-fulls mt-2 text-right">
{selected === "" ? "not selected" : selected}
</p>
<p class="mt-2">{setting.name.label}:</p>
</div>

View File

@@ -0,0 +1,75 @@
<script lang="ts" module>
import type { WithElementRef } from "bits-ui";
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
import { type VariantProps, tv } from "tailwind-variants";
export const buttonVariants = tv({
base: "focus-visible:ring-ring inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90 shadow",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-sm",
outline:
"border-input bg-background hover:bg-accent hover:text-accent-foreground border shadow-sm",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-sm",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
WithElementRef<HTMLAnchorAttributes> & {
variant?: ButtonVariant;
size?: ButtonSize;
};
</script>
<script lang="ts">
import { cn } from "$lib/utils.js";
let {
class: className,
variant = "default",
size = "default",
ref = $bindable(null),
href = undefined,
type = "button",
children,
...restProps
}: ButtonProps = $props();
</script>
{#if href}
<a
bind:this={ref}
class={cn(buttonVariants({ variant, size }), className)}
{href}
{...restProps}
>
{@render children?.()}
</a>
{:else}
<button
bind:this={ref}
class={cn(buttonVariants({ variant, size }), className)}
{type}
{...restProps}
>
{@render children?.()}
</button>
{/if}

View File

@@ -0,0 +1,17 @@
import Root, {
type ButtonProps,
type ButtonSize,
type ButtonVariant,
buttonVariants,
} from "./button.svelte";
export {
Root,
type ButtonProps as Props,
//
Root as Button,
buttonVariants,
type ButtonProps,
type ButtonSize,
type ButtonVariant,
};

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div bind:this={ref} class={cn("p-6", className)} {...restProps}>
{@render children?.()}
</div>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
</script>
<p bind:this={ref} class={cn("text-muted-foreground text-sm", className)} {...restProps}>
{@render children?.()}
</p>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div bind:this={ref} class={cn("flex items-center p-6 pt-0", className)} {...restProps}>
{@render children?.()}
</div>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div bind:this={ref} class={cn("flex flex-col space-y-1.5 p-6 pb-0", className)} {...restProps}>
{@render children?.()}
</div>

View File

@@ -0,0 +1,25 @@
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
level = 3,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
level?: 1 | 2 | 3 | 4 | 5 | 6;
} = $props();
</script>
<div
role="heading"
aria-level={level}
bind:this={ref}
class={cn("font-semibold leading-none tracking-tight", className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
class={cn("bg-card text-card-foreground rounded-xl border shadow", className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,22 @@
import Root from "./card.svelte";
import Content from "./card-content.svelte";
import Description from "./card-description.svelte";
import Footer from "./card-footer.svelte";
import Header from "./card-header.svelte";
import Title from "./card-title.svelte";
export {
Root,
Content,
Description,
Footer,
Header,
Title,
//
Root as Card,
Content as CardContent,
Description as CardDescription,
Footer as CardFooter,
Header as CardHeader,
Title as CardTitle,
};

View File

@@ -0,0 +1,35 @@
<script lang="ts">
import { Checkbox as CheckboxPrimitive, type WithoutChildrenOrChild } from "bits-ui";
import Check from "lucide-svelte/icons/check";
import Minus from "lucide-svelte/icons/minus";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
checked = $bindable(false),
indeterminate = $bindable(false),
...restProps
}: WithoutChildrenOrChild<CheckboxPrimitive.RootProps> = $props();
</script>
<CheckboxPrimitive.Root
class={cn(
"border-primary focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground peer box-content size-4 shrink-0 rounded-sm border shadow focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50 data-[disabled=true]:cursor-not-allowed data-[disabled=true]:opacity-50",
className
)}
bind:checked
bind:ref
bind:indeterminate
{...restProps}
>
{#snippet children({ checked, indeterminate })}
<span class="flex items-center justify-center text-current">
{#if indeterminate}
<Minus class="size-4" />
{:else}
<Check class={cn("size-4", !checked && "text-transparent")} />
{/if}
</span>
{/snippet}
</CheckboxPrimitive.Root>

View File

@@ -0,0 +1,6 @@
import Root from "./checkbox.svelte";
export {
Root,
//
Root as Checkbox,
};

View File

@@ -0,0 +1,35 @@
<script lang="ts">
import {
Command as CommandPrimitive,
Dialog as DialogPrimitive,
type WithoutChildrenOrChild,
} from "bits-ui";
import type { Snippet } from "svelte";
import Command from "./command.svelte";
import * as Dialog from "$lib/components/ui/dialog/index.js";
let {
open = $bindable(false),
ref = $bindable(null),
value = $bindable(""),
portalProps,
children,
...restProps
}: WithoutChildrenOrChild<DialogPrimitive.RootProps> &
WithoutChildrenOrChild<CommandPrimitive.RootProps> & {
portalProps?: DialogPrimitive.PortalProps;
children: Snippet;
} = $props();
</script>
<Dialog.Root bind:open {...restProps}>
<Dialog.Content class="overflow-hidden p-0" {portalProps}>
<Command
class="[&_[data-cmdk-group-heading]]:px-2 [&_[data-cmdk-group-heading]]:font-medium [&_[data-cmdk-group]:not([hidden])_~[data-cmdk-group]]:pt-0 [&_[data-cmdk-group]]:px-2 [&_[data-cmdk-input-wrapper]_svg]:h-5 [&_[data-cmdk-input-wrapper]_svg]:w-5 [&_[data-cmdk-input]]:h-12 [&_[data-cmdk-item]]:px-2 [&_[data-cmdk-item]]:py-3 [&_[data-cmdk-item]_svg]:h-5 [&_[data-cmdk-item]_svg]:w-5"
{...restProps}
bind:value
bind:ref
{children}
/>
</Dialog.Content>
</Dialog.Root>

View File

@@ -0,0 +1,12 @@
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CommandPrimitive.EmptyProps = $props();
</script>
<CommandPrimitive.Empty bind:ref class={cn("py-6 text-center text-sm", className)} {...restProps} />

View File

@@ -0,0 +1,29 @@
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
heading,
...restProps
}: CommandPrimitive.GroupProps & {
heading?: string;
} = $props();
</script>
<CommandPrimitive.Group
class={cn("text-foreground overflow-hidden p-1", className)}
bind:ref
{...restProps}
>
{#if heading}
<CommandPrimitive.GroupHeading
class="text-muted-foreground px-2 py-1.5 text-xs font-medium"
>
{heading}
</CommandPrimitive.GroupHeading>
{/if}
<CommandPrimitive.GroupItems {children} />
</CommandPrimitive.Group>

View File

@@ -0,0 +1,25 @@
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import Search from "lucide-svelte/icons/search";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
value = $bindable(""),
...restProps
}: CommandPrimitive.InputProps = $props();
</script>
<div class="flex items-center border-b px-3" data-command-input-wrapper="">
<Search class="mr-2 size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
class={cn(
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-base outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
bind:ref
bind:value
{...restProps}
/>
</div>

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CommandPrimitive.ItemProps = $props();
</script>
<CommandPrimitive.Item
class={cn(
"aria-selected:bg-accent aria-selected:text-accent-foreground relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
className
)}
bind:ref
{...restProps}
/>

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CommandPrimitive.LinkItemProps = $props();
</script>
<CommandPrimitive.LinkItem
class={cn(
"aria-selected:bg-accent aria-selected:text-accent-foreground relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
className
)}
bind:ref
{...restProps}
/>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CommandPrimitive.ListProps = $props();
</script>
<CommandPrimitive.List
class={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
bind:ref
{...restProps}
/>

View File

@@ -0,0 +1,12 @@
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CommandPrimitive.SeparatorProps = $props();
</script>
<CommandPrimitive.Separator bind:ref class={cn("bg-border -mx-1 h-px", className)} {...restProps} />

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
</script>
<span
class={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)}
{...restProps}
bind:this={ref}
>
{@render children?.()}
</span>

View File

@@ -0,0 +1,21 @@
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
value = $bindable(""),
ref = $bindable(null),
class: className,
...restProps
}: CommandPrimitive.RootProps = $props();
</script>
<CommandPrimitive.Root
class={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className
)}
bind:ref
bind:value
{...restProps}
/>

View File

@@ -0,0 +1,40 @@
import { Command as CommandPrimitive } from "bits-ui";
import Root from "./command.svelte";
import Dialog from "./command-dialog.svelte";
import Empty from "./command-empty.svelte";
import Group from "./command-group.svelte";
import Item from "./command-item.svelte";
import Input from "./command-input.svelte";
import List from "./command-list.svelte";
import Separator from "./command-separator.svelte";
import Shortcut from "./command-shortcut.svelte";
import LinkItem from "./command-link-item.svelte";
const Loading: typeof CommandPrimitive.Loading = CommandPrimitive.Loading;
export {
Root,
Dialog,
Empty,
Group,
Item,
LinkItem,
Input,
List,
Separator,
Shortcut,
Loading,
//
Root as Command,
Dialog as CommandDialog,
Empty as CommandEmpty,
Group as CommandGroup,
Item as CommandItem,
LinkItem as CommandLinkItem,
Input as CommandInput,
List as CommandList,
Separator as CommandSeparator,
Shortcut as CommandShortcut,
Loading as CommandLoading,
};

View File

@@ -0,0 +1,38 @@
<script lang="ts">
import { Dialog as DialogPrimitive, type WithoutChildrenOrChild } from "bits-ui";
import X from "lucide-svelte/icons/x";
import type { Snippet } from "svelte";
import * as Dialog from "./index.js";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
portalProps,
children,
...restProps
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
portalProps?: DialogPrimitive.PortalProps;
children: Snippet;
} = $props();
</script>
<Dialog.Portal {...portalProps}>
<Dialog.Overlay />
<DialogPrimitive.Content
bind:ref
class={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg",
className
)}
{...restProps}
>
{@render children?.()}
<DialogPrimitive.Close
class="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none"
>
<X class="size-4" />
<span class="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</Dialog.Portal>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: DialogPrimitive.DescriptionProps = $props();
</script>
<DialogPrimitive.Description
bind:ref
class={cn("text-muted-foreground text-sm", className)}
{...restProps}
/>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
class={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: DialogPrimitive.OverlayProps = $props();
</script>
<DialogPrimitive.Overlay
bind:ref
class={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
className
)}
{...restProps}
/>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: DialogPrimitive.TitleProps = $props();
</script>
<DialogPrimitive.Title
bind:ref
class={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...restProps}
/>

View File

@@ -0,0 +1,37 @@
import { Dialog as DialogPrimitive } from "bits-ui";
import Title from "./dialog-title.svelte";
import Footer from "./dialog-footer.svelte";
import Header from "./dialog-header.svelte";
import Overlay from "./dialog-overlay.svelte";
import Content from "./dialog-content.svelte";
import Description from "./dialog-description.svelte";
const Root: typeof DialogPrimitive.Root = DialogPrimitive.Root;
const Trigger: typeof DialogPrimitive.Trigger = DialogPrimitive.Trigger;
const Close: typeof DialogPrimitive.Close = DialogPrimitive.Close;
const Portal: typeof DialogPrimitive.Portal = DialogPrimitive.Portal;
export {
Root,
Title,
Portal,
Footer,
Header,
Trigger,
Overlay,
Content,
Description,
Close,
//
Root as Dialog,
Title as DialogTitle,
Portal as DialogPortal,
Footer as DialogFooter,
Header as DialogHeader,
Trigger as DialogTrigger,
Overlay as DialogOverlay,
Content as DialogContent,
Description as DialogDescription,
Close as DialogClose,
};

View File

@@ -0,0 +1,7 @@
import Root from "./label.svelte";
export {
Root,
//
Root as Label,
};

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import { Label as LabelPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: LabelPrimitive.RootProps = $props();
</script>
<LabelPrimitive.Root
bind:ref
class={cn(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
className
)}
{...restProps}
/>

View File

@@ -0,0 +1,17 @@
import { Popover as PopoverPrimitive } from "bits-ui";
import Content from "./popover-content.svelte";
const Root = PopoverPrimitive.Root;
const Trigger = PopoverPrimitive.Trigger;
const Close = PopoverPrimitive.Close;
export {
Root,
Content,
Trigger,
Close,
//
Root as Popover,
Content as PopoverContent,
Trigger as PopoverTrigger,
Close as PopoverClose,
};

View File

@@ -0,0 +1,28 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import { Popover as PopoverPrimitive } from "bits-ui";
let {
ref = $bindable(null),
class: className,
align = "center",
sideOffset = 4,
portalProps,
...restProps
}: PopoverPrimitive.ContentProps & {
portalProps?: PopoverPrimitive.PortalProps;
} = $props();
</script>
<PopoverPrimitive.Portal {...portalProps}>
<PopoverPrimitive.Content
bind:ref
{align}
{sideOffset}
class={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 rounded-md border p-4 shadow-md outline-none",
className
)}
{...restProps}
/>
</PopoverPrimitive.Portal>

View File

@@ -0,0 +1,34 @@
import { Select as SelectPrimitive } from "bits-ui";
import GroupHeading from "./select-group-heading.svelte";
import Item from "./select-item.svelte";
import Content from "./select-content.svelte";
import Trigger from "./select-trigger.svelte";
import Separator from "./select-separator.svelte";
import ScrollDownButton from "./select-scroll-down-button.svelte";
import ScrollUpButton from "./select-scroll-up-button.svelte";
const Root = SelectPrimitive.Root;
const Group = SelectPrimitive.Group;
export {
Root,
Item,
Group,
GroupHeading,
Content,
Trigger,
Separator,
ScrollDownButton,
ScrollUpButton,
//
Root as Select,
Item as SelectItem,
Group as SelectGroup,
GroupHeading as SelectGroupHeading,
Content as SelectContent,
Trigger as SelectTrigger,
Separator as SelectSeparator,
ScrollDownButton as SelectScrollDownButton,
ScrollUpButton as SelectScrollUpButton,
};

View File

@@ -0,0 +1,38 @@
<script lang="ts">
import { Select as SelectPrimitive, type WithoutChild } from "bits-ui";
import * as Select from "./index.js";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
sideOffset = 4,
portalProps,
children,
...restProps
}: WithoutChild<SelectPrimitive.ContentProps> & {
portalProps?: SelectPrimitive.PortalProps;
} = $props();
</script>
<SelectPrimitive.Portal {...portalProps}>
<SelectPrimitive.Content
bind:ref
{sideOffset}
class={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 bg-popover text-popover-foreground relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border shadow-md data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
{...restProps}
>
<Select.ScrollUpButton />
<SelectPrimitive.Viewport
class={cn(
"h-[var(--bits-select-anchor-height)] w-full min-w-[var(--bits-select-anchor-width)] p-1"
)}
>
{@render children?.()}
</SelectPrimitive.Viewport>
<Select.ScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import { Select as SelectPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: SelectPrimitive.GroupHeadingProps = $props();
</script>
<SelectPrimitive.GroupHeading
bind:ref
class={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...restProps}
/>

View File

@@ -0,0 +1,37 @@
<script lang="ts">
import { Select as SelectPrimitive, type WithoutChild } from "bits-ui";
import Check from "lucide-svelte/icons/check";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
value,
label,
children: childrenProp,
...restProps
}: WithoutChild<SelectPrimitive.ItemProps> = $props();
</script>
<SelectPrimitive.Item
bind:ref
{value}
class={cn(
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...restProps}
>
{#snippet children({ selected, highlighted })}
<span class="absolute right-2 flex size-3.5 items-center justify-center">
{#if selected}
<Check class="size-4" />
{/if}
</span>
{#if childrenProp}
{@render childrenProp({ selected, highlighted })}
{:else}
{label || value}
{/if}
{/snippet}
</SelectPrimitive.Item>

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import ChevronDown from "lucide-svelte/icons/chevron-down";
import { Select as SelectPrimitive, type WithoutChildrenOrChild } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: WithoutChildrenOrChild<SelectPrimitive.ScrollDownButtonProps> = $props();
</script>
<SelectPrimitive.ScrollDownButton
bind:ref
class={cn("flex cursor-default items-center justify-center py-1", className)}
{...restProps}
>
<ChevronDown class="size-4" />
</SelectPrimitive.ScrollDownButton>

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import ChevronUp from "lucide-svelte/icons/chevron-up";
import { Select as SelectPrimitive, type WithoutChildrenOrChild } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: WithoutChildrenOrChild<SelectPrimitive.ScrollDownButtonProps> = $props();
</script>
<SelectPrimitive.ScrollUpButton
bind:ref
class={cn("flex cursor-default items-center justify-center py-1", className)}
{...restProps}
>
<ChevronUp class="size-4" />
</SelectPrimitive.ScrollUpButton>

View File

@@ -0,0 +1,13 @@
<script lang="ts">
import type { Separator as SeparatorPrimitive } from "bits-ui";
import { Separator } from "$lib/components/ui/separator/index.js";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: SeparatorPrimitive.RootProps = $props();
</script>
<Separator bind:ref class={cn("bg-muted -mx-1 my-1 h-px", className)} {...restProps} />

View File

@@ -0,0 +1,24 @@
<script lang="ts">
import { Select as SelectPrimitive, type WithoutChild } from "bits-ui";
import ChevronDown from "lucide-svelte/icons/chevron-down";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithoutChild<SelectPrimitive.TriggerProps> = $props();
</script>
<SelectPrimitive.Trigger
bind:ref
class={cn(
"border-input ring-offset-background data-[placeholder]:text-muted-foreground focus:ring-ring flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-1 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...restProps}
>
{@render children?.()}
<ChevronDown class="size-4 opacity-50" />
</SelectPrimitive.Trigger>

View File

@@ -0,0 +1,7 @@
import Root from "./separator.svelte";
export {
Root,
//
Root as Separator,
};

View File

@@ -0,0 +1,22 @@
<script lang="ts">
import { Separator as SeparatorPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
orientation = "horizontal",
...restProps
}: SeparatorPrimitive.RootProps = $props();
</script>
<SeparatorPrimitive.Root
bind:ref
class={cn(
"bg-border shrink-0",
orientation === "horizontal" ? "h-[1px] w-full" : "min-h-full w-[1px]",
className
)}
{orientation}
{...restProps}
/>

View File

@@ -0,0 +1 @@
export { default as Toaster } from "./sonner.svelte";

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import { Toaster as Sonner, type ToasterProps as SonnerProps } from "svelte-sonner";
import { mode } from "mode-watcher";
let restProps: SonnerProps = $props();
</script>
<Sonner
theme={$mode}
class="toaster group"
toastOptions={{
classes: {
toast: "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...restProps}
/>

View File

@@ -0,0 +1,18 @@
import { Tooltip as TooltipPrimitive } from "bits-ui";
import Content from "./tooltip-content.svelte";
const Root = TooltipPrimitive.Root;
const Trigger = TooltipPrimitive.Trigger;
const Provider = TooltipPrimitive.Provider;
export {
Root,
Trigger,
Content,
Provider,
//
Root as Tooltip,
Content as TooltipContent,
Trigger as TooltipTrigger,
Provider as TooltipProvider,
};

View File

@@ -0,0 +1,21 @@
<script lang="ts">
import { Tooltip as TooltipPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
sideOffset = 4,
...restProps
}: TooltipPrimitive.ContentProps = $props();
</script>
<TooltipPrimitive.Content
bind:ref
{sideOffset}
class={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 overflow-hidden rounded-md px-3 py-1.5 text-xs",
className
)}
{...restProps}
/>

View File

@@ -0,0 +1,26 @@
import { signalRState } from "./signalr/api.svelte";
class ExportState {
hasExportQueued = false;
async queueExport(
formatId: string,
outputDirectory: string,
options: Map<string, string>,
) {
await signalRState.api?.server.queueExport(
formatId,
outputDirectory,
options,
);
this.hasExportQueued = true;
}
async startExport() {
await signalRState.api?.server.startExport();
this.hasExportQueued = false;
}
}
export const exportState = $state<ExportState>(new ExportState());

View File

@@ -0,0 +1,32 @@
interface StringValue {
id: string;
label: string;
}
interface Setting {
type: "combobox" | "option" | "filepath";
name: StringValue;
description?: string;
condition?: {
id: string;
value: string | boolean | string[];
};
}
interface ComboboxSetting extends Setting {
type: "combobox";
default?: string;
values: StringValue[];
}
interface OptionSetting extends Setting {
type: "option";
default?: boolean;
}
interface FilepathSetting extends Setting {
type: "filepath";
directoryPath: boolean;
}
type SettingTypes = ComboboxSetting | OptionSetting | FilepathSetting;

View File

@@ -0,0 +1,55 @@
import * as signalR from "@microsoft/signalr";
import { SignalRClientApi } from "./client-api";
import { SignalRServerApi } from "./server-api";
import { getSignalRUrl } from "$lib/tauri";
class SignalRApi {
readonly connection: signalR.HubConnection;
readonly client: SignalRClientApi;
readonly server: SignalRServerApi;
constructor(connection: signalR.HubConnection) {
this.connection = connection;
this.client = new SignalRClientApi(connection);
this.server = new SignalRServerApi(connection);
}
}
class SignalRState {
api = $state<SignalRApi>();
get apiAvailable() {
return this.api !== undefined;
}
async start() {
const url = await getSignalRUrl();
const connection = new signalR.HubConnectionBuilder()
.withUrl(url)
.withAutomaticReconnect()
.build();
const api = new SignalRApi(connection);
try {
await connection.start();
} catch (ex) {
throw new Error(`Failed to start SignalR connection: ${ex}`);
}
this.api = api;
}
async stop() {
try {
await this.api?.connection?.stop();
} catch (ex) {
throw new Error(`Failed to stop SignalR connection: ${ex}`);
}
this.api = undefined;
}
}
export const signalRState = new SignalRState();

View File

@@ -0,0 +1,57 @@
import { goto } from "$app/navigation";
import * as signalR from "@microsoft/signalr";
import { toast } from "svelte-sonner";
export class SignalRClientApi {
private connection: signalR.HubConnection;
constructor(connection: signalR.HubConnection) {
this.connection = connection;
this.connection.on("ShowInfoToast", (message: string) => {
toast.info(message);
});
this.connection.on("ShowSuccessToast", (message: string) => {
toast.success(message);
});
this.connection.on("ShowErrorToast", (message: string) => {
toast.error(message);
});
// HACK: This is put here to be persistent, as the normal import screen gets killed once the loading screen begins
// todo: improve this
this.connection.on("OnImportCompleted", async () => {
await goto("/export");
});
}
onLogMessageReceived(
handler: (message: string) => Promise<void>,
): () => void {
return this.registerHandler("ShowLogMessage", handler);
}
onLoadingStarted(handler: () => Promise<void>): () => void {
return this.registerHandler("BeginLoading", handler);
}
onLoadingFinished(handler: () => Promise<void>): () => void {
return this.registerHandler("FinishLoading", handler);
}
onImportCompleted(handler: () => Promise<void>): () => void {
return this.registerHandler("OnImportCompleted", handler);
}
private registerHandler(
name: string,
handler: (...args: any[]) => Promise<void>,
): () => void {
this.connection.on(name, handler);
return () => {
this.connection.off(name, handler);
};
}
}

View File

@@ -0,0 +1,48 @@
import * as signalR from "@microsoft/signalr";
export class SignalRServerApi {
private connection: signalR.HubConnection;
constructor(connection: signalR.HubConnection) {
this.connection = connection;
}
async sendUiLaunched() {
return await this.connection.send("OnUiLaunched");
}
async submitInputFiles(inputFiles: string[]) {
return await this.connection.send("SubmitInputFiles", inputFiles);
}
async queueExport(
exportId: string,
targetDirectory: string,
settings: Map<string, string>,
) {
return await this.connection.send(
"QueueExport",
exportId,
targetDirectory,
Object.fromEntries(settings),
);
}
async startExport() {
return await this.connection.send("StartExport");
}
async getPotentialUnityVersions(): Promise<string[]> {
return await this.connection.invoke<string[]>(
"GetPotentialUnityVersions",
);
}
async exportIl2CppFiles(targetDirectory: string) {
return await this.connection.send("ExportIl2CppFiles", targetDirectory);
}
async getInspectorVersion(): Promise<string> {
return await this.connection.invoke<string>("GetInspectorVersion");
}
}

View File

@@ -0,0 +1,16 @@
import { invoke } from "@tauri-apps/api/core";
import { open, save } from "@tauri-apps/plugin-dialog";
export async function getSignalRUrl() {
const port = await invoke<string>("get_signalr_url");
if (port === "") {
throw new Error("No SignalR port specified.");
}
if (port.match(/[0-9]*/) === null) {
throw new Error("Invalid SignalR port specified.");
}
return `http://localhost:${port}/il2cpp`;
}

View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

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,133 @@
<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 { 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,43 @@
<script lang="ts">
import Button from "$lib/components/ui/button/button.svelte";
import { signalRState } from "$lib/signalr/api.svelte";
import { open } from "@tauri-apps/plugin-dialog";
async function exportIl2CppFiles(e: Event) {
e.preventDefault();
const exportDirectory = await open({
title: "Select the output folder",
directory: true,
multiple: false,
recursive: false,
});
if (exportDirectory === null) return;
await signalRState.api?.server.exportIl2CppFiles(exportDirectory);
}
</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
</h1>
<div class="mx-5 mt-10 grid h-full grid-cols-2 gap-4 sm:gap-6">
<Button
class="sm:p-10"
variant="outline"
onclick={exportIl2CppFiles}
>
Export IL2CPP files
</Button>
</div>
</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,57 @@
<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 { exportState } from "$lib/export.svelte";
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>
{#if exportState.hasExportQueued}
<Button onclick={() => exportState.startExport()} variant="default"
>Start export</Button
>
{/if}
<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,196 @@
<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 { toast } from "svelte-sonner";
import { onMount } from "svelte";
import { exportState } from "$lib/export.svelte";
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 exportState.queueExport(formatId, exportDirectory, settings);
if (shouldStartExport) {
await exportState.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;
});
onMount(() => {
// we dismiss all toasts so that they don't block the export/queue buttons
toast.dismiss();
});
</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: "flattenhierarchy",
label: "Don't nest folders (flatten hierarchy)",
},
default: false,
condition: {
id: "layout",
value: ["namespace", "class", "tree"],
},
},
{
type: "combobox",
name: {
id: "sortingmode",
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: "mustcompile",
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: "unityassembliespath",
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",
},
];
};

Some files were not shown because too many files have changed in this diff Show More