edit code
This commit is contained in:
parent
f3040abc5d
commit
f14a731cae
|
|
@ -88,6 +88,9 @@ const CodeHeader = define('CodeHeader', {
|
||||||
borderBottom: `1px solid ${theme('colors-border')}`,
|
borderBottom: `1px solid ${theme('colors-border')}`,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
})
|
})
|
||||||
|
|
||||||
const ErrorBox = define('ErrorBox', {
|
const ErrorBox = define('ErrorBox', {
|
||||||
|
|
@ -190,6 +193,46 @@ const DownloadButton = define('DownloadButton', {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const EditButton = define('EditButton', {
|
||||||
|
base: 'button',
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '6px',
|
||||||
|
padding: '6px 12px',
|
||||||
|
backgroundColor: theme('colors-bgElement'),
|
||||||
|
color: theme('colors-text'),
|
||||||
|
border: `1px solid ${theme('colors-border')}`,
|
||||||
|
borderRadius: theme('radius-md'),
|
||||||
|
fontSize: '13px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
states: {
|
||||||
|
':hover': {
|
||||||
|
backgroundColor: theme('colors-bgHover'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const EditLink = define('EditLink', {
|
||||||
|
base: 'a',
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '6px',
|
||||||
|
padding: '6px 12px',
|
||||||
|
backgroundColor: theme('colors-bgElement'),
|
||||||
|
color: theme('colors-text'),
|
||||||
|
border: `1px solid ${theme('colors-border')}`,
|
||||||
|
borderRadius: theme('radius-md'),
|
||||||
|
fontSize: '13px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
textDecoration: 'none',
|
||||||
|
states: {
|
||||||
|
':hover': {
|
||||||
|
backgroundColor: theme('colors-bgHover'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
const FolderIcon = () => (
|
const FolderIcon = () => (
|
||||||
<FileIcon viewBox="0 0 24 24">
|
<FileIcon viewBox="0 0 24 24">
|
||||||
<path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z" />
|
<path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z" />
|
||||||
|
|
@ -206,6 +249,7 @@ interface LayoutProps {
|
||||||
title: string
|
title: string
|
||||||
children: Child
|
children: Child
|
||||||
highlight?: boolean
|
highlight?: boolean
|
||||||
|
editable?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileMemoryScript = `
|
const fileMemoryScript = `
|
||||||
|
|
@ -233,7 +277,7 @@ const fileMemoryScript = `
|
||||||
})();
|
})();
|
||||||
`
|
`
|
||||||
|
|
||||||
function Layout({ title, children, highlight }: LayoutProps) {
|
function Layout({ title, children, highlight, editable }: LayoutProps) {
|
||||||
return (
|
return (
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
|
@ -241,13 +285,27 @@ function Layout({ title, children, highlight }: LayoutProps) {
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
<link rel="stylesheet" href="/styles.css" />
|
<link rel="stylesheet" href="/styles.css" />
|
||||||
{highlight && (
|
{highlight && !editable && (
|
||||||
<>
|
<>
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css" media="(prefers-color-scheme: light)" />
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css" media="(prefers-color-scheme: light)" />
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" media="(prefers-color-scheme: dark)" />
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" media="(prefers-color-scheme: dark)" />
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{editable && (
|
||||||
|
<>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css" media="(prefers-color-scheme: light)" />
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" media="(prefers-color-scheme: dark)" />
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-typescript.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-jsx.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-tsx.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-json.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-bash.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-yaml.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-markdown.min.js"></script>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script dangerouslySetInnerHTML={{ __html: fileMemoryScript }} />
|
<script dangerouslySetInnerHTML={{ __html: fileMemoryScript }} />
|
||||||
|
|
@ -255,7 +313,7 @@ function Layout({ title, children, highlight }: LayoutProps) {
|
||||||
<Container>
|
<Container>
|
||||||
{children}
|
{children}
|
||||||
</Container>
|
</Container>
|
||||||
{highlight && <script dangerouslySetInnerHTML={{ __html: 'hljs.highlightAll();' }} />}
|
{highlight && !editable && <script dangerouslySetInnerHTML={{ __html: 'hljs.highlightAll();' }} />}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
)
|
||||||
|
|
@ -284,6 +342,26 @@ app.get('/raw', async c => {
|
||||||
return new Response(file)
|
return new Response(file)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
app.post('/save', async c => {
|
||||||
|
const appName = c.req.query('app')
|
||||||
|
const version = c.req.query('version') || 'current'
|
||||||
|
const filePath = c.req.query('file')
|
||||||
|
|
||||||
|
if (!appName || !filePath) {
|
||||||
|
return c.text('Missing app or file parameter', 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullPath = join(APPS_DIR, appName, version, filePath)
|
||||||
|
const content = await c.req.text()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Bun.write(fullPath, content)
|
||||||
|
return c.text('OK')
|
||||||
|
} catch (err) {
|
||||||
|
return c.text(`Failed to save: ${err}`, 500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
async function listFiles(appPath: string, subPath: string = '') {
|
async function listFiles(appPath: string, subPath: string = '') {
|
||||||
const fullPath = join(appPath, subPath)
|
const fullPath = join(appPath, subPath)
|
||||||
const entries = await readdir(fullPath, { withFileTypes: true })
|
const entries = await readdir(fullPath, { withFileTypes: true })
|
||||||
|
|
@ -373,6 +451,29 @@ function getLanguage(filename: string): string {
|
||||||
return langMap[ext] || 'plaintext'
|
return langMap[ext] || 'plaintext'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPrismLanguage(filename: string): string {
|
||||||
|
const ext = extname(filename).toLowerCase()
|
||||||
|
const langMap: Record<string, string> = {
|
||||||
|
'.js': 'javascript',
|
||||||
|
'.jsx': 'javascript',
|
||||||
|
'.ts': 'typescript',
|
||||||
|
'.tsx': 'typescript',
|
||||||
|
'.json': 'json',
|
||||||
|
'.css': 'css',
|
||||||
|
'.html': 'html',
|
||||||
|
'.md': 'markdown',
|
||||||
|
'.sh': 'bash',
|
||||||
|
'.yml': 'yaml',
|
||||||
|
'.yaml': 'yaml',
|
||||||
|
'.py': 'python',
|
||||||
|
'.rb': 'ruby',
|
||||||
|
'.go': 'go',
|
||||||
|
'.rs': 'rust',
|
||||||
|
'.sql': 'sql',
|
||||||
|
}
|
||||||
|
return langMap[ext] || 'plaintext'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
app.get('/', async c => {
|
app.get('/', async c => {
|
||||||
const appName = c.req.query('app')
|
const appName = c.req.query('app')
|
||||||
|
|
@ -478,12 +579,91 @@ app.get('/', async c => {
|
||||||
// Text file - show with syntax highlighting
|
// Text file - show with syntax highlighting
|
||||||
const content = readFileSync(fullPath, 'utf-8')
|
const content = readFileSync(fullPath, 'utf-8')
|
||||||
const language = getLanguage(filename)
|
const language = getLanguage(filename)
|
||||||
|
const prismLang = getPrismLanguage(filename)
|
||||||
|
const edit = c.req.query('edit') === '1'
|
||||||
|
|
||||||
|
if (edit) {
|
||||||
|
const editorScript = `
|
||||||
|
import { CodeJar } from 'https://cdn.jsdelivr.net/npm/codejar@4.2.0/dist/codejar.js';
|
||||||
|
|
||||||
|
const editor = document.getElementById('editor');
|
||||||
|
const saveBtn = document.getElementById('save-btn');
|
||||||
|
const status = document.getElementById('save-status');
|
||||||
|
let dirty = false;
|
||||||
|
|
||||||
|
const highlight = (el) => {
|
||||||
|
Prism.highlightElement(el);
|
||||||
|
};
|
||||||
|
|
||||||
|
const jar = CodeJar(editor, highlight, { tab: ' ', addClosing: false });
|
||||||
|
|
||||||
|
// Initial highlight
|
||||||
|
highlight(editor);
|
||||||
|
|
||||||
|
jar.onUpdate(() => {
|
||||||
|
if (!dirty) {
|
||||||
|
dirty = true;
|
||||||
|
saveBtn.textContent = 'Save *';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
saveBtn.onclick = async () => {
|
||||||
|
if (!dirty) return;
|
||||||
|
saveBtn.disabled = true;
|
||||||
|
status.textContent = 'Saving...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/save?app=${appName}${versionParam}&file=${filePath}', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'text/plain' },
|
||||||
|
body: jar.toString()
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Save failed');
|
||||||
|
dirty = false;
|
||||||
|
saveBtn.textContent = 'Save';
|
||||||
|
saveBtn.disabled = false;
|
||||||
|
status.textContent = 'Saved!';
|
||||||
|
setTimeout(() => { status.textContent = ''; }, 2000);
|
||||||
|
} catch (err) {
|
||||||
|
saveBtn.disabled = false;
|
||||||
|
status.textContent = 'Error: ' + err.message;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
||||||
|
e.preventDefault();
|
||||||
|
saveBtn.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
`
|
||||||
|
return c.html(
|
||||||
|
<Layout title={`${appName}/${filePath}`} editable>
|
||||||
|
<PathBreadcrumb appName={appName} filePath={filePath} versionParam={versionParam} />
|
||||||
|
<CodeBlock>
|
||||||
|
<CodeHeader>
|
||||||
|
<span>{filename}</span>
|
||||||
|
<div style="display:flex;align-items:center;gap:8px">
|
||||||
|
<span id="save-status" style="font-size:12px;font-weight:normal;color:var(--colors-textMuted)"></span>
|
||||||
|
<EditButton id="save-btn">Save</EditButton>
|
||||||
|
<EditLink href={`/?app=${appName}${versionParam}&file=${filePath}`}>Done</EditLink>
|
||||||
|
</div>
|
||||||
|
</CodeHeader>
|
||||||
|
<pre id="editor" class={`language-${prismLang}`} contenteditable style="margin:0;padding:15px;min-height:300px;outline:none">{content}</pre>
|
||||||
|
</CodeBlock>
|
||||||
|
<script type="module" dangerouslySetInnerHTML={{ __html: editorScript }} />
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return c.html(
|
return c.html(
|
||||||
<Layout title={`${appName}/${filePath}`} highlight>
|
<Layout title={`${appName}/${filePath}`} highlight>
|
||||||
<PathBreadcrumb appName={appName} filePath={filePath} versionParam={versionParam} />
|
<PathBreadcrumb appName={appName} filePath={filePath} versionParam={versionParam} />
|
||||||
<CodeBlock>
|
<CodeBlock>
|
||||||
<CodeHeader>{filename}</CodeHeader>
|
<CodeHeader>
|
||||||
|
<span>{filename}</span>
|
||||||
|
<EditLink href={`/?app=${appName}${versionParam}&file=${filePath}&edit=1`}>Edit</EditLink>
|
||||||
|
</CodeHeader>
|
||||||
<pre><code class={`language-${language}`}>{content}</code></pre>
|
<pre><code class={`language-${language}`}>{content}</code></pre>
|
||||||
</CodeBlock>
|
</CodeBlock>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user