feat: add skill manager interface
This commit is contained in:
@@ -1,59 +1,335 @@
|
||||
#app {
|
||||
height: 100vh;
|
||||
text-align: center;
|
||||
.app-shell {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
grid-template-columns: 240px minmax(0, 1fr);
|
||||
background: #f5f7fa;
|
||||
color: #17202a;
|
||||
}
|
||||
|
||||
#logo {
|
||||
display: block;
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
margin: auto;
|
||||
padding: 10% 0 0;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-origin: content-box;
|
||||
.sidebar {
|
||||
background: #ffffff;
|
||||
border-right: 1px solid #d7dde5;
|
||||
padding: 18px 14px;
|
||||
}
|
||||
|
||||
.result {
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
margin: 1.5rem auto;
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 4px 6px 18px;
|
||||
border-bottom: 1px solid #e6eaf0;
|
||||
}
|
||||
|
||||
.input-box .btn {
|
||||
width: 60px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
border-radius: 3px;
|
||||
border: none;
|
||||
margin: 0 0 0 20px;
|
||||
padding: 0 8px;
|
||||
.brand-mark {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 8px;
|
||||
background: #1d4ed8;
|
||||
color: #ffffff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.brand h1 {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.brand span,
|
||||
.topbar span {
|
||||
color: #657386;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.tabs button,
|
||||
button {
|
||||
min-height: 34px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 7px;
|
||||
border: 1px solid #cfd7e3;
|
||||
border-radius: 6px;
|
||||
background: #ffffff;
|
||||
color: #17202a;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.input-box .btn:hover {
|
||||
background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%);
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.input-box .input {
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
.tabs button {
|
||||
justify-content: flex-start;
|
||||
padding: 0 10px;
|
||||
background-color: rgba(240, 240, 240, 1);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.input-box .input:hover {
|
||||
border: none;
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
button:hover:not(:disabled) {
|
||||
border-color: #1d4ed8;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.input-box .input:focus {
|
||||
border: none;
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.56;
|
||||
}
|
||||
|
||||
button.active,
|
||||
.tabs button.active,
|
||||
.segmented button.active {
|
||||
background: #eaf1ff;
|
||||
border-color: #1d4ed8;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
button.secondary {
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
button.ghost {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
button.danger {
|
||||
border-color: #f2c8c8;
|
||||
color: #b42318;
|
||||
}
|
||||
|
||||
button.danger:hover:not(:disabled) {
|
||||
border-color: #b42318;
|
||||
color: #8a1f16;
|
||||
}
|
||||
|
||||
.icon-only {
|
||||
width: 34px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.workspace {
|
||||
min-width: 0;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
min-height: 58px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.topbar h2 {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.notice {
|
||||
margin-bottom: 12px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #b9dfc5;
|
||||
border-radius: 6px;
|
||||
background: #eefbf2;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.notice.error {
|
||||
border-color: #f1b7b7;
|
||||
background: #fff1f1;
|
||||
color: #9f1c1c;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: #ffffff;
|
||||
border: 1px solid #d7dde5;
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(180px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
label,
|
||||
.field {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
label span,
|
||||
.field > span {
|
||||
color: #4b5b6f;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
min-height: 34px;
|
||||
border: 1px solid #cfd7e3;
|
||||
border-radius: 6px;
|
||||
background: #ffffff;
|
||||
color: #17202a;
|
||||
font: inherit;
|
||||
padding: 0 10px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
border-color: #1d4ed8;
|
||||
box-shadow: 0 0 0 3px rgba(29, 78, 216, 0.12);
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
grid-template-columns: 18px 1fr;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.checkbox input {
|
||||
width: 16px;
|
||||
min-height: 16px;
|
||||
}
|
||||
|
||||
.segmented {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.toolbar.split {
|
||||
justify-content: space-between;
|
||||
margin-top: 0;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.search {
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.table-wrap {
|
||||
overflow: auto;
|
||||
border: 1px solid #e2e7ee;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
min-width: 840px;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
height: 44px;
|
||||
border-bottom: 1px solid #e8edf3;
|
||||
padding: 0 10px;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f8fafc;
|
||||
color: #4b5b6f;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.name-cell {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.path-cell {
|
||||
max-width: 280px;
|
||||
color: #4b5b6f;
|
||||
font-family: Consolas, "Courier New", monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.row-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.row-actions button {
|
||||
min-height: 30px;
|
||||
padding: 0 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 22px;
|
||||
padding: 0 8px;
|
||||
border-radius: 999px;
|
||||
background: #eef2f7;
|
||||
color: #4b5b6f;
|
||||
font-size: 12px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.badge.downloaded,
|
||||
.badge.installed,
|
||||
.badge.local {
|
||||
background: #e8f7ee;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.badge.check_failed,
|
||||
.badge.error {
|
||||
background: #fff1f1;
|
||||
color: #9f1c1c;
|
||||
}
|
||||
|
||||
.empty {
|
||||
height: 72px;
|
||||
text-align: center;
|
||||
color: #657386;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.app-shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid #d7dde5;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,479 @@
|
||||
import {useState} from 'react';
|
||||
import logo from './assets/images/logo-universal.png';
|
||||
import {useEffect, useMemo, useState} from 'react';
|
||||
import {
|
||||
Code2,
|
||||
Download,
|
||||
FolderOpen,
|
||||
Link2,
|
||||
PlugZap,
|
||||
RefreshCw,
|
||||
Save,
|
||||
Settings,
|
||||
Trash2,
|
||||
Unlink,
|
||||
} from 'lucide-react';
|
||||
import './App.css';
|
||||
import {Greet} from "../wailsjs/go/main/App";
|
||||
import {api} from './api';
|
||||
import {Config, RemoteSkill, SkillState, defaultConfig, saveConfigRequest} from './types';
|
||||
import {domain} from '../wailsjs/go/models';
|
||||
|
||||
type Tab = 'config' | 'remote' | 'local';
|
||||
|
||||
function App() {
|
||||
const [resultText, setResultText] = useState("Please enter your name below 👇");
|
||||
const [name, setName] = useState('');
|
||||
const updateName = (e: any) => setName(e.target.value);
|
||||
const updateResultText = (result: string) => setResultText(result);
|
||||
const [activeTab, setActiveTab] = useState<Tab>('config');
|
||||
const [config, setConfig] = useState<Config>(defaultConfig());
|
||||
const [password, setPassword] = useState('');
|
||||
const [token, setToken] = useState('');
|
||||
const [remoteSkills, setRemoteSkills] = useState<RemoteSkill[]>([]);
|
||||
const [localSkills, setLocalSkills] = useState<SkillState[]>([]);
|
||||
const [search, setSearch] = useState('');
|
||||
const [message, setMessage] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [busy, setBusy] = useState('');
|
||||
|
||||
function greet() {
|
||||
Greet(name).then(updateResultText);
|
||||
useEffect(() => {
|
||||
void loadInitialData();
|
||||
}, []);
|
||||
|
||||
const filteredRemote = useMemo(() => {
|
||||
const needle = search.trim().toLowerCase();
|
||||
if (!needle) {
|
||||
return remoteSkills;
|
||||
}
|
||||
return remoteSkills.filter((skill) => {
|
||||
return [skill.name, skill.fullName, skill.description]
|
||||
.filter(Boolean)
|
||||
.some((value) => value.toLowerCase().includes(needle));
|
||||
});
|
||||
}, [remoteSkills, search]);
|
||||
|
||||
async function loadInitialData() {
|
||||
await run('load', async () => {
|
||||
const loaded = await api.loadConfig();
|
||||
setConfig(domain.Config.createFrom(loaded));
|
||||
await refreshLocal();
|
||||
});
|
||||
}
|
||||
|
||||
async function refreshRemote() {
|
||||
await run('remote:refresh', async () => {
|
||||
setRemoteSkills(await api.listRemoteSkills());
|
||||
setMessage('Remote list refreshed');
|
||||
});
|
||||
}
|
||||
|
||||
async function refreshLocal() {
|
||||
setLocalSkills(await api.listLocalSkills());
|
||||
}
|
||||
|
||||
async function saveConfig() {
|
||||
await run('config:save', async () => {
|
||||
await api.saveConfig(saveConfigRequest(config, password, token));
|
||||
setPassword('');
|
||||
setToken('');
|
||||
setMessage('Config saved');
|
||||
});
|
||||
}
|
||||
|
||||
async function testConnection() {
|
||||
await run('config:test', async () => {
|
||||
const result = await api.testConnection(saveConfigRequest(config, password, token));
|
||||
setMessage(result.ok ? `Connected as ${result.username}` : result.message);
|
||||
});
|
||||
}
|
||||
|
||||
async function downloadOrUpdate(skill: RemoteSkill) {
|
||||
await run(`remote:${skill.name}`, async () => {
|
||||
if (skill.isDownloaded) {
|
||||
await api.updateSkill(config.gitea.org, skill.name);
|
||||
setMessage(`${skill.name} updated`);
|
||||
} else {
|
||||
await api.downloadSkill(skill);
|
||||
setMessage(`${skill.name} downloaded`);
|
||||
}
|
||||
await refreshLocal();
|
||||
await refreshRemote();
|
||||
});
|
||||
}
|
||||
|
||||
async function updateLocal(skill: SkillState) {
|
||||
await run(`local:update:${skill.repo}`, async () => {
|
||||
await api.updateSkill(skill.org, skill.repo);
|
||||
await refreshLocal();
|
||||
setMessage(`${skill.repo} updated`);
|
||||
});
|
||||
}
|
||||
|
||||
async function install(skill: SkillState, targetID: 'codex' | 'claude') {
|
||||
await run(`install:${targetID}:${skill.repo}`, async () => {
|
||||
await api.installSkill(skill.org, skill.repo, targetID);
|
||||
await refreshLocal();
|
||||
setMessage(`${skill.repo} installed to ${targetLabel(targetID)}`);
|
||||
});
|
||||
}
|
||||
|
||||
async function uninstall(skill: SkillState, targetID: 'codex' | 'claude') {
|
||||
await run(`uninstall:${targetID}:${skill.repo}`, async () => {
|
||||
await api.uninstallSkill(skill.org, skill.repo, targetID);
|
||||
await refreshLocal();
|
||||
setMessage(`${skill.repo} uninstalled from ${targetLabel(targetID)}`);
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteSkill(skill: SkillState) {
|
||||
if (!window.confirm(`Delete local skill ${skill.repo}?`)) {
|
||||
return;
|
||||
}
|
||||
await run(`delete:${skill.repo}`, async () => {
|
||||
await api.deleteSkill(skill.org, skill.repo);
|
||||
await refreshLocal();
|
||||
setMessage(`${skill.repo} deleted`);
|
||||
});
|
||||
}
|
||||
|
||||
async function openFolder(skill: SkillState) {
|
||||
await run(`folder:${skill.repo}`, async () => {
|
||||
await api.openFolder(skill.org, skill.repo);
|
||||
});
|
||||
}
|
||||
|
||||
async function openRemoteFolder(skill: RemoteSkill) {
|
||||
await run(`remote:folder:${skill.name}`, async () => {
|
||||
await api.openFolder(config.gitea.org, skill.name);
|
||||
});
|
||||
}
|
||||
|
||||
async function openCode(skill: SkillState) {
|
||||
await run(`code:${skill.repo}`, async () => {
|
||||
await api.openInVSCode(skill.org, skill.repo);
|
||||
});
|
||||
}
|
||||
|
||||
async function run(key: string, action: () => Promise<void>) {
|
||||
setBusy(key);
|
||||
setError('');
|
||||
setMessage('');
|
||||
try {
|
||||
await action();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setBusy('');
|
||||
}
|
||||
}
|
||||
|
||||
function updateConfig(mutator: (current: Config) => unknown) {
|
||||
const next = domain.Config.createFrom(config);
|
||||
mutator(next);
|
||||
setConfig(next);
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="App">
|
||||
<img src={logo} id="logo" alt="logo"/>
|
||||
<div id="result" className="result">{resultText}</div>
|
||||
<div id="input" className="input-box">
|
||||
<input id="name" className="input" onChange={updateName} autoComplete="off" name="input" type="text"/>
|
||||
<button className="btn" onClick={greet}>Greet</button>
|
||||
<main className="app-shell">
|
||||
<aside className="sidebar">
|
||||
<div className="brand">
|
||||
<div className="brand-mark">S</div>
|
||||
<div>
|
||||
<h1>Skill Manager</h1>
|
||||
<span>Gitea / Codex / Claude</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
<nav className="tabs" aria-label="Primary">
|
||||
<button className={activeTab === 'config' ? 'active' : ''} onClick={() => setActiveTab('config')}>
|
||||
<Settings size={17} /> Config
|
||||
</button>
|
||||
<button className={activeTab === 'remote' ? 'active' : ''} onClick={() => setActiveTab('remote')}>
|
||||
<Download size={17} /> Remote
|
||||
</button>
|
||||
<button className={activeTab === 'local' ? 'active' : ''} onClick={() => setActiveTab('local')}>
|
||||
<FolderOpen size={17} /> Local
|
||||
</button>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<section className="workspace">
|
||||
<header className="topbar">
|
||||
<div>
|
||||
<h2>{pageTitle(activeTab)}</h2>
|
||||
<span>{statusSummary(remoteSkills, localSkills)}</span>
|
||||
</div>
|
||||
<button className="ghost" onClick={() => run('auto:update', async () => { await api.runAutoUpdate(); await refreshLocal(); })} disabled={busy !== ''}>
|
||||
<RefreshCw size={16} /> Check Updates
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{(message || error) && (
|
||||
<div className={error ? 'notice error' : 'notice'}>
|
||||
{error || message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'config' && (
|
||||
<section className="panel">
|
||||
<div className="form-grid">
|
||||
<label>
|
||||
<span>Gitea Base URL</span>
|
||||
<input
|
||||
value={config.gitea.baseURL || ''}
|
||||
placeholder="https://gitea.example.com"
|
||||
onChange={(event) => updateConfig((next) => { next.gitea.baseURL = event.target.value; })}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>Organization</span>
|
||||
<input
|
||||
value={config.gitea.org || ''}
|
||||
onChange={(event) => updateConfig((next) => { next.gitea.org = event.target.value; })}
|
||||
/>
|
||||
</label>
|
||||
<div className="field">
|
||||
<span>Auth Type</span>
|
||||
<div className="segmented">
|
||||
<button
|
||||
className={config.gitea.authType !== 'token' ? 'active' : ''}
|
||||
onClick={() => updateConfig((next) => { next.gitea.authType = 'password'; })}
|
||||
>
|
||||
Password
|
||||
</button>
|
||||
<button
|
||||
className={config.gitea.authType === 'token' ? 'active' : ''}
|
||||
onClick={() => updateConfig((next) => { next.gitea.authType = 'token'; })}
|
||||
>
|
||||
Token
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<label>
|
||||
<span>Username</span>
|
||||
<input
|
||||
value={config.gitea.username || ''}
|
||||
onChange={(event) => updateConfig((next) => { next.gitea.username = event.target.value; })}
|
||||
/>
|
||||
</label>
|
||||
{config.gitea.authType === 'token' ? (
|
||||
<label>
|
||||
<span>Token</span>
|
||||
<input type="password" value={token} onChange={(event) => setToken(event.target.value)} />
|
||||
</label>
|
||||
) : (
|
||||
<label>
|
||||
<span>Password</span>
|
||||
<input type="password" value={password} onChange={(event) => setPassword(event.target.value)} />
|
||||
</label>
|
||||
)}
|
||||
<label>
|
||||
<span>Credential Key</span>
|
||||
<input
|
||||
value={config.gitea.credentialKey || ''}
|
||||
onChange={(event) => updateConfig((next) => { next.gitea.credentialKey = event.target.value; })}
|
||||
/>
|
||||
</label>
|
||||
<label className="checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(config.update.autoUpdate)}
|
||||
onChange={(event) => updateConfig((next) => { next.update.autoUpdate = event.target.checked; })}
|
||||
/>
|
||||
<span>Auto update</span>
|
||||
</label>
|
||||
<label className="checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(config.update.checkOnStartup)}
|
||||
onChange={(event) => updateConfig((next) => { next.update.checkOnStartup = event.target.checked; })}
|
||||
/>
|
||||
<span>Check on startup</span>
|
||||
</label>
|
||||
<label>
|
||||
<span>Interval Minutes</span>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={config.update.intervalMinutes || 60}
|
||||
onChange={(event) => updateConfig((next) => { next.update.intervalMinutes = Number(event.target.value); })}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="toolbar">
|
||||
<button onClick={saveConfig} disabled={busy !== ''}>
|
||||
<Save size={16} /> Save
|
||||
</button>
|
||||
<button className="secondary" onClick={testConnection} disabled={busy !== ''}>
|
||||
<PlugZap size={16} /> Test
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{activeTab === 'remote' && (
|
||||
<section className="panel">
|
||||
<div className="toolbar split">
|
||||
<input
|
||||
className="search"
|
||||
value={search}
|
||||
placeholder="Search repositories"
|
||||
onChange={(event) => setSearch(event.target.value)}
|
||||
/>
|
||||
<button onClick={refreshRemote} disabled={busy !== ''}>
|
||||
<RefreshCw size={16} /> Refresh
|
||||
</button>
|
||||
</div>
|
||||
<div className="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Branch</th>
|
||||
<th>Status</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredRemote.map((skill) => (
|
||||
<tr key={skill.fullName || skill.name}>
|
||||
<td className="name-cell">{skill.name}</td>
|
||||
<td>{skill.description || '-'}</td>
|
||||
<td>{skill.defaultBranch || 'main'}</td>
|
||||
<td>{badge(skill.status || (skill.isDownloaded ? 'downloaded' : 'remote'))}</td>
|
||||
<td>
|
||||
<div className="row-actions">
|
||||
<button onClick={() => downloadOrUpdate(skill)} disabled={busy !== ''} title={skill.isDownloaded ? 'Update' : 'Download'}>
|
||||
{skill.isDownloaded ? <RefreshCw size={15} /> : <Download size={15} />}
|
||||
{skill.isDownloaded ? 'Update' : 'Download'}
|
||||
</button>
|
||||
{skill.isDownloaded && (
|
||||
<button className="icon-only" onClick={() => openRemoteFolder(skill)} title="Open folder">
|
||||
<FolderOpen size={15} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{filteredRemote.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={5} className="empty">No remote skills</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{activeTab === 'local' && (
|
||||
<section className="panel">
|
||||
<div className="toolbar">
|
||||
<button onClick={refreshLocal} disabled={busy !== ''}>
|
||||
<RefreshCw size={16} /> Refresh
|
||||
</button>
|
||||
</div>
|
||||
<div className="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Path</th>
|
||||
<th>Commit</th>
|
||||
<th>Status</th>
|
||||
<th>Codex</th>
|
||||
<th>Claude</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{localSkills.map((skill) => (
|
||||
<tr key={`${skill.org}/${skill.repo}`}>
|
||||
<td className="name-cell">{skill.repo}</td>
|
||||
<td className="path-cell" title={skill.localPath}>{skill.localPath}</td>
|
||||
<td>{shortCommit(skill.currentCommit)}</td>
|
||||
<td>{skill.lastError ? <span className="badge error">{skill.lastError}</span> : <span className="badge local">ready</span>}</td>
|
||||
<td>{targetBadge(skill, 'codex')}</td>
|
||||
<td>{targetBadge(skill, 'claude')}</td>
|
||||
<td>
|
||||
<div className="row-actions">
|
||||
<button onClick={() => updateLocal(skill)} disabled={busy !== ''} title="Update">
|
||||
<RefreshCw size={15} /> Update
|
||||
</button>
|
||||
{targetInstalled(skill, 'codex') ? (
|
||||
<button className="secondary" onClick={() => uninstall(skill, 'codex')} disabled={busy !== ''} title="Uninstall Codex">
|
||||
<Unlink size={15} /> Codex
|
||||
</button>
|
||||
) : (
|
||||
<button className="secondary" onClick={() => install(skill, 'codex')} disabled={busy !== ''} title="Install Codex">
|
||||
<Link2 size={15} /> Codex
|
||||
</button>
|
||||
)}
|
||||
{targetInstalled(skill, 'claude') ? (
|
||||
<button className="secondary" onClick={() => uninstall(skill, 'claude')} disabled={busy !== ''} title="Uninstall Claude">
|
||||
<Unlink size={15} /> Claude
|
||||
</button>
|
||||
) : (
|
||||
<button className="secondary" onClick={() => install(skill, 'claude')} disabled={busy !== ''} title="Install Claude">
|
||||
<Link2 size={15} /> Claude
|
||||
</button>
|
||||
)}
|
||||
<button className="icon-only" onClick={() => openCode(skill)} disabled={busy !== ''} title="Open VS Code">
|
||||
<Code2 size={15} />
|
||||
</button>
|
||||
<button className="icon-only" onClick={() => openFolder(skill)} disabled={busy !== ''} title="Open folder">
|
||||
<FolderOpen size={15} />
|
||||
</button>
|
||||
<button className="danger icon-only" onClick={() => deleteSkill(skill)} disabled={busy !== ''} title="Delete local">
|
||||
<Trash2 size={15} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{localSkills.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={7} className="empty">No local skills</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default App
|
||||
function pageTitle(tab: Tab) {
|
||||
if (tab === 'remote') {
|
||||
return 'Remote Market';
|
||||
}
|
||||
if (tab === 'local') {
|
||||
return 'Local Skills';
|
||||
}
|
||||
return 'Configuration';
|
||||
}
|
||||
|
||||
function statusSummary(remote: RemoteSkill[], local: SkillState[]) {
|
||||
return `${remote.length} remote / ${local.length} local`;
|
||||
}
|
||||
|
||||
function badge(status: string) {
|
||||
const label = status.replace(/_/g, ' ') || 'remote';
|
||||
return <span className={`badge ${status}`}>{label}</span>;
|
||||
}
|
||||
|
||||
function targetBadge(skill: SkillState, targetID: 'codex' | 'claude') {
|
||||
return targetInstalled(skill, targetID) ? <span className="badge installed">installed</span> : <span className="badge">not installed</span>;
|
||||
}
|
||||
|
||||
function targetInstalled(skill: SkillState, targetID: 'codex' | 'claude') {
|
||||
return Boolean(skill.installedTargets?.[targetID]);
|
||||
}
|
||||
|
||||
function targetLabel(targetID: 'codex' | 'claude') {
|
||||
return targetID === 'codex' ? 'Codex' : 'Claude';
|
||||
}
|
||||
|
||||
function shortCommit(commit: string) {
|
||||
return commit ? commit.slice(0, 8) : '-';
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
32
frontend/src/api.ts
Normal file
32
frontend/src/api.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
DeleteSkill,
|
||||
DownloadSkill,
|
||||
InstallSkill,
|
||||
ListLocalSkills,
|
||||
ListRemoteSkills,
|
||||
LoadConfig,
|
||||
OpenFolder,
|
||||
OpenInVSCode,
|
||||
RunAutoUpdate,
|
||||
SaveConfig,
|
||||
TestConnection,
|
||||
UninstallSkill,
|
||||
UpdateSkill,
|
||||
} from '../wailsjs/go/main/App';
|
||||
import type {RemoteSkill, SaveConfigRequest} from './types';
|
||||
|
||||
export const api = {
|
||||
loadConfig: () => LoadConfig(),
|
||||
saveConfig: (request: SaveConfigRequest) => SaveConfig(request),
|
||||
testConnection: (request: SaveConfigRequest) => TestConnection(request),
|
||||
listRemoteSkills: () => ListRemoteSkills(),
|
||||
listLocalSkills: () => ListLocalSkills(),
|
||||
downloadSkill: (skill: RemoteSkill) => DownloadSkill(skill),
|
||||
updateSkill: (org: string, repo: string) => UpdateSkill(org, repo),
|
||||
installSkill: (org: string, repo: string, targetID: string) => InstallSkill(org, repo, targetID),
|
||||
uninstallSkill: (org: string, repo: string, targetID: string) => UninstallSkill(org, repo, targetID),
|
||||
deleteSkill: (org: string, repo: string) => DeleteSkill(org, repo),
|
||||
openInVSCode: (org: string, repo: string) => OpenInVSCode(org, repo),
|
||||
openFolder: (org: string, repo: string) => OpenFolder(org, repo),
|
||||
runAutoUpdate: () => RunAutoUpdate(),
|
||||
};
|
||||
@@ -1,26 +1,23 @@
|
||||
html {
|
||||
background-color: rgba(27, 38, 54, 1);
|
||||
text-align: center;
|
||||
color: white;
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: white;
|
||||
font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
|
||||
background: #f5f7fa;
|
||||
color: #17202a;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Nunito";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local(""),
|
||||
url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2");
|
||||
src: local(""), url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2");
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100vh;
|
||||
text-align: center;
|
||||
* {
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
28
frontend/src/types.ts
Normal file
28
frontend/src/types.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import {domain} from '../wailsjs/go/models';
|
||||
|
||||
export type Config = domain.Config;
|
||||
export type RemoteSkill = domain.RemoteSkill;
|
||||
export type SkillState = domain.SkillState;
|
||||
export type SaveConfigRequest = domain.SaveConfigRequest;
|
||||
export type TestConnectionResult = domain.TestConnectionResult;
|
||||
|
||||
export function defaultConfig(): Config {
|
||||
return domain.Config.createFrom({
|
||||
gitea: {
|
||||
baseURL: '',
|
||||
org: '',
|
||||
authType: 'password',
|
||||
username: '',
|
||||
credentialKey: 'sgg-ai-skill-manager:gitea',
|
||||
},
|
||||
update: {
|
||||
autoUpdate: true,
|
||||
checkOnStartup: true,
|
||||
intervalMinutes: 60,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function saveConfigRequest(config: Config, password: string, token: string): SaveConfigRequest {
|
||||
return domain.SaveConfigRequest.createFrom({config, password, token});
|
||||
}
|
||||
27
frontend/wailsjs/go/main/App.d.ts
vendored
27
frontend/wailsjs/go/main/App.d.ts
vendored
@@ -1,4 +1,29 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
import {domain} from '../models';
|
||||
|
||||
export function Greet(arg1: string): Promise<string>;
|
||||
export function DeleteSkill(arg1:string,arg2:string):Promise<void>;
|
||||
|
||||
export function DownloadSkill(arg1:domain.RemoteSkill):Promise<domain.SkillState>;
|
||||
|
||||
export function InstallSkill(arg1:string,arg2:string,arg3:string):Promise<domain.SkillState>;
|
||||
|
||||
export function ListLocalSkills():Promise<Array<domain.SkillState>>;
|
||||
|
||||
export function ListRemoteSkills():Promise<Array<domain.RemoteSkill>>;
|
||||
|
||||
export function LoadConfig():Promise<domain.Config>;
|
||||
|
||||
export function OpenFolder(arg1:string,arg2:string):Promise<void>;
|
||||
|
||||
export function OpenInVSCode(arg1:string,arg2:string):Promise<void>;
|
||||
|
||||
export function RunAutoUpdate():Promise<void>;
|
||||
|
||||
export function SaveConfig(arg1:domain.SaveConfigRequest):Promise<void>;
|
||||
|
||||
export function TestConnection(arg1:domain.SaveConfigRequest):Promise<domain.TestConnectionResult>;
|
||||
|
||||
export function UninstallSkill(arg1:string,arg2:string,arg3:string):Promise<domain.SkillState>;
|
||||
|
||||
export function UpdateSkill(arg1:string,arg2:string):Promise<domain.SkillState>;
|
||||
|
||||
@@ -2,6 +2,54 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function Greet(arg1) {
|
||||
return window['go']['main']['App']['Greet'](arg1);
|
||||
export function DeleteSkill(arg1, arg2) {
|
||||
return window['go']['main']['App']['DeleteSkill'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function DownloadSkill(arg1) {
|
||||
return window['go']['main']['App']['DownloadSkill'](arg1);
|
||||
}
|
||||
|
||||
export function InstallSkill(arg1, arg2, arg3) {
|
||||
return window['go']['main']['App']['InstallSkill'](arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
export function ListLocalSkills() {
|
||||
return window['go']['main']['App']['ListLocalSkills']();
|
||||
}
|
||||
|
||||
export function ListRemoteSkills() {
|
||||
return window['go']['main']['App']['ListRemoteSkills']();
|
||||
}
|
||||
|
||||
export function LoadConfig() {
|
||||
return window['go']['main']['App']['LoadConfig']();
|
||||
}
|
||||
|
||||
export function OpenFolder(arg1, arg2) {
|
||||
return window['go']['main']['App']['OpenFolder'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function OpenInVSCode(arg1, arg2) {
|
||||
return window['go']['main']['App']['OpenInVSCode'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function RunAutoUpdate() {
|
||||
return window['go']['main']['App']['RunAutoUpdate']();
|
||||
}
|
||||
|
||||
export function SaveConfig(arg1) {
|
||||
return window['go']['main']['App']['SaveConfig'](arg1);
|
||||
}
|
||||
|
||||
export function TestConnection(arg1) {
|
||||
return window['go']['main']['App']['TestConnection'](arg1);
|
||||
}
|
||||
|
||||
export function UninstallSkill(arg1, arg2, arg3) {
|
||||
return window['go']['main']['App']['UninstallSkill'](arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
export function UpdateSkill(arg1, arg2) {
|
||||
return window['go']['main']['App']['UpdateSkill'](arg1, arg2);
|
||||
}
|
||||
|
||||
218
frontend/wailsjs/go/models.ts
Normal file
218
frontend/wailsjs/go/models.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
export namespace domain {
|
||||
|
||||
export class UpdateConfig {
|
||||
autoUpdate: boolean;
|
||||
checkOnStartup: boolean;
|
||||
intervalMinutes: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new UpdateConfig(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.autoUpdate = source["autoUpdate"];
|
||||
this.checkOnStartup = source["checkOnStartup"];
|
||||
this.intervalMinutes = source["intervalMinutes"];
|
||||
}
|
||||
}
|
||||
export class GiteaConfig {
|
||||
baseURL: string;
|
||||
org: string;
|
||||
authType: string;
|
||||
username: string;
|
||||
credentialKey: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new GiteaConfig(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.baseURL = source["baseURL"];
|
||||
this.org = source["org"];
|
||||
this.authType = source["authType"];
|
||||
this.username = source["username"];
|
||||
this.credentialKey = source["credentialKey"];
|
||||
}
|
||||
}
|
||||
export class Config {
|
||||
gitea: GiteaConfig;
|
||||
update: UpdateConfig;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Config(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.gitea = this.convertValues(source["gitea"], GiteaConfig);
|
||||
this.update = this.convertValues(source["update"], UpdateConfig);
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
|
||||
export class InstalledTarget {
|
||||
path: string;
|
||||
linkType: string;
|
||||
targetPath: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new InstalledTarget(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.path = source["path"];
|
||||
this.linkType = source["linkType"];
|
||||
this.targetPath = source["targetPath"];
|
||||
}
|
||||
}
|
||||
export class RemoteSkill {
|
||||
name: string;
|
||||
fullName: string;
|
||||
description: string;
|
||||
cloneURL: string;
|
||||
sshURL: string;
|
||||
defaultBranch: string;
|
||||
updatedAt: string;
|
||||
isDownloaded: boolean;
|
||||
status: string;
|
||||
error: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new RemoteSkill(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.name = source["name"];
|
||||
this.fullName = source["fullName"];
|
||||
this.description = source["description"];
|
||||
this.cloneURL = source["cloneURL"];
|
||||
this.sshURL = source["sshURL"];
|
||||
this.defaultBranch = source["defaultBranch"];
|
||||
this.updatedAt = source["updatedAt"];
|
||||
this.isDownloaded = source["isDownloaded"];
|
||||
this.status = source["status"];
|
||||
this.error = source["error"];
|
||||
}
|
||||
}
|
||||
export class SaveConfigRequest {
|
||||
config: Config;
|
||||
password: string;
|
||||
token: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new SaveConfigRequest(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.config = this.convertValues(source["config"], Config);
|
||||
this.password = source["password"];
|
||||
this.token = source["token"];
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class SkillState {
|
||||
org: string;
|
||||
repo: string;
|
||||
localPath: string;
|
||||
remoteURL: string;
|
||||
defaultBranch: string;
|
||||
currentCommit: string;
|
||||
lastCheckedAt: string;
|
||||
lastError: string;
|
||||
installedTargets: Record<string, InstalledTarget>;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new SkillState(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.org = source["org"];
|
||||
this.repo = source["repo"];
|
||||
this.localPath = source["localPath"];
|
||||
this.remoteURL = source["remoteURL"];
|
||||
this.defaultBranch = source["defaultBranch"];
|
||||
this.currentCommit = source["currentCommit"];
|
||||
this.lastCheckedAt = source["lastCheckedAt"];
|
||||
this.lastError = source["lastError"];
|
||||
this.installedTargets = this.convertValues(source["installedTargets"], InstalledTarget, true);
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class TestConnectionResult {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
username: string;
|
||||
org: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new TestConnectionResult(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.ok = source["ok"];
|
||||
this.message = source["message"];
|
||||
this.username = source["username"];
|
||||
this.org = source["org"];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
131
frontend/wailsjs/runtime/runtime.d.ts
vendored
131
frontend/wailsjs/runtime/runtime.d.ts
vendored
@@ -38,22 +38,22 @@ export interface EnvironmentInfo {
|
||||
export function EventsEmit(eventName: string, ...data: any): void;
|
||||
|
||||
// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
|
||||
export function EventsOn(eventName: string, callback: (...data: any) => void): void;
|
||||
export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
|
||||
|
||||
// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
|
||||
// sets up a listener for the given event name, but will only trigger a given number times.
|
||||
export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): void;
|
||||
export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
|
||||
|
||||
// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
|
||||
// sets up a listener for the given event name, but will only trigger once.
|
||||
export function EventsOnce(eventName: string, callback: (...data: any) => void): void;
|
||||
export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
|
||||
|
||||
// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsff)
|
||||
// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
|
||||
// unregisters the listener for the given event name.
|
||||
export function EventsOff(eventName: string): void;
|
||||
export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
|
||||
|
||||
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
|
||||
// unregisters all event listeners.
|
||||
// unregisters all listeners.
|
||||
export function EventsOffAll(): void;
|
||||
|
||||
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
|
||||
@@ -128,6 +128,10 @@ export function WindowFullscreen(): void;
|
||||
// Restores the previous window dimensions and position prior to full screen.
|
||||
export function WindowUnfullscreen(): void;
|
||||
|
||||
// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
|
||||
// Returns the state of the window, i.e. whether the window is in full screen mode or not.
|
||||
export function WindowIsFullscreen(): Promise<boolean>;
|
||||
|
||||
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
|
||||
// Sets the width and height of the window.
|
||||
export function WindowSetSize(width: number, height: number): void;
|
||||
@@ -174,6 +178,10 @@ export function WindowToggleMaximise(): void;
|
||||
// Restores the window to the dimensions and position prior to maximising.
|
||||
export function WindowUnmaximise(): void;
|
||||
|
||||
// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
|
||||
// Returns the state of the window, i.e. whether the window is maximised or not.
|
||||
export function WindowIsMaximised(): Promise<boolean>;
|
||||
|
||||
// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
|
||||
// Minimises the window.
|
||||
export function WindowMinimise(): void;
|
||||
@@ -182,6 +190,14 @@ export function WindowMinimise(): void;
|
||||
// Restores the window to the dimensions and position prior to minimising.
|
||||
export function WindowUnminimise(): void;
|
||||
|
||||
// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
|
||||
// Returns the state of the window, i.e. whether the window is minimised or not.
|
||||
export function WindowIsMinimised(): Promise<boolean>;
|
||||
|
||||
// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
|
||||
// Returns the state of the window, i.e. whether the window is normal or not.
|
||||
export function WindowIsNormal(): Promise<boolean>;
|
||||
|
||||
// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
|
||||
// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
|
||||
export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
|
||||
@@ -209,3 +225,106 @@ export function Hide(): void;
|
||||
// [Show](https://wails.io/docs/reference/runtime/intro#show)
|
||||
// Shows the application.
|
||||
export function Show(): void;
|
||||
|
||||
// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
|
||||
// Returns the current text stored on clipboard
|
||||
export function ClipboardGetText(): Promise<string>;
|
||||
|
||||
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
|
||||
// Sets a text on the clipboard
|
||||
export function ClipboardSetText(text: string): Promise<boolean>;
|
||||
|
||||
// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
|
||||
// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||
export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
|
||||
|
||||
// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
|
||||
// OnFileDropOff removes the drag and drop listeners and handlers.
|
||||
export function OnFileDropOff() :void
|
||||
|
||||
// Check if the file path resolver is available
|
||||
export function CanResolveFilePaths(): boolean;
|
||||
|
||||
// Resolves file paths for an array of files
|
||||
export function ResolveFilePaths(files: File[]): void
|
||||
|
||||
// Notification types
|
||||
export interface NotificationOptions {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string; // macOS and Linux only
|
||||
body?: string;
|
||||
categoryId?: string;
|
||||
data?: { [key: string]: any };
|
||||
}
|
||||
|
||||
export interface NotificationAction {
|
||||
id?: string;
|
||||
title?: string;
|
||||
destructive?: boolean; // macOS-specific
|
||||
}
|
||||
|
||||
export interface NotificationCategory {
|
||||
id?: string;
|
||||
actions?: NotificationAction[];
|
||||
hasReplyField?: boolean;
|
||||
replyPlaceholder?: string;
|
||||
replyButtonTitle?: string;
|
||||
}
|
||||
|
||||
// [InitializeNotifications](https://wails.io/docs/reference/runtime/notification#initializenotifications)
|
||||
// Initializes the notification service for the application.
|
||||
// This must be called before sending any notifications.
|
||||
export function InitializeNotifications(): Promise<void>;
|
||||
|
||||
// [CleanupNotifications](https://wails.io/docs/reference/runtime/notification#cleanupnotifications)
|
||||
// Cleans up notification resources and releases any held connections.
|
||||
export function CleanupNotifications(): Promise<void>;
|
||||
|
||||
// [IsNotificationAvailable](https://wails.io/docs/reference/runtime/notification#isnotificationavailable)
|
||||
// Checks if notifications are available on the current platform.
|
||||
export function IsNotificationAvailable(): Promise<boolean>;
|
||||
|
||||
// [RequestNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#requestnotificationauthorization)
|
||||
// Requests notification authorization from the user (macOS only).
|
||||
export function RequestNotificationAuthorization(): Promise<boolean>;
|
||||
|
||||
// [CheckNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#checknotificationauthorization)
|
||||
// Checks the current notification authorization status (macOS only).
|
||||
export function CheckNotificationAuthorization(): Promise<boolean>;
|
||||
|
||||
// [SendNotification](https://wails.io/docs/reference/runtime/notification#sendnotification)
|
||||
// Sends a basic notification with the given options.
|
||||
export function SendNotification(options: NotificationOptions): Promise<void>;
|
||||
|
||||
// [SendNotificationWithActions](https://wails.io/docs/reference/runtime/notification#sendnotificationwithactions)
|
||||
// Sends a notification with action buttons. Requires a registered category.
|
||||
export function SendNotificationWithActions(options: NotificationOptions): Promise<void>;
|
||||
|
||||
// [RegisterNotificationCategory](https://wails.io/docs/reference/runtime/notification#registernotificationcategory)
|
||||
// Registers a notification category that can be used with SendNotificationWithActions.
|
||||
export function RegisterNotificationCategory(category: NotificationCategory): Promise<void>;
|
||||
|
||||
// [RemoveNotificationCategory](https://wails.io/docs/reference/runtime/notification#removenotificationcategory)
|
||||
// Removes a previously registered notification category.
|
||||
export function RemoveNotificationCategory(categoryId: string): Promise<void>;
|
||||
|
||||
// [RemoveAllPendingNotifications](https://wails.io/docs/reference/runtime/notification#removeallpendingnotifications)
|
||||
// Removes all pending notifications from the notification center.
|
||||
export function RemoveAllPendingNotifications(): Promise<void>;
|
||||
|
||||
// [RemovePendingNotification](https://wails.io/docs/reference/runtime/notification#removependingnotification)
|
||||
// Removes a specific pending notification by its identifier.
|
||||
export function RemovePendingNotification(identifier: string): Promise<void>;
|
||||
|
||||
// [RemoveAllDeliveredNotifications](https://wails.io/docs/reference/runtime/notification#removealldeliverednotifications)
|
||||
// Removes all delivered notifications from the notification center.
|
||||
export function RemoveAllDeliveredNotifications(): Promise<void>;
|
||||
|
||||
// [RemoveDeliveredNotification](https://wails.io/docs/reference/runtime/notification#removedeliverednotification)
|
||||
// Removes a specific delivered notification by its identifier.
|
||||
export function RemoveDeliveredNotification(identifier: string): Promise<void>;
|
||||
|
||||
// [RemoveNotification](https://wails.io/docs/reference/runtime/notification#removenotification)
|
||||
// Removes a notification by its identifier (cross-platform convenience function).
|
||||
export function RemoveNotification(identifier: string): Promise<void>;
|
||||
@@ -37,15 +37,15 @@ export function LogFatal(message) {
|
||||
}
|
||||
|
||||
export function EventsOnMultiple(eventName, callback, maxCallbacks) {
|
||||
window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
|
||||
return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
|
||||
}
|
||||
|
||||
export function EventsOn(eventName, callback) {
|
||||
EventsOnMultiple(eventName, callback, -1);
|
||||
return EventsOnMultiple(eventName, callback, -1);
|
||||
}
|
||||
|
||||
export function EventsOff(eventName) {
|
||||
return window.runtime.EventsOff(eventName);
|
||||
export function EventsOff(eventName, ...additionalEventNames) {
|
||||
return window.runtime.EventsOff(eventName, ...additionalEventNames);
|
||||
}
|
||||
|
||||
export function EventsOffAll() {
|
||||
@@ -53,7 +53,7 @@ export function EventsOffAll() {
|
||||
}
|
||||
|
||||
export function EventsOnce(eventName, callback) {
|
||||
EventsOnMultiple(eventName, callback, 1);
|
||||
return EventsOnMultiple(eventName, callback, 1);
|
||||
}
|
||||
|
||||
export function EventsEmit(eventName) {
|
||||
@@ -101,6 +101,10 @@ export function WindowUnfullscreen() {
|
||||
window.runtime.WindowUnfullscreen();
|
||||
}
|
||||
|
||||
export function WindowIsFullscreen() {
|
||||
return window.runtime.WindowIsFullscreen();
|
||||
}
|
||||
|
||||
export function WindowGetSize() {
|
||||
return window.runtime.WindowGetSize();
|
||||
}
|
||||
@@ -145,6 +149,10 @@ export function WindowUnmaximise() {
|
||||
window.runtime.WindowUnmaximise();
|
||||
}
|
||||
|
||||
export function WindowIsMaximised() {
|
||||
return window.runtime.WindowIsMaximised();
|
||||
}
|
||||
|
||||
export function WindowMinimise() {
|
||||
window.runtime.WindowMinimise();
|
||||
}
|
||||
@@ -161,6 +169,14 @@ export function ScreenGetAll() {
|
||||
return window.runtime.ScreenGetAll();
|
||||
}
|
||||
|
||||
export function WindowIsMinimised() {
|
||||
return window.runtime.WindowIsMinimised();
|
||||
}
|
||||
|
||||
export function WindowIsNormal() {
|
||||
return window.runtime.WindowIsNormal();
|
||||
}
|
||||
|
||||
export function BrowserOpenURL(url) {
|
||||
window.runtime.BrowserOpenURL(url);
|
||||
}
|
||||
@@ -180,3 +196,103 @@ export function Hide() {
|
||||
export function Show() {
|
||||
window.runtime.Show();
|
||||
}
|
||||
|
||||
export function ClipboardGetText() {
|
||||
return window.runtime.ClipboardGetText();
|
||||
}
|
||||
|
||||
export function ClipboardSetText(text) {
|
||||
return window.runtime.ClipboardSetText(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||
*
|
||||
* @export
|
||||
* @callback OnFileDropCallback
|
||||
* @param {number} x - x coordinate of the drop
|
||||
* @param {number} y - y coordinate of the drop
|
||||
* @param {string[]} paths - A list of file paths.
|
||||
*/
|
||||
|
||||
/**
|
||||
* OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||
*
|
||||
* @export
|
||||
* @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||
* @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
|
||||
*/
|
||||
export function OnFileDrop(callback, useDropTarget) {
|
||||
return window.runtime.OnFileDrop(callback, useDropTarget);
|
||||
}
|
||||
|
||||
/**
|
||||
* OnFileDropOff removes the drag and drop listeners and handlers.
|
||||
*/
|
||||
export function OnFileDropOff() {
|
||||
return window.runtime.OnFileDropOff();
|
||||
}
|
||||
|
||||
export function CanResolveFilePaths() {
|
||||
return window.runtime.CanResolveFilePaths();
|
||||
}
|
||||
|
||||
export function ResolveFilePaths(files) {
|
||||
return window.runtime.ResolveFilePaths(files);
|
||||
}
|
||||
|
||||
export function InitializeNotifications() {
|
||||
return window.runtime.InitializeNotifications();
|
||||
}
|
||||
|
||||
export function CleanupNotifications() {
|
||||
return window.runtime.CleanupNotifications();
|
||||
}
|
||||
|
||||
export function IsNotificationAvailable() {
|
||||
return window.runtime.IsNotificationAvailable();
|
||||
}
|
||||
|
||||
export function RequestNotificationAuthorization() {
|
||||
return window.runtime.RequestNotificationAuthorization();
|
||||
}
|
||||
|
||||
export function CheckNotificationAuthorization() {
|
||||
return window.runtime.CheckNotificationAuthorization();
|
||||
}
|
||||
|
||||
export function SendNotification(options) {
|
||||
return window.runtime.SendNotification(options);
|
||||
}
|
||||
|
||||
export function SendNotificationWithActions(options) {
|
||||
return window.runtime.SendNotificationWithActions(options);
|
||||
}
|
||||
|
||||
export function RegisterNotificationCategory(category) {
|
||||
return window.runtime.RegisterNotificationCategory(category);
|
||||
}
|
||||
|
||||
export function RemoveNotificationCategory(categoryId) {
|
||||
return window.runtime.RemoveNotificationCategory(categoryId);
|
||||
}
|
||||
|
||||
export function RemoveAllPendingNotifications() {
|
||||
return window.runtime.RemoveAllPendingNotifications();
|
||||
}
|
||||
|
||||
export function RemovePendingNotification(identifier) {
|
||||
return window.runtime.RemovePendingNotification(identifier);
|
||||
}
|
||||
|
||||
export function RemoveAllDeliveredNotifications() {
|
||||
return window.runtime.RemoveAllDeliveredNotifications();
|
||||
}
|
||||
|
||||
export function RemoveDeliveredNotification(identifier) {
|
||||
return window.runtime.RemoveDeliveredNotification(identifier);
|
||||
}
|
||||
|
||||
export function RemoveNotification(identifier) {
|
||||
return window.runtime.RemoveNotification(identifier);
|
||||
}
|
||||
Reference in New Issue
Block a user