Skip to content

Commit

Permalink
feat: save and restore workspaces, add menu bar (#79)
Browse files Browse the repository at this point in the history
  • Loading branch information
j4k0xb authored Jun 4, 2024
1 parent e86c35d commit 4863fda
Show file tree
Hide file tree
Showing 12 changed files with 3,687 additions and 2,849 deletions.
1 change: 1 addition & 0 deletions apps/playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"dependencies": {
"@babel/generator": "^7.24.4",
"@babel/types": "^7.24.0",
"idb": "^8.0.0",
"monaco-editor": "^0.47.0",
"sandybox": "^1.1.2",
"solid-js": "^1.8.16",
Expand Down
71 changes: 67 additions & 4 deletions apps/playground/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
import * as monaco from 'monaco-editor';
import { For, Show, createMemo, createSignal, onCleanup } from 'solid-js';
import {
For,
Show,
batch,
createMemo,
createSignal,
onCleanup,
} from 'solid-js';
import { createStore } from 'solid-js/store';
import Breadcrumbs from './components/Breadcrumbs';
import Menu from './components/Menu';
import MonacoEditor from './components/MonacoEditor';
import ProgressBar from './components/ProgressBar';
import Sidebar from './components/Sidebar';
import Tab from './components/Tab';
import { DeobfuscateContextProvider } from './context/DeobfuscateContext';
import { settings } from './hooks/useSettings';
import { useWorkspaces, type Workspace } from './indexeddb';
import { debounce } from './utils/debounce';
import { downloadFile } from './utils/download';
import type { DeobfuscateResult } from './webcrack.worker';

export const [settings, setSettings] = createStore({
export const [config, setConfig] = createStore({
deobfuscate: true,
unminify: true,
unpack: true,
Expand All @@ -17,6 +30,7 @@ export const [settings, setSettings] = createStore({
});

function App() {
const { saveModels, setWorkspaceId } = useWorkspaces();
const [untitledCounter, setUntitledCounter] = createSignal(1);
const [models, setModels] = createSignal<monaco.editor.ITextModel[]>([
monaco.editor.createModel(
Expand All @@ -39,6 +53,41 @@ function App() {
const filePaths = createMemo(() =>
fileModels().map((model) => model.uri.path),
);
const hasNonEmptyModels = () => models().some((m) => m.getValueLength() > 0);

window.onbeforeunload = () => {
if (settings.confirmOnLeave && hasNonEmptyModels()) {
saveModels(models()).catch(console.error);
return true;
}
return undefined;
};

const saveModelsDebounced = debounce(() => {
settings.workspaceHistory && saveModels(models()).catch(console.error);
}, 1000);

async function restoreWorkspace(workspace: Workspace) {
await saveModels(models());
setWorkspaceId(workspace.id);

batch(() => {
models().forEach((model) => model.dispose());

setModels(
workspace.models.map((model) =>
monaco.editor.createModel(
model.value,
model.language,
monaco.Uri.parse(model.uri),
),
),
);

setTabs(untitledModels());
setActiveTab(untitledModels()[0]);
});
}

onCleanup(() => {
models().forEach((model) => model.dispose());
Expand Down Expand Up @@ -138,11 +187,24 @@ function App() {
return (
<DeobfuscateContextProvider
code={activeTab()?.getValue()}
options={{ ...settings }}
options={{ ...config }}
onResult={onDeobfuscateResult}
onError={onDeobfuscateError}
>
<div class="h-screen flex">
<ProgressBar />
<Menu
onFileOpen={(content) => {
openUntitledTab().setValue(content);
}}
onSave={() => {
if (activeTab()) downloadFile(activeTab()!);
}}
onRestore={(workspace) => {
restoreWorkspace(workspace).catch(console.error);
}}
/>

<div class="flex" style="height: calc(100vh - 44px)">
<Sidebar paths={filePaths()} onFileClick={openFile} />

<main class="flex-1 overflow-auto">
Expand Down Expand Up @@ -182,6 +244,7 @@ function App() {
models={models()}
currentModel={activeTab()}
onModelChange={openTab}
onValueChange={saveModelsDebounced}
/>
</main>
</div>
Expand Down
130 changes: 130 additions & 0 deletions apps/playground/src/components/Menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { For } from 'solid-js';
import { setSettings, settings, type Settings } from '../hooks/useSettings';
import { useWorkspaces, type Workspace } from '../indexeddb';

interface Props {
onFileOpen?: (content: string) => void;
onSave?: () => void;
onRestore?: (workspace: Workspace) => void;
}

export default function Menu(props: Props) {
const { workspaces } = useWorkspaces();

function openFile() {
const input = document.createElement('input');
input.type = 'file';
input.onchange = async (event) => {
const file = (event.target as HTMLInputElement).files?.[0];
if (!file) return;
const content = await file.text();
props.onFileOpen?.(content);
};
input.click();
}

return (
<ul class="menu menu-sm menu-horizontal bg-base-200 w-full">
<li>
<details>
<summary>File</summary>
<ul class="min-w-52 z-10 !px-0">
<li>
<a onClick={openFile}>Open File…</a>
</li>
<li>
<div class="dropdown dropdown-right dropdown-hover transform-none">
<div tabindex="0" role="button">
Open Recent
</div>
<ul
tabindex="0"
class="dropdown-content z-10 menu ml-0 p-2 shadow bg-base-100 rounded-box"
>
<For each={workspaces()} fallback={<li>No recent files</li>}>
{(workspace) => (
<li>
<a
onClick={() => props.onRestore?.(workspace)}
class="truncate"
>
{new Date(workspace.timestamp).toLocaleString()} -
<code>{workspace.models[0].value.slice(0, 20)}</code>
</a>
</li>
)}
</For>
</ul>
</div>
</li>
<li>
<a onClick={props.onSave}>Save</a>
</li>
</ul>
</details>
</li>
<li>
<details>
<summary>Settings</summary>
<ul class="min-w-52 z-10">
<li>
<label class="h-10 flex items-center">
Theme
<select
class="select select-sm ml-auto"
value={settings.theme}
onChange={(e) =>
setSettings(
'theme',
e.currentTarget.value as Settings['theme'],
)
}
>
<option value="dark">Dark</option>
<option value="light">Light</option>
<option value="system">System</option>
</select>
</label>
</li>
<li>
<label class="h-10 flex items-center">
Confirm on Leave
<input
type="checkbox"
class="checkbox checkbox-sm ml-auto"
checked={settings.confirmOnLeave}
onChange={(e) =>
setSettings('confirmOnLeave', e.currentTarget.checked)
}
/>
</label>
</li>
<li>
<label class="h-10 flex items-center">
Workspace History
<input
type="checkbox"
class="checkbox checkbox-sm ml-auto"
checked={settings.workspaceHistory}
onChange={(e) =>
setSettings('workspaceHistory', e.currentTarget.checked)
}
/>
</label>
</li>
</ul>
</details>
</li>
<li>
<a href="https://github.com/j4k0xb/webcrack" target="_blank">
GitHub
</a>
</li>
<li>
<a href="/docs" target="_blank">
Documentation
</a>
</li>
</ul>
);
}
46 changes: 17 additions & 29 deletions apps/playground/src/components/MonacoEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import * as monaco from 'monaco-editor';
import { createEffect, onCleanup, onMount } from 'solid-js';
import { useDeobfuscateContext } from '../context/DeobfuscateContext';
import { useTheme } from '../hooks/useTheme';
import { theme } from '../hooks/useTheme';
import { registerEvalSelection } from '../monaco/eval-selection';
import { PlaceholderContentWidget } from '../monaco/placeholder-widget';
import { downloadFile } from '../utils/download';

interface Props {
models: monaco.editor.ITextModel[];
currentModel?: monaco.editor.ITextModel;
onModelChange?: (model: monaco.editor.ITextModel) => void;
onValueChange?: (value: string) => void;
onSave?: (value: string) => void;
}

monaco.editor.defineTheme('dark', {
Expand All @@ -20,7 +23,6 @@ monaco.editor.defineTheme('dark', {

export default function MonacoEditor(props: Props) {
const { deobfuscate } = useDeobfuscateContext();
const [theme] = useTheme();
const viewStates = new WeakMap<
monaco.editor.ITextModel,
monaco.editor.ICodeEditorViewState
Expand Down Expand Up @@ -57,6 +59,11 @@ export default function MonacoEditor(props: Props) {
editor.focus();
}

editor.onDidChangeModelContent(() => {
const model = editor.getModel();
if (model) props.onValueChange?.(model.getValue());
});

// Go to definition
const editorOpener = monaco.editor.registerEditorOpener({
openCodeEditor(_source, resource, selectionOrPosition) {
Expand Down Expand Up @@ -100,29 +107,13 @@ export default function MonacoEditor(props: Props) {

const saveAction = editor.addAction({
id: 'editor.action.save',
label: 'Save',
label: 'File: Save',
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS],
run(editor) {
const code = editor.getValue();
if (code === '') return;

const blob = new Blob([code], {
type: 'application/javascript;charset=utf-8',
});
const url = URL.createObjectURL(blob);
const link = document.createElement('a');

link.setAttribute('href', url);
link.setAttribute(
'download',
`deobfuscated-${new Date().toISOString()}.js`,
);
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);

URL.revokeObjectURL(url);
run() {
const model = editor.getModel();
if (model) {
downloadFile(model);
}
},
});

Expand All @@ -138,16 +129,13 @@ export default function MonacoEditor(props: Props) {
editorOpener.dispose();
placeholder.dispose();
deobfuscateAction.dispose();
saveAction.dispose();
evalAction.dispose();
saveAction.dispose();
});
});

return (
<div
ref={container}
class="editor"
style="height: calc(100vh - 64px)"
></div>
<div ref={container} class="editor" style="height: calc(100vh - 108px)" />
);
}
29 changes: 29 additions & 0 deletions apps/playground/src/components/ProgressBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Show, createEffect, createSignal } from 'solid-js';
import { useDeobfuscateContext } from '../context/DeobfuscateContext';

export default function ProgressBar() {
const { deobfuscating, progress } = useDeobfuscateContext();
const [progressShown, setProgressShown] = createSignal(false);

createEffect(() => {
if (deobfuscating()) setProgressShown(true);
else if (progress() === 100) setTimeout(() => setProgressShown(false), 500);
else setProgressShown(false);
});

return (
<Show when={progressShown()}>
<style>
{`
.progress::-webkit-progress-value {
transition: width 300ms ease;
}
`}
</style>
<progress
class="progress progress-info absolute top-0 h-0.5 w-full z-10 pointer-events-none bg-transparent"
value={progress() / 100}
/>
</Show>
);
}
Loading

0 comments on commit 4863fda

Please sign in to comment.