All checks were successful
PR Check / check (pull_request) Successful in 58s
Logo dropped at project root by user. Bake it everywhere it matters:
Assets:
- assets/muyue.ico — multi-res (16/24/32/48/64/128/256) generated via PIL
- assets/muyue-{16,32,64,128,256,512}.png — clean PNG resizes
- LogoMuyue.png kept at root as the source of truth
Windows binary (.exe):
- CI runs `rsrc -ico assets/muyue.ico -arch {amd64,arm64} -o cmd/muyue/rsrc_windows_{amd64,arm64}.syso`
before `go build` (both ci-main.yml and ci-develop.yml)
- Go automatically links *.syso files matching the target GOOS/GOARCH —
no code change in the cmd/muyue main package
- .syso files are gitignored: regenerated at every build, never committed
- Existing install-shortcuts subcommand already uses IconLocation =
"$exe,0" so the embedded icon flows automatically into Desktop +
Start Menu .lnk files
Web UI:
- web/public/favicon-{16,32}.png + muyue.png + muyue-64.png
- web/index.html: real <link rel="icon"> tags (16/32 PNG + apple-touch),
replacing the placeholder SVG hexagon
- App.jsx header: 22×22 logo image rendered next to the "MUYUE" wordmark
(rounded 4px corners for visual consistency with the source logo)
Install snippet (ci-main.yml changelog template):
- Idempotent first line: `New-Item -ItemType Directory -Force -Path $dest`
to handle the case where the user re-runs after a partial install
Versioning unchanged (still v0.7.3 — these additions stay on the same
release branch / PR #9).
170 lines
6.2 KiB
JavaScript
170 lines
6.2 KiB
JavaScript
import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
|
||
import { LayoutDashboard, Sparkles, Terminal, Settings, TestTube2 } 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'
|
||
import Tests from './Tests'
|
||
import OnboardingWizard from './OnboardingWizard'
|
||
|
||
export default function App() {
|
||
const [activeTab, setActiveTab] = useState('dash')
|
||
const [info, setInfo] = useState({})
|
||
const [clock, setClock] = useState(new Date())
|
||
const [isSudo, setIsSudo] = useState(false)
|
||
const [dashRefreshKey, setDashRefreshKey] = useState(0)
|
||
const dashRefreshRef = useRef(null)
|
||
const [config, setConfig] = useState(null)
|
||
const [showOnboarding, setShowOnboarding] = useState(false)
|
||
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: 'tests', label: 'Tests', icon: <TestTube2 size={15} /> },
|
||
{ id: 'config', label: t('tabs.config'), icon: <Settings size={15} /> },
|
||
], [t])
|
||
|
||
useEffect(() => {
|
||
api.getInfo().then(d => { setInfo(d); setIsSudo(!!d.sudo) }).catch(() => {})
|
||
api.getConfig().then(d => {
|
||
setConfig(d)
|
||
const theme = d.profile?.preferences?.theme || 'cyberpunk-red'
|
||
applyTheme(getTheme(theme))
|
||
const hasProfile = d.profile?.name || d.profile?.pseudo
|
||
if (!hasProfile) setShowOnboarding(true)
|
||
}).catch(() => {
|
||
applyTheme(getTheme('cyberpunk-red'))
|
||
setShowOnboarding(true)
|
||
})
|
||
}, [])
|
||
|
||
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: 'tests',
|
||
Digit5: 'config',
|
||
}
|
||
if (map[e.code]) {
|
||
e.preventDefault()
|
||
setActiveTab(map[e.code])
|
||
return
|
||
}
|
||
if (e.ctrlKey && e.code === 'KeyR') {
|
||
e.preventDefault()
|
||
if (dashRefreshRef.current) dashRefreshRef.current()
|
||
}
|
||
}
|
||
window.addEventListener('keydown', onKey)
|
||
return () => window.removeEventListener('keydown', onKey)
|
||
}, [])
|
||
|
||
const switchTab = useCallback((tabId) => setActiveTab(tabId), [])
|
||
|
||
useEffect(() => {
|
||
const handler = () => setActiveTab('shell')
|
||
window.addEventListener('navigate-to-shell', handler)
|
||
return () => window.removeEventListener('navigate-to-shell', handler)
|
||
}, [])
|
||
|
||
const WINDOW_SHORTCUTS = useMemo(() => ({
|
||
dash: [],
|
||
studio: [
|
||
{ keys: layout.keys.enter, desc: t('statusbar.sendMessage') },
|
||
{ keys: `${layout.keys.shift}+${layout.keys.enter}`, desc: t('statusbar.newLine') },
|
||
],
|
||
shell: [
|
||
{ keys: `${layout.keys.ctrl}+${layout.keys.shift}+C`, desc: t('statusbar.copy') },
|
||
{ keys: `${layout.keys.ctrl}+${layout.keys.shift}+V`, desc: t('statusbar.paste') },
|
||
{ keys: `${layout.keys.ctrl}+${layout.keys.shift}+F`, desc: t('statusbar.search') },
|
||
{ keys: `${layout.keys.ctrl}++/${layout.keys.ctrl}+−`, desc: t('statusbar.zoom') },
|
||
{ keys: layout.keys.enter, desc: t('statusbar.runCommand') },
|
||
{ keys: `${layout.keys.up}/${layout.keys.down}`, desc: t('statusbar.commandHistory') },
|
||
],
|
||
tests: [],
|
||
config: [],
|
||
}), [layout, t])
|
||
|
||
return (
|
||
<div className="app-layout">
|
||
<header className="header">
|
||
<div className="header-brand">
|
||
<img src="/muyue-64.png" alt="Muyue" className="header-logo-img" width="22" height="22" style={{ borderRadius: 4, verticalAlign: 'middle' }} />
|
||
<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" />
|
||
|
||
<span className="header-clock">
|
||
{clock.toLocaleTimeString(layout.locale, { hour: '2-digit', minute: '2-digit' })}
|
||
</span>
|
||
</header>
|
||
|
||
<main className="content">
|
||
<div className={activeTab === 'dash' ? '' : 'tab-hidden'}><Dashboard api={api} refreshRef={dashRefreshRef} /></div>
|
||
<div className={activeTab === 'studio' ? '' : 'tab-hidden'}><Studio api={api} /></div>
|
||
<div className={activeTab === 'shell' ? '' : 'tab-hidden'}><Shell api={api} isSudo={isSudo} /></div>
|
||
<div className={activeTab === 'tests' ? '' : 'tab-hidden'}><Tests api={api} /></div>
|
||
<div className={activeTab === 'config' ? '' : 'tab-hidden'}><Config api={api} /></div>
|
||
</main>
|
||
|
||
<footer className="statusbar">
|
||
<div className="statusbar-left">
|
||
{isSudo && <span className="statusbar-sudo">⚡ SUDO</span>}
|
||
{activeTab === 'dash' && (
|
||
<span className="statusbar-shortcut">
|
||
<kbd>{layout.keys.ctrl}+R</kbd> refresh
|
||
</span>
|
||
)}
|
||
<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>
|
||
|
||
{showOnboarding && <OnboardingWizard api={api} onComplete={() => setShowOnboarding(false)} />}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function FooterShortcuts({ shortcuts }) {
|
||
return shortcuts.map((s, i) => (
|
||
<span key={i} className="statusbar-shortcut">
|
||
<kbd>{s.keys}</kbd> {s.desc}
|
||
</span>
|
||
))
|
||
}
|