diff --git a/frontend/src/App.css b/frontend/src/App.css index f949d9c..9ba5835 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -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; - cursor: pointer; +.brand-mark { + width: 38px; + height: 38px; + display: grid; + place-items: center; + border-radius: 8px; + background: #1d4ed8; + color: #ffffff; + font-weight: 700; } -.input-box .btn:hover { - background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); - color: #333333; +.brand h1 { + margin: 0; + font-size: 17px; + line-height: 22px; } -.input-box .input { - border: none; - border-radius: 3px; - outline: none; - height: 30px; - line-height: 30px; - padding: 0 10px; - background-color: rgba(240, 240, 240, 1); - -webkit-font-smoothing: antialiased; +.brand span, +.topbar span { + color: #657386; + font-size: 12px; } -.input-box .input:hover { - border: none; - background-color: rgba(255, 255, 255, 1); +.tabs { + display: grid; + gap: 6px; + margin-top: 18px; } -.input-box .input:focus { - border: none; - background-color: rgba(255, 255, 255, 1); -} \ No newline at end of file +.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; +} + +.tabs button { + justify-content: flex-start; + padding: 0 10px; +} + +button:hover:not(:disabled) { + border-color: #1d4ed8; + color: #1d4ed8; +} + +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; + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a6e56f9..222a7fe 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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('config'); + const [config, setConfig] = useState(defaultConfig()); + const [password, setPassword] = useState(''); + const [token, setToken] = useState(''); + const [remoteSkills, setRemoteSkills] = useState([]); + const [localSkills, setLocalSkills] = useState([]); + 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]); - return ( -
- -
{resultText}
-
- - -
+ 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) { + 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 ( +
+ + +
+
+
+

{pageTitle(activeTab)}

+ {statusSummary(remoteSkills, localSkills)} +
+ +
+ + {(message || error) && ( +
+ {error || message} +
+ )} + + {activeTab === 'config' && ( +
+
+ + +
+ Auth Type +
+ + +
+
+ + {config.gitea.authType === 'token' ? ( + + ) : ( + + )} + + + + +
+
+ + +
+
+ )} + + {activeTab === 'remote' && ( +
+
+ setSearch(event.target.value)} + /> + +
+
+ + + + + + + + + + + + {filteredRemote.map((skill) => ( + + + + + + + + ))} + {filteredRemote.length === 0 && ( + + + + )} + +
NameDescriptionBranchStatusAction
{skill.name}{skill.description || '-'}{skill.defaultBranch || 'main'}{badge(skill.status || (skill.isDownloaded ? 'downloaded' : 'remote'))} +
+ + {skill.isDownloaded && ( + + )} +
+
No remote skills
+
+
+ )} + + {activeTab === 'local' && ( +
+
+ +
+
+ + + + + + + + + + + + + + {localSkills.map((skill) => ( + + + + + + + + + + ))} + {localSkills.length === 0 && ( + + + + )} + +
NamePathCommitStatusCodexClaudeActions
{skill.repo}{skill.localPath}{shortCommit(skill.currentCommit)}{skill.lastError ? {skill.lastError} : ready}{targetBadge(skill, 'codex')}{targetBadge(skill, 'claude')} +
+ + {targetInstalled(skill, 'codex') ? ( + + ) : ( + + )} + {targetInstalled(skill, 'claude') ? ( + + ) : ( + + )} + + + +
+
No local skills
+
+
+ )} +
+
+ ); } -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 {label}; +} + +function targetBadge(skill: SkillState, targetID: 'codex' | 'claude') { + return targetInstalled(skill, targetID) ? installed : not installed; +} + +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; diff --git a/frontend/src/api.ts b/frontend/src/api.ts new file mode 100644 index 0000000..d5b25c6 --- /dev/null +++ b/frontend/src/api.ts @@ -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(), +}; diff --git a/frontend/src/style.css b/frontend/src/style.css index 3940d6c..301f3d5 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -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; + margin: 0; + 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"); + font-family: "Nunito"; + font-style: normal; + font-weight: 400; + src: local(""), url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2"); } -#app { - height: 100vh; - text-align: center; +* { + letter-spacing: 0; } diff --git a/frontend/src/types.ts b/frontend/src/types.ts new file mode 100644 index 0000000..d4c4ce6 --- /dev/null +++ b/frontend/src/types.ts @@ -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}); +} diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts index 43173cf..8505328 100644 --- a/frontend/wailsjs/go/main/App.d.ts +++ b/frontend/wailsjs/go/main/App.d.ts @@ -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; +export function DeleteSkill(arg1:string,arg2:string):Promise; + +export function DownloadSkill(arg1:domain.RemoteSkill):Promise; + +export function InstallSkill(arg1:string,arg2:string,arg3:string):Promise; + +export function ListLocalSkills():Promise>; + +export function ListRemoteSkills():Promise>; + +export function LoadConfig():Promise; + +export function OpenFolder(arg1:string,arg2:string):Promise; + +export function OpenInVSCode(arg1:string,arg2:string):Promise; + +export function RunAutoUpdate():Promise; + +export function SaveConfig(arg1:domain.SaveConfigRequest):Promise; + +export function TestConnection(arg1:domain.SaveConfigRequest):Promise; + +export function UninstallSkill(arg1:string,arg2:string,arg3:string):Promise; + +export function UpdateSkill(arg1:string,arg2:string):Promise; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js index 0ee085c..29c4dc6 100644 --- a/frontend/wailsjs/go/main/App.js +++ b/frontend/wailsjs/go/main/App.js @@ -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); } diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts new file mode 100644 index 0000000..cf47c7b --- /dev/null +++ b/frontend/wailsjs/go/models.ts @@ -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; + + 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"]; + } + } + +} + diff --git a/frontend/wailsjs/runtime/runtime.d.ts b/frontend/wailsjs/runtime/runtime.d.ts index 336fb07..3bbea84 100644 --- a/frontend/wailsjs/runtime/runtime.d.ts +++ b/frontend/wailsjs/runtime/runtime.d.ts @@ -21,8 +21,8 @@ export interface Size { export interface Screen { isCurrent: boolean; isPrimary: boolean; - width: number - height: number + width : number + height : number } // Environment information such as platform, buildtype, ... @@ -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; + // [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; + // [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; + +// [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; + // [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; + +// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext) +// Sets a text on the clipboard +export function ClipboardSetText(text: string): Promise; + +// [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; + +// [CleanupNotifications](https://wails.io/docs/reference/runtime/notification#cleanupnotifications) +// Cleans up notification resources and releases any held connections. +export function CleanupNotifications(): Promise; + +// [IsNotificationAvailable](https://wails.io/docs/reference/runtime/notification#isnotificationavailable) +// Checks if notifications are available on the current platform. +export function IsNotificationAvailable(): Promise; + +// [RequestNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#requestnotificationauthorization) +// Requests notification authorization from the user (macOS only). +export function RequestNotificationAuthorization(): Promise; + +// [CheckNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#checknotificationauthorization) +// Checks the current notification authorization status (macOS only). +export function CheckNotificationAuthorization(): Promise; + +// [SendNotification](https://wails.io/docs/reference/runtime/notification#sendnotification) +// Sends a basic notification with the given options. +export function SendNotification(options: NotificationOptions): Promise; + +// [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; + +// [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; + +// [RemoveNotificationCategory](https://wails.io/docs/reference/runtime/notification#removenotificationcategory) +// Removes a previously registered notification category. +export function RemoveNotificationCategory(categoryId: string): Promise; + +// [RemoveAllPendingNotifications](https://wails.io/docs/reference/runtime/notification#removeallpendingnotifications) +// Removes all pending notifications from the notification center. +export function RemoveAllPendingNotifications(): Promise; + +// [RemovePendingNotification](https://wails.io/docs/reference/runtime/notification#removependingnotification) +// Removes a specific pending notification by its identifier. +export function RemovePendingNotification(identifier: string): Promise; + +// [RemoveAllDeliveredNotifications](https://wails.io/docs/reference/runtime/notification#removealldeliverednotifications) +// Removes all delivered notifications from the notification center. +export function RemoveAllDeliveredNotifications(): Promise; + +// [RemoveDeliveredNotification](https://wails.io/docs/reference/runtime/notification#removedeliverednotification) +// Removes a specific delivered notification by its identifier. +export function RemoveDeliveredNotification(identifier: string): Promise; + +// [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; \ No newline at end of file diff --git a/frontend/wailsjs/runtime/runtime.js b/frontend/wailsjs/runtime/runtime.js index b5ae16d..556621e 100644 --- a/frontend/wailsjs/runtime/runtime.js +++ b/frontend/wailsjs/runtime/runtime.js @@ -37,23 +37,23 @@ 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() { - return window.runtime.EventsOffAll(); + return window.runtime.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); +} \ No newline at end of file