Compare commits
No commits in common. "ae9e7e17ed0a0e3fea951cb1d789f742cce94ea6" and "7d1d8cf8c06115130bb0d8eb27be602b600fad91" have entirely different histories.
ae9e7e17ed
...
7d1d8cf8c0
10 changed files with 44 additions and 183 deletions
|
@ -1,6 +1,5 @@
|
|||
import type { RunnerController } from './components/runner/Runner.tsx'
|
||||
|
||||
import { ColorModeProvider, ColorModeScript } from '@kobalte/core'
|
||||
|
||||
import { workerInit } from 'mtcute-repl-worker/client'
|
||||
import { createSignal, lazy, onCleanup, onMount, Show } from 'solid-js'
|
||||
import { EditorTabs } from './components/editor/EditorTabs.tsx'
|
||||
|
@ -17,7 +16,6 @@ export function App() {
|
|||
const [updating, setUpdating] = createSignal(true)
|
||||
const [showSettings, setShowSettings] = createSignal(false)
|
||||
const [settingsTab, setSettingsTab] = createSignal<SettingsTab>('accounts')
|
||||
const [runnerController, setRunnerController] = createSignal<RunnerController>()
|
||||
|
||||
const [isResizing, setIsResizing] = createSignal(false)
|
||||
const [sizes, setSizes] = createSignal([0.5, 0.5])
|
||||
|
@ -78,10 +76,7 @@ export function App() {
|
|||
<Resizable sizes={sizes()} onSizesChange={e => setSizes(e)} orientation="horizontal" class="size-full max-h-[calc(100vh-57px)]">
|
||||
<ResizablePanel class="h-full overflow-x-auto overflow-y-hidden" minSize={0.2}>
|
||||
<EditorTabs />
|
||||
<Editor
|
||||
class="size-full"
|
||||
onRun={() => runnerController()?.run()}
|
||||
/>
|
||||
<Editor class="size-full" />
|
||||
</ResizablePanel>
|
||||
<ResizableHandle
|
||||
withHandle
|
||||
|
@ -95,10 +90,7 @@ export function App() {
|
|||
class="flex max-h-full flex-col overflow-hidden"
|
||||
minSize={0.2}
|
||||
>
|
||||
<Runner
|
||||
isResizing={isResizing()}
|
||||
controllerRef={setRunnerController}
|
||||
/>
|
||||
<Runner isResizing={isResizing()} />
|
||||
</ResizablePanel>
|
||||
</Resizable>
|
||||
</Show>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useColorModeValue } from '@kobalte/core'
|
||||
import { KeyCode, KeyMod, editor as mEditor, Uri } from 'monaco-editor'
|
||||
import { editor as mEditor, Uri } from 'monaco-editor'
|
||||
|
||||
import { createEffect, on, onMount } from 'solid-js'
|
||||
import { $activeTab, $tabs, type EditorTab } from '../../store/tabs.ts'
|
||||
|
@ -9,7 +9,6 @@ import './Editor.css'
|
|||
|
||||
export interface EditorProps {
|
||||
class?: string
|
||||
onRun: () => void
|
||||
}
|
||||
|
||||
const DEFAULT_CODE = `
|
||||
|
@ -23,8 +22,6 @@ export const self = await tg.getMe()
|
|||
console.log(self)
|
||||
`.trimStart()
|
||||
|
||||
const LOCAL_STORAGE_PREFIX = 'repl:tab-content:'
|
||||
|
||||
function findChangedTab(a: EditorTab[], b: EditorTab[]) {
|
||||
const set = new Set(a.map(tab => tab.id))
|
||||
for (const tab of b) {
|
||||
|
@ -42,6 +39,7 @@ export default function Editor(props: EditorProps) {
|
|||
let editor: mEditor.IStandaloneCodeEditor | undefined
|
||||
|
||||
const monacoTheme = useColorModeValue('latte', 'mocha')
|
||||
// const monacoTheme = () => scheme() === 'dark' ? 'ayu-dark' : 'ayu-light'
|
||||
const modelsByTab = new Map<string, mEditor.ITextModel>()
|
||||
|
||||
onMount(async () => {
|
||||
|
@ -77,24 +75,15 @@ export default function Editor(props: EditorProps) {
|
|||
await setupMonaco()
|
||||
|
||||
for (const tab of tabs()) {
|
||||
const storedCode = localStorage.getItem(LOCAL_STORAGE_PREFIX + tab.id)
|
||||
const model = mEditor.createModel(storedCode ?? (tab.main ? DEFAULT_CODE : ''), 'typescript', Uri.parse(`file:///${tab.id}.ts`))
|
||||
const model = mEditor.createModel(tab.main ? DEFAULT_CODE : '', 'typescript', Uri.parse(`file:///${tab.id}.ts`))
|
||||
modelsByTab.set(tab.id, model)
|
||||
}
|
||||
|
||||
editor.setModel(modelsByTab.get(activeTab())!)
|
||||
|
||||
editor.addCommand(KeyMod.CtrlCmd | KeyCode.Enter, () => {
|
||||
props.onRun()
|
||||
})
|
||||
|
||||
editor.onDidChangeModelContent(() => {
|
||||
const currentTab = tabs().find(tab => tab.id === activeTab())!
|
||||
const content = editor?.getModel()?.getValue()
|
||||
if (!currentTab || !content) return
|
||||
|
||||
localStorage.setItem(LOCAL_STORAGE_PREFIX + currentTab.id, content)
|
||||
})
|
||||
// editor.onDidChangeModelContent(() => {
|
||||
// props.onCodeChange(editor?.getValue() ?? '')
|
||||
// })
|
||||
|
||||
return () => editor?.dispose()
|
||||
})
|
||||
|
@ -147,7 +136,6 @@ export default function Editor(props: EditorProps) {
|
|||
if (!changed) return
|
||||
modelsByTab.get(changed.id)?.dispose()
|
||||
modelsByTab.delete(changed.id)
|
||||
localStorage.removeItem(LOCAL_STORAGE_PREFIX + changed.id)
|
||||
}
|
||||
}))
|
||||
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
import type { DropdownMenuTriggerProps } from '@kobalte/core/dropdown-menu'
|
||||
import type { mtcute } from 'mtcute-repl-worker/client'
|
||||
import type { Setter } from 'solid-js'
|
||||
import type { CustomTypeScriptWorker } from '../editor/utils/custom-worker.ts'
|
||||
import { timers } from '@fuman/utils'
|
||||
import { persistentAtom } from '@nanostores/persistent'
|
||||
import { LucideCheck, LucidePlay, LucidePlug, LucideRefreshCw, LucideSettings2, LucideSkull, LucideUnplug } from 'lucide-solid'
|
||||
import { LucideCheck, LucidePlay, LucidePlug, LucideRefreshCw, LucideSkull, LucideUnplug } from 'lucide-solid'
|
||||
import { languages, Uri } from 'monaco-editor/esm/vs/editor/editor.api.js'
|
||||
import { createEffect, createSignal, on, onCleanup, onMount } from 'solid-js'
|
||||
import { Dynamic } from 'solid-js/web'
|
||||
import { toast } from 'solid-sonner'
|
||||
import { Button } from '../../lib/components/ui/button.tsx'
|
||||
import {
|
||||
DropdownMenu,
|
||||
|
@ -39,14 +37,7 @@ const $enableVerbose = persistentAtom('repl:verboseLogs', false, {
|
|||
decode: value => value === 'true',
|
||||
})
|
||||
|
||||
export interface RunnerController {
|
||||
run: () => void
|
||||
}
|
||||
|
||||
export function Runner(props: {
|
||||
isResizing: boolean
|
||||
controllerRef: Setter<RunnerController | undefined>
|
||||
}) {
|
||||
export function Runner(props: { isResizing: boolean }) {
|
||||
const [devtoolsIframe, setDevtoolsIframe] = createSignal<HTMLIFrameElement | undefined>()
|
||||
const [runnerIframe, setRunnerIframe] = createSignal<HTMLIFrameElement>()
|
||||
const [runnerLoaded, setRunnerLoaded] = createSignal(false)
|
||||
|
@ -163,10 +154,6 @@ export function Runner(props: {
|
|||
|
||||
onMount(async () => {
|
||||
window.addEventListener('message', handleMessage)
|
||||
|
||||
props.controllerRef({
|
||||
run: () => handleRun(),
|
||||
})
|
||||
})
|
||||
onCleanup(() => {
|
||||
window.removeEventListener('message', handleMessage)
|
||||
|
@ -181,11 +168,6 @@ export function Runner(props: {
|
|||
}, { defer: true }))
|
||||
|
||||
async function handleRun() {
|
||||
if ($activeAccountId.get() === undefined) {
|
||||
toast('You need to log in to run a script')
|
||||
return
|
||||
}
|
||||
|
||||
const getWorker = await languages.typescript.getTypeScriptWorker()
|
||||
const worker = await getWorker(Uri.parse('file:///main.ts')) as unknown as CustomTypeScriptWorker
|
||||
|
||||
|
@ -268,34 +250,30 @@ export function Runner(props: {
|
|||
</Button>
|
||||
)}
|
||||
<div class="flex-1" />
|
||||
<div class="mr-2 flex items-center text-xs font-medium">
|
||||
{{
|
||||
offline: 'Disconnected',
|
||||
connecting: 'Connecting...',
|
||||
updating: 'Updating...',
|
||||
connected: 'Ready',
|
||||
}[connectionState()]}
|
||||
<div class={cn(
|
||||
'ml-2 size-2 rounded-full',
|
||||
{
|
||||
connected: 'bg-green-500',
|
||||
updating: 'bg-yellow-500',
|
||||
connecting: 'bg-yellow-500',
|
||||
offline: 'bg-neutral-400',
|
||||
}[connectionState()],
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
as={(props: DropdownMenuTriggerProps) => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="size-7"
|
||||
size="xs"
|
||||
{...props}
|
||||
>
|
||||
<LucideSettings2 class="size-4" />
|
||||
{{
|
||||
offline: 'Disconnected',
|
||||
connecting: 'Connecting...',
|
||||
updating: 'Updating...',
|
||||
connected: 'Ready',
|
||||
}[connectionState()]}
|
||||
<div class={cn(
|
||||
'ml-2 size-2 rounded-full',
|
||||
{
|
||||
connected: 'bg-green-500',
|
||||
updating: 'bg-yellow-500',
|
||||
connecting: 'bg-yellow-500',
|
||||
offline: 'bg-neutral-400',
|
||||
}[connectionState()],
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
|
|
|
@ -13,7 +13,6 @@ import {
|
|||
LucideRefreshCw,
|
||||
LucideSearch,
|
||||
LucideTrash,
|
||||
LucideTriangleAlert,
|
||||
LucideUser,
|
||||
LucideX,
|
||||
} from 'lucide-solid'
|
||||
|
@ -23,7 +22,6 @@ import { nanoid } from 'nanoid'
|
|||
import { createEffect, createMemo, createSignal, For, on, onCleanup, Show } from 'solid-js'
|
||||
import { toast } from 'solid-sonner'
|
||||
import { copyToClipboard } from '../../lib/clipboard.tsx'
|
||||
import { Alert, AlertDescription, AlertTitle } from '../../lib/components/ui/alert.tsx'
|
||||
import { Badge } from '../../lib/components/ui/badge.tsx'
|
||||
import { Button } from '../../lib/components/ui/button.tsx'
|
||||
import { Dialog, DialogContent } from '../../lib/components/ui/dialog.tsx'
|
||||
|
@ -300,7 +298,7 @@ export function AccountsTab() {
|
|||
return accounts()
|
||||
}
|
||||
|
||||
return accounts()?.filter((account) => {
|
||||
return accounts().filter((account) => {
|
||||
return account.name.toLowerCase().includes(query) || account.telegramId.toString().includes(query)
|
||||
})
|
||||
})
|
||||
|
@ -308,25 +306,9 @@ export function AccountsTab() {
|
|||
return (
|
||||
<>
|
||||
<Show
|
||||
when={accounts()?.length !== 0}
|
||||
when={accounts().length !== 0}
|
||||
fallback={(
|
||||
<div class="flex h-full flex-col items-center justify-center gap-4 px-2 text-muted-foreground">
|
||||
<Alert variant="destructive" class="max-w-md">
|
||||
<LucideTriangleAlert class="size-4" />
|
||||
<AlertTitle class="font-bold">Warning</AlertTitle>
|
||||
<AlertDescription>
|
||||
This is an
|
||||
{' '}
|
||||
<b>unofficial</b>
|
||||
{' '}
|
||||
Telegram application.
|
||||
<br />
|
||||
You might trigger anti-spam measures and get banned.
|
||||
<br />
|
||||
Proceed at your own risk.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div class="flex h-full flex-col items-center justify-center gap-4 text-muted-foreground">
|
||||
No accounts yet
|
||||
<div class="flex flex-row gap-2">
|
||||
<Button
|
||||
|
|
|
@ -25,7 +25,7 @@ export function TdataImportDialog(props: {
|
|||
const [error, setError] = createSignal<string | undefined>('')
|
||||
const [loading, setLoading] = createSignal(false)
|
||||
|
||||
const accountExists = (id: number) => $accounts.get()?.some(it => it.telegramId === id)
|
||||
const accountExists = (id: number) => $accounts.get().some(it => it.telegramId === id)
|
||||
|
||||
let abortController: AbortController | undefined
|
||||
const handleSubmit = async () => {
|
||||
|
|
|
@ -81,7 +81,7 @@ function QrLoginStep(props: StepProps<'qr'>) {
|
|||
/>
|
||||
) : <Spinner indeterminate class="size-10" />}
|
||||
</div>
|
||||
<ol class="mt-4 list-inside list-decimal text-sm text-muted-foreground">
|
||||
<ol class="text-muted-foreground mt-4 list-inside list-decimal text-sm">
|
||||
<li>Open Telegram on your phone</li>
|
||||
<li>
|
||||
Go to
|
||||
|
@ -147,7 +147,7 @@ function PhoneNumberStep(props: StepProps<'phone'>) {
|
|||
<h2 class="mt-4 text-xl font-bold">
|
||||
Log in with phone number
|
||||
</h2>
|
||||
<div class="mt-2 text-center text-sm text-muted-foreground">
|
||||
<div class="text-muted-foreground mt-2 text-center text-sm">
|
||||
Please confirm your country code
|
||||
<br />
|
||||
and enter your phone number
|
||||
|
@ -177,7 +177,7 @@ function PhoneNumberStep(props: StepProps<'phone'>) {
|
|||
<TextFieldErrorMessage>{error()}</TextFieldErrorMessage>
|
||||
</TextFieldRoot>
|
||||
<div class="flex-1" />
|
||||
<div class="text-center text-sm text-muted-foreground">
|
||||
<div class="text-muted-foreground text-center text-sm">
|
||||
or,
|
||||
{' '}
|
||||
<a
|
||||
|
@ -309,7 +309,7 @@ function OtpStep(props: StepProps<'otp'>) {
|
|||
Wrong number?
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-center text-sm text-muted-foreground">
|
||||
<div class="text-muted-foreground mt-4 text-center text-sm">
|
||||
{description()}
|
||||
</div>
|
||||
<div class="mt-4 flex flex-col items-center text-center">
|
||||
|
@ -363,7 +363,7 @@ function OtpStep(props: StepProps<'otp'>) {
|
|||
</OTPFieldGroup>
|
||||
</OTPField>
|
||||
{error() && (
|
||||
<div class="mt-1 text-sm text-error-foreground">{error()}</div>
|
||||
<div class="text-error-foreground mt-1 text-sm">{error()}</div>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
|
@ -438,7 +438,7 @@ function PasswordStep(props: StepProps<'password'>) {
|
|||
<h2 class="text-xl font-bold">
|
||||
2FA password
|
||||
</h2>
|
||||
<div class="mt-4 text-center text-sm text-muted-foreground">
|
||||
<div class="text-muted-foreground mt-4 text-center text-sm">
|
||||
Your account is protected with an additional password.
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
|
@ -483,9 +483,9 @@ function DoneStep(props: StepProps<'done'>) {
|
|||
<div class="flex flex-col items-center justify-center">
|
||||
<AccountAvatar
|
||||
account={props.ctx.account}
|
||||
class="mb-4 size-24 animate-scale-up shadow-sm fill-mode-forwards"
|
||||
class="animate-scale-up fill-mode-forwards mb-4 size-24 shadow-sm"
|
||||
/>
|
||||
<div class="animate-fade-out-down text-center font-medium fill-mode-forwards">
|
||||
<div class="animate-fade-out-down fill-mode-forwards text-center font-medium">
|
||||
Welcome,
|
||||
{' '}
|
||||
{props.ctx.account.name}
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
import type { AlertRootProps } from '@kobalte/core/alert'
|
||||
import type { PolymorphicProps } from '@kobalte/core/polymorphic'
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
import type { ComponentProps, ValidComponent } from 'solid-js'
|
||||
import { Alert as AlertPrimitive } from '@kobalte/core/alert'
|
||||
import { cva } from 'class-variance-authority'
|
||||
import { splitProps } from 'solid-js'
|
||||
import { cn } from '../../utils.ts'
|
||||
|
||||
export const alertVariants = cva(
|
||||
'relative w-full rounded-lg border px-4 py-3 text-sm [&:has(svg)]:pl-11 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-background text-foreground',
|
||||
destructive:
|
||||
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
type alertProps<T extends ValidComponent = 'div'> = AlertRootProps<T> &
|
||||
VariantProps<typeof alertVariants> & {
|
||||
class?: string
|
||||
}
|
||||
|
||||
export function Alert<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, alertProps<T>>) {
|
||||
const [local, rest] = splitProps(props as alertProps, ['class', 'variant'])
|
||||
|
||||
return (
|
||||
<AlertPrimitive
|
||||
class={cn(
|
||||
alertVariants({
|
||||
variant: props.variant,
|
||||
}),
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function AlertTitle(props: ComponentProps<'div'>) {
|
||||
const [local, rest] = splitProps(props, ['class'])
|
||||
|
||||
return (
|
||||
<div
|
||||
class={cn('font-medium leading-5 tracking-tight', local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function AlertDescription(props: ComponentProps<'div'>) {
|
||||
const [local, rest] = splitProps(props, ['class'])
|
||||
|
||||
return (
|
||||
<div class={cn('text-sm [&_p]:leading-relaxed', local.class)} {...rest} />
|
||||
)
|
||||
}
|
|
@ -2,13 +2,13 @@ import type { TelegramAccount } from 'mtcute-repl-worker/client'
|
|||
import { computed } from 'nanostores'
|
||||
import { linkedAtom } from './link.ts'
|
||||
|
||||
export const $accounts = linkedAtom<TelegramAccount[] | undefined>('accounts')
|
||||
export const $accounts = linkedAtom<TelegramAccount[]>('accounts')
|
||||
export const $activeAccountId = linkedAtom<string | undefined>('activeAccountId')
|
||||
|
||||
export const $activeAccount = computed([$accounts, $activeAccountId], (accounts, activeAccountId) => {
|
||||
if (!activeAccountId) return null
|
||||
|
||||
const account = accounts?.find(account => account.id === activeAccountId)
|
||||
const account = accounts.find(account => account.id === activeAccountId)
|
||||
if (!account) return null
|
||||
|
||||
return account
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { persistentAtom } from '@nanostores/persistent'
|
||||
import { atom } from 'nanostores'
|
||||
|
||||
export interface EditorTab {
|
||||
|
@ -7,15 +6,12 @@ export interface EditorTab {
|
|||
main: boolean
|
||||
}
|
||||
|
||||
export const $tabs = persistentAtom<EditorTab[]>('repl:tabs', [
|
||||
export const $tabs = atom<EditorTab[]>([
|
||||
{
|
||||
id: 'main',
|
||||
fileName: 'main.ts',
|
||||
main: true,
|
||||
},
|
||||
], {
|
||||
encode: JSON.stringify,
|
||||
decode: JSON.parse,
|
||||
})
|
||||
])
|
||||
|
||||
export const $activeTab = atom('main')
|
||||
|
|
|
@ -141,16 +141,6 @@ window.addEventListener('message', async ({ data }) => {
|
|||
currentScriptId = nanoid()
|
||||
await swInvokeMethodInner({ event: 'UPLOAD_SCRIPT', name: currentScriptId, files: data.files }, asNonNull(navigator.serviceWorker.controller))
|
||||
|
||||
if (!window.tg) {
|
||||
// shouldnt happen but just in case
|
||||
console.warn('[mtcute-repl] Telegram client not initialized yet')
|
||||
return
|
||||
}
|
||||
|
||||
if (lastConnectionState === 'offline') {
|
||||
await window.tg.connect()
|
||||
}
|
||||
|
||||
const el = document.createElement('script')
|
||||
el.type = 'module'
|
||||
let script = `import * as result from "/sw/runtime/script/${currentScriptId}/main.js";`
|
||||
|
@ -187,7 +177,6 @@ window.addEventListener('message', async ({ data }) => {
|
|||
initClient(lastAccountId, data.verboseLogs)
|
||||
}
|
||||
window.parent.postMessage({ event: 'CONNECTION_STATE', value: 'offline' }, HOST_ORIGIN)
|
||||
lastConnectionState = 'offline'
|
||||
} else if (data.event === 'RECONNECT') {
|
||||
if (window.tg !== undefined) {
|
||||
window.tg.connect()
|
||||
|
|
Loading…
Reference in a new issue