All checks were successful
Beta Release / beta (push) Successful in 39s
- Terminal: multi-tab sessions, SSH connections, shell detection (zsh/bash/fish/wsl/powershell) - Config: inline profile & provider editing, system update management - Dashboard: grid layout with inline tools/notifications/workflows sections - Add lucide-react icons, i18n keys (FR/EN), and new CSS components 💾 Generated with Crush Assisted-by: GLM-5-Turbo via Crush <crush@charm.land>
157 lines
5.3 KiB
JavaScript
157 lines
5.3 KiB
JavaScript
import { useState, useEffect, useCallback, useMemo } from 'react'
|
|
import { LayoutDashboard, Sparkles, Terminal, Settings } from 'lucide-react'
|
|
import api from '../api/client'
|
|
import { getTheme, applyTheme } from '../themes'
|
|
import { useI18n } from '../i18n'
|
|
import Dashboard from './Dashboard'
|
|
import Studio from './Studio'
|
|
import Shell from './Shell'
|
|
import Config from './Config'
|
|
|
|
export default function App() {
|
|
const [activeTab, setActiveTab] = useState('dash')
|
|
const [info, setInfo] = useState({})
|
|
const [clock, setClock] = useState(new Date())
|
|
const [updates, setUpdates] = useState([])
|
|
const [tools, setTools] = useState([])
|
|
const { t, layout } = useI18n()
|
|
|
|
const TABS = useMemo(() => [
|
|
{ id: 'dash', label: t('tabs.dashboard'), icon: <LayoutDashboard size={15} /> },
|
|
{ id: 'studio', label: t('tabs.studio'), icon: <Sparkles size={15} /> },
|
|
{ id: 'shell', label: t('tabs.shell'), icon: <Terminal size={15} /> },
|
|
{ id: 'config', label: t('tabs.config'), icon: <Settings size={15} /> },
|
|
], [t])
|
|
|
|
useEffect(() => {
|
|
api.getInfo().then(setInfo).catch(() => {})
|
|
api.getTools().then(d => setTools(d.tools || [])).catch(() => {})
|
|
api.getUpdates().then(d => setUpdates(d.updates || [])).catch(() => {})
|
|
applyTheme(getTheme('cyberpunk-red'))
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
const id = setInterval(() => setClock(new Date()), 1000)
|
|
return () => clearInterval(id)
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
const onKey = (e) => {
|
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return
|
|
if (!e.ctrlKey && !e.metaKey) return
|
|
const map = {
|
|
Digit1: 'dash',
|
|
Digit2: 'studio',
|
|
Digit3: 'shell',
|
|
Digit4: 'config',
|
|
}
|
|
if (map[e.code]) {
|
|
e.preventDefault()
|
|
setActiveTab(map[e.code])
|
|
}
|
|
}
|
|
window.addEventListener('keydown', onKey)
|
|
return () => window.removeEventListener('keydown', onKey)
|
|
}, [])
|
|
|
|
const switchTab = useCallback((tabId) => setActiveTab(tabId), [])
|
|
|
|
const hasUpdates = updates.some(u => u.needsUpdate)
|
|
const installed = tools.filter(t => t.installed).length
|
|
|
|
const WINDOW_SHORTCUTS = useMemo(() => ({
|
|
dash: [
|
|
{ keys: `${layout.keys.ctrl}+${layout.keys.range}`, desc: t('statusbar.switchWindow') },
|
|
],
|
|
studio: [
|
|
{ keys: layout.keys.enter, desc: t('statusbar.sendMessage') },
|
|
{ keys: `${layout.keys.shift}+${layout.keys.enter}`, desc: t('statusbar.newLine') },
|
|
{ keys: `${layout.keys.ctrl}+${layout.keys.range}`, desc: t('statusbar.switchWindow') },
|
|
],
|
|
shell: [
|
|
{ keys: layout.keys.enter, desc: t('statusbar.runCommand') },
|
|
{ keys: `${layout.keys.up}/${layout.keys.down}`, desc: t('statusbar.commandHistory') },
|
|
{ keys: `${layout.keys.ctrl}+${layout.keys.range}`, desc: t('statusbar.switchWindow') },
|
|
],
|
|
config: [
|
|
{ keys: `${layout.keys.ctrl}+${layout.keys.range}`, desc: t('statusbar.switchWindow') },
|
|
],
|
|
}), [layout, t])
|
|
|
|
const renderContent = () => {
|
|
switch (activeTab) {
|
|
case 'dash': return <Dashboard tools={tools} updates={updates} api={api} onRescan={t => setTools(t)} />
|
|
case 'studio': return <Studio api={api} />
|
|
case 'shell': return <Shell api={api} />
|
|
case 'config': return <Config api={api} onThemeChange={() => {}} />
|
|
default: return null
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="app-layout">
|
|
<header className="header">
|
|
<div className="header-brand">
|
|
<span className="header-logo">MUYUE</span>
|
|
<span className="header-version">v{info.version || '...'}</span>
|
|
</div>
|
|
|
|
<nav className="header-nav">
|
|
{TABS.map(tab => (
|
|
<div
|
|
key={tab.id}
|
|
className={`nav-tab ${activeTab === tab.id ? 'active' : ''}`}
|
|
onClick={() => switchTab(tab.id)}
|
|
role="tab"
|
|
aria-selected={activeTab === tab.id}
|
|
>
|
|
<span className="tab-icon">{tab.icon}</span>
|
|
{tab.label}
|
|
</div>
|
|
))}
|
|
</nav>
|
|
|
|
<div className="header-spacer" />
|
|
|
|
<div className="header-indicators">
|
|
<span
|
|
className={`indicator ${installed > 0 ? 'ok' : 'off'}`}
|
|
title={t('header.toolsInstalled', { count: installed })}
|
|
/>
|
|
<span
|
|
className={`indicator ${hasUpdates ? 'warn' : 'ok'}`}
|
|
title={hasUpdates ? t('header.updatesAvailable') : t('header.upToDate')}
|
|
/>
|
|
</div>
|
|
|
|
<span className="header-clock">
|
|
{clock.toLocaleTimeString(layout.locale, { hour: '2-digit', minute: '2-digit' })}
|
|
</span>
|
|
</header>
|
|
|
|
<main className="content fade-in" key={`${activeTab}-${TABS.length}`}>
|
|
{renderContent()}
|
|
</main>
|
|
|
|
<footer className="statusbar">
|
|
<div className="statusbar-left">
|
|
<FooterShortcuts shortcuts={WINDOW_SHORTCUTS[activeTab] || []} />
|
|
</div>
|
|
<div className="statusbar-right">
|
|
<span style={{ fontFamily: 'var(--font-mono)' }}>
|
|
{layout.keys.ctrl}+{layout.keys.range} {t('statusbar.switchWindow')}
|
|
</span>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function FooterShortcuts({ shortcuts }) {
|
|
return shortcuts.map((s, i) => (
|
|
<span key={i} className="statusbar-shortcut">
|
|
<kbd>{s.keys}</kbd> {s.desc}
|
|
</span>
|
|
))
|
|
}
|